trickle-observe 0.2.63 → 0.2.64

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.
@@ -0,0 +1,21 @@
1
+ /**
2
+ * trickle/next-loader — Webpack loader for Next.js component instrumentation.
3
+ *
4
+ * Applied via withTrickle() in next.config.js. Instruments .tsx/.jsx files
5
+ * with render tracking, useState change tracking, and hook observability —
6
+ * the same transforms the Vite plugin provides, but via webpack's loader API.
7
+ *
8
+ * Do not use directly. Use withTrickle() from 'trickle-observe/next-plugin' instead.
9
+ */
10
+ interface LoaderOptions {
11
+ backendUrl?: string;
12
+ include?: string[];
13
+ exclude?: string[];
14
+ debug?: boolean;
15
+ traceVars?: boolean;
16
+ }
17
+ export default function trickleNextLoader(this: {
18
+ resourcePath: string;
19
+ getOptions(): LoaderOptions;
20
+ }, source: string): string;
21
+ export {};
@@ -0,0 +1,50 @@
1
+ "use strict";
2
+ /**
3
+ * trickle/next-loader — Webpack loader for Next.js component instrumentation.
4
+ *
5
+ * Applied via withTrickle() in next.config.js. Instruments .tsx/.jsx files
6
+ * with render tracking, useState change tracking, and hook observability —
7
+ * the same transforms the Vite plugin provides, but via webpack's loader API.
8
+ *
9
+ * Do not use directly. Use withTrickle() from 'trickle-observe/next-plugin' instead.
10
+ */
11
+ var __importDefault = (this && this.__importDefault) || function (mod) {
12
+ return (mod && mod.__esModule) ? mod : { "default": mod };
13
+ };
14
+ Object.defineProperty(exports, "__esModule", { value: true });
15
+ exports.default = trickleNextLoader;
16
+ const path_1 = __importDefault(require("path"));
17
+ const vite_plugin_1 = require("./vite-plugin");
18
+ // webpack loader — `this` is the LoaderContext (must not be an arrow function)
19
+ function trickleNextLoader(source) {
20
+ const options = (this.getOptions && this.getOptions()) || {};
21
+ const resourcePath = this.resourcePath;
22
+ // Skip node_modules and trickle internals
23
+ if (resourcePath.includes('node_modules') || resourcePath.includes('trickle-observe')) {
24
+ return source;
25
+ }
26
+ // Include/exclude filters
27
+ if (options.include && options.include.length > 0) {
28
+ if (!options.include.some(p => resourcePath.includes(p)))
29
+ return source;
30
+ }
31
+ if (options.exclude && options.exclude.length > 0) {
32
+ if (options.exclude.some(p => resourcePath.includes(p)))
33
+ return source;
34
+ }
35
+ const backendUrl = options.backendUrl ?? process.env.TRICKLE_BACKEND_URL ?? 'http://localhost:4888';
36
+ const debug = options.debug ?? (process.env.TRICKLE_DEBUG === '1');
37
+ const traceVars = options.traceVars ?? true;
38
+ const moduleName = path_1.default.basename(resourcePath).replace(/\.[jt]sx?$/, '');
39
+ try {
40
+ const transformed = (0, vite_plugin_1.transformEsmSource)(source, resourcePath, moduleName, backendUrl, debug, traceVars, source);
41
+ if (debug && transformed !== source) {
42
+ console.log(`[trickle/next] Instrumented ${resourcePath}`);
43
+ }
44
+ return transformed;
45
+ }
46
+ catch {
47
+ // Never crash the build
48
+ return source;
49
+ }
50
+ }
@@ -0,0 +1,63 @@
1
+ /**
2
+ * trickle/next-plugin — Next.js observability via withTrickle().
3
+ *
4
+ * Wraps your Next.js config to add trickle's webpack loader, which instruments
5
+ * all React components (.tsx/.jsx) with render tracking, useState change tracking,
6
+ * and hook observability — the same hints you see when using the Vite plugin,
7
+ * but for Next.js (App Router and Pages Router, Client and Server Components).
8
+ *
9
+ * Setup in next.config.js:
10
+ *
11
+ * const { withTrickle } = require('trickle-observe/next-plugin');
12
+ *
13
+ * module.exports = withTrickle({
14
+ * // ...your existing Next.js config
15
+ * });
16
+ *
17
+ * Or with options:
18
+ *
19
+ * module.exports = withTrickle({
20
+ * reactStrictMode: true,
21
+ * }, {
22
+ * backendUrl: process.env.TRICKLE_BACKEND_URL,
23
+ * debug: process.env.TRICKLE_DEBUG === '1',
24
+ * });
25
+ *
26
+ * Environment variables:
27
+ * TRICKLE_BACKEND_URL — Backend URL (default: http://localhost:4888)
28
+ * TRICKLE_DEBUG — Set to "1" for debug logging
29
+ */
30
+ export interface TrickleNextOptions {
31
+ /** Backend URL (default: http://localhost:4888 or TRICKLE_BACKEND_URL env var) */
32
+ backendUrl?: string;
33
+ /** Only instrument files whose paths contain one of these substrings */
34
+ include?: string[];
35
+ /** Skip files whose paths contain one of these substrings */
36
+ exclude?: string[];
37
+ /** Enable debug logging */
38
+ debug?: boolean;
39
+ /** Enable variable tracing (default: true) */
40
+ traceVars?: boolean;
41
+ }
42
+ type NextConfig = Record<string, unknown> & {
43
+ webpack?: (config: WebpackConfig, context: WebpackContext) => WebpackConfig;
44
+ };
45
+ interface WebpackConfig {
46
+ module: {
47
+ rules: unknown[];
48
+ };
49
+ [key: string]: unknown;
50
+ }
51
+ interface WebpackContext {
52
+ isServer: boolean;
53
+ nextRuntime?: string;
54
+ [key: string]: unknown;
55
+ }
56
+ /**
57
+ * Wrap your Next.js config with trickle observability.
58
+ *
59
+ * Adds a webpack loader that instruments all .tsx/.jsx component files at build
60
+ * time with render tracking, useState change tracking, and hook observability.
61
+ */
62
+ export declare function withTrickle(nextConfig?: NextConfig, options?: TrickleNextOptions): NextConfig;
63
+ export {};
@@ -0,0 +1,65 @@
1
+ "use strict";
2
+ /**
3
+ * trickle/next-plugin — Next.js observability via withTrickle().
4
+ *
5
+ * Wraps your Next.js config to add trickle's webpack loader, which instruments
6
+ * all React components (.tsx/.jsx) with render tracking, useState change tracking,
7
+ * and hook observability — the same hints you see when using the Vite plugin,
8
+ * but for Next.js (App Router and Pages Router, Client and Server Components).
9
+ *
10
+ * Setup in next.config.js:
11
+ *
12
+ * const { withTrickle } = require('trickle-observe/next-plugin');
13
+ *
14
+ * module.exports = withTrickle({
15
+ * // ...your existing Next.js config
16
+ * });
17
+ *
18
+ * Or with options:
19
+ *
20
+ * module.exports = withTrickle({
21
+ * reactStrictMode: true,
22
+ * }, {
23
+ * backendUrl: process.env.TRICKLE_BACKEND_URL,
24
+ * debug: process.env.TRICKLE_DEBUG === '1',
25
+ * });
26
+ *
27
+ * Environment variables:
28
+ * TRICKLE_BACKEND_URL — Backend URL (default: http://localhost:4888)
29
+ * TRICKLE_DEBUG — Set to "1" for debug logging
30
+ */
31
+ var __importDefault = (this && this.__importDefault) || function (mod) {
32
+ return (mod && mod.__esModule) ? mod : { "default": mod };
33
+ };
34
+ Object.defineProperty(exports, "__esModule", { value: true });
35
+ exports.withTrickle = withTrickle;
36
+ const path_1 = __importDefault(require("path"));
37
+ /**
38
+ * Wrap your Next.js config with trickle observability.
39
+ *
40
+ * Adds a webpack loader that instruments all .tsx/.jsx component files at build
41
+ * time with render tracking, useState change tracking, and hook observability.
42
+ */
43
+ function withTrickle(nextConfig = {}, options = {}) {
44
+ const loaderPath = path_1.default.resolve(__dirname, './next-loader.js');
45
+ return {
46
+ ...nextConfig,
47
+ webpack(config, context) {
48
+ // Preserve existing webpack config
49
+ if (typeof nextConfig.webpack === 'function') {
50
+ config = nextConfig.webpack(config, context);
51
+ }
52
+ config.module.rules.push({
53
+ test: /\.(tsx?|jsx?)$/,
54
+ exclude: /node_modules/,
55
+ use: [
56
+ {
57
+ loader: loaderPath,
58
+ options,
59
+ },
60
+ ],
61
+ });
62
+ return config;
63
+ },
64
+ };
65
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "trickle-observe",
3
- "version": "0.2.63",
3
+ "version": "0.2.64",
4
4
  "description": "Runtime type observability for JavaScript applications",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -18,11 +18,13 @@
18
18
  },
19
19
  "./trace-var": "./dist/trace-var.js",
20
20
  "./lambda": "./dist/lambda.js",
21
- "./metro-transformer": "./dist/metro-transformer.js"
21
+ "./metro-transformer": "./dist/metro-transformer.js",
22
+ "./next-plugin": "./dist/next-plugin.js",
23
+ "./next-loader": "./dist/next-loader.js"
22
24
  },
23
25
  "scripts": {
24
26
  "build": "tsc && tsc -p tsconfig.esm.json",
25
- "test": "npm run build && node --experimental-strip-types --test src/vite-plugin.test.ts src/lambda.test.ts src/metro-transformer.test.ts",
27
+ "test": "npm run build && node --experimental-strip-types --test src/vite-plugin.test.ts src/lambda.test.ts src/metro-transformer.test.ts src/next-plugin.test.ts",
26
28
  "prepublishOnly": "npm run build"
27
29
  },
28
30
  "optionalDependencies": {
@@ -0,0 +1,55 @@
1
+ /**
2
+ * trickle/next-loader — Webpack loader for Next.js component instrumentation.
3
+ *
4
+ * Applied via withTrickle() in next.config.js. Instruments .tsx/.jsx files
5
+ * with render tracking, useState change tracking, and hook observability —
6
+ * the same transforms the Vite plugin provides, but via webpack's loader API.
7
+ *
8
+ * Do not use directly. Use withTrickle() from 'trickle-observe/next-plugin' instead.
9
+ */
10
+
11
+ import path from 'path';
12
+ import { transformEsmSource } from './vite-plugin';
13
+
14
+ interface LoaderOptions {
15
+ backendUrl?: string;
16
+ include?: string[];
17
+ exclude?: string[];
18
+ debug?: boolean;
19
+ traceVars?: boolean;
20
+ }
21
+
22
+ // webpack loader — `this` is the LoaderContext (must not be an arrow function)
23
+ export default function trickleNextLoader(this: { resourcePath: string; getOptions(): LoaderOptions }, source: string): string {
24
+ const options: LoaderOptions = (this.getOptions && this.getOptions()) || {};
25
+ const resourcePath = this.resourcePath;
26
+
27
+ // Skip node_modules and trickle internals
28
+ if (resourcePath.includes('node_modules') || resourcePath.includes('trickle-observe')) {
29
+ return source;
30
+ }
31
+
32
+ // Include/exclude filters
33
+ if (options.include && options.include.length > 0) {
34
+ if (!options.include.some(p => resourcePath.includes(p))) return source;
35
+ }
36
+ if (options.exclude && options.exclude.length > 0) {
37
+ if (options.exclude.some(p => resourcePath.includes(p))) return source;
38
+ }
39
+
40
+ const backendUrl = options.backendUrl ?? process.env.TRICKLE_BACKEND_URL ?? 'http://localhost:4888';
41
+ const debug = options.debug ?? (process.env.TRICKLE_DEBUG === '1');
42
+ const traceVars = options.traceVars ?? true;
43
+ const moduleName = path.basename(resourcePath).replace(/\.[jt]sx?$/, '');
44
+
45
+ try {
46
+ const transformed = transformEsmSource(source, resourcePath, moduleName, backendUrl, debug, traceVars, source);
47
+ if (debug && transformed !== source) {
48
+ console.log(`[trickle/next] Instrumented ${resourcePath}`);
49
+ }
50
+ return transformed;
51
+ } catch {
52
+ // Never crash the build
53
+ return source;
54
+ }
55
+ }
@@ -0,0 +1,158 @@
1
+ /**
2
+ * Unit tests for the Next.js plugin and loader (withTrickle, next-loader).
3
+ *
4
+ * Tests the transformation logic directly using transformEsmSource,
5
+ * mirroring what the webpack loader applies to Next.js component files.
6
+ *
7
+ * Run with: node --experimental-strip-types --test src/next-plugin.test.ts
8
+ */
9
+ import { describe, it } from 'node:test';
10
+ import assert from 'node:assert/strict';
11
+ import { transformEsmSource } from '../dist/vite-plugin.js';
12
+
13
+ const BACKEND = 'http://localhost:4888';
14
+
15
+ function transformNextTsx(code: string, filename = '/app/components/MyComponent.tsx'): string {
16
+ return transformEsmSource(code, filename, 'MyComponent', BACKEND, false, false);
17
+ }
18
+
19
+ function transformNextTs(code: string, filename = '/app/utils/helper.ts'): string {
20
+ return transformEsmSource(code, filename, 'helper', BACKEND, false, false);
21
+ }
22
+
23
+ // ── Next.js component patterns ─────────────────────────────────────────────
24
+
25
+ describe('Next.js Client Component tracking', () => {
26
+ it('tracks a "use client" component with useState', () => {
27
+ const code = [
28
+ `'use client';`,
29
+ `import { useState } from 'react';`,
30
+ `export default function Counter() {`,
31
+ ` const [count, setCount] = useState(0);`,
32
+ ` return <div>{count}</div>;`,
33
+ `}`,
34
+ ].join('\n');
35
+ const out = transformNextTsx(code);
36
+ assert.ok(out.includes('__trickle_rc'), 'should track render count');
37
+ assert.ok(out.includes('__trickle_ss'), 'should track useState');
38
+ });
39
+
40
+ it('tracks a "use client" arrow component with hooks', () => {
41
+ const code = [
42
+ `'use client';`,
43
+ `import { useState, useEffect } from 'react';`,
44
+ `const ProductCart: React.FC<Props> = ({ items }) => {`,
45
+ ` const [open, setOpen] = useState(false);`,
46
+ ` useEffect(() => { syncCart(); }, [items]);`,
47
+ ` return <div />;`,
48
+ `};`,
49
+ ].join('\n');
50
+ const out = transformNextTsx(code);
51
+ assert.ok(out.includes('__trickle_rc'), 'should track render');
52
+ assert.ok(out.includes('__trickle_ss'), 'should track useState');
53
+ assert.ok(out.includes('__trickle_hw'), 'should track useEffect');
54
+ });
55
+ });
56
+
57
+ describe('Next.js Server Component tracking', () => {
58
+ it('tracks a Server Component (no use client directive)', () => {
59
+ const code = [
60
+ `import { db } from '@/lib/db';`,
61
+ `export default async function ProductPage({ params }: { params: { id: string } }) {`,
62
+ ` const product = await db.product.findUnique({ where: { id: params.id } });`,
63
+ ` return <div>{product?.name}</div>;`,
64
+ `}`,
65
+ ].join('\n');
66
+ const out = transformNextTsx(code);
67
+ assert.ok(out.includes('__trickle_rc'), 'should track Server Component renders');
68
+ });
69
+
70
+ it('tracks a named Server Component export', () => {
71
+ const code = [
72
+ `export function Navbar({ user }: { user: User }) {`,
73
+ ` return <nav>{user.name}</nav>;`,
74
+ `}`,
75
+ ].join('\n');
76
+ const out = transformNextTsx(code);
77
+ assert.ok(out.includes('__trickle_rc'), 'should track named export server component');
78
+ });
79
+ });
80
+
81
+ describe('Next.js App Router component patterns', () => {
82
+ it('tracks export default function (most common Next.js page pattern)', () => {
83
+ const code = `export default function Page() { return <main />; }`;
84
+ const out = transformNextTsx(code);
85
+ assert.ok(out.includes('__trickle_rc'), 'should track default page export');
86
+ });
87
+
88
+ it('tracks React.memo wrapped component in Next.js', () => {
89
+ const code = [
90
+ `const ProductCard = React.memo(({ product }: { product: Product }) => {`,
91
+ ` return <div>{product.name}</div>;`,
92
+ `});`,
93
+ ].join('\n');
94
+ const out = transformNextTsx(code);
95
+ assert.ok(out.includes('__trickle_rc'), 'should track memo-wrapped Next.js component');
96
+ });
97
+
98
+ it('tracks React.FC typed component in Next.js', () => {
99
+ const code = [
100
+ `const SiteHeader: React.FC<{ title: string }> = ({ title }) => {`,
101
+ ` return <header>{title}</header>;`,
102
+ `};`,
103
+ ].join('\n');
104
+ const out = transformNextTsx(code);
105
+ assert.ok(out.includes('__trickle_rc'), 'should track React.FC component');
106
+ });
107
+ });
108
+
109
+ describe('Next.js: withTrickle plugin registration', () => {
110
+ it('withTrickle returns an object with a webpack function', async () => {
111
+ const { withTrickle } = await import('../dist/next-plugin.js');
112
+ const result = withTrickle({ reactStrictMode: true });
113
+ assert.ok(result, 'withTrickle should return a config object');
114
+ assert.equal(typeof result.webpack, 'function', 'should add webpack function');
115
+ assert.equal(result.reactStrictMode, true, 'should preserve existing config');
116
+ });
117
+
118
+ it('withTrickle webpack function returns config with trickle loader rule', async () => {
119
+ const { withTrickle } = await import('../dist/next-plugin.js');
120
+ const result = withTrickle({});
121
+ const mockConfig = { module: { rules: [] } };
122
+ const updatedConfig = result.webpack(mockConfig, { isServer: false });
123
+ assert.ok(updatedConfig.module.rules.length > 0, 'should add at least one rule');
124
+ const rule = updatedConfig.module.rules[0] as { test: RegExp; use: { loader: string }[] };
125
+ assert.ok(rule.test instanceof RegExp, 'rule should have a test regex');
126
+ assert.ok(rule.test.test('Component.tsx'), 'rule should match .tsx files');
127
+ assert.ok(rule.test.test('Component.jsx'), 'rule should match .jsx files');
128
+ assert.ok(!rule.test.test('helper.py'), 'rule should not match .py files');
129
+ });
130
+
131
+ it('withTrickle preserves existing webpack config', async () => {
132
+ const { withTrickle } = await import('../dist/next-plugin.js');
133
+ let calledWith = false;
134
+ const originalWebpack = (_config: unknown, _ctx: unknown) => { calledWith = true; return { module: { rules: [] } }; };
135
+ const result = withTrickle({ webpack: originalWebpack });
136
+ result.webpack({ module: { rules: [] } }, { isServer: false });
137
+ assert.ok(calledWith, 'should call original webpack function');
138
+ });
139
+
140
+ it('withTrickle works with no arguments', async () => {
141
+ const { withTrickle } = await import('../dist/next-plugin.js');
142
+ assert.doesNotThrow(() => withTrickle(), 'withTrickle() with no args should not throw');
143
+ });
144
+ });
145
+
146
+ describe('Next.js: does not instrument non-React files', () => {
147
+ it('does not inject render tracker in .ts utility files', () => {
148
+ const code = `export function formatPrice(amount: number): string { return '$' + amount; }`;
149
+ const out = transformNextTs(code);
150
+ assert.ok(!out.includes('__trickle_rc'), 'should not inject render tracker in .ts files');
151
+ });
152
+
153
+ it('does not inject useState tracker in .ts files', () => {
154
+ const code = `function helper() { const [x, setX] = useState(0); return x; }`;
155
+ const out = transformNextTs(code);
156
+ assert.ok(!out.includes('__trickle_ss'), 'should not track useState in .ts files');
157
+ });
158
+ });
@@ -0,0 +1,92 @@
1
+ /**
2
+ * trickle/next-plugin — Next.js observability via withTrickle().
3
+ *
4
+ * Wraps your Next.js config to add trickle's webpack loader, which instruments
5
+ * all React components (.tsx/.jsx) with render tracking, useState change tracking,
6
+ * and hook observability — the same hints you see when using the Vite plugin,
7
+ * but for Next.js (App Router and Pages Router, Client and Server Components).
8
+ *
9
+ * Setup in next.config.js:
10
+ *
11
+ * const { withTrickle } = require('trickle-observe/next-plugin');
12
+ *
13
+ * module.exports = withTrickle({
14
+ * // ...your existing Next.js config
15
+ * });
16
+ *
17
+ * Or with options:
18
+ *
19
+ * module.exports = withTrickle({
20
+ * reactStrictMode: true,
21
+ * }, {
22
+ * backendUrl: process.env.TRICKLE_BACKEND_URL,
23
+ * debug: process.env.TRICKLE_DEBUG === '1',
24
+ * });
25
+ *
26
+ * Environment variables:
27
+ * TRICKLE_BACKEND_URL — Backend URL (default: http://localhost:4888)
28
+ * TRICKLE_DEBUG — Set to "1" for debug logging
29
+ */
30
+
31
+ import path from 'path';
32
+
33
+ export interface TrickleNextOptions {
34
+ /** Backend URL (default: http://localhost:4888 or TRICKLE_BACKEND_URL env var) */
35
+ backendUrl?: string;
36
+ /** Only instrument files whose paths contain one of these substrings */
37
+ include?: string[];
38
+ /** Skip files whose paths contain one of these substrings */
39
+ exclude?: string[];
40
+ /** Enable debug logging */
41
+ debug?: boolean;
42
+ /** Enable variable tracing (default: true) */
43
+ traceVars?: boolean;
44
+ }
45
+
46
+ type NextConfig = Record<string, unknown> & {
47
+ webpack?: (config: WebpackConfig, context: WebpackContext) => WebpackConfig;
48
+ };
49
+
50
+ interface WebpackConfig {
51
+ module: { rules: unknown[] };
52
+ [key: string]: unknown;
53
+ }
54
+
55
+ interface WebpackContext {
56
+ isServer: boolean;
57
+ nextRuntime?: string;
58
+ [key: string]: unknown;
59
+ }
60
+
61
+ /**
62
+ * Wrap your Next.js config with trickle observability.
63
+ *
64
+ * Adds a webpack loader that instruments all .tsx/.jsx component files at build
65
+ * time with render tracking, useState change tracking, and hook observability.
66
+ */
67
+ export function withTrickle(nextConfig: NextConfig = {}, options: TrickleNextOptions = {}): NextConfig {
68
+ const loaderPath = path.resolve(__dirname, './next-loader.js');
69
+
70
+ return {
71
+ ...nextConfig,
72
+ webpack(config: WebpackConfig, context: WebpackContext) {
73
+ // Preserve existing webpack config
74
+ if (typeof nextConfig.webpack === 'function') {
75
+ config = nextConfig.webpack(config, context);
76
+ }
77
+
78
+ config.module.rules.push({
79
+ test: /\.(tsx?|jsx?)$/,
80
+ exclude: /node_modules/,
81
+ use: [
82
+ {
83
+ loader: loaderPath,
84
+ options,
85
+ },
86
+ ],
87
+ });
88
+
89
+ return config;
90
+ },
91
+ };
92
+ }