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