@leftium/gg 0.0.31 → 0.0.34

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,11 +1,14 @@
1
+ import { parse } from 'svelte/compiler';
2
+ import * as acorn from 'acorn';
3
+ import { tsPlugin } from '@sveltejs/acorn-typescript';
1
4
  /**
2
- * Vite plugin that rewrites bare `gg(...)` calls to `gg.ns('callpoint', ...)`
3
- * at build time. This gives each call site a unique namespace with zero runtime
4
- * cost no stack trace parsing needed.
5
+ * Vite plugin that rewrites `gg(...)` and `gg.ns(...)` calls to
6
+ * `gg._ns({ns, file, line, col}, ...)` at build time. This gives each call
7
+ * site a unique namespace plus source location metadata for open-in-editor
8
+ * support, with zero runtime cost — no stack trace parsing needed.
5
9
  *
6
- * Works in both dev and prod. When the plugin is installed, `gg.ns()` is called
7
- * with the callpoint baked in as a string literal. Without the plugin, gg()
8
- * falls back to runtime stack parsing in dev and bare `gg:` in prod.
10
+ * Works in both dev and prod. Without the plugin, gg() falls back to runtime
11
+ * stack parsing in dev and bare `gg:` in prod.
9
12
  *
10
13
  * @example
11
14
  * // vite.config.ts
@@ -20,6 +23,7 @@ export default function ggCallSitesPlugin(options = {}) {
20
23
  const srcRootRegex = new RegExp(srcRootPattern, 'i');
21
24
  return {
22
25
  name: 'gg-call-sites',
26
+ enforce: 'pre',
23
27
  config() {
24
28
  // Set a compile-time flag so gg() can detect the plugin is installed.
25
29
  // Vite replaces all occurrences of __GG_TAG_PLUGIN__ with true at build time,
@@ -35,109 +39,607 @@ export default function ggCallSitesPlugin(options = {}) {
35
39
  if (!/\.(js|ts|svelte|jsx|tsx|mjs|mts)(\?.*)?$/.test(id))
36
40
  return null;
37
41
  // Quick bail: no gg calls in this file
38
- if (!code.includes('gg('))
42
+ if (!code.includes('gg(') && !code.includes('gg.ns('))
39
43
  return null;
40
44
  // Don't transform gg's own source files
41
45
  if (id.includes('/lib/gg.') || id.includes('/lib/debug'))
42
46
  return null;
43
- // Build the short callpoint from the file path
47
+ // Build the short callpoint from the file path (strips src/ prefix)
44
48
  // e.g. "/Users/me/project/src/routes/+page.svelte" → "routes/+page.svelte"
45
49
  const shortPath = id.replace(srcRootRegex, '');
46
- return transformGgCalls(code, shortPath);
50
+ // Build the file path preserving src/ prefix (for open-in-editor)
51
+ // e.g. "/Users/me/project/src/routes/+page.svelte" → "src/routes/+page.svelte"
52
+ // $1 captures "/src/" or "/chunks/", so strip the leading slash
53
+ const filePath = id.replace(srcRootRegex, '$1').replace(/^\//, '');
54
+ // For .svelte files, use svelte.parse() AST to find code ranges
55
+ // and function scopes. This distinguishes real JS expressions
56
+ // ({gg()}, onclick, etc.) from prose text mentioning "gg()",
57
+ // and uses estree AST for function name detection (no regex).
58
+ let svelteInfo;
59
+ let jsFunctionScopes;
60
+ if (/\.svelte(\?.*)?$/.test(id)) {
61
+ svelteInfo = collectCodeRanges(code);
62
+ if (svelteInfo.ranges.length === 0)
63
+ return null;
64
+ }
65
+ else {
66
+ // For .js/.ts files, parse with acorn to extract function scopes
67
+ jsFunctionScopes = parseJavaScript(code);
68
+ }
69
+ return transformGgCalls(code, shortPath, filePath, svelteInfo, jsFunctionScopes);
47
70
  }
48
71
  };
49
72
  }
50
73
  /**
51
- * Find the enclosing function name for a given position in source code.
52
- * Scans backwards from the position looking for function/method declarations.
74
+ * Parse JavaScript/TypeScript code using acorn to extract function scopes.
75
+ * Returns function scope ranges for accurate function name detection in .js/.ts files.
76
+ * Uses @sveltejs/acorn-typescript plugin to handle TypeScript syntax.
77
+ *
78
+ * For .svelte files, use `collectCodeRanges()` instead (which uses svelte.parse()).
53
79
  */
54
- function findEnclosingFunction(code, position) {
55
- // Look backwards from the gg( call for the nearest function declaration
56
- const before = code.slice(0, position);
57
- // Try several patterns, take the closest (last) match
58
- // Named function: function handleClick(
59
- // Arrow in variable: const handleClick = (...) =>
60
- // Arrow in variable: let handleClick = (...) =>
61
- // Method shorthand: handleClick() {
62
- // Method: handleClick: function(
63
- // Class method: async handleClick(
64
- const patterns = [
65
- // function declarations: function foo(
66
- /function\s+([a-zA-Z_$][a-zA-Z0-9_$]*)\s*\(/g,
67
- // const/let/var assignment to arrow or function: const foo =
68
- /(?:const|let|var)\s+([a-zA-Z_$][a-zA-Z0-9_$]*)\s*=/g,
69
- // object method shorthand: foo() { or async foo() {
70
- /(?:async\s+)?([a-zA-Z_$][a-zA-Z0-9_$]*)\s*\([^)]*\)\s*\{/g,
71
- // object property function: foo: function
72
- /([a-zA-Z_$][a-zA-Z0-9_$]*)\s*:\s*(?:async\s+)?function/g
73
- ];
74
- let closestName = '';
75
- let closestPos = -1;
76
- for (const pattern of patterns) {
77
- let match;
78
- while ((match = pattern.exec(before)) !== null) {
79
- const name = match[1];
80
- // Skip common false positives
81
- if ([
82
- 'if',
83
- 'for',
84
- 'while',
85
- 'switch',
86
- 'catch',
87
- 'return',
88
- 'import',
89
- 'export',
90
- 'from',
91
- 'new',
92
- 'typeof',
93
- 'instanceof',
94
- 'void',
95
- 'delete',
96
- 'throw',
97
- 'case',
98
- 'else',
99
- 'in',
100
- 'of',
101
- 'do',
102
- 'try',
103
- 'class',
104
- 'super',
105
- 'this',
106
- 'with',
107
- 'yield',
108
- 'await',
109
- 'debugger',
110
- 'default'
111
- ].includes(name)) {
112
- continue;
80
+ export function parseJavaScript(code) {
81
+ try {
82
+ // Parse as ES2022+ with TypeScript support
83
+ // sourceType: 'module' allows import/export, 'script' for regular scripts
84
+ // NOTE: @sveltejs/acorn-typescript REQUIRES locations: true
85
+ const parser = acorn.Parser.extend(tsPlugin());
86
+ const ast = parser.parse(code, {
87
+ ecmaVersion: 'latest',
88
+ sourceType: 'module',
89
+ locations: true, // Required by @sveltejs/acorn-typescript
90
+ ranges: true // Enable byte ranges for AST nodes
91
+ });
92
+ const scopes = [];
93
+ // Reuse the same AST walker we built for Svelte
94
+ collectFunctionScopes(ast.body, scopes);
95
+ scopes.sort((a, b) => a.start - b.start);
96
+ return scopes;
97
+ }
98
+ catch {
99
+ // If acorn can't parse it, fall back to empty scopes.
100
+ // The file might be malformed or use syntax we don't support yet.
101
+ return [];
102
+ }
103
+ }
104
+ /**
105
+ * Use `svelte.parse()` to collect all code ranges and function scopes in a .svelte file.
106
+ *
107
+ * Code ranges identify where JS expressions live:
108
+ * - `<script>` blocks (context: 'script')
109
+ * - Template expressions: `{expr}`, `onclick={expr}`, `bind:value={expr}`,
110
+ * `class:name={expr}`, `{#if expr}`, `{#each expr}`, etc. (context: 'template')
111
+ *
112
+ * Function scopes are extracted from the estree AST in script blocks, mapping
113
+ * byte ranges to enclosing function names. This replaces regex-based function
114
+ * detection for .svelte files.
115
+ *
116
+ * Text nodes (prose) are NOT included, so `gg()` in `<p>text gg()</p>` is never transformed.
117
+ */
118
+ export function collectCodeRanges(code) {
119
+ try {
120
+ const ast = parse(code, { modern: true });
121
+ const ranges = [];
122
+ const functionScopes = [];
123
+ // Script blocks (instance + module)
124
+ // The Svelte AST Program node has start/end at runtime but TypeScript's
125
+ // estree Program type doesn't declare them — cast through any.
126
+ if (ast.instance) {
127
+ const content = ast.instance.content;
128
+ ranges.push({ start: content.start, end: content.end, context: 'script' });
129
+ collectFunctionScopes(ast.instance.content.body, functionScopes);
130
+ }
131
+ if (ast.module) {
132
+ const content = ast.module.content;
133
+ ranges.push({ start: content.start, end: content.end, context: 'script' });
134
+ collectFunctionScopes(ast.module.content.body, functionScopes);
135
+ }
136
+ // Walk the template fragment to find all expression positions
137
+ walkFragment(ast.fragment, ranges);
138
+ // Sort function scopes by start position for efficient lookup
139
+ functionScopes.sort((a, b) => a.start - b.start);
140
+ return { ranges, functionScopes };
141
+ }
142
+ catch {
143
+ // If svelte.parse() fails, the Svelte compiler will also reject this file,
144
+ // so there's no point transforming gg() calls — return empty.
145
+ return { ranges: [], functionScopes: [] };
146
+ }
147
+ }
148
+ /**
149
+ * Walk an estree AST body to collect function scope ranges.
150
+ * Extracts function names from:
151
+ * - FunctionDeclaration: `function foo() {}`
152
+ * - VariableDeclarator with ArrowFunctionExpression/FunctionExpression: `const foo = () => {}`
153
+ * - Property with FunctionExpression: `{ method() {} }` or `{ prop: function() {} }`
154
+ * - MethodDefinition: `class Foo { bar() {} }`
155
+ */
156
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
157
+ function collectFunctionScopes(nodes, scopes) {
158
+ if (!nodes)
159
+ return;
160
+ for (const node of nodes) {
161
+ collectFunctionScopesFromNode(node, scopes);
162
+ }
163
+ }
164
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
165
+ function collectFunctionScopesFromNode(node, scopes) {
166
+ if (!node || typeof node !== 'object' || !node.type)
167
+ return;
168
+ switch (node.type) {
169
+ case 'ExportNamedDeclaration':
170
+ case 'ExportDefaultDeclaration':
171
+ // export function foo() {} or export default function foo() {}
172
+ if (node.declaration) {
173
+ collectFunctionScopesFromNode(node.declaration, scopes);
174
+ }
175
+ return;
176
+ case 'FunctionDeclaration':
177
+ if (node.id?.name && node.body) {
178
+ scopes.push({ start: node.body.start, end: node.body.end, name: node.id.name });
179
+ }
180
+ // Recurse into the function body for nested functions
181
+ if (node.body?.body)
182
+ collectFunctionScopes(node.body.body, scopes);
183
+ return;
184
+ case 'VariableDeclaration':
185
+ for (const decl of node.declarations || []) {
186
+ collectFunctionScopesFromNode(decl, scopes);
187
+ }
188
+ return;
189
+ case 'VariableDeclarator':
190
+ // const foo = () => {} or const foo = function() {}
191
+ if (node.id?.name &&
192
+ node.init &&
193
+ (node.init.type === 'ArrowFunctionExpression' || node.init.type === 'FunctionExpression')) {
194
+ const body = node.init.body;
195
+ if (body) {
196
+ // Arrow with block body: () => { ... }
197
+ // Arrow with expression body: () => expr (use the arrow's range)
198
+ const start = body.type === 'BlockStatement' ? body.start : node.init.start;
199
+ const end = body.type === 'BlockStatement' ? body.end : node.init.end;
200
+ scopes.push({ start, end, name: node.id.name });
201
+ }
202
+ // Recurse into the function body
203
+ if (body?.body)
204
+ collectFunctionScopes(body.body, scopes);
205
+ }
206
+ // Recurse into object/array initializers for nested functions
207
+ if (node.init)
208
+ collectFunctionScopesFromNode(node.init, scopes);
209
+ return;
210
+ case 'ExpressionStatement':
211
+ collectFunctionScopesFromNode(node.expression, scopes);
212
+ return;
213
+ case 'ObjectExpression':
214
+ for (const prop of node.properties || []) {
215
+ collectFunctionScopesFromNode(prop, scopes);
216
+ }
217
+ return;
218
+ case 'Property':
219
+ // { method() {} } or { prop: function() {} }
220
+ if (node.key?.name &&
221
+ node.value &&
222
+ (node.value.type === 'FunctionExpression' || node.value.type === 'ArrowFunctionExpression')) {
223
+ const body = node.value.body;
224
+ if (body) {
225
+ const start = body.type === 'BlockStatement' ? body.start : node.value.start;
226
+ const end = body.type === 'BlockStatement' ? body.end : node.value.end;
227
+ scopes.push({ start, end, name: node.key.name });
228
+ }
229
+ if (body?.body)
230
+ collectFunctionScopes(body.body, scopes);
231
+ }
232
+ return;
233
+ case 'MethodDefinition':
234
+ // class Foo { bar() {} }
235
+ if (node.key?.name && node.value?.body) {
236
+ scopes.push({
237
+ start: node.value.body.start,
238
+ end: node.value.body.end,
239
+ name: node.key.name
240
+ });
241
+ if (node.value.body?.body)
242
+ collectFunctionScopes(node.value.body.body, scopes);
243
+ }
244
+ return;
245
+ case 'ClassDeclaration':
246
+ case 'ClassExpression':
247
+ if (node.body?.body) {
248
+ for (const member of node.body.body) {
249
+ collectFunctionScopesFromNode(member, scopes);
250
+ }
251
+ }
252
+ return;
253
+ case 'IfStatement':
254
+ if (node.consequent)
255
+ collectFunctionScopesFromNode(node.consequent, scopes);
256
+ if (node.alternate)
257
+ collectFunctionScopesFromNode(node.alternate, scopes);
258
+ return;
259
+ case 'BlockStatement':
260
+ if (node.body)
261
+ collectFunctionScopes(node.body, scopes);
262
+ return;
263
+ case 'ForStatement':
264
+ case 'ForInStatement':
265
+ case 'ForOfStatement':
266
+ case 'WhileStatement':
267
+ case 'DoWhileStatement':
268
+ if (node.body)
269
+ collectFunctionScopesFromNode(node.body, scopes);
270
+ return;
271
+ case 'TryStatement':
272
+ if (node.block)
273
+ collectFunctionScopesFromNode(node.block, scopes);
274
+ if (node.handler?.body)
275
+ collectFunctionScopesFromNode(node.handler.body, scopes);
276
+ if (node.finalizer)
277
+ collectFunctionScopesFromNode(node.finalizer, scopes);
278
+ return;
279
+ case 'SwitchStatement':
280
+ for (const c of node.cases || []) {
281
+ if (c.consequent)
282
+ collectFunctionScopes(c.consequent, scopes);
283
+ }
284
+ return;
285
+ case 'ReturnStatement':
286
+ if (node.argument)
287
+ collectFunctionScopesFromNode(node.argument, scopes);
288
+ return;
289
+ case 'CallExpression':
290
+ // e.g. onMount(() => { gg() })
291
+ for (const arg of node.arguments || []) {
292
+ if (arg.type === 'ArrowFunctionExpression' || arg.type === 'FunctionExpression') {
293
+ // Anonymous callback — don't add a scope (no name to show),
294
+ // but recurse for nested named functions
295
+ if (arg.body?.body)
296
+ collectFunctionScopes(arg.body.body, scopes);
297
+ }
298
+ }
299
+ return;
300
+ }
301
+ }
302
+ /**
303
+ * Find the innermost enclosing function name for a byte position
304
+ * using the pre-built function scope map.
305
+ * Returns empty string if not inside any named function.
306
+ */
307
+ export function findEnclosingFunctionFromScopes(pos, scopes) {
308
+ // Scopes can be nested; find the innermost (smallest range) that contains pos
309
+ let bestName = '';
310
+ let bestSize = Infinity;
311
+ for (const scope of scopes) {
312
+ if (pos >= scope.start && pos < scope.end) {
313
+ const size = scope.end - scope.start;
314
+ if (size < bestSize) {
315
+ bestSize = size;
316
+ bestName = scope.name;
317
+ }
318
+ }
319
+ }
320
+ return bestName;
321
+ }
322
+ /**
323
+ * Recursively walk a Svelte AST fragment to collect template expression ranges.
324
+ */
325
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
326
+ function walkFragment(fragment, ranges) {
327
+ if (!fragment?.nodes)
328
+ return;
329
+ for (const node of fragment.nodes) {
330
+ walkNode(node, ranges);
331
+ }
332
+ }
333
+ /**
334
+ * Walk a single AST node, collecting expression ranges for template code.
335
+ */
336
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
337
+ function walkNode(node, ranges) {
338
+ if (!node || typeof node !== 'object')
339
+ return;
340
+ switch (node.type) {
341
+ // Template expression tags: {expr}
342
+ case 'ExpressionTag':
343
+ case 'HtmlTag':
344
+ case 'RenderTag':
345
+ case 'AttachTag':
346
+ if (node.expression && node.start != null && node.end != null) {
347
+ ranges.push({ start: node.start, end: node.end, context: 'template' });
348
+ }
349
+ return; // expressions are leaf nodes for our purposes
350
+ // Block tags with expressions: {#if expr}, {#each expr}, {#await expr}, {#key expr}
351
+ case 'IfBlock':
352
+ if (node.test)
353
+ addExprRange(node.test, ranges);
354
+ walkFragment(node.consequent, ranges);
355
+ if (node.alternate)
356
+ walkFragment(node.alternate, ranges);
357
+ return;
358
+ case 'EachBlock':
359
+ if (node.expression)
360
+ addExprRange(node.expression, ranges);
361
+ if (node.key)
362
+ addExprRange(node.key, ranges);
363
+ walkFragment(node.body, ranges);
364
+ if (node.fallback)
365
+ walkFragment(node.fallback, ranges);
366
+ return;
367
+ case 'AwaitBlock':
368
+ if (node.expression)
369
+ addExprRange(node.expression, ranges);
370
+ walkFragment(node.pending, ranges);
371
+ walkFragment(node.then, ranges);
372
+ walkFragment(node.catch, ranges);
373
+ return;
374
+ case 'KeyBlock':
375
+ if (node.expression)
376
+ addExprRange(node.expression, ranges);
377
+ walkFragment(node.fragment, ranges);
378
+ return;
379
+ case 'SnippetBlock':
380
+ walkFragment(node.body, ranges);
381
+ return;
382
+ // {@const ...} — contains a declaration, not a simple expression
383
+ case 'ConstTag':
384
+ if (node.declaration) {
385
+ ranges.push({ start: node.start, end: node.end, context: 'template' });
386
+ }
387
+ return;
388
+ // Elements and components — walk attributes + children
389
+ case 'RegularElement':
390
+ case 'Component':
391
+ case 'SvelteElement':
392
+ case 'SvelteComponent':
393
+ case 'SvelteBody':
394
+ case 'SvelteWindow':
395
+ case 'SvelteDocument':
396
+ case 'SvelteHead':
397
+ case 'SvelteSelf':
398
+ case 'SvelteFragment':
399
+ case 'SvelteBoundary':
400
+ case 'TitleElement':
401
+ case 'SlotElement':
402
+ walkAttributes(node.attributes, ranges);
403
+ walkFragment(node.fragment, ranges);
404
+ return;
405
+ // Text nodes — skip (prose, not code)
406
+ case 'Text':
407
+ case 'Comment':
408
+ return;
409
+ default:
410
+ // Unknown node type — try to walk children defensively
411
+ if (node.fragment)
412
+ walkFragment(node.fragment, ranges);
413
+ if (node.children)
414
+ walkFragment({ nodes: node.children }, ranges);
415
+ return;
416
+ }
417
+ }
418
+ /**
419
+ * Walk element attributes to find expression ranges.
420
+ */
421
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
422
+ function walkAttributes(attrs, ranges) {
423
+ if (!attrs)
424
+ return;
425
+ for (const attr of attrs) {
426
+ switch (attr.type) {
427
+ case 'Attribute':
428
+ // value can be: true | ExpressionTag | Array<Text | ExpressionTag>
429
+ if (attr.value === true)
430
+ break;
431
+ if (Array.isArray(attr.value)) {
432
+ for (const part of attr.value) {
433
+ if (part.type === 'ExpressionTag') {
434
+ ranges.push({ start: part.start, end: part.end, context: 'template' });
435
+ }
436
+ }
437
+ }
438
+ else if (attr.value?.type === 'ExpressionTag') {
439
+ ranges.push({ start: attr.value.start, end: attr.value.end, context: 'template' });
440
+ }
441
+ break;
442
+ case 'SpreadAttribute':
443
+ if (attr.expression) {
444
+ ranges.push({ start: attr.start, end: attr.end, context: 'template' });
445
+ }
446
+ break;
447
+ // Directives: bind:, class:, style:, on:, use:, transition:, animate:, attach:
448
+ case 'BindDirective':
449
+ case 'ClassDirective':
450
+ case 'StyleDirective':
451
+ case 'OnDirective':
452
+ case 'UseDirective':
453
+ case 'TransitionDirective':
454
+ case 'AnimateDirective':
455
+ if (attr.expression) {
456
+ addExprRange(attr.expression, ranges);
457
+ }
458
+ // StyleDirective value can be an array
459
+ if (attr.value && Array.isArray(attr.value)) {
460
+ for (const part of attr.value) {
461
+ if (part.type === 'ExpressionTag') {
462
+ ranges.push({ start: part.start, end: part.end, context: 'template' });
463
+ }
464
+ }
465
+ }
466
+ break;
467
+ case 'AttachTag':
468
+ if (attr.expression) {
469
+ ranges.push({ start: attr.start, end: attr.end, context: 'template' });
470
+ }
471
+ break;
472
+ }
473
+ }
474
+ }
475
+ /**
476
+ * Add a template expression range from an AST expression node.
477
+ */
478
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
479
+ function addExprRange(expr, ranges) {
480
+ if (expr && expr.start != null && expr.end != null) {
481
+ ranges.push({ start: expr.start, end: expr.end, context: 'template' });
482
+ }
483
+ }
484
+ /**
485
+ * Check if a character position falls within any of the given code ranges.
486
+ * Returns the matching range, or undefined if not in any range.
487
+ */
488
+ function findCodeRange(pos, ranges) {
489
+ for (const r of ranges) {
490
+ if (pos >= r.start && pos < r.end)
491
+ return r;
492
+ }
493
+ return undefined;
494
+ }
495
+ /**
496
+ * Compute 1-based line number and column for a character offset in source code.
497
+ */
498
+ function getLineCol(code, offset) {
499
+ const line = code.slice(0, offset).split('\n').length;
500
+ const col = offset - code.lastIndexOf('\n', offset - 1);
501
+ return { line, col };
502
+ }
503
+ /**
504
+ * Find the matching closing paren for an opening paren at `openPos`.
505
+ * Handles nested parens, brackets, braces, strings, template literals, and comments.
506
+ * Returns the index of the closing ')' or -1 if not found.
507
+ */
508
+ function findMatchingParen(code, openPos) {
509
+ let depth = 1;
510
+ let j = openPos + 1;
511
+ while (j < code.length && depth > 0) {
512
+ const ch = code[j];
513
+ // Skip string literals
514
+ if (ch === '"' || ch === "'") {
515
+ j++;
516
+ while (j < code.length && code[j] !== ch) {
517
+ if (code[j] === '\\')
518
+ j++;
519
+ j++;
113
520
  }
114
- if (match.index > closestPos) {
115
- closestPos = match.index;
116
- closestName = name;
521
+ j++; // skip closing quote
522
+ continue;
523
+ }
524
+ // Skip template literals
525
+ if (ch === '`') {
526
+ j++;
527
+ let tmplDepth = 0;
528
+ while (j < code.length) {
529
+ if (code[j] === '\\') {
530
+ j += 2;
531
+ continue;
532
+ }
533
+ if (code[j] === '$' && code[j + 1] === '{') {
534
+ tmplDepth++;
535
+ j += 2;
536
+ continue;
537
+ }
538
+ if (code[j] === '}' && tmplDepth > 0) {
539
+ tmplDepth--;
540
+ j++;
541
+ continue;
542
+ }
543
+ if (code[j] === '`' && tmplDepth === 0) {
544
+ j++;
545
+ break;
546
+ }
547
+ j++;
117
548
  }
549
+ continue;
118
550
  }
551
+ // Skip single-line comments
552
+ if (ch === '/' && code[j + 1] === '/') {
553
+ const end = code.indexOf('\n', j);
554
+ j = end === -1 ? code.length : end + 1;
555
+ continue;
556
+ }
557
+ // Skip multi-line comments
558
+ if (ch === '/' && code[j + 1] === '*') {
559
+ const end = code.indexOf('*/', j + 2);
560
+ j = end === -1 ? code.length : end + 2;
561
+ continue;
562
+ }
563
+ if (ch === '(' || ch === '[' || ch === '{')
564
+ depth++;
565
+ else if (ch === ')' || ch === ']' || ch === '}')
566
+ depth--;
567
+ if (depth > 0)
568
+ j++;
119
569
  }
120
- return closestName;
570
+ return depth === 0 ? j : -1;
121
571
  }
122
572
  /**
123
- * Transform gg() calls in source code to gg.ns('callpoint', ...) calls.
573
+ * Escape a string for embedding as a single-quoted JS string literal.
574
+ */
575
+ export function escapeForString(s) {
576
+ return s.replace(/\\/g, '\\\\').replace(/'/g, "\\'").replace(/\n/g, '\\n').replace(/\r/g, '\\r');
577
+ }
578
+ /**
579
+ * Transform gg() and gg.ns() calls in source code to gg._ns({ns, file, line, col, src}, ...) calls.
124
580
  *
125
581
  * Handles:
126
- * - bare gg(...) → gg.ns('callpoint', ...)
127
- * - gg.ns(...) → left untouched (user-specified namespace)
128
- * - gg.enable, gg.disable, gg.clearPersist, gg._onLog → left untouched
582
+ * - bare gg(expr) → gg._ns({ns, file, line, col, src: 'expr'}, expr)
583
+ * - gg.ns('label', expr) → gg._ns({ns, file, line, col, src: 'expr'}, expr)
584
+ * - label supports template variables: $NS, $FN, $FILE, $LINE, $COL
585
+ * - plain label (no variables) is used as-is (no auto @fn append)
586
+ * - gg.enable, gg.disable, gg.clearPersist, gg._onLog, gg._ns → left untouched
129
587
  * - gg inside strings and comments → left untouched
588
+ *
589
+ * For .svelte files, `svelteInfo` (from `collectCodeRanges()`) determines which
590
+ * positions contain JS code and provides AST-based function scope detection.
591
+ * Script ranges use `{...}` object literal syntax; template ranges use `gg._o()`
592
+ * function-call syntax (no braces in Svelte markup). Positions outside any code
593
+ * range (e.g. prose text) are skipped.
594
+ *
595
+ * For .js/.ts files, `jsFunctionScopes` (from `parseJavaScript()`) provides
596
+ * AST-based function scope detection (no regex fallback).
130
597
  */
131
- function transformGgCalls(code, shortPath) {
132
- // Match gg( that is:
133
- // - not preceded by a dot (would be obj.gg() — not our function)
134
- // - not preceded by a word char (would be dogg() or something)
135
- // - not followed by a dot before the paren (gg.ns, gg.enable, etc.)
136
- //
598
+ export function transformGgCalls(code, shortPath, filePath, svelteInfo, jsFunctionScopes) {
137
599
  // We use a manual scan approach to correctly handle strings and comments.
138
600
  const result = [];
139
601
  let lastIndex = 0;
140
602
  let modified = false;
603
+ const escapedFile = escapeForString(filePath);
604
+ /**
605
+ * Find the code range containing `pos`, or undefined if outside all ranges.
606
+ * For non-.svelte files (no svelteInfo), returns a synthetic 'script' range.
607
+ */
608
+ function rangeAt(pos) {
609
+ if (!svelteInfo)
610
+ return { start: 0, end: code.length, context: 'script' };
611
+ return findCodeRange(pos, svelteInfo.ranges);
612
+ }
613
+ /**
614
+ * Find the enclosing function name for a position.
615
+ * - .svelte files: uses estree AST function scope map from svelte.parse()
616
+ * - .js/.ts files: uses estree AST function scope map from acorn.parse()
617
+ * - template code ranges: always returns '' (no enclosing function from script)
618
+ */
619
+ function getFunctionName(pos, range) {
620
+ if (range.context === 'template')
621
+ return '';
622
+ if (svelteInfo)
623
+ return findEnclosingFunctionFromScopes(pos, svelteInfo.functionScopes);
624
+ if (jsFunctionScopes)
625
+ return findEnclosingFunctionFromScopes(pos, jsFunctionScopes);
626
+ return ''; // Should not reach here unless both svelteInfo and jsFunctionScopes are undefined
627
+ }
628
+ /**
629
+ * Build the options argument for gg._ns().
630
+ * Inside <script>: {ns:'...',file:'...',line:N,col:N} (object literal)
631
+ * In template: gg._o('...','...',N,N) (function call — no braces)
632
+ */
633
+ function buildOptions(range, ns, line, col, src) {
634
+ if (range.context === 'script') {
635
+ return src
636
+ ? `{ns:'${ns}',file:'${escapedFile}',line:${line},col:${col},src:'${src}'}`
637
+ : `{ns:'${ns}',file:'${escapedFile}',line:${line},col:${col}}`;
638
+ }
639
+ return src
640
+ ? `gg._o('${ns}','${escapedFile}',${line},${col},'${src}')`
641
+ : `gg._o('${ns}','${escapedFile}',${line},${col})`;
642
+ }
141
643
  // States for string/comment tracking
142
644
  let i = 0;
143
645
  while (i < code.length) {
@@ -197,42 +699,128 @@ function transformGgCalls(code, shortPath) {
197
699
  }
198
700
  continue;
199
701
  }
200
- // Look for 'gg(' pattern
201
- if (code[i] === 'g' && code[i + 1] === 'g' && code[i + 2] === '(') {
702
+ // Look for 'gg' pattern — could be gg( or gg.ns(
703
+ if (code[i] === 'g' && code[i + 1] === 'g') {
704
+ // In .svelte files, skip gg outside code ranges (prose text, etc.)
705
+ const range = rangeAt(i);
706
+ if (!range) {
707
+ i++;
708
+ continue;
709
+ }
202
710
  // Check preceding character: must not be a word char or dot
203
711
  const prevChar = i > 0 ? code[i - 1] : '';
204
712
  if (prevChar && /[a-zA-Z0-9_$.]/.test(prevChar)) {
205
713
  i++;
206
714
  continue;
207
715
  }
208
- // Check it's not gg.something (gg.ns, gg.enable, etc.)
209
- // At this point we know code[i..i+2] is "gg(" — it's a bare call
210
- // Find the enclosing function
211
- const fnName = findEnclosingFunction(code, i);
212
- const callpoint = `${shortPath}${fnName ? `@${fnName}` : ''}`;
213
- const escaped = callpoint.replace(/\\/g, '\\\\').replace(/'/g, "\\'");
214
- // Emit everything before this match
215
- result.push(code.slice(lastIndex, i));
216
- // Replace gg( with gg.ns('callpoint',
217
- // Need to handle gg() with no args → gg.ns('callpoint')
218
- // and gg(x) → gg.ns('callpoint', x)
219
- // Peek ahead to check if it's gg() with no args
220
- const afterParen = code.indexOf(')', i + 3);
221
- const betweenParens = code.slice(i + 3, afterParen);
222
- const isNoArgs = betweenParens.trim() === '';
223
- if (isNoArgs && afterParen !== -1 && !betweenParens.includes('(')) {
224
- // gg() gg.ns('callpoint')
225
- result.push(`gg.ns('${escaped}')`);
226
- lastIndex = afterParen + 1;
227
- i = afterParen + 1;
716
+ // Case 1: gg.ns('label', ...) gg._ns({ns: 'label', file, line, col, src}, ...)
717
+ if (code.slice(i + 2, i + 6) === '.ns(') {
718
+ const { line, col } = getLineCol(code, i);
719
+ const fnName = getFunctionName(i, range);
720
+ const openParenPos = i + 5; // position of '(' in 'gg.ns('
721
+ // Find matching closing paren for the entire gg.ns(...) call
722
+ const closeParenPos = findMatchingParen(code, openParenPos);
723
+ if (closeParenPos === -1) {
724
+ i += 6;
725
+ continue;
726
+ }
727
+ // Extract the first argument (the namespace string)
728
+ // Look for the string literal after 'gg.ns('
729
+ const afterNsParen = i + 6; // position after 'gg.ns('
730
+ const quoteChar = code[afterNsParen];
731
+ if (quoteChar === "'" || quoteChar === '"') {
732
+ // Find the closing quote
733
+ let j = afterNsParen + 1;
734
+ while (j < code.length && code[j] !== quoteChar) {
735
+ if (code[j] === '\\')
736
+ j++; // skip escaped chars
737
+ j++;
738
+ }
739
+ // j now points to closing quote
740
+ const nsLabelRaw = code.slice(afterNsParen + 1, j);
741
+ // Build callpoint: substitute $NS/$FN/$FILE/$LINE/$COL template variables.
742
+ // The auto-generated callpoint (file@fn) is what bare gg() would produce.
743
+ const autoCallpoint = `${shortPath}${fnName ? `@${fnName}` : ''}`;
744
+ const callpoint = escapeForString(nsLabelRaw
745
+ .replace(/\$NS/g, autoCallpoint)
746
+ .replace(/\$FN/g, fnName)
747
+ .replace(/\$FILE/g, shortPath)
748
+ .replace(/\$LINE/g, String(line))
749
+ .replace(/\$COL/g, String(col)));
750
+ // Check if there are more args after the string
751
+ const afterClosingQuote = j + 1;
752
+ let k = afterClosingQuote;
753
+ while (k < code.length && /\s/.test(code[k]))
754
+ k++;
755
+ if (code[k] === ')') {
756
+ // gg.ns('label') → gg._ns(opts)
757
+ result.push(code.slice(lastIndex, i));
758
+ result.push(`gg._ns(${buildOptions(range, callpoint, line, col)})`);
759
+ lastIndex = k + 1;
760
+ i = k + 1;
761
+ }
762
+ else if (code[k] === ',') {
763
+ // gg.ns('label', args...) → gg._ns(opts, args...)
764
+ let argsStart = k + 1;
765
+ while (argsStart < closeParenPos && /\s/.test(code[argsStart]))
766
+ argsStart++;
767
+ const argsSrc = code.slice(argsStart, closeParenPos).trim();
768
+ const escapedSrc = escapeForString(argsSrc);
769
+ result.push(code.slice(lastIndex, i));
770
+ result.push(`gg._ns(${buildOptions(range, callpoint, line, col, escapedSrc)}, `);
771
+ lastIndex = k + 1; // skip past the comma, keep args as-is
772
+ i = k + 1;
773
+ }
774
+ else {
775
+ // Unexpected — leave untouched
776
+ i += 6;
777
+ continue;
778
+ }
779
+ modified = true;
780
+ continue;
781
+ }
782
+ // Non-string first arg to gg.ns — skip (can't extract ns at build time)
783
+ i += 6;
784
+ continue;
228
785
  }
229
- else {
230
- // gg(args...) gg.ns('callpoint', args...)
231
- result.push(`gg.ns('${escaped}', `);
232
- lastIndex = i + 3; // skip past "gg("
233
- i = i + 3;
786
+ // Skip other gg.* calls (gg.enable, gg.disable, gg._ns, gg._onLog, etc.)
787
+ if (code[i + 2] === '.') {
788
+ i += 3;
789
+ continue;
234
790
  }
235
- modified = true;
791
+ // Case 2: bare gg(...) → gg._ns({ns, file, line, col, src}, ...)
792
+ if (code[i + 2] === '(') {
793
+ const { line, col } = getLineCol(code, i);
794
+ const fnName = getFunctionName(i, range);
795
+ const callpoint = `${shortPath}${fnName ? `@${fnName}` : ''}`;
796
+ const escapedNs = escapeForString(callpoint);
797
+ const openParenPos = i + 2; // position of '(' in 'gg('
798
+ // Find matching closing paren
799
+ const closeParenPos = findMatchingParen(code, openParenPos);
800
+ if (closeParenPos === -1) {
801
+ i += 3;
802
+ continue;
803
+ }
804
+ const argsText = code.slice(openParenPos + 1, closeParenPos).trim();
805
+ // Emit everything before this match
806
+ result.push(code.slice(lastIndex, i));
807
+ if (argsText === '') {
808
+ // gg() → gg._ns(opts)
809
+ result.push(`gg._ns(${buildOptions(range, escapedNs, line, col)})`);
810
+ lastIndex = closeParenPos + 1;
811
+ i = closeParenPos + 1;
812
+ }
813
+ else {
814
+ // gg(expr) → gg._ns(opts, expr)
815
+ const escapedSrc = escapeForString(argsText);
816
+ result.push(`gg._ns(${buildOptions(range, escapedNs, line, col, escapedSrc)}, `);
817
+ lastIndex = openParenPos + 1; // keep original args
818
+ i = openParenPos + 1;
819
+ }
820
+ modified = true;
821
+ continue;
822
+ }
823
+ i++;
236
824
  continue;
237
825
  }
238
826
  i++;