@nitronjs/framework 0.2.26 → 0.3.0
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/README.md +260 -170
- package/lib/Auth/Auth.js +2 -2
- package/lib/Build/CssBuilder.js +5 -7
- package/lib/Build/EffectivePropUsage.js +174 -0
- package/lib/Build/FactoryTransform.js +1 -21
- package/lib/Build/FileAnalyzer.js +2 -33
- package/lib/Build/Manager.js +390 -58
- package/lib/Build/PropUsageAnalyzer.js +1189 -0
- package/lib/Build/jsxRuntime.js +25 -155
- package/lib/Build/plugins.js +212 -146
- package/lib/Build/propUtils.js +70 -0
- package/lib/Console/Commands/DevCommand.js +30 -10
- package/lib/Console/Commands/MakeCommand.js +8 -1
- package/lib/Console/Output.js +0 -2
- package/lib/Console/Stubs/rsc-consumer.tsx +74 -0
- package/lib/Console/Stubs/vendor-dev.tsx +30 -41
- package/lib/Console/Stubs/vendor.tsx +25 -1
- package/lib/Core/Config.js +0 -6
- package/lib/Core/Paths.js +0 -19
- package/lib/Database/Migration/Checksum.js +0 -3
- package/lib/Database/Migration/MigrationRepository.js +0 -8
- package/lib/Database/Migration/MigrationRunner.js +1 -2
- package/lib/Database/Model.js +19 -11
- package/lib/Database/QueryBuilder.js +25 -4
- package/lib/Database/Schema/Blueprint.js +10 -0
- package/lib/Database/Schema/Manager.js +2 -0
- package/lib/Date/DateTime.js +1 -1
- package/lib/Dev/DevContext.js +44 -0
- package/lib/Dev/DevErrorPage.js +990 -0
- package/lib/Dev/DevIndicator.js +836 -0
- package/lib/HMR/Server.js +16 -37
- package/lib/Http/Server.js +177 -24
- package/lib/Logging/Log.js +34 -2
- package/lib/Mail/Mail.js +41 -10
- package/lib/Route/Router.js +43 -19
- package/lib/Runtime/Entry.js +10 -6
- package/lib/Session/Manager.js +144 -1
- package/lib/Session/Redis.js +117 -0
- package/lib/Session/Session.js +0 -4
- package/lib/Support/Str.js +6 -4
- package/lib/Translation/Lang.js +376 -32
- package/lib/Translation/pluralize.js +81 -0
- package/lib/Validation/MagicBytes.js +120 -0
- package/lib/Validation/Validator.js +46 -29
- package/lib/View/Client/hmr-client.js +100 -90
- package/lib/View/Client/spa.js +121 -50
- package/lib/View/ClientManifest.js +60 -0
- package/lib/View/FlightRenderer.js +100 -0
- package/lib/View/Layout.js +0 -3
- package/lib/View/PropFilter.js +81 -0
- package/lib/View/View.js +230 -495
- package/lib/index.d.ts +22 -1
- package/package.json +3 -2
- package/skeleton/config/app.js +1 -0
- package/skeleton/config/server.js +13 -0
- package/skeleton/config/session.js +4 -0
- package/lib/Build/HydrationBuilder.js +0 -190
- package/lib/Console/Stubs/page-hydration-dev.tsx +0 -72
- package/lib/Console/Stubs/page-hydration.tsx +0 -53
|
@@ -0,0 +1,1189 @@
|
|
|
1
|
+
import { parse } from "@babel/parser";
|
|
2
|
+
import traverse from "@babel/traverse";
|
|
3
|
+
import fs from "fs";
|
|
4
|
+
import { mergeUsageTrees } from "./propUtils.js";
|
|
5
|
+
|
|
6
|
+
const _traverse = traverse.default;
|
|
7
|
+
const ARRAY_METHODS = new Set(["map", "forEach", "filter", "find", "some", "every", "reduce", "flatMap", "findIndex"]);
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Analyzes "use client" components at build time to extract prop usage trees.
|
|
11
|
+
* Determines which props (and sub-properties) a client component actually accesses,
|
|
12
|
+
* enabling the runtime to strip unused data from the Flight payload.
|
|
13
|
+
*
|
|
14
|
+
* Returns:
|
|
15
|
+
* - An object (usage tree) if prop usage is detectable
|
|
16
|
+
* - null if all props should pass through (spread, dynamic access, parse failure)
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* // Component: function Card({ item }) { return <div>{item.title}</div> }
|
|
20
|
+
* // Result: { item: { title: true } }
|
|
21
|
+
*/
|
|
22
|
+
class PropUsageAnalyzer {
|
|
23
|
+
/**
|
|
24
|
+
* Analyze a client component file and extract its prop usage tree.
|
|
25
|
+
* @param {string} filePath - Absolute path to the .tsx file.
|
|
26
|
+
* @returns {object|null} Usage tree or null (passthrough).
|
|
27
|
+
*/
|
|
28
|
+
static analyze(filePath) {
|
|
29
|
+
const parsed = parseFile(filePath);
|
|
30
|
+
if (!parsed) return null;
|
|
31
|
+
|
|
32
|
+
const funcPaths = findComponentFunction(parsed.ast);
|
|
33
|
+
if (!funcPaths) return null;
|
|
34
|
+
|
|
35
|
+
// Analyze all exported components and merge their usage trees.
|
|
36
|
+
// This way all named exports' props are included in the filter.
|
|
37
|
+
let merged = null;
|
|
38
|
+
|
|
39
|
+
for (const funcPath of funcPaths) {
|
|
40
|
+
const usage = extractPropUsage(funcPath);
|
|
41
|
+
|
|
42
|
+
if (usage === null) return null;
|
|
43
|
+
if (!merged) { merged = usage; }
|
|
44
|
+
else if (usage) { merged = mergeUsageTrees(merged, usage); }
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Meta export can also access props (e.g. Meta = ({ admin }) => ({ title: admin.username }))
|
|
48
|
+
// Merge Meta's usage so safeParams includes fields needed by both component and Meta.
|
|
49
|
+
const metaFuncPath = findMetaFunction(parsed.ast);
|
|
50
|
+
|
|
51
|
+
if (!metaFuncPath) return merged;
|
|
52
|
+
|
|
53
|
+
const metaUsage = extractPropUsage(metaFuncPath);
|
|
54
|
+
|
|
55
|
+
if (!metaUsage) return merged;
|
|
56
|
+
if (!merged) return metaUsage;
|
|
57
|
+
|
|
58
|
+
return mergeUsageTrees(merged, metaUsage);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Analyze JSX prop forwarding patterns in a client component.
|
|
63
|
+
* Detects when a component passes its props directly to child components.
|
|
64
|
+
*
|
|
65
|
+
* @example
|
|
66
|
+
* // function Wrapper({ data }) {
|
|
67
|
+
* // return <Display info={data} />;
|
|
68
|
+
* // }
|
|
69
|
+
* // Result: { forwards: [{ parentProp: "data", childImport: "./Display", childProp: "info" }] }
|
|
70
|
+
*
|
|
71
|
+
* @param {string} filePath - Absolute path to the .tsx file.
|
|
72
|
+
* @returns {{forwards: Array, imports: object}|null}
|
|
73
|
+
*/
|
|
74
|
+
static analyzeForwarding(filePath) {
|
|
75
|
+
const parsed = parseFile(filePath);
|
|
76
|
+
if (!parsed) return null;
|
|
77
|
+
|
|
78
|
+
const imports = extractImports(parsed.ast);
|
|
79
|
+
const funcPaths = findComponentFunction(parsed.ast);
|
|
80
|
+
if (!funcPaths) return null;
|
|
81
|
+
|
|
82
|
+
// Use the primary component (default export, or first named) for forwarding analysis
|
|
83
|
+
return extractForwards(funcPaths[0], imports);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Analyze a file and extract prop usage, forwarding, and translation keys
|
|
88
|
+
* from a single parse pass. Use this instead of calling analyze() + analyzeForwarding()
|
|
89
|
+
* separately to avoid double file I/O and double Babel parse.
|
|
90
|
+
*
|
|
91
|
+
* @param {string} filePath - Absolute path to the .tsx/.jsx file.
|
|
92
|
+
* @returns {{propUsage: object|null, forwarding: {forwards: Array, imports: object}|null, hasDefaultExport: boolean, translationKeys: string[]|null}}
|
|
93
|
+
*/
|
|
94
|
+
static analyzeAll(filePath) {
|
|
95
|
+
const parsed = parseFile(filePath);
|
|
96
|
+
if (!parsed) return { propUsage: null, forwarding: null, hasDefaultExport: false, translationKeys: null };
|
|
97
|
+
|
|
98
|
+
// Extract translation keys BEFORE findComponentFunction (server files may not have components)
|
|
99
|
+
const translationKeys = extractTranslationKeys(parsed.ast);
|
|
100
|
+
|
|
101
|
+
const hasDefaultExport = hasDefault(parsed.ast);
|
|
102
|
+
|
|
103
|
+
const funcPaths = findComponentFunction(parsed.ast);
|
|
104
|
+
if (!funcPaths) return { propUsage: null, forwarding: null, hasDefaultExport, translationKeys };
|
|
105
|
+
|
|
106
|
+
// Merge all exported components' usage trees into one
|
|
107
|
+
let propUsage = null;
|
|
108
|
+
|
|
109
|
+
for (const funcPath of funcPaths) {
|
|
110
|
+
const usage = extractPropUsage(funcPath);
|
|
111
|
+
|
|
112
|
+
if (usage === null) { propUsage = null; break; }
|
|
113
|
+
if (!propUsage) { propUsage = usage; }
|
|
114
|
+
else if (usage) { propUsage = mergeUsageTrees(propUsage, usage); }
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Also merge Meta export usage
|
|
118
|
+
const metaFuncPath = findMetaFunction(parsed.ast);
|
|
119
|
+
|
|
120
|
+
if (metaFuncPath) {
|
|
121
|
+
const metaUsage = extractPropUsage(metaFuncPath);
|
|
122
|
+
|
|
123
|
+
if (metaUsage) {
|
|
124
|
+
propUsage = propUsage ? mergeUsageTrees(propUsage, metaUsage) : metaUsage;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const imports = extractImports(parsed.ast);
|
|
129
|
+
const forwarding = extractForwards(funcPaths[0], imports);
|
|
130
|
+
|
|
131
|
+
return { propUsage, forwarding, hasDefaultExport, translationKeys };
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Parses a file and returns source + AST. Shared by analyze() and analyzeForwarding().
|
|
138
|
+
* @param {string} filePath
|
|
139
|
+
* @returns {{source: string, ast: object}|null}
|
|
140
|
+
*/
|
|
141
|
+
function parseFile(filePath) {
|
|
142
|
+
let source;
|
|
143
|
+
|
|
144
|
+
try {
|
|
145
|
+
source = fs.readFileSync(filePath, "utf8");
|
|
146
|
+
}
|
|
147
|
+
catch {
|
|
148
|
+
return null;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
let ast;
|
|
152
|
+
|
|
153
|
+
try {
|
|
154
|
+
ast = parse(source, {
|
|
155
|
+
sourceType: "module",
|
|
156
|
+
plugins: ["typescript", "jsx"]
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
catch {
|
|
160
|
+
return null;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return { source, ast };
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Extracts all imports from an AST as { localName → importSource }.
|
|
168
|
+
* @param {object} ast
|
|
169
|
+
* @returns {object}
|
|
170
|
+
*/
|
|
171
|
+
function extractImports(ast) {
|
|
172
|
+
const imports = {};
|
|
173
|
+
|
|
174
|
+
_traverse(ast, {
|
|
175
|
+
ImportDeclaration(p) {
|
|
176
|
+
const src = p.node.source.value;
|
|
177
|
+
|
|
178
|
+
for (const spec of p.node.specifiers) {
|
|
179
|
+
imports[spec.local.name] = src;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
return imports;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Extracts JSX prop forwarding patterns from a component function.
|
|
189
|
+
* @param {import("@babel/traverse").NodePath} funcPath
|
|
190
|
+
* @param {object} imports - Import map { localName → importSource }.
|
|
191
|
+
* @returns {{forwards: Array, imports: object}}
|
|
192
|
+
*/
|
|
193
|
+
function extractForwards(funcPath, imports) {
|
|
194
|
+
const propVarMap = extractPropVarMap(funcPath);
|
|
195
|
+
const propsIdentifier = extractPropsIdentifier(funcPath);
|
|
196
|
+
|
|
197
|
+
if (!propVarMap && !propsIdentifier) return { forwards: [], imports };
|
|
198
|
+
|
|
199
|
+
const forwards = [];
|
|
200
|
+
|
|
201
|
+
funcPath.traverse({
|
|
202
|
+
JSXOpeningElement(jsxPath) {
|
|
203
|
+
const tag = jsxPath.node.name;
|
|
204
|
+
if (tag.type !== "JSXIdentifier") return;
|
|
205
|
+
|
|
206
|
+
const tagName = tag.name;
|
|
207
|
+
if (!tagName || tagName[0] !== tagName[0].toUpperCase()) return;
|
|
208
|
+
if (!imports[tagName]) return;
|
|
209
|
+
|
|
210
|
+
for (const attr of jsxPath.node.attributes) {
|
|
211
|
+
if (attr.type !== "JSXAttribute") continue;
|
|
212
|
+
|
|
213
|
+
const childProp = attr.name?.name;
|
|
214
|
+
if (!childProp) continue;
|
|
215
|
+
|
|
216
|
+
const value = attr.value;
|
|
217
|
+
if (!value || value.type !== "JSXExpressionContainer") continue;
|
|
218
|
+
|
|
219
|
+
const expr = value.expression;
|
|
220
|
+
|
|
221
|
+
// Pattern 1: { data } destructured → <Child info={data} />
|
|
222
|
+
if (propVarMap && expr.type === "Identifier" && propVarMap.has(expr.name)) {
|
|
223
|
+
forwards.push({
|
|
224
|
+
parentProp: propVarMap.get(expr.name),
|
|
225
|
+
childImport: imports[tagName],
|
|
226
|
+
childProp,
|
|
227
|
+
childTag: tagName
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// Pattern 2: (props) identifier → <Child info={props.data} />
|
|
232
|
+
if (propsIdentifier &&
|
|
233
|
+
expr.type === "MemberExpression" &&
|
|
234
|
+
expr.object.type === "Identifier" &&
|
|
235
|
+
expr.object.name === propsIdentifier &&
|
|
236
|
+
!expr.computed &&
|
|
237
|
+
expr.property.type === "Identifier") {
|
|
238
|
+
forwards.push({
|
|
239
|
+
parentProp: expr.property.name,
|
|
240
|
+
childImport: imports[tagName],
|
|
241
|
+
childProp,
|
|
242
|
+
childTag: tagName
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
return { forwards, imports };
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Extracts a Map of variable names → original prop keys from destructured params.
|
|
254
|
+
* { admin: myAdmin } → Map { "myAdmin" → "admin" }
|
|
255
|
+
* { admin } → Map { "admin" → "admin" }
|
|
256
|
+
* @param {import("@babel/traverse").NodePath} funcPath
|
|
257
|
+
* @returns {Map<string, string>|null}
|
|
258
|
+
*/
|
|
259
|
+
function extractPropVarMap(funcPath) {
|
|
260
|
+
const params = funcPath.node.params;
|
|
261
|
+
if (!params || params.length === 0) return null;
|
|
262
|
+
|
|
263
|
+
let pattern = params[0];
|
|
264
|
+
|
|
265
|
+
if (pattern.type === "AssignmentPattern") pattern = pattern.left;
|
|
266
|
+
if (pattern.type !== "ObjectPattern") return null;
|
|
267
|
+
|
|
268
|
+
const map = new Map();
|
|
269
|
+
|
|
270
|
+
for (const prop of pattern.properties) {
|
|
271
|
+
if (prop.type === "RestElement") return null;
|
|
272
|
+
|
|
273
|
+
const key = prop.key?.name || prop.key?.value;
|
|
274
|
+
if (!key) continue;
|
|
275
|
+
|
|
276
|
+
const value = prop.value;
|
|
277
|
+
|
|
278
|
+
if (value && value.type === "Identifier") {
|
|
279
|
+
map.set(value.name, key);
|
|
280
|
+
}
|
|
281
|
+
else if (value && value.type === "AssignmentPattern" && value.left?.type === "Identifier") {
|
|
282
|
+
map.set(value.left.name, key);
|
|
283
|
+
}
|
|
284
|
+
else {
|
|
285
|
+
map.set(key, key);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
return map.size > 0 ? map : null;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Extracts the props identifier name when using (props) => ... pattern.
|
|
294
|
+
* @param {import("@babel/traverse").NodePath} funcPath
|
|
295
|
+
* @returns {string|null}
|
|
296
|
+
*/
|
|
297
|
+
function extractPropsIdentifier(funcPath) {
|
|
298
|
+
const params = funcPath.node.params;
|
|
299
|
+
if (!params || params.length === 0) return null;
|
|
300
|
+
|
|
301
|
+
let param = params[0];
|
|
302
|
+
|
|
303
|
+
if (param.type === "AssignmentPattern") param = param.left;
|
|
304
|
+
if (param.type === "Identifier") return param.name;
|
|
305
|
+
|
|
306
|
+
return null;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* Finds all exported component functions in the AST.
|
|
311
|
+
* Returns the default export (priority) plus any named export components.
|
|
312
|
+
* Meta and Layout named exports are excluded (not components).
|
|
313
|
+
* @param {object} ast - Babel AST.
|
|
314
|
+
* @returns {import("@babel/traverse").NodePath|null} Single function path, or null.
|
|
315
|
+
*/
|
|
316
|
+
function findComponentFunction(ast) {
|
|
317
|
+
let defaultExport = null;
|
|
318
|
+
const namedExports = [];
|
|
319
|
+
const SKIP_NAMES = new Set(["Meta", "Layout"]);
|
|
320
|
+
|
|
321
|
+
_traverse(ast, {
|
|
322
|
+
ExportDefaultDeclaration(path) {
|
|
323
|
+
const decl = path.node.declaration;
|
|
324
|
+
|
|
325
|
+
// export default function Comp() {}
|
|
326
|
+
if (decl.type === "FunctionDeclaration") {
|
|
327
|
+
defaultExport = path.get("declaration");
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// export default () => {}
|
|
332
|
+
if (decl.type === "ArrowFunctionExpression" || decl.type === "FunctionExpression") {
|
|
333
|
+
defaultExport = path.get("declaration");
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// export default memo(() => {}) or forwardRef(() => {})
|
|
338
|
+
if (decl.type === "CallExpression") {
|
|
339
|
+
const unwrapped = unwrapHOC(decl, path.get("declaration"));
|
|
340
|
+
|
|
341
|
+
if (unwrapped) {
|
|
342
|
+
defaultExport = unwrapped;
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// export default Comp (reference to a variable or function)
|
|
348
|
+
if (decl.type === "Identifier") {
|
|
349
|
+
const binding = path.scope.getBinding(decl.name);
|
|
350
|
+
if (!binding) return;
|
|
351
|
+
|
|
352
|
+
const bp = binding.path;
|
|
353
|
+
|
|
354
|
+
// function Comp() {}
|
|
355
|
+
if (bp.isFunctionDeclaration()) {
|
|
356
|
+
defaultExport = bp;
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// const Comp = () => {} or const Comp = memo(() => {})
|
|
361
|
+
if (bp.isVariableDeclarator()) {
|
|
362
|
+
const init = bp.node.init;
|
|
363
|
+
if (!init) return;
|
|
364
|
+
|
|
365
|
+
if (init.type === "ArrowFunctionExpression" || init.type === "FunctionExpression") {
|
|
366
|
+
defaultExport = bp.get("init");
|
|
367
|
+
return;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
const unwrapped = unwrapHOC(init, bp.get("init"));
|
|
371
|
+
|
|
372
|
+
if (unwrapped) {
|
|
373
|
+
defaultExport = unwrapped;
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
},
|
|
378
|
+
|
|
379
|
+
ExportNamedDeclaration(path) {
|
|
380
|
+
const decl = path.node.declaration;
|
|
381
|
+
if (!decl) return;
|
|
382
|
+
|
|
383
|
+
// export function Button({ label }) {}
|
|
384
|
+
if (decl.type === "FunctionDeclaration" && decl.id?.name && !SKIP_NAMES.has(decl.id.name)) {
|
|
385
|
+
namedExports.push(path.get("declaration"));
|
|
386
|
+
return;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
// export const Button = () => {} or export const Button = memo(...)
|
|
390
|
+
if (decl.type === "VariableDeclaration") {
|
|
391
|
+
for (let i = 0; i < decl.declarations.length; i++) {
|
|
392
|
+
const declarator = decl.declarations[i];
|
|
393
|
+
const init = declarator.init;
|
|
394
|
+
|
|
395
|
+
if (!init || SKIP_NAMES.has(declarator.id?.name)) continue;
|
|
396
|
+
|
|
397
|
+
if (init.type === "ArrowFunctionExpression" || init.type === "FunctionExpression") {
|
|
398
|
+
namedExports.push(path.get(`declaration.declarations.${i}.init`));
|
|
399
|
+
}
|
|
400
|
+
else if (init.type === "CallExpression") {
|
|
401
|
+
const unwrapped = unwrapHOC(init, path.get(`declaration.declarations.${i}.init`));
|
|
402
|
+
|
|
403
|
+
if (unwrapped) {
|
|
404
|
+
namedExports.push(unwrapped);
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
// Return all found component functions for merged analysis.
|
|
413
|
+
const all = [];
|
|
414
|
+
if (defaultExport) all.push(defaultExport);
|
|
415
|
+
all.push(...namedExports);
|
|
416
|
+
|
|
417
|
+
return all.length > 0 ? all : null;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
/**
|
|
421
|
+
* Finds the exported Meta function in the AST.
|
|
422
|
+
* Meta can be a function that receives props and returns meta tags:
|
|
423
|
+
* export const Meta = (props) => ({ title: props.pageData.title })
|
|
424
|
+
* export const Meta = ({ admin }) => ({ title: admin.username })
|
|
425
|
+
* Static Meta objects (export const Meta = { title: "Dashboard" }) are skipped
|
|
426
|
+
* because they don't access props.
|
|
427
|
+
* @param {object} ast - Babel AST.
|
|
428
|
+
* @returns {import("@babel/traverse").NodePath|null}
|
|
429
|
+
*/
|
|
430
|
+
function findMetaFunction(ast) {
|
|
431
|
+
let result = null;
|
|
432
|
+
|
|
433
|
+
_traverse(ast, {
|
|
434
|
+
ExportNamedDeclaration(path) {
|
|
435
|
+
const decl = path.node.declaration;
|
|
436
|
+
if (!decl || decl.type !== "VariableDeclaration") return;
|
|
437
|
+
|
|
438
|
+
for (let i = 0; i < decl.declarations.length; i++) {
|
|
439
|
+
const declarator = decl.declarations[i];
|
|
440
|
+
|
|
441
|
+
if (declarator.id?.name !== "Meta") continue;
|
|
442
|
+
|
|
443
|
+
const init = declarator.init;
|
|
444
|
+
if (!init) continue;
|
|
445
|
+
|
|
446
|
+
if (init.type === "ArrowFunctionExpression" || init.type === "FunctionExpression") {
|
|
447
|
+
result = path.get(`declaration.declarations.${i}.init`);
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
});
|
|
452
|
+
|
|
453
|
+
return result;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
/**
|
|
457
|
+
* Checks if the AST has an ExportDefaultDeclaration.
|
|
458
|
+
* Used by the wrapper plugin to decide whether default import is safe.
|
|
459
|
+
* @param {object} ast - Babel AST.
|
|
460
|
+
* @returns {boolean}
|
|
461
|
+
*/
|
|
462
|
+
function hasDefault(ast) {
|
|
463
|
+
for (const node of ast.program.body) {
|
|
464
|
+
if (node.type === "ExportDefaultDeclaration") return true;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
return false;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
/**
|
|
471
|
+
* Unwraps HOC patterns like memo() and forwardRef() to find the inner function.
|
|
472
|
+
* @param {object} callNode - CallExpression AST node.
|
|
473
|
+
* @param {import("@babel/traverse").NodePath} callPath - Babel path for the call.
|
|
474
|
+
* @returns {import("@babel/traverse").NodePath|null}
|
|
475
|
+
*/
|
|
476
|
+
function unwrapHOC(callNode, callPath) {
|
|
477
|
+
const callee = callNode.callee;
|
|
478
|
+
|
|
479
|
+
if (!callee) return null;
|
|
480
|
+
|
|
481
|
+
const calleeName = callee.type === "Identifier"
|
|
482
|
+
? callee.name
|
|
483
|
+
: callee.type === "MemberExpression" && callee.property?.name
|
|
484
|
+
? callee.property.name
|
|
485
|
+
: null;
|
|
486
|
+
|
|
487
|
+
if (!calleeName || !["memo", "forwardRef"].includes(calleeName)) return null;
|
|
488
|
+
|
|
489
|
+
const arg = callNode.arguments[0];
|
|
490
|
+
if (!arg) return null;
|
|
491
|
+
|
|
492
|
+
if (arg.type === "ArrowFunctionExpression" || arg.type === "FunctionExpression" || arg.type === "FunctionDeclaration") {
|
|
493
|
+
return callPath.get("arguments.0");
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
// Nested HOC: memo(forwardRef(fn)) → recurse into inner CallExpression
|
|
497
|
+
if (arg.type === "CallExpression") {
|
|
498
|
+
return unwrapHOC(arg, callPath.get("arguments.0"));
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
return null;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
/**
|
|
505
|
+
* Extracts prop usage from a component function path.
|
|
506
|
+
* @param {import("@babel/traverse").NodePath} funcPath
|
|
507
|
+
* @returns {object|null}
|
|
508
|
+
*/
|
|
509
|
+
function extractPropUsage(funcPath) {
|
|
510
|
+
const params = funcPath.node.params;
|
|
511
|
+
|
|
512
|
+
if (!params || params.length === 0) return {};
|
|
513
|
+
|
|
514
|
+
const firstParam = params[0];
|
|
515
|
+
|
|
516
|
+
// ({ name, admin }) => ...
|
|
517
|
+
if (firstParam.type === "ObjectPattern") {
|
|
518
|
+
return analyzeObjectPattern(firstParam, funcPath);
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
// (props) => ...
|
|
522
|
+
if (firstParam.type === "Identifier") {
|
|
523
|
+
return analyzePropsIdentifier(firstParam.name, funcPath);
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
// (props: Props) with TypeAnnotation — underlying is still Identifier or ObjectPattern
|
|
527
|
+
if (firstParam.type === "AssignmentPattern") {
|
|
528
|
+
const left = firstParam.left;
|
|
529
|
+
|
|
530
|
+
if (left.type === "ObjectPattern") {
|
|
531
|
+
return analyzeObjectPattern(left, funcPath);
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
if (left.type === "Identifier") {
|
|
535
|
+
return analyzePropsIdentifier(left.name, funcPath);
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
return null;
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
/**
|
|
543
|
+
* Analyzes destructured props: ({ name, admin, ...rest }) => ...
|
|
544
|
+
* @param {object} pattern - ObjectPattern AST node.
|
|
545
|
+
* @param {import("@babel/traverse").NodePath} funcPath
|
|
546
|
+
* @returns {object|null}
|
|
547
|
+
*/
|
|
548
|
+
function analyzeObjectPattern(pattern, funcPath) {
|
|
549
|
+
const usage = {};
|
|
550
|
+
const varsToTrack = [];
|
|
551
|
+
|
|
552
|
+
for (const prop of pattern.properties) {
|
|
553
|
+
// Rest spread: { ...rest } → passthrough (can't determine usage)
|
|
554
|
+
if (prop.type === "RestElement") return null;
|
|
555
|
+
|
|
556
|
+
const key = prop.key?.name || prop.key?.value;
|
|
557
|
+
if (!key) continue;
|
|
558
|
+
|
|
559
|
+
const value = prop.value || prop;
|
|
560
|
+
|
|
561
|
+
if (value.type === "Identifier") {
|
|
562
|
+
// Simple: { admin } or { admin: myAdmin }
|
|
563
|
+
varsToTrack.push({ key, varName: value.name });
|
|
564
|
+
}
|
|
565
|
+
else if (value.type === "ObjectPattern") {
|
|
566
|
+
// Nested destructuring: { admin: { name, role } }
|
|
567
|
+
usage[key] = buildNestedDestructureTree(value);
|
|
568
|
+
}
|
|
569
|
+
else if (value.type === "AssignmentPattern") {
|
|
570
|
+
// Default value: { name = "default" }
|
|
571
|
+
const left = value.left;
|
|
572
|
+
|
|
573
|
+
if (left.type === "Identifier") {
|
|
574
|
+
varsToTrack.push({ key, varName: left.name });
|
|
575
|
+
}
|
|
576
|
+
else if (left.type === "ObjectPattern") {
|
|
577
|
+
usage[key] = buildNestedDestructureTree(left);
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
// Track how each destructured variable is used in the function body
|
|
583
|
+
const bodyStart = funcPath.node.body?.start ?? 0;
|
|
584
|
+
|
|
585
|
+
for (const { key, varName } of varsToTrack) {
|
|
586
|
+
const propUsage = trackVariableAccess(varName, funcPath, bodyStart);
|
|
587
|
+
|
|
588
|
+
if (propUsage !== false) {
|
|
589
|
+
usage[key] = propUsage;
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
return usage;
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
/**
|
|
597
|
+
* Builds usage tree from nested destructuring.
|
|
598
|
+
* { admin: { name, mfa: { status } } } → { name: true, mfa: { status: true } }
|
|
599
|
+
* @param {object} pattern - ObjectPattern AST node.
|
|
600
|
+
* @returns {object|true}
|
|
601
|
+
*/
|
|
602
|
+
function buildNestedDestructureTree(pattern) {
|
|
603
|
+
const tree = {};
|
|
604
|
+
|
|
605
|
+
for (const prop of pattern.properties) {
|
|
606
|
+
if (prop.type === "RestElement") return true;
|
|
607
|
+
|
|
608
|
+
const key = prop.key?.name || prop.key?.value;
|
|
609
|
+
if (!key) continue;
|
|
610
|
+
|
|
611
|
+
const value = prop.value || prop;
|
|
612
|
+
|
|
613
|
+
if (value.type === "ObjectPattern") {
|
|
614
|
+
tree[key] = buildNestedDestructureTree(value);
|
|
615
|
+
}
|
|
616
|
+
else {
|
|
617
|
+
tree[key] = true;
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
return Object.keys(tree).length > 0 ? tree : true;
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
/**
|
|
625
|
+
* Tracks how a variable is accessed in the function body.
|
|
626
|
+
* Collects MemberExpression chains to build a sub-property tree.
|
|
627
|
+
*
|
|
628
|
+
* @param {string} varName - Variable name to track.
|
|
629
|
+
* @param {import("@babel/traverse").NodePath} funcPath - Function path.
|
|
630
|
+
* @param {number} bodyStart - Start position of function body.
|
|
631
|
+
* @returns {object|true} Sub-tree or true (used as whole).
|
|
632
|
+
*/
|
|
633
|
+
function trackVariableAccess(varName, funcPath, bodyStart) {
|
|
634
|
+
let usedAsWhole = false;
|
|
635
|
+
const chains = [];
|
|
636
|
+
const arrayIterations = [];
|
|
637
|
+
|
|
638
|
+
funcPath.traverse({
|
|
639
|
+
Identifier(idPath) {
|
|
640
|
+
// Skip identifiers before the function body (params, type annotations)
|
|
641
|
+
if (idPath.node.start < bodyStart) return;
|
|
642
|
+
|
|
643
|
+
if (idPath.node.name !== varName) return;
|
|
644
|
+
|
|
645
|
+
// Skip type annotations
|
|
646
|
+
if (isInTypeAnnotation(idPath)) return;
|
|
647
|
+
|
|
648
|
+
// Skip if this is a property key (not value): { admin: something }
|
|
649
|
+
if (idPath.parentPath.isObjectProperty() &&
|
|
650
|
+
idPath.parentPath.node.key === idPath.node &&
|
|
651
|
+
!idPath.parentPath.node.computed) {
|
|
652
|
+
return;
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
// Skip import specifiers
|
|
656
|
+
if (idPath.parentPath.isImportSpecifier() || idPath.parentPath.isImportDefaultSpecifier()) {
|
|
657
|
+
return;
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
// Check if part of a MemberExpression chain (including optional chaining ?.)
|
|
661
|
+
const isMember = idPath.parentPath.isMemberExpression() || idPath.parentPath.isOptionalMemberExpression();
|
|
662
|
+
|
|
663
|
+
if (isMember &&
|
|
664
|
+
idPath.parentPath.node.object === idPath.node &&
|
|
665
|
+
!idPath.parentPath.node.computed) {
|
|
666
|
+
|
|
667
|
+
const chain = collectMemberChain(idPath.parentPath);
|
|
668
|
+
const outermost = getOutermostMemberExpr(idPath.parentPath);
|
|
669
|
+
|
|
670
|
+
// Check for...of: for (const item of varName.items)
|
|
671
|
+
if (outermost.parentPath.isForOfStatement() &&
|
|
672
|
+
outermost.parentPath.node.right === outermost.node) {
|
|
673
|
+
const loopVar = extractForOfVariable(outermost.parentPath.node);
|
|
674
|
+
|
|
675
|
+
if (loopVar) {
|
|
676
|
+
arrayIterations.push({ prefixChain: chain, loopVar, scopePath: outermost.parentPath });
|
|
677
|
+
return;
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
const isCallCallee = (outermost.parentPath.isCallExpression() || outermost.parentPath.isOptionalCallExpression()) &&
|
|
682
|
+
outermost.parentPath.node.callee === outermost.node;
|
|
683
|
+
|
|
684
|
+
if (isCallCallee) {
|
|
685
|
+
const methodName = chain[chain.length - 1];
|
|
686
|
+
|
|
687
|
+
if (ARRAY_METHODS.has(methodName)) {
|
|
688
|
+
const prefixChain = chain.slice(0, -1);
|
|
689
|
+
const callbacks = collectArrayCallbacks(outermost.parentPath, methodName);
|
|
690
|
+
|
|
691
|
+
if (callbacks.length > 0) {
|
|
692
|
+
arrayIterations.push({ prefixChain, callbacks });
|
|
693
|
+
return;
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
usedAsWhole = true;
|
|
697
|
+
return;
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
chain.pop();
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
if (chain.length === 0) {
|
|
704
|
+
usedAsWhole = true;
|
|
705
|
+
}
|
|
706
|
+
else {
|
|
707
|
+
chains.push(chain);
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
return;
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
// Check direct for...of: for (const item of varName)
|
|
714
|
+
if (idPath.parentPath.isForOfStatement() &&
|
|
715
|
+
idPath.parentPath.node.right === idPath.node) {
|
|
716
|
+
const loopVar = extractForOfVariable(idPath.parentPath.node);
|
|
717
|
+
|
|
718
|
+
if (loopVar) {
|
|
719
|
+
arrayIterations.push({ prefixChain: [], loopVar, scopePath: idPath.parentPath });
|
|
720
|
+
return;
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
// Guard/truthy checks (isAdmin && ..., isAdmin ? ... : ...) need the
|
|
725
|
+
// value itself but not its sub-properties. Mark as whole usage so
|
|
726
|
+
// boolean props are kept in the filtered output.
|
|
727
|
+
if (isGuardCheck(idPath)) {
|
|
728
|
+
usedAsWhole = true;
|
|
729
|
+
return;
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
// Used directly (as argument, JSX attribute value, assignment, etc.)
|
|
733
|
+
usedAsWhole = true;
|
|
734
|
+
}
|
|
735
|
+
});
|
|
736
|
+
|
|
737
|
+
if (usedAsWhole) return true;
|
|
738
|
+
if (chains.length === 0 && arrayIterations.length === 0) return false;
|
|
739
|
+
|
|
740
|
+
const tree = chains.length > 0 ? buildTreeFromChains(chains) : {};
|
|
741
|
+
|
|
742
|
+
for (const iteration of arrayIterations) {
|
|
743
|
+
const elementUsage = processArrayIteration(iteration);
|
|
744
|
+
|
|
745
|
+
if (elementUsage === null) return true;
|
|
746
|
+
|
|
747
|
+
mergeArrayUsage(tree, iteration.prefixChain, elementUsage);
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
return Object.keys(tree).length > 0 ? tree : false;
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
/**
|
|
754
|
+
* Extracts the loop variable from a ForOfStatement node.
|
|
755
|
+
* Handles: const item, let item, destructured { name, id }.
|
|
756
|
+
* @param {object} forOfNode - ForOfStatement AST node.
|
|
757
|
+
* @returns {{type: string, name?: string, pattern?: object}|null}
|
|
758
|
+
*/
|
|
759
|
+
function extractForOfVariable(forOfNode) {
|
|
760
|
+
const left = forOfNode.left;
|
|
761
|
+
|
|
762
|
+
if (left.type !== "VariableDeclaration") return null;
|
|
763
|
+
|
|
764
|
+
const id = left.declarations[0]?.id;
|
|
765
|
+
if (!id) return null;
|
|
766
|
+
|
|
767
|
+
if (id.type === "Identifier") return { type: "identifier", name: id.name };
|
|
768
|
+
if (id.type === "ObjectPattern") return { type: "pattern", pattern: id };
|
|
769
|
+
|
|
770
|
+
return null;
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
/**
|
|
774
|
+
* Collects all array method callbacks from an initial call and any chained calls.
|
|
775
|
+
* items.filter(cb1).map(cb2) → [cb1Info, cb2Info]
|
|
776
|
+
* @param {import("@babel/traverse").NodePath} callExprPath - Initial CallExpression path.
|
|
777
|
+
* @param {string} methodName - Initial method name.
|
|
778
|
+
* @returns {Array<{param: object, callbackPath: import("@babel/traverse").NodePath}>}
|
|
779
|
+
*/
|
|
780
|
+
function collectArrayCallbacks(callExprPath, methodName) {
|
|
781
|
+
const callbacks = [];
|
|
782
|
+
|
|
783
|
+
const cb = callExprPath.node.arguments[0];
|
|
784
|
+
|
|
785
|
+
if (cb && (cb.type === "ArrowFunctionExpression" || cb.type === "FunctionExpression")) {
|
|
786
|
+
const pIdx = methodName === "reduce" ? 1 : 0;
|
|
787
|
+
const param = cb.params?.[pIdx];
|
|
788
|
+
|
|
789
|
+
if (param) {
|
|
790
|
+
callbacks.push({ param, callbackPath: callExprPath.get("arguments.0") });
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
// Follow chain upward for chained array methods: .filter(cb).map(cb)
|
|
795
|
+
let current = callExprPath;
|
|
796
|
+
|
|
797
|
+
while (current.parentPath.isMemberExpression() &&
|
|
798
|
+
current.parentPath.node.object === current.node &&
|
|
799
|
+
!current.parentPath.node.computed) {
|
|
800
|
+
|
|
801
|
+
const chainedMethod = current.parentPath.node.property?.name;
|
|
802
|
+
const gp = current.parentPath.parentPath;
|
|
803
|
+
|
|
804
|
+
if (gp.isCallExpression() && gp.node.callee === current.parentPath.node && ARRAY_METHODS.has(chainedMethod)) {
|
|
805
|
+
const chainedCb = gp.node.arguments[0];
|
|
806
|
+
|
|
807
|
+
if (chainedCb && (chainedCb.type === "ArrowFunctionExpression" || chainedCb.type === "FunctionExpression")) {
|
|
808
|
+
const pIdx = chainedMethod === "reduce" ? 1 : 0;
|
|
809
|
+
const param = chainedCb.params?.[pIdx];
|
|
810
|
+
|
|
811
|
+
if (param) {
|
|
812
|
+
callbacks.push({ param, callbackPath: gp.get("arguments.0") });
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
current = gp;
|
|
817
|
+
}
|
|
818
|
+
else {
|
|
819
|
+
break;
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
return callbacks;
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
/**
|
|
827
|
+
* Processes a collected array iteration (method callbacks or for...of) into element usage.
|
|
828
|
+
* @param {object} iteration - Collected iteration info.
|
|
829
|
+
* @returns {object|true|null} Element usage tree, true (whole), or null (unanalyzable).
|
|
830
|
+
*/
|
|
831
|
+
function processArrayIteration(iteration) {
|
|
832
|
+
if (iteration.callbacks) {
|
|
833
|
+
let merged = {};
|
|
834
|
+
|
|
835
|
+
for (const { param, callbackPath } of iteration.callbacks) {
|
|
836
|
+
const usage = analyzeCallbackParam(param, callbackPath);
|
|
837
|
+
|
|
838
|
+
if (usage === null) return null;
|
|
839
|
+
if (usage === true) return true;
|
|
840
|
+
|
|
841
|
+
merged = mergeUsageTrees(merged, usage);
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
return merged;
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
if (iteration.loopVar) {
|
|
848
|
+
const bodyStart = iteration.scopePath.node.body?.start ?? 0;
|
|
849
|
+
|
|
850
|
+
if (iteration.loopVar.type === "identifier") {
|
|
851
|
+
const usage = trackVariableAccess(iteration.loopVar.name, iteration.scopePath, bodyStart);
|
|
852
|
+
return usage === false ? {} : usage;
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
if (iteration.loopVar.type === "pattern") {
|
|
856
|
+
return buildNestedDestructureTree(iteration.loopVar.pattern);
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
return null;
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
/**
|
|
864
|
+
* Analyzes a callback parameter to extract element property usage.
|
|
865
|
+
* @param {object} param - Callback parameter AST node.
|
|
866
|
+
* @param {import("@babel/traverse").NodePath} callbackPath - Callback function path.
|
|
867
|
+
* @returns {object|true|null} Usage tree, true (whole), or null (unanalyzable).
|
|
868
|
+
*/
|
|
869
|
+
function analyzeCallbackParam(param, callbackPath) {
|
|
870
|
+
if (param.type === "ObjectPattern") {
|
|
871
|
+
return buildNestedDestructureTree(param);
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
if (param.type === "Identifier") {
|
|
875
|
+
const bodyStart = callbackPath.node.body?.start ?? 0;
|
|
876
|
+
const usage = trackVariableAccess(param.name, callbackPath, bodyStart);
|
|
877
|
+
return usage === false ? {} : usage;
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
if (param.type === "AssignmentPattern") {
|
|
881
|
+
const left = param.left;
|
|
882
|
+
|
|
883
|
+
if (left.type === "ObjectPattern") return buildNestedDestructureTree(left);
|
|
884
|
+
|
|
885
|
+
if (left.type === "Identifier") {
|
|
886
|
+
const bodyStart = callbackPath.node.body?.start ?? 0;
|
|
887
|
+
const usage = trackVariableAccess(left.name, callbackPath, bodyStart);
|
|
888
|
+
return usage === false ? {} : usage;
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
return null;
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
/**
|
|
896
|
+
* Merges array element usage into the tree at the correct position.
|
|
897
|
+
* @param {object} tree - Usage tree to merge into.
|
|
898
|
+
* @param {string[]} prefixChain - Chain of property names leading to the array.
|
|
899
|
+
* @param {object|true} elementUsage - Element usage tree.
|
|
900
|
+
*/
|
|
901
|
+
function mergeArrayUsage(tree, prefixChain, elementUsage) {
|
|
902
|
+
let current = tree;
|
|
903
|
+
|
|
904
|
+
for (const key of prefixChain) {
|
|
905
|
+
if (current[key] === true) return;
|
|
906
|
+
|
|
907
|
+
if (!current[key] || typeof current[key] !== "object") {
|
|
908
|
+
current[key] = {};
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
current = current[key];
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
if (current["[]"]) {
|
|
915
|
+
current["[]"] = mergeUsageTrees(current["[]"], elementUsage);
|
|
916
|
+
}
|
|
917
|
+
else {
|
|
918
|
+
current["[]"] = elementUsage;
|
|
919
|
+
}
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
/**
|
|
923
|
+
* Collects property names from a MemberExpression chain.
|
|
924
|
+
* admin.mfa.status → ["mfa", "status"]
|
|
925
|
+
* @param {import("@babel/traverse").NodePath} memberPath
|
|
926
|
+
* @returns {string[]}
|
|
927
|
+
*/
|
|
928
|
+
function collectMemberChain(memberPath) {
|
|
929
|
+
const chain = [];
|
|
930
|
+
let current = memberPath;
|
|
931
|
+
|
|
932
|
+
while ((current.isMemberExpression() || current.isOptionalMemberExpression()) && !current.node.computed) {
|
|
933
|
+
const propName = current.node.property.name || current.node.property.value;
|
|
934
|
+
if (!propName) break;
|
|
935
|
+
|
|
936
|
+
chain.push(propName);
|
|
937
|
+
|
|
938
|
+
const parentIsMember = current.parentPath.isMemberExpression() || current.parentPath.isOptionalMemberExpression();
|
|
939
|
+
|
|
940
|
+
if (parentIsMember &&
|
|
941
|
+
current.parentPath.node.object === current.node &&
|
|
942
|
+
!current.parentPath.node.computed) {
|
|
943
|
+
current = current.parentPath;
|
|
944
|
+
}
|
|
945
|
+
else {
|
|
946
|
+
break;
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
return chain;
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
/**
|
|
954
|
+
* Walks up to the outermost MemberExpression in a chain.
|
|
955
|
+
* @param {import("@babel/traverse").NodePath} memberPath
|
|
956
|
+
* @returns {import("@babel/traverse").NodePath}
|
|
957
|
+
*/
|
|
958
|
+
function getOutermostMemberExpr(memberPath) {
|
|
959
|
+
let current = memberPath;
|
|
960
|
+
|
|
961
|
+
while ((current.parentPath.isMemberExpression() || current.parentPath.isOptionalMemberExpression()) &&
|
|
962
|
+
current.parentPath.node.object === current.node) {
|
|
963
|
+
current = current.parentPath;
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
return current;
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
/**
|
|
970
|
+
* Analyzes props accessed via identifier pattern: (props) => props.name
|
|
971
|
+
* @param {string} propsName - The parameter name (e.g., "props").
|
|
972
|
+
* @param {import("@babel/traverse").NodePath} funcPath
|
|
973
|
+
* @returns {object|null}
|
|
974
|
+
*/
|
|
975
|
+
function analyzePropsIdentifier(propsName, funcPath) {
|
|
976
|
+
let usedAsWhole = false;
|
|
977
|
+
const chains = [];
|
|
978
|
+
const arrayIterations = [];
|
|
979
|
+
const bodyStart = funcPath.node.body?.start ?? 0;
|
|
980
|
+
|
|
981
|
+
funcPath.traverse({
|
|
982
|
+
Identifier(idPath) {
|
|
983
|
+
if (idPath.node.start < bodyStart) return;
|
|
984
|
+
if (idPath.node.name !== propsName) return;
|
|
985
|
+
if (isInTypeAnnotation(idPath)) return;
|
|
986
|
+
|
|
987
|
+
// Skip property keys
|
|
988
|
+
if (idPath.parentPath.isObjectProperty() &&
|
|
989
|
+
idPath.parentPath.node.key === idPath.node &&
|
|
990
|
+
!idPath.parentPath.node.computed) {
|
|
991
|
+
return;
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
// props.X access (including optional chaining props?.X)
|
|
995
|
+
const isPropsMember = idPath.parentPath.isMemberExpression() || idPath.parentPath.isOptionalMemberExpression();
|
|
996
|
+
|
|
997
|
+
if (isPropsMember &&
|
|
998
|
+
idPath.parentPath.node.object === idPath.node &&
|
|
999
|
+
!idPath.parentPath.node.computed) {
|
|
1000
|
+
|
|
1001
|
+
const chain = collectMemberChain(idPath.parentPath);
|
|
1002
|
+
const outermost = getOutermostMemberExpr(idPath.parentPath);
|
|
1003
|
+
|
|
1004
|
+
const isCallCallee = (outermost.parentPath.isCallExpression() || outermost.parentPath.isOptionalCallExpression()) &&
|
|
1005
|
+
outermost.parentPath.node.callee === outermost.node;
|
|
1006
|
+
|
|
1007
|
+
if (isCallCallee) {
|
|
1008
|
+
const methodName = chain[chain.length - 1];
|
|
1009
|
+
|
|
1010
|
+
if (ARRAY_METHODS.has(methodName)) {
|
|
1011
|
+
const prefixChain = chain.slice(0, -1);
|
|
1012
|
+
const callbacks = collectArrayCallbacks(outermost.parentPath, methodName);
|
|
1013
|
+
|
|
1014
|
+
if (callbacks.length > 0) {
|
|
1015
|
+
arrayIterations.push({ prefixChain, callbacks });
|
|
1016
|
+
return;
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
usedAsWhole = true;
|
|
1020
|
+
return;
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
chain.pop();
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
if (chain.length === 0) {
|
|
1027
|
+
usedAsWhole = true;
|
|
1028
|
+
}
|
|
1029
|
+
else {
|
|
1030
|
+
chains.push(chain);
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
return;
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
// props used as a whole (destructured in body, passed as arg, etc.)
|
|
1037
|
+
usedAsWhole = true;
|
|
1038
|
+
}
|
|
1039
|
+
});
|
|
1040
|
+
|
|
1041
|
+
if (usedAsWhole) return null;
|
|
1042
|
+
if (chains.length === 0 && arrayIterations.length === 0) return {};
|
|
1043
|
+
|
|
1044
|
+
const tree = chains.length > 0 ? buildTreeFromChains(chains) : {};
|
|
1045
|
+
|
|
1046
|
+
for (const iteration of arrayIterations) {
|
|
1047
|
+
const elementUsage = processArrayIteration(iteration);
|
|
1048
|
+
|
|
1049
|
+
if (elementUsage === null) return null;
|
|
1050
|
+
|
|
1051
|
+
mergeArrayUsage(tree, iteration.prefixChain, elementUsage);
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
return Object.keys(tree).length > 0 ? tree : {};
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
/**
|
|
1058
|
+
* Checks if an identifier is used in a guard/truthy check position.
|
|
1059
|
+
* These patterns test existence without reading object contents:
|
|
1060
|
+
* - data && <jsx> (LogicalExpression left operand)
|
|
1061
|
+
* - data ? <a> : <b> (ConditionalExpression test)
|
|
1062
|
+
* - !data (UnaryExpression !)
|
|
1063
|
+
* - if (data) { ... } (IfStatement test)
|
|
1064
|
+
*
|
|
1065
|
+
* @param {import("@babel/traverse").NodePath} idPath
|
|
1066
|
+
* @returns {boolean}
|
|
1067
|
+
*/
|
|
1068
|
+
function isGuardCheck(idPath) {
|
|
1069
|
+
const parent = idPath.parentPath;
|
|
1070
|
+
const node = parent.node;
|
|
1071
|
+
|
|
1072
|
+
// data && ..., data || ..., data ?? ...
|
|
1073
|
+
if (parent.isLogicalExpression() && node.left === idPath.node) return true;
|
|
1074
|
+
|
|
1075
|
+
// data ? ... : ...
|
|
1076
|
+
if (parent.isConditionalExpression() && node.test === idPath.node) return true;
|
|
1077
|
+
|
|
1078
|
+
// !data
|
|
1079
|
+
if (parent.isUnaryExpression() && node.operator === "!") return true;
|
|
1080
|
+
|
|
1081
|
+
// if (data) { ... }
|
|
1082
|
+
if (parent.isIfStatement() && node.test === idPath.node) return true;
|
|
1083
|
+
|
|
1084
|
+
return false;
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
/**
|
|
1088
|
+
* Checks if a path is inside a TypeScript type annotation.
|
|
1089
|
+
* @param {import("@babel/traverse").NodePath} idPath
|
|
1090
|
+
* @returns {boolean}
|
|
1091
|
+
*/
|
|
1092
|
+
function isInTypeAnnotation(idPath) {
|
|
1093
|
+
let current = idPath.parentPath;
|
|
1094
|
+
|
|
1095
|
+
while (current) {
|
|
1096
|
+
const type = current.node?.type;
|
|
1097
|
+
if (!type) break;
|
|
1098
|
+
|
|
1099
|
+
if (type.startsWith("TS") || type === "TypeAnnotation") return true;
|
|
1100
|
+
if (current.isStatement() || current.isFunction() || current.isProgram()) break;
|
|
1101
|
+
|
|
1102
|
+
current = current.parentPath;
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
return false;
|
|
1106
|
+
}
|
|
1107
|
+
|
|
1108
|
+
/**
|
|
1109
|
+
* Builds a usage tree from an array of property access chains.
|
|
1110
|
+
*
|
|
1111
|
+
* [["admin", "name"], ["admin", "mfa", "status"], ["title"]]
|
|
1112
|
+
* → { admin: { name: true, mfa: { status: true } }, title: true }
|
|
1113
|
+
*
|
|
1114
|
+
* @param {string[][]} chains
|
|
1115
|
+
* @returns {object}
|
|
1116
|
+
*/
|
|
1117
|
+
function buildTreeFromChains(chains) {
|
|
1118
|
+
const tree = {};
|
|
1119
|
+
|
|
1120
|
+
for (const chain of chains) {
|
|
1121
|
+
let current = tree;
|
|
1122
|
+
|
|
1123
|
+
for (let i = 0; i < chain.length; i++) {
|
|
1124
|
+
const key = chain[i];
|
|
1125
|
+
|
|
1126
|
+
if (i === chain.length - 1) {
|
|
1127
|
+
// Last element — mark as used
|
|
1128
|
+
if (typeof current[key] === "object") {
|
|
1129
|
+
// Was a sub-tree, but also accessed directly → pass whole value
|
|
1130
|
+
current[key] = true;
|
|
1131
|
+
}
|
|
1132
|
+
else {
|
|
1133
|
+
current[key] = true;
|
|
1134
|
+
}
|
|
1135
|
+
}
|
|
1136
|
+
else {
|
|
1137
|
+
// Intermediate — needs to be an object for deeper access
|
|
1138
|
+
if (current[key] === true) {
|
|
1139
|
+
break; // Already full usage, don't downgrade
|
|
1140
|
+
}
|
|
1141
|
+
|
|
1142
|
+
if (!current[key] || typeof current[key] !== "object") {
|
|
1143
|
+
current[key] = {};
|
|
1144
|
+
}
|
|
1145
|
+
|
|
1146
|
+
current = current[key];
|
|
1147
|
+
}
|
|
1148
|
+
}
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1151
|
+
return tree;
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1154
|
+
/**
|
|
1155
|
+
* Extracts translation keys from __() and lang() calls in the AST.
|
|
1156
|
+
* Returns an array of string literal keys, or null if any dynamic key is found.
|
|
1157
|
+
* @param {object} ast - Babel AST.
|
|
1158
|
+
* @returns {string[]|null}
|
|
1159
|
+
*/
|
|
1160
|
+
function extractTranslationKeys(ast) {
|
|
1161
|
+
const keys = new Set();
|
|
1162
|
+
let hasDynamic = false;
|
|
1163
|
+
|
|
1164
|
+
_traverse(ast, {
|
|
1165
|
+
CallExpression(p) {
|
|
1166
|
+
const callee = p.node.callee;
|
|
1167
|
+
|
|
1168
|
+
if (callee.type !== "Identifier" || (callee.name !== "__" && callee.name !== "lang")) return;
|
|
1169
|
+
|
|
1170
|
+
const firstArg = p.node.arguments[0];
|
|
1171
|
+
|
|
1172
|
+
if (!firstArg) return;
|
|
1173
|
+
|
|
1174
|
+
if (firstArg.type === "StringLiteral") {
|
|
1175
|
+
keys.add(firstArg.value);
|
|
1176
|
+
}
|
|
1177
|
+
else {
|
|
1178
|
+
hasDynamic = true;
|
|
1179
|
+
}
|
|
1180
|
+
}
|
|
1181
|
+
});
|
|
1182
|
+
|
|
1183
|
+
if (hasDynamic) return null;
|
|
1184
|
+
if (keys.size === 0) return [];
|
|
1185
|
+
|
|
1186
|
+
return [...keys];
|
|
1187
|
+
}
|
|
1188
|
+
|
|
1189
|
+
export default PropUsageAnalyzer;
|