@deneb-ui/cli 2.0.85 → 2.0.87
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.
- package/bin/index.js +39 -2
- package/package.json +2 -2
- package/src/arc/__tests__/arc.test.cjs +1145 -0
- package/src/arc/adapters/accordion.cjs +32 -0
- package/src/arc/adapters/dialog.cjs +26 -0
- package/src/arc/adapters/embla.cjs +29 -0
- package/src/arc/adapters/gallery.cjs +23 -0
- package/src/arc/adapters/picture-source.cjs +71 -0
- package/src/arc/adapters/registry.cjs +156 -0
- package/src/arc/adapters/slick.cjs +26 -0
- package/src/arc/adapters/swiper.cjs +29 -0
- package/src/arc/adapters/tabs.cjs +29 -0
- package/src/arc/adapters/ui-frameworks.cjs +140 -0
- package/src/arc/adapters.cjs +20 -151
- package/src/arc/canonical-paths.cjs +95 -0
- package/src/arc/data-flow.cjs +158 -0
- package/src/arc/diff.cjs +136 -0
- package/src/arc/explain.cjs +113 -0
- package/src/arc/field-paths.cjs +25 -13
- package/src/arc/index.cjs +118 -6
- package/src/arc/ir-builder.cjs +581 -0
- package/src/arc/manifest.cjs +18 -0
- package/src/arc/planner.cjs +106 -2
- package/src/arc/printer.cjs +18 -5
- package/src/arc/residual.cjs +9 -3
- package/src/arc/rsc-boundary.cjs +154 -0
- package/src/arc/runtime-validator.cjs +286 -0
- package/src/arc/semantic.cjs +217 -16
- package/src/arc/text-fragment-analyzer.cjs +173 -0
- package/src/arc/transformer.cjs +627 -9
|
@@ -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
|
+
};
|
package/src/arc/manifest.cjs
CHANGED
|
@@ -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,
|