taist 0.1.8 → 0.1.10

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/README.md CHANGED
@@ -1,7 +1,7 @@
1
1
  # Taist - AI Test Runner
2
2
  ## Token-Optimized Testing Framework for AI-Assisted Development
3
3
 
4
- Version: 0.1.0 | January 2025 | [Technical Specification](./SPEC.md)
4
+ Version: 0.1.9 | January 2025 | [Technical Specification](./SPEC.md)
5
5
 
6
6
  ---
7
7
 
@@ -135,6 +135,7 @@ tracer.instrument(MyClass, 'MyClass');
135
135
  | Method | Use Case | Setup |
136
136
  |--------|----------|-------|
137
137
  | **ESM Loader** | Node.js apps, automatic tracing | `node --import taist/module-patcher app.js` |
138
+ | **Build-Time** | Bundled apps (Directus, Vite) | `taist/vite-plugin` in build config |
138
139
  | **Import-based** | Express apps, selective tracing | `import 'taist/instrument'` |
139
140
  | **Programmatic** | Full control, multiple tracers | `new ServiceTracer()` |
140
141
 
@@ -294,6 +295,113 @@ const service = new DynamicService();
294
295
  const traced = instrumentService(service, 'DynamicService');
295
296
  ```
296
297
 
298
+ #### Module-Level Instrumentation
299
+
300
+ When you have a module with multiple exported functions or classes, use `instrumentModule` to wrap all exports at once:
301
+
302
+ ```javascript
303
+ import { instrumentModule } from 'taist/instrument';
304
+ import * as orderServices from './services/order.js';
305
+
306
+ // Wrap all exports from the module
307
+ export const Order = instrumentModule(orderServices, 'Order');
308
+
309
+ // Now all functions in Order are traced:
310
+ // Order.createOrder() → traced as "Order.createOrder"
311
+ // Order.getOrder() → traced as "Order.getOrder"
312
+ // Order.updateOrder() → traced as "Order.updateOrder"
313
+ ```
314
+
315
+ This is particularly useful for:
316
+ - **GraphQL resolvers** - Instrument all resolver functions in a module
317
+ - **Service layers** - Wrap entire service modules without individual instrumentation
318
+ - **Utility modules** - Trace helper functions across a module
319
+
320
+ **How it works:**
321
+ - Functions are wrapped with context-aware tracing
322
+ - Classes are wrapped so new instances are automatically instrumented
323
+ - Non-function exports are passed through unchanged
324
+
325
+ ### Build-Time Instrumentation (Bundled Apps)
326
+
327
+ For applications that bundle their code (like Directus extensions, Vite apps, etc.), runtime instrumentation can't see inside the bundle. Use the **Rollup/Vite plugin** to instrument during build instead.
328
+
329
+ **The Problem:**
330
+ ```
331
+ src/order.js ─┐
332
+ src/user.js ─┼─► Bundler ─► dist/bundle.js (one file)
333
+ src/utils.js ─┘
334
+
335
+ ESM loader only sees this one module
336
+ ```
337
+
338
+ **The Solution:** Instrument source files BEFORE bundling:
339
+ ```
340
+ src/order.js ─► instrument ─┐
341
+ src/user.js ─► instrument ─┼─► Bundler ─► dist/bundle.js
342
+ src/utils.js ─► instrument ─┘ (instrumented!)
343
+ ```
344
+
345
+ #### Vite Configuration
346
+
347
+ ```javascript
348
+ // vite.config.js
349
+ import { defineConfig } from 'vite';
350
+ import taistPlugin from 'taist/vite-plugin';
351
+
352
+ export default defineConfig({
353
+ plugins: [
354
+ taistPlugin({
355
+ include: ['src/**/*.js', 'src/**/*.ts'],
356
+ exclude: ['**/*.test.js']
357
+ })
358
+ ],
359
+ build: {
360
+ rollupOptions: {
361
+ // Keep taist as external - it's a runtime dependency
362
+ external: ['taist/lib/trace-reporter.js', 'taist/lib/trace-context.js']
363
+ }
364
+ }
365
+ });
366
+ ```
367
+
368
+ #### Rollup Configuration
369
+
370
+ ```javascript
371
+ // rollup.config.js
372
+ import taistPlugin from 'taist/rollup-plugin';
373
+
374
+ export default {
375
+ input: 'src/index.js',
376
+ output: { file: 'dist/bundle.js', format: 'es' },
377
+ plugins: [
378
+ taistPlugin({
379
+ include: ['src/**/*.js']
380
+ })
381
+ ],
382
+ external: ['taist/lib/trace-reporter.js', 'taist/lib/trace-context.js']
383
+ };
384
+ ```
385
+
386
+ #### Plugin Options
387
+
388
+ | Option | Type | Default | Description |
389
+ |--------|------|---------|-------------|
390
+ | `include` | `string[]` | `['src/**/*.js']` | Glob patterns for files to instrument |
391
+ | `exclude` | `string[]` | `['**/node_modules/**']` | Glob patterns to skip |
392
+ | `enabled` | `boolean` | `true` | Enable/disable (respects `TAIST_ENABLED` env) |
393
+
394
+ **When to use:**
395
+ - Directus extensions
396
+ - Vite/Rollup bundled applications
397
+ - Any code that gets bundled before deployment
398
+ - When ESM loader can't intercept your modules
399
+
400
+ **Runtime requirements:**
401
+ - `taist` must be installed as a dependency of the host application
402
+ - Set `TAIST_ENABLED=true` and `TAIST_COLLECTOR_SOCKET` at runtime
403
+ - The built code will send traces to the collector when executed
404
+
297
405
  ---
298
406
 
299
407
  ## Test Integration
@@ -0,0 +1,120 @@
1
+ /**
2
+ * Taist Rollup Plugin - Build-time instrumentation for bundled code
3
+ *
4
+ * Transforms source files during build to add tracing, BEFORE bundling
5
+ * collapses them into a single file. This enables deep tracing of internal
6
+ * functions within bundled applications.
7
+ *
8
+ * Usage:
9
+ * import taistPlugin from 'taist/rollup-plugin';
10
+ *
11
+ * export default {
12
+ * plugins: [
13
+ * taistPlugin({
14
+ * include: ['src/**\/*.js'],
15
+ * exclude: ['**\/*.test.js']
16
+ * })
17
+ * ]
18
+ * };
19
+ */
20
+
21
+ import { transformSource } from './transform.js';
22
+ import { shouldInstrument, matchGlob, loadConfig } from './config-loader.js';
23
+ import path from 'node:path';
24
+
25
+ /**
26
+ * Create a Taist Rollup plugin
27
+ * @param {Object} options - Plugin options
28
+ * @param {string[]} [options.include] - Glob patterns for files to instrument
29
+ * @param {string[]} [options.exclude] - Glob patterns for files to skip
30
+ * @param {boolean} [options.enabled] - Enable/disable plugin (default: true, or TAIST_ENABLED env)
31
+ * @returns {import('rollup').Plugin}
32
+ */
33
+ export function taistPlugin(options = {}) {
34
+ // Determine if enabled
35
+ const enabled = options.enabled ?? (process.env.TAIST_ENABLED !== 'false');
36
+
37
+ if (!enabled) {
38
+ return {
39
+ name: 'taist',
40
+ // No-op plugin when disabled
41
+ };
42
+ }
43
+
44
+ // Build config from options or load from file
45
+ let config;
46
+
47
+ return {
48
+ name: 'taist',
49
+
50
+ async buildStart() {
51
+ if (options.include || options.exclude) {
52
+ // Use provided options
53
+ config = {
54
+ include: options.include || ['**/*.js', '**/*.ts'],
55
+ exclude: options.exclude || ['**/node_modules/**', '**/*.test.*', '**/*.spec.*'],
56
+ };
57
+ } else {
58
+ // Load from .taistrc.json
59
+ config = await loadConfig();
60
+ if (config.include.length === 0) {
61
+ // Default to instrumenting src/ if no config
62
+ config.include = ['src/**/*.js', 'src/**/*.ts'];
63
+ }
64
+ }
65
+ },
66
+
67
+ transform(code, id) {
68
+ // Skip if no config yet
69
+ if (!config) {
70
+ return null;
71
+ }
72
+
73
+ // Skip node_modules
74
+ if (id.includes('node_modules')) {
75
+ return null;
76
+ }
77
+
78
+ // Skip non-JS/TS files
79
+ if (!id.match(/\.[jt]sx?$/)) {
80
+ return null;
81
+ }
82
+
83
+ // Get relative path for matching
84
+ const relativePath = path.relative(process.cwd(), id);
85
+
86
+ // Check include patterns
87
+ const included = config.include.some(pattern => matchGlob(relativePath, pattern));
88
+ if (!included) {
89
+ return null;
90
+ }
91
+
92
+ // Check exclude patterns
93
+ const excluded = config.exclude.some(pattern => matchGlob(relativePath, pattern));
94
+ if (excluded) {
95
+ return null;
96
+ }
97
+
98
+ try {
99
+ // Transform the source
100
+ const transformed = transformSource(code, {
101
+ filename: id,
102
+ useReporter: true,
103
+ // Use package paths - these should be externalized or bundled with the app
104
+ traceReporterPath: null, // Uses default 'taist/lib/trace-reporter.js'
105
+ traceContextPath: null, // Uses default 'taist/lib/trace-context.js'
106
+ });
107
+
108
+ return {
109
+ code: transformed,
110
+ map: null, // TODO: Add sourcemap support
111
+ };
112
+ } catch (err) {
113
+ this.warn(`Failed to transform ${relativePath}: ${err.message}`);
114
+ return null;
115
+ }
116
+ },
117
+ };
118
+ }
119
+
120
+ export default taistPlugin;
package/lib/transform.js CHANGED
@@ -242,10 +242,18 @@ const __taist_module = "${moduleName}";
242
242
 
243
243
  let transformed = shebang + injection + sourceWithoutShebang;
244
244
 
245
- // Rename original exports
246
- for (const exp of exports) {
245
+ // Separate classes from functions - classes need different handling to preserve hoisting
246
+ const classExports = exports.filter(e => e.type === "class");
247
+ const functionExports = exports.filter(e => e.type !== "class");
248
+
249
+ // For CLASSES: Keep original export, instrument in-place (preserves hoisting)
250
+ // This avoids TDZ issues with circular dependencies
251
+ // We'll add __taist_instrumentClass() calls at the end instead of re-exporting
252
+
253
+ // For FUNCTIONS: Rename and re-export (original behavior)
254
+ for (const exp of functionExports) {
247
255
  if (exp.declaration === "inline") {
248
- // Inline exports: export function/class/const Name
256
+ // Inline exports: export function/const Name
249
257
  if (exp.type === "function") {
250
258
  // Rename: export function foo -> function __taist_orig_foo
251
259
  transformed = transformed.replace(
@@ -261,23 +269,10 @@ const __taist_module = "${moduleName}";
261
269
  new RegExp(`export\\s+const\\s+${exp.name}\\s*=`, "g"),
262
270
  `const __taist_orig_${exp.name} =`
263
271
  );
264
- } else if (exp.type === "class") {
265
- // Rename: export class Foo -> class __taist_orig_Foo
266
- transformed = transformed.replace(
267
- new RegExp(`export\\s+class\\s+${exp.name}\\b`, "g"),
268
- `class __taist_orig_${exp.name}`
269
- );
270
272
  }
271
273
  } else if (exp.declaration === "named") {
272
- // Named exports: class Foo {...} then export { Foo }
273
- // Rename the declaration (not the export)
274
- if (exp.type === "class") {
275
- // Rename: class Foo -> class __taist_orig_Foo (handles extends)
276
- transformed = transformed.replace(
277
- new RegExp(`\\bclass\\s+${exp.name}\\s*(extends\\s+\\w+\\s*)?([{<])`, "g"),
278
- (match, ext, brace) => `class __taist_orig_${exp.name} ${ext || ''}${brace}`
279
- );
280
- } else if (exp.type === "function") {
274
+ // Named exports: function foo {...} then export { foo }
275
+ if (exp.type === "function") {
281
276
  // Rename: function foo -> function __taist_orig_foo
282
277
  transformed = transformed.replace(
283
278
  new RegExp(`\\bfunction\\s+${exp.name}\\s*\\(`, "g"),
@@ -292,10 +287,10 @@ const __taist_module = "${moduleName}";
292
287
  }
293
288
  }
294
289
 
295
- // Remove named export statements that we're replacing
290
+ // Remove named export statements for FUNCTIONS we're replacing (not classes)
296
291
  // Match: export { Name1, Name2 } and remove names we're wrapping
297
- const namedExportNames = exports.filter(e => e.declaration === "named").map(e => e.name);
298
- if (namedExportNames.length > 0) {
292
+ const namedFunctionExportNames = functionExports.filter(e => e.declaration === "named").map(e => e.name);
293
+ if (namedFunctionExportNames.length > 0) {
299
294
  transformed = transformed.replace(
300
295
  /export\s*\{([^}]+)\}/g,
301
296
  (match, names) => {
@@ -303,7 +298,7 @@ const __taist_module = "${moduleName}";
303
298
  .map(n => n.trim())
304
299
  .filter(n => {
305
300
  const name = n.split(/\s+as\s+/)[0].trim();
306
- return !namedExportNames.includes(name);
301
+ return !namedFunctionExportNames.includes(name);
307
302
  });
308
303
  if (remaining.length === 0) {
309
304
  return '// export moved to end';
@@ -313,11 +308,12 @@ const __taist_module = "${moduleName}";
313
308
  );
314
309
  }
315
310
 
316
- // Track default exports that need to be moved to after re-exports
311
+ // Track default exports for FUNCTIONS that need to be moved to after re-exports
317
312
  const defaultExports = [];
318
313
 
319
- // Remove `export default Name;` from original location (will add after re-exports)
320
- for (const exp of exports) {
314
+ // Remove `export default Name;` from original location for FUNCTIONS only
315
+ // (Classes keep their default export in place)
316
+ for (const exp of functionExports) {
321
317
  // Match: export default Name;
322
318
  if (transformed.match(new RegExp(`export\\s+default\\s+${exp.name}\\s*;`))) {
323
319
  transformed = transformed.replace(
@@ -336,22 +332,34 @@ const __taist_module = "${moduleName}";
336
332
  }
337
333
  }
338
334
 
339
- // Add wrapped re-exports at the end
335
+ // Add wrapped re-exports for FUNCTIONS at the end
340
336
  // Only add module prefix if it differs from export name to avoid "Calculator.Calculator"
341
- const reexports = exports
337
+ const functionReexports = functionExports
342
338
  .map((exp) => {
343
339
  const nameExpr = `(__taist_module === "${exp.name}" ? "${exp.name}" : __taist_module + ".${exp.name}")`;
344
- if (exp.type === "class") {
345
- // Classes use instrument() to wrap prototype methods
346
- return `export const ${exp.name} = __taist_instrumentClass(__taist_orig_${exp.name}, ${nameExpr});`;
347
- } else {
348
- // Functions use wrapMethod()
349
- return `export const ${exp.name} = __taist_wrap(__taist_orig_${exp.name}, ${nameExpr});`;
350
- }
340
+ // Functions use wrapMethod()
341
+ return `export const ${exp.name} = __taist_wrap(__taist_orig_${exp.name}, ${nameExpr});`;
351
342
  })
352
343
  .join("\n");
353
344
 
354
- transformed += `\n\n// --- TAIST WRAPPED EXPORTS ---\n${reexports}\n`;
345
+ // Add in-place instrumentation for CLASSES (preserves hoisting/TDZ)
346
+ // __taist_instrumentClass mutates the prototype in-place
347
+ const classInstrumentations = classExports
348
+ .map((exp) => {
349
+ const nameExpr = `(__taist_module === "${exp.name}" ? "${exp.name}" : __taist_module + ".${exp.name}")`;
350
+ return `__taist_instrumentClass(${exp.name}, ${nameExpr});`;
351
+ })
352
+ .join("\n");
353
+
354
+ transformed += `\n\n// --- TAIST INSTRUMENTATION ---\n`;
355
+
356
+ if (functionReexports) {
357
+ transformed += `// Wrapped function exports\n${functionReexports}\n`;
358
+ }
359
+
360
+ if (classInstrumentations) {
361
+ transformed += `// In-place class instrumentation (preserves hoisting)\n${classInstrumentations}\n`;
362
+ }
355
363
 
356
364
  // Add default exports at the end (after the wrapped versions are defined)
357
365
  for (const name of defaultExports) {
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Taist Vite Plugin - Build-time instrumentation for bundled code
3
+ *
4
+ * Vite uses Rollup under the hood, so this re-exports the Rollup plugin.
5
+ *
6
+ * Usage:
7
+ * import taistPlugin from 'taist/vite-plugin';
8
+ *
9
+ * export default defineConfig({
10
+ * plugins: [
11
+ * taistPlugin({
12
+ * include: ['src/**\/*.js'],
13
+ * exclude: ['**\/*.test.js']
14
+ * })
15
+ * ]
16
+ * });
17
+ */
18
+
19
+ import { taistPlugin } from './rollup-plugin.js';
20
+
21
+ export { taistPlugin };
22
+ export default taistPlugin;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "taist",
3
- "version": "0.1.8",
3
+ "version": "0.1.10",
4
4
  "description": "Token-Optimized Testing Framework for AI-Assisted Development",
5
5
  "main": "index.js",
6
6
  "type": "module",
@@ -46,6 +46,14 @@
46
46
  "./trace-context": "./lib/trace-context.js",
47
47
  "./instrument-all": "./lib/instrument-all.js",
48
48
  "./config-loader": "./lib/config-loader.js",
49
+ "./rollup-plugin": {
50
+ "types": "./types/rollup-plugin.d.ts",
51
+ "default": "./lib/rollup-plugin.js"
52
+ },
53
+ "./vite-plugin": {
54
+ "types": "./types/vite-plugin.d.ts",
55
+ "default": "./lib/vite-plugin.js"
56
+ },
49
57
  "./lib/*": "./lib/*"
50
58
  },
51
59
  "scripts": {
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Taist Rollup Plugin Type Definitions
3
+ */
4
+
5
+ import type { Plugin } from 'rollup';
6
+
7
+ /**
8
+ * Options for the Taist Rollup/Vite plugin
9
+ */
10
+ export interface TaistPluginOptions {
11
+ /**
12
+ * Glob patterns for files to instrument.
13
+ * If not provided, loads from .taistrc.json or defaults to ['src/**\/*.js', 'src/**\/*.ts']
14
+ */
15
+ include?: string[];
16
+
17
+ /**
18
+ * Glob patterns for files to skip.
19
+ * Defaults to ['**\/node_modules/**', '**\/*.test.*', '**\/*.spec.*']
20
+ */
21
+ exclude?: string[];
22
+
23
+ /**
24
+ * Enable/disable the plugin.
25
+ * Defaults to true, or respects TAIST_ENABLED environment variable.
26
+ */
27
+ enabled?: boolean;
28
+ }
29
+
30
+ /**
31
+ * Create a Taist Rollup plugin for build-time instrumentation.
32
+ *
33
+ * Transforms source files during build to add tracing, BEFORE bundling
34
+ * collapses them into a single file. This enables deep tracing of internal
35
+ * functions within bundled applications like Directus extensions.
36
+ *
37
+ * @param options - Plugin options
38
+ * @returns Rollup plugin
39
+ *
40
+ * @example
41
+ * // rollup.config.js
42
+ * import taistPlugin from 'taist/rollup-plugin';
43
+ *
44
+ * export default {
45
+ * input: 'src/index.js',
46
+ * output: { file: 'dist/bundle.js', format: 'es' },
47
+ * plugins: [
48
+ * taistPlugin({
49
+ * include: ['src/**\/*.js'],
50
+ * exclude: ['**\/*.test.js']
51
+ * })
52
+ * ]
53
+ * };
54
+ */
55
+ export function taistPlugin(options?: TaistPluginOptions): Plugin;
56
+
57
+ export default taistPlugin;
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Taist Vite Plugin Type Definitions
3
+ *
4
+ * Re-exports the Rollup plugin types since Vite uses Rollup internally.
5
+ */
6
+
7
+ import type { Plugin } from 'vite';
8
+ import type { TaistPluginOptions } from './rollup-plugin';
9
+
10
+ export type { TaistPluginOptions };
11
+
12
+ /**
13
+ * Create a Taist Vite plugin for build-time instrumentation.
14
+ *
15
+ * Transforms source files during build to add tracing, BEFORE bundling
16
+ * collapses them into a single file. This enables deep tracing of internal
17
+ * functions within bundled applications like Directus extensions.
18
+ *
19
+ * @param options - Plugin options
20
+ * @returns Vite plugin
21
+ *
22
+ * @example
23
+ * // vite.config.js
24
+ * import { defineConfig } from 'vite';
25
+ * import taistPlugin from 'taist/vite-plugin';
26
+ *
27
+ * export default defineConfig({
28
+ * plugins: [
29
+ * taistPlugin({
30
+ * include: ['src/**\/*.js'],
31
+ * exclude: ['**\/*.test.js']
32
+ * })
33
+ * ]
34
+ * });
35
+ */
36
+ export function taistPlugin(options?: TaistPluginOptions): Plugin;
37
+
38
+ export default taistPlugin;