taist 0.1.9 → 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.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
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) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "taist",
3
- "version": "0.1.9",
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",