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 +82 -1
- package/lib/config-loader.js +6 -4
- package/lib/toon-formatter.js +0 -49
- package/lib/transform.js +47 -35
- package/package.json +1 -1
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.
|
|
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/config-loader.js
CHANGED
|
@@ -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, "
|
|
84
|
-
.replace(/\*\*/g, "
|
|
85
|
-
.replace(/\*/g, "[^/]*")
|
|
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);
|
package/lib/toon-formatter.js
CHANGED
|
@@ -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
|
-
//
|
|
246
|
-
|
|
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/
|
|
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:
|
|
273
|
-
|
|
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
|
|
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
|
|
298
|
-
if (
|
|
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 !
|
|
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
|
|
320
|
-
|
|
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
|
|
341
|
+
const functionReexports = functionExports
|
|
342
342
|
.map((exp) => {
|
|
343
343
|
const nameExpr = `(__taist_module === "${exp.name}" ? "${exp.name}" : __taist_module + ".${exp.name}")`;
|
|
344
|
-
|
|
345
|
-
|
|
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
|
-
|
|
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) {
|