gtx-cli 2.5.35 → 2.5.36

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,539 @@
1
+ import * as t from '@babel/types';
2
+ import { buildImportMap } from './buildImportMap.js';
3
+ import { resolveImportPath } from './resolveImportPath.js';
4
+ import { parse } from '@babel/parser';
5
+ import fs from 'node:fs';
6
+ import { warnDeclareStaticNoResultsSync, warnFunctionNotFoundSync, warnInvalidDeclareVarNameSync, } from '../../../console/index.js';
7
+ import traverseModule from '@babel/traverse';
8
+ import { DECLARE_VAR_FUNCTION, GT_LIBRARIES } from './constants.js';
9
+ import { declareVar } from 'generaltranslation/internal';
10
+ import { isStaticExpression } from '../evaluateJsx.js';
11
+ import generateModule from '@babel/generator';
12
+ // Handle CommonJS/ESM interop
13
+ const traverse = traverseModule.default || traverseModule;
14
+ const generate = generateModule.default || generateModule;
15
+ /**
16
+ * Cache for resolved import paths to avoid redundant I/O operations.
17
+ */
18
+ const resolveImportPathCache = new Map();
19
+ /**
20
+ * Cache for processed functions to avoid re-parsing the same files.
21
+ */
22
+ const processFunctionCache = new Map();
23
+ /**
24
+ * Processes a string expression node and resolves any function calls within it
25
+ * This handles cases like:
26
+ * - "hello" (string literal)
27
+ * - "hello" + world() (binary expression with function call)
28
+ * - Math.random() > 0.5 ? "day" : "night" (conditional expression)
29
+ * - greeting() (function call that returns string or conditional)
30
+ *
31
+ * @param node - The AST node to process
32
+ * @param tPath - NodePath for scope resolution
33
+ * @param file - Current file path
34
+ * @param parsingOptions - Parsing configuration
35
+ * @param warnings - Set to collect warning messages
36
+ * @returns Node | null
37
+ */
38
+ export function parseStringExpression(node, tPath, file, parsingOptions, warnings = new Set()) {
39
+ // Handle string literals
40
+ if (t.isStringLiteral(node)) {
41
+ return { type: 'text', text: node.value };
42
+ }
43
+ // Handle numeric literals
44
+ if (t.isNumericLiteral(node)) {
45
+ return { type: 'text', text: String(node.value) };
46
+ }
47
+ // Handle boolean literals
48
+ if (t.isBooleanLiteral(node)) {
49
+ return { type: 'text', text: String(node.value) };
50
+ }
51
+ // Handle null literal
52
+ if (t.isNullLiteral(node)) {
53
+ return { type: 'text', text: 'null' };
54
+ }
55
+ // Handle template literals
56
+ if (t.isTemplateLiteral(node)) {
57
+ const parts = [];
58
+ for (let index = 0; index < node.quasis.length; index++) {
59
+ const quasi = node.quasis[index];
60
+ const text = quasi.value.cooked ?? quasi.value.raw ?? '';
61
+ if (text) {
62
+ parts.push({ type: 'text', text });
63
+ }
64
+ const exprNode = node.expressions[index];
65
+ if (exprNode && t.isExpression(exprNode)) {
66
+ const result = parseStringExpression(exprNode, tPath, file, parsingOptions, warnings);
67
+ if (result === null) {
68
+ return null;
69
+ }
70
+ parts.push(result);
71
+ }
72
+ }
73
+ if (parts.length === 0) {
74
+ return { type: 'text', text: '' };
75
+ }
76
+ if (parts.length === 1) {
77
+ return parts[0];
78
+ }
79
+ return { type: 'sequence', nodes: parts };
80
+ }
81
+ // Handle binary expressions (e.g., "hello" + world())
82
+ if (t.isBinaryExpression(node) && node.operator === '+') {
83
+ if (!t.isExpression(node.left) || !t.isExpression(node.right)) {
84
+ return null;
85
+ }
86
+ const leftResult = parseStringExpression(node.left, tPath, file, parsingOptions, warnings);
87
+ const rightResult = parseStringExpression(node.right, tPath, file, parsingOptions, warnings);
88
+ if (leftResult === null || rightResult === null) {
89
+ return null;
90
+ }
91
+ return { type: 'sequence', nodes: [leftResult, rightResult] };
92
+ }
93
+ // Handle conditional expressions (e.g., cond ? "day" : "night")
94
+ if (t.isConditionalExpression(node)) {
95
+ if (!t.isExpression(node.consequent) || !t.isExpression(node.alternate)) {
96
+ return null;
97
+ }
98
+ const consequentResult = parseStringExpression(node.consequent, tPath, file, parsingOptions, warnings);
99
+ const alternateResult = parseStringExpression(node.alternate, tPath, file, parsingOptions, warnings);
100
+ if (consequentResult === null || alternateResult === null) {
101
+ return null;
102
+ }
103
+ // Create a choice node with both branches
104
+ return {
105
+ type: 'choice',
106
+ nodes: [consequentResult, alternateResult],
107
+ };
108
+ }
109
+ // Handle variable references (e.g., result)
110
+ if (t.isIdentifier(node)) {
111
+ const binding = tPath.scope.getBinding(node.name);
112
+ if (!binding) {
113
+ // Variable not found in scope
114
+ return null;
115
+ }
116
+ // Check if it's a const/let/var with an initializer
117
+ if (binding.path.isVariableDeclarator() && binding.path.node.init) {
118
+ const init = binding.path.node.init;
119
+ if (t.isExpression(init)) {
120
+ // Recursively resolve the initializer
121
+ return parseStringExpression(init, binding.path, file, parsingOptions, warnings);
122
+ }
123
+ }
124
+ // Not a resolvable variable
125
+ return null;
126
+ }
127
+ // Handle function calls (e.g., getName())
128
+ if (t.isCallExpression(node) && t.isIdentifier(node.callee)) {
129
+ const functionName = node.callee.name;
130
+ const calleeBinding = tPath.scope.getBinding(functionName);
131
+ if (!calleeBinding) {
132
+ // Function not found in scope
133
+ warnings.add(warnFunctionNotFoundSync(file, functionName, `${node.callee.loc?.start?.line}:${node.callee.loc?.start?.column}`));
134
+ return null;
135
+ }
136
+ // Check if this is an imported function
137
+ const programPath = tPath.scope.getProgramParent().path;
138
+ const importedFunctionsMap = buildImportMap(programPath);
139
+ if (importedFunctionsMap.has(functionName)) {
140
+ // Function is imported - resolve cross-file
141
+ let originalName;
142
+ if (calleeBinding.path.isImportSpecifier()) {
143
+ originalName = t.isIdentifier(calleeBinding.path.node.imported)
144
+ ? calleeBinding.path.node.imported.name
145
+ : calleeBinding.path.node.imported.value;
146
+ }
147
+ else if (calleeBinding.path.isImportDefaultSpecifier()) {
148
+ originalName = calleeBinding.path.node.local.name;
149
+ }
150
+ else if (calleeBinding.path.isImportNamespaceSpecifier()) {
151
+ originalName = calleeBinding.path.node.local.name;
152
+ }
153
+ const importPath = importedFunctionsMap.get(functionName);
154
+ // Handle declareVar function
155
+ if (originalName === DECLARE_VAR_FUNCTION &&
156
+ GT_LIBRARIES.includes(importPath)) {
157
+ // check for name field eg declareVar('test', { $name: 'test' })
158
+ if (node.arguments.length > 1 &&
159
+ t.isObjectExpression(node.arguments[1])) {
160
+ const name = node.arguments[1].properties
161
+ .filter((prop) => t.isObjectProperty(prop))
162
+ .find((prop) => t.isIdentifier(prop.key) && prop.key.name === '$name')?.value;
163
+ if (name) {
164
+ if (!t.isExpression(name)) {
165
+ warnings.add(warnInvalidDeclareVarNameSync(file, generate(name).code, `${node.arguments[1].loc?.start?.line}:${node.arguments[1].loc?.start?.column}`));
166
+ return null;
167
+ }
168
+ const staticResult = isStaticExpression(name);
169
+ if (!staticResult.isStatic) {
170
+ warnings.add(warnInvalidDeclareVarNameSync(file, generate(name).code, `${node.arguments[1].loc?.start?.line}:${node.arguments[1].loc?.start?.column}`));
171
+ return null;
172
+ }
173
+ return {
174
+ type: 'text',
175
+ text: declareVar('', { $name: staticResult.value }),
176
+ };
177
+ }
178
+ }
179
+ return {
180
+ type: 'text',
181
+ text: declareVar(''),
182
+ };
183
+ }
184
+ const filePath = resolveImportPath(file, importPath, parsingOptions, resolveImportPathCache);
185
+ if (filePath && originalName) {
186
+ return resolveFunctionInFile(filePath, originalName, parsingOptions, warnings);
187
+ }
188
+ return null;
189
+ }
190
+ // Resolve the function locally and get its return values
191
+ return resolveFunctionCall(calleeBinding, tPath, file, parsingOptions, warnings);
192
+ }
193
+ // Handle parenthesized expressions
194
+ if (t.isParenthesizedExpression(node)) {
195
+ return parseStringExpression(node.expression, tPath, file, parsingOptions, warnings);
196
+ }
197
+ // Handle unary expressions (e.g., -123)
198
+ if (t.isUnaryExpression(node)) {
199
+ let operator = '';
200
+ if (node.operator === '-') {
201
+ operator = node.operator;
202
+ }
203
+ if (t.isNumericLiteral(node.argument)) {
204
+ if (node.argument.value === 0) {
205
+ return { type: 'text', text: '0' };
206
+ }
207
+ else {
208
+ return {
209
+ type: 'text',
210
+ text: operator + node.argument.value.toString(),
211
+ };
212
+ }
213
+ }
214
+ return null;
215
+ }
216
+ // Unsupported expression type
217
+ return null;
218
+ }
219
+ /**
220
+ * Resolves a function call by traversing its body and collecting return values
221
+ */
222
+ function resolveFunctionCall(calleeBinding, tPath, file, parsingOptions, warnings) {
223
+ if (!calleeBinding) {
224
+ return null;
225
+ }
226
+ const bindingPath = calleeBinding.path;
227
+ const branches = [];
228
+ // Handle function declarations: function time() { return "day"; }
229
+ if (bindingPath.isFunctionDeclaration()) {
230
+ bindingPath.traverse({
231
+ // Don't skip nested functions - let parseStringExpression handle function calls
232
+ ReturnStatement(returnPath) {
233
+ // Only process return statements that are direct children of this function
234
+ // Skip return statements from nested functions (they'll be handled when those functions are called)
235
+ const parentFunction = returnPath.getFunctionParent();
236
+ if (parentFunction?.node !== bindingPath.node) {
237
+ // This return belongs to a nested function, skip it
238
+ return;
239
+ }
240
+ const returnArg = returnPath.node.argument;
241
+ if (!returnArg || !t.isExpression(returnArg)) {
242
+ return;
243
+ }
244
+ const returnResult = parseStringExpression(returnArg, returnPath, file, parsingOptions, warnings);
245
+ if (returnResult !== null) {
246
+ branches.push(returnResult);
247
+ }
248
+ },
249
+ });
250
+ }
251
+ // Handle arrow functions: const time = () => "day"
252
+ else if (bindingPath.isVariableDeclarator()) {
253
+ const init = bindingPath.get('init');
254
+ if (!init.isArrowFunctionExpression()) {
255
+ return null;
256
+ }
257
+ const body = init.get('body');
258
+ // Handle expression body: () => "day"
259
+ if (body.isExpression()) {
260
+ const bodyResult = parseStringExpression(body.node, body, file, parsingOptions, warnings);
261
+ if (bodyResult !== null) {
262
+ branches.push(bodyResult);
263
+ }
264
+ }
265
+ // Handle block body: () => { return "day"; }
266
+ else if (body.isBlockStatement()) {
267
+ const arrowFunction = init.node;
268
+ body.traverse({
269
+ // Don't skip nested functions - let parseStringExpression handle function calls
270
+ ReturnStatement(returnPath) {
271
+ // Only process return statements that are direct children of this function
272
+ // Skip return statements from nested functions (they'll be handled when those functions are called)
273
+ const parentFunction = returnPath.getFunctionParent();
274
+ if (parentFunction?.node !== arrowFunction) {
275
+ // This return belongs to a nested function, skip it
276
+ return;
277
+ }
278
+ const returnArg = returnPath.node.argument;
279
+ if (!returnArg || !t.isExpression(returnArg)) {
280
+ return;
281
+ }
282
+ const returnResult = parseStringExpression(returnArg, returnPath, file, parsingOptions, warnings);
283
+ if (returnResult !== null) {
284
+ branches.push(returnResult);
285
+ }
286
+ },
287
+ });
288
+ }
289
+ }
290
+ if (branches.length === 0) {
291
+ return null;
292
+ }
293
+ if (branches.length === 1) {
294
+ return branches[0];
295
+ }
296
+ return { type: 'choice', nodes: branches };
297
+ }
298
+ /**
299
+ * Resolves a function definition in an external file
300
+ */
301
+ function resolveFunctionInFile(filePath, functionName, parsingOptions, warnings) {
302
+ // Check cache first
303
+ const cacheKey = `${filePath}::${functionName}`;
304
+ if (processFunctionCache.has(cacheKey)) {
305
+ return processFunctionCache.get(cacheKey) ?? null;
306
+ }
307
+ let result = null;
308
+ try {
309
+ const code = fs.readFileSync(filePath, 'utf8');
310
+ const ast = parse(code, {
311
+ sourceType: 'module',
312
+ plugins: ['jsx', 'typescript'],
313
+ });
314
+ traverse(ast, {
315
+ // Handle re-exports: export * from './utils1'
316
+ ExportAllDeclaration(path) {
317
+ // Only follow re-exports if we haven't found the function yet
318
+ if (result !== null)
319
+ return;
320
+ if (t.isStringLiteral(path.node.source)) {
321
+ const reexportPath = path.node.source.value;
322
+ const resolvedPath = resolveImportPath(filePath, reexportPath, parsingOptions, resolveImportPathCache);
323
+ if (resolvedPath) {
324
+ // Recursively resolve in the re-exported file
325
+ const reexportResult = resolveFunctionInFile(resolvedPath, functionName, parsingOptions, warnings);
326
+ if (reexportResult) {
327
+ result = reexportResult;
328
+ }
329
+ }
330
+ }
331
+ },
332
+ // Handle named re-exports: export { fn1 } from './utils'
333
+ ExportNamedDeclaration(path) {
334
+ // Only follow re-exports if we haven't found the function yet
335
+ if (result !== null)
336
+ return;
337
+ // Check if this is a re-export with a source
338
+ if (path.node.source && t.isStringLiteral(path.node.source)) {
339
+ // Check if any of the exported specifiers match our function name
340
+ const hasMatchingExport = path.node.specifiers.some((spec) => {
341
+ if (t.isExportSpecifier(spec)) {
342
+ const exportedName = t.isIdentifier(spec.exported)
343
+ ? spec.exported.name
344
+ : spec.exported.value;
345
+ return exportedName === functionName;
346
+ }
347
+ return false;
348
+ });
349
+ if (hasMatchingExport) {
350
+ const reexportPath = path.node.source.value;
351
+ const resolvedPath = resolveImportPath(filePath, reexportPath, parsingOptions, resolveImportPathCache);
352
+ if (resolvedPath) {
353
+ // Find the original name in case it was renamed
354
+ const specifier = path.node.specifiers.find((spec) => {
355
+ if (t.isExportSpecifier(spec)) {
356
+ const exportedName = t.isIdentifier(spec.exported)
357
+ ? spec.exported.name
358
+ : spec.exported.value;
359
+ return exportedName === functionName;
360
+ }
361
+ return false;
362
+ });
363
+ let originalName = functionName;
364
+ if (specifier &&
365
+ t.isExportSpecifier(specifier) &&
366
+ t.isIdentifier(specifier.local)) {
367
+ originalName = specifier.local.name;
368
+ }
369
+ // Recursively resolve in the re-exported file
370
+ const reexportResult = resolveFunctionInFile(resolvedPath, originalName, parsingOptions, warnings);
371
+ if (reexportResult) {
372
+ result = reexportResult;
373
+ }
374
+ }
375
+ }
376
+ }
377
+ },
378
+ // Handle function declarations: function interjection() { ... }
379
+ FunctionDeclaration(path) {
380
+ if (path.node.id?.name === functionName && result === null) {
381
+ const branches = [];
382
+ path.traverse({
383
+ Function(innerPath) {
384
+ // Skip nested functions
385
+ innerPath.skip();
386
+ },
387
+ ReturnStatement(returnPath) {
388
+ if (!t.isReturnStatement(returnPath.node)) {
389
+ return;
390
+ }
391
+ const returnArg = returnPath.node.argument;
392
+ if (!returnArg || !t.isExpression(returnArg)) {
393
+ return;
394
+ }
395
+ const returnResult = parseStringExpression(returnArg, returnPath, filePath, parsingOptions, warnings);
396
+ if (returnResult !== null) {
397
+ branches.push(returnResult);
398
+ }
399
+ },
400
+ });
401
+ if (branches.length === 1) {
402
+ result = branches[0];
403
+ }
404
+ else if (branches.length > 1) {
405
+ result = { type: 'choice', nodes: branches };
406
+ }
407
+ }
408
+ },
409
+ // Handle variable declarations: const interjection = () => { ... }
410
+ VariableDeclarator(path) {
411
+ if (t.isIdentifier(path.node.id) &&
412
+ path.node.id.name === functionName &&
413
+ path.node.init &&
414
+ (t.isArrowFunctionExpression(path.node.init) ||
415
+ t.isFunctionExpression(path.node.init)) &&
416
+ result === null) {
417
+ const init = path.get('init');
418
+ if (!init.isArrowFunctionExpression() &&
419
+ !init.isFunctionExpression()) {
420
+ return;
421
+ }
422
+ const bodyPath = init.get('body');
423
+ const branches = [];
424
+ // Handle expression body: () => "day"
425
+ if (!Array.isArray(bodyPath) && t.isExpression(bodyPath.node)) {
426
+ const bodyResult = parseStringExpression(bodyPath.node, bodyPath, filePath, parsingOptions, warnings);
427
+ if (bodyResult !== null) {
428
+ branches.push(bodyResult);
429
+ }
430
+ }
431
+ // Handle block body: () => { return "day"; }
432
+ else if (!Array.isArray(bodyPath) &&
433
+ t.isBlockStatement(bodyPath.node)) {
434
+ const arrowFunction = init.node;
435
+ bodyPath.traverse({
436
+ Function(innerPath) {
437
+ // Skip nested functions
438
+ innerPath.skip();
439
+ },
440
+ ReturnStatement(returnPath) {
441
+ // Only process return statements that are direct children of this function
442
+ const parentFunction = returnPath.getFunctionParent();
443
+ if (parentFunction?.node !== arrowFunction) {
444
+ return;
445
+ }
446
+ if (!t.isReturnStatement(returnPath.node)) {
447
+ return;
448
+ }
449
+ const returnArg = returnPath.node.argument;
450
+ if (!returnArg || !t.isExpression(returnArg)) {
451
+ return;
452
+ }
453
+ const returnResult = parseStringExpression(returnArg, returnPath, filePath, parsingOptions, warnings);
454
+ if (returnResult !== null) {
455
+ branches.push(returnResult);
456
+ }
457
+ },
458
+ });
459
+ }
460
+ if (branches.length === 1) {
461
+ result = branches[0];
462
+ }
463
+ else if (branches.length > 1) {
464
+ result = { type: 'choice', nodes: branches };
465
+ }
466
+ }
467
+ },
468
+ });
469
+ }
470
+ catch (error) {
471
+ // File read or parse error - return null
472
+ warnings.add(warnDeclareStaticNoResultsSync(filePath, functionName, 'file read/parse error: ' + error));
473
+ result = null;
474
+ }
475
+ // Cache the result
476
+ processFunctionCache.set(cacheKey, result);
477
+ return result;
478
+ }
479
+ /**
480
+ * Converts a Node tree to an array of all possible string combinations
481
+ * This is a helper function for compatibility with existing code
482
+ */
483
+ export function nodeToStrings(node) {
484
+ if (node === null) {
485
+ return [];
486
+ }
487
+ // Handle TextNode
488
+ if (typeof node === 'object' &&
489
+ node !== null &&
490
+ 'type' in node &&
491
+ node.type === 'text') {
492
+ return [node.text];
493
+ }
494
+ // Handle SequenceNode - concatenate all parts
495
+ if (typeof node === 'object' &&
496
+ node !== null &&
497
+ 'type' in node &&
498
+ node.type === 'sequence') {
499
+ const partResults = node.nodes.map((n) => nodeToStrings(n));
500
+ return cartesianProduct(partResults);
501
+ }
502
+ // Handle ChoiceNode - flatten all branches
503
+ if (typeof node === 'object' &&
504
+ node !== null &&
505
+ 'type' in node &&
506
+ node.type === 'choice') {
507
+ const allStrings = [];
508
+ for (const branch of node.nodes) {
509
+ allStrings.push(...nodeToStrings(branch));
510
+ }
511
+ return [...new Set(allStrings)]; // Deduplicate
512
+ }
513
+ return [];
514
+ }
515
+ /**
516
+ * Creates cartesian product of string arrays and concatenates them
517
+ * @example cartesianProduct([["Hello "], ["day", "night"]]) → ["Hello day", "Hello night"]
518
+ */
519
+ function cartesianProduct(arrays) {
520
+ if (arrays.length === 0) {
521
+ return [];
522
+ }
523
+ if (arrays.length === 1) {
524
+ return arrays[0];
525
+ }
526
+ // Start with first array
527
+ let result = arrays[0];
528
+ // Combine with each subsequent array
529
+ for (let i = 1; i < arrays.length; i++) {
530
+ const newResult = [];
531
+ for (const prev of result) {
532
+ for (const curr of arrays[i]) {
533
+ newResult.push(prev + curr);
534
+ }
535
+ }
536
+ result = newResult;
537
+ }
538
+ return result;
539
+ }
@@ -5,6 +5,16 @@ import type { ParsingConfigOptions } from '../../../types/parsing.js';
5
5
  * Clears all caches. Useful for testing or when file system changes.
6
6
  */
7
7
  export declare function clearParsingCaches(): void;
8
+ /**
9
+ * Recursively resolves variable assignments to find all aliases of a translation callback parameter.
10
+ * Handles cases like: const t = translate; const a = translate; const b = a; const c = b;
11
+ *
12
+ * @param scope The scope to search within
13
+ * @param variableName The variable name to resolve
14
+ * @param visited Set to track already visited variables to prevent infinite loops
15
+ * @returns Array of all variable names that reference the original translation callback
16
+ */
17
+ export declare function resolveVariableAliases(scope: any, variableName: string, visited?: Set<string>): string[];
8
18
  /**
9
19
  * Main entry point for parsing translation strings from useGT() and getGT() calls.
10
20
  *