@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/dist/gg.d.ts CHANGED
@@ -1,4 +1,3 @@
1
- import ErrorStackParser from 'error-stack-parser';
2
1
  /**
3
2
  * Hook for capturing gg() output (used by Eruda plugin)
4
3
  */
@@ -9,13 +8,16 @@ interface CapturedEntry {
9
8
  message: string;
10
9
  args: unknown[];
11
10
  timestamp: number;
11
+ file?: string;
12
+ line?: number;
13
+ col?: number;
14
+ src?: string;
12
15
  }
13
16
  type OnLogCallback = (entry: CapturedEntry) => void;
14
17
  export declare function gg(): {
15
18
  fileName: string;
16
19
  functionName: string;
17
20
  url: string;
18
- stack: ErrorStackParser.StackFrame[];
19
21
  };
20
22
  export declare function gg<T>(arg: T, ...args: unknown[]): T;
21
23
  export declare namespace gg {
@@ -85,6 +87,20 @@ export declare function bg(color: string): ChainableColorFn;
85
87
  export declare namespace gg {
86
88
  let _onLog: OnLogCallback | null;
87
89
  let ns: (nsLabel: string, ...args: unknown[]) => unknown;
90
+ let _ns: (options: {
91
+ ns: string;
92
+ file?: string;
93
+ line?: number;
94
+ col?: number;
95
+ src?: string;
96
+ }, ...args: unknown[]) => unknown;
97
+ let _o: (ns: string, file?: string, line?: number, col?: number, src?: string) => {
98
+ ns: string;
99
+ file?: string;
100
+ line?: number;
101
+ col?: number;
102
+ src?: string;
103
+ };
88
104
  }
89
105
  /**
90
106
  * Run gg diagnostics and log configuration status
package/dist/gg.js CHANGED
@@ -1,5 +1,4 @@
1
- import debugFactory from './debug.js';
2
- import ErrorStackParser from 'error-stack-parser';
1
+ import debugFactory, {} from './debug/index.js';
3
2
  import { BROWSER, DEV } from 'esm-env';
4
3
  import { toWordTuple } from './words.js';
5
4
  const _ggCallSitesPlugin = typeof __GG_TAG_PLUGIN__ !== 'undefined' ? __GG_TAG_PLUGIN__ : false;
@@ -9,23 +8,21 @@ const _ggCallSitesPlugin = typeof __GG_TAG_PLUGIN__ !== 'undefined' ? __GG_TAG_P
9
8
  */
10
9
  function createGgDebugger(namespace) {
11
10
  const dbg = debugFactory(namespace);
12
- // Store the original formatArgs (if it exists)
13
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
11
+ // Store the original formatArgs
14
12
  const originalFormatArgs = dbg.formatArgs;
15
13
  // Override formatArgs to add padding to the namespace display
16
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
17
14
  dbg.formatArgs = function (args) {
18
15
  // Call original formatArgs first
19
16
  if (originalFormatArgs) {
20
- originalFormatArgs.call(this, args);
17
+ originalFormatArgs.call(dbg, args);
21
18
  }
22
19
  // Extract the callpoint from namespace (strip 'gg:' prefix and any URL suffix)
23
- const nsMatch = this.namespace.match(/^gg:([^h]+?)(?:http|$)/);
24
- const callpoint = nsMatch ? nsMatch[1] : this.namespace.replace(/^gg:/, '');
20
+ const nsMatch = dbg.namespace.match(/^gg:([^h]+?)(?:http|$)/);
21
+ const callpoint = nsMatch ? nsMatch[1] : dbg.namespace.replace(/^gg:/, '');
25
22
  const paddedCallpoint = callpoint.padEnd(maxCallpointLength, ' ');
26
23
  // Replace the namespace in the formatted string with padded version
27
24
  if (typeof args[0] === 'string') {
28
- args[0] = args[0].replace(this.namespace, `gg:${paddedCallpoint}`);
25
+ args[0] = args[0].replace(dbg.namespace, `gg:${paddedCallpoint}`);
29
26
  }
30
27
  };
31
28
  return dbg;
@@ -91,7 +88,6 @@ function getServerPort() {
91
88
  }
92
89
  });
93
90
  }
94
- const timestampRegex = /(\?t=\d+)?$/;
95
91
  const port = await getServerPort();
96
92
  /**
97
93
  * Determines if gg should be enabled based on environment and runtime triggers.
@@ -169,6 +165,23 @@ const namespaceToLogFunction = new Map();
169
165
  let maxCallpointLength = 0;
170
166
  // Cache: raw stack line → word tuple (avoids re-hashing the same call site)
171
167
  const stackLineCache = new Map();
168
+ /**
169
+ * Resolve the callpoint for the caller at the given stack depth.
170
+ * depth=2 → caller of gg(), depth=3 → caller of gg.ns() (extra frame).
171
+ */
172
+ function resolveCallpoint(depth) {
173
+ const rawStack = new Error().stack || '';
174
+ const callerLine = rawStack.split('\n')[depth] || rawStack;
175
+ const callerKey = callerLine.replace(/:\d+:\d+\)?$/, '').trim();
176
+ const callpoint = stackLineCache.get(callerKey) ?? toWordTuple(callerKey);
177
+ if (!stackLineCache.has(callerKey)) {
178
+ stackLineCache.set(callerKey, callpoint);
179
+ }
180
+ if (callpoint.length < 80 && callpoint.length > maxCallpointLength) {
181
+ maxCallpointLength = callpoint.length;
182
+ }
183
+ return callpoint;
184
+ }
172
185
  /**
173
186
  * Reset the namespace width tracking.
174
187
  * Useful after configuration checks that may have long callpoint paths.
@@ -176,80 +189,37 @@ const stackLineCache = new Map();
176
189
  function resetNamespaceWidth() {
177
190
  maxCallpointLength = 0;
178
191
  }
179
- function openInEditorUrl(fileName) {
180
- return ggConfig.openInEditorUrlTemplate.replace('$FILENAME', encodeURIComponent(fileName).replaceAll('%2F', '/'));
192
+ function openInEditorUrl(fileName, line, col) {
193
+ let url = ggConfig.openInEditorUrlTemplate.replace('$FILENAME', encodeURIComponent(fileName).replaceAll('%2F', '/'));
194
+ if (line != null)
195
+ url += `&line=${line}`;
196
+ if (col != null)
197
+ url += `&col=${col}`;
198
+ return url;
181
199
  }
182
200
  export function gg(...args) {
183
201
  if (!ggConfig.enabled || isCloudflareWorker()) {
184
- return args.length ? args[0] : { url: '', stack: [] };
185
- }
186
- // Initialize return values
187
- let fileName = '';
188
- let functionName = '';
189
- let url = '';
190
- let stack = [];
191
- let namespace = 'gg:';
192
- // When ggCallSitesPlugin is installed, all bare gg() calls are rewritten to gg.ns()
193
- // at build time, so this code path only runs for un-transformed calls.
194
- // Skip expensive stack parsing if the plugin is handling callpoints.
195
- if (!_ggCallSitesPlugin) {
196
- if (DEV) {
197
- // Development without plugin: full stack parsing for detailed callpoint info
198
- // Ignore first stack frame, which is always the call to gg() itself.
199
- stack = ErrorStackParser.parse(new Error()).splice(1);
200
- // Example: http://localhost:5173/src/routes/+page.svelte
201
- const filename = stack[0].fileName?.replace(timestampRegex, '') || '';
202
- // Example: src/routes/+page.svelte
203
- const filenameToOpen = filename.replace(srcRootRegex, '$<folderName>/');
204
- url = openInEditorUrl(filenameToOpen);
205
- // Example: routes/+page.svelte
206
- fileName = filename.replace(srcRootRegex, '');
207
- functionName = stack[0].functionName || '';
208
- // A callpoint is uniquely identified by the filename plus function name
209
- const callpoint = `${fileName}${functionName ? `@${functionName}` : ''}`;
210
- if (callpoint.length < 80 && callpoint.length > maxCallpointLength) {
211
- maxCallpointLength = callpoint.length;
212
- }
213
- // Namespace without padding - keeps colors stable
214
- // Editor link appended if enabled
215
- namespace = `gg:${callpoint}${ggConfig.editorLink ? url : ''}`;
216
- }
217
- else {
218
- // Production without plugin: cheap stack hash → deterministic word tuple
219
- // Avoids expensive ErrorStackParser; just grabs the raw stack line and hashes it.
220
- // Same call site always produces the same word pair (e.g. "calm-fox").
221
- const rawStack = new Error().stack || '';
222
- // Stack line [2]: skip "Error" header [0] and gg() frame [1]
223
- const callerLine = rawStack.split('\n')[2] || rawStack;
224
- // Strip line:col numbers so all gg() calls within the same function
225
- // hash to the same word tuple. In minified builds, multiple gg() calls
226
- // in one function differ only by column offset — we want them grouped.
227
- // Chrome: "at handleClick (chunk-abc.js:1:45892)" → "at handleClick (chunk-abc.js)"
228
- // Firefox: "handleClick@https://...:1:45892" → "handleClick@https://..."
229
- const callerKey = callerLine.replace(/:\d+:\d+\)?$/, '').trim();
230
- const callpoint = stackLineCache.get(callerKey) ?? toWordTuple(callerKey);
231
- if (!stackLineCache.has(callerKey)) {
232
- stackLineCache.set(callerKey, callpoint);
233
- }
234
- if (callpoint.length < 80 && callpoint.length > maxCallpointLength) {
235
- maxCallpointLength = callpoint.length;
236
- }
237
- namespace = `gg:${callpoint}`;
238
- }
239
- }
202
+ return args.length ? args[0] : { fileName: '', functionName: '', url: '' };
203
+ }
204
+ // Without the call-sites plugin, use cheap stack hash → deterministic word tuple.
205
+ // When the plugin IS installed, all gg() calls are rewritten to gg._ns() at build time,
206
+ // so this code path only runs for un-transformed calls (i.e. plugin not installed).
207
+ // Same call site always produces the same word pair (e.g. "calm-fox").
208
+ // depth=2: skip "Error" header [0] and gg() frame [1]
209
+ const callpoint = resolveCallpoint(2);
210
+ const namespace = `gg:${callpoint}`;
240
211
  const ggLogFunction = namespaceToLogFunction.get(namespace) ||
241
212
  namespaceToLogFunction.set(namespace, createGgDebugger(namespace)).get(namespace);
242
213
  // Prepare args for logging
243
214
  let logArgs;
244
215
  let returnValue;
245
216
  if (!args.length) {
246
- // No arguments: log editor link
247
- logArgs = [` 📝📝 ${url} 👀👀`];
217
+ // No arguments: return stub call-site info (no open-in-editor without plugin)
218
+ logArgs = [` 📝 ${callpoint} (install gg-call-sites-plugin for editor links)`];
248
219
  returnValue = {
249
- fileName,
250
- functionName,
251
- url,
252
- stack
220
+ fileName: callpoint,
221
+ functionName: '',
222
+ url: ''
253
223
  };
254
224
  }
255
225
  else if (args.length === 1) {
@@ -288,22 +258,54 @@ export function gg(...args) {
288
258
  /**
289
259
  * gg.ns() - Log with an explicit namespace (callpoint label).
290
260
  *
291
- * In production builds, the ggCallSitesPlugin Vite plugin rewrites bare gg() calls
292
- * to gg.ns('callpoint', ...) so each call site gets a unique namespace even
293
- * after minification. Users can also call gg.ns() directly to set a meaningful
294
- * label that survives across builds.
261
+ * Users call gg.ns() directly to set a meaningful label that survives
262
+ * across builds. For the internal plugin-generated version with file
263
+ * metadata, see gg._ns().
264
+ *
265
+ * The label supports template variables (substituted by the vite plugin
266
+ * at build time, or at runtime for $NS):
267
+ * $NS - auto-generated callpoint (file@fn with plugin, word-tuple without)
268
+ * $FN - enclosing function name (plugin only, empty without)
269
+ * $FILE - short file path (plugin only, empty without)
270
+ * $LINE - line number (plugin only, empty without)
271
+ * $COL - column number (plugin only, empty without)
295
272
  *
296
273
  * @param nsLabel - The namespace label (appears as gg:<nsLabel> in output)
297
274
  * @param args - Same arguments as gg()
298
275
  * @returns Same as gg() - the first arg, or call-site info if no args
299
276
  *
300
277
  * @example
301
- * gg.ns("auth", "login failed") // logs under namespace "gg:auth"
302
- * gg.ns("cart", item, quantity) // logs under namespace "gg:cart"
278
+ * gg.ns("auth", "login failed") // gg:auth
279
+ * gg.ns("ERROR:$NS", msg) // gg:ERROR:routes/+page.svelte@handleClick (with plugin)
280
+ * // → gg:ERROR:calm-fox (without plugin)
281
+ * gg.ns("$NS:validation", fieldName) // → gg:routes/+page.svelte@handleClick:validation
303
282
  */
304
283
  gg.ns = function (nsLabel, ...args) {
284
+ // Resolve $NS at runtime (word-tuple fallback when plugin isn't installed).
285
+ // With the plugin, $NS is already substituted at build time before this runs.
286
+ // depth=3: skip "Error" [0], resolveCallpoint [1], gg.ns [2] → caller [3]
287
+ if (nsLabel.includes('$NS')) {
288
+ const callpoint = resolveCallpoint(3);
289
+ nsLabel = nsLabel.replace(/\$NS/g, callpoint);
290
+ }
291
+ return gg._ns({ ns: nsLabel }, ...args);
292
+ };
293
+ /**
294
+ * gg._ns() - Internal: log with namespace and source file metadata.
295
+ *
296
+ * Called by the ggCallSitesPlugin Vite plugin, which rewrites both bare gg()
297
+ * calls and manual gg.ns() calls to gg._ns({ns, file, line, col}, ...) at
298
+ * build time. This gives each call site a unique namespace plus the source
299
+ * location for open-in-editor support.
300
+ *
301
+ * @param options - { ns: string; file?: string; line?: number; col?: number }
302
+ * @param args - Same arguments as gg()
303
+ * @returns Same as gg() - the first arg, or call-site info if no args
304
+ */
305
+ gg._ns = function (options, ...args) {
306
+ const { ns: nsLabel, file, line, col, src } = options;
305
307
  if (!ggConfig.enabled || isCloudflareWorker()) {
306
- return args.length ? args[0] : { url: '', stack: [] };
308
+ return args.length ? args[0] : { fileName: '', functionName: '', url: '' };
307
309
  }
308
310
  const namespace = `gg:${nsLabel}`;
309
311
  if (nsLabel.length < 80 && nsLabel.length > maxCallpointLength) {
@@ -311,12 +313,17 @@ gg.ns = function (nsLabel, ...args) {
311
313
  }
312
314
  const ggLogFunction = namespaceToLogFunction.get(namespace) ||
313
315
  namespaceToLogFunction.set(namespace, createGgDebugger(namespace)).get(namespace);
314
- // Prepare args for logging
316
+ // Prepare args for logging (console output is value-only; src is carried
317
+ // on CapturedEntry for the Eruda UI to display on hover)
315
318
  let logArgs;
316
319
  let returnValue;
317
320
  if (!args.length) {
321
+ // No arguments: return call-site info for open-in-editor
322
+ const fileName = file ? file.replace(srcRootRegex, '') : nsLabel;
323
+ const functionName = nsLabel.includes('@') ? nsLabel.split('@').pop() || '' : '';
324
+ const url = file ? openInEditorUrl(file, line, col) : '';
318
325
  logArgs = [` 📝 ${nsLabel}`];
319
- returnValue = { fileName: '', functionName: '', url: '', stack: [] };
326
+ returnValue = { fileName, functionName, url };
320
327
  }
321
328
  else if (args.length === 1) {
322
329
  logArgs = [args[0]];
@@ -340,7 +347,11 @@ gg.ns = function (nsLabel, ...args) {
340
347
  diff: ggLogFunction.diff || 0,
341
348
  message: logArgs.length === 1 ? String(logArgs[0]) : logArgs.map(String).join(' '),
342
349
  args: logArgs,
343
- timestamp: Date.now()
350
+ timestamp: Date.now(),
351
+ file,
352
+ line,
353
+ col,
354
+ src
344
355
  };
345
356
  if (_onLogCallback) {
346
357
  _onLogCallback(entry);
@@ -350,6 +361,18 @@ gg.ns = function (nsLabel, ...args) {
350
361
  }
351
362
  return returnValue;
352
363
  };
364
+ /**
365
+ * gg._o() - Internal: build options object for gg._ns() without object literal syntax.
366
+ *
367
+ * Used by the vite plugin to transform gg() calls in Svelte template markup,
368
+ * where object literals ({...}) would break Svelte's template parser.
369
+ *
370
+ * In <script> blocks: gg._ns({ns:'...', file:'...', line:1, col:1}, args)
371
+ * In template markup: gg._ns(gg._o('...','...',1,1), args)
372
+ */
373
+ gg._o = function (ns, file, line, col, src) {
374
+ return { ns, file, line, col, src };
375
+ };
353
376
  gg.disable = isCloudflareWorker() ? () => { } : debugFactory.disable;
354
377
  gg.enable = isCloudflareWorker() ? () => { } : debugFactory.enable;
355
378
  /**
@@ -506,26 +529,28 @@ Object.defineProperty(gg, '_onLog', {
506
529
  // eslint-disable-next-line @typescript-eslint/no-namespace
507
530
  (function (gg) {
508
531
  })(gg || (gg = {}));
532
+ // Track if diagnostics have already run to prevent double execution
533
+ let diagnosticsRan = false;
509
534
  /**
510
535
  * Run gg diagnostics and log configuration status
511
536
  * Can be called immediately or delayed (e.g., after Eruda loads)
512
537
  */
513
538
  export async function runGgDiagnostics() {
514
- if (!ggConfig.showHints || isCloudflareWorker())
539
+ if (!ggConfig.showHints || isCloudflareWorker() || diagnosticsRan)
515
540
  return;
541
+ diagnosticsRan = true;
542
+ // Create test debugger for server-side enabled check
516
543
  const ggLogTest = debugFactory('gg:TEST');
517
544
  let ggMessage = '\n';
518
- // Utilities for forming ggMessage:
519
545
  const message = (s) => (ggMessage += `${s}\n`);
520
546
  const checkbox = (test) => (test ? '✅' : '❌');
521
547
  const makeHint = (test, ifTrue, ifFalse = '') => (test ? ifTrue : ifFalse);
522
- // Use plain console.log for diagnostics - appears in Eruda's Console tab
523
548
  console.log(`Loaded gg module. Checking configuration...`);
524
- if (ggConfig.enabled && ggLogTest.enabled) {
525
- gg('If you can see this logg, gg configured correctly!');
549
+ const configOk = BROWSER ? ggConfig.enabled : ggConfig.enabled && ggLogTest.enabled;
550
+ if (configOk) {
526
551
  message(`No problems detected:`);
527
552
  if (BROWSER) {
528
- message(`ℹ️ If gg output not visible: enable "Verbose" log level in DevTools, or check Eruda's GG tab.`);
553
+ message(`ℹ️ gg messages appear in the Eruda GG panel. Use Settings > Native Console to also show in browser console.`);
529
554
  }
530
555
  }
531
556
  else {
@@ -541,35 +566,27 @@ export async function runGgDiagnostics() {
541
566
  }
542
567
  }
543
568
  message(`${checkbox(ggConfig.enabled)} gg enabled: ${ggConfig.enabled}${enableHint}`);
544
- if (BROWSER) {
545
- const hint = makeHint(!ggLogTest.enabled, " (Try `localStorage.debug = 'gg:*'`)");
546
- message(`${checkbox(ggLogTest.enabled)} localStorage.debug: ${localStorage?.debug}${hint}`);
547
- }
548
- else {
549
- const hint = makeHint(!ggLogTest.enabled, ' (Try `DEBUG=gg:* npm dev`)');
569
+ if (!BROWSER) {
570
+ // Server-side: check DEBUG env var (the only output path on the server)
571
+ const hint = makeHint(!ggLogTest.enabled, ' (Try `DEBUG=gg:* npm run dev`)');
550
572
  if (dotenvModule) {
551
- dotenvModule.config(); // Load the environment variables
573
+ dotenvModule.config();
552
574
  }
553
575
  message(`${checkbox(ggLogTest.enabled)} DEBUG env variable: ${process?.env?.DEBUG}${hint}`);
554
576
  }
555
- // Optional plugin diagnostics listed last
556
- message(makeHint(_ggCallSitesPlugin, `✅ (optional) gg-call-sites vite plugin detected! Call-site namespaces baked in at build time.`, `⚠️ (optional) gg-call-sites vite plugin not detected. Add ggCallSitesPlugin() to vite.config.ts for build-time call-site namespaces (faster/more reliable). Without plugin, prod uses word-tuple names (e.g. calm-fox) as unique call-site identifiers.`));
577
+ // Optional plugin diagnostics
578
+ message(makeHint(_ggCallSitesPlugin, `✅ gg-call-sites vite plugin detected! Call-site namespaces and open-in-editor links baked in at build time.`, `⚠️ gg-call-sites vite plugin not detected. Add ggCallSitesPlugin() to vite.config.ts for file:line call-site namespaces and open-in-editor links. Without plugin, using word-tuple names (e.g. calm-fox) as call-site identifiers.`));
557
579
  if (BROWSER && DEV) {
558
580
  const { status } = await fetch('/__open-in-editor?file=+');
559
581
  message(makeHint(status === 222, `✅ (optional) open-in-editor vite plugin detected! (status code: ${status}) Clickable links open source files in editor.`, `⚠️ (optional) open-in-editor vite plugin not detected. (status code: ${status}) Add openInEditorPlugin() to vite.config.ts for clickable links that open source files in editor`));
560
582
  }
561
- // Use plain console.log for diagnostics - appears in Eruda's Console tab
562
583
  console.log(ggMessage);
563
- // Reset namespace width after configuration check
564
- // This prevents the long callpoint from the config check from affecting subsequent logs
565
584
  resetNamespaceWidth();
566
585
  }
567
- // Run diagnostics immediately on module load if Eruda is not being used
568
- // (If Eruda will load, the loader will call runGgDiagnostics after Eruda is ready)
569
- if (ggConfig.showHints && !isCloudflareWorker()) {
570
- // Only run immediately if we're not in a context where Eruda might load
571
- // In browser dev mode, assume Eruda might load and skip immediate diagnostics
572
- if (!BROWSER || !DEV) {
573
- runGgDiagnostics();
574
- }
586
+ // Run diagnostics immediately on module load ONLY in Node.js environments
587
+ // In browser, the Eruda loader (if configured) will call runGgDiagnostics()
588
+ // after Eruda is ready. If Eruda is not configured, diagnostics won't run
589
+ // in browser (user must manually check console or call runGgDiagnostics()).
590
+ if (ggConfig.showHints && !isCloudflareWorker() && !BROWSER) {
591
+ runGgDiagnostics();
575
592
  }
package/dist/index.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { gg, fg, bg } from './gg.js';
2
2
  import openInEditorPlugin from './open-in-editor.js';
3
3
  import ggCallSitesPlugin from './gg-call-sites-plugin.js';
4
+ export { default as GgConsole } from './GgConsole.svelte';
4
5
  export { gg, fg, bg, openInEditorPlugin, ggCallSitesPlugin };
package/dist/index.js CHANGED
@@ -2,4 +2,5 @@
2
2
  import { gg, fg, bg } from './gg.js';
3
3
  import openInEditorPlugin from './open-in-editor.js';
4
4
  import ggCallSitesPlugin from './gg-call-sites-plugin.js';
5
+ export { default as GgConsole } from './GgConsole.svelte';
5
6
  export { gg, fg, bg, openInEditorPlugin, ggCallSitesPlugin };
@@ -16,17 +16,30 @@ export default function openInEditorPlugin(specifiedEditor, srcRoot, onErrorCall
16
16
  name: 'open-in-editor',
17
17
  configureServer(server) {
18
18
  server.middlewares.use('/__open-in-editor', (req, res) => {
19
- const { file } = url.parse(req.url || '', true).query || {};
19
+ const { file, line, col, editor } = url.parse(req.url || '', true).query || {};
20
20
  if (!file) {
21
21
  res.statusCode = 500;
22
22
  res.end(`open-in-editor-plugin: required query param "file" is missing.`);
23
23
  }
24
24
  else {
25
25
  res.statusCode = 222;
26
- launch(path.resolve(srcRoot, file), specifiedEditor, onErrorCallback);
26
+ // launch-editor supports file:line:col format for cursor positioning
27
+ let fileArg = path.resolve(srcRoot, file);
28
+ if (line)
29
+ fileArg += `:${line}`;
30
+ if (line && col)
31
+ fileArg += `:${col}`;
32
+ // Use editor from query param if provided, otherwise fall back to plugin config
33
+ const editorToUse = typeof editor === 'string' && editor ? editor : specifiedEditor;
34
+ launch(fileArg, editorToUse, onErrorCallback);
27
35
  res.end('<p>You may safely close this window.</p><script>window.close()</script>');
28
36
  }
29
37
  });
38
+ // Expose project root for client-side $ROOT variable (forward slashes for URI compat)
39
+ server.middlewares.use('/__gg-project-root', (_req, res) => {
40
+ res.setHeader('Content-Type', 'text/plain');
41
+ res.end(srcRoot.replace(/\\/g, '/'));
42
+ });
30
43
  }
31
44
  };
32
45
  }
package/dist/vite.d.ts ADDED
@@ -0,0 +1,37 @@
1
+ import type { Plugin } from 'vite';
2
+ import ggCallSitesPlugin from './gg-call-sites-plugin.js';
3
+ import type { GgCallSitesPluginOptions } from './gg-call-sites-plugin.js';
4
+ import openInEditorPlugin from './open-in-editor.js';
5
+ export interface GgPluginsOptions {
6
+ /**
7
+ * Options for the call-sites Vite plugin (source metadata rewriting).
8
+ * @default {}
9
+ */
10
+ callSites?: GgCallSitesPluginOptions;
11
+ /**
12
+ * Enable the open-in-editor Vite plugin (dev server middleware).
13
+ * Set to `false` to disable.
14
+ * @default true
15
+ */
16
+ openInEditor?: boolean;
17
+ }
18
+ /**
19
+ * All gg Vite plugins bundled together.
20
+ *
21
+ * Includes:
22
+ * - `ggCallSitesPlugin` — rewrites `gg()` calls with source file/line/col metadata
23
+ * - `openInEditorPlugin` — adds `/__open-in-editor` dev server middleware
24
+ * - Automatic `es2022` build target (required for top-level await)
25
+ *
26
+ * @example
27
+ * ```ts
28
+ * import ggPlugins from '@leftium/gg/vite';
29
+ *
30
+ * export default defineConfig({
31
+ * plugins: [sveltekit(), ...ggPlugins()]
32
+ * });
33
+ * ```
34
+ */
35
+ export default function ggPlugins(options?: GgPluginsOptions): Plugin[];
36
+ export { ggCallSitesPlugin, openInEditorPlugin };
37
+ export type { GgCallSitesPluginOptions };
package/dist/vite.js ADDED
@@ -0,0 +1,46 @@
1
+ import ggCallSitesPlugin from './gg-call-sites-plugin.js';
2
+ import openInEditorPlugin from './open-in-editor.js';
3
+ /**
4
+ * All gg Vite plugins bundled together.
5
+ *
6
+ * Includes:
7
+ * - `ggCallSitesPlugin` — rewrites `gg()` calls with source file/line/col metadata
8
+ * - `openInEditorPlugin` — adds `/__open-in-editor` dev server middleware
9
+ * - Automatic `es2022` build target (required for top-level await)
10
+ *
11
+ * @example
12
+ * ```ts
13
+ * import ggPlugins from '@leftium/gg/vite';
14
+ *
15
+ * export default defineConfig({
16
+ * plugins: [sveltekit(), ...ggPlugins()]
17
+ * });
18
+ * ```
19
+ */
20
+ export default function ggPlugins(options = {}) {
21
+ const plugins = [];
22
+ // Ensure es2022 target for top-level await support
23
+ plugins.push({
24
+ name: 'gg-config',
25
+ config() {
26
+ return {
27
+ build: {
28
+ target: 'es2022'
29
+ },
30
+ optimizeDeps: {
31
+ esbuildOptions: {
32
+ target: 'es2022',
33
+ supported: { 'top-level-await': true }
34
+ }
35
+ }
36
+ };
37
+ }
38
+ });
39
+ plugins.push(ggCallSitesPlugin(options.callSites));
40
+ if (options.openInEditor !== false) {
41
+ plugins.push(openInEditorPlugin());
42
+ }
43
+ return plugins;
44
+ }
45
+ // Allow granular imports for advanced users
46
+ export { ggCallSitesPlugin, openInEditorPlugin };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@leftium/gg",
3
- "version": "0.0.31",
3
+ "version": "0.0.34",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/Leftium/gg.git"
@@ -9,9 +9,7 @@
9
9
  "files": [
10
10
  "dist",
11
11
  "!dist/**/*.test.*",
12
- "!dist/**/*.spec.*",
13
- "!dist/debug/",
14
- "patches/"
12
+ "!dist/**/*.spec.*"
15
13
  ],
16
14
  "sideEffects": [
17
15
  "**/*.css"
@@ -21,8 +19,13 @@
21
19
  "exports": {
22
20
  ".": {
23
21
  "types": "./dist/index.d.ts",
22
+ "svelte": "./dist/index.js",
24
23
  "default": "./dist/index.js"
25
24
  },
25
+ "./vite": {
26
+ "types": "./dist/vite.d.ts",
27
+ "default": "./dist/vite.js"
28
+ },
26
29
  "./eruda": {
27
30
  "types": "./dist/eruda/index.d.ts",
28
31
  "default": "./dist/eruda/index.js"
@@ -31,21 +34,21 @@
31
34
  "peerDependencies": {
32
35
  "svelte": "^5.0.0"
33
36
  },
37
+ "peerDependenciesMeta": {
38
+ "svelte": {
39
+ "optional": true
40
+ }
41
+ },
34
42
  "devDependencies": {
35
43
  "@eslint/compat": "^2.0.2",
36
44
  "@eslint/js": "^10.0.1",
37
45
  "@picocss/pico": "^2.1.1",
38
- "@rollup/plugin-commonjs": "^29.0.0",
39
- "@rollup/plugin-node-resolve": "^16.0.3",
40
- "@rollup/plugin-terser": "^0.4.4",
41
46
  "@sveltejs/adapter-vercel": "^6.3.1",
42
47
  "@sveltejs/kit": "^2.50.2",
43
48
  "@sveltejs/package": "^2.5.7",
44
49
  "@sveltejs/vite-plugin-svelte": "^6.2.4",
45
- "@types/debug": "^4.1.12",
46
50
  "@types/node": "^25.2.2",
47
51
  "add": "^2.0.6",
48
- "debug": "^4.4.3",
49
52
  "eruda": "^3.4.3",
50
53
  "eslint": "^10.0.0",
51
54
  "eslint-config-prettier": "^10.1.8",
@@ -54,21 +57,23 @@
54
57
  "prettier": "^3.8.1",
55
58
  "prettier-plugin-svelte": "^3.4.1",
56
59
  "publint": "^0.3.17",
57
- "rollup": "^4.57.1",
58
60
  "supports-color": "^10.2.2",
59
61
  "svelte": "^5.50.1",
60
62
  "svelte-check": "^4.3.6",
61
63
  "typescript": "^5.9.3",
62
64
  "typescript-eslint": "^8.55.0",
63
65
  "vite": "^7.3.1",
64
- "vite-plugin-devtools-json": "^1.0.0"
66
+ "vite-plugin-devtools-json": "^1.0.0",
67
+ "vitest": "^4.0.18"
65
68
  },
66
69
  "keywords": [
67
70
  "svelte"
68
71
  ],
69
72
  "dependencies": {
73
+ "@sveltejs/acorn-typescript": "^1.0.9",
74
+ "@types/estree": "^1.0.8",
75
+ "acorn": "^8.15.0",
70
76
  "dotenv": "^17.2.4",
71
- "error-stack-parser": "^2.1.4",
72
77
  "esm-env": "^1.2.2",
73
78
  "launch-editor": "^2.12.0"
74
79
  },
@@ -79,6 +84,8 @@
79
84
  "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
80
85
  "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
81
86
  "format": "prettier --write .",
82
- "lint": "prettier --check . && eslint ."
87
+ "lint": "prettier --check . && eslint .",
88
+ "test": "vitest run",
89
+ "test:watch": "vitest"
83
90
  }
84
91
  }
@@ -1,2 +0,0 @@
1
- export { w as default };
2
- declare var w: any;