@deneb-ui/cli 2.0.85 → 2.0.86

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,581 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const recast = require('recast');
6
+ const {
7
+ parseSource,
8
+ locKey,
9
+ getJsxName,
10
+ findJsxAttribute,
11
+ hasJsxAttribute,
12
+ unwrapExpr,
13
+ } = require('./ast.cjs');
14
+ const { rel, toPosix, isJsxFile, isSourceFile } = require('./fs-utils.cjs');
15
+ const { resolveImportSpecifier } = require('./scanner.cjs');
16
+ const { unrollCollectionSource, collectVariableAliases } = require('./data-flow.cjs');
17
+
18
+ /**
19
+ * Deneb ARC v2 — Project-Wide Intermediate Representation (IR) Builder
20
+ *
21
+ * Scans the entire project upfront to construct an application-level graph:
22
+ * 1. Component Graph: Component declarations, export names, props shapes, interfaces, and member usages.
23
+ * 2. Data Sources: Array declarations (inline or imported) and their sample object shapes.
24
+ * 3. Collection Flow: Maps .map() iteration loops to their data source and child components.
25
+ * 4. Asset Imports: Next.js static asset imports mapped to clean public URL paths.
26
+ * 5. CSS Background Images: Discovers Tailwind arbitrary bg-[url(...)] and inline styles.
27
+ */
28
+
29
+ function unwrapTypeCasts(node) {
30
+ let curr = node;
31
+ while (
32
+ curr &&
33
+ (curr.type === 'TSAsExpression' ||
34
+ curr.type === 'TSTypeAssertion' ||
35
+ curr.type === 'TypeCastExpression' ||
36
+ curr.type === 'TSNonNullExpression')
37
+ ) {
38
+ curr = curr.expression;
39
+ }
40
+ return curr;
41
+ }
42
+
43
+ function objectLiteralToPlain(node) {
44
+ if (!node || node.type !== 'ObjectExpression') return null;
45
+ const out = {};
46
+ for (const prop of node.properties || []) {
47
+ if (prop.type !== 'ObjectProperty' && prop.type !== 'Property') continue;
48
+ const key = prop.key && (prop.key.name || prop.key.value);
49
+ if (!key || prop.computed) continue;
50
+ const val = unwrapTypeCasts(prop.value);
51
+ if (!val) continue;
52
+
53
+ if (val.type === 'StringLiteral' || (val.type === 'Literal' && typeof val.value === 'string')) {
54
+ out[key] = String(val.value);
55
+ } else if (val.type === 'NumericLiteral' || (val.type === 'Literal' && typeof val.value === 'number')) {
56
+ out[key] = val.value;
57
+ } else if (val.type === 'BooleanLiteral' || (val.type === 'Literal' && typeof val.value === 'boolean')) {
58
+ out[key] = val.value;
59
+ } else if (val.type === 'ObjectExpression') {
60
+ out[key] = objectLiteralToPlain(val);
61
+ } else if (val.type === 'ArrayExpression') {
62
+ out[key] = (val.elements || [])
63
+ .map((el) => {
64
+ const item = unwrapTypeCasts(el);
65
+ if (item?.type === 'StringLiteral' || (item?.type === 'Literal' && typeof item.value === 'string')) {
66
+ return String(item.value);
67
+ }
68
+ if (item?.type === 'ObjectExpression') {
69
+ return objectLiteralToPlain(item);
70
+ }
71
+ return null;
72
+ })
73
+ .filter(Boolean);
74
+ }
75
+ }
76
+ return out;
77
+ }
78
+
79
+ /**
80
+ * Extracts public asset path if an import resolves inside the public/ folder.
81
+ */
82
+ function resolvePublicAssetUrl(profile, fromFileAbs, specifier) {
83
+ if (!specifier || !profile?.root) return null;
84
+ const publicDir = path.join(profile.root, 'public');
85
+ if (!fs.existsSync(publicDir)) return null;
86
+
87
+ let resolved = null;
88
+ if (specifier.startsWith('.')) {
89
+ resolved = path.resolve(path.dirname(fromFileAbs), specifier);
90
+ } else {
91
+ for (const [alias, target] of Object.entries(profile.aliasMap || {})) {
92
+ const aliasPrefix = alias.replace(/\*$/, '');
93
+ const targetPrefix = String(target).replace(/\*$/, '');
94
+ if (alias.includes('*') && specifier.startsWith(aliasPrefix)) {
95
+ resolved = path.join(targetPrefix, specifier.slice(aliasPrefix.length));
96
+ break;
97
+ } else if (specifier === alias || specifier.startsWith(alias + '/')) {
98
+ resolved = path.join(targetPrefix, specifier.slice(alias.length));
99
+ break;
100
+ }
101
+ }
102
+ }
103
+
104
+ if (resolved) {
105
+ const ext = path.extname(resolved).toLowerCase();
106
+ const isImage = ['.png', '.jpg', '.jpeg', '.svg', '.webp', '.avif', '.gif', '.ico'].includes(ext);
107
+ if (isImage && fs.existsSync(resolved)) {
108
+ const relToPublic = path.relative(publicDir, resolved);
109
+ if (!relToPublic.startsWith('..') && !path.isAbsolute(relToPublic)) {
110
+ return '/' + relToPublic.replace(/\\/g, '/');
111
+ }
112
+ }
113
+ }
114
+ return null;
115
+ }
116
+
117
+ /**
118
+ * Resolves local JSON or JS/TS mock data files from import specifiers.
119
+ */
120
+ function resolveDataImport(profile, fromFileAbs, specifier) {
121
+ if (!specifier || !profile?.root) return null;
122
+ if (specifier.startsWith('.')) {
123
+ const direct = path.resolve(path.dirname(fromFileAbs), specifier);
124
+ if (fs.existsSync(direct) && fs.statSync(direct).isFile()) return direct;
125
+ for (const ext of ['.json', '.ts', '.js', '.tsx', '.jsx']) {
126
+ if (fs.existsSync(direct + ext)) return direct + ext;
127
+ }
128
+ } else {
129
+ for (const [alias, target] of Object.entries(profile.aliasMap || {})) {
130
+ const aliasPrefix = alias.replace(/\*$/, '');
131
+ const targetPrefix = String(target).replace(/\*$/, '');
132
+ if (alias.includes('*') && specifier.startsWith(aliasPrefix)) {
133
+ const remainder = specifier.slice(aliasPrefix.length);
134
+ const candidate = path.join(targetPrefix, remainder);
135
+ if (fs.existsSync(candidate) && fs.statSync(candidate).isFile()) return candidate;
136
+ for (const ext of ['.json', '.ts', '.js', '.tsx', '.jsx']) {
137
+ if (fs.existsSync(candidate + ext)) return candidate + ext;
138
+ }
139
+ } else if (specifier === alias || specifier.startsWith(alias + '/')) {
140
+ const remainder = specifier.slice(alias.length);
141
+ const candidate = path.join(targetPrefix, remainder);
142
+ if (fs.existsSync(candidate) && fs.statSync(candidate).isFile()) return candidate;
143
+ for (const ext of ['.json', '.ts', '.js', '.tsx', '.jsx']) {
144
+ if (fs.existsSync(candidate + ext)) return candidate + ext;
145
+ }
146
+ }
147
+ }
148
+ }
149
+ return null;
150
+ }
151
+
152
+ /**
153
+ * Collects member accesses on a parameter identifier across a function body.
154
+ * e.g. for parameter 'product', collects ['name', 'price', 'image'] from product.name, product.price
155
+ */
156
+ function collectParamMemberAccesses(fnBodyNode, paramName) {
157
+ const members = new Set();
158
+ if (!fnBodyNode || !paramName) return members;
159
+
160
+ recast.types.visit(fnBodyNode, {
161
+ visitMemberExpression(pathNode) {
162
+ const node = pathNode.node;
163
+ if (
164
+ node.object &&
165
+ (node.object.type === 'Identifier' || node.object.type === 'JSXIdentifier') &&
166
+ node.object.name === paramName &&
167
+ node.property &&
168
+ !node.computed
169
+ ) {
170
+ members.add(node.property.name || node.property.value);
171
+ }
172
+ this.traverse(pathNode);
173
+ },
174
+ });
175
+ return members;
176
+ }
177
+
178
+ /**
179
+ * Inspects a function's first parameter (the props argument) to extract its pattern.
180
+ */
181
+ function extractPropsSignature(fnNode) {
182
+ const params = fnNode.params || [];
183
+ if (!params.length) return { kind: 'none', names: [], paramName: null, typeAnnotationName: null };
184
+
185
+ const firstParam = params[0];
186
+ let typeAnnotationName = null;
187
+ if (firstParam.typeAnnotation && firstParam.typeAnnotation.typeAnnotation) {
188
+ const typeNode = firstParam.typeAnnotation.typeAnnotation;
189
+ if (typeNode.type === 'TSTypeReference' && typeNode.typeName) {
190
+ typeAnnotationName = typeNode.typeName.name || null;
191
+ }
192
+ }
193
+
194
+ if (firstParam.type === 'ObjectPattern') {
195
+ const names = [];
196
+ for (const prop of firstParam.properties || []) {
197
+ if (prop.type === 'ObjectProperty' || prop.type === 'Property') {
198
+ const name = prop.key && (prop.key.name || prop.key.value);
199
+ if (name) names.push(name);
200
+ }
201
+ }
202
+ return { kind: 'destructured', names, paramName: null, typeAnnotationName };
203
+ }
204
+
205
+ if (firstParam.type === 'Identifier') {
206
+ return { kind: 'identifier', names: [], paramName: firstParam.name, typeAnnotationName };
207
+ }
208
+
209
+ return { kind: 'unknown', names: [], paramName: null, typeAnnotationName };
210
+ }
211
+
212
+ /**
213
+ * Builds the complete project-wide intermediate representation.
214
+ */
215
+ function buildProjectIR(profile) {
216
+ const projectDir = profile.root;
217
+ const components = new Map(); // componentName -> component metadata
218
+ const dataSources = new Map(); // arrayName -> array data
219
+ const collections = []; // discovered collection loops
220
+ const assetImports = new Map(); // file -> Map of identifier -> public URL
221
+ const backgroundImages = []; // Tailwind or inline background image usages
222
+
223
+ // Pass 1: Parse and harvest components, arrays, and asset imports from every file
224
+ for (const relativeFile of profile.jsxFiles || []) {
225
+ const absPath = path.join(projectDir, relativeFile);
226
+ let code = '';
227
+ try {
228
+ code = fs.readFileSync(absPath, 'utf8');
229
+ } catch {
230
+ continue;
231
+ }
232
+
233
+ let ast;
234
+ try {
235
+ ast = parseSource(code, relativeFile);
236
+ } catch {
237
+ continue;
238
+ }
239
+
240
+ const fileAssetImports = new Map();
241
+ const fileImports = new Map(); // localIdentifier -> specifier
242
+
243
+ // Inspect imports for Next.js static asset images and local dependencies
244
+ recast.types.visit(ast, {
245
+ visitImportDeclaration(pathNode) {
246
+ const specifier = pathNode.node.source?.value;
247
+ if (!specifier) return false;
248
+
249
+ for (const spec of pathNode.node.specifiers || []) {
250
+ const localName = spec.local?.name;
251
+ if (!localName) continue;
252
+ fileImports.set(localName, specifier);
253
+
254
+ const publicAssetUrl = resolvePublicAssetUrl(profile, absPath, specifier);
255
+ if (publicAssetUrl) {
256
+ fileAssetImports.set(localName, publicAssetUrl);
257
+ }
258
+
259
+ // Check if import resolves to local JSON or JS/TS mock data file
260
+ const dataFile = resolveDataImport(profile, absPath, specifier);
261
+ if (dataFile && fs.existsSync(dataFile)) {
262
+ const ext = path.extname(dataFile).toLowerCase();
263
+ if (ext === '.json') {
264
+ try {
265
+ const raw = JSON.parse(fs.readFileSync(dataFile, 'utf8'));
266
+ if (Array.isArray(raw) && raw.length > 0) {
267
+ dataSources.set(localName, {
268
+ name: localName,
269
+ file: rel(projectDir, dataFile),
270
+ kind: 'imported-json',
271
+ items: raw,
272
+ fieldKeys: typeof raw[0] === 'object' && raw[0] !== null ? Object.keys(raw[0]) : [],
273
+ });
274
+ } else if (raw && typeof raw === 'object') {
275
+ for (const [key, val] of Object.entries(raw)) {
276
+ if (Array.isArray(val) && val.length > 0) {
277
+ dataSources.set(key, {
278
+ name: key,
279
+ file: rel(projectDir, dataFile),
280
+ kind: 'imported-json',
281
+ items: val,
282
+ fieldKeys: typeof val[0] === 'object' && val[0] !== null ? Object.keys(val[0]) : [],
283
+ });
284
+ }
285
+ }
286
+ }
287
+ } catch {
288
+ // ignore corrupt json
289
+ }
290
+ }
291
+ }
292
+ }
293
+ return false;
294
+ },
295
+ visitVariableDeclarator(pathNode) {
296
+ const node = pathNode.node;
297
+ if (!node.id || node.id.type !== 'Identifier' || !node.init) return false;
298
+ const init = unwrapTypeCasts(node.init);
299
+
300
+ // Inline array declaration (mock data)
301
+ if (init.type === 'ArrayExpression') {
302
+ const items = [];
303
+ for (const el of init.elements || []) {
304
+ const inner = unwrapTypeCasts(el);
305
+ if (inner?.type === 'ObjectExpression') {
306
+ const plain = objectLiteralToPlain(inner);
307
+ if (plain && Object.keys(plain).length > 0) items.push(plain);
308
+ } else if (inner?.type === 'StringLiteral' || (inner?.type === 'Literal' && typeof inner.value === 'string')) {
309
+ items.push(String(inner.value));
310
+ }
311
+ }
312
+ if (items.length > 0) {
313
+ dataSources.set(node.id.name, {
314
+ name: node.id.name,
315
+ file: relativeFile,
316
+ kind: 'inline-array',
317
+ items,
318
+ fieldKeys: typeof items[0] === 'object' ? Object.keys(items[0]) : [],
319
+ });
320
+ }
321
+ }
322
+ this.traverse(pathNode);
323
+ },
324
+ });
325
+
326
+ if (fileAssetImports.size > 0) {
327
+ assetImports.set(relativeFile, fileAssetImports);
328
+ }
329
+
330
+ // Inspect component declarations
331
+ recast.types.visit(ast, {
332
+ visitFunctionDeclaration(pathNode) {
333
+ registerComponentFromNode(pathNode.node, pathNode.node.id?.name, 'function', relativeFile, ast);
334
+ this.traverse(pathNode);
335
+ },
336
+ visitVariableDeclarator(pathNode) {
337
+ const node = pathNode.node;
338
+ if (
339
+ node.id?.type === 'Identifier' &&
340
+ /^[A-Z]/.test(node.id.name) &&
341
+ node.init &&
342
+ (node.init.type === 'ArrowFunctionExpression' || node.init.type === 'FunctionExpression')
343
+ ) {
344
+ registerComponentFromNode(node.init, node.id.name, 'arrow', relativeFile, ast);
345
+ }
346
+ this.traverse(pathNode);
347
+ },
348
+ visitExportDefaultDeclaration(pathNode) {
349
+ const decl = pathNode.node.declaration;
350
+ if (decl && (decl.type === 'FunctionDeclaration' || decl.type === 'ArrowFunctionExpression' || decl.type === 'FunctionExpression')) {
351
+ const name = decl.id?.name || inferComponentNameFromFile(relativeFile);
352
+ registerComponentFromNode(decl, name, 'default-export', relativeFile, ast, true);
353
+ }
354
+ this.traverse(pathNode);
355
+ },
356
+ // Check for Tailwind arbitrary background images: className="... bg-[url('/img.jpg')] ..."
357
+ visitJSXAttribute(pathNode) {
358
+ const attr = pathNode.node;
359
+ if (attr.name?.name === 'className' || attr.name?.name === 'class') {
360
+ const classVal = attr.value?.value || (attr.value?.expression?.value);
361
+ if (typeof classVal === 'string') {
362
+ const bgMatch = classVal.match(/\bbg-\[url\(['"]?([^'"\)]+)['"]?\)\]/);
363
+ if (bgMatch && bgMatch[1]) {
364
+ backgroundImages.push({
365
+ file: relativeFile,
366
+ loc: locKey(pathNode.node),
367
+ url: bgMatch[1],
368
+ rawClass: classVal,
369
+ });
370
+ }
371
+ }
372
+ }
373
+ this.traverse(pathNode);
374
+ },
375
+ });
376
+
377
+ function registerComponentFromNode(fnNode, compName, kind, file, fileAst, isDefault = false) {
378
+ if (!compName || !/^[A-Z]/.test(compName)) return;
379
+ const propsSig = extractPropsSignature(fnNode);
380
+ const memberAccesses = new Map();
381
+
382
+ if (propsSig.kind === 'destructured') {
383
+ for (const propName of propsSig.names) {
384
+ const accessed = collectParamMemberAccesses(fnNode.body, propName);
385
+ if (accessed.size > 0) memberAccesses.set(propName, [...accessed]);
386
+ }
387
+ } else if (propsSig.kind === 'identifier' && propsSig.paramName) {
388
+ const accessed = collectParamMemberAccesses(fnNode.body, propsSig.paramName);
389
+ if (accessed.size > 0) memberAccesses.set(propsSig.paramName, [...accessed]);
390
+ }
391
+
392
+ components.set(compName, {
393
+ name: compName,
394
+ file,
395
+ kind,
396
+ isDefault,
397
+ propsSignature: propsSig,
398
+ memberAccesses: Object.fromEntries(memberAccesses.entries()),
399
+ });
400
+ }
401
+ }
402
+
403
+ const componentUsages = [];
404
+
405
+ // Pass 2: Connect .map() loops with their target components across files & collect component usages
406
+ for (const relativeFile of profile.jsxFiles || []) {
407
+ const absPath = path.join(projectDir, relativeFile);
408
+ let code = '';
409
+ try {
410
+ code = fs.readFileSync(absPath, 'utf8');
411
+ } catch {
412
+ continue;
413
+ }
414
+
415
+ let ast;
416
+ try {
417
+ ast = parseSource(code, relativeFile);
418
+ } catch {
419
+ continue;
420
+ }
421
+
422
+ const aliases = collectVariableAliases(ast);
423
+
424
+ recast.types.visit(ast, {
425
+ visitJSXElement(pathNode) {
426
+ const elem = pathNode.node;
427
+ const tagName = getJsxName(elem);
428
+ if (/^[A-Z]/.test(tagName) && components.has(tagName)) {
429
+ const passedLiteralProps = {};
430
+ for (const attr of elem.openingElement?.attributes || []) {
431
+ if (attr.type === 'JSXAttribute' && attr.name && attr.value) {
432
+ const pName = attr.name.name;
433
+ if (attr.value.type === 'StringLiteral' || attr.value.type === 'Literal') {
434
+ passedLiteralProps[pName] = attr.value.value;
435
+ } else if (attr.value.type === 'JSXExpressionContainer') {
436
+ const expr = attr.value.expression;
437
+ if (expr && (expr.type === 'StringLiteral' || expr.type === 'Literal')) {
438
+ passedLiteralProps[pName] = expr.value;
439
+ } else if (expr && (expr.type === 'NumericLiteral' || (expr.type === 'Literal' && typeof expr.value === 'number'))) {
440
+ passedLiteralProps[pName] = expr.value;
441
+ }
442
+ }
443
+ }
444
+ }
445
+ if (Object.keys(passedLiteralProps).length > 0) {
446
+ componentUsages.push({
447
+ componentName: tagName,
448
+ file: relativeFile,
449
+ loc: locKey(elem),
450
+ props: passedLiteralProps,
451
+ });
452
+ }
453
+ }
454
+ this.traverse(pathNode);
455
+ },
456
+ visitCallExpression(pathNode) {
457
+ const node = pathNode.node;
458
+ const callee = node.callee;
459
+ if (
460
+ callee?.type === 'MemberExpression' &&
461
+ !callee.computed &&
462
+ callee.property?.name === 'map'
463
+ ) {
464
+ const unrolled = unrollCollectionSource(callee.object, aliases);
465
+ const arrayName = unrolled?.rootName || (callee.object?.type === 'Identifier' ? callee.object.name : null);
466
+ if (!arrayName) {
467
+ this.traverse(pathNode);
468
+ return;
469
+ }
470
+ const callback = (node.arguments || [])[0];
471
+ if (callback && (callback.type === 'ArrowFunctionExpression' || callback.type === 'FunctionExpression')) {
472
+ const params = callback.params || [];
473
+ const itemParam = params[0]?.type === 'Identifier' ? params[0].name : null;
474
+ const indexParam = params[1]?.type === 'Identifier' ? params[1].name : null;
475
+
476
+ // Find returned JSX root element
477
+ const rootElement = getCallbackRootElement(callback);
478
+ if (rootElement) {
479
+ const rootTagName = getJsxName(rootElement);
480
+ const isChildCustomComponent = /^[A-Z]/.test(rootTagName) && components.has(rootTagName);
481
+ const childComponent = isChildCustomComponent ? components.get(rootTagName) : null;
482
+
483
+ // Check what props are passed to this child component
484
+ let passedProps = {};
485
+ let hasSpreadItem = false;
486
+ if (rootElement.openingElement) {
487
+ for (const attr of rootElement.openingElement.attributes || []) {
488
+ if (attr.type === 'JSXAttribute' && attr.name) {
489
+ const propName = attr.name.name;
490
+ const propVal = attr.value?.expression || attr.value;
491
+ if (propVal && (propVal.type === 'Identifier' || propVal.type === 'JSXIdentifier')) {
492
+ passedProps[propName] = propVal.name;
493
+ }
494
+ } else if (attr.type === 'JSXSpreadAttribute') {
495
+ const expr = attr.argument;
496
+ if (expr && (expr.type === 'Identifier' || expr.type === 'JSXIdentifier') && expr.name === itemParam) {
497
+ hasSpreadItem = true;
498
+ }
499
+ }
500
+ }
501
+ }
502
+
503
+ collections.push({
504
+ file: relativeFile,
505
+ loc: locKey(node),
506
+ rootLoc: locKey(rootElement),
507
+ arrayName,
508
+ itemParam,
509
+ indexParam,
510
+ rootTagName,
511
+ isChildCustomComponent,
512
+ childComponent,
513
+ passedProps,
514
+ hasSpreadItem,
515
+ pipelineChain: unrolled?.chain || [],
516
+ dataSource: dataSources.get(arrayName) || null,
517
+ });
518
+ }
519
+ }
520
+ }
521
+ this.traverse(pathNode);
522
+ },
523
+ });
524
+ }
525
+
526
+ function getCallbackRootElement(callback) {
527
+ if (!callback) return null;
528
+ const body = callback.body;
529
+ if (!body) return null;
530
+ if (body.type === 'JSXElement') return body;
531
+ if (body.type === 'ParenthesizedExpression') return getCallbackRootElement({ body: body.expression });
532
+ if (body.type === 'BlockStatement') {
533
+ for (const stmt of body.body || []) {
534
+ if (stmt.type === 'ReturnStatement' && stmt.argument) {
535
+ const arg = stmt.argument.type === 'ParenthesizedExpression' ? stmt.argument.expression : stmt.argument;
536
+ if (arg?.type === 'JSXElement') return arg;
537
+ }
538
+ }
539
+ }
540
+ return null;
541
+ }
542
+
543
+ function inferComponentNameFromFile(filePath) {
544
+ const base = path.basename(filePath, path.extname(filePath));
545
+ if (base === 'index' || base === 'page') {
546
+ const parent = path.basename(path.dirname(filePath));
547
+ return parent.charAt(0).toUpperCase() + parent.slice(1);
548
+ }
549
+ return base.charAt(0).toUpperCase() + base.slice(1);
550
+ }
551
+
552
+ return {
553
+ components,
554
+ dataSources,
555
+ collections,
556
+ assetImports,
557
+ backgroundImages,
558
+ componentUsages,
559
+ getComponent(name) {
560
+ return components.get(name) || null;
561
+ },
562
+ getDataSource(name) {
563
+ return dataSources.get(name) || null;
564
+ },
565
+ getCollectionByLoc(loc) {
566
+ return collections.find((c) => c.loc === loc || c.rootLoc === loc) || null;
567
+ },
568
+ getAssetImport(file, identifier) {
569
+ return assetImports.get(file)?.get(identifier) || null;
570
+ },
571
+ getComponentUsages(name) {
572
+ return componentUsages.filter((u) => u.componentName === name);
573
+ },
574
+ };
575
+ }
576
+
577
+ module.exports = {
578
+ buildProjectIR,
579
+ resolvePublicAssetUrl,
580
+ objectLiteralToPlain,
581
+ };
@@ -279,6 +279,24 @@ function collectFieldsFromPlan(plan) {
279
279
  if (t.labelField) {
280
280
  fields.push({ path: t.labelField, type: 'text', value: t.labelFallback });
281
281
  }
282
+ if (t.highlightField) {
283
+ fields.push({ path: t.highlightField, type: 'text', value: t.highlightFallback });
284
+ }
285
+ if (t.operation === 'bind-highlighted-heading' && t.field) {
286
+ const highlightFrag = (t.fragments || []).find((f) => f.type === 'span' || f.type === 'format');
287
+ if (highlightFrag) {
288
+ fields.push({ path: `${t.field}Highlight`, type: 'text', value: highlightFrag.text });
289
+ }
290
+ }
291
+ if (t.propTransforms) {
292
+ for (const [propName, propInfo] of Object.entries(t.propTransforms)) {
293
+ fields.push({
294
+ path: propInfo.field,
295
+ type: propInfo.type || 'text',
296
+ value: propInfo.fallback,
297
+ });
298
+ }
299
+ }
282
300
  if (t.listField) {
283
301
  fields.push({
284
302
  path: t.listField,