taist 0.1.9 → 0.1.11

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.8 | 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
 
@@ -321,6 +322,86 @@ This is particularly useful for:
321
322
  - Classes are wrapped so new instances are automatically instrumented
322
323
  - Non-function exports are passed through unchanged
323
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
+
324
405
  ---
325
406
 
326
407
  ## Test Integration
@@ -77,12 +77,14 @@ export function shouldInstrument(modulePath, config) {
77
77
  * Simple glob matching (supports * and **)
78
78
  */
79
79
  export function matchGlob(str, pattern) {
80
- // Convert glob pattern to regex
80
+ // Convert glob pattern to regex using placeholders to prevent double-replacement
81
81
  const regexPattern = pattern
82
82
  .replace(/[.+^${}()|[\]\\]/g, "\\$&") // Escape special chars
83
- .replace(/\*\*\//g, "(?:.*\\/)?") // **/ matches zero or more directories
84
- .replace(/\*\*/g, ".*") // ** matches anything
85
- .replace(/\*/g, "[^/]*"); // * matches any chars except /
83
+ .replace(/\*\*\//g, "\0ANYDIR\0") // Placeholder for **/
84
+ .replace(/\*\*/g, "\0ANY\0") // Placeholder for **
85
+ .replace(/\*/g, "[^/]*") // * matches any chars except /
86
+ .replace(/\0ANYDIR\0/g, "(?:.*\\/)?") // Restore **/ - matches zero or more directories
87
+ .replace(/\0ANY\0/g, ".*"); // Restore ** - matches anything
86
88
 
87
89
  const regex = new RegExp(`^${regexPattern}$`);
88
90
  return regex.test(str);
@@ -70,15 +70,6 @@ export class ToonFormatter {
70
70
  });
71
71
  }
72
72
 
73
- // Trace
74
- if (results.trace && results.trace.length > 0) {
75
- lines.push('');
76
- lines.push('TRACE:');
77
- results.trace.forEach(entry => {
78
- lines.push(this.formatTraceEntry(entry));
79
- });
80
- }
81
-
82
73
  // Coverage
83
74
  if (results.coverage) {
84
75
  lines.push('');
@@ -154,46 +145,6 @@ export class ToonFormatter {
154
145
  return lines;
155
146
  }
156
147
 
157
- /**
158
- * Format trace entry with depth-based indentation for execution tree
159
- */
160
- formatTraceEntry(entry) {
161
- const parts = [];
162
-
163
- // Function name
164
- parts.push(`fn:${this.abbreviateFunctionName(entry.name)}`);
165
-
166
- // Duration
167
- if (entry.duration !== undefined) {
168
- parts.push(`ms:${Math.round(entry.duration)}`);
169
- }
170
-
171
- // Arguments (if present)
172
- if (entry.args && entry.args.length > 0) {
173
- const args = entry.args
174
- .slice(0, this.options.maxArrayItems)
175
- .map(arg => this.formatValue(arg))
176
- .join(',');
177
- parts.push(`args:[${args}]`);
178
- }
179
-
180
- // Return value (if present and not undefined)
181
- if (entry.result !== undefined) {
182
- parts.push(`ret:${this.formatValue(entry.result)}`);
183
- }
184
-
185
- // Error (if present)
186
- if (entry.error) {
187
- parts.push(`err:${this.cleanErrorMessage(entry.error)}`);
188
- }
189
-
190
- // Calculate indentation based on depth (2 spaces base + 2 per depth level)
191
- const depth = entry.depth || 0;
192
- const indent = ' ' + ' '.repeat(depth);
193
-
194
- return `${indent}${parts.join(' ')}`;
195
- }
196
-
197
148
  /**
198
149
  * Abbreviate function name for compact output
199
150
  */
package/lib/transform.js CHANGED
@@ -134,6 +134,10 @@ export function transformSource(source, moduleNameOrOptions, tracerImportPath) {
134
134
  import { getGlobalReporter as __taist_getReporter } from "${reporterPath}";
135
135
  import { getContext as __taist_getContext, runWithContext as __taist_runWithContext, generateId as __taist_generateId } from "${traceContextPath}";
136
136
  const __taist_reporter = __taist_getReporter();
137
+ // Eagerly connect to collector if socket path is set (build-time instrumentation)
138
+ if (process.env.TAIST_COLLECTOR_SOCKET && !__taist_reporter.isConnected()) {
139
+ __taist_reporter.connectEager();
140
+ }
137
141
  const __taist_wrap = (fn, name) => {
138
142
  if (typeof fn !== 'function') return fn;
139
143
  const wrapped = function(...args) {
@@ -242,10 +246,18 @@ const __taist_module = "${moduleName}";
242
246
 
243
247
  let transformed = shebang + injection + sourceWithoutShebang;
244
248
 
245
- // Rename original exports
246
- for (const exp of exports) {
249
+ // Separate classes from functions - classes need different handling to preserve hoisting
250
+ const classExports = exports.filter(e => e.type === "class");
251
+ const functionExports = exports.filter(e => e.type !== "class");
252
+
253
+ // For CLASSES: Keep original export, instrument in-place (preserves hoisting)
254
+ // This avoids TDZ issues with circular dependencies
255
+ // We'll add __taist_instrumentClass() calls at the end instead of re-exporting
256
+
257
+ // For FUNCTIONS: Rename and re-export (original behavior)
258
+ for (const exp of functionExports) {
247
259
  if (exp.declaration === "inline") {
248
- // Inline exports: export function/class/const Name
260
+ // Inline exports: export function/const Name
249
261
  if (exp.type === "function") {
250
262
  // Rename: export function foo -> function __taist_orig_foo
251
263
  transformed = transformed.replace(
@@ -261,23 +273,10 @@ const __taist_module = "${moduleName}";
261
273
  new RegExp(`export\\s+const\\s+${exp.name}\\s*=`, "g"),
262
274
  `const __taist_orig_${exp.name} =`
263
275
  );
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
276
  }
271
277
  } 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") {
278
+ // Named exports: function foo {...} then export { foo }
279
+ if (exp.type === "function") {
281
280
  // Rename: function foo -> function __taist_orig_foo
282
281
  transformed = transformed.replace(
283
282
  new RegExp(`\\bfunction\\s+${exp.name}\\s*\\(`, "g"),
@@ -292,10 +291,10 @@ const __taist_module = "${moduleName}";
292
291
  }
293
292
  }
294
293
 
295
- // Remove named export statements that we're replacing
294
+ // Remove named export statements for FUNCTIONS we're replacing (not classes)
296
295
  // 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) {
296
+ const namedFunctionExportNames = functionExports.filter(e => e.declaration === "named").map(e => e.name);
297
+ if (namedFunctionExportNames.length > 0) {
299
298
  transformed = transformed.replace(
300
299
  /export\s*\{([^}]+)\}/g,
301
300
  (match, names) => {
@@ -303,7 +302,7 @@ const __taist_module = "${moduleName}";
303
302
  .map(n => n.trim())
304
303
  .filter(n => {
305
304
  const name = n.split(/\s+as\s+/)[0].trim();
306
- return !namedExportNames.includes(name);
305
+ return !namedFunctionExportNames.includes(name);
307
306
  });
308
307
  if (remaining.length === 0) {
309
308
  return '// export moved to end';
@@ -313,11 +312,12 @@ const __taist_module = "${moduleName}";
313
312
  );
314
313
  }
315
314
 
316
- // Track default exports that need to be moved to after re-exports
315
+ // Track default exports for FUNCTIONS that need to be moved to after re-exports
317
316
  const defaultExports = [];
318
317
 
319
- // Remove `export default Name;` from original location (will add after re-exports)
320
- for (const exp of exports) {
318
+ // Remove `export default Name;` from original location for FUNCTIONS only
319
+ // (Classes keep their default export in place)
320
+ for (const exp of functionExports) {
321
321
  // Match: export default Name;
322
322
  if (transformed.match(new RegExp(`export\\s+default\\s+${exp.name}\\s*;`))) {
323
323
  transformed = transformed.replace(
@@ -336,22 +336,34 @@ const __taist_module = "${moduleName}";
336
336
  }
337
337
  }
338
338
 
339
- // Add wrapped re-exports at the end
339
+ // Add wrapped re-exports for FUNCTIONS at the end
340
340
  // Only add module prefix if it differs from export name to avoid "Calculator.Calculator"
341
- const reexports = exports
341
+ const functionReexports = functionExports
342
342
  .map((exp) => {
343
343
  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
- }
344
+ // Functions use wrapMethod()
345
+ return `export const ${exp.name} = __taist_wrap(__taist_orig_${exp.name}, ${nameExpr});`;
351
346
  })
352
347
  .join("\n");
353
348
 
354
- transformed += `\n\n// --- TAIST WRAPPED EXPORTS ---\n${reexports}\n`;
349
+ // Add in-place instrumentation for CLASSES (preserves hoisting/TDZ)
350
+ // __taist_instrumentClass mutates the prototype in-place
351
+ const classInstrumentations = classExports
352
+ .map((exp) => {
353
+ const nameExpr = `(__taist_module === "${exp.name}" ? "${exp.name}" : __taist_module + ".${exp.name}")`;
354
+ return `__taist_instrumentClass(${exp.name}, ${nameExpr});`;
355
+ })
356
+ .join("\n");
357
+
358
+ transformed += `\n\n// --- TAIST INSTRUMENTATION ---\n`;
359
+
360
+ if (functionReexports) {
361
+ transformed += `// Wrapped function exports\n${functionReexports}\n`;
362
+ }
363
+
364
+ if (classInstrumentations) {
365
+ transformed += `// In-place class instrumentation (preserves hoisting)\n${classInstrumentations}\n`;
366
+ }
355
367
 
356
368
  // Add default exports at the end (after the wrapped versions are defined)
357
369
  for (const name of defaultExports) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "taist",
3
- "version": "0.1.9",
3
+ "version": "0.1.11",
4
4
  "description": "Token-Optimized Testing Framework for AI-Assisted Development",
5
5
  "main": "index.js",
6
6
  "type": "module",