@leftium/gg 0.0.47 → 0.0.49

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.js CHANGED
@@ -41,18 +41,11 @@ if (isCloudflareWorker()) {
41
41
  }
42
42
  // Lazy-load Node.js modules to avoid top-level await (Safari compatibility).
43
43
  // The imports start immediately but don't block module evaluation.
44
- let dotenvModule = null;
45
44
  let httpModule = null;
46
45
  function loadServerModules() {
47
46
  if (isCloudflareWorker() || BROWSER)
48
47
  return Promise.resolve();
49
48
  return (async () => {
50
- try {
51
- dotenvModule = await import('dotenv');
52
- }
53
- catch {
54
- // dotenv not available — optional dependency
55
- }
56
49
  try {
57
50
  httpModule = await import('http');
58
51
  }
@@ -221,7 +214,8 @@ function openInEditorUrl(fileName, line, col) {
221
214
  }
222
215
  export function gg(...args) {
223
216
  if (!ggConfig.enabled || isCloudflareWorker()) {
224
- return args.length ? args[0] : { fileName: '', functionName: '', url: '' };
217
+ // Return a no-op chain that skips logging
218
+ return new GgChain(args[0], args, { ns: '' }, true);
225
219
  }
226
220
  // Without the call-sites plugin, use cheap stack hash → deterministic word tuple.
227
221
  // When the plugin IS installed, all gg() calls are rewritten to gg._ns() at build time,
@@ -229,49 +223,166 @@ export function gg(...args) {
229
223
  // Same call site always produces the same word pair (e.g. "calm-fox").
230
224
  // depth=2: skip "Error" header [0] and gg() frame [1]
231
225
  const callpoint = resolveCallpoint(2);
232
- return ggLog({ ns: callpoint }, ...args);
226
+ return new GgChain(args[0], args, { ns: callpoint });
233
227
  }
234
228
  /**
235
- * gg.ns() - Log with an explicit namespace (callpoint label).
229
+ * gg.here() - Return call-site info for open-in-editor.
236
230
  *
237
- * Users call gg.ns() directly to set a meaningful label that survives
238
- * across builds. For the internal plugin-generated version with file
239
- * metadata, see gg._ns().
240
- *
241
- * The label supports template variables (substituted by the vite plugin
242
- * at build time, or at runtime for $NS):
243
- * $NS - auto-generated callpoint (file@fn with plugin, word-tuple without)
244
- * $FN - enclosing function name (plugin only, empty without)
245
- * $FILE - short file path (plugin only, empty without)
246
- * $LINE - line number (plugin only, empty without)
247
- * $COL - column number (plugin only, empty without)
248
- *
249
- * @param nsLabel - The namespace label (appears as gg:<nsLabel> in output)
250
- * @param args - Same arguments as gg()
251
- * @returns Same as gg() - the first arg, or call-site info if no args
231
+ * Replaces the old no-arg gg() overload. Returns an object with the
232
+ * file name, function name, and URL for opening the source in an editor.
252
233
  *
253
234
  * @example
254
- * gg.ns("auth", "login failed") // → gg:auth
255
- * gg.ns("ERROR:$NS", msg) // → gg:ERROR:routes/+page.svelte@handleClick (with plugin)
256
- * // → gg:ERROR:calm-fox (without plugin)
257
- * gg.ns("$NS:validation", fieldName) // → gg:routes/+page.svelte@handleClick:validation
235
+ * <OpenInEditorLink gg={gg.here()} />
258
236
  */
259
- gg.ns = function (nsLabel, ...args) {
260
- // Resolve $NS at runtime (word-tuple fallback when plugin isn't installed).
261
- // With the plugin, $NS is already substituted at build time before this runs.
262
- // depth=3: skip "Error" [0], resolveCallpoint [1], gg.ns [2] → caller [3]
263
- if (nsLabel.includes('$NS')) {
264
- const callpoint = resolveCallpoint(3);
265
- nsLabel = nsLabel.replace(/\$NS/g, callpoint);
237
+ gg.here = function () {
238
+ if (!ggConfig.enabled || isCloudflareWorker()) {
239
+ return { fileName: '', functionName: '', url: '' };
266
240
  }
267
- return gg._ns({ ns: nsLabel }, ...args);
241
+ const callpoint = resolveCallpoint(3);
242
+ const namespace = `gg:${callpoint}`;
243
+ // Log the call-site info
244
+ const ggLogFunction = namespaceToLogFunction.get(namespace) ||
245
+ namespaceToLogFunction.set(namespace, createGgDebugger(namespace)).get(namespace);
246
+ ggLogFunction(` 📝 ${callpoint}`);
247
+ return {
248
+ fileName: callpoint,
249
+ functionName: callpoint.includes('@') ? callpoint.split('@').pop() || '' : '',
250
+ url: ''
251
+ };
268
252
  };
253
+ /**
254
+ * Resolve template variables in a namespace label using metadata from the plugin.
255
+ *
256
+ * The Vite plugin bakes the auto-generated callpoint into options.ns at build time
257
+ * (e.g. "routes/+page.svelte@handleClick"). This function extracts components from
258
+ * that callpoint and substitutes template variables:
259
+ *
260
+ * $NS - the full auto-generated callpoint (or runtime word-tuple fallback)
261
+ * $FN - the function name portion (after @)
262
+ * $FILE - the file path portion (before @)
263
+ * $LINE - the line number
264
+ * $COL - the column number
265
+ */
266
+ function resolveNsTemplateVars(label, options) {
267
+ if (!label.includes('$'))
268
+ return label;
269
+ const ns = options.ns || '';
270
+ // $NS: use the full auto-generated callpoint. If no plugin, fall back to runtime stack hash.
271
+ if (label.includes('$NS')) {
272
+ const callpoint = ns || resolveCallpoint(4);
273
+ label = label.replace(/\$NS/g, callpoint);
274
+ }
275
+ // $FN: extract function name from "file@fn" format
276
+ if (label.includes('$FN')) {
277
+ const fn = ns.includes('@') ? ns.split('@').pop() || '' : '';
278
+ label = label.replace(/\$FN/g, fn);
279
+ }
280
+ // $FILE: extract file path from "file@fn" format
281
+ if (label.includes('$FILE')) {
282
+ const file = ns.includes('@') ? ns.split('@')[0] : ns;
283
+ label = label.replace(/\$FILE/g, file);
284
+ }
285
+ // $LINE / $COL: from plugin metadata
286
+ if (label.includes('$LINE')) {
287
+ label = label.replace(/\$LINE/g, String(options.line ?? ''));
288
+ }
289
+ if (label.includes('$COL')) {
290
+ label = label.replace(/\$COL/g, String(options.col ?? ''));
291
+ }
292
+ return label;
293
+ }
294
+ /**
295
+ * Chainable wrapper returned by gg(). Collects modifiers (.ns(), .warn(), etc.)
296
+ * and auto-flushes the log on the next microtask. Use `.v` to flush immediately
297
+ * and get the passthrough value.
298
+ *
299
+ * @example
300
+ * gg(value) // logs on microtask
301
+ * gg(value).ns('label').warn() // logs with namespace + warn level
302
+ * const x = gg(value).v // logs immediately, returns value
303
+ * const x = gg(value).ns('foo').v // logs with namespace, returns value
304
+ */
305
+ export class GgChain {
306
+ #value;
307
+ #args;
308
+ #options;
309
+ #flushed = false;
310
+ #disabled;
311
+ constructor(value, args, options, disabled = false) {
312
+ this.#value = value;
313
+ this.#args = args;
314
+ this.#options = options;
315
+ this.#disabled = disabled;
316
+ if (!disabled) {
317
+ // Auto-flush on microtask if not flushed synchronously by .v or another trigger
318
+ queueMicrotask(() => this.#flush());
319
+ }
320
+ }
321
+ /** Set a custom namespace for this log entry.
322
+ *
323
+ * Supports template variables (resolved from plugin-provided metadata):
324
+ * $NS - auto-generated callpoint (file@fn with plugin, word-tuple without)
325
+ * $FN - enclosing function name (extracted from $NS)
326
+ * $FILE - short file path (extracted from $NS)
327
+ * $LINE - line number
328
+ * $COL - column number
329
+ */
330
+ ns(label) {
331
+ this.#options.ns = resolveNsTemplateVars(label, this.#options);
332
+ return this;
333
+ }
334
+ /** Set log level to info (blue indicator). */
335
+ info() {
336
+ this.#options.level = 'info';
337
+ return this;
338
+ }
339
+ /** Set log level to warn (yellow indicator). */
340
+ warn() {
341
+ this.#options.level = 'warn';
342
+ return this;
343
+ }
344
+ /** Set log level to error (red indicator, captures stack trace). */
345
+ error() {
346
+ this.#options.level = 'error';
347
+ this.#options.stack = getErrorStack(this.#args[0], 3);
348
+ return this;
349
+ }
350
+ /** Include a full stack trace with this log entry. */
351
+ trace() {
352
+ this.#options.stack = captureStack(3);
353
+ return this;
354
+ }
355
+ /** Format the log output as an ASCII table. */
356
+ table(columns) {
357
+ const { keys, rows } = formatTable(this.#args[0], columns);
358
+ this.#options.tableData = { keys, rows };
359
+ // Override args to show '(table)' label, matching original gg.table() behavior
360
+ this.#args = ['(table)'];
361
+ // Also emit native console.table
362
+ if (columns) {
363
+ console.table(this.#value, columns);
364
+ }
365
+ else {
366
+ console.table(this.#value);
367
+ }
368
+ return this;
369
+ }
370
+ /** Flush the log immediately and return the passthrough value. */
371
+ get v() {
372
+ this.#flush();
373
+ return this.#value;
374
+ }
375
+ #flush() {
376
+ if (this.#flushed)
377
+ return;
378
+ this.#flushed = true;
379
+ ggLog(this.#options, ...this.#args);
380
+ }
381
+ }
269
382
  /**
270
383
  * Core logging function shared by all gg methods.
271
384
  *
272
- * All public methods (gg, gg.ns, gg.warn, gg.error, gg.table, etc.)
273
- * funnel through this function. It handles namespace resolution,
274
- * debug output, capture hook, and passthrough return.
385
+ * Handles namespace resolution, debug output, capture hook, and return value.
275
386
  */
276
387
  function ggLog(options, ...args) {
277
388
  const { ns: nsLabel, file, line, col, src, level, stack, tableData } = options;
@@ -286,24 +397,7 @@ function ggLog(options, ...args) {
286
397
  namespaceToLogFunction.set(namespace, createGgDebugger(namespace)).get(namespace);
287
398
  // Prepare args for logging (console output is value-only; src is carried
288
399
  // on CapturedEntry for the Eruda UI to display on hover)
289
- let logArgs;
290
- let returnValue;
291
- if (!args.length) {
292
- // No arguments: return call-site info for open-in-editor
293
- const fileName = file ? file.replace(srcRootRegex, '') : nsLabel;
294
- const functionName = nsLabel.includes('@') ? nsLabel.split('@').pop() || '' : '';
295
- const url = file ? openInEditorUrl(file, line, col) : '';
296
- logArgs = [` 📝 ${nsLabel}`];
297
- returnValue = { fileName, functionName, url };
298
- }
299
- else if (args.length === 1) {
300
- logArgs = [args[0]];
301
- returnValue = args[0];
302
- }
303
- else {
304
- logArgs = [args[0], ...args.slice(1)];
305
- returnValue = args[0];
306
- }
400
+ const logArgs = args.length === 0 ? ['(no args)'] : [...args];
307
401
  // Add level prefix emoji for info/warn/error
308
402
  if (level === 'info') {
309
403
  logArgs[0] = `ℹ️ ${logArgs[0]}`;
@@ -351,22 +445,39 @@ function ggLog(options, ...args) {
351
445
  else {
352
446
  earlyLogBuffer.push(entry);
353
447
  }
354
- return returnValue;
355
448
  }
356
449
  /**
357
450
  * gg._ns() - Internal: log with namespace and source file metadata.
358
451
  *
359
- * Called by the ggCallSitesPlugin Vite plugin, which rewrites both bare gg()
360
- * calls and manual gg.ns() calls to gg._ns({ns, file, line, col}, ...) at
361
- * build time. This gives each call site a unique namespace plus the source
452
+ * Called by the ggCallSitesPlugin Vite plugin, which rewrites bare gg()
453
+ * calls to gg._ns({ns, file, line, col, src}, ...) at build time.
454
+ * This gives each call site a unique namespace plus the source
362
455
  * location for open-in-editor support.
363
456
  *
364
- * @param options - { ns: string; file?: string; line?: number; col?: number }
365
- * @param args - Same arguments as gg()
366
- * @returns Same as gg() - the first arg, or call-site info if no args
457
+ * Returns a GgChain for chaining modifiers (.ns(), .warn(), etc.)
367
458
  */
368
459
  gg._ns = function (options, ...args) {
369
- return ggLog(options, ...args);
460
+ const disabled = !ggConfig.enabled || isCloudflareWorker();
461
+ return new GgChain(args[0], args, options, disabled);
462
+ };
463
+ /**
464
+ * gg._here() - Internal: call-site info with source metadata from Vite plugin.
465
+ *
466
+ * Called by the ggCallSitesPlugin when it rewrites gg.here() calls.
467
+ */
468
+ gg._here = function (options) {
469
+ if (!ggConfig.enabled || isCloudflareWorker()) {
470
+ return { fileName: '', functionName: '', url: '' };
471
+ }
472
+ const { ns: nsLabel, file, line, col } = options;
473
+ const namespace = `gg:${nsLabel}`;
474
+ const ggLogFunction = namespaceToLogFunction.get(namespace) ||
475
+ namespaceToLogFunction.set(namespace, createGgDebugger(namespace)).get(namespace);
476
+ ggLogFunction(` 📝 ${nsLabel}`);
477
+ const fileName = file ? file.replace(srcRootRegex, '') : nsLabel;
478
+ const functionName = nsLabel.includes('@') ? nsLabel.split('@').pop() || '' : '';
479
+ const url = file ? openInEditorUrl(file, line, col) : '';
480
+ return { fileName, functionName, url };
370
481
  };
371
482
  /**
372
483
  * gg._o() - Internal: build options object for gg._ns() without object literal syntax.
@@ -423,181 +534,66 @@ function getErrorStack(firstArg, skipFrames) {
423
534
  }
424
535
  return captureStack(skipFrames);
425
536
  }
537
+ // Timer storage for gg.time / gg.timeEnd / gg.timeLog
538
+ // Maps timer label → { start: number, ns?: string, options?: LogOptions }
539
+ const timers = new Map();
426
540
  /**
427
- * gg.info() - Log at info level.
428
- *
429
- * Passthrough: returns the first argument.
430
- * In Eruda, entries are styled with a blue/info indicator.
431
- *
432
- * @example
433
- * gg.info('System startup complete');
434
- * const config = gg.info(loadedConfig, 'loaded config');
435
- */
436
- gg.info = function (...args) {
437
- if (!ggConfig.enabled || isCloudflareWorker()) {
438
- return args.length ? args[0] : undefined;
439
- }
440
- const callpoint = resolveCallpoint(3);
441
- return ggLog({ ns: callpoint, level: 'info' }, ...args);
442
- };
443
- /**
444
- * gg._info() - Internal: info with call-site metadata from Vite plugin.
445
- */
446
- gg._info = function (options, ...args) {
447
- return ggLog({ ...options, level: 'info' }, ...args);
448
- };
449
- /**
450
- * gg.warn() - Log at warning level.
451
- *
452
- * Passthrough: returns the first argument.
453
- * In Eruda, entries are styled with a yellow/warning indicator.
454
- *
455
- * @example
456
- * gg.warn('deprecated API used');
457
- * const result = gg.warn(computeValue(), 'might be slow');
458
- */
459
- gg.warn = function (...args) {
460
- if (!ggConfig.enabled || isCloudflareWorker()) {
461
- return args.length ? args[0] : undefined;
462
- }
463
- const callpoint = resolveCallpoint(3);
464
- return ggLog({ ns: callpoint, level: 'warn' }, ...args);
465
- };
466
- /**
467
- * gg._warn() - Internal: warn with call-site metadata from Vite plugin.
468
- */
469
- gg._warn = function (options, ...args) {
470
- return ggLog({ ...options, level: 'warn' }, ...args);
471
- };
472
- /**
473
- * gg.error() - Log at error level.
474
- *
475
- * Passthrough: returns the first argument.
476
- * Captures a stack trace silently — visible in Eruda via a collapsible toggle.
477
- * If the first argument is an Error object, its .stack is used instead.
478
- *
479
- * @example
480
- * gg.error('connection failed');
481
- * gg.error(new Error('timeout'));
482
- * const val = gg.error(response, 'unexpected status');
483
- */
484
- gg.error = function (...args) {
485
- if (!ggConfig.enabled || isCloudflareWorker()) {
486
- return args.length ? args[0] : undefined;
487
- }
488
- const callpoint = resolveCallpoint(3);
489
- const stack = getErrorStack(args[0], 4);
490
- return ggLog({ ns: callpoint, level: 'error', stack }, ...args);
491
- };
492
- /**
493
- * gg._error() - Internal: error with call-site metadata from Vite plugin.
494
- */
495
- gg._error = function (options, ...args) {
496
- const stack = getErrorStack(args[0], 3);
497
- return ggLog({ ...options, level: 'error', stack }, ...args);
498
- };
499
- /**
500
- * gg.assert() - Log only if condition is false.
501
- *
502
- * Like console.assert: if the first argument is falsy, logs the remaining
503
- * arguments at error level. If the condition is truthy, does nothing.
504
- * Passthrough: always returns the condition value.
541
+ * Chainable wrapper returned by gg.time(). Only supports .ns() for setting
542
+ * the namespace for the entire timer group (inherited by timeLog/timeEnd).
505
543
  *
506
544
  * @example
507
- * gg.assert(user != null, 'user should exist');
508
- * gg.assert(list.length > 0, 'list is empty', list);
509
- */
510
- gg.assert = function (condition, ...args) {
511
- if (!condition) {
512
- if (!ggConfig.enabled || isCloudflareWorker())
513
- return condition;
514
- const callpoint = resolveCallpoint(3);
515
- const stack = captureStack(4);
516
- const assertArgs = args.length > 0 ? args : ['Assertion failed'];
517
- ggLog({ ns: callpoint, level: 'error', stack }, ...assertArgs);
518
- }
519
- return condition;
520
- };
521
- /**
522
- * gg._assert() - Internal: assert with call-site metadata from Vite plugin.
523
- */
524
- gg._assert = function (options, condition, ...args) {
525
- if (!condition) {
526
- if (!ggConfig.enabled || isCloudflareWorker())
527
- return condition;
528
- const stack = captureStack(3);
529
- const assertArgs = args.length > 0 ? args : ['Assertion failed'];
530
- ggLog({ ...options, level: 'error', stack }, ...assertArgs);
545
+ * gg.time('fetch').ns('api-pipeline')
546
+ * gg.time('fetch').ns('$FN:timers') // template vars work too
547
+ */
548
+ export class GgTimerChain {
549
+ #label;
550
+ #options;
551
+ constructor(label, options) {
552
+ this.#label = label;
553
+ this.#options = options;
554
+ }
555
+ /** Set a custom namespace for this timer group.
556
+ * Supports the same template variables as GgChain.ns().
557
+ */
558
+ ns(label) {
559
+ const resolved = resolveNsTemplateVars(label, this.#options);
560
+ const timer = timers.get(this.#label);
561
+ if (timer)
562
+ timer.ns = resolved;
563
+ return this;
531
564
  }
532
- return condition;
533
- };
565
+ }
534
566
  /**
535
- * gg.table() - Log tabular data.
567
+ * gg.time() - Start a named timer. Returns a GgTimerChain for optional .ns() chaining.
536
568
  *
537
- * Formats an array of objects (or an object of objects) as an ASCII table.
538
- * Passthrough: returns the data argument.
569
+ * @param label - Timer label (default: 'default')
539
570
  *
540
571
  * @example
541
- * gg.table([{name: 'Alice', age: 30}, {name: 'Bob', age: 25}]);
542
- * gg.table({a: {x: 1}, b: {x: 2}});
543
- */
544
- gg.table = function (data, columns) {
545
- if (!ggConfig.enabled || isCloudflareWorker())
546
- return data;
547
- const callpoint = resolveCallpoint(3);
548
- const { keys, rows } = formatTable(data, columns);
549
- ggLog({ ns: callpoint, tableData: { keys, rows } }, '(table)');
550
- // Also emit a native console.table for proper rendering in browser/Node consoles
551
- if (columns) {
552
- console.table(data, columns);
553
- }
554
- else {
555
- console.table(data);
556
- }
557
- return data;
558
- };
559
- /**
560
- * gg._table() - Internal: table with call-site metadata from Vite plugin.
561
- */
562
- gg._table = function (options, data, columns) {
563
- if (!ggConfig.enabled || isCloudflareWorker())
564
- return data;
565
- const { keys, rows } = formatTable(data, columns);
566
- ggLog({ ...options, tableData: { keys, rows } }, '(table)');
567
- if (columns) {
568
- console.table(data, columns);
569
- }
570
- else {
571
- console.table(data);
572
- }
573
- return data;
574
- };
575
- // Timer storage for gg.time / gg.timeEnd / gg.timeLog
576
- const timers = new Map();
577
- /**
578
- * gg.time() - Start a named timer.
579
- *
580
- * @example
581
- * gg.time('fetch');
582
- * const data = await fetchData();
583
- * gg.timeEnd('fetch'); // logs "+123ms fetch: 456ms"
572
+ * gg.time('fetch') // basic timer
573
+ * gg.time('fetch').ns('api-pipeline') // with namespace (inherited by timeLog/timeEnd)
574
+ * gg.time('fetch').ns('$FN:timers') // with template variable (plugin)
584
575
  */
585
576
  gg.time = function (label = 'default') {
586
- if (!ggConfig.enabled || isCloudflareWorker())
587
- return;
588
- timers.set(label, performance.now());
577
+ const options = { ns: resolveCallpoint(3) };
578
+ if (ggConfig.enabled && !isCloudflareWorker()) {
579
+ timers.set(label, { start: performance.now(), options });
580
+ }
581
+ return new GgTimerChain(label, options);
589
582
  };
590
583
  /** gg._time() - Internal: time with call-site metadata from Vite plugin. */
591
- gg._time = function (_options, label = 'default') {
592
- if (!ggConfig.enabled || isCloudflareWorker())
593
- return;
594
- timers.set(label, performance.now());
584
+ gg._time = function (options, label = 'default') {
585
+ if (ggConfig.enabled && !isCloudflareWorker()) {
586
+ timers.set(label, { start: performance.now(), options });
587
+ }
588
+ return new GgTimerChain(label, options);
595
589
  };
596
590
  /**
597
591
  * gg.timeLog() - Log the current elapsed time without stopping the timer.
598
592
  *
593
+ * Inherits the namespace set by gg.time().ns() for this timer label.
594
+ *
599
595
  * @example
600
- * gg.time('process');
596
+ * gg.time('process').ns('my-namespace');
601
597
  * // ... step 1 ...
602
598
  * gg.timeLog('process', 'step 1 done');
603
599
  * // ... step 2 ...
@@ -606,92 +602,66 @@ gg._time = function (_options, label = 'default') {
606
602
  gg.timeLog = function (label = 'default', ...args) {
607
603
  if (!ggConfig.enabled || isCloudflareWorker())
608
604
  return;
609
- const start = timers.get(label);
610
- if (start === undefined) {
605
+ const timer = timers.get(label);
606
+ if (timer === undefined) {
611
607
  const callpoint = resolveCallpoint(3);
612
608
  ggLog({ ns: callpoint, level: 'warn' }, `Timer '${label}' does not exist`);
613
609
  return;
614
610
  }
615
- const elapsed = performance.now() - start;
616
- const callpoint = resolveCallpoint(3);
617
- ggLog({ ns: callpoint }, `${label}: ${formatElapsed(elapsed)}`, ...args);
611
+ const elapsed = performance.now() - timer.start;
612
+ const ns = timer.ns ?? timer.options?.ns ?? resolveCallpoint(3);
613
+ ggLog({ ...timer.options, ns }, `${label}: ${formatElapsed(elapsed)}`, ...args);
618
614
  };
619
615
  /** gg._timeLog() - Internal: timeLog with call-site metadata from Vite plugin. */
620
616
  gg._timeLog = function (options, label = 'default', ...args) {
621
617
  if (!ggConfig.enabled || isCloudflareWorker())
622
618
  return;
623
- const start = timers.get(label);
624
- if (start === undefined) {
619
+ const timer = timers.get(label);
620
+ if (timer === undefined) {
625
621
  ggLog({ ...options, level: 'warn' }, `Timer '${label}' does not exist`);
626
622
  return;
627
623
  }
628
- const elapsed = performance.now() - start;
629
- ggLog(options, `${label}: ${formatElapsed(elapsed)}`, ...args);
624
+ const elapsed = performance.now() - timer.start;
625
+ const ns = timer.ns ?? timer.options?.ns ?? options.ns;
626
+ ggLog({ ...options, ns }, `${label}: ${formatElapsed(elapsed)}`, ...args);
630
627
  };
631
628
  /**
632
629
  * gg.timeEnd() - Stop a named timer and log the elapsed time.
633
630
  *
631
+ * Inherits the namespace set by gg.time().ns() for this timer label.
632
+ *
634
633
  * @example
635
- * gg.time('fetch');
634
+ * gg.time('fetch').ns('api-pipeline');
636
635
  * const data = await fetchData();
637
- * gg.timeEnd('fetch'); // logs "fetch: 456.12ms"
636
+ * gg.timeEnd('fetch'); // logs under 'api-pipeline' namespace
638
637
  */
639
638
  gg.timeEnd = function (label = 'default') {
640
639
  if (!ggConfig.enabled || isCloudflareWorker())
641
640
  return;
642
- const start = timers.get(label);
643
- if (start === undefined) {
641
+ const timer = timers.get(label);
642
+ if (timer === undefined) {
644
643
  const callpoint = resolveCallpoint(3);
645
644
  ggLog({ ns: callpoint, level: 'warn' }, `Timer '${label}' does not exist`);
646
645
  return;
647
646
  }
648
- const elapsed = performance.now() - start;
647
+ const elapsed = performance.now() - timer.start;
649
648
  timers.delete(label);
650
- const callpoint = resolveCallpoint(3);
651
- ggLog({ ns: callpoint }, `${label}: ${formatElapsed(elapsed)}`);
649
+ const ns = timer.ns ?? timer.options?.ns ?? resolveCallpoint(3);
650
+ ggLog({ ...timer.options, ns }, `${label}: ${formatElapsed(elapsed)}`);
652
651
  };
653
652
  /** gg._timeEnd() - Internal: timeEnd with call-site metadata from Vite plugin. */
654
653
  gg._timeEnd = function (options, label = 'default') {
655
654
  if (!ggConfig.enabled || isCloudflareWorker())
656
655
  return;
657
- const start = timers.get(label);
658
- if (start === undefined) {
656
+ const timer = timers.get(label);
657
+ if (timer === undefined) {
659
658
  ggLog({ ...options, level: 'warn' }, `Timer '${label}' does not exist`);
660
659
  return;
661
660
  }
662
- const elapsed = performance.now() - start;
661
+ const elapsed = performance.now() - timer.start;
663
662
  timers.delete(label);
664
- ggLog(options, `${label}: ${formatElapsed(elapsed)}`);
665
- };
666
- /**
667
- * gg.trace() - Log with a stack trace.
668
- *
669
- * Like console.trace: logs the arguments plus a full stack trace.
670
- * Passthrough: returns the first argument.
671
- *
672
- * @example
673
- * gg.trace('how did we get here?');
674
- * const val = gg.trace(result, 'call path');
675
- */
676
- gg.trace = function (...args) {
677
- if (!ggConfig.enabled || isCloudflareWorker()) {
678
- return args.length ? args[0] : undefined;
679
- }
680
- const callpoint = resolveCallpoint(3);
681
- const stack = captureStack(4);
682
- const traceArgs = args.length > 0 ? args : ['Trace'];
683
- return ggLog({ ns: callpoint, stack }, ...traceArgs);
684
- };
685
- /**
686
- * gg._trace() - Internal: trace with call-site metadata from Vite plugin.
687
- */
688
- gg._trace = function (options, ...args) {
689
- if (!ggConfig.enabled || isCloudflareWorker()) {
690
- return args.length ? args[0] : undefined;
691
- }
692
- const stack = captureStack(3);
693
- const traceArgs = args.length > 0 ? args : ['Trace'];
694
- return ggLog({ ...options, stack }, ...traceArgs);
663
+ const ns = timer.ns ?? timer.options?.ns ?? options.ns;
664
+ ggLog({ ...options, ns }, `${label}: ${formatElapsed(elapsed)}`);
695
665
  };
696
666
  /**
697
667
  * Format elapsed time with appropriate precision.
@@ -986,7 +956,7 @@ export async function runGgDiagnostics() {
986
956
  if (!ggConfig.showHints || isCloudflareWorker() || diagnosticsRan)
987
957
  return;
988
958
  diagnosticsRan = true;
989
- // Ensure server modules (dotenv) and debug factory are loaded before diagnostics
959
+ // Ensure server modules and debug factory are loaded before diagnostics
990
960
  await serverModulesReady;
991
961
  await debugReady;
992
962
  // Create test debugger for server-side enabled check
@@ -1018,10 +988,7 @@ export async function runGgDiagnostics() {
1018
988
  message(`${checkbox(ggConfig.enabled)} gg enabled: ${ggConfig.enabled}${enableHint}`);
1019
989
  if (!BROWSER) {
1020
990
  // Server-side: check DEBUG env var (the only output path on the server)
1021
- const hint = makeHint(!ggLogTest.enabled, ' (Try `DEBUG=gg:* npm run dev`)');
1022
- if (dotenvModule) {
1023
- dotenvModule.config();
1024
- }
991
+ const hint = makeHint(!ggLogTest.enabled, ' (Try `DEBUG=gg:* npm run dev` or use --env-file=.env)');
1025
992
  message(`${checkbox(ggLogTest.enabled)} DEBUG env variable: ${process?.env?.DEBUG}${hint}`);
1026
993
  }
1027
994
  // Optional plugin diagnostics
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { gg, fg, bg, bold, italic, underline, dim } from './gg.js';
1
+ import { gg, GgChain, GgTimerChain, fg, bg, bold, italic, underline, dim } from './gg.js';
2
2
  import openInEditorPlugin from './open-in-editor.js';
3
3
  import ggCallSitesPlugin from './gg-call-sites-plugin.js';
4
4
  export { default as GgConsole } from './GgConsole.svelte';
5
- export { gg, fg, bg, bold, italic, underline, dim, openInEditorPlugin, ggCallSitesPlugin };
5
+ export { gg, GgChain, GgTimerChain, fg, bg, bold, italic, underline, dim, openInEditorPlugin, ggCallSitesPlugin };