@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.
- package/README.md +117 -36
- package/dist/GgConsole.svelte +12 -0
- package/dist/GgConsole.svelte.d.ts +4 -0
- package/dist/OpenInEditorLink.svelte +8 -10
- package/dist/OpenInEditorLink.svelte.d.ts +6 -3
- package/dist/debug/browser.d.ts +10 -0
- package/dist/debug/browser.js +102 -0
- package/dist/debug/common.d.ts +41 -0
- package/dist/debug/common.js +191 -0
- package/dist/debug/index.d.ts +9 -0
- package/dist/debug/index.js +11 -0
- package/dist/debug/node.d.ts +10 -0
- package/dist/debug/node.js +137 -0
- package/dist/eruda/loader.js +0 -11
- package/dist/eruda/plugin.js +765 -25
- package/dist/eruda/types.d.ts +8 -5
- package/dist/gg-call-sites-plugin.d.ts +90 -6
- package/dist/gg-call-sites-plugin.js +700 -112
- package/dist/gg.d.ts +18 -2
- package/dist/gg.js +127 -110
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/open-in-editor.js +15 -2
- package/dist/vite.d.ts +37 -0
- package/dist/vite.js +46 -0
- package/package.json +20 -13
- package/dist/debug-bundled.d.ts +0 -2
- package/dist/debug-bundled.js +0 -3
- package/dist/debug.d.ts +0 -2
- package/dist/debug.js +0 -15
- package/patches/debug@4.4.3.patch +0 -35
|
@@ -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
|
|
3
|
-
* at build time. This gives each call
|
|
4
|
-
*
|
|
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.
|
|
7
|
-
*
|
|
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
|
-
|
|
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
|
-
*
|
|
52
|
-
*
|
|
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
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
//
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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
|
|
570
|
+
return depth === 0 ? j : -1;
|
|
121
571
|
}
|
|
122
572
|
/**
|
|
123
|
-
*
|
|
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(
|
|
127
|
-
* - gg.ns(
|
|
128
|
-
*
|
|
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
|
|
201
|
-
if (code[i] === 'g' && code[i + 1] === 'g'
|
|
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
|
-
//
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
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
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
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
|
-
|
|
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++;
|