i18next-cli 1.49.0 → 1.49.1
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/dist/cjs/cli.js +1 -1
- package/dist/cjs/extractor/core/ast-visitors.js +143 -19
- package/dist/cjs/extractor/parsers/expression-resolver.js +135 -2
- package/dist/esm/cli.js +1 -1
- package/dist/esm/extractor/core/ast-visitors.js +143 -19
- package/dist/esm/extractor/parsers/expression-resolver.js +135 -2
- package/package.json +1 -1
- package/types/extractor/core/ast-visitors.d.ts +6 -0
- package/types/extractor/core/ast-visitors.d.ts.map +1 -1
- package/types/extractor/parsers/expression-resolver.d.ts +18 -0
- package/types/extractor/parsers/expression-resolver.d.ts.map +1 -1
package/dist/cjs/cli.js
CHANGED
|
@@ -31,7 +31,7 @@ const program = new commander.Command();
|
|
|
31
31
|
program
|
|
32
32
|
.name('i18next-cli')
|
|
33
33
|
.description('A unified, high-performance i18next CLI.')
|
|
34
|
-
.version('1.49.
|
|
34
|
+
.version('1.49.1'); // This string is replaced with the actual version at build time by rollup
|
|
35
35
|
// new: global config override option
|
|
36
36
|
program.option('-c, --config <path>', 'Path to i18next-cli config file (overrides detection)');
|
|
37
37
|
program
|
|
@@ -301,6 +301,18 @@ class ASTVisitors {
|
|
|
301
301
|
}
|
|
302
302
|
this.hooks.onAfterVisitNode?.(node);
|
|
303
303
|
// --- END VISIT LOGIC ---
|
|
304
|
+
// Detect array iteration calls (.map / .forEach / .flatMap etc.) on a known
|
|
305
|
+
// as-const array so the callback parameter is bound to the array values while
|
|
306
|
+
// the callback body is walked. We inject the binding BEFORE generic recursion
|
|
307
|
+
// and remove it AFTER, so the whole subtree sees the correct value.
|
|
308
|
+
let arrayCallbackCleanup;
|
|
309
|
+
if (node.type === 'CallExpression') {
|
|
310
|
+
const info = this.tryGetArrayIterationCallbackInfo(node);
|
|
311
|
+
if (info) {
|
|
312
|
+
this.expressionResolver.setTemporaryVariable(info.paramName, info.values);
|
|
313
|
+
arrayCallbackCleanup = () => this.expressionResolver.deleteTemporaryVariable(info.paramName);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
304
316
|
// --- RECURSION ---
|
|
305
317
|
// Recurse into the children of the current node
|
|
306
318
|
for (const key in node) {
|
|
@@ -308,49 +320,112 @@ class ASTVisitors {
|
|
|
308
320
|
continue;
|
|
309
321
|
const child = node[key];
|
|
310
322
|
if (Array.isArray(child)) {
|
|
311
|
-
// Pre-scan array children
|
|
312
|
-
//
|
|
313
|
-
//
|
|
314
|
-
//
|
|
315
|
-
//
|
|
323
|
+
// Pre-scan array children in THREE passes:
|
|
324
|
+
// Pass 1 — variables WITH init (arrays, objects, strings, fns) + enums
|
|
325
|
+
// Pass 2 — type aliases + functions (may depend on pass-1 arrays)
|
|
326
|
+
// Pass 3 — `declare const x: Type` (no init; depends on pass-2 type aliases)
|
|
327
|
+
// This ordering ensures e.g.:
|
|
328
|
+
// const OPTS = ['a','b'] as const → pass 1
|
|
329
|
+
// type T = (typeof OPTS)[number] → pass 2 (resolves OPTS)
|
|
330
|
+
// declare const v: T → pass 3 (resolves T)
|
|
331
|
+
// ── Pass 1: variables with init ──────────────────────────────────────
|
|
316
332
|
for (const item of child) {
|
|
317
333
|
if (!item || typeof item !== 'object')
|
|
318
334
|
continue;
|
|
319
|
-
// Direct declarator
|
|
320
|
-
if (item.type === 'VariableDeclarator') {
|
|
335
|
+
// Direct declarator (rare)
|
|
336
|
+
if (item.type === 'VariableDeclarator' && item.init) {
|
|
321
337
|
this.scopeManager.handleVariableDeclarator(item);
|
|
322
338
|
this.expressionResolver.captureVariableDeclarator(item);
|
|
323
339
|
continue;
|
|
324
340
|
}
|
|
325
|
-
// enum declarations
|
|
326
|
-
if (item
|
|
341
|
+
// enum declarations
|
|
342
|
+
if (item.id && Array.isArray(item.members)) {
|
|
327
343
|
this.expressionResolver.captureEnumDeclaration(item);
|
|
328
|
-
// continue to allow further traversal
|
|
329
344
|
}
|
|
330
|
-
//
|
|
345
|
+
// Bare VariableDeclaration — only declarators that have an init
|
|
346
|
+
if (item.type === 'VariableDeclaration' && Array.isArray(item.declarations)) {
|
|
347
|
+
for (const decl of item.declarations) {
|
|
348
|
+
if (decl?.type === 'VariableDeclarator' && decl.init) {
|
|
349
|
+
this.scopeManager.handleVariableDeclarator(decl);
|
|
350
|
+
this.expressionResolver.captureVariableDeclarator(decl);
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
// ExportDeclaration wrapping VariableDeclaration — only inited declarators
|
|
355
|
+
if ((item.type === 'ExportDeclaration' || item.type === 'ExportNamedDeclaration') && item.declaration) {
|
|
356
|
+
const inner = item.declaration;
|
|
357
|
+
if (inner.type === 'VariableDeclaration' && Array.isArray(inner.declarations)) {
|
|
358
|
+
for (const vd of inner.declarations) {
|
|
359
|
+
if (vd?.type === 'VariableDeclarator' && vd.init) {
|
|
360
|
+
this.scopeManager.handleVariableDeclarator(vd);
|
|
361
|
+
this.expressionResolver.captureVariableDeclarator(vd);
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
// ── Pass 2: type aliases + functions ─────────────────────────────────
|
|
368
|
+
for (const item of child) {
|
|
369
|
+
if (!item || typeof item !== 'object')
|
|
370
|
+
continue;
|
|
331
371
|
if (item.type === 'TsTypeAliasDeclaration' || item.type === 'TSTypeAliasDeclaration' || item.type === 'TsTypeAliasDecl') {
|
|
332
372
|
this.expressionResolver.captureTypeAliasDeclaration(item);
|
|
333
373
|
}
|
|
334
374
|
if (item.type === 'FunctionDeclaration' || item.type === 'FnDecl') {
|
|
335
375
|
this.expressionResolver.captureFunctionDeclaration(item);
|
|
336
376
|
}
|
|
337
|
-
// Also handle ExportDeclaration wrapping either of the above
|
|
338
377
|
if ((item.type === 'ExportDeclaration' || item.type === 'ExportNamedDeclaration') && item.declaration) {
|
|
339
|
-
const
|
|
340
|
-
if (
|
|
341
|
-
this.expressionResolver.captureTypeAliasDeclaration(
|
|
378
|
+
const inner = item.declaration;
|
|
379
|
+
if (inner.type === 'TsTypeAliasDeclaration' || inner.type === 'TSTypeAliasDeclaration' || inner.type === 'TsTypeAliasDecl') {
|
|
380
|
+
this.expressionResolver.captureTypeAliasDeclaration(inner);
|
|
342
381
|
}
|
|
343
|
-
if (
|
|
344
|
-
this.expressionResolver.captureFunctionDeclaration(
|
|
382
|
+
if (inner.type === 'FunctionDeclaration' || inner.type === 'FnDecl') {
|
|
383
|
+
this.expressionResolver.captureFunctionDeclaration(inner);
|
|
345
384
|
}
|
|
346
385
|
}
|
|
347
|
-
|
|
386
|
+
}
|
|
387
|
+
// ── Pass 3: `declare const x: Type` — no init, depends on type aliases ─
|
|
388
|
+
// Also re-processes ArrayPattern destructuring (e.g. useState<T>) whose
|
|
389
|
+
// type argument resolution failed in Pass 1 because typeAliasTable was empty.
|
|
390
|
+
for (const item of child) {
|
|
391
|
+
if (!item || typeof item !== 'object')
|
|
392
|
+
continue;
|
|
393
|
+
// Direct declarator with no init
|
|
394
|
+
if (item.type === 'VariableDeclarator' && !item.init) {
|
|
395
|
+
this.scopeManager.handleVariableDeclarator(item);
|
|
396
|
+
this.expressionResolver.captureVariableDeclarator(item);
|
|
397
|
+
continue;
|
|
398
|
+
}
|
|
399
|
+
// ArrayPattern destructuring with init — re-run now that type aliases are populated
|
|
400
|
+
if (item.type === 'VariableDeclarator' && item.init && item.id?.type === 'ArrayPattern') {
|
|
401
|
+
this.expressionResolver.captureVariableDeclarator(item);
|
|
402
|
+
continue;
|
|
403
|
+
}
|
|
404
|
+
// VariableDeclaration — process no-init declarators and re-process ArrayPattern ones
|
|
348
405
|
if (item.type === 'VariableDeclaration' && Array.isArray(item.declarations)) {
|
|
349
406
|
for (const decl of item.declarations) {
|
|
350
|
-
if (decl
|
|
407
|
+
if (!decl.init) {
|
|
351
408
|
this.scopeManager.handleVariableDeclarator(decl);
|
|
352
409
|
this.expressionResolver.captureVariableDeclarator(decl);
|
|
353
410
|
}
|
|
411
|
+
else if (decl.id?.type === 'ArrayPattern') {
|
|
412
|
+
this.expressionResolver.captureVariableDeclarator(decl);
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
// ExportDeclaration wrapping — same logic
|
|
417
|
+
if ((item.type === 'ExportDeclaration' || item.type === 'ExportNamedDeclaration') && item.declaration) {
|
|
418
|
+
const inner = item.declaration;
|
|
419
|
+
if (inner.type === 'VariableDeclaration' && Array.isArray(inner.declarations)) {
|
|
420
|
+
for (const vd of inner.declarations) {
|
|
421
|
+
if (!vd.init) {
|
|
422
|
+
this.scopeManager.handleVariableDeclarator(vd);
|
|
423
|
+
this.expressionResolver.captureVariableDeclarator(vd);
|
|
424
|
+
}
|
|
425
|
+
else if (vd.id?.type === 'ArrayPattern') {
|
|
426
|
+
this.expressionResolver.captureVariableDeclarator(vd);
|
|
427
|
+
}
|
|
428
|
+
}
|
|
354
429
|
}
|
|
355
430
|
}
|
|
356
431
|
}
|
|
@@ -370,11 +445,60 @@ class ASTVisitors {
|
|
|
370
445
|
}
|
|
371
446
|
}
|
|
372
447
|
// --- END RECURSION ---
|
|
448
|
+
// Remove temporary callback param binding if one was injected for this node
|
|
449
|
+
arrayCallbackCleanup?.();
|
|
373
450
|
// LEAVE SCOPE for functions
|
|
374
451
|
if (isNewScope) {
|
|
375
452
|
this.scopeManager.exitScope();
|
|
376
453
|
}
|
|
377
454
|
}
|
|
455
|
+
/**
|
|
456
|
+
* If `node` is a call like `ARRAY.map(param => ...)` where ARRAY is a known
|
|
457
|
+
* string-array constant, returns the callback's first parameter name and the
|
|
458
|
+
* array values so the caller can inject a temporary variable binding.
|
|
459
|
+
*/
|
|
460
|
+
tryGetArrayIterationCallbackInfo(node) {
|
|
461
|
+
try {
|
|
462
|
+
const callee = node.callee;
|
|
463
|
+
if (callee?.type !== 'MemberExpression')
|
|
464
|
+
return undefined;
|
|
465
|
+
const prop = callee.property;
|
|
466
|
+
if (prop?.type !== 'Identifier')
|
|
467
|
+
return undefined;
|
|
468
|
+
if (!['map', 'forEach', 'flatMap', 'filter', 'find', 'some', 'every'].includes(prop.value))
|
|
469
|
+
return undefined;
|
|
470
|
+
// The object must be an identifier whose value is a known string array
|
|
471
|
+
const obj = callee.object;
|
|
472
|
+
if (obj?.type !== 'Identifier')
|
|
473
|
+
return undefined;
|
|
474
|
+
const values = this.expressionResolver.getVariableValues(obj.value);
|
|
475
|
+
if (!values || values.length === 0)
|
|
476
|
+
return undefined;
|
|
477
|
+
// First argument must be a callback with at least one parameter
|
|
478
|
+
const callbackArg = node.arguments?.[0]?.expression;
|
|
479
|
+
if (!callbackArg)
|
|
480
|
+
return undefined;
|
|
481
|
+
// Normalise param across SWC shapes: ArrowFunctionExpression / FunctionExpression
|
|
482
|
+
const params = callbackArg.params ?? callbackArg.parameters ?? [];
|
|
483
|
+
const firstParam = params[0];
|
|
484
|
+
if (!firstParam)
|
|
485
|
+
return undefined;
|
|
486
|
+
// SWC wraps params in `Param { pat: Identifier }` or exposes them directly
|
|
487
|
+
const ident = firstParam.type === 'Identifier'
|
|
488
|
+
? firstParam
|
|
489
|
+
: firstParam.type === 'Param' && firstParam.pat?.type === 'Identifier'
|
|
490
|
+
? firstParam.pat
|
|
491
|
+
: firstParam.type === 'AssignmentPattern' && firstParam.left?.type === 'Identifier'
|
|
492
|
+
? firstParam.left
|
|
493
|
+
: null;
|
|
494
|
+
if (!ident)
|
|
495
|
+
return undefined;
|
|
496
|
+
return { paramName: ident.value, values };
|
|
497
|
+
}
|
|
498
|
+
catch {
|
|
499
|
+
return undefined;
|
|
500
|
+
}
|
|
501
|
+
}
|
|
378
502
|
/**
|
|
379
503
|
* Retrieves variable information from the scope chain.
|
|
380
504
|
* Searches from innermost to outermost scope.
|
|
@@ -12,6 +12,15 @@ class ExpressionResolver {
|
|
|
12
12
|
// Per-file table for type aliases: Maps typeName -> string[]
|
|
13
13
|
// e.g. `type ChangeType = 'all' | 'next' | 'this'` -> { ChangeType: ['all', 'next', 'this'] }
|
|
14
14
|
typeAliasTable = new Map();
|
|
15
|
+
// Shared (cross-file) table for string-array constants (e.g. `as const` arrays).
|
|
16
|
+
// Persists across resetFileSymbols() so exported arrays are visible to importers.
|
|
17
|
+
sharedVariableTable = new Map();
|
|
18
|
+
// Shared (cross-file) table for type aliases — populated alongside typeAliasTable.
|
|
19
|
+
// Persists across resetFileSymbols() so exported type aliases are visible to importers.
|
|
20
|
+
sharedTypeAliasTable = new Map();
|
|
21
|
+
// Temporary per-scope variable overrides, used to inject .map() / .forEach()
|
|
22
|
+
// callback parameters while the callback body is being walked.
|
|
23
|
+
temporaryVariables = new Map();
|
|
15
24
|
constructor(hooks) {
|
|
16
25
|
this.hooks = hooks;
|
|
17
26
|
}
|
|
@@ -21,6 +30,7 @@ class ExpressionResolver {
|
|
|
21
30
|
resetFileSymbols() {
|
|
22
31
|
this.variableTable.clear();
|
|
23
32
|
this.typeAliasTable.clear();
|
|
33
|
+
this.temporaryVariables.clear();
|
|
24
34
|
}
|
|
25
35
|
/**
|
|
26
36
|
* Capture a VariableDeclarator node to record simple statically analyzable
|
|
@@ -36,6 +46,41 @@ class ExpressionResolver {
|
|
|
36
46
|
try {
|
|
37
47
|
if (!node || !node.id)
|
|
38
48
|
return;
|
|
49
|
+
// ── ArrayPattern id: `const [x, y] = fn<T>(...)` ────────────────────────
|
|
50
|
+
// Handles `const [state] = useState<'a'|'b'>('a')` or similar generic calls
|
|
51
|
+
// where the type argument is a finite string-literal union.
|
|
52
|
+
if (node.id.type === 'ArrayPattern' && node.init) {
|
|
53
|
+
const init = node.init;
|
|
54
|
+
// Unwrap await / as-expressions
|
|
55
|
+
let callExpr = init;
|
|
56
|
+
while (callExpr?.type === 'AwaitExpression')
|
|
57
|
+
callExpr = callExpr.argument;
|
|
58
|
+
while (callExpr?.type === 'TsConstAssertion' ||
|
|
59
|
+
callExpr?.type === 'TsAsExpression' ||
|
|
60
|
+
callExpr?.type === 'TsSatisfiesExpression')
|
|
61
|
+
callExpr = callExpr.expression;
|
|
62
|
+
if (callExpr?.type === 'CallExpression') {
|
|
63
|
+
const typeArgs = callExpr.typeArguments?.params ??
|
|
64
|
+
callExpr.typeParameters?.params ??
|
|
65
|
+
[];
|
|
66
|
+
if (typeArgs.length > 0) {
|
|
67
|
+
const vals = this.resolvePossibleStringValuesFromType(typeArgs[0]);
|
|
68
|
+
if (vals.length > 0) {
|
|
69
|
+
// Bind each array-pattern element: first element is the state variable
|
|
70
|
+
for (const el of node.id.elements) {
|
|
71
|
+
if (!el)
|
|
72
|
+
continue;
|
|
73
|
+
const ident = el.type === 'Identifier' ? el : (el.type === 'AssignmentPattern' && el.left?.type === 'Identifier' ? el.left : null);
|
|
74
|
+
if (ident) {
|
|
75
|
+
this.variableTable.set(ident.value, vals);
|
|
76
|
+
}
|
|
77
|
+
break; // only bind the first element (the state value, not the setter)
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
39
84
|
// only handle simple identifier bindings like `const x = ...`
|
|
40
85
|
if (node.id.type !== 'Identifier')
|
|
41
86
|
return;
|
|
@@ -86,6 +131,24 @@ class ExpressionResolver {
|
|
|
86
131
|
return;
|
|
87
132
|
}
|
|
88
133
|
}
|
|
134
|
+
// ArrayExpression -> list of string values
|
|
135
|
+
// Handles `const OPTS = ['a', 'b', 'c'] as const`
|
|
136
|
+
if (unwrappedInit.type === 'ArrayExpression' && Array.isArray(unwrappedInit.elements)) {
|
|
137
|
+
const vals = [];
|
|
138
|
+
for (const elem of unwrappedInit.elements) {
|
|
139
|
+
if (!elem || !elem.expression)
|
|
140
|
+
continue;
|
|
141
|
+
const resolved = this.resolvePossibleStringValuesFromExpression(elem.expression);
|
|
142
|
+
if (resolved.length === 1)
|
|
143
|
+
vals.push(resolved[0]);
|
|
144
|
+
}
|
|
145
|
+
if (vals.length > 0) {
|
|
146
|
+
this.variableTable.set(name, vals);
|
|
147
|
+
// Also share so importing files can see this array
|
|
148
|
+
this.sharedVariableTable.set(name, vals);
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
89
152
|
// For other initializers, try to resolve to one-or-more strings
|
|
90
153
|
const vals = this.resolvePossibleStringValuesFromExpression(init);
|
|
91
154
|
if (vals.length > 0) {
|
|
@@ -129,6 +192,8 @@ class ExpressionResolver {
|
|
|
129
192
|
const vals = this.resolvePossibleStringValuesFromType(tsType);
|
|
130
193
|
if (vals.length > 0) {
|
|
131
194
|
this.typeAliasTable.set(name, vals);
|
|
195
|
+
// Also share so importing files can resolve this alias by name
|
|
196
|
+
this.sharedTypeAliasTable.set(name, vals);
|
|
132
197
|
}
|
|
133
198
|
}
|
|
134
199
|
catch {
|
|
@@ -180,6 +245,33 @@ class ExpressionResolver {
|
|
|
180
245
|
return raw.typeAnnotation;
|
|
181
246
|
return raw;
|
|
182
247
|
}
|
|
248
|
+
/**
|
|
249
|
+
* Temporarily bind a variable name to a set of string values.
|
|
250
|
+
* Used by ast-visitors to inject .map()/.forEach() callback parameters.
|
|
251
|
+
* Call deleteTemporaryVariable() after walking the callback body.
|
|
252
|
+
*/
|
|
253
|
+
setTemporaryVariable(name, values) {
|
|
254
|
+
this.temporaryVariables.set(name, values);
|
|
255
|
+
}
|
|
256
|
+
/**
|
|
257
|
+
* Remove a previously-injected temporary variable binding.
|
|
258
|
+
*/
|
|
259
|
+
deleteTemporaryVariable(name) {
|
|
260
|
+
this.temporaryVariables.delete(name);
|
|
261
|
+
}
|
|
262
|
+
/**
|
|
263
|
+
* Return the array values stored for a variable name, checking all tables.
|
|
264
|
+
* Returns undefined if the name is not a known string array.
|
|
265
|
+
*/
|
|
266
|
+
getVariableValues(name) {
|
|
267
|
+
const tmp = this.temporaryVariables.get(name);
|
|
268
|
+
if (tmp)
|
|
269
|
+
return tmp;
|
|
270
|
+
const v = this.variableTable.get(name);
|
|
271
|
+
if (Array.isArray(v))
|
|
272
|
+
return v;
|
|
273
|
+
return this.sharedVariableTable.get(name);
|
|
274
|
+
}
|
|
183
275
|
/**
|
|
184
276
|
* Capture a TypeScript enum declaration so members can be resolved later.
|
|
185
277
|
* Accepts SWC node shapes like `TsEnumDeclaration` / `TSEnumDeclaration`.
|
|
@@ -421,9 +513,18 @@ class ExpressionResolver {
|
|
|
421
513
|
}
|
|
422
514
|
// Identifier resolution via captured per-file variable table only
|
|
423
515
|
if (expression.type === 'Identifier') {
|
|
516
|
+
// Check temporary (callback param) overrides first
|
|
517
|
+
const tmp = this.temporaryVariables.get(expression.value);
|
|
518
|
+
if (tmp)
|
|
519
|
+
return tmp;
|
|
424
520
|
const v = this.variableTable.get(expression.value);
|
|
425
|
-
if (!v)
|
|
521
|
+
if (!v) {
|
|
522
|
+
// Fall back to shared cross-file array table
|
|
523
|
+
const sv = this.sharedVariableTable.get(expression.value);
|
|
524
|
+
if (sv)
|
|
525
|
+
return sv;
|
|
426
526
|
return [];
|
|
527
|
+
}
|
|
427
528
|
if (Array.isArray(v))
|
|
428
529
|
return v;
|
|
429
530
|
// object map - cannot be used directly as key, so return empty
|
|
@@ -433,6 +534,11 @@ class ExpressionResolver {
|
|
|
433
534
|
return [];
|
|
434
535
|
}
|
|
435
536
|
resolvePossibleStringValuesFromType(type, returnEmptyStrings = false) {
|
|
537
|
+
// Unwrap TsParenthesizedType — SWC explicitly emits these for grouped types like
|
|
538
|
+
// `(typeof X)[number]` where `(typeof X)` becomes TsParenthesizedType { typeAnnotation: TsTypeQuery }
|
|
539
|
+
if (type.type === 'TsParenthesizedType') {
|
|
540
|
+
return this.resolvePossibleStringValuesFromType(type.typeAnnotation, returnEmptyStrings);
|
|
541
|
+
}
|
|
436
542
|
if (type.type === 'TsUnionType') {
|
|
437
543
|
return type.types.flatMap((t) => this.resolvePossibleStringValuesFromType(t, returnEmptyStrings));
|
|
438
544
|
}
|
|
@@ -456,11 +562,38 @@ class ExpressionResolver {
|
|
|
456
562
|
? type.typeName.value
|
|
457
563
|
: undefined;
|
|
458
564
|
if (typeName) {
|
|
459
|
-
const aliasVals = this.typeAliasTable.get(typeName);
|
|
565
|
+
const aliasVals = this.typeAliasTable.get(typeName) ?? this.sharedTypeAliasTable.get(typeName);
|
|
460
566
|
if (aliasVals && aliasVals.length > 0)
|
|
461
567
|
return aliasVals;
|
|
462
568
|
}
|
|
463
569
|
}
|
|
570
|
+
// `(typeof ACCESS_OPTIONS)[number]` — resolve through the shared array variable table.
|
|
571
|
+
// SWC emits: TsIndexedAccessType {
|
|
572
|
+
// objectType: TsParenthesizedType { typeAnnotation: TsTypeQuery { exprName: Identifier } }
|
|
573
|
+
// indexType: TsKeywordType
|
|
574
|
+
// }
|
|
575
|
+
// The parens around `typeof X` produce a TsParenthesizedType wrapper that we must unwrap.
|
|
576
|
+
if (type.type === 'TsIndexedAccessType') {
|
|
577
|
+
try {
|
|
578
|
+
let objType = type.objectType;
|
|
579
|
+
// Unwrap TsParenthesizedType wrapper (SWC preserves explicit parens in type positions)
|
|
580
|
+
while (objType?.type === 'TsParenthesizedType') {
|
|
581
|
+
objType = objType.typeAnnotation;
|
|
582
|
+
}
|
|
583
|
+
if (objType?.type === 'TsTypeQuery' || objType?.type === 'TSTypeQuery') {
|
|
584
|
+
// SWC: TsTypeQuery.exprName is TsEntityName (Identifier | TsQualifiedName)
|
|
585
|
+
const exprName = objType.exprName ?? objType.expr ?? objType.entityName;
|
|
586
|
+
// access .value (Identifier) or fall back to .name for alternate SWC builds
|
|
587
|
+
const varName = exprName?.value ?? exprName?.name;
|
|
588
|
+
if (varName) {
|
|
589
|
+
const vals = this.getVariableValues(varName);
|
|
590
|
+
if (vals && vals.length > 0)
|
|
591
|
+
return vals;
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
catch { }
|
|
596
|
+
}
|
|
464
597
|
// We can't statically determine the value of other expressions (e.g., variables, function calls)
|
|
465
598
|
return [];
|
|
466
599
|
}
|
package/dist/esm/cli.js
CHANGED
|
@@ -29,7 +29,7 @@ const program = new Command();
|
|
|
29
29
|
program
|
|
30
30
|
.name('i18next-cli')
|
|
31
31
|
.description('A unified, high-performance i18next CLI.')
|
|
32
|
-
.version('1.49.
|
|
32
|
+
.version('1.49.1'); // This string is replaced with the actual version at build time by rollup
|
|
33
33
|
// new: global config override option
|
|
34
34
|
program.option('-c, --config <path>', 'Path to i18next-cli config file (overrides detection)');
|
|
35
35
|
program
|
|
@@ -299,6 +299,18 @@ class ASTVisitors {
|
|
|
299
299
|
}
|
|
300
300
|
this.hooks.onAfterVisitNode?.(node);
|
|
301
301
|
// --- END VISIT LOGIC ---
|
|
302
|
+
// Detect array iteration calls (.map / .forEach / .flatMap etc.) on a known
|
|
303
|
+
// as-const array so the callback parameter is bound to the array values while
|
|
304
|
+
// the callback body is walked. We inject the binding BEFORE generic recursion
|
|
305
|
+
// and remove it AFTER, so the whole subtree sees the correct value.
|
|
306
|
+
let arrayCallbackCleanup;
|
|
307
|
+
if (node.type === 'CallExpression') {
|
|
308
|
+
const info = this.tryGetArrayIterationCallbackInfo(node);
|
|
309
|
+
if (info) {
|
|
310
|
+
this.expressionResolver.setTemporaryVariable(info.paramName, info.values);
|
|
311
|
+
arrayCallbackCleanup = () => this.expressionResolver.deleteTemporaryVariable(info.paramName);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
302
314
|
// --- RECURSION ---
|
|
303
315
|
// Recurse into the children of the current node
|
|
304
316
|
for (const key in node) {
|
|
@@ -306,49 +318,112 @@ class ASTVisitors {
|
|
|
306
318
|
continue;
|
|
307
319
|
const child = node[key];
|
|
308
320
|
if (Array.isArray(child)) {
|
|
309
|
-
// Pre-scan array children
|
|
310
|
-
//
|
|
311
|
-
//
|
|
312
|
-
//
|
|
313
|
-
//
|
|
321
|
+
// Pre-scan array children in THREE passes:
|
|
322
|
+
// Pass 1 — variables WITH init (arrays, objects, strings, fns) + enums
|
|
323
|
+
// Pass 2 — type aliases + functions (may depend on pass-1 arrays)
|
|
324
|
+
// Pass 3 — `declare const x: Type` (no init; depends on pass-2 type aliases)
|
|
325
|
+
// This ordering ensures e.g.:
|
|
326
|
+
// const OPTS = ['a','b'] as const → pass 1
|
|
327
|
+
// type T = (typeof OPTS)[number] → pass 2 (resolves OPTS)
|
|
328
|
+
// declare const v: T → pass 3 (resolves T)
|
|
329
|
+
// ── Pass 1: variables with init ──────────────────────────────────────
|
|
314
330
|
for (const item of child) {
|
|
315
331
|
if (!item || typeof item !== 'object')
|
|
316
332
|
continue;
|
|
317
|
-
// Direct declarator
|
|
318
|
-
if (item.type === 'VariableDeclarator') {
|
|
333
|
+
// Direct declarator (rare)
|
|
334
|
+
if (item.type === 'VariableDeclarator' && item.init) {
|
|
319
335
|
this.scopeManager.handleVariableDeclarator(item);
|
|
320
336
|
this.expressionResolver.captureVariableDeclarator(item);
|
|
321
337
|
continue;
|
|
322
338
|
}
|
|
323
|
-
// enum declarations
|
|
324
|
-
if (item
|
|
339
|
+
// enum declarations
|
|
340
|
+
if (item.id && Array.isArray(item.members)) {
|
|
325
341
|
this.expressionResolver.captureEnumDeclaration(item);
|
|
326
|
-
// continue to allow further traversal
|
|
327
342
|
}
|
|
328
|
-
//
|
|
343
|
+
// Bare VariableDeclaration — only declarators that have an init
|
|
344
|
+
if (item.type === 'VariableDeclaration' && Array.isArray(item.declarations)) {
|
|
345
|
+
for (const decl of item.declarations) {
|
|
346
|
+
if (decl?.type === 'VariableDeclarator' && decl.init) {
|
|
347
|
+
this.scopeManager.handleVariableDeclarator(decl);
|
|
348
|
+
this.expressionResolver.captureVariableDeclarator(decl);
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
// ExportDeclaration wrapping VariableDeclaration — only inited declarators
|
|
353
|
+
if ((item.type === 'ExportDeclaration' || item.type === 'ExportNamedDeclaration') && item.declaration) {
|
|
354
|
+
const inner = item.declaration;
|
|
355
|
+
if (inner.type === 'VariableDeclaration' && Array.isArray(inner.declarations)) {
|
|
356
|
+
for (const vd of inner.declarations) {
|
|
357
|
+
if (vd?.type === 'VariableDeclarator' && vd.init) {
|
|
358
|
+
this.scopeManager.handleVariableDeclarator(vd);
|
|
359
|
+
this.expressionResolver.captureVariableDeclarator(vd);
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
// ── Pass 2: type aliases + functions ─────────────────────────────────
|
|
366
|
+
for (const item of child) {
|
|
367
|
+
if (!item || typeof item !== 'object')
|
|
368
|
+
continue;
|
|
329
369
|
if (item.type === 'TsTypeAliasDeclaration' || item.type === 'TSTypeAliasDeclaration' || item.type === 'TsTypeAliasDecl') {
|
|
330
370
|
this.expressionResolver.captureTypeAliasDeclaration(item);
|
|
331
371
|
}
|
|
332
372
|
if (item.type === 'FunctionDeclaration' || item.type === 'FnDecl') {
|
|
333
373
|
this.expressionResolver.captureFunctionDeclaration(item);
|
|
334
374
|
}
|
|
335
|
-
// Also handle ExportDeclaration wrapping either of the above
|
|
336
375
|
if ((item.type === 'ExportDeclaration' || item.type === 'ExportNamedDeclaration') && item.declaration) {
|
|
337
|
-
const
|
|
338
|
-
if (
|
|
339
|
-
this.expressionResolver.captureTypeAliasDeclaration(
|
|
376
|
+
const inner = item.declaration;
|
|
377
|
+
if (inner.type === 'TsTypeAliasDeclaration' || inner.type === 'TSTypeAliasDeclaration' || inner.type === 'TsTypeAliasDecl') {
|
|
378
|
+
this.expressionResolver.captureTypeAliasDeclaration(inner);
|
|
340
379
|
}
|
|
341
|
-
if (
|
|
342
|
-
this.expressionResolver.captureFunctionDeclaration(
|
|
380
|
+
if (inner.type === 'FunctionDeclaration' || inner.type === 'FnDecl') {
|
|
381
|
+
this.expressionResolver.captureFunctionDeclaration(inner);
|
|
343
382
|
}
|
|
344
383
|
}
|
|
345
|
-
|
|
384
|
+
}
|
|
385
|
+
// ── Pass 3: `declare const x: Type` — no init, depends on type aliases ─
|
|
386
|
+
// Also re-processes ArrayPattern destructuring (e.g. useState<T>) whose
|
|
387
|
+
// type argument resolution failed in Pass 1 because typeAliasTable was empty.
|
|
388
|
+
for (const item of child) {
|
|
389
|
+
if (!item || typeof item !== 'object')
|
|
390
|
+
continue;
|
|
391
|
+
// Direct declarator with no init
|
|
392
|
+
if (item.type === 'VariableDeclarator' && !item.init) {
|
|
393
|
+
this.scopeManager.handleVariableDeclarator(item);
|
|
394
|
+
this.expressionResolver.captureVariableDeclarator(item);
|
|
395
|
+
continue;
|
|
396
|
+
}
|
|
397
|
+
// ArrayPattern destructuring with init — re-run now that type aliases are populated
|
|
398
|
+
if (item.type === 'VariableDeclarator' && item.init && item.id?.type === 'ArrayPattern') {
|
|
399
|
+
this.expressionResolver.captureVariableDeclarator(item);
|
|
400
|
+
continue;
|
|
401
|
+
}
|
|
402
|
+
// VariableDeclaration — process no-init declarators and re-process ArrayPattern ones
|
|
346
403
|
if (item.type === 'VariableDeclaration' && Array.isArray(item.declarations)) {
|
|
347
404
|
for (const decl of item.declarations) {
|
|
348
|
-
if (decl
|
|
405
|
+
if (!decl.init) {
|
|
349
406
|
this.scopeManager.handleVariableDeclarator(decl);
|
|
350
407
|
this.expressionResolver.captureVariableDeclarator(decl);
|
|
351
408
|
}
|
|
409
|
+
else if (decl.id?.type === 'ArrayPattern') {
|
|
410
|
+
this.expressionResolver.captureVariableDeclarator(decl);
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
// ExportDeclaration wrapping — same logic
|
|
415
|
+
if ((item.type === 'ExportDeclaration' || item.type === 'ExportNamedDeclaration') && item.declaration) {
|
|
416
|
+
const inner = item.declaration;
|
|
417
|
+
if (inner.type === 'VariableDeclaration' && Array.isArray(inner.declarations)) {
|
|
418
|
+
for (const vd of inner.declarations) {
|
|
419
|
+
if (!vd.init) {
|
|
420
|
+
this.scopeManager.handleVariableDeclarator(vd);
|
|
421
|
+
this.expressionResolver.captureVariableDeclarator(vd);
|
|
422
|
+
}
|
|
423
|
+
else if (vd.id?.type === 'ArrayPattern') {
|
|
424
|
+
this.expressionResolver.captureVariableDeclarator(vd);
|
|
425
|
+
}
|
|
426
|
+
}
|
|
352
427
|
}
|
|
353
428
|
}
|
|
354
429
|
}
|
|
@@ -368,11 +443,60 @@ class ASTVisitors {
|
|
|
368
443
|
}
|
|
369
444
|
}
|
|
370
445
|
// --- END RECURSION ---
|
|
446
|
+
// Remove temporary callback param binding if one was injected for this node
|
|
447
|
+
arrayCallbackCleanup?.();
|
|
371
448
|
// LEAVE SCOPE for functions
|
|
372
449
|
if (isNewScope) {
|
|
373
450
|
this.scopeManager.exitScope();
|
|
374
451
|
}
|
|
375
452
|
}
|
|
453
|
+
/**
|
|
454
|
+
* If `node` is a call like `ARRAY.map(param => ...)` where ARRAY is a known
|
|
455
|
+
* string-array constant, returns the callback's first parameter name and the
|
|
456
|
+
* array values so the caller can inject a temporary variable binding.
|
|
457
|
+
*/
|
|
458
|
+
tryGetArrayIterationCallbackInfo(node) {
|
|
459
|
+
try {
|
|
460
|
+
const callee = node.callee;
|
|
461
|
+
if (callee?.type !== 'MemberExpression')
|
|
462
|
+
return undefined;
|
|
463
|
+
const prop = callee.property;
|
|
464
|
+
if (prop?.type !== 'Identifier')
|
|
465
|
+
return undefined;
|
|
466
|
+
if (!['map', 'forEach', 'flatMap', 'filter', 'find', 'some', 'every'].includes(prop.value))
|
|
467
|
+
return undefined;
|
|
468
|
+
// The object must be an identifier whose value is a known string array
|
|
469
|
+
const obj = callee.object;
|
|
470
|
+
if (obj?.type !== 'Identifier')
|
|
471
|
+
return undefined;
|
|
472
|
+
const values = this.expressionResolver.getVariableValues(obj.value);
|
|
473
|
+
if (!values || values.length === 0)
|
|
474
|
+
return undefined;
|
|
475
|
+
// First argument must be a callback with at least one parameter
|
|
476
|
+
const callbackArg = node.arguments?.[0]?.expression;
|
|
477
|
+
if (!callbackArg)
|
|
478
|
+
return undefined;
|
|
479
|
+
// Normalise param across SWC shapes: ArrowFunctionExpression / FunctionExpression
|
|
480
|
+
const params = callbackArg.params ?? callbackArg.parameters ?? [];
|
|
481
|
+
const firstParam = params[0];
|
|
482
|
+
if (!firstParam)
|
|
483
|
+
return undefined;
|
|
484
|
+
// SWC wraps params in `Param { pat: Identifier }` or exposes them directly
|
|
485
|
+
const ident = firstParam.type === 'Identifier'
|
|
486
|
+
? firstParam
|
|
487
|
+
: firstParam.type === 'Param' && firstParam.pat?.type === 'Identifier'
|
|
488
|
+
? firstParam.pat
|
|
489
|
+
: firstParam.type === 'AssignmentPattern' && firstParam.left?.type === 'Identifier'
|
|
490
|
+
? firstParam.left
|
|
491
|
+
: null;
|
|
492
|
+
if (!ident)
|
|
493
|
+
return undefined;
|
|
494
|
+
return { paramName: ident.value, values };
|
|
495
|
+
}
|
|
496
|
+
catch {
|
|
497
|
+
return undefined;
|
|
498
|
+
}
|
|
499
|
+
}
|
|
376
500
|
/**
|
|
377
501
|
* Retrieves variable information from the scope chain.
|
|
378
502
|
* Searches from innermost to outermost scope.
|
|
@@ -10,6 +10,15 @@ class ExpressionResolver {
|
|
|
10
10
|
// Per-file table for type aliases: Maps typeName -> string[]
|
|
11
11
|
// e.g. `type ChangeType = 'all' | 'next' | 'this'` -> { ChangeType: ['all', 'next', 'this'] }
|
|
12
12
|
typeAliasTable = new Map();
|
|
13
|
+
// Shared (cross-file) table for string-array constants (e.g. `as const` arrays).
|
|
14
|
+
// Persists across resetFileSymbols() so exported arrays are visible to importers.
|
|
15
|
+
sharedVariableTable = new Map();
|
|
16
|
+
// Shared (cross-file) table for type aliases — populated alongside typeAliasTable.
|
|
17
|
+
// Persists across resetFileSymbols() so exported type aliases are visible to importers.
|
|
18
|
+
sharedTypeAliasTable = new Map();
|
|
19
|
+
// Temporary per-scope variable overrides, used to inject .map() / .forEach()
|
|
20
|
+
// callback parameters while the callback body is being walked.
|
|
21
|
+
temporaryVariables = new Map();
|
|
13
22
|
constructor(hooks) {
|
|
14
23
|
this.hooks = hooks;
|
|
15
24
|
}
|
|
@@ -19,6 +28,7 @@ class ExpressionResolver {
|
|
|
19
28
|
resetFileSymbols() {
|
|
20
29
|
this.variableTable.clear();
|
|
21
30
|
this.typeAliasTable.clear();
|
|
31
|
+
this.temporaryVariables.clear();
|
|
22
32
|
}
|
|
23
33
|
/**
|
|
24
34
|
* Capture a VariableDeclarator node to record simple statically analyzable
|
|
@@ -34,6 +44,41 @@ class ExpressionResolver {
|
|
|
34
44
|
try {
|
|
35
45
|
if (!node || !node.id)
|
|
36
46
|
return;
|
|
47
|
+
// ── ArrayPattern id: `const [x, y] = fn<T>(...)` ────────────────────────
|
|
48
|
+
// Handles `const [state] = useState<'a'|'b'>('a')` or similar generic calls
|
|
49
|
+
// where the type argument is a finite string-literal union.
|
|
50
|
+
if (node.id.type === 'ArrayPattern' && node.init) {
|
|
51
|
+
const init = node.init;
|
|
52
|
+
// Unwrap await / as-expressions
|
|
53
|
+
let callExpr = init;
|
|
54
|
+
while (callExpr?.type === 'AwaitExpression')
|
|
55
|
+
callExpr = callExpr.argument;
|
|
56
|
+
while (callExpr?.type === 'TsConstAssertion' ||
|
|
57
|
+
callExpr?.type === 'TsAsExpression' ||
|
|
58
|
+
callExpr?.type === 'TsSatisfiesExpression')
|
|
59
|
+
callExpr = callExpr.expression;
|
|
60
|
+
if (callExpr?.type === 'CallExpression') {
|
|
61
|
+
const typeArgs = callExpr.typeArguments?.params ??
|
|
62
|
+
callExpr.typeParameters?.params ??
|
|
63
|
+
[];
|
|
64
|
+
if (typeArgs.length > 0) {
|
|
65
|
+
const vals = this.resolvePossibleStringValuesFromType(typeArgs[0]);
|
|
66
|
+
if (vals.length > 0) {
|
|
67
|
+
// Bind each array-pattern element: first element is the state variable
|
|
68
|
+
for (const el of node.id.elements) {
|
|
69
|
+
if (!el)
|
|
70
|
+
continue;
|
|
71
|
+
const ident = el.type === 'Identifier' ? el : (el.type === 'AssignmentPattern' && el.left?.type === 'Identifier' ? el.left : null);
|
|
72
|
+
if (ident) {
|
|
73
|
+
this.variableTable.set(ident.value, vals);
|
|
74
|
+
}
|
|
75
|
+
break; // only bind the first element (the state value, not the setter)
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
37
82
|
// only handle simple identifier bindings like `const x = ...`
|
|
38
83
|
if (node.id.type !== 'Identifier')
|
|
39
84
|
return;
|
|
@@ -84,6 +129,24 @@ class ExpressionResolver {
|
|
|
84
129
|
return;
|
|
85
130
|
}
|
|
86
131
|
}
|
|
132
|
+
// ArrayExpression -> list of string values
|
|
133
|
+
// Handles `const OPTS = ['a', 'b', 'c'] as const`
|
|
134
|
+
if (unwrappedInit.type === 'ArrayExpression' && Array.isArray(unwrappedInit.elements)) {
|
|
135
|
+
const vals = [];
|
|
136
|
+
for (const elem of unwrappedInit.elements) {
|
|
137
|
+
if (!elem || !elem.expression)
|
|
138
|
+
continue;
|
|
139
|
+
const resolved = this.resolvePossibleStringValuesFromExpression(elem.expression);
|
|
140
|
+
if (resolved.length === 1)
|
|
141
|
+
vals.push(resolved[0]);
|
|
142
|
+
}
|
|
143
|
+
if (vals.length > 0) {
|
|
144
|
+
this.variableTable.set(name, vals);
|
|
145
|
+
// Also share so importing files can see this array
|
|
146
|
+
this.sharedVariableTable.set(name, vals);
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
87
150
|
// For other initializers, try to resolve to one-or-more strings
|
|
88
151
|
const vals = this.resolvePossibleStringValuesFromExpression(init);
|
|
89
152
|
if (vals.length > 0) {
|
|
@@ -127,6 +190,8 @@ class ExpressionResolver {
|
|
|
127
190
|
const vals = this.resolvePossibleStringValuesFromType(tsType);
|
|
128
191
|
if (vals.length > 0) {
|
|
129
192
|
this.typeAliasTable.set(name, vals);
|
|
193
|
+
// Also share so importing files can resolve this alias by name
|
|
194
|
+
this.sharedTypeAliasTable.set(name, vals);
|
|
130
195
|
}
|
|
131
196
|
}
|
|
132
197
|
catch {
|
|
@@ -178,6 +243,33 @@ class ExpressionResolver {
|
|
|
178
243
|
return raw.typeAnnotation;
|
|
179
244
|
return raw;
|
|
180
245
|
}
|
|
246
|
+
/**
|
|
247
|
+
* Temporarily bind a variable name to a set of string values.
|
|
248
|
+
* Used by ast-visitors to inject .map()/.forEach() callback parameters.
|
|
249
|
+
* Call deleteTemporaryVariable() after walking the callback body.
|
|
250
|
+
*/
|
|
251
|
+
setTemporaryVariable(name, values) {
|
|
252
|
+
this.temporaryVariables.set(name, values);
|
|
253
|
+
}
|
|
254
|
+
/**
|
|
255
|
+
* Remove a previously-injected temporary variable binding.
|
|
256
|
+
*/
|
|
257
|
+
deleteTemporaryVariable(name) {
|
|
258
|
+
this.temporaryVariables.delete(name);
|
|
259
|
+
}
|
|
260
|
+
/**
|
|
261
|
+
* Return the array values stored for a variable name, checking all tables.
|
|
262
|
+
* Returns undefined if the name is not a known string array.
|
|
263
|
+
*/
|
|
264
|
+
getVariableValues(name) {
|
|
265
|
+
const tmp = this.temporaryVariables.get(name);
|
|
266
|
+
if (tmp)
|
|
267
|
+
return tmp;
|
|
268
|
+
const v = this.variableTable.get(name);
|
|
269
|
+
if (Array.isArray(v))
|
|
270
|
+
return v;
|
|
271
|
+
return this.sharedVariableTable.get(name);
|
|
272
|
+
}
|
|
181
273
|
/**
|
|
182
274
|
* Capture a TypeScript enum declaration so members can be resolved later.
|
|
183
275
|
* Accepts SWC node shapes like `TsEnumDeclaration` / `TSEnumDeclaration`.
|
|
@@ -419,9 +511,18 @@ class ExpressionResolver {
|
|
|
419
511
|
}
|
|
420
512
|
// Identifier resolution via captured per-file variable table only
|
|
421
513
|
if (expression.type === 'Identifier') {
|
|
514
|
+
// Check temporary (callback param) overrides first
|
|
515
|
+
const tmp = this.temporaryVariables.get(expression.value);
|
|
516
|
+
if (tmp)
|
|
517
|
+
return tmp;
|
|
422
518
|
const v = this.variableTable.get(expression.value);
|
|
423
|
-
if (!v)
|
|
519
|
+
if (!v) {
|
|
520
|
+
// Fall back to shared cross-file array table
|
|
521
|
+
const sv = this.sharedVariableTable.get(expression.value);
|
|
522
|
+
if (sv)
|
|
523
|
+
return sv;
|
|
424
524
|
return [];
|
|
525
|
+
}
|
|
425
526
|
if (Array.isArray(v))
|
|
426
527
|
return v;
|
|
427
528
|
// object map - cannot be used directly as key, so return empty
|
|
@@ -431,6 +532,11 @@ class ExpressionResolver {
|
|
|
431
532
|
return [];
|
|
432
533
|
}
|
|
433
534
|
resolvePossibleStringValuesFromType(type, returnEmptyStrings = false) {
|
|
535
|
+
// Unwrap TsParenthesizedType — SWC explicitly emits these for grouped types like
|
|
536
|
+
// `(typeof X)[number]` where `(typeof X)` becomes TsParenthesizedType { typeAnnotation: TsTypeQuery }
|
|
537
|
+
if (type.type === 'TsParenthesizedType') {
|
|
538
|
+
return this.resolvePossibleStringValuesFromType(type.typeAnnotation, returnEmptyStrings);
|
|
539
|
+
}
|
|
434
540
|
if (type.type === 'TsUnionType') {
|
|
435
541
|
return type.types.flatMap((t) => this.resolvePossibleStringValuesFromType(t, returnEmptyStrings));
|
|
436
542
|
}
|
|
@@ -454,11 +560,38 @@ class ExpressionResolver {
|
|
|
454
560
|
? type.typeName.value
|
|
455
561
|
: undefined;
|
|
456
562
|
if (typeName) {
|
|
457
|
-
const aliasVals = this.typeAliasTable.get(typeName);
|
|
563
|
+
const aliasVals = this.typeAliasTable.get(typeName) ?? this.sharedTypeAliasTable.get(typeName);
|
|
458
564
|
if (aliasVals && aliasVals.length > 0)
|
|
459
565
|
return aliasVals;
|
|
460
566
|
}
|
|
461
567
|
}
|
|
568
|
+
// `(typeof ACCESS_OPTIONS)[number]` — resolve through the shared array variable table.
|
|
569
|
+
// SWC emits: TsIndexedAccessType {
|
|
570
|
+
// objectType: TsParenthesizedType { typeAnnotation: TsTypeQuery { exprName: Identifier } }
|
|
571
|
+
// indexType: TsKeywordType
|
|
572
|
+
// }
|
|
573
|
+
// The parens around `typeof X` produce a TsParenthesizedType wrapper that we must unwrap.
|
|
574
|
+
if (type.type === 'TsIndexedAccessType') {
|
|
575
|
+
try {
|
|
576
|
+
let objType = type.objectType;
|
|
577
|
+
// Unwrap TsParenthesizedType wrapper (SWC preserves explicit parens in type positions)
|
|
578
|
+
while (objType?.type === 'TsParenthesizedType') {
|
|
579
|
+
objType = objType.typeAnnotation;
|
|
580
|
+
}
|
|
581
|
+
if (objType?.type === 'TsTypeQuery' || objType?.type === 'TSTypeQuery') {
|
|
582
|
+
// SWC: TsTypeQuery.exprName is TsEntityName (Identifier | TsQualifiedName)
|
|
583
|
+
const exprName = objType.exprName ?? objType.expr ?? objType.entityName;
|
|
584
|
+
// access .value (Identifier) or fall back to .name for alternate SWC builds
|
|
585
|
+
const varName = exprName?.value ?? exprName?.name;
|
|
586
|
+
if (varName) {
|
|
587
|
+
const vals = this.getVariableValues(varName);
|
|
588
|
+
if (vals && vals.length > 0)
|
|
589
|
+
return vals;
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
catch { }
|
|
594
|
+
}
|
|
462
595
|
// We can't statically determine the value of other expressions (e.g., variables, function calls)
|
|
463
596
|
return [];
|
|
464
597
|
}
|
package/package.json
CHANGED
|
@@ -65,6 +65,12 @@ export declare class ASTVisitors {
|
|
|
65
65
|
* @private
|
|
66
66
|
*/
|
|
67
67
|
private walk;
|
|
68
|
+
/**
|
|
69
|
+
* If `node` is a call like `ARRAY.map(param => ...)` where ARRAY is a known
|
|
70
|
+
* string-array constant, returns the callback's first parameter name and the
|
|
71
|
+
* array values so the caller can inject a temporary variable binding.
|
|
72
|
+
*/
|
|
73
|
+
private tryGetArrayIterationCallbackInfo;
|
|
68
74
|
/**
|
|
69
75
|
* Retrieves variable information from the scope chain.
|
|
70
76
|
* Searches from innermost to outermost scope.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ast-visitors.d.ts","sourceRoot":"","sources":["../../../src/extractor/core/ast-visitors.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAQ,MAAM,WAAW,CAAA;AAC7C,OAAO,KAAK,EAAE,aAAa,EAAE,oBAAoB,EAAE,MAAM,EAAE,eAAe,EAAE,SAAS,EAAE,MAAM,aAAa,CAAA;AAC1G,OAAO,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAA;AACvD,OAAO,EAAE,kBAAkB,EAAE,MAAM,gCAAgC,CAAA;AAInE;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,qBAAa,WAAW;IACtB,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAe;IAC7C,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAuC;IAC9D,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAQ;IAC/B,OAAO,CAAC,KAAK,CAAiB;IAE9B,IAAW,UAAU,gBAEpB;IAED,SAAgB,YAAY,EAAE,YAAY,CAAA;IAC1C,OAAO,CAAC,QAAQ,CAAC,kBAAkB,CAAoB;IACvD,OAAO,CAAC,QAAQ,CAAC,qBAAqB,CAAuB;IAC7D,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAY;IACvC,OAAO,CAAC,WAAW,CAAa;IAChC,OAAO,CAAC,WAAW,CAAa;IAEhC;;;;;;OAMG;gBAED,MAAM,EAAE,IAAI,CAAC,oBAAoB,EAAE,SAAS,CAAC,EAC7C,aAAa,EAAE,aAAa,EAC5B,MAAM,EAAE,MAAM,EACd,KAAK,CAAC,EAAE,eAAe,EACvB,kBAAkB,CAAC,EAAE,kBAAkB;IAiCzC;;;;;OAKG;IACI,KAAK,CAAE,IAAI,EAAE,MAAM,GAAG,IAAI;IAUjC;;;;;;;;;;;;OAYG;IACH,OAAO,CAAC,IAAI;
|
|
1
|
+
{"version":3,"file":"ast-visitors.d.ts","sourceRoot":"","sources":["../../../src/extractor/core/ast-visitors.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAQ,MAAM,WAAW,CAAA;AAC7C,OAAO,KAAK,EAAE,aAAa,EAAE,oBAAoB,EAAE,MAAM,EAAE,eAAe,EAAE,SAAS,EAAE,MAAM,aAAa,CAAA;AAC1G,OAAO,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAA;AACvD,OAAO,EAAE,kBAAkB,EAAE,MAAM,gCAAgC,CAAA;AAInE;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,qBAAa,WAAW;IACtB,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAe;IAC7C,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAuC;IAC9D,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAQ;IAC/B,OAAO,CAAC,KAAK,CAAiB;IAE9B,IAAW,UAAU,gBAEpB;IAED,SAAgB,YAAY,EAAE,YAAY,CAAA;IAC1C,OAAO,CAAC,QAAQ,CAAC,kBAAkB,CAAoB;IACvD,OAAO,CAAC,QAAQ,CAAC,qBAAqB,CAAuB;IAC7D,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAY;IACvC,OAAO,CAAC,WAAW,CAAa;IAChC,OAAO,CAAC,WAAW,CAAa;IAEhC;;;;;;OAMG;gBAED,MAAM,EAAE,IAAI,CAAC,oBAAoB,EAAE,SAAS,CAAC,EAC7C,aAAa,EAAE,aAAa,EAC5B,MAAM,EAAE,MAAM,EACd,KAAK,CAAC,EAAE,eAAe,EACvB,kBAAkB,CAAC,EAAE,kBAAkB;IAiCzC;;;;;OAKG;IACI,KAAK,CAAE,IAAI,EAAE,MAAM,GAAG,IAAI;IAUjC;;;;;;;;;;;;OAYG;IACH,OAAO,CAAC,IAAI;IAgWZ;;;;OAIG;IACH,OAAO,CAAC,gCAAgC;IAwCxC;;;;;;;;OAQG;IACI,eAAe,CAAE,IAAI,EAAE,MAAM,GAAG,SAAS,GAAG,SAAS;IAI5D;;OAEG;IACI,cAAc,CAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,IAAI;IAKxD;;;;;;OAMG;IACI,cAAc,IAAK,MAAM;IAIhC;;OAEG;IACI,cAAc,IAAK,MAAM;CAGjC"}
|
|
@@ -5,6 +5,9 @@ export declare class ExpressionResolver {
|
|
|
5
5
|
private variableTable;
|
|
6
6
|
private sharedEnumTable;
|
|
7
7
|
private typeAliasTable;
|
|
8
|
+
private sharedVariableTable;
|
|
9
|
+
private sharedTypeAliasTable;
|
|
10
|
+
private temporaryVariables;
|
|
8
11
|
constructor(hooks: ASTVisitorHooks);
|
|
9
12
|
/**
|
|
10
13
|
* Clear per-file captured variables. Enums / shared maps are kept.
|
|
@@ -46,6 +49,21 @@ export declare class ExpressionResolver {
|
|
|
46
49
|
* SWC may wrap it in a `TsTypeAnnotation` node — this unwraps it.
|
|
47
50
|
*/
|
|
48
51
|
private extractTypeAnnotation;
|
|
52
|
+
/**
|
|
53
|
+
* Temporarily bind a variable name to a set of string values.
|
|
54
|
+
* Used by ast-visitors to inject .map()/.forEach() callback parameters.
|
|
55
|
+
* Call deleteTemporaryVariable() after walking the callback body.
|
|
56
|
+
*/
|
|
57
|
+
setTemporaryVariable(name: string, values: string[]): void;
|
|
58
|
+
/**
|
|
59
|
+
* Remove a previously-injected temporary variable binding.
|
|
60
|
+
*/
|
|
61
|
+
deleteTemporaryVariable(name: string): void;
|
|
62
|
+
/**
|
|
63
|
+
* Return the array values stored for a variable name, checking all tables.
|
|
64
|
+
* Returns undefined if the name is not a known string array.
|
|
65
|
+
*/
|
|
66
|
+
getVariableValues(name: string): string[] | undefined;
|
|
49
67
|
/**
|
|
50
68
|
* Capture a TypeScript enum declaration so members can be resolved later.
|
|
51
69
|
* Accepts SWC node shapes like `TsEnumDeclaration` / `TSEnumDeclaration`.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"expression-resolver.d.ts","sourceRoot":"","sources":["../../../src/extractor/parsers/expression-resolver.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAkD,MAAM,WAAW,CAAA;AAC3F,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,aAAa,CAAA;AAElD,qBAAa,kBAAkB;IAC7B,OAAO,CAAC,KAAK,CAAiB;IAK9B,OAAO,CAAC,aAAa,CAA4D;IAGjF,OAAO,CAAC,eAAe,CAAiD;IAIxE,OAAO,CAAC,cAAc,CAAmC;
|
|
1
|
+
{"version":3,"file":"expression-resolver.d.ts","sourceRoot":"","sources":["../../../src/extractor/parsers/expression-resolver.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAkD,MAAM,WAAW,CAAA;AAC3F,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,aAAa,CAAA;AAElD,qBAAa,kBAAkB;IAC7B,OAAO,CAAC,KAAK,CAAiB;IAK9B,OAAO,CAAC,aAAa,CAA4D;IAGjF,OAAO,CAAC,eAAe,CAAiD;IAIxE,OAAO,CAAC,cAAc,CAAmC;IAIzD,OAAO,CAAC,mBAAmB,CAAmC;IAI9D,OAAO,CAAC,oBAAoB,CAAmC;IAI/D,OAAO,CAAC,kBAAkB,CAAmC;gBAEhD,KAAK,EAAE,eAAe;IAInC;;OAEG;IACI,gBAAgB,IAAK,IAAI;IAMhC;;;;;;;;;OASG;IACH,yBAAyB,CAAE,IAAI,EAAE,GAAG,GAAG,IAAI;IAwI3C;;;;;;;OAOG;IACH,2BAA2B,CAAE,IAAI,EAAE,GAAG,GAAG,IAAI;IAkB7C;;;;;;;;;OASG;IACH,0BAA0B,CAAE,IAAI,EAAE,GAAG,GAAG,IAAI;IAoB5C;;;OAGG;IACH,OAAO,CAAC,qBAAqB;IAQ7B;;;;OAIG;IACI,oBAAoB,CAAE,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,GAAG,IAAI;IAIlE;;OAEG;IACI,uBAAuB,CAAE,IAAI,EAAE,MAAM,GAAG,IAAI;IAInD;;;OAGG;IACI,iBAAiB,CAAE,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,SAAS;IAQ7D;;;;;OAKG;IACH,sBAAsB,CAAE,IAAI,EAAE,GAAG,GAAG,IAAI;IAwBxC;;;;;;;OAOG;IACH,kCAAkC,CAAE,UAAU,EAAE,UAAU,GAAG,MAAM,EAAE;IAKrE;;;;;;;OAOG;IACH,8BAA8B,CAAE,UAAU,EAAE,UAAU,GAAG,MAAM,EAAE;IAKjE;;;;;;;;;;;;;;;;;;OAkBG;IACH,OAAO,CAAC,yCAAyC;IAgMjD,OAAO,CAAC,mCAAmC;IAsE3C;;;;;;OAMG;IACH,OAAO,CAAC,6CAA6C;IAyBrD;;;;;;OAMG;IACH,OAAO,CAAC,kDAAkD;CAwB3D"}
|