@leftium/gg 0.0.52 → 0.0.54
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/eruda/index.js +5 -9
- package/dist/gg-file-sink-plugin.js +164 -101
- package/dist/gg.d.ts +14 -0
- package/dist/gg.js +108 -26
- package/package.json +1 -1
package/dist/eruda/index.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { BROWSER } from 'esm-env';
|
|
2
2
|
import { shouldLoadEruda, loadEruda } from './loader.js';
|
|
3
|
+
import { enableGgRuntime } from '../gg.js';
|
|
3
4
|
let initialized = false;
|
|
4
5
|
/**
|
|
5
6
|
* Initialize the gg Eruda plugin
|
|
@@ -62,16 +63,11 @@ function setupGestureDetection(options) {
|
|
|
62
63
|
// Reset timer on each tap
|
|
63
64
|
if (tapTimer)
|
|
64
65
|
clearTimeout(tapTimer);
|
|
65
|
-
// If 5 taps detected, load Eruda
|
|
66
|
+
// If 5 taps detected, enable gg logging and load Eruda
|
|
66
67
|
if (tapCount >= 5) {
|
|
67
|
-
console.log('[gg] 5 taps detected, loading Eruda
|
|
68
|
-
//
|
|
69
|
-
|
|
70
|
-
localStorage.setItem('gg-enabled', 'true');
|
|
71
|
-
}
|
|
72
|
-
catch {
|
|
73
|
-
// localStorage might not be available
|
|
74
|
-
}
|
|
68
|
+
console.log('[gg] 5 taps detected, enabling gg logging and loading Eruda (session-only). For persistent logging across app restarts, run: localStorage.setItem("gg-enabled", "true")');
|
|
69
|
+
// Enable gg runtime for this session (flips ggConfig.enabled in memory only)
|
|
70
|
+
enableGgRuntime();
|
|
75
71
|
loadEruda(options);
|
|
76
72
|
resetTaps();
|
|
77
73
|
return;
|
|
@@ -5,14 +5,10 @@ import { matchesPattern } from './pattern.js';
|
|
|
5
5
|
/**
|
|
6
6
|
* Serialize a CapturedEntry for writing to the JSONL log file.
|
|
7
7
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
* Server path: `serializeEntry(entry, 'server')` → appended via configureServer listener
|
|
14
|
-
* SSR path: ssrInjection string (search for `__ggFileSinkServerWriter`)
|
|
15
|
-
* Browser path: injection string (search for `__ggFileSinkSender`)
|
|
8
|
+
* Used by:
|
|
9
|
+
* - configureServer listener (plugin's own gg instance)
|
|
10
|
+
* - globalThis.__ggFileSink.write() (SSR gg instances via self-registration)
|
|
11
|
+
* - Virtual module sender mirrors this schema as inline JS (search for `__ggFileSinkSender`)
|
|
16
12
|
*/
|
|
17
13
|
function serializeEntry(entry, env, origin) {
|
|
18
14
|
const out = {
|
|
@@ -20,7 +16,7 @@ function serializeEntry(entry, env, origin) {
|
|
|
20
16
|
msg: entry.message,
|
|
21
17
|
ts: entry.timestamp,
|
|
22
18
|
env,
|
|
23
|
-
diff: entry.diff
|
|
19
|
+
diff: entry.diff,
|
|
24
20
|
};
|
|
25
21
|
if (entry.level && entry.level !== 'debug')
|
|
26
22
|
out.lvl = entry.level;
|
|
@@ -161,88 +157,97 @@ function collapseRepeats(entries) {
|
|
|
161
157
|
}
|
|
162
158
|
return out;
|
|
163
159
|
}
|
|
160
|
+
/**
|
|
161
|
+
* Virtual module ID for the browser-side file sink sender.
|
|
162
|
+
*
|
|
163
|
+
* Virtual modules go through Vite's normal transform pipeline (NOT pre-bundled
|
|
164
|
+
* by esbuild), so `import.meta.hot` is available. This solves the fundamental
|
|
165
|
+
* problem: code inside pre-bundled deps (like gg.js) cannot use import.meta.hot
|
|
166
|
+
* because esbuild evaluates `typeof import.meta.hot` as "undefined" during
|
|
167
|
+
* dep optimization and tree-shakes the entire block.
|
|
168
|
+
*
|
|
169
|
+
* The virtual module is imported via a <script type="module"> tag injected by
|
|
170
|
+
* the `transformIndexHtml` hook below.
|
|
171
|
+
*/
|
|
172
|
+
const VIRTUAL_MODULE_ID = 'virtual:gg-file-sink-sender';
|
|
173
|
+
const RESOLVED_VIRTUAL_MODULE_ID = '\0' + VIRTUAL_MODULE_ID;
|
|
164
174
|
export default function ggFileSinkPlugin(options = {}) {
|
|
165
175
|
let logFile;
|
|
166
176
|
let serverSideListener = null;
|
|
167
|
-
let ggModulePath = '';
|
|
168
177
|
return {
|
|
169
178
|
name: 'gg-file-sink',
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
const candidates = [
|
|
175
|
-
path.resolve(config.root, 'src/lib/gg.ts'),
|
|
176
|
-
path.resolve(config.root, 'node_modules/@leftium/gg/src/lib/gg.ts')
|
|
177
|
-
];
|
|
178
|
-
ggModulePath = candidates.find((p) => fs.existsSync(p)) ?? candidates[0];
|
|
179
|
+
// Virtual module: resolve and load the browser-side HMR sender.
|
|
180
|
+
resolveId(id) {
|
|
181
|
+
if (id === VIRTUAL_MODULE_ID)
|
|
182
|
+
return RESOLVED_VIRTUAL_MODULE_ID;
|
|
179
183
|
},
|
|
180
|
-
|
|
181
|
-
if (id
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
//
|
|
185
|
-
//
|
|
186
|
-
//
|
|
187
|
-
//
|
|
188
|
-
//
|
|
189
|
-
//
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
if (entry.tableData) s.table = entry.tableData;
|
|
209
|
-
try { __ggAppendFileSync(__ggLogFile, JSON.stringify(s) + '\\n'); } catch {}
|
|
210
|
-
});
|
|
184
|
+
load(id) {
|
|
185
|
+
if (id === RESOLVED_VIRTUAL_MODULE_ID) {
|
|
186
|
+
// This code runs through Vite's normal transform pipeline (not
|
|
187
|
+
// pre-bundled), so import.meta.hot is properly available.
|
|
188
|
+
//
|
|
189
|
+
// Uses a dual strategy for sending log entries to the dev server:
|
|
190
|
+
// 1. import.meta.hot.send() via HMR WebSocket (fast, no HTTP overhead)
|
|
191
|
+
// 2. fetch() POST to /__gg/logs as fallback (works even if HMR is unavailable)
|
|
192
|
+
//
|
|
193
|
+
// NOTE: this string mirrors serializeEntry() above — keep in sync.
|
|
194
|
+
return `
|
|
195
|
+
import { gg } from '@leftium/gg';
|
|
196
|
+
|
|
197
|
+
const origin =
|
|
198
|
+
typeof window !== 'undefined' && '__TAURI_INTERNALS__' in window
|
|
199
|
+
? 'tauri'
|
|
200
|
+
: 'browser';
|
|
201
|
+
|
|
202
|
+
// Batch entries and flush via fetch POST — works regardless of HMR state.
|
|
203
|
+
let __ggPendingEntries = [];
|
|
204
|
+
let __ggFlushTimer = null;
|
|
205
|
+
function __ggFlushEntries() {
|
|
206
|
+
__ggFlushTimer = null;
|
|
207
|
+
if (__ggPendingEntries.length === 0) return;
|
|
208
|
+
const batch = __ggPendingEntries;
|
|
209
|
+
__ggPendingEntries = [];
|
|
210
|
+
const body = batch.map(e => JSON.stringify(e)).join('\\n');
|
|
211
|
+
fetch('/__gg/logs', { method: 'POST', body, headers: { 'Content-Type': 'text/plain' } }).catch(() => {});
|
|
211
212
|
}
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
msg: entry.message,
|
|
231
|
-
ts: entry.timestamp,
|
|
232
|
-
env: 'client',
|
|
233
|
-
origin: __ggFileSinkOrigin,
|
|
234
|
-
diff: entry.diff,
|
|
235
|
-
};
|
|
236
|
-
if (entry.level && entry.level !== 'debug') s.lvl = entry.level;
|
|
237
|
-
if (entry.file) s.file = entry.file;
|
|
238
|
-
if (entry.line !== undefined) s.line = entry.line;
|
|
239
|
-
if (entry.src) s.src = entry.src;
|
|
240
|
-
if (entry.tableData) s.table = entry.tableData;
|
|
213
|
+
|
|
214
|
+
gg.addLogListener(function __ggFileSinkSender(entry) {
|
|
215
|
+
const s = {
|
|
216
|
+
ns: entry.namespace,
|
|
217
|
+
msg: entry.message,
|
|
218
|
+
ts: entry.timestamp,
|
|
219
|
+
env: 'client',
|
|
220
|
+
origin,
|
|
221
|
+
diff: entry.diff,
|
|
222
|
+
};
|
|
223
|
+
if (entry.level && entry.level !== 'debug') s.lvl = entry.level;
|
|
224
|
+
if (entry.file) s.file = entry.file;
|
|
225
|
+
if (entry.line !== undefined) s.line = entry.line;
|
|
226
|
+
if (entry.src) s.src = entry.src;
|
|
227
|
+
if (entry.tableData) s.table = entry.tableData;
|
|
228
|
+
|
|
229
|
+
// Try HMR first (lowest latency), fall back to batched fetch
|
|
230
|
+
if (import.meta.hot) {
|
|
241
231
|
import.meta.hot.send('gg:log', { entry: s });
|
|
242
|
-
}
|
|
243
|
-
|
|
232
|
+
} else {
|
|
233
|
+
__ggPendingEntries.push(s);
|
|
234
|
+
if (!__ggFlushTimer) __ggFlushTimer = setTimeout(__ggFlushEntries, 100);
|
|
235
|
+
}
|
|
236
|
+
});
|
|
244
237
|
`;
|
|
245
|
-
|
|
238
|
+
}
|
|
239
|
+
},
|
|
240
|
+
// Inject the virtual module into the HTML page so it runs in the browser.
|
|
241
|
+
// Uses both transformIndexHtml (plain Vite apps) and a response-intercepting
|
|
242
|
+
// middleware (SvelteKit and other frameworks that bypass Vite's HTML pipeline).
|
|
243
|
+
transformIndexHtml() {
|
|
244
|
+
return [
|
|
245
|
+
{
|
|
246
|
+
tag: 'script',
|
|
247
|
+
attrs: { type: 'module', src: `/@id/${VIRTUAL_MODULE_ID}` },
|
|
248
|
+
injectTo: 'head',
|
|
249
|
+
},
|
|
250
|
+
];
|
|
246
251
|
},
|
|
247
252
|
configureServer(server) {
|
|
248
253
|
// Truncate/create log file once the actual port is known.
|
|
@@ -250,26 +255,29 @@ if (import.meta.hot) {
|
|
|
250
255
|
// before listening fires, so no entries are written to the wrong file.
|
|
251
256
|
server.httpServer?.once('listening', () => {
|
|
252
257
|
const addr = server.httpServer?.address();
|
|
253
|
-
const port = addr && typeof addr === 'object'
|
|
254
|
-
|
|
258
|
+
const port = addr && typeof addr === 'object'
|
|
259
|
+
? addr.port
|
|
260
|
+
: (server.config.server.port ?? 5173);
|
|
261
|
+
const dir = options.dir
|
|
262
|
+
? path.resolve(options.dir)
|
|
263
|
+
: path.resolve(process.cwd(), '.gg');
|
|
255
264
|
fs.mkdirSync(dir, { recursive: true });
|
|
256
265
|
logFile = path.join(dir, `logs-${port}.jsonl`);
|
|
257
266
|
fs.writeFileSync(logFile, '');
|
|
258
267
|
});
|
|
259
|
-
// Expose appendFileSync + logFile path via globalThis so the SSR-injected
|
|
260
|
-
// listener (running in Vite's separate module runner context) can write to the
|
|
261
|
-
// same file without needing its own fs import.
|
|
262
|
-
globalThis.__ggFileSink = {
|
|
263
|
-
appendFileSync: fs.appendFileSync.bind(fs),
|
|
264
|
-
get logFile() {
|
|
265
|
-
return logFile;
|
|
266
|
-
}
|
|
267
|
-
};
|
|
268
268
|
const appendEntry = (serialized) => {
|
|
269
269
|
if (!logFile)
|
|
270
270
|
return;
|
|
271
271
|
fs.appendFileSync(logFile, JSON.stringify(serialized) + '\n');
|
|
272
272
|
};
|
|
273
|
+
// Expose a write() function via globalThis so ANY gg module instance
|
|
274
|
+
// (SSR, pre-bundled, monorepo-hoisted) can self-register a listener
|
|
275
|
+
// that writes to the log file. This replaces the broken transform hook.
|
|
276
|
+
globalThis.__ggFileSink = {
|
|
277
|
+
write(entry, env, origin) {
|
|
278
|
+
appendEntry(serializeEntry(entry, env, origin));
|
|
279
|
+
},
|
|
280
|
+
};
|
|
273
281
|
// Client-side entries arrive via HMR custom event
|
|
274
282
|
server.hot.on('gg:log', (data) => {
|
|
275
283
|
if (!data?.entry)
|
|
@@ -277,7 +285,7 @@ if (import.meta.hot) {
|
|
|
277
285
|
const serialized = {
|
|
278
286
|
...data.entry,
|
|
279
287
|
env: 'client',
|
|
280
|
-
origin: data.entry.origin ?? 'browser'
|
|
288
|
+
origin: data.entry.origin ?? 'browser',
|
|
281
289
|
};
|
|
282
290
|
appendEntry(serialized);
|
|
283
291
|
});
|
|
@@ -297,6 +305,29 @@ if (import.meta.hot) {
|
|
|
297
305
|
// /__gg/ index — JSON status for agents and developers
|
|
298
306
|
server.middlewares.use('/__gg', (req, res, next) => {
|
|
299
307
|
const pathname = new URL(req.url || '/', 'http://x').pathname;
|
|
308
|
+
// /__gg/stack — dump Connect middleware stack for debugging
|
|
309
|
+
if (pathname === '/stack') {
|
|
310
|
+
const stack = server.middlewares.stack;
|
|
311
|
+
const routes = stack.map((layer, i) => ({
|
|
312
|
+
i,
|
|
313
|
+
route: layer.route,
|
|
314
|
+
name: layer.handle?.name || '(anonymous)',
|
|
315
|
+
}));
|
|
316
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
317
|
+
res.end(JSON.stringify(routes, null, 2));
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
// /__gg/sender — redirect to the virtual module URL so Vite's
|
|
321
|
+
// normal transform pipeline handles it (including HMR injection).
|
|
322
|
+
// Direct transformRequest() fails in consumer apps because the
|
|
323
|
+
// module graph hasn't loaded the virtual module yet at request time.
|
|
324
|
+
if (pathname === '/sender') {
|
|
325
|
+
res.writeHead(302, {
|
|
326
|
+
Location: `/@id/${VIRTUAL_MODULE_ID}`,
|
|
327
|
+
});
|
|
328
|
+
res.end();
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
300
331
|
// Only handle exact /__gg or /__gg/ — let other /__gg/* routes fall through
|
|
301
332
|
if (pathname !== '' && pathname !== '/')
|
|
302
333
|
return next();
|
|
@@ -312,7 +343,9 @@ if (import.meta.hot) {
|
|
|
312
343
|
}
|
|
313
344
|
const port = (() => {
|
|
314
345
|
const addr = server.httpServer?.address();
|
|
315
|
-
return addr && typeof addr === 'object'
|
|
346
|
+
return addr && typeof addr === 'object'
|
|
347
|
+
? addr.port
|
|
348
|
+
: (server.config.server.port ?? 5173);
|
|
316
349
|
})();
|
|
317
350
|
const body = JSON.stringify({
|
|
318
351
|
plugin: 'gg-file-sink',
|
|
@@ -321,8 +354,8 @@ if (import.meta.hot) {
|
|
|
321
354
|
endpoints: {
|
|
322
355
|
'GET /__gg/logs': 'read deduplicated JSONL entries (?filter=, ?since=, ?env=, ?origin=, ?all, ?mismatch, ?raw)',
|
|
323
356
|
'DELETE /__gg/logs': 'truncate log file',
|
|
324
|
-
'GET /__gg/project-root': 'project root path'
|
|
325
|
-
}
|
|
357
|
+
'GET /__gg/project-root': 'project root path',
|
|
358
|
+
},
|
|
326
359
|
}, null, 2);
|
|
327
360
|
res.statusCode = 200;
|
|
328
361
|
res.setHeader('Content-Type', 'application/json; charset=utf-8');
|
|
@@ -337,6 +370,31 @@ if (import.meta.hot) {
|
|
|
337
370
|
res.end();
|
|
338
371
|
return;
|
|
339
372
|
}
|
|
373
|
+
// POST: receive client-side log entries via fetch (fallback for HMR)
|
|
374
|
+
if (method === 'POST') {
|
|
375
|
+
let body = '';
|
|
376
|
+
req.on('data', (chunk) => {
|
|
377
|
+
body += chunk.toString();
|
|
378
|
+
});
|
|
379
|
+
req.on('end', () => {
|
|
380
|
+
try {
|
|
381
|
+
const lines = body.split('\n').filter((l) => l.trim());
|
|
382
|
+
for (const line of lines) {
|
|
383
|
+
const entry = JSON.parse(line);
|
|
384
|
+
entry.env = 'client';
|
|
385
|
+
entry.origin = entry.origin ?? 'browser';
|
|
386
|
+
appendEntry(entry);
|
|
387
|
+
}
|
|
388
|
+
res.statusCode = 204;
|
|
389
|
+
res.end();
|
|
390
|
+
}
|
|
391
|
+
catch (err) {
|
|
392
|
+
res.statusCode = 400;
|
|
393
|
+
res.end(String(err));
|
|
394
|
+
}
|
|
395
|
+
});
|
|
396
|
+
return;
|
|
397
|
+
}
|
|
340
398
|
if (method === 'DELETE') {
|
|
341
399
|
try {
|
|
342
400
|
fs.writeFileSync(logFile, '');
|
|
@@ -358,7 +416,9 @@ if (import.meta.hot) {
|
|
|
358
416
|
const noCollapse = params.has('raw');
|
|
359
417
|
res.setHeader('Content-Type', 'text/plain; charset=utf-8');
|
|
360
418
|
const fileContent = fs.readFileSync(logFile, 'utf-8');
|
|
361
|
-
const lines = fileContent
|
|
419
|
+
const lines = fileContent
|
|
420
|
+
.split('\n')
|
|
421
|
+
.filter((l) => l.trim());
|
|
362
422
|
// Pre-dedup filters: namespace glob and timestamp (symmetric — don't affect cross-env index)
|
|
363
423
|
const preFiltered = lines.filter((l) => filterLinePreDedup(l, params));
|
|
364
424
|
// Parse surviving lines for dedup/mismatch pass
|
|
@@ -375,9 +435,12 @@ if (import.meta.hot) {
|
|
|
375
435
|
// Post-dedup filters: env and origin (applied after so cross-env index is intact)
|
|
376
436
|
const postFiltered = deduped.filter((e) => filterEntryPostDedup(e, params));
|
|
377
437
|
// Collapse consecutive repeated messages (count field), unless ?raw
|
|
378
|
-
const result = noCollapse
|
|
438
|
+
const result = noCollapse
|
|
439
|
+
? postFiltered
|
|
440
|
+
: collapseRepeats(postFiltered);
|
|
379
441
|
res.statusCode = 200;
|
|
380
|
-
res.end(result.map((e) => JSON.stringify(e)).join('\n') +
|
|
442
|
+
res.end(result.map((e) => JSON.stringify(e)).join('\n') +
|
|
443
|
+
(result.length ? '\n' : ''));
|
|
381
444
|
}
|
|
382
445
|
catch (err) {
|
|
383
446
|
res.statusCode = 500;
|
|
@@ -389,6 +452,6 @@ if (import.meta.hot) {
|
|
|
389
452
|
res.setHeader('Allow', 'GET, HEAD, DELETE');
|
|
390
453
|
res.end('Method Not Allowed');
|
|
391
454
|
});
|
|
392
|
-
}
|
|
455
|
+
},
|
|
393
456
|
};
|
|
394
457
|
}
|
package/dist/gg.d.ts
CHANGED
|
@@ -91,6 +91,20 @@ export declare class GgChain<T> {
|
|
|
91
91
|
/** Flush the log immediately and return the passthrough value. */
|
|
92
92
|
get v(): T;
|
|
93
93
|
}
|
|
94
|
+
/**
|
|
95
|
+
* Enable gg logging at runtime (e.g., after 5-tap gesture in production).
|
|
96
|
+
* Flips the internal enabled flag so subsequent gg() calls start capturing.
|
|
97
|
+
* This is session-only — does not persist to localStorage, so the next
|
|
98
|
+
* page load / app launch starts with gg disabled again.
|
|
99
|
+
*
|
|
100
|
+
* To enable gg across app restarts (e.g., in a prod Tauri app), run this
|
|
101
|
+
* in the Eruda console instead:
|
|
102
|
+
* localStorage.setItem("gg-enabled", "true")
|
|
103
|
+
* Clear with: localStorage.removeItem("gg-enabled") or gg.clearPersist()
|
|
104
|
+
*
|
|
105
|
+
* @internal Used by the Eruda gesture handler — not part of the public API.
|
|
106
|
+
*/
|
|
107
|
+
export declare function enableGgRuntime(): void;
|
|
94
108
|
/**
|
|
95
109
|
* Chainable wrapper returned by gg.time(). Only supports .ns() for setting
|
|
96
110
|
* the namespace for the entire timer group (inherited by timeLog/timeEnd).
|
package/dist/gg.js
CHANGED
|
@@ -71,7 +71,8 @@ function getServerPort() {
|
|
|
71
71
|
return new Promise((resolve) => {
|
|
72
72
|
if (BROWSER) {
|
|
73
73
|
// Browser environment
|
|
74
|
-
const currentPort = window.location.port ||
|
|
74
|
+
const currentPort = window.location.port ||
|
|
75
|
+
(window.location.protocol === 'https:' ? '443' : '80');
|
|
75
76
|
// Resolve the promise with the detected port
|
|
76
77
|
resolve(currentPort);
|
|
77
78
|
}
|
|
@@ -165,7 +166,7 @@ const ggConfig = {
|
|
|
165
166
|
// filename B : http://localhost:5173/src/lib/gg.ts
|
|
166
167
|
// srcRootprefix : http://localhost:5173/src/
|
|
167
168
|
// <folderName> group: src
|
|
168
|
-
srcRootPattern: '.*?(/(?<folderName>src|chunks)/)'
|
|
169
|
+
srcRootPattern: '.*?(/(?<folderName>src|chunks)/)',
|
|
169
170
|
};
|
|
170
171
|
const srcRootRegex = new RegExp(ggConfig.srcRootPattern, 'i');
|
|
171
172
|
// To maintain unique millisecond diffs for each callpoint:
|
|
@@ -240,12 +241,16 @@ gg.here = () => {
|
|
|
240
241
|
const namespace = callpoint;
|
|
241
242
|
// Log the call-site info
|
|
242
243
|
const ggLogFunction = namespaceToLogFunction.get(namespace) ||
|
|
243
|
-
namespaceToLogFunction
|
|
244
|
+
namespaceToLogFunction
|
|
245
|
+
.set(namespace, createGgDebugger(namespace))
|
|
246
|
+
.get(namespace);
|
|
244
247
|
ggLogFunction(` 📝 ${callpoint}`);
|
|
245
248
|
return {
|
|
246
249
|
fileName: callpoint,
|
|
247
|
-
functionName: callpoint.includes('@')
|
|
248
|
-
|
|
250
|
+
functionName: callpoint.includes('@')
|
|
251
|
+
? callpoint.split('@').pop() || ''
|
|
252
|
+
: '',
|
|
253
|
+
url: '',
|
|
249
254
|
};
|
|
250
255
|
};
|
|
251
256
|
/**
|
|
@@ -424,7 +429,10 @@ function formatValue(v) {
|
|
|
424
429
|
/** JSON replacer that limits nesting depth to avoid huge output. */
|
|
425
430
|
function jsonReplacer(key, value) {
|
|
426
431
|
// 'this' is the parent object; key === '' at the root level
|
|
427
|
-
if (key !== '' &&
|
|
432
|
+
if (key !== '' &&
|
|
433
|
+
typeof value === 'object' &&
|
|
434
|
+
value !== null &&
|
|
435
|
+
!Array.isArray(value)) {
|
|
428
436
|
// Count depth by checking how many ancestors are objects/arrays
|
|
429
437
|
// Simple approximation: truncate any nested object at depth > 2
|
|
430
438
|
const str = JSON.stringify(value);
|
|
@@ -434,7 +442,7 @@ function jsonReplacer(key, value) {
|
|
|
434
442
|
return value;
|
|
435
443
|
}
|
|
436
444
|
function ggLog(options, ...args) {
|
|
437
|
-
const { ns: nsLabel, file, line, col, src, level, stack, tableData } = options;
|
|
445
|
+
const { ns: nsLabel, file, line, col, src, level, stack, tableData, } = options;
|
|
438
446
|
if (!ggConfig.enabled) {
|
|
439
447
|
return args.length ? args[0] : { fileName: '', functionName: '', url: '' };
|
|
440
448
|
}
|
|
@@ -443,12 +451,20 @@ function ggLog(options, ...args) {
|
|
|
443
451
|
maxCallpointLength = namespace.length;
|
|
444
452
|
}
|
|
445
453
|
const ggLogFunction = namespaceToLogFunction.get(namespace) ||
|
|
446
|
-
namespaceToLogFunction
|
|
454
|
+
namespaceToLogFunction
|
|
455
|
+
.set(namespace, createGgDebugger(namespace))
|
|
456
|
+
.get(namespace);
|
|
447
457
|
// Prepare args for logging (console output is value-only; src is carried
|
|
448
458
|
// on CapturedEntry for the Eruda UI to display on hover)
|
|
449
459
|
const logArgs = args.length === 0 ? ['(no args)'] : [...args];
|
|
450
460
|
// Add level prefix emoji for info/warn/error
|
|
451
|
-
const levelEmoji = level === 'info'
|
|
461
|
+
const levelEmoji = level === 'info'
|
|
462
|
+
? 'ℹ️'
|
|
463
|
+
: level === 'warn'
|
|
464
|
+
? '⚠️'
|
|
465
|
+
: level === 'error'
|
|
466
|
+
? '⛔'
|
|
467
|
+
: '';
|
|
452
468
|
if (levelEmoji) {
|
|
453
469
|
if (typeof logArgs[0] === 'string') {
|
|
454
470
|
logArgs[0] = `${levelEmoji} ${logArgs[0]}`;
|
|
@@ -478,7 +494,9 @@ function ggLog(options, ...args) {
|
|
|
478
494
|
namespace,
|
|
479
495
|
color: ggLogFunction.color,
|
|
480
496
|
diff,
|
|
481
|
-
message: logArgs.length === 1
|
|
497
|
+
message: logArgs.length === 1
|
|
498
|
+
? formatValue(logArgs[0])
|
|
499
|
+
: logArgs.map(formatValue).join(' '),
|
|
482
500
|
args: logArgs,
|
|
483
501
|
timestamp: Date.now(),
|
|
484
502
|
file,
|
|
@@ -487,7 +505,7 @@ function ggLog(options, ...args) {
|
|
|
487
505
|
src,
|
|
488
506
|
level,
|
|
489
507
|
stack,
|
|
490
|
-
tableData
|
|
508
|
+
tableData,
|
|
491
509
|
};
|
|
492
510
|
// Always buffer — earlyLogBuffer is a persistent replay log so late-registering
|
|
493
511
|
// listeners (e.g. Eruda mounting after the file-sink listener) still receive entries
|
|
@@ -525,10 +543,14 @@ gg._here = (options) => {
|
|
|
525
543
|
const { ns: nsLabel, file, line, col } = options;
|
|
526
544
|
const namespace = nsLabel.startsWith('gg:') ? nsLabel : `gg:${nsLabel}`;
|
|
527
545
|
const ggLogFunction = namespaceToLogFunction.get(namespace) ||
|
|
528
|
-
namespaceToLogFunction
|
|
546
|
+
namespaceToLogFunction
|
|
547
|
+
.set(namespace, createGgDebugger(namespace))
|
|
548
|
+
.get(namespace);
|
|
529
549
|
ggLogFunction(` 📝 ${namespace}`);
|
|
530
550
|
const fileName = file ? file.replace(srcRootRegex, '') : nsLabel;
|
|
531
|
-
const functionName = nsLabel.includes('@')
|
|
551
|
+
const functionName = nsLabel.includes('@')
|
|
552
|
+
? nsLabel.split('@').pop() || ''
|
|
553
|
+
: '';
|
|
532
554
|
const url = file ? openInEditorUrl(file, line, col) : '';
|
|
533
555
|
return { fileName, functionName, url };
|
|
534
556
|
};
|
|
@@ -543,7 +565,9 @@ gg._here = (options) => {
|
|
|
543
565
|
*/
|
|
544
566
|
gg._o = (ns, file, line, col, src) => ({ ns, file, line, col, src });
|
|
545
567
|
gg.disable = isCloudflareWorker() ? () => '' : () => debugFactory.disable();
|
|
546
|
-
gg.enable = isCloudflareWorker()
|
|
568
|
+
gg.enable = isCloudflareWorker()
|
|
569
|
+
? () => { }
|
|
570
|
+
: (ns) => debugFactory.enable(ns);
|
|
547
571
|
/**
|
|
548
572
|
* Clear the persisted gg-enabled state from localStorage.
|
|
549
573
|
* Useful to reset production trigger after testing with ?gg parameter.
|
|
@@ -559,6 +583,22 @@ gg.clearPersist = () => {
|
|
|
559
583
|
}
|
|
560
584
|
}
|
|
561
585
|
};
|
|
586
|
+
/**
|
|
587
|
+
* Enable gg logging at runtime (e.g., after 5-tap gesture in production).
|
|
588
|
+
* Flips the internal enabled flag so subsequent gg() calls start capturing.
|
|
589
|
+
* This is session-only — does not persist to localStorage, so the next
|
|
590
|
+
* page load / app launch starts with gg disabled again.
|
|
591
|
+
*
|
|
592
|
+
* To enable gg across app restarts (e.g., in a prod Tauri app), run this
|
|
593
|
+
* in the Eruda console instead:
|
|
594
|
+
* localStorage.setItem("gg-enabled", "true")
|
|
595
|
+
* Clear with: localStorage.removeItem("gg-enabled") or gg.clearPersist()
|
|
596
|
+
*
|
|
597
|
+
* @internal Used by the Eruda gesture handler — not part of the public API.
|
|
598
|
+
*/
|
|
599
|
+
export function enableGgRuntime() {
|
|
600
|
+
ggConfig.enabled = true;
|
|
601
|
+
}
|
|
562
602
|
// ── Console-like methods ───────────────────────────────────────────────
|
|
563
603
|
// Each public method (gg.warn, gg.error, etc.) has a corresponding internal
|
|
564
604
|
// method (gg._warn, gg._error, etc.) that accepts call-site metadata from
|
|
@@ -759,7 +799,9 @@ function formatTable(data, columns) {
|
|
|
759
799
|
allKeys = Array.from(keySet);
|
|
760
800
|
rows = data.map((item, i) => ({
|
|
761
801
|
'(index)': i,
|
|
762
|
-
...(item && typeof item === 'object'
|
|
802
|
+
...(item && typeof item === 'object'
|
|
803
|
+
? item
|
|
804
|
+
: { Value: item }),
|
|
763
805
|
}));
|
|
764
806
|
}
|
|
765
807
|
}
|
|
@@ -783,7 +825,7 @@ function formatTable(data, columns) {
|
|
|
783
825
|
'(index)': key,
|
|
784
826
|
...(val && typeof val === 'object' && !Array.isArray(val)
|
|
785
827
|
? val
|
|
786
|
-
: { Value: val })
|
|
828
|
+
: { Value: val }),
|
|
787
829
|
}));
|
|
788
830
|
}
|
|
789
831
|
// Apply column filter
|
|
@@ -821,7 +863,7 @@ function parseColor(color) {
|
|
|
821
863
|
grey: '#808080',
|
|
822
864
|
orange: '#ffa500',
|
|
823
865
|
purple: '#800080',
|
|
824
|
-
pink: '#ffc0cb'
|
|
866
|
+
pink: '#ffc0cb',
|
|
825
867
|
};
|
|
826
868
|
// Check named colors first
|
|
827
869
|
const normalized = color.toLowerCase().trim();
|
|
@@ -834,7 +876,7 @@ function parseColor(color) {
|
|
|
834
876
|
return {
|
|
835
877
|
r: parseInt(hexMatch[1], 16),
|
|
836
878
|
g: parseInt(hexMatch[2], 16),
|
|
837
|
-
b: parseInt(hexMatch[3], 16)
|
|
879
|
+
b: parseInt(hexMatch[3], 16),
|
|
838
880
|
};
|
|
839
881
|
}
|
|
840
882
|
// Parse short hex (#rgb)
|
|
@@ -843,7 +885,7 @@ function parseColor(color) {
|
|
|
843
885
|
return {
|
|
844
886
|
r: parseInt(shortHexMatch[1] + shortHexMatch[1], 16),
|
|
845
887
|
g: parseInt(shortHexMatch[2] + shortHexMatch[2], 16),
|
|
846
|
-
b: parseInt(shortHexMatch[3] + shortHexMatch[3], 16)
|
|
888
|
+
b: parseInt(shortHexMatch[3] + shortHexMatch[3], 16),
|
|
847
889
|
};
|
|
848
890
|
}
|
|
849
891
|
// Parse rgb(r,g,b) or rgba(r,g,b,a)
|
|
@@ -852,7 +894,7 @@ function parseColor(color) {
|
|
|
852
894
|
return {
|
|
853
895
|
r: parseInt(rgbMatch[1]),
|
|
854
896
|
g: parseInt(rgbMatch[2]),
|
|
855
|
-
b: parseInt(rgbMatch[3])
|
|
897
|
+
b: parseInt(rgbMatch[3]),
|
|
856
898
|
};
|
|
857
899
|
}
|
|
858
900
|
return null;
|
|
@@ -864,7 +906,7 @@ const STYLE_CODES = {
|
|
|
864
906
|
bold: '\x1b[1m',
|
|
865
907
|
dim: '\x1b[2m',
|
|
866
908
|
italic: '\x1b[3m',
|
|
867
|
-
underline: '\x1b[4m'
|
|
909
|
+
underline: '\x1b[4m',
|
|
868
910
|
};
|
|
869
911
|
/**
|
|
870
912
|
* Internal helper to create chainable color function with method chaining
|
|
@@ -1006,14 +1048,14 @@ Object.defineProperty(gg, 'addLogListener', {
|
|
|
1006
1048
|
}
|
|
1007
1049
|
},
|
|
1008
1050
|
writable: false,
|
|
1009
|
-
configurable: true
|
|
1051
|
+
configurable: true,
|
|
1010
1052
|
});
|
|
1011
1053
|
Object.defineProperty(gg, 'removeLogListener', {
|
|
1012
1054
|
value(callback) {
|
|
1013
1055
|
_logListeners.delete(callback);
|
|
1014
1056
|
},
|
|
1015
1057
|
writable: false,
|
|
1016
|
-
configurable: true
|
|
1058
|
+
configurable: true,
|
|
1017
1059
|
});
|
|
1018
1060
|
// Legacy gg._onLog — backward-compatible single-slot alias
|
|
1019
1061
|
Object.defineProperty(gg, '_onLog', {
|
|
@@ -1033,7 +1075,7 @@ Object.defineProperty(gg, '_onLog', {
|
|
|
1033
1075
|
earlyLogBuffer.forEach((entry) => callback(entry));
|
|
1034
1076
|
}
|
|
1035
1077
|
}
|
|
1036
|
-
}
|
|
1078
|
+
},
|
|
1037
1079
|
});
|
|
1038
1080
|
// Namespace for adding properties to the gg function
|
|
1039
1081
|
// eslint-disable-next-line @typescript-eslint/no-namespace
|
|
@@ -1057,9 +1099,11 @@ export async function runGgDiagnostics() {
|
|
|
1057
1099
|
let ggMessage = '\n';
|
|
1058
1100
|
const message = (s) => (ggMessage += `${s}\n`);
|
|
1059
1101
|
const checkbox = (test) => (test ? '✅' : '❌');
|
|
1060
|
-
const makeHint = (test, ifTrue, ifFalse = '') =>
|
|
1102
|
+
const makeHint = (test, ifTrue, ifFalse = '') => test ? ifTrue : ifFalse;
|
|
1061
1103
|
console.log(`Loaded gg module. Checking configuration...`);
|
|
1062
|
-
const configOk = BROWSER
|
|
1104
|
+
const configOk = BROWSER
|
|
1105
|
+
? ggConfig.enabled
|
|
1106
|
+
: ggConfig.enabled && ggLogTest.enabled;
|
|
1063
1107
|
if (configOk) {
|
|
1064
1108
|
message(`No problems detected:`);
|
|
1065
1109
|
if (BROWSER) {
|
|
@@ -1104,3 +1148,41 @@ export async function runGgDiagnostics() {
|
|
|
1104
1148
|
if (ggConfig.showHints && !isCloudflareWorker() && !BROWSER) {
|
|
1105
1149
|
runGgDiagnostics();
|
|
1106
1150
|
}
|
|
1151
|
+
// ── File sink: browser → dev server relay ──────────────────────────────
|
|
1152
|
+
// Dynamically import the file-sink sender module served by ggFileSinkPlugin.
|
|
1153
|
+
// The sender registers a log listener that relays entries to the Vite dev
|
|
1154
|
+
// server via import.meta.hot.send() over the HMR WebSocket.
|
|
1155
|
+
//
|
|
1156
|
+
// Why dynamic import instead of inline code?
|
|
1157
|
+
// The gg module is pre-bundled by Vite's dep optimizer (esbuild), which
|
|
1158
|
+
// evaluates `typeof import.meta.hot` as "undefined" and tree-shakes any
|
|
1159
|
+
// code guarded by it. Dynamic import() is preserved by esbuild, and the
|
|
1160
|
+
// imported module (served at /__gg/sender.mjs by the plugin's middleware)
|
|
1161
|
+
// goes through Vite's normal transform pipeline where import.meta.hot IS
|
|
1162
|
+
// available.
|
|
1163
|
+
//
|
|
1164
|
+
// The .catch() silently handles the case where ggFileSinkPlugin isn't active
|
|
1165
|
+
// (e.g., production builds, or projects that don't use the file sink).
|
|
1166
|
+
if (BROWSER && DEV) {
|
|
1167
|
+
// Import the virtual module directly through Vite's module URL.
|
|
1168
|
+
// Dynamic string construction defeats Rollup's static import analysis —
|
|
1169
|
+
// without this, `vite build` fails because Rollup tries to resolve the
|
|
1170
|
+
// dev-server-only URL. The runtime guard (BROWSER && DEV) prevents
|
|
1171
|
+
// execution in production, but Rollup analyzes statically.
|
|
1172
|
+
const senderUrl = '/@id/' + 'virtual:gg-file-sink-sender';
|
|
1173
|
+
import(/* @vite-ignore */ senderUrl).catch(() => { });
|
|
1174
|
+
}
|
|
1175
|
+
// ── File sink: server-side self-registration via globalThis bridge ───────
|
|
1176
|
+
// When ggFileSinkPlugin is active, configureServer sets globalThis.__ggFileSink
|
|
1177
|
+
// with a write() function that serializes and appends entries to the JSONL file.
|
|
1178
|
+
// Any gg module instance (SSR, pre-bundled, monorepo-hoisted) that sees the
|
|
1179
|
+
// bridge registers a listener — no transform hook or path matching needed.
|
|
1180
|
+
if (!BROWSER &&
|
|
1181
|
+
typeof globalThis !== 'undefined' &&
|
|
1182
|
+
globalThis.__ggFileSink &&
|
|
1183
|
+
typeof globalThis.__ggFileSink?.write === 'function') {
|
|
1184
|
+
const sink = globalThis.__ggFileSink;
|
|
1185
|
+
gg.addLogListener((entry) => {
|
|
1186
|
+
sink.write(entry, 'server');
|
|
1187
|
+
});
|
|
1188
|
+
}
|