@leftium/gg 0.0.33 → 0.0.35

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.
@@ -1,3 +1,6 @@
1
+ import { parse } from 'svelte/compiler';
2
+ import * as acorn from 'acorn';
3
+ import { tsPlugin } from '@sveltejs/acorn-typescript';
1
4
  /**
2
5
  * Vite plugin that rewrites `gg(...)` and `gg.ns(...)` calls to
3
6
  * `gg._ns({ns, file, line, col}, ...)` at build time. This gives each call
@@ -36,7 +39,16 @@ export default function ggCallSitesPlugin(options = {}) {
36
39
  if (!/\.(js|ts|svelte|jsx|tsx|mjs|mts)(\?.*)?$/.test(id))
37
40
  return null;
38
41
  // Quick bail: no gg calls in this file
39
- if (!code.includes('gg(') && !code.includes('gg.ns('))
42
+ if (!code.includes('gg(') &&
43
+ !code.includes('gg.ns(') &&
44
+ !code.includes('gg.warn(') &&
45
+ !code.includes('gg.error(') &&
46
+ !code.includes('gg.table(') &&
47
+ !code.includes('gg.trace(') &&
48
+ !code.includes('gg.assert(') &&
49
+ !code.includes('gg.time(') &&
50
+ !code.includes('gg.timeLog(') &&
51
+ !code.includes('gg.timeEnd('))
40
52
  return null;
41
53
  // Don't transform gg's own source files
42
54
  if (id.includes('/lib/gg.') || id.includes('/lib/debug'))
@@ -48,119 +60,446 @@ export default function ggCallSitesPlugin(options = {}) {
48
60
  // e.g. "/Users/me/project/src/routes/+page.svelte" → "src/routes/+page.svelte"
49
61
  // $1 captures "/src/" or "/chunks/", so strip the leading slash
50
62
  const filePath = id.replace(srcRootRegex, '$1').replace(/^\//, '');
51
- // For .svelte files (with enforce:'pre', we see raw source), restrict
52
- // transforms to <script> blocks only template markup may contain
53
- // text like "gg()" that isn't actual code, and object literals in
54
- // the transformed output would break Svelte's template parser.
55
- let scriptRanges;
63
+ // For .svelte files, use svelte.parse() AST to find code ranges
64
+ // and function scopes. This distinguishes real JS expressions
65
+ // ({gg()}, onclick, etc.) from prose text mentioning "gg()",
66
+ // and uses estree AST for function name detection (no regex).
67
+ let svelteInfo;
68
+ let jsFunctionScopes;
56
69
  if (/\.svelte(\?.*)?$/.test(id)) {
57
- scriptRanges = findScriptRanges(code);
58
- if (scriptRanges.length === 0)
70
+ svelteInfo = collectCodeRanges(code);
71
+ if (svelteInfo.ranges.length === 0)
59
72
  return null;
60
73
  }
61
- return transformGgCalls(code, shortPath, filePath, scriptRanges);
74
+ else {
75
+ // For .js/.ts files, parse with acorn to extract function scopes
76
+ jsFunctionScopes = parseJavaScript(code);
77
+ }
78
+ return transformGgCalls(code, shortPath, filePath, svelteInfo, jsFunctionScopes);
62
79
  }
63
80
  };
64
81
  }
65
82
  /**
66
- * Find the start/end byte offsets of all <script> blocks in a .svelte file.
67
- * Returns ranges covering the inner content (after the opening tag, before </script>).
83
+ * Parse JavaScript/TypeScript code using acorn to extract function scopes.
84
+ * Returns function scope ranges for accurate function name detection in .js/.ts files.
85
+ * Uses @sveltejs/acorn-typescript plugin to handle TypeScript syntax.
86
+ *
87
+ * For .svelte files, use `collectCodeRanges()` instead (which uses svelte.parse()).
68
88
  */
69
- function findScriptRanges(code) {
70
- const ranges = [];
71
- // Match <script ...> tags (with optional attributes like lang="ts")
72
- const openRegex = /<script\b[^>]*>/gi;
73
- let match;
74
- while ((match = openRegex.exec(code)) !== null) {
75
- const contentStart = match.index + match[0].length;
76
- const closeIdx = code.indexOf('</script>', contentStart);
77
- if (closeIdx !== -1) {
78
- ranges.push({ start: contentStart, end: closeIdx });
89
+ export function parseJavaScript(code) {
90
+ try {
91
+ // Parse as ES2022+ with TypeScript support
92
+ // sourceType: 'module' allows import/export, 'script' for regular scripts
93
+ // NOTE: @sveltejs/acorn-typescript REQUIRES locations: true
94
+ const parser = acorn.Parser.extend(tsPlugin());
95
+ const ast = parser.parse(code, {
96
+ ecmaVersion: 'latest',
97
+ sourceType: 'module',
98
+ locations: true, // Required by @sveltejs/acorn-typescript
99
+ ranges: true // Enable byte ranges for AST nodes
100
+ });
101
+ const scopes = [];
102
+ // Reuse the same AST walker we built for Svelte
103
+ collectFunctionScopes(ast.body, scopes);
104
+ scopes.sort((a, b) => a.start - b.start);
105
+ return scopes;
106
+ }
107
+ catch {
108
+ // If acorn can't parse it, fall back to empty scopes.
109
+ // The file might be malformed or use syntax we don't support yet.
110
+ return [];
111
+ }
112
+ }
113
+ /**
114
+ * Use `svelte.parse()` to collect all code ranges and function scopes in a .svelte file.
115
+ *
116
+ * Code ranges identify where JS expressions live:
117
+ * - `<script>` blocks (context: 'script')
118
+ * - Template expressions: `{expr}`, `onclick={expr}`, `bind:value={expr}`,
119
+ * `class:name={expr}`, `{#if expr}`, `{#each expr}`, etc. (context: 'template')
120
+ *
121
+ * Function scopes are extracted from the estree AST in script blocks, mapping
122
+ * byte ranges to enclosing function names. This replaces regex-based function
123
+ * detection for .svelte files.
124
+ *
125
+ * Text nodes (prose) are NOT included, so `gg()` in `<p>text gg()</p>` is never transformed.
126
+ */
127
+ export function collectCodeRanges(code) {
128
+ try {
129
+ const ast = parse(code, { modern: true });
130
+ const ranges = [];
131
+ const functionScopes = [];
132
+ // Script blocks (instance + module)
133
+ // The Svelte AST Program node has start/end at runtime but TypeScript's
134
+ // estree Program type doesn't declare them — we know they exist.
135
+ if (ast.instance) {
136
+ const content = ast.instance.content;
137
+ ranges.push({ start: content.start, end: content.end, context: 'script' });
138
+ collectFunctionScopes(ast.instance.content.body, functionScopes);
139
+ }
140
+ if (ast.module) {
141
+ const content = ast.module.content;
142
+ ranges.push({ start: content.start, end: content.end, context: 'script' });
143
+ collectFunctionScopes(ast.module.content.body, functionScopes);
79
144
  }
145
+ // Walk the template fragment to find all expression positions
146
+ walkFragment(ast.fragment, ranges);
147
+ // Sort function scopes by start position for efficient lookup
148
+ functionScopes.sort((a, b) => a.start - b.start);
149
+ return { ranges, functionScopes };
150
+ }
151
+ catch {
152
+ // If svelte.parse() fails, the Svelte compiler will also reject this file,
153
+ // so there's no point transforming gg() calls — return empty.
154
+ return { ranges: [], functionScopes: [] };
80
155
  }
81
- return ranges;
82
156
  }
83
157
  /**
84
- * Check if a character position falls within any of the given ranges.
158
+ * Walk an estree AST body to collect function scope ranges.
159
+ * Extracts function names from:
160
+ * - FunctionDeclaration: `function foo() {}`
161
+ * - VariableDeclarator with ArrowFunctionExpression/FunctionExpression: `const foo = () => {}`
162
+ * - Property with FunctionExpression: `{ method() {} }` or `{ prop: function() {} }`
163
+ * - MethodDefinition: `class Foo { bar() {} }`
85
164
  */
86
- function isInRanges(pos, ranges) {
87
- for (const r of ranges) {
88
- if (pos >= r.start && pos < r.end)
89
- return true;
165
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
166
+ function collectFunctionScopes(nodes, scopes) {
167
+ if (!nodes)
168
+ return;
169
+ for (const node of nodes) {
170
+ collectFunctionScopesFromNode(node, scopes);
171
+ }
172
+ }
173
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
174
+ function collectFunctionScopesFromNode(node, scopes) {
175
+ if (!node || typeof node !== 'object' || !node.type)
176
+ return;
177
+ switch (node.type) {
178
+ case 'ExportNamedDeclaration':
179
+ case 'ExportDefaultDeclaration':
180
+ // export function foo() {} or export default function foo() {}
181
+ if (node.declaration) {
182
+ collectFunctionScopesFromNode(node.declaration, scopes);
183
+ }
184
+ return;
185
+ case 'FunctionDeclaration':
186
+ if (node.id?.name && node.body) {
187
+ scopes.push({ start: node.body.start, end: node.body.end, name: node.id.name });
188
+ }
189
+ // Recurse into the function body for nested functions
190
+ if (node.body?.body)
191
+ collectFunctionScopes(node.body.body, scopes);
192
+ return;
193
+ case 'VariableDeclaration':
194
+ for (const decl of node.declarations || []) {
195
+ collectFunctionScopesFromNode(decl, scopes);
196
+ }
197
+ return;
198
+ case 'VariableDeclarator':
199
+ // const foo = () => {} or const foo = function() {}
200
+ if (node.id?.name &&
201
+ node.init &&
202
+ (node.init.type === 'ArrowFunctionExpression' || node.init.type === 'FunctionExpression')) {
203
+ const body = node.init.body;
204
+ if (body) {
205
+ // Arrow with block body: () => { ... }
206
+ // Arrow with expression body: () => expr (use the arrow's range)
207
+ const start = body.type === 'BlockStatement' ? body.start : node.init.start;
208
+ const end = body.type === 'BlockStatement' ? body.end : node.init.end;
209
+ scopes.push({ start, end, name: node.id.name });
210
+ }
211
+ // Recurse into the function body
212
+ if (body?.body)
213
+ collectFunctionScopes(body.body, scopes);
214
+ }
215
+ // Recurse into object/array initializers for nested functions
216
+ if (node.init)
217
+ collectFunctionScopesFromNode(node.init, scopes);
218
+ return;
219
+ case 'ExpressionStatement':
220
+ collectFunctionScopesFromNode(node.expression, scopes);
221
+ return;
222
+ case 'ObjectExpression':
223
+ for (const prop of node.properties || []) {
224
+ collectFunctionScopesFromNode(prop, scopes);
225
+ }
226
+ return;
227
+ case 'Property':
228
+ // { method() {} } or { prop: function() {} }
229
+ if (node.key?.name &&
230
+ node.value &&
231
+ (node.value.type === 'FunctionExpression' || node.value.type === 'ArrowFunctionExpression')) {
232
+ const body = node.value.body;
233
+ if (body) {
234
+ const start = body.type === 'BlockStatement' ? body.start : node.value.start;
235
+ const end = body.type === 'BlockStatement' ? body.end : node.value.end;
236
+ scopes.push({ start, end, name: node.key.name });
237
+ }
238
+ if (body?.body)
239
+ collectFunctionScopes(body.body, scopes);
240
+ }
241
+ return;
242
+ case 'MethodDefinition':
243
+ // class Foo { bar() {} }
244
+ if (node.key?.name && node.value?.body) {
245
+ scopes.push({
246
+ start: node.value.body.start,
247
+ end: node.value.body.end,
248
+ name: node.key.name
249
+ });
250
+ if (node.value.body?.body)
251
+ collectFunctionScopes(node.value.body.body, scopes);
252
+ }
253
+ return;
254
+ case 'ClassDeclaration':
255
+ case 'ClassExpression':
256
+ if (node.body?.body) {
257
+ for (const member of node.body.body) {
258
+ collectFunctionScopesFromNode(member, scopes);
259
+ }
260
+ }
261
+ return;
262
+ case 'IfStatement':
263
+ if (node.consequent)
264
+ collectFunctionScopesFromNode(node.consequent, scopes);
265
+ if (node.alternate)
266
+ collectFunctionScopesFromNode(node.alternate, scopes);
267
+ return;
268
+ case 'BlockStatement':
269
+ if (node.body)
270
+ collectFunctionScopes(node.body, scopes);
271
+ return;
272
+ case 'ForStatement':
273
+ case 'ForInStatement':
274
+ case 'ForOfStatement':
275
+ case 'WhileStatement':
276
+ case 'DoWhileStatement':
277
+ if (node.body)
278
+ collectFunctionScopesFromNode(node.body, scopes);
279
+ return;
280
+ case 'TryStatement':
281
+ if (node.block)
282
+ collectFunctionScopesFromNode(node.block, scopes);
283
+ if (node.handler?.body)
284
+ collectFunctionScopesFromNode(node.handler.body, scopes);
285
+ if (node.finalizer)
286
+ collectFunctionScopesFromNode(node.finalizer, scopes);
287
+ return;
288
+ case 'SwitchStatement':
289
+ for (const c of node.cases || []) {
290
+ if (c.consequent)
291
+ collectFunctionScopes(c.consequent, scopes);
292
+ }
293
+ return;
294
+ case 'ReturnStatement':
295
+ if (node.argument)
296
+ collectFunctionScopesFromNode(node.argument, scopes);
297
+ return;
298
+ case 'CallExpression':
299
+ // e.g. onMount(() => { gg() })
300
+ for (const arg of node.arguments || []) {
301
+ if (arg.type === 'ArrowFunctionExpression' || arg.type === 'FunctionExpression') {
302
+ // Anonymous callback — don't add a scope (no name to show),
303
+ // but recurse for nested named functions
304
+ if (arg.body?.body)
305
+ collectFunctionScopes(arg.body.body, scopes);
306
+ }
307
+ }
308
+ return;
90
309
  }
91
- return false;
92
310
  }
93
311
  /**
94
- * Find the enclosing function name for a given position in source code.
95
- * Scans backwards from the position looking for function/method declarations.
312
+ * Find the innermost enclosing function name for a byte position
313
+ * using the pre-built function scope map.
314
+ * Returns empty string if not inside any named function.
96
315
  */
97
- function findEnclosingFunction(code, position) {
98
- // Look backwards from the gg( call for the nearest function declaration
99
- const before = code.slice(0, position);
100
- // Try several patterns, take the closest (last) match
101
- // Named function: function handleClick(
102
- // Arrow in variable: const handleClick = (...) =>
103
- // Arrow in variable: let handleClick = (...) =>
104
- // Method shorthand: handleClick() {
105
- // Method: handleClick: function(
106
- // Class method: async handleClick(
107
- const patterns = [
108
- // function declarations: function foo(
109
- /function\s+([a-zA-Z_$][a-zA-Z0-9_$]*)\s*\(/g,
110
- // const/let/var assignment to arrow or function: const foo =
111
- /(?:const|let|var)\s+([a-zA-Z_$][a-zA-Z0-9_$]*)\s*=/g,
112
- // object method shorthand: foo() { or async foo() {
113
- /(?:async\s+)?([a-zA-Z_$][a-zA-Z0-9_$]*)\s*\([^)]*\)\s*\{/g,
114
- // object property function: foo: function
115
- /([a-zA-Z_$][a-zA-Z0-9_$]*)\s*:\s*(?:async\s+)?function/g
116
- ];
117
- let closestName = '';
118
- let closestPos = -1;
119
- for (const pattern of patterns) {
120
- let match;
121
- while ((match = pattern.exec(before)) !== null) {
122
- const name = match[1];
123
- // Skip common false positives
124
- if ([
125
- 'if',
126
- 'for',
127
- 'while',
128
- 'switch',
129
- 'catch',
130
- 'return',
131
- 'import',
132
- 'export',
133
- 'from',
134
- 'new',
135
- 'typeof',
136
- 'instanceof',
137
- 'void',
138
- 'delete',
139
- 'throw',
140
- 'case',
141
- 'else',
142
- 'in',
143
- 'of',
144
- 'do',
145
- 'try',
146
- 'class',
147
- 'super',
148
- 'this',
149
- 'with',
150
- 'yield',
151
- 'await',
152
- 'debugger',
153
- 'default'
154
- ].includes(name)) {
155
- continue;
316
+ export function findEnclosingFunctionFromScopes(pos, scopes) {
317
+ // Scopes can be nested; find the innermost (smallest range) that contains pos
318
+ let bestName = '';
319
+ let bestSize = Infinity;
320
+ for (const scope of scopes) {
321
+ if (pos >= scope.start && pos < scope.end) {
322
+ const size = scope.end - scope.start;
323
+ if (size < bestSize) {
324
+ bestSize = size;
325
+ bestName = scope.name;
156
326
  }
157
- if (match.index > closestPos) {
158
- closestPos = match.index;
159
- closestName = name;
327
+ }
328
+ }
329
+ return bestName;
330
+ }
331
+ /**
332
+ * Recursively walk a Svelte AST fragment to collect template expression ranges.
333
+ */
334
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
335
+ function walkFragment(fragment, ranges) {
336
+ if (!fragment?.nodes)
337
+ return;
338
+ for (const node of fragment.nodes) {
339
+ walkNode(node, ranges);
340
+ }
341
+ }
342
+ /**
343
+ * Walk a single AST node, collecting expression ranges for template code.
344
+ */
345
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
346
+ function walkNode(node, ranges) {
347
+ if (!node || typeof node !== 'object')
348
+ return;
349
+ switch (node.type) {
350
+ // Template expression tags: {expr}
351
+ case 'ExpressionTag':
352
+ case 'HtmlTag':
353
+ case 'RenderTag':
354
+ case 'AttachTag':
355
+ if (node.expression && node.start != null && node.end != null) {
356
+ ranges.push({ start: node.start, end: node.end, context: 'template' });
357
+ }
358
+ return; // expressions are leaf nodes for our purposes
359
+ // Block tags with expressions: {#if expr}, {#each expr}, {#await expr}, {#key expr}
360
+ case 'IfBlock':
361
+ if (node.test)
362
+ addExprRange(node.test, ranges);
363
+ walkFragment(node.consequent, ranges);
364
+ if (node.alternate)
365
+ walkFragment(node.alternate, ranges);
366
+ return;
367
+ case 'EachBlock':
368
+ if (node.expression)
369
+ addExprRange(node.expression, ranges);
370
+ if (node.key)
371
+ addExprRange(node.key, ranges);
372
+ walkFragment(node.body, ranges);
373
+ if (node.fallback)
374
+ walkFragment(node.fallback, ranges);
375
+ return;
376
+ case 'AwaitBlock':
377
+ if (node.expression)
378
+ addExprRange(node.expression, ranges);
379
+ walkFragment(node.pending, ranges);
380
+ walkFragment(node.then, ranges);
381
+ walkFragment(node.catch, ranges);
382
+ return;
383
+ case 'KeyBlock':
384
+ if (node.expression)
385
+ addExprRange(node.expression, ranges);
386
+ walkFragment(node.fragment, ranges);
387
+ return;
388
+ case 'SnippetBlock':
389
+ walkFragment(node.body, ranges);
390
+ return;
391
+ // {@const ...} — contains a declaration, not a simple expression
392
+ case 'ConstTag':
393
+ if (node.declaration) {
394
+ ranges.push({ start: node.start, end: node.end, context: 'template' });
160
395
  }
396
+ return;
397
+ // Elements and components — walk attributes + children
398
+ case 'RegularElement':
399
+ case 'Component':
400
+ case 'SvelteElement':
401
+ case 'SvelteComponent':
402
+ case 'SvelteBody':
403
+ case 'SvelteWindow':
404
+ case 'SvelteDocument':
405
+ case 'SvelteHead':
406
+ case 'SvelteSelf':
407
+ case 'SvelteFragment':
408
+ case 'SvelteBoundary':
409
+ case 'TitleElement':
410
+ case 'SlotElement':
411
+ walkAttributes(node.attributes, ranges);
412
+ walkFragment(node.fragment, ranges);
413
+ return;
414
+ // Text nodes — skip (prose, not code)
415
+ case 'Text':
416
+ case 'Comment':
417
+ return;
418
+ default:
419
+ // Unknown node type — try to walk children defensively
420
+ if (node.fragment)
421
+ walkFragment(node.fragment, ranges);
422
+ if (node.children)
423
+ walkFragment({ nodes: node.children }, ranges);
424
+ return;
425
+ }
426
+ }
427
+ /**
428
+ * Walk element attributes to find expression ranges.
429
+ */
430
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
431
+ function walkAttributes(attrs, ranges) {
432
+ if (!attrs)
433
+ return;
434
+ for (const attr of attrs) {
435
+ switch (attr.type) {
436
+ case 'Attribute':
437
+ // value can be: true | ExpressionTag | Array<Text | ExpressionTag>
438
+ if (attr.value === true)
439
+ break;
440
+ if (Array.isArray(attr.value)) {
441
+ for (const part of attr.value) {
442
+ if (part.type === 'ExpressionTag') {
443
+ ranges.push({ start: part.start, end: part.end, context: 'template' });
444
+ }
445
+ }
446
+ }
447
+ else if (attr.value?.type === 'ExpressionTag') {
448
+ ranges.push({ start: attr.value.start, end: attr.value.end, context: 'template' });
449
+ }
450
+ break;
451
+ case 'SpreadAttribute':
452
+ if (attr.expression) {
453
+ ranges.push({ start: attr.start, end: attr.end, context: 'template' });
454
+ }
455
+ break;
456
+ // Directives: bind:, class:, style:, on:, use:, transition:, animate:, attach:
457
+ case 'BindDirective':
458
+ case 'ClassDirective':
459
+ case 'StyleDirective':
460
+ case 'OnDirective':
461
+ case 'UseDirective':
462
+ case 'TransitionDirective':
463
+ case 'AnimateDirective':
464
+ if (attr.expression) {
465
+ addExprRange(attr.expression, ranges);
466
+ }
467
+ // StyleDirective value can be an array
468
+ if (attr.value && Array.isArray(attr.value)) {
469
+ for (const part of attr.value) {
470
+ if (part.type === 'ExpressionTag') {
471
+ ranges.push({ start: part.start, end: part.end, context: 'template' });
472
+ }
473
+ }
474
+ }
475
+ break;
476
+ case 'AttachTag':
477
+ if (attr.expression) {
478
+ ranges.push({ start: attr.start, end: attr.end, context: 'template' });
479
+ }
480
+ break;
161
481
  }
162
482
  }
163
- return closestName;
483
+ }
484
+ /**
485
+ * Add a template expression range from an AST expression node.
486
+ */
487
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
488
+ function addExprRange(expr, ranges) {
489
+ if (expr && expr.start != null && expr.end != null) {
490
+ ranges.push({ start: expr.start, end: expr.end, context: 'template' });
491
+ }
492
+ }
493
+ /**
494
+ * Check if a character position falls within any of the given code ranges.
495
+ * Returns the matching range, or undefined if not in any range.
496
+ */
497
+ function findCodeRange(pos, ranges) {
498
+ for (const r of ranges) {
499
+ if (pos >= r.start && pos < r.end)
500
+ return r;
501
+ }
502
+ return undefined;
164
503
  }
165
504
  /**
166
505
  * Compute 1-based line number and column for a character offset in source code.
@@ -242,7 +581,7 @@ function findMatchingParen(code, openPos) {
242
581
  /**
243
582
  * Escape a string for embedding as a single-quoted JS string literal.
244
583
  */
245
- function escapeForString(s) {
584
+ export function escapeForString(s) {
246
585
  return s.replace(/\\/g, '\\\\').replace(/'/g, "\\'").replace(/\n/g, '\\n').replace(/\r/g, '\\r');
247
586
  }
248
587
  /**
@@ -251,78 +590,136 @@ function escapeForString(s) {
251
590
  * Handles:
252
591
  * - bare gg(expr) → gg._ns({ns, file, line, col, src: 'expr'}, expr)
253
592
  * - gg.ns('label', expr) → gg._ns({ns, file, line, col, src: 'expr'}, expr)
593
+ * - label supports template variables: $NS, $FN, $FILE, $LINE, $COL
594
+ * - plain label (no variables) is used as-is (no auto @fn append)
254
595
  * - gg.enable, gg.disable, gg.clearPersist, gg._onLog, gg._ns → left untouched
255
596
  * - gg inside strings and comments → left untouched
597
+ *
598
+ * For .svelte files, `svelteInfo` (from `collectCodeRanges()`) determines which
599
+ * positions contain JS code and provides AST-based function scope detection.
600
+ * Script ranges use `{...}` object literal syntax; template ranges use `gg._o()`
601
+ * function-call syntax (no braces in Svelte markup). Positions outside any code
602
+ * range (e.g. prose text) are skipped.
603
+ *
604
+ * For .js/.ts files, `jsFunctionScopes` (from `parseJavaScript()`) provides
605
+ * AST-based function scope detection (no regex fallback).
256
606
  */
257
- function transformGgCalls(code, shortPath, filePath, scriptRanges) {
607
+ export function transformGgCalls(code, shortPath, filePath, svelteInfo, jsFunctionScopes) {
258
608
  // We use a manual scan approach to correctly handle strings and comments.
259
609
  const result = [];
260
610
  let lastIndex = 0;
261
611
  let modified = false;
262
612
  const escapedFile = escapeForString(filePath);
613
+ /**
614
+ * Find the code range containing `pos`, or undefined if outside all ranges.
615
+ * For non-.svelte files (no svelteInfo), returns a synthetic 'script' range.
616
+ */
617
+ function rangeAt(pos) {
618
+ if (!svelteInfo)
619
+ return { start: 0, end: code.length, context: 'script' };
620
+ return findCodeRange(pos, svelteInfo.ranges);
621
+ }
622
+ /**
623
+ * Find the enclosing function name for a position.
624
+ * - .svelte files: uses estree AST function scope map from svelte.parse()
625
+ * - .js/.ts files: uses estree AST function scope map from acorn.parse()
626
+ * - template code ranges: always returns '' (no enclosing function from script)
627
+ */
628
+ function getFunctionName(pos, range) {
629
+ if (range.context === 'template')
630
+ return '';
631
+ if (svelteInfo)
632
+ return findEnclosingFunctionFromScopes(pos, svelteInfo.functionScopes);
633
+ if (jsFunctionScopes)
634
+ return findEnclosingFunctionFromScopes(pos, jsFunctionScopes);
635
+ return ''; // Should not reach here unless both svelteInfo and jsFunctionScopes are undefined
636
+ }
637
+ /**
638
+ * Build the options argument for gg._ns().
639
+ * Inside <script>: {ns:'...',file:'...',line:N,col:N} (object literal)
640
+ * In template: gg._o('...','...',N,N) (function call — no braces)
641
+ */
642
+ function buildOptions(range, ns, line, col, src) {
643
+ if (range.context === 'script') {
644
+ return src
645
+ ? `{ns:'${ns}',file:'${escapedFile}',line:${line},col:${col},src:'${src}'}`
646
+ : `{ns:'${ns}',file:'${escapedFile}',line:${line},col:${col}}`;
647
+ }
648
+ return src
649
+ ? `gg._o('${ns}','${escapedFile}',${line},${col},'${src}')`
650
+ : `gg._o('${ns}','${escapedFile}',${line},${col})`;
651
+ }
263
652
  // States for string/comment tracking
264
653
  let i = 0;
265
654
  while (i < code.length) {
266
- // Skip single-line comments
267
- if (code[i] === '/' && code[i + 1] === '/') {
268
- const end = code.indexOf('\n', i);
269
- i = end === -1 ? code.length : end + 1;
270
- continue;
271
- }
272
- // Skip multi-line comments
273
- if (code[i] === '/' && code[i + 1] === '*') {
274
- const end = code.indexOf('*/', i + 2);
275
- i = end === -1 ? code.length : end + 2;
276
- continue;
277
- }
278
- // Skip template literals (backticks)
279
- if (code[i] === '`') {
280
- i++;
281
- let depth = 0;
282
- while (i < code.length) {
283
- if (code[i] === '\\') {
284
- i += 2;
285
- continue;
286
- }
287
- if (code[i] === '$' && code[i + 1] === '{') {
288
- depth++;
289
- i += 2;
290
- continue;
291
- }
292
- if (code[i] === '}' && depth > 0) {
293
- depth--;
294
- i++;
295
- continue;
296
- }
297
- if (code[i] === '`' && depth === 0) {
655
+ // For .svelte files, only apply JS string/comment/backtick skipping inside
656
+ // code ranges (script blocks + template expressions). Outside code ranges,
657
+ // characters like ' " ` // /* are just HTML prose — NOT JS syntax.
658
+ // e.g. "Eruda's" contains an apostrophe that is NOT a JS string delimiter.
659
+ const inCodeRange = !svelteInfo || !!rangeAt(i);
660
+ if (inCodeRange) {
661
+ // Skip single-line comments
662
+ if (code[i] === '/' && code[i + 1] === '/') {
663
+ const end = code.indexOf('\n', i);
664
+ i = end === -1 ? code.length : end + 1;
665
+ continue;
666
+ }
667
+ // Skip multi-line comments
668
+ if (code[i] === '/' && code[i + 1] === '*') {
669
+ const end = code.indexOf('*/', i + 2);
670
+ i = end === -1 ? code.length : end + 2;
671
+ continue;
672
+ }
673
+ // Skip template literals (backticks)
674
+ if (code[i] === '`') {
675
+ i++;
676
+ let depth = 0;
677
+ while (i < code.length) {
678
+ if (code[i] === '\\') {
679
+ i += 2;
680
+ continue;
681
+ }
682
+ if (code[i] === '$' && code[i + 1] === '{') {
683
+ depth++;
684
+ i += 2;
685
+ continue;
686
+ }
687
+ if (code[i] === '}' && depth > 0) {
688
+ depth--;
689
+ i++;
690
+ continue;
691
+ }
692
+ if (code[i] === '`' && depth === 0) {
693
+ i++;
694
+ break;
695
+ }
298
696
  i++;
299
- break;
300
697
  }
301
- i++;
698
+ continue;
302
699
  }
303
- continue;
304
- }
305
- // Skip strings (single and double quotes)
306
- if (code[i] === '"' || code[i] === "'") {
307
- const quote = code[i];
308
- i++;
309
- while (i < code.length) {
310
- if (code[i] === '\\') {
311
- i += 2;
312
- continue;
313
- }
314
- if (code[i] === quote) {
700
+ // Skip strings (single and double quotes)
701
+ if (code[i] === '"' || code[i] === "'") {
702
+ const quote = code[i];
703
+ i++;
704
+ while (i < code.length) {
705
+ if (code[i] === '\\') {
706
+ i += 2;
707
+ continue;
708
+ }
709
+ if (code[i] === quote) {
710
+ i++;
711
+ break;
712
+ }
315
713
  i++;
316
- break;
317
714
  }
318
- i++;
715
+ continue;
319
716
  }
320
- continue;
321
717
  }
322
718
  // Look for 'gg' pattern — could be gg( or gg.ns(
323
719
  if (code[i] === 'g' && code[i + 1] === 'g') {
324
- // In .svelte files, skip gg outside <script> blocks
325
- if (scriptRanges && !isInRanges(i, scriptRanges)) {
720
+ // In .svelte files, skip gg outside code ranges (prose text, etc.)
721
+ const range = rangeAt(i);
722
+ if (!range) {
326
723
  i++;
327
724
  continue;
328
725
  }
@@ -335,7 +732,7 @@ function transformGgCalls(code, shortPath, filePath, scriptRanges) {
335
732
  // Case 1: gg.ns('label', ...) → gg._ns({ns: 'label', file, line, col, src}, ...)
336
733
  if (code.slice(i + 2, i + 6) === '.ns(') {
337
734
  const { line, col } = getLineCol(code, i);
338
- const fnName = findEnclosingFunction(code, i);
735
+ const fnName = getFunctionName(i, range);
339
736
  const openParenPos = i + 5; // position of '(' in 'gg.ns('
340
737
  // Find matching closing paren for the entire gg.ns(...) call
341
738
  const closeParenPos = findMatchingParen(code, openParenPos);
@@ -357,33 +754,36 @@ function transformGgCalls(code, shortPath, filePath, scriptRanges) {
357
754
  }
358
755
  // j now points to closing quote
359
756
  const nsLabelRaw = code.slice(afterNsParen + 1, j);
360
- const escapedNs = escapeForString(nsLabelRaw);
361
- // Build callpoint: use user's ns label + enclosing function
362
- const callpoint = `${escapedNs}${fnName ? `@${fnName}` : ''}`;
757
+ // Build callpoint: substitute $NS/$FN/$FILE/$LINE/$COL template variables.
758
+ // The auto-generated callpoint (file@fn) is what bare gg() would produce.
759
+ const autoCallpoint = `${shortPath}${fnName ? `@${fnName}` : ''}`;
760
+ const callpoint = escapeForString(nsLabelRaw
761
+ .replace(/\$NS/g, autoCallpoint)
762
+ .replace(/\$FN/g, fnName)
763
+ .replace(/\$FILE/g, shortPath)
764
+ .replace(/\$LINE/g, String(line))
765
+ .replace(/\$COL/g, String(col)));
363
766
  // Check if there are more args after the string
364
767
  const afterClosingQuote = j + 1;
365
768
  let k = afterClosingQuote;
366
769
  while (k < code.length && /\s/.test(code[k]))
367
770
  k++;
368
771
  if (code[k] === ')') {
369
- // gg.ns('label') → gg._ns({...})
370
- const optionsObj = `{ns:'${callpoint}',file:'${escapedFile}',line:${line},col:${col}}`;
772
+ // gg.ns('label') → gg._ns(opts)
371
773
  result.push(code.slice(lastIndex, i));
372
- result.push(`gg._ns(${optionsObj})`);
774
+ result.push(`gg._ns(${buildOptions(range, callpoint, line, col)})`);
373
775
  lastIndex = k + 1;
374
776
  i = k + 1;
375
777
  }
376
778
  else if (code[k] === ',') {
377
- // gg.ns('label', args...) → gg._ns({..., src:'args source'}, args...)
378
- // Extract source text of remaining args (after the comma)
779
+ // gg.ns('label', args...) → gg._ns(opts, args...)
379
780
  let argsStart = k + 1;
380
781
  while (argsStart < closeParenPos && /\s/.test(code[argsStart]))
381
782
  argsStart++;
382
783
  const argsSrc = code.slice(argsStart, closeParenPos).trim();
383
784
  const escapedSrc = escapeForString(argsSrc);
384
- const optionsObj = `{ns:'${callpoint}',file:'${escapedFile}',line:${line},col:${col},src:'${escapedSrc}'}`;
385
785
  result.push(code.slice(lastIndex, i));
386
- result.push(`gg._ns(${optionsObj}, `);
786
+ result.push(`gg._ns(${buildOptions(range, callpoint, line, col, escapedSrc)}, `);
387
787
  lastIndex = k + 1; // skip past the comma, keep args as-is
388
788
  i = k + 1;
389
789
  }
@@ -399,7 +799,44 @@ function transformGgCalls(code, shortPath, filePath, scriptRanges) {
399
799
  i += 6;
400
800
  continue;
401
801
  }
402
- // Skip other gg.* calls (gg.enable, gg.disable, gg._ns, gg._onLog, etc.)
802
+ // Case 1b: gg.warn/error/table/trace/assert gg._warn/_error/_table/_trace/_assert
803
+ // These methods are rewritten like bare gg() but with their internal variant.
804
+ const dotMethodMatch = code
805
+ .slice(i + 2)
806
+ .match(/^\.(warn|error|table|trace|assert|time|timeLog|timeEnd)\(/);
807
+ if (dotMethodMatch) {
808
+ const methodName = dotMethodMatch[1];
809
+ const internalName = `_${methodName}`;
810
+ const methodCallLen = 2 + 1 + methodName.length + 1; // 'gg' + '.' + method + '('
811
+ const openParenPos = i + methodCallLen - 1;
812
+ const { line, col } = getLineCol(code, i);
813
+ const fnName = getFunctionName(i, range);
814
+ const callpoint = `${shortPath}${fnName ? `@${fnName}` : ''}`;
815
+ const escapedNs = escapeForString(callpoint);
816
+ const closeParenPos = findMatchingParen(code, openParenPos);
817
+ if (closeParenPos === -1) {
818
+ i += methodCallLen;
819
+ continue;
820
+ }
821
+ const argsText = code.slice(openParenPos + 1, closeParenPos).trim();
822
+ result.push(code.slice(lastIndex, i));
823
+ if (argsText === '') {
824
+ // gg.warn() → gg._warn(opts)
825
+ result.push(`gg.${internalName}(${buildOptions(range, escapedNs, line, col)})`);
826
+ lastIndex = closeParenPos + 1;
827
+ i = closeParenPos + 1;
828
+ }
829
+ else {
830
+ // gg.warn(expr) → gg._warn(opts, expr)
831
+ const escapedSrc = escapeForString(argsText);
832
+ result.push(`gg.${internalName}(${buildOptions(range, escapedNs, line, col, escapedSrc)}, `);
833
+ lastIndex = openParenPos + 1; // keep original args
834
+ i = openParenPos + 1;
835
+ }
836
+ modified = true;
837
+ continue;
838
+ }
839
+ // Skip other gg.* calls (gg.enable, gg.disable, gg._ns, gg._onLog, gg.time, etc.)
403
840
  if (code[i + 2] === '.') {
404
841
  i += 3;
405
842
  continue;
@@ -407,7 +844,7 @@ function transformGgCalls(code, shortPath, filePath, scriptRanges) {
407
844
  // Case 2: bare gg(...) → gg._ns({ns, file, line, col, src}, ...)
408
845
  if (code[i + 2] === '(') {
409
846
  const { line, col } = getLineCol(code, i);
410
- const fnName = findEnclosingFunction(code, i);
847
+ const fnName = getFunctionName(i, range);
411
848
  const callpoint = `${shortPath}${fnName ? `@${fnName}` : ''}`;
412
849
  const escapedNs = escapeForString(callpoint);
413
850
  const openParenPos = i + 2; // position of '(' in 'gg('
@@ -421,17 +858,15 @@ function transformGgCalls(code, shortPath, filePath, scriptRanges) {
421
858
  // Emit everything before this match
422
859
  result.push(code.slice(lastIndex, i));
423
860
  if (argsText === '') {
424
- // gg() → gg._ns({...})
425
- const optionsObj = `{ns:'${escapedNs}',file:'${escapedFile}',line:${line},col:${col}}`;
426
- result.push(`gg._ns(${optionsObj})`);
861
+ // gg() → gg._ns(opts)
862
+ result.push(`gg._ns(${buildOptions(range, escapedNs, line, col)})`);
427
863
  lastIndex = closeParenPos + 1;
428
864
  i = closeParenPos + 1;
429
865
  }
430
866
  else {
431
- // gg(expr) → gg._ns({..., src:'expr'}, expr)
867
+ // gg(expr) → gg._ns(opts, expr)
432
868
  const escapedSrc = escapeForString(argsText);
433
- const optionsObj = `{ns:'${escapedNs}',file:'${escapedFile}',line:${line},col:${col},src:'${escapedSrc}'}`;
434
- result.push(`gg._ns(${optionsObj}, `);
869
+ result.push(`gg._ns(${buildOptions(range, escapedNs, line, col, escapedSrc)}, `);
435
870
  lastIndex = openParenPos + 1; // keep original args
436
871
  i = openParenPos + 1;
437
872
  }