trickle-observe 0.2.73 → 0.2.74
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/next-loader.d.ts +2 -0
- package/dist/next-loader.js +6 -3
- package/dist/next-plugin.d.ts +3 -0
- package/dist/next-plugin.js +72 -2
- package/dist/vite-plugin.d.ts +3 -1
- package/dist/vite-plugin.js +17 -3
- package/dist-esm/vite-plugin.js +17 -3
- package/package.json +1 -1
- package/src/next-loader.ts +8 -3
- package/src/next-plugin.ts +79 -2
- package/src/vite-plugin.ts +31 -2
package/dist/next-loader.d.ts
CHANGED
package/dist/next-loader.js
CHANGED
|
@@ -35,12 +35,15 @@ function trickleNextLoader(source) {
|
|
|
35
35
|
const backendUrl = options.backendUrl ?? process.env.TRICKLE_BACKEND_URL ?? 'http://localhost:4888';
|
|
36
36
|
const debug = options.debug ?? (process.env.TRICKLE_DEBUG === '1');
|
|
37
37
|
const traceVars = options.traceVars ?? true;
|
|
38
|
+
const isServer = options.isServer ?? true;
|
|
39
|
+
const ingestPort = options.ingestPort ?? 4889;
|
|
38
40
|
const moduleName = path_1.default.basename(resourcePath).replace(/\.[jt]sx?$/, '');
|
|
39
41
|
try {
|
|
40
|
-
//
|
|
41
|
-
const
|
|
42
|
+
// Server: use node:fs for writing. Client: use fetch() to ingest server.
|
|
43
|
+
const ingestUrl = isServer ? null : `http://localhost:${ingestPort}/__trickle_vars`;
|
|
44
|
+
const transformed = (0, vite_plugin_1.transformEsmSource)(source, resourcePath, moduleName, backendUrl, debug, traceVars, source, isServer, ingestUrl);
|
|
42
45
|
if (debug && transformed !== source) {
|
|
43
|
-
console.log(`[trickle/next] Instrumented ${resourcePath}`);
|
|
46
|
+
console.log(`[trickle/next] Instrumented ${resourcePath} [${isServer ? 'server' : 'client'}]`);
|
|
44
47
|
}
|
|
45
48
|
return transformed;
|
|
46
49
|
}
|
package/dist/next-plugin.d.ts
CHANGED
|
@@ -38,6 +38,8 @@ export interface TrickleNextOptions {
|
|
|
38
38
|
debug?: boolean;
|
|
39
39
|
/** Enable variable tracing (default: true) */
|
|
40
40
|
traceVars?: boolean;
|
|
41
|
+
/** Port for the client-side ingest server (default: 4889) */
|
|
42
|
+
ingestPort?: number;
|
|
41
43
|
}
|
|
42
44
|
type NextConfig = Record<string, unknown> & {
|
|
43
45
|
webpack?: (config: WebpackConfig, context: WebpackContext) => WebpackConfig;
|
|
@@ -58,6 +60,7 @@ interface WebpackContext {
|
|
|
58
60
|
*
|
|
59
61
|
* Adds a webpack loader that instruments all .tsx/.jsx component files at build
|
|
60
62
|
* time with render tracking, useState change tracking, and hook observability.
|
|
63
|
+
* Also starts a tiny HTTP ingest server for client-side variable data.
|
|
61
64
|
*/
|
|
62
65
|
export declare function withTrickle(nextConfig?: NextConfig, options?: TrickleNextOptions): NextConfig;
|
|
63
66
|
export {};
|
package/dist/next-plugin.js
CHANGED
|
@@ -34,14 +34,78 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
34
34
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
35
35
|
exports.withTrickle = withTrickle;
|
|
36
36
|
const path_1 = __importDefault(require("path"));
|
|
37
|
+
const http_1 = __importDefault(require("http"));
|
|
38
|
+
const fs_1 = __importDefault(require("fs"));
|
|
39
|
+
/** Track whether the ingest server is already running (avoid starting twice) */
|
|
40
|
+
let ingestServerStarted = false;
|
|
41
|
+
/**
|
|
42
|
+
* Start a tiny HTTP server that receives variable data from browser clients
|
|
43
|
+
* and writes it to .trickle/variables.jsonl.
|
|
44
|
+
*/
|
|
45
|
+
function startIngestServer(port, debug) {
|
|
46
|
+
if (ingestServerStarted)
|
|
47
|
+
return;
|
|
48
|
+
ingestServerStarted = true;
|
|
49
|
+
const dir = process.env.TRICKLE_LOCAL_DIR || path_1.default.join(process.cwd(), '.trickle');
|
|
50
|
+
try {
|
|
51
|
+
fs_1.default.mkdirSync(dir, { recursive: true });
|
|
52
|
+
}
|
|
53
|
+
catch { }
|
|
54
|
+
const varsFile = path_1.default.join(dir, 'variables.jsonl');
|
|
55
|
+
const server = http_1.default.createServer((req, res) => {
|
|
56
|
+
// CORS headers for browser fetch
|
|
57
|
+
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
58
|
+
res.setHeader('Access-Control-Allow-Methods', 'POST, OPTIONS');
|
|
59
|
+
res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
|
|
60
|
+
if (req.method === 'OPTIONS') {
|
|
61
|
+
res.writeHead(204);
|
|
62
|
+
res.end();
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
if (req.method === 'POST' && req.url === '/__trickle_vars') {
|
|
66
|
+
let body = '';
|
|
67
|
+
req.on('data', (chunk) => { body += chunk; });
|
|
68
|
+
req.on('end', () => {
|
|
69
|
+
try {
|
|
70
|
+
if (body) {
|
|
71
|
+
fs_1.default.appendFileSync(varsFile, body);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
catch { }
|
|
75
|
+
res.writeHead(200);
|
|
76
|
+
res.end('ok');
|
|
77
|
+
});
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
res.writeHead(404);
|
|
81
|
+
res.end();
|
|
82
|
+
});
|
|
83
|
+
server.listen(port, () => {
|
|
84
|
+
if (debug) {
|
|
85
|
+
console.log(`[trickle/next] Ingest server listening on http://localhost:${port} → ${varsFile}`);
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
server.on('error', (err) => {
|
|
89
|
+
if (err.code === 'EADDRINUSE') {
|
|
90
|
+
// Another trickle instance already has the port — that's fine
|
|
91
|
+
if (debug)
|
|
92
|
+
console.log(`[trickle/next] Ingest port ${port} already in use (OK)`);
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
// Don't keep the process alive just for this server
|
|
96
|
+
server.unref();
|
|
97
|
+
}
|
|
37
98
|
/**
|
|
38
99
|
* Wrap your Next.js config with trickle observability.
|
|
39
100
|
*
|
|
40
101
|
* Adds a webpack loader that instruments all .tsx/.jsx component files at build
|
|
41
102
|
* time with render tracking, useState change tracking, and hook observability.
|
|
103
|
+
* Also starts a tiny HTTP ingest server for client-side variable data.
|
|
42
104
|
*/
|
|
43
105
|
function withTrickle(nextConfig = {}, options = {}) {
|
|
44
106
|
const loaderPath = path_1.default.resolve(__dirname, './next-loader.js');
|
|
107
|
+
const ingestPort = options.ingestPort ?? 4889;
|
|
108
|
+
const debug = options.debug ?? (process.env.TRICKLE_DEBUG === '1');
|
|
45
109
|
return {
|
|
46
110
|
...nextConfig,
|
|
47
111
|
webpack(config, context) {
|
|
@@ -49,13 +113,19 @@ function withTrickle(nextConfig = {}, options = {}) {
|
|
|
49
113
|
if (typeof nextConfig.webpack === 'function') {
|
|
50
114
|
config = nextConfig.webpack(config, context);
|
|
51
115
|
}
|
|
116
|
+
// Start the ingest server for client-side data (once)
|
|
117
|
+
startIngestServer(ingestPort, debug);
|
|
52
118
|
config.module.rules.push({
|
|
53
119
|
test: /\.(tsx?|jsx?)$/,
|
|
54
|
-
exclude: /node_modules/,
|
|
120
|
+
exclude: /node_modules|trickle-observe|client-js/,
|
|
55
121
|
use: [
|
|
56
122
|
{
|
|
57
123
|
loader: loaderPath,
|
|
58
|
-
options
|
|
124
|
+
options: {
|
|
125
|
+
...options,
|
|
126
|
+
isServer: context.isServer,
|
|
127
|
+
ingestPort,
|
|
128
|
+
},
|
|
59
129
|
},
|
|
60
130
|
],
|
|
61
131
|
});
|
package/dist/vite-plugin.d.ts
CHANGED
|
@@ -40,5 +40,7 @@ export declare function tricklePlugin(options?: TricklePluginOptions): {
|
|
|
40
40
|
map: null;
|
|
41
41
|
} | null;
|
|
42
42
|
};
|
|
43
|
-
export declare function transformEsmSource(source: string, filename: string, moduleName: string, backendUrl: string, debug: boolean, traceVars: boolean, originalSource?: string | null, isSSR?: boolean
|
|
43
|
+
export declare function transformEsmSource(source: string, filename: string, moduleName: string, backendUrl: string, debug: boolean, traceVars: boolean, originalSource?: string | null, isSSR?: boolean,
|
|
44
|
+
/** URL for fetch-based browser transport (Next.js client). When set and isSSR=false, uses fetch() instead of import.meta.hot */
|
|
45
|
+
ingestUrl?: string | null): string;
|
|
44
46
|
export default tricklePlugin;
|
package/dist/vite-plugin.js
CHANGED
|
@@ -894,7 +894,9 @@ function findOriginalLineDestructured(origLines, varNames, transformedLine) {
|
|
|
894
894
|
function escapeRegexStr(str) {
|
|
895
895
|
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
896
896
|
}
|
|
897
|
-
function transformEsmSource(source, filename, moduleName, backendUrl, debug, traceVars, originalSource, isSSR
|
|
897
|
+
function transformEsmSource(source, filename, moduleName, backendUrl, debug, traceVars, originalSource, isSSR,
|
|
898
|
+
/** URL for fetch-based browser transport (Next.js client). When set and isSSR=false, uses fetch() instead of import.meta.hot */
|
|
899
|
+
ingestUrl) {
|
|
898
900
|
// Detect React files for component render tracking
|
|
899
901
|
const isReactFile = /\.(tsx|jsx)$/.test(filename);
|
|
900
902
|
// Match top-level and nested function declarations (including async, export, export default)
|
|
@@ -1339,9 +1341,13 @@ function transformEsmSource(source, filename, moduleName, backendUrl, debug, tra
|
|
|
1339
1341
|
// SSR/Node.js mode — write directly to file system
|
|
1340
1342
|
prefixLines.push(`let __trickle_varsFile = null;`, `function __trickle_send(line) {`, ` try {`, ` if (!__trickle_varsFile) {`, ` const dir = process.env.TRICKLE_LOCAL_DIR || __trickle_join(process.cwd(), '.trickle');`, ` try { __trickle_mkdirSync(dir, { recursive: true }); } catch(e) {}`, ` __trickle_varsFile = __trickle_join(dir, 'variables.jsonl');`, ` }`, ` __trickle_appendFileSync(__trickle_varsFile, line + '\\n');`, ` } catch(e) {}`, `}`);
|
|
1341
1343
|
}
|
|
1344
|
+
else if (ingestUrl) {
|
|
1345
|
+
// Browser mode with fetch transport (Next.js client)
|
|
1346
|
+
prefixLines.push(`const __trickle_sendBuf = [];`, `let __trickle_sendTimer = null;`, `function __trickle_flush() {`, ` if (__trickle_sendBuf.length === 0) return;`, ` const lines = __trickle_sendBuf.join('\\n') + '\\n';`, ` __trickle_sendBuf.length = 0;`, ` try { fetch(${JSON.stringify(ingestUrl)}, { method: 'POST', body: lines, headers: { 'Content-Type': 'text/plain' } }).catch(function(){}); } catch(e) {}`, `}`, `function __trickle_send(line) {`, ` __trickle_sendBuf.push(line);`, ` if (!__trickle_sendTimer) {`, ` __trickle_sendTimer = setTimeout(function() { __trickle_sendTimer = null; __trickle_flush(); }, 300);`, ` }`, `}`);
|
|
1347
|
+
}
|
|
1342
1348
|
else {
|
|
1343
1349
|
// Browser mode — buffer and send via Vite HMR WebSocket
|
|
1344
|
-
prefixLines.push(`const __trickle_sendBuf = [];`, `let __trickle_sendTimer = null;`, `function __trickle_flush() {`, ` if (__trickle_sendBuf.length === 0) return;`, ` const lines = __trickle_sendBuf.join('\\n') + '\\n';`, ` __trickle_sendBuf.length = 0;`, ` try { if (import.meta.hot) import.meta.hot.send('trickle:vars', { lines }); } catch(e) {}`, `}`, `function __trickle_send(line) {`, ` __trickle_sendBuf.push(line);`, ` if (!__trickle_sendTimer) {`, ` __trickle_sendTimer = setTimeout(()
|
|
1350
|
+
prefixLines.push(`const __trickle_sendBuf = [];`, `let __trickle_sendTimer = null;`, `function __trickle_flush() {`, ` if (__trickle_sendBuf.length === 0) return;`, ` const lines = __trickle_sendBuf.join('\\n') + '\\n';`, ` __trickle_sendBuf.length = 0;`, ` try { if (import.meta.hot) import.meta.hot.send('trickle:vars', { lines }); } catch(e) {}`, `}`, `function __trickle_send(line) {`, ` __trickle_sendBuf.push(line);`, ` if (!__trickle_sendTimer) {`, ` __trickle_sendTimer = setTimeout(function() { __trickle_sendTimer = null; __trickle_flush(); }, 300);`, ` }`, `}`);
|
|
1345
1351
|
}
|
|
1346
1352
|
}
|
|
1347
1353
|
// Add variable tracing if needed — inlined to avoid import resolution issues in Vite SSR.
|
|
@@ -1453,6 +1459,14 @@ function transformEsmSource(source, filename, moduleName, backendUrl, debug, tra
|
|
|
1453
1459
|
for (const { position, code } of allInsertions) {
|
|
1454
1460
|
result = result.slice(0, position) + code + result.slice(position);
|
|
1455
1461
|
}
|
|
1456
|
-
|
|
1462
|
+
// Preserve 'use client' / 'use server' directives — they must be the first expression
|
|
1463
|
+
// in the file (before any imports or code). Extract them from result and prepend before prefix.
|
|
1464
|
+
let directive = '';
|
|
1465
|
+
const directiveMatch = result.match(/^(\s*(?:'use client'|"use client"|'use server'|"use server")\s*;?\s*\n?)/);
|
|
1466
|
+
if (directiveMatch) {
|
|
1467
|
+
directive = directiveMatch[1];
|
|
1468
|
+
result = result.slice(directiveMatch[0].length);
|
|
1469
|
+
}
|
|
1470
|
+
return directive + prefix + result;
|
|
1457
1471
|
}
|
|
1458
1472
|
exports.default = tricklePlugin;
|
package/dist-esm/vite-plugin.js
CHANGED
|
@@ -887,7 +887,9 @@ function findOriginalLineDestructured(origLines, varNames, transformedLine) {
|
|
|
887
887
|
function escapeRegexStr(str) {
|
|
888
888
|
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
889
889
|
}
|
|
890
|
-
export function transformEsmSource(source, filename, moduleName, backendUrl, debug, traceVars, originalSource, isSSR
|
|
890
|
+
export function transformEsmSource(source, filename, moduleName, backendUrl, debug, traceVars, originalSource, isSSR,
|
|
891
|
+
/** URL for fetch-based browser transport (Next.js client). When set and isSSR=false, uses fetch() instead of import.meta.hot */
|
|
892
|
+
ingestUrl) {
|
|
891
893
|
// Detect React files for component render tracking
|
|
892
894
|
const isReactFile = /\.(tsx|jsx)$/.test(filename);
|
|
893
895
|
// Match top-level and nested function declarations (including async, export, export default)
|
|
@@ -1332,9 +1334,13 @@ export function transformEsmSource(source, filename, moduleName, backendUrl, deb
|
|
|
1332
1334
|
// SSR/Node.js mode — write directly to file system
|
|
1333
1335
|
prefixLines.push(`let __trickle_varsFile = null;`, `function __trickle_send(line) {`, ` try {`, ` if (!__trickle_varsFile) {`, ` const dir = process.env.TRICKLE_LOCAL_DIR || __trickle_join(process.cwd(), '.trickle');`, ` try { __trickle_mkdirSync(dir, { recursive: true }); } catch(e) {}`, ` __trickle_varsFile = __trickle_join(dir, 'variables.jsonl');`, ` }`, ` __trickle_appendFileSync(__trickle_varsFile, line + '\\n');`, ` } catch(e) {}`, `}`);
|
|
1334
1336
|
}
|
|
1337
|
+
else if (ingestUrl) {
|
|
1338
|
+
// Browser mode with fetch transport (Next.js client)
|
|
1339
|
+
prefixLines.push(`const __trickle_sendBuf = [];`, `let __trickle_sendTimer = null;`, `function __trickle_flush() {`, ` if (__trickle_sendBuf.length === 0) return;`, ` const lines = __trickle_sendBuf.join('\\n') + '\\n';`, ` __trickle_sendBuf.length = 0;`, ` try { fetch(${JSON.stringify(ingestUrl)}, { method: 'POST', body: lines, headers: { 'Content-Type': 'text/plain' } }).catch(function(){}); } catch(e) {}`, `}`, `function __trickle_send(line) {`, ` __trickle_sendBuf.push(line);`, ` if (!__trickle_sendTimer) {`, ` __trickle_sendTimer = setTimeout(function() { __trickle_sendTimer = null; __trickle_flush(); }, 300);`, ` }`, `}`);
|
|
1340
|
+
}
|
|
1335
1341
|
else {
|
|
1336
1342
|
// Browser mode — buffer and send via Vite HMR WebSocket
|
|
1337
|
-
prefixLines.push(`const __trickle_sendBuf = [];`, `let __trickle_sendTimer = null;`, `function __trickle_flush() {`, ` if (__trickle_sendBuf.length === 0) return;`, ` const lines = __trickle_sendBuf.join('\\n') + '\\n';`, ` __trickle_sendBuf.length = 0;`, ` try { if (import.meta.hot) import.meta.hot.send('trickle:vars', { lines }); } catch(e) {}`, `}`, `function __trickle_send(line) {`, ` __trickle_sendBuf.push(line);`, ` if (!__trickle_sendTimer) {`, ` __trickle_sendTimer = setTimeout(()
|
|
1343
|
+
prefixLines.push(`const __trickle_sendBuf = [];`, `let __trickle_sendTimer = null;`, `function __trickle_flush() {`, ` if (__trickle_sendBuf.length === 0) return;`, ` const lines = __trickle_sendBuf.join('\\n') + '\\n';`, ` __trickle_sendBuf.length = 0;`, ` try { if (import.meta.hot) import.meta.hot.send('trickle:vars', { lines }); } catch(e) {}`, `}`, `function __trickle_send(line) {`, ` __trickle_sendBuf.push(line);`, ` if (!__trickle_sendTimer) {`, ` __trickle_sendTimer = setTimeout(function() { __trickle_sendTimer = null; __trickle_flush(); }, 300);`, ` }`, `}`);
|
|
1338
1344
|
}
|
|
1339
1345
|
}
|
|
1340
1346
|
// Add variable tracing if needed — inlined to avoid import resolution issues in Vite SSR.
|
|
@@ -1446,6 +1452,14 @@ export function transformEsmSource(source, filename, moduleName, backendUrl, deb
|
|
|
1446
1452
|
for (const { position, code } of allInsertions) {
|
|
1447
1453
|
result = result.slice(0, position) + code + result.slice(position);
|
|
1448
1454
|
}
|
|
1449
|
-
|
|
1455
|
+
// Preserve 'use client' / 'use server' directives — they must be the first expression
|
|
1456
|
+
// in the file (before any imports or code). Extract them from result and prepend before prefix.
|
|
1457
|
+
let directive = '';
|
|
1458
|
+
const directiveMatch = result.match(/^(\s*(?:'use client'|"use client"|'use server'|"use server")\s*;?\s*\n?)/);
|
|
1459
|
+
if (directiveMatch) {
|
|
1460
|
+
directive = directiveMatch[1];
|
|
1461
|
+
result = result.slice(directiveMatch[0].length);
|
|
1462
|
+
}
|
|
1463
|
+
return directive + prefix + result;
|
|
1450
1464
|
}
|
|
1451
1465
|
export default tricklePlugin;
|
package/package.json
CHANGED
package/src/next-loader.ts
CHANGED
|
@@ -17,6 +17,8 @@ interface LoaderOptions {
|
|
|
17
17
|
exclude?: string[];
|
|
18
18
|
debug?: boolean;
|
|
19
19
|
traceVars?: boolean;
|
|
20
|
+
isServer?: boolean;
|
|
21
|
+
ingestPort?: number;
|
|
20
22
|
}
|
|
21
23
|
|
|
22
24
|
// webpack loader — `this` is the LoaderContext (must not be an arrow function)
|
|
@@ -40,13 +42,16 @@ export default function trickleNextLoader(this: { resourcePath: string; getOptio
|
|
|
40
42
|
const backendUrl = options.backendUrl ?? process.env.TRICKLE_BACKEND_URL ?? 'http://localhost:4888';
|
|
41
43
|
const debug = options.debug ?? (process.env.TRICKLE_DEBUG === '1');
|
|
42
44
|
const traceVars = options.traceVars ?? true;
|
|
45
|
+
const isServer = options.isServer ?? true;
|
|
46
|
+
const ingestPort = options.ingestPort ?? 4889;
|
|
43
47
|
const moduleName = path.basename(resourcePath).replace(/\.[jt]sx?$/, '');
|
|
44
48
|
|
|
45
49
|
try {
|
|
46
|
-
//
|
|
47
|
-
const
|
|
50
|
+
// Server: use node:fs for writing. Client: use fetch() to ingest server.
|
|
51
|
+
const ingestUrl = isServer ? null : `http://localhost:${ingestPort}/__trickle_vars`;
|
|
52
|
+
const transformed = transformEsmSource(source, resourcePath, moduleName, backendUrl, debug, traceVars, source, isServer, ingestUrl);
|
|
48
53
|
if (debug && transformed !== source) {
|
|
49
|
-
console.log(`[trickle/next] Instrumented ${resourcePath}`);
|
|
54
|
+
console.log(`[trickle/next] Instrumented ${resourcePath} [${isServer ? 'server' : 'client'}]`);
|
|
50
55
|
}
|
|
51
56
|
return transformed;
|
|
52
57
|
} catch {
|
package/src/next-plugin.ts
CHANGED
|
@@ -29,6 +29,8 @@
|
|
|
29
29
|
*/
|
|
30
30
|
|
|
31
31
|
import path from 'path';
|
|
32
|
+
import http from 'http';
|
|
33
|
+
import fs from 'fs';
|
|
32
34
|
|
|
33
35
|
export interface TrickleNextOptions {
|
|
34
36
|
/** Backend URL (default: http://localhost:4888 or TRICKLE_BACKEND_URL env var) */
|
|
@@ -41,6 +43,8 @@ export interface TrickleNextOptions {
|
|
|
41
43
|
debug?: boolean;
|
|
42
44
|
/** Enable variable tracing (default: true) */
|
|
43
45
|
traceVars?: boolean;
|
|
46
|
+
/** Port for the client-side ingest server (default: 4889) */
|
|
47
|
+
ingestPort?: number;
|
|
44
48
|
}
|
|
45
49
|
|
|
46
50
|
type NextConfig = Record<string, unknown> & {
|
|
@@ -58,14 +62,80 @@ interface WebpackContext {
|
|
|
58
62
|
[key: string]: unknown;
|
|
59
63
|
}
|
|
60
64
|
|
|
65
|
+
/** Track whether the ingest server is already running (avoid starting twice) */
|
|
66
|
+
let ingestServerStarted = false;
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Start a tiny HTTP server that receives variable data from browser clients
|
|
70
|
+
* and writes it to .trickle/variables.jsonl.
|
|
71
|
+
*/
|
|
72
|
+
function startIngestServer(port: number, debug: boolean): void {
|
|
73
|
+
if (ingestServerStarted) return;
|
|
74
|
+
ingestServerStarted = true;
|
|
75
|
+
|
|
76
|
+
const dir = process.env.TRICKLE_LOCAL_DIR || path.join(process.cwd(), '.trickle');
|
|
77
|
+
try { fs.mkdirSync(dir, { recursive: true }); } catch {}
|
|
78
|
+
const varsFile = path.join(dir, 'variables.jsonl');
|
|
79
|
+
|
|
80
|
+
const server = http.createServer((req, res) => {
|
|
81
|
+
// CORS headers for browser fetch
|
|
82
|
+
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
83
|
+
res.setHeader('Access-Control-Allow-Methods', 'POST, OPTIONS');
|
|
84
|
+
res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
|
|
85
|
+
|
|
86
|
+
if (req.method === 'OPTIONS') {
|
|
87
|
+
res.writeHead(204);
|
|
88
|
+
res.end();
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (req.method === 'POST' && req.url === '/__trickle_vars') {
|
|
93
|
+
let body = '';
|
|
94
|
+
req.on('data', (chunk: string) => { body += chunk; });
|
|
95
|
+
req.on('end', () => {
|
|
96
|
+
try {
|
|
97
|
+
if (body) {
|
|
98
|
+
fs.appendFileSync(varsFile, body);
|
|
99
|
+
}
|
|
100
|
+
} catch {}
|
|
101
|
+
res.writeHead(200);
|
|
102
|
+
res.end('ok');
|
|
103
|
+
});
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
res.writeHead(404);
|
|
108
|
+
res.end();
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
server.listen(port, () => {
|
|
112
|
+
if (debug) {
|
|
113
|
+
console.log(`[trickle/next] Ingest server listening on http://localhost:${port} → ${varsFile}`);
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
server.on('error', (err: NodeJS.ErrnoException) => {
|
|
118
|
+
if (err.code === 'EADDRINUSE') {
|
|
119
|
+
// Another trickle instance already has the port — that's fine
|
|
120
|
+
if (debug) console.log(`[trickle/next] Ingest port ${port} already in use (OK)`);
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
// Don't keep the process alive just for this server
|
|
125
|
+
server.unref();
|
|
126
|
+
}
|
|
127
|
+
|
|
61
128
|
/**
|
|
62
129
|
* Wrap your Next.js config with trickle observability.
|
|
63
130
|
*
|
|
64
131
|
* Adds a webpack loader that instruments all .tsx/.jsx component files at build
|
|
65
132
|
* time with render tracking, useState change tracking, and hook observability.
|
|
133
|
+
* Also starts a tiny HTTP ingest server for client-side variable data.
|
|
66
134
|
*/
|
|
67
135
|
export function withTrickle(nextConfig: NextConfig = {}, options: TrickleNextOptions = {}): NextConfig {
|
|
68
136
|
const loaderPath = path.resolve(__dirname, './next-loader.js');
|
|
137
|
+
const ingestPort = options.ingestPort ?? 4889;
|
|
138
|
+
const debug = options.debug ?? (process.env.TRICKLE_DEBUG === '1');
|
|
69
139
|
|
|
70
140
|
return {
|
|
71
141
|
...nextConfig,
|
|
@@ -75,13 +145,20 @@ export function withTrickle(nextConfig: NextConfig = {}, options: TrickleNextOpt
|
|
|
75
145
|
config = nextConfig.webpack(config, context);
|
|
76
146
|
}
|
|
77
147
|
|
|
148
|
+
// Start the ingest server for client-side data (once)
|
|
149
|
+
startIngestServer(ingestPort, debug);
|
|
150
|
+
|
|
78
151
|
config.module.rules.push({
|
|
79
152
|
test: /\.(tsx?|jsx?)$/,
|
|
80
|
-
exclude: /node_modules/,
|
|
153
|
+
exclude: /node_modules|trickle-observe|client-js/,
|
|
81
154
|
use: [
|
|
82
155
|
{
|
|
83
156
|
loader: loaderPath,
|
|
84
|
-
options
|
|
157
|
+
options: {
|
|
158
|
+
...options,
|
|
159
|
+
isServer: context.isServer,
|
|
160
|
+
ingestPort,
|
|
161
|
+
},
|
|
85
162
|
},
|
|
86
163
|
],
|
|
87
164
|
});
|
package/src/vite-plugin.ts
CHANGED
|
@@ -889,6 +889,8 @@ export function transformEsmSource(
|
|
|
889
889
|
traceVars: boolean,
|
|
890
890
|
originalSource?: string | null,
|
|
891
891
|
isSSR?: boolean,
|
|
892
|
+
/** URL for fetch-based browser transport (Next.js client). When set and isSSR=false, uses fetch() instead of import.meta.hot */
|
|
893
|
+
ingestUrl?: string | null,
|
|
892
894
|
): string {
|
|
893
895
|
// Detect React files for component render tracking
|
|
894
896
|
const isReactFile = /\.(tsx|jsx)$/.test(filename);
|
|
@@ -1370,6 +1372,24 @@ export function transformEsmSource(
|
|
|
1370
1372
|
` } catch(e) {}`,
|
|
1371
1373
|
`}`,
|
|
1372
1374
|
);
|
|
1375
|
+
} else if (ingestUrl) {
|
|
1376
|
+
// Browser mode with fetch transport (Next.js client)
|
|
1377
|
+
prefixLines.push(
|
|
1378
|
+
`const __trickle_sendBuf = [];`,
|
|
1379
|
+
`let __trickle_sendTimer = null;`,
|
|
1380
|
+
`function __trickle_flush() {`,
|
|
1381
|
+
` if (__trickle_sendBuf.length === 0) return;`,
|
|
1382
|
+
` const lines = __trickle_sendBuf.join('\\n') + '\\n';`,
|
|
1383
|
+
` __trickle_sendBuf.length = 0;`,
|
|
1384
|
+
` try { fetch(${JSON.stringify(ingestUrl)}, { method: 'POST', body: lines, headers: { 'Content-Type': 'text/plain' } }).catch(function(){}); } catch(e) {}`,
|
|
1385
|
+
`}`,
|
|
1386
|
+
`function __trickle_send(line) {`,
|
|
1387
|
+
` __trickle_sendBuf.push(line);`,
|
|
1388
|
+
` if (!__trickle_sendTimer) {`,
|
|
1389
|
+
` __trickle_sendTimer = setTimeout(function() { __trickle_sendTimer = null; __trickle_flush(); }, 300);`,
|
|
1390
|
+
` }`,
|
|
1391
|
+
`}`,
|
|
1392
|
+
);
|
|
1373
1393
|
} else {
|
|
1374
1394
|
// Browser mode — buffer and send via Vite HMR WebSocket
|
|
1375
1395
|
prefixLines.push(
|
|
@@ -1384,7 +1404,7 @@ export function transformEsmSource(
|
|
|
1384
1404
|
`function __trickle_send(line) {`,
|
|
1385
1405
|
` __trickle_sendBuf.push(line);`,
|
|
1386
1406
|
` if (!__trickle_sendTimer) {`,
|
|
1387
|
-
` __trickle_sendTimer = setTimeout(()
|
|
1407
|
+
` __trickle_sendTimer = setTimeout(function() { __trickle_sendTimer = null; __trickle_flush(); }, 300);`,
|
|
1388
1408
|
` }`,
|
|
1389
1409
|
`}`,
|
|
1390
1410
|
);
|
|
@@ -1643,7 +1663,16 @@ export function transformEsmSource(
|
|
|
1643
1663
|
result = result.slice(0, position) + code + result.slice(position);
|
|
1644
1664
|
}
|
|
1645
1665
|
|
|
1646
|
-
|
|
1666
|
+
// Preserve 'use client' / 'use server' directives — they must be the first expression
|
|
1667
|
+
// in the file (before any imports or code). Extract them from result and prepend before prefix.
|
|
1668
|
+
let directive = '';
|
|
1669
|
+
const directiveMatch = result.match(/^(\s*(?:'use client'|"use client"|'use server'|"use server")\s*;?\s*\n?)/);
|
|
1670
|
+
if (directiveMatch) {
|
|
1671
|
+
directive = directiveMatch[1];
|
|
1672
|
+
result = result.slice(directiveMatch[0].length);
|
|
1673
|
+
}
|
|
1674
|
+
|
|
1675
|
+
return directive + prefix + result;
|
|
1647
1676
|
}
|
|
1648
1677
|
|
|
1649
1678
|
export default tricklePlugin;
|