tryscript 0.1.4 → 0.1.6
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 +2 -2
- package/dist/bin.cjs +2 -2
- package/dist/bin.cjs.map +1 -1
- package/dist/bin.mjs +2 -2
- package/dist/bin.mjs.map +1 -1
- package/dist/index.cjs +1 -1
- package/dist/index.d.cts +10 -1
- package/dist/index.d.mts +10 -1
- package/dist/index.mjs +1 -1
- package/dist/{src-BGWMAShO.cjs → src-1oEnK7GG.cjs} +115 -4
- package/dist/src-1oEnK7GG.cjs.map +1 -0
- package/dist/{src-D60Uy8QA.mjs → src-Bd9-Y0qp.mjs} +116 -5
- package/dist/src-Bd9-Y0qp.mjs.map +1 -0
- package/docs/tryscript-reference.md +172 -60
- package/package.json +1 -1
- package/dist/src-BGWMAShO.cjs.map +0 -1
- package/dist/src-D60Uy8QA.mjs.map +0 -1
|
@@ -165,6 +165,9 @@ fixtures: # Files to copy to sandbox
|
|
|
165
165
|
- data/input.txt
|
|
166
166
|
before: npm run build # Run before first test
|
|
167
167
|
after: rm -rf ./cache # Run after all tests
|
|
168
|
+
path: # Directories to prepend to PATH
|
|
169
|
+
- ../dist
|
|
170
|
+
- $TRYSCRIPT_PACKAGE_BIN # Access node_modules/.bin via env var
|
|
168
171
|
---
|
|
169
172
|
```
|
|
170
173
|
|
|
@@ -180,6 +183,7 @@ after: rm -rf ./cache # Run after all tests
|
|
|
180
183
|
| `fixtures` | `array` | `[]` | Files to copy to sandbox |
|
|
181
184
|
| `before` | `string` | - | Shell command before first test |
|
|
182
185
|
| `after` | `string` | - | Shell command after all tests |
|
|
186
|
+
| `path` | `string[]` | `[]` | Directories to prepend to PATH (supports `$VAR` expansion) |
|
|
183
187
|
|
|
184
188
|
## Sandbox Mode
|
|
185
189
|
|
|
@@ -226,6 +230,135 @@ $ $CLI --version
|
|
|
226
230
|
|
|
227
231
|
**Important:** Variables are for the shell, not for output matching.
|
|
228
232
|
|
|
233
|
+
### Built-in Environment Variables
|
|
234
|
+
|
|
235
|
+
Tryscript sets these environment variables for test commands:
|
|
236
|
+
|
|
237
|
+
| Variable | Description |
|
|
238
|
+
|----------|-------------|
|
|
239
|
+
| `NO_COLOR` | Set to `"1"` by default (disables colors) |
|
|
240
|
+
| `FORCE_COLOR` | Set to `"0"` (disables forced colors) |
|
|
241
|
+
| `TRYSCRIPT_TEST_DIR` | Absolute path to directory containing the test file |
|
|
242
|
+
| `TRYSCRIPT_PACKAGE_ROOT` | Absolute path to directory containing nearest `package.json` (if found) |
|
|
243
|
+
| `TRYSCRIPT_GIT_ROOT` | Absolute path to directory containing nearest `.git` (if found) |
|
|
244
|
+
| `TRYSCRIPT_PROJECT_ROOT` | Most specific of `PACKAGE_ROOT` or `GIT_ROOT` (deepest path) |
|
|
245
|
+
| `TRYSCRIPT_PACKAGE_BIN` | Absolute path to `node_modules/.bin` directory (if exists) |
|
|
246
|
+
|
|
247
|
+
**Project root variables** help write portable tests that work across different project types:
|
|
248
|
+
|
|
249
|
+
- **`TRYSCRIPT_PACKAGE_ROOT`** - For npm/Node.js projects with `package.json`
|
|
250
|
+
- **`TRYSCRIPT_GIT_ROOT`** - For any git repository (Rust, Go, Python, etc.)
|
|
251
|
+
- **`TRYSCRIPT_PROJECT_ROOT`** - Use this when you don't care about project type
|
|
252
|
+
- **`TRYSCRIPT_PACKAGE_BIN`** - For npm packages with `node_modules/.bin` (use in `path:`)
|
|
253
|
+
|
|
254
|
+
**Example using TRYSCRIPT_PROJECT_ROOT:**
|
|
255
|
+
```console
|
|
256
|
+
$ test -n "$TRYSCRIPT_PROJECT_ROOT" && echo "in a project"
|
|
257
|
+
in a project
|
|
258
|
+
? 0
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
## Testing CLI Applications
|
|
262
|
+
|
|
263
|
+
Tryscript provides several ways to make CLI binaries available in tests.
|
|
264
|
+
|
|
265
|
+
### path: Custom Binary Directories
|
|
266
|
+
|
|
267
|
+
Use `path` to prepend directories to PATH, making executables available by name:
|
|
268
|
+
|
|
269
|
+
```yaml
|
|
270
|
+
---
|
|
271
|
+
sandbox: true
|
|
272
|
+
path:
|
|
273
|
+
- ../dist # Relative to test file directory
|
|
274
|
+
- $TRYSCRIPT_PACKAGE_BIN # Use node_modules/.bin via env var
|
|
275
|
+
---
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
```console
|
|
279
|
+
$ my-cli --version
|
|
280
|
+
1.0.0
|
|
281
|
+
? 0
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
**Key behaviors:**
|
|
285
|
+
- Paths are resolved relative to the test file directory (not the sandbox CWD)
|
|
286
|
+
- Multiple paths are prepended in order (first has highest priority)
|
|
287
|
+
- Works with or without sandbox mode
|
|
288
|
+
- Frontmatter and config file paths are merged (frontmatter first)
|
|
289
|
+
- **Environment variable expansion:** Path entries support standard shell variable syntax:
|
|
290
|
+
- `$VAR` - expands any environment variable (lowercase or uppercase)
|
|
291
|
+
- `${VAR}` - braced syntax also supported
|
|
292
|
+
- Tryscript env vars (`TRYSCRIPT_*`) are checked first, then process env vars
|
|
293
|
+
- Undefined variables expand to empty string
|
|
294
|
+
|
|
295
|
+
### Using node_modules/.bin (npm/pnpm/bun)
|
|
296
|
+
|
|
297
|
+
For Node.js projects using npm, pnpm, or bun, use `$TRYSCRIPT_PACKAGE_BIN` to access installed CLI tools:
|
|
298
|
+
|
|
299
|
+
```yaml
|
|
300
|
+
---
|
|
301
|
+
sandbox: true
|
|
302
|
+
path:
|
|
303
|
+
- $TRYSCRIPT_PACKAGE_BIN # Expands to node_modules/.bin
|
|
304
|
+
---
|
|
305
|
+
|
|
306
|
+
# Test: Run your CLI by name
|
|
307
|
+
```console
|
|
308
|
+
$ my-cli --version
|
|
309
|
+
1.0.0
|
|
310
|
+
? 0
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
# Test: Use any installed dev dependency
|
|
314
|
+
```console
|
|
315
|
+
$ prettier --check src/
|
|
316
|
+
[..]
|
|
317
|
+
? 0
|
|
318
|
+
```
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
This works for any executable installed via `npm install`, `pnpm add`, or `bun add`. The variable only expands if `node_modules/.bin` exists (i.e., after running your package manager's install command).
|
|
322
|
+
|
|
323
|
+
**Typical project setup:**
|
|
324
|
+
```
|
|
325
|
+
my-project/
|
|
326
|
+
├── package.json # TRYSCRIPT_PACKAGE_ROOT points here
|
|
327
|
+
├── node_modules/
|
|
328
|
+
│ └── .bin/ # TRYSCRIPT_PACKAGE_BIN points here
|
|
329
|
+
│ ├── prettier
|
|
330
|
+
│ ├── eslint
|
|
331
|
+
│ └── my-cli # Your package's bin entry
|
|
332
|
+
└── tests/
|
|
333
|
+
└── cli.tryscript.md # Your test file
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
### Language-Specific Examples
|
|
337
|
+
|
|
338
|
+
**Rust CLIs:**
|
|
339
|
+
```yaml
|
|
340
|
+
---
|
|
341
|
+
path:
|
|
342
|
+
- ../target/release
|
|
343
|
+
---
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
**Python with venv:**
|
|
347
|
+
```yaml
|
|
348
|
+
---
|
|
349
|
+
path:
|
|
350
|
+
- ../.venv/bin
|
|
351
|
+
---
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
**Go CLIs:**
|
|
355
|
+
```yaml
|
|
356
|
+
---
|
|
357
|
+
path:
|
|
358
|
+
- ../bin
|
|
359
|
+
---
|
|
360
|
+
```
|
|
361
|
+
|
|
229
362
|
## Test Annotations
|
|
230
363
|
|
|
231
364
|
Control test execution with HTML comments:
|
|
@@ -364,7 +497,7 @@ All coverage options mirror [c8](https://github.com/bcoe/c8) CLI flags for famil
|
|
|
364
497
|
| `--coverage-skip-full` | Hide 100% covered files | `false` |
|
|
365
498
|
| `--coverage-allow-external` | Allow files outside cwd | `false` |
|
|
366
499
|
| `--coverage-monocart` | Use monocart for accurate line counts | `false` |
|
|
367
|
-
| `--merge-lcov <path>` | Merge with external LCOV file (e.g., vitest) |
|
|
500
|
+
| `--merge-lcov <path>` | Merge with external LCOV file (e.g., vitest coverage) | - |
|
|
368
501
|
|
|
369
502
|
## Code Coverage
|
|
370
503
|
|
|
@@ -413,81 +546,58 @@ By default, tryscript coverage:
|
|
|
413
546
|
|
|
414
547
|
### Merging Coverage from Multiple Sources
|
|
415
548
|
|
|
416
|
-
|
|
417
|
-
To get complete coverage, merge results from both sources using the `--merge-lcov` flag.
|
|
418
|
-
|
|
419
|
-
#### Recommended: Built-in LCOV Merging
|
|
549
|
+
The `coverage` command merges V8 coverage from multiple CLI commands into a single report:
|
|
420
550
|
|
|
421
551
|
```bash
|
|
422
|
-
#
|
|
423
|
-
|
|
552
|
+
# Merge coverage from multiple CLI test commands
|
|
553
|
+
tryscript coverage "tryscript run tests/cli/" "node dist/bin.mjs --help"
|
|
424
554
|
|
|
425
|
-
#
|
|
426
|
-
tryscript
|
|
555
|
+
# With monocart for accurate line counts
|
|
556
|
+
tryscript coverage --monocart "tryscript run tests/"
|
|
427
557
|
```
|
|
428
558
|
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
- `coverage-summary.json` - JSON summary (for badge generation)
|
|
438
|
-
- `index.html` - HTML coverage report
|
|
439
|
-
|
|
440
|
-
**In package.json:**
|
|
441
|
-
```json
|
|
442
|
-
{
|
|
443
|
-
"scripts": {
|
|
444
|
-
"test:coverage": "vitest run --coverage && tryscript run 'tests/**/*.tryscript.md' --coverage --merge-lcov coverage/lcov.info"
|
|
445
|
-
}
|
|
446
|
-
}
|
|
447
|
-
```
|
|
448
|
-
|
|
449
|
-
**Example CI workflow** (GitHub Actions):
|
|
450
|
-
```yaml
|
|
451
|
-
- run: pnpm test:coverage
|
|
559
|
+
> **Important: Vitest Incompatibility**
|
|
560
|
+
>
|
|
561
|
+
> The `tryscript coverage` command uses `NODE_V8_COVERAGE` to collect coverage data from subprocesses.
|
|
562
|
+
> However, **vitest does not use `NODE_V8_COVERAGE`** - it controls the V8 profiler directly via
|
|
563
|
+
> `node:inspector` ([see vitest PR #2786](https://github.com/vitest-dev/vitest/pull/2786)).
|
|
564
|
+
>
|
|
565
|
+
> This means `tryscript coverage "vitest run" ...` will NOT collect coverage from vitest tests.
|
|
566
|
+
> The coverage command will warn you if a command produces no new coverage files.
|
|
452
567
|
|
|
453
|
-
|
|
454
|
-
- uses: jpb06/coverage-badges-action@v1
|
|
455
|
-
with:
|
|
456
|
-
coverage-summary-path: coverage-tryscript/coverage-summary.json
|
|
568
|
+
#### Merging Vitest + Tryscript Coverage
|
|
457
569
|
|
|
458
|
-
|
|
459
|
-
# - uses: codecov/codecov-action@v4
|
|
460
|
-
# with:
|
|
461
|
-
# files: coverage-tryscript/lcov.info
|
|
462
|
-
```
|
|
570
|
+
Use the built-in `--merge-lcov` flag to combine vitest and tryscript coverage in one step:
|
|
463
571
|
|
|
464
|
-
|
|
572
|
+
```bash
|
|
573
|
+
# Step 1: Run vitest with its own coverage (generates coverage/lcov.info)
|
|
574
|
+
vitest run --coverage
|
|
465
575
|
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
| `tryscript run --coverage` | Code executed by CLI subprocess spawns |
|
|
470
|
-
| `--merge-lcov` | Combines both into single report |
|
|
576
|
+
# Step 2: Run tryscript with coverage, merging vitest's LCOV file
|
|
577
|
+
tryscript run --coverage --merge-lcov coverage/lcov.info tests/
|
|
578
|
+
```
|
|
471
579
|
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
580
|
+
The `--merge-lcov` flag:
|
|
581
|
+
- Automatically adds the `lcov` reporter if not already specified
|
|
582
|
+
- Merges the external LCOV file with tryscript's generated coverage
|
|
583
|
+
- Outputs the combined `lcov.info` and `coverage-summary.json` for badge generation
|
|
475
584
|
|
|
476
|
-
|
|
585
|
+
**Alternative: Manual LCOV Merging**
|
|
477
586
|
|
|
478
|
-
|
|
479
|
-
the `tryscript coverage` command provides a simpler workflow:
|
|
587
|
+
If you need more control, you can merge LCOV files manually:
|
|
480
588
|
|
|
481
589
|
```bash
|
|
482
|
-
#
|
|
483
|
-
|
|
590
|
+
# Step 1: Run vitest with its own coverage
|
|
591
|
+
vitest run --coverage
|
|
484
592
|
|
|
485
|
-
#
|
|
486
|
-
tryscript coverage --
|
|
593
|
+
# Step 2: Run tryscript with coverage
|
|
594
|
+
tryscript run --coverage --coverage-reporter lcov tests/
|
|
595
|
+
|
|
596
|
+
# Step 3: Merge the LCOV files using lcov or a merge tool
|
|
597
|
+
lcov -a coverage/lcov.info -a coverage-tryscript/lcov.info -o coverage-merged/lcov.info
|
|
487
598
|
```
|
|
488
599
|
|
|
489
|
-
|
|
490
|
-
> if you have unit tests that import code directly.
|
|
600
|
+
Or use tools like `nyc merge`, `istanbul-merge`, or custom scripts to combine LCOV/JSON coverage.
|
|
491
601
|
|
|
492
602
|
#### Coverage Command Options
|
|
493
603
|
|
|
@@ -622,7 +732,7 @@ export default defineConfig({
|
|
|
622
732
|
| `allowExternal` | `--coverage-allow-external` | Allow external files |
|
|
623
733
|
| `src` | - | Source dir for mapping (config only) |
|
|
624
734
|
| `monocart` | `--coverage-monocart` | AST-aware line counts |
|
|
625
|
-
| `mergeLcov` | `--merge-lcov` |
|
|
735
|
+
| `mergeLcov` | `--merge-lcov` | Merge with external LCOV file |
|
|
626
736
|
|
|
627
737
|
## Best Practices
|
|
628
738
|
|
|
@@ -691,6 +801,8 @@ export default defineConfig({
|
|
|
691
801
|
VERSION: '\\d+\\.\\d+\\.\\d+',
|
|
692
802
|
UUID: '[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}',
|
|
693
803
|
},
|
|
804
|
+
// CLI testing configuration
|
|
805
|
+
path: ['./dist'], // Directories to add to PATH
|
|
694
806
|
});
|
|
695
807
|
```
|
|
696
808
|
|
package/package.json
CHANGED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"src-BGWMAShO.cjs","names":["DEFAULT_COVERAGE_CONFIG: ResolvedCoverageConfig","module","config: TestConfig","blocks: TestBlock[]","match: RegExpExecArray | null","commandLines: string[]","outputLines: string[]","stderrLines: string[]","expectedStderr: string | undefined","cwd: string","combinedChunks: { data: Buffer; type: 'stdout' | 'stderr' }[]","stdoutChunks: Buffer[]","stderrChunks: Buffer[]","VERSION: string"],"sources":["../src/lib/config.ts","../src/lib/parser.ts","../src/lib/runner.ts","../src/lib/matcher.ts","../src/index.ts"],"sourcesContent":["import { pathToFileURL } from 'node:url';\nimport { existsSync } from 'node:fs';\nimport { resolve } from 'node:path';\nimport type { TestConfig, CoverageConfig } from './types.js';\n\n/** Fixture configuration for copying files to sandbox directory */\nexport interface Fixture {\n /** Source path (resolved relative to test file) */\n source: string;\n /** Destination path (resolved relative to sandbox dir) */\n dest?: string;\n}\n\nexport interface TryscriptConfig {\n /** Working directory for commands (default: test file directory) */\n cwd?: string;\n /** Run in isolated sandbox: true = empty temp, path = copy to temp */\n sandbox?: boolean | string;\n /** Fixtures to copy to sandbox directory before tests */\n fixtures?: (string | Fixture)[];\n /** Script to run before first test block */\n before?: string;\n /** Script to run after all test blocks */\n after?: string;\n env?: Record<string, string>;\n timeout?: number;\n patterns?: Record<string, RegExp | string>;\n tests?: string[];\n /** Coverage configuration (used with --coverage flag) */\n coverage?: CoverageConfig;\n}\n\n/** Resolved coverage config with required fields (except optional mergeLcov) */\nexport type ResolvedCoverageConfig = Omit<Required<CoverageConfig>, 'mergeLcov'> & {\n mergeLcov?: string;\n};\n\n/** Default coverage configuration values. */\nexport const DEFAULT_COVERAGE_CONFIG: ResolvedCoverageConfig = {\n reportsDir: 'coverage-tryscript',\n reporters: ['text', 'html'],\n include: ['dist/**'],\n exclude: [],\n excludeNodeModules: true,\n excludeAfterRemap: false,\n skipFull: false,\n allowExternal: false,\n src: 'src',\n monocart: false,\n};\n\n/**\n * Resolve coverage options by merging user config with defaults.\n */\nexport function resolveCoverageConfig(config?: CoverageConfig): ResolvedCoverageConfig {\n return {\n reportsDir: config?.reportsDir ?? DEFAULT_COVERAGE_CONFIG.reportsDir,\n reporters: config?.reporters ?? DEFAULT_COVERAGE_CONFIG.reporters,\n include: config?.include ?? DEFAULT_COVERAGE_CONFIG.include,\n exclude: config?.exclude ?? DEFAULT_COVERAGE_CONFIG.exclude,\n excludeNodeModules: config?.excludeNodeModules ?? DEFAULT_COVERAGE_CONFIG.excludeNodeModules,\n excludeAfterRemap: config?.excludeAfterRemap ?? DEFAULT_COVERAGE_CONFIG.excludeAfterRemap,\n skipFull: config?.skipFull ?? DEFAULT_COVERAGE_CONFIG.skipFull,\n allowExternal: config?.allowExternal ?? DEFAULT_COVERAGE_CONFIG.allowExternal,\n src: config?.src ?? DEFAULT_COVERAGE_CONFIG.src,\n monocart: config?.monocart ?? DEFAULT_COVERAGE_CONFIG.monocart,\n mergeLcov: config?.mergeLcov,\n };\n}\n\nconst CONFIG_FILES = ['tryscript.config.ts', 'tryscript.config.js', 'tryscript.config.mjs'];\n\n/**\n * Load config file using dynamic import.\n * Supports both TypeScript (via tsx/ts-node) and JavaScript configs.\n */\nexport async function loadConfig(baseDir: string): Promise<TryscriptConfig> {\n for (const filename of CONFIG_FILES) {\n const configPath = resolve(baseDir, filename);\n if (existsSync(configPath)) {\n const configUrl = pathToFileURL(configPath).href;\n const module = (await import(configUrl)) as { default?: TryscriptConfig } | TryscriptConfig;\n return (module as { default?: TryscriptConfig }).default ?? (module as TryscriptConfig);\n }\n }\n return {};\n}\n\n/**\n * Merge config with frontmatter overrides.\n * Frontmatter takes precedence over config file.\n */\nexport function mergeConfig(base: TryscriptConfig, frontmatter: TestConfig): TryscriptConfig {\n return {\n ...base,\n ...frontmatter,\n env: { ...base.env, ...frontmatter.env },\n patterns: { ...base.patterns, ...frontmatter.patterns },\n fixtures: [...(base.fixtures ?? []), ...(frontmatter.fixtures ?? [])],\n };\n}\n\n/**\n * Helper for typed config files.\n */\nexport function defineConfig(config: TryscriptConfig): TryscriptConfig {\n return config;\n}\n","import { parse as parseYaml } from 'yaml';\nimport type { TestConfig, TestBlock, TestFile } from './types.js';\n\n/** Regex to match YAML frontmatter at the start of a file */\nconst FRONTMATTER_REGEX = /^---\\r?\\n([\\s\\S]*?)\\r?\\n---\\r?\\n/;\n\n/** Regex to match fenced code blocks with console/bash info string */\nconst CODE_BLOCK_REGEX = /```(console|bash)\\r?\\n([\\s\\S]*?)```/g;\n\n/** Regex to match markdown headings (for test names) */\nconst HEADING_REGEX = /^#+\\s+(?:Test:\\s*)?(.+)$/m;\n\n/** Regex to match skip annotation in heading or nearby HTML comment */\nconst SKIP_ANNOTATION_REGEX = /<!--\\s*skip\\s*-->/i;\n\n/** Regex to match only annotation in heading or nearby HTML comment */\nconst ONLY_ANNOTATION_REGEX = /<!--\\s*only\\s*-->/i;\n\n/**\n * Parse a .tryscript.md file into structured test data.\n */\nexport function parseTestFile(content: string, filePath: string): TestFile {\n const rawContent = content;\n let config: TestConfig = {};\n let body = content;\n\n // Extract frontmatter if present\n const frontmatterMatch = FRONTMATTER_REGEX.exec(content);\n if (frontmatterMatch) {\n const yamlContent = frontmatterMatch[1] ?? '';\n config = parseYaml(yamlContent) as TestConfig;\n body = content.slice(frontmatterMatch[0].length);\n }\n\n // Parse all console blocks\n const blocks: TestBlock[] = [];\n\n // Reset regex lastIndex\n CODE_BLOCK_REGEX.lastIndex = 0;\n\n let match: RegExpExecArray | null;\n while ((match = CODE_BLOCK_REGEX.exec(body)) !== null) {\n const blockContent = match[2] ?? '';\n const blockStart = match.index;\n\n // Find the line number (1-indexed)\n const precedingContent = content.slice(0, content.indexOf(match[0]));\n const lineNumber = precedingContent.split('\\n').length;\n\n // Look for a heading before this block (for test name)\n const contentBefore = body.slice(0, blockStart);\n const lastHeadingMatch = [\n ...contentBefore.matchAll(new RegExp(HEADING_REGEX.source, 'gm')),\n ].pop();\n const name = lastHeadingMatch?.[1]?.trim();\n\n // Check for skip/only annotations in the heading line or nearby comments\n const headingContext = lastHeadingMatch\n ? contentBefore.slice(contentBefore.lastIndexOf(lastHeadingMatch[0]))\n : '';\n const skip = SKIP_ANNOTATION_REGEX.test(headingContext);\n const only = ONLY_ANNOTATION_REGEX.test(headingContext);\n\n // Parse the block content\n const parsed = parseBlockContent(blockContent);\n if (parsed) {\n blocks.push({\n name,\n command: parsed.command,\n expectedOutput: parsed.expectedOutput,\n expectedStderr: parsed.expectedStderr,\n expectedExitCode: parsed.expectedExitCode,\n lineNumber,\n rawContent: match[0],\n skip,\n only,\n });\n }\n }\n\n return { path: filePath, config, blocks, rawContent };\n}\n\n/**\n * Parse the content of a single console block.\n */\nfunction parseBlockContent(content: string): {\n command: string;\n expectedOutput: string;\n expectedStderr?: string;\n expectedExitCode: number;\n} | null {\n const lines = content.split('\\n');\n const commandLines: string[] = [];\n const outputLines: string[] = [];\n const stderrLines: string[] = [];\n let expectedExitCode = 0;\n let inCommand = false;\n\n for (const line of lines) {\n if (line.startsWith('$ ')) {\n // Start of a command\n inCommand = true;\n commandLines.push(line.slice(2));\n } else if (line.startsWith('> ') && inCommand) {\n // Continuation of a multi-line command\n commandLines.push(line.slice(2));\n } else if (line.startsWith('? ')) {\n // Exit code specification\n inCommand = false;\n expectedExitCode = parseInt(line.slice(2).trim(), 10);\n } else if (line.startsWith('! ')) {\n // Stderr line (prefixed with !)\n inCommand = false;\n stderrLines.push(line.slice(2));\n } else {\n // Output line (stdout or combined)\n inCommand = false;\n outputLines.push(line);\n }\n }\n\n if (commandLines.length === 0) {\n return null;\n }\n\n // Join command lines, handling shell continuations\n let command = '';\n for (let i = 0; i < commandLines.length; i++) {\n const line = commandLines[i] ?? '';\n if (line.endsWith('\\\\')) {\n command += line.slice(0, -1) + ' ';\n } else {\n command += line;\n if (i < commandLines.length - 1) {\n command += ' ';\n }\n }\n }\n\n // Join output lines, preserving blank lines but trimming trailing empty lines\n let expectedOutput = outputLines.join('\\n');\n expectedOutput = expectedOutput.replace(/\\n+$/, '');\n if (expectedOutput) {\n expectedOutput += '\\n';\n }\n\n // Join stderr lines if any\n let expectedStderr: string | undefined;\n if (stderrLines.length > 0) {\n expectedStderr = stderrLines.join('\\n');\n expectedStderr = expectedStderr.replace(/\\n+$/, '');\n if (expectedStderr) {\n expectedStderr += '\\n';\n }\n }\n\n return { command: command.trim(), expectedOutput, expectedStderr, expectedExitCode };\n}\n","import { spawn } from 'node:child_process';\nimport { mkdtemp, realpath, rm, cp } from 'node:fs/promises';\nimport { tmpdir } from 'node:os';\nimport { join, dirname, resolve, basename } from 'node:path';\nimport treeKill from 'tree-kill';\nimport type { TestBlock, TestBlockResult } from './types.js';\nimport type { TryscriptConfig, Fixture } from './config.js';\n\n/** Default timeout in milliseconds */\nconst DEFAULT_TIMEOUT = 30_000;\n\n/**\n * Execution context for a test file.\n * Created once per file, contains directory paths and config.\n */\nexport interface ExecutionContext {\n /** Temporary directory for this test file (resolved, no symlinks) */\n tempDir: string;\n /** Directory containing the test file */\n testDir: string;\n /** Working directory for command execution */\n cwd: string;\n /** Whether running in sandbox mode */\n sandbox: boolean;\n /** Environment variables */\n env: Record<string, string>;\n /** Timeout per command */\n timeout: number;\n /** Before hook script */\n before?: string;\n /** After hook script */\n after?: string;\n /** Whether before hook has been run */\n beforeRan?: boolean;\n}\n\n/**\n * Normalize fixture config to Fixture object.\n */\nfunction normalizeFixture(fixture: string | Fixture): Fixture {\n if (typeof fixture === 'string') {\n return { source: fixture };\n }\n return fixture;\n}\n\n/**\n * Setup fixtures by copying files to sandbox directory.\n */\nasync function setupFixtures(\n fixtures: (string | Fixture)[] | undefined,\n testDir: string,\n sandboxDir: string,\n): Promise<void> {\n if (!fixtures || fixtures.length === 0) {\n return;\n }\n\n for (const f of fixtures) {\n const fixture = normalizeFixture(f);\n const src = resolve(testDir, fixture.source);\n const destName = fixture.dest ?? basename(fixture.source);\n const dst = resolve(sandboxDir, destName);\n await cp(src, dst, { recursive: true });\n }\n}\n\n/**\n * Create an execution context for a test file.\n * @param config - Test configuration\n * @param testFilePath - Path to the test file\n * @param coverageEnv - Optional coverage environment variables (e.g., NODE_V8_COVERAGE)\n */\nexport async function createExecutionContext(\n config: TryscriptConfig,\n testFilePath: string,\n coverageEnv?: Record<string, string>,\n): Promise<ExecutionContext> {\n // Create temp directory and resolve symlinks (e.g., /var -> /private/var on macOS)\n // This ensures [CWD] and [ROOT] patterns match pwd output\n const rawTempDir = await mkdtemp(join(tmpdir(), 'tryscript-'));\n const tempDir = await realpath(rawTempDir);\n\n // Resolve test file directory for portable test commands\n const testDir = resolve(dirname(testFilePath));\n\n // Determine working directory based on sandbox config\n let cwd: string;\n let sandbox = false;\n\n if (config.sandbox === true) {\n // Empty sandbox: run in temp directory\n cwd = tempDir;\n sandbox = true;\n } else if (typeof config.sandbox === 'string') {\n // Copy directory to sandbox: copy source to temp, run in temp\n const srcPath = resolve(testDir, config.sandbox);\n await cp(srcPath, tempDir, { recursive: true });\n cwd = tempDir;\n sandbox = true;\n } else if (config.cwd) {\n // Run in specified directory (relative to test file)\n cwd = resolve(testDir, config.cwd);\n } else {\n // Default: run in test file directory\n cwd = testDir;\n }\n\n // Copy additional fixtures to sandbox (only if sandbox enabled)\n if (sandbox && config.fixtures) {\n await setupFixtures(config.fixtures, testDir, tempDir);\n }\n\n const ctx: ExecutionContext = {\n tempDir,\n testDir,\n cwd,\n sandbox,\n env: {\n ...process.env,\n ...config.env,\n ...coverageEnv,\n // Disable colors by default for deterministic output\n NO_COLOR: config.env?.NO_COLOR ?? '1',\n FORCE_COLOR: '0',\n // Provide test directory for portable test commands\n TRYSCRIPT_TEST_DIR: testDir,\n } as Record<string, string>,\n timeout: config.timeout ?? DEFAULT_TIMEOUT,\n before: config.before,\n after: config.after,\n };\n\n return ctx;\n}\n\n/**\n * Clean up execution context (remove temp directory).\n */\nexport async function cleanupExecutionContext(ctx: ExecutionContext): Promise<void> {\n await rm(ctx.tempDir, { recursive: true, force: true });\n}\n\n/**\n * Run the before hook if it hasn't run yet.\n */\nexport async function runBeforeHook(ctx: ExecutionContext): Promise<void> {\n if (ctx.before && !ctx.beforeRan) {\n ctx.beforeRan = true;\n await executeCommand(ctx.before, ctx);\n }\n}\n\n/**\n * Run the after hook.\n */\nexport async function runAfterHook(ctx: ExecutionContext): Promise<void> {\n if (ctx.after) {\n await executeCommand(ctx.after, ctx);\n }\n}\n\n/**\n * Run a single test block and return the result.\n */\nexport async function runBlock(block: TestBlock, ctx: ExecutionContext): Promise<TestBlockResult> {\n const startTime = Date.now();\n\n // Handle skip annotation\n if (block.skip) {\n return {\n block,\n passed: true,\n actualOutput: '',\n actualExitCode: 0,\n duration: 0,\n skipped: true,\n };\n }\n\n try {\n // Run before hook if this is the first test\n await runBeforeHook(ctx);\n\n // Execute command directly (shell handles $VAR expansion)\n const { output, stdout, stderr, exitCode } = await executeCommand(block.command, ctx);\n\n const duration = Date.now() - startTime;\n\n return {\n block,\n passed: true, // Matching handled separately\n actualOutput: output,\n actualStdout: stdout,\n actualStderr: stderr,\n actualExitCode: exitCode,\n duration,\n };\n } catch (error) {\n const duration = Date.now() - startTime;\n const message = error instanceof Error ? error.message : String(error);\n\n return {\n block,\n passed: false,\n actualOutput: '',\n actualExitCode: -1,\n duration,\n error: message,\n };\n }\n}\n\n/** Command execution result with separate stdout/stderr */\ninterface CommandResult {\n output: string;\n stdout: string;\n stderr: string;\n exitCode: number;\n}\n\n/**\n * Execute a command and capture output.\n */\nasync function executeCommand(command: string, ctx: ExecutionContext): Promise<CommandResult> {\n return new Promise((resolve, reject) => {\n const proc = spawn(command, {\n shell: true,\n cwd: ctx.cwd,\n env: ctx.env as NodeJS.ProcessEnv,\n // Pipe both to capture\n stdio: ['ignore', 'pipe', 'pipe'],\n });\n\n const combinedChunks: { data: Buffer; type: 'stdout' | 'stderr' }[] = [];\n const stdoutChunks: Buffer[] = [];\n const stderrChunks: Buffer[] = [];\n\n // Capture data as it comes in to preserve order\n proc.stdout.on('data', (data: Buffer) => {\n combinedChunks.push({ data, type: 'stdout' });\n stdoutChunks.push(data);\n });\n proc.stderr.on('data', (data: Buffer) => {\n combinedChunks.push({ data, type: 'stderr' });\n stderrChunks.push(data);\n });\n\n const timeoutId = setTimeout(() => {\n if (proc.pid) {\n treeKill(proc.pid, 'SIGKILL');\n }\n reject(new Error(`Command timed out after ${ctx.timeout}ms`));\n }, ctx.timeout);\n\n proc.on('close', (code) => {\n clearTimeout(timeoutId);\n const output = Buffer.concat(combinedChunks.map((c) => c.data)).toString('utf-8');\n const stdout = Buffer.concat(stdoutChunks).toString('utf-8');\n const stderr = Buffer.concat(stderrChunks).toString('utf-8');\n resolve({\n output,\n stdout,\n stderr,\n exitCode: code ?? 0,\n });\n });\n\n proc.on('error', (err) => {\n clearTimeout(timeoutId);\n reject(err);\n });\n });\n}\n","import stripAnsi from 'strip-ansi';\n\n/**\n * Escape special regex characters in a string.\n */\nfunction escapeRegex(str: string): string {\n return str.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&');\n}\n\n// Marker prefix for patterns (uses Unicode private use chars that won't appear in normal output)\nconst MARKER = '\\uE000';\n\n/**\n * Convert expected output with elision patterns to a regex.\n *\n * Handles (matching trycmd):\n * - [..] — matches any characters on the same line (trycmd: [^\\n]*?)\n * - ... — matches zero or more complete lines (trycmd: \\n(([^\\n]*\\n)*)?)\n * - [EXE] — matches .exe on Windows, empty otherwise\n * - [ROOT] — replaced with test root directory (pre-processed)\n * - [CWD] — replaced with current working directory (pre-processed)\n * - Custom [NAME] patterns from config (trycmd: TestCases::insert_var)\n */\nfunction patternToRegex(\n expected: string,\n customPatterns: Record<string, string | RegExp> = {},\n): RegExp {\n // Build a map of markers to their regex replacements\n const replacements = new Map<string, string>();\n let markerIndex = 0;\n\n const getMarker = (): string => {\n return `${MARKER}${markerIndex++}${MARKER}`;\n };\n\n let processed = expected;\n\n // Replace [..] with marker\n const dotdotMarker = getMarker();\n replacements.set(dotdotMarker, '[^\\\\n]*');\n processed = processed.replaceAll('[..]', dotdotMarker);\n\n // Replace ... (followed by newline) with marker\n const ellipsisMarker = getMarker();\n replacements.set(ellipsisMarker, '(?:[^\\\\n]*\\\\n)*');\n processed = processed.replace(/\\.\\.\\.\\n/g, ellipsisMarker);\n\n // Replace [EXE] with marker\n const exeMarker = getMarker();\n const exe = process.platform === 'win32' ? '\\\\.exe' : '';\n replacements.set(exeMarker, exe);\n processed = processed.replaceAll('[EXE]', exeMarker);\n\n // Replace custom patterns with markers\n for (const [name, pattern] of Object.entries(customPatterns)) {\n const placeholder = `[${name}]`;\n const patternStr = pattern instanceof RegExp ? pattern.source : pattern;\n const marker = getMarker();\n replacements.set(marker, `(${patternStr})`);\n processed = processed.replaceAll(placeholder, marker);\n }\n\n // Escape special regex characters\n let regex = escapeRegex(processed);\n\n // Restore markers to their regex replacements\n for (const [marker, replacement] of replacements) {\n regex = regex.replaceAll(escapeRegex(marker), replacement);\n }\n\n // Match the entire string (dotall mode for . to match newlines if needed)\n return new RegExp(`^${regex}$`, 's');\n}\n\n/**\n * Pre-process expected output to replace path placeholders with actual paths.\n * This happens BEFORE pattern matching.\n */\nfunction preprocessPaths(expected: string, context: { root: string; cwd: string }): string {\n let result = expected;\n // Normalize paths for comparison (use forward slashes)\n const normalizedRoot = context.root.replace(/\\\\/g, '/');\n const normalizedCwd = context.cwd.replace(/\\\\/g, '/');\n result = result.replaceAll('[ROOT]', normalizedRoot);\n result = result.replaceAll('[CWD]', normalizedCwd);\n return result;\n}\n\n/**\n * Normalize actual output for comparison.\n * - Remove ANSI escape codes (colors, etc.)\n * - Normalize line endings to \\n\n * - Normalize paths (Windows backslashes to forward slashes)\n * - Trim trailing whitespace from lines\n * - Ensure single trailing newline\n */\nexport function normalizeOutput(output: string): string {\n // Remove ANSI escape codes first\n let normalized = stripAnsi(output);\n\n normalized = normalized\n .replace(/\\r\\n/g, '\\n')\n .replace(/\\r/g, '\\n')\n .split('\\n')\n .map((line) => line.trimEnd())\n .join('\\n')\n .replace(/\\n+$/, '\\n');\n\n // Handle empty output\n if (normalized === '\\n') {\n normalized = '';\n }\n\n return normalized;\n}\n\n/**\n * Check if actual output matches expected pattern.\n */\nexport function matchOutput(\n actual: string,\n expected: string,\n context: { root: string; cwd: string },\n customPatterns: Record<string, string | RegExp> = {},\n): boolean {\n const normalizedActual = normalizeOutput(actual);\n const normalizedExpected = normalizeOutput(expected);\n\n // Empty expected matches empty actual\n if (normalizedExpected === '' && normalizedActual === '') {\n return true;\n }\n\n const preprocessed = preprocessPaths(normalizedExpected, context);\n const regex = patternToRegex(preprocessed, customPatterns);\n return regex.test(normalizedActual);\n}\n","// Public API exports\n\n// Version constant (injected at build time)\ndeclare const __VERSION__: string;\nexport const VERSION: string = typeof __VERSION__ !== 'undefined' ? __VERSION__ : 'development';\n\n// Config helper\nexport { defineConfig } from './lib/config.js';\nexport type { TryscriptConfig } from './lib/config.js';\n\n// Types\nexport type {\n TestConfig,\n TestBlock,\n TestFile,\n TestBlockResult,\n TestFileResult,\n TestRunSummary,\n CoverageConfig,\n CoverageContext,\n} from './lib/types.js';\n\n// Core functions (for programmatic use)\nexport { parseTestFile } from './lib/parser.js';\nexport { runBlock, createExecutionContext, cleanupExecutionContext } from './lib/runner.js';\nexport type { ExecutionContext } from './lib/runner.js';\nexport { matchOutput, normalizeOutput } from './lib/matcher.js';\n"],"mappings":"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"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"src-D60Uy8QA.mjs","names":["DEFAULT_COVERAGE_CONFIG: ResolvedCoverageConfig","config: TestConfig","parseYaml","blocks: TestBlock[]","match: RegExpExecArray | null","commandLines: string[]","outputLines: string[]","stderrLines: string[]","expectedStderr: string | undefined","cwd: string","combinedChunks: { data: Buffer; type: 'stdout' | 'stderr' }[]","stdoutChunks: Buffer[]","stderrChunks: Buffer[]","VERSION: string"],"sources":["../src/lib/config.ts","../src/lib/parser.ts","../src/lib/runner.ts","../src/lib/matcher.ts","../src/index.ts"],"sourcesContent":["import { pathToFileURL } from 'node:url';\nimport { existsSync } from 'node:fs';\nimport { resolve } from 'node:path';\nimport type { TestConfig, CoverageConfig } from './types.js';\n\n/** Fixture configuration for copying files to sandbox directory */\nexport interface Fixture {\n /** Source path (resolved relative to test file) */\n source: string;\n /** Destination path (resolved relative to sandbox dir) */\n dest?: string;\n}\n\nexport interface TryscriptConfig {\n /** Working directory for commands (default: test file directory) */\n cwd?: string;\n /** Run in isolated sandbox: true = empty temp, path = copy to temp */\n sandbox?: boolean | string;\n /** Fixtures to copy to sandbox directory before tests */\n fixtures?: (string | Fixture)[];\n /** Script to run before first test block */\n before?: string;\n /** Script to run after all test blocks */\n after?: string;\n env?: Record<string, string>;\n timeout?: number;\n patterns?: Record<string, RegExp | string>;\n tests?: string[];\n /** Coverage configuration (used with --coverage flag) */\n coverage?: CoverageConfig;\n}\n\n/** Resolved coverage config with required fields (except optional mergeLcov) */\nexport type ResolvedCoverageConfig = Omit<Required<CoverageConfig>, 'mergeLcov'> & {\n mergeLcov?: string;\n};\n\n/** Default coverage configuration values. */\nexport const DEFAULT_COVERAGE_CONFIG: ResolvedCoverageConfig = {\n reportsDir: 'coverage-tryscript',\n reporters: ['text', 'html'],\n include: ['dist/**'],\n exclude: [],\n excludeNodeModules: true,\n excludeAfterRemap: false,\n skipFull: false,\n allowExternal: false,\n src: 'src',\n monocart: false,\n};\n\n/**\n * Resolve coverage options by merging user config with defaults.\n */\nexport function resolveCoverageConfig(config?: CoverageConfig): ResolvedCoverageConfig {\n return {\n reportsDir: config?.reportsDir ?? DEFAULT_COVERAGE_CONFIG.reportsDir,\n reporters: config?.reporters ?? DEFAULT_COVERAGE_CONFIG.reporters,\n include: config?.include ?? DEFAULT_COVERAGE_CONFIG.include,\n exclude: config?.exclude ?? DEFAULT_COVERAGE_CONFIG.exclude,\n excludeNodeModules: config?.excludeNodeModules ?? DEFAULT_COVERAGE_CONFIG.excludeNodeModules,\n excludeAfterRemap: config?.excludeAfterRemap ?? DEFAULT_COVERAGE_CONFIG.excludeAfterRemap,\n skipFull: config?.skipFull ?? DEFAULT_COVERAGE_CONFIG.skipFull,\n allowExternal: config?.allowExternal ?? DEFAULT_COVERAGE_CONFIG.allowExternal,\n src: config?.src ?? DEFAULT_COVERAGE_CONFIG.src,\n monocart: config?.monocart ?? DEFAULT_COVERAGE_CONFIG.monocart,\n mergeLcov: config?.mergeLcov,\n };\n}\n\nconst CONFIG_FILES = ['tryscript.config.ts', 'tryscript.config.js', 'tryscript.config.mjs'];\n\n/**\n * Load config file using dynamic import.\n * Supports both TypeScript (via tsx/ts-node) and JavaScript configs.\n */\nexport async function loadConfig(baseDir: string): Promise<TryscriptConfig> {\n for (const filename of CONFIG_FILES) {\n const configPath = resolve(baseDir, filename);\n if (existsSync(configPath)) {\n const configUrl = pathToFileURL(configPath).href;\n const module = (await import(configUrl)) as { default?: TryscriptConfig } | TryscriptConfig;\n return (module as { default?: TryscriptConfig }).default ?? (module as TryscriptConfig);\n }\n }\n return {};\n}\n\n/**\n * Merge config with frontmatter overrides.\n * Frontmatter takes precedence over config file.\n */\nexport function mergeConfig(base: TryscriptConfig, frontmatter: TestConfig): TryscriptConfig {\n return {\n ...base,\n ...frontmatter,\n env: { ...base.env, ...frontmatter.env },\n patterns: { ...base.patterns, ...frontmatter.patterns },\n fixtures: [...(base.fixtures ?? []), ...(frontmatter.fixtures ?? [])],\n };\n}\n\n/**\n * Helper for typed config files.\n */\nexport function defineConfig(config: TryscriptConfig): TryscriptConfig {\n return config;\n}\n","import { parse as parseYaml } from 'yaml';\nimport type { TestConfig, TestBlock, TestFile } from './types.js';\n\n/** Regex to match YAML frontmatter at the start of a file */\nconst FRONTMATTER_REGEX = /^---\\r?\\n([\\s\\S]*?)\\r?\\n---\\r?\\n/;\n\n/** Regex to match fenced code blocks with console/bash info string */\nconst CODE_BLOCK_REGEX = /```(console|bash)\\r?\\n([\\s\\S]*?)```/g;\n\n/** Regex to match markdown headings (for test names) */\nconst HEADING_REGEX = /^#+\\s+(?:Test:\\s*)?(.+)$/m;\n\n/** Regex to match skip annotation in heading or nearby HTML comment */\nconst SKIP_ANNOTATION_REGEX = /<!--\\s*skip\\s*-->/i;\n\n/** Regex to match only annotation in heading or nearby HTML comment */\nconst ONLY_ANNOTATION_REGEX = /<!--\\s*only\\s*-->/i;\n\n/**\n * Parse a .tryscript.md file into structured test data.\n */\nexport function parseTestFile(content: string, filePath: string): TestFile {\n const rawContent = content;\n let config: TestConfig = {};\n let body = content;\n\n // Extract frontmatter if present\n const frontmatterMatch = FRONTMATTER_REGEX.exec(content);\n if (frontmatterMatch) {\n const yamlContent = frontmatterMatch[1] ?? '';\n config = parseYaml(yamlContent) as TestConfig;\n body = content.slice(frontmatterMatch[0].length);\n }\n\n // Parse all console blocks\n const blocks: TestBlock[] = [];\n\n // Reset regex lastIndex\n CODE_BLOCK_REGEX.lastIndex = 0;\n\n let match: RegExpExecArray | null;\n while ((match = CODE_BLOCK_REGEX.exec(body)) !== null) {\n const blockContent = match[2] ?? '';\n const blockStart = match.index;\n\n // Find the line number (1-indexed)\n const precedingContent = content.slice(0, content.indexOf(match[0]));\n const lineNumber = precedingContent.split('\\n').length;\n\n // Look for a heading before this block (for test name)\n const contentBefore = body.slice(0, blockStart);\n const lastHeadingMatch = [\n ...contentBefore.matchAll(new RegExp(HEADING_REGEX.source, 'gm')),\n ].pop();\n const name = lastHeadingMatch?.[1]?.trim();\n\n // Check for skip/only annotations in the heading line or nearby comments\n const headingContext = lastHeadingMatch\n ? contentBefore.slice(contentBefore.lastIndexOf(lastHeadingMatch[0]))\n : '';\n const skip = SKIP_ANNOTATION_REGEX.test(headingContext);\n const only = ONLY_ANNOTATION_REGEX.test(headingContext);\n\n // Parse the block content\n const parsed = parseBlockContent(blockContent);\n if (parsed) {\n blocks.push({\n name,\n command: parsed.command,\n expectedOutput: parsed.expectedOutput,\n expectedStderr: parsed.expectedStderr,\n expectedExitCode: parsed.expectedExitCode,\n lineNumber,\n rawContent: match[0],\n skip,\n only,\n });\n }\n }\n\n return { path: filePath, config, blocks, rawContent };\n}\n\n/**\n * Parse the content of a single console block.\n */\nfunction parseBlockContent(content: string): {\n command: string;\n expectedOutput: string;\n expectedStderr?: string;\n expectedExitCode: number;\n} | null {\n const lines = content.split('\\n');\n const commandLines: string[] = [];\n const outputLines: string[] = [];\n const stderrLines: string[] = [];\n let expectedExitCode = 0;\n let inCommand = false;\n\n for (const line of lines) {\n if (line.startsWith('$ ')) {\n // Start of a command\n inCommand = true;\n commandLines.push(line.slice(2));\n } else if (line.startsWith('> ') && inCommand) {\n // Continuation of a multi-line command\n commandLines.push(line.slice(2));\n } else if (line.startsWith('? ')) {\n // Exit code specification\n inCommand = false;\n expectedExitCode = parseInt(line.slice(2).trim(), 10);\n } else if (line.startsWith('! ')) {\n // Stderr line (prefixed with !)\n inCommand = false;\n stderrLines.push(line.slice(2));\n } else {\n // Output line (stdout or combined)\n inCommand = false;\n outputLines.push(line);\n }\n }\n\n if (commandLines.length === 0) {\n return null;\n }\n\n // Join command lines, handling shell continuations\n let command = '';\n for (let i = 0; i < commandLines.length; i++) {\n const line = commandLines[i] ?? '';\n if (line.endsWith('\\\\')) {\n command += line.slice(0, -1) + ' ';\n } else {\n command += line;\n if (i < commandLines.length - 1) {\n command += ' ';\n }\n }\n }\n\n // Join output lines, preserving blank lines but trimming trailing empty lines\n let expectedOutput = outputLines.join('\\n');\n expectedOutput = expectedOutput.replace(/\\n+$/, '');\n if (expectedOutput) {\n expectedOutput += '\\n';\n }\n\n // Join stderr lines if any\n let expectedStderr: string | undefined;\n if (stderrLines.length > 0) {\n expectedStderr = stderrLines.join('\\n');\n expectedStderr = expectedStderr.replace(/\\n+$/, '');\n if (expectedStderr) {\n expectedStderr += '\\n';\n }\n }\n\n return { command: command.trim(), expectedOutput, expectedStderr, expectedExitCode };\n}\n","import { spawn } from 'node:child_process';\nimport { mkdtemp, realpath, rm, cp } from 'node:fs/promises';\nimport { tmpdir } from 'node:os';\nimport { join, dirname, resolve, basename } from 'node:path';\nimport treeKill from 'tree-kill';\nimport type { TestBlock, TestBlockResult } from './types.js';\nimport type { TryscriptConfig, Fixture } from './config.js';\n\n/** Default timeout in milliseconds */\nconst DEFAULT_TIMEOUT = 30_000;\n\n/**\n * Execution context for a test file.\n * Created once per file, contains directory paths and config.\n */\nexport interface ExecutionContext {\n /** Temporary directory for this test file (resolved, no symlinks) */\n tempDir: string;\n /** Directory containing the test file */\n testDir: string;\n /** Working directory for command execution */\n cwd: string;\n /** Whether running in sandbox mode */\n sandbox: boolean;\n /** Environment variables */\n env: Record<string, string>;\n /** Timeout per command */\n timeout: number;\n /** Before hook script */\n before?: string;\n /** After hook script */\n after?: string;\n /** Whether before hook has been run */\n beforeRan?: boolean;\n}\n\n/**\n * Normalize fixture config to Fixture object.\n */\nfunction normalizeFixture(fixture: string | Fixture): Fixture {\n if (typeof fixture === 'string') {\n return { source: fixture };\n }\n return fixture;\n}\n\n/**\n * Setup fixtures by copying files to sandbox directory.\n */\nasync function setupFixtures(\n fixtures: (string | Fixture)[] | undefined,\n testDir: string,\n sandboxDir: string,\n): Promise<void> {\n if (!fixtures || fixtures.length === 0) {\n return;\n }\n\n for (const f of fixtures) {\n const fixture = normalizeFixture(f);\n const src = resolve(testDir, fixture.source);\n const destName = fixture.dest ?? basename(fixture.source);\n const dst = resolve(sandboxDir, destName);\n await cp(src, dst, { recursive: true });\n }\n}\n\n/**\n * Create an execution context for a test file.\n * @param config - Test configuration\n * @param testFilePath - Path to the test file\n * @param coverageEnv - Optional coverage environment variables (e.g., NODE_V8_COVERAGE)\n */\nexport async function createExecutionContext(\n config: TryscriptConfig,\n testFilePath: string,\n coverageEnv?: Record<string, string>,\n): Promise<ExecutionContext> {\n // Create temp directory and resolve symlinks (e.g., /var -> /private/var on macOS)\n // This ensures [CWD] and [ROOT] patterns match pwd output\n const rawTempDir = await mkdtemp(join(tmpdir(), 'tryscript-'));\n const tempDir = await realpath(rawTempDir);\n\n // Resolve test file directory for portable test commands\n const testDir = resolve(dirname(testFilePath));\n\n // Determine working directory based on sandbox config\n let cwd: string;\n let sandbox = false;\n\n if (config.sandbox === true) {\n // Empty sandbox: run in temp directory\n cwd = tempDir;\n sandbox = true;\n } else if (typeof config.sandbox === 'string') {\n // Copy directory to sandbox: copy source to temp, run in temp\n const srcPath = resolve(testDir, config.sandbox);\n await cp(srcPath, tempDir, { recursive: true });\n cwd = tempDir;\n sandbox = true;\n } else if (config.cwd) {\n // Run in specified directory (relative to test file)\n cwd = resolve(testDir, config.cwd);\n } else {\n // Default: run in test file directory\n cwd = testDir;\n }\n\n // Copy additional fixtures to sandbox (only if sandbox enabled)\n if (sandbox && config.fixtures) {\n await setupFixtures(config.fixtures, testDir, tempDir);\n }\n\n const ctx: ExecutionContext = {\n tempDir,\n testDir,\n cwd,\n sandbox,\n env: {\n ...process.env,\n ...config.env,\n ...coverageEnv,\n // Disable colors by default for deterministic output\n NO_COLOR: config.env?.NO_COLOR ?? '1',\n FORCE_COLOR: '0',\n // Provide test directory for portable test commands\n TRYSCRIPT_TEST_DIR: testDir,\n } as Record<string, string>,\n timeout: config.timeout ?? DEFAULT_TIMEOUT,\n before: config.before,\n after: config.after,\n };\n\n return ctx;\n}\n\n/**\n * Clean up execution context (remove temp directory).\n */\nexport async function cleanupExecutionContext(ctx: ExecutionContext): Promise<void> {\n await rm(ctx.tempDir, { recursive: true, force: true });\n}\n\n/**\n * Run the before hook if it hasn't run yet.\n */\nexport async function runBeforeHook(ctx: ExecutionContext): Promise<void> {\n if (ctx.before && !ctx.beforeRan) {\n ctx.beforeRan = true;\n await executeCommand(ctx.before, ctx);\n }\n}\n\n/**\n * Run the after hook.\n */\nexport async function runAfterHook(ctx: ExecutionContext): Promise<void> {\n if (ctx.after) {\n await executeCommand(ctx.after, ctx);\n }\n}\n\n/**\n * Run a single test block and return the result.\n */\nexport async function runBlock(block: TestBlock, ctx: ExecutionContext): Promise<TestBlockResult> {\n const startTime = Date.now();\n\n // Handle skip annotation\n if (block.skip) {\n return {\n block,\n passed: true,\n actualOutput: '',\n actualExitCode: 0,\n duration: 0,\n skipped: true,\n };\n }\n\n try {\n // Run before hook if this is the first test\n await runBeforeHook(ctx);\n\n // Execute command directly (shell handles $VAR expansion)\n const { output, stdout, stderr, exitCode } = await executeCommand(block.command, ctx);\n\n const duration = Date.now() - startTime;\n\n return {\n block,\n passed: true, // Matching handled separately\n actualOutput: output,\n actualStdout: stdout,\n actualStderr: stderr,\n actualExitCode: exitCode,\n duration,\n };\n } catch (error) {\n const duration = Date.now() - startTime;\n const message = error instanceof Error ? error.message : String(error);\n\n return {\n block,\n passed: false,\n actualOutput: '',\n actualExitCode: -1,\n duration,\n error: message,\n };\n }\n}\n\n/** Command execution result with separate stdout/stderr */\ninterface CommandResult {\n output: string;\n stdout: string;\n stderr: string;\n exitCode: number;\n}\n\n/**\n * Execute a command and capture output.\n */\nasync function executeCommand(command: string, ctx: ExecutionContext): Promise<CommandResult> {\n return new Promise((resolve, reject) => {\n const proc = spawn(command, {\n shell: true,\n cwd: ctx.cwd,\n env: ctx.env as NodeJS.ProcessEnv,\n // Pipe both to capture\n stdio: ['ignore', 'pipe', 'pipe'],\n });\n\n const combinedChunks: { data: Buffer; type: 'stdout' | 'stderr' }[] = [];\n const stdoutChunks: Buffer[] = [];\n const stderrChunks: Buffer[] = [];\n\n // Capture data as it comes in to preserve order\n proc.stdout.on('data', (data: Buffer) => {\n combinedChunks.push({ data, type: 'stdout' });\n stdoutChunks.push(data);\n });\n proc.stderr.on('data', (data: Buffer) => {\n combinedChunks.push({ data, type: 'stderr' });\n stderrChunks.push(data);\n });\n\n const timeoutId = setTimeout(() => {\n if (proc.pid) {\n treeKill(proc.pid, 'SIGKILL');\n }\n reject(new Error(`Command timed out after ${ctx.timeout}ms`));\n }, ctx.timeout);\n\n proc.on('close', (code) => {\n clearTimeout(timeoutId);\n const output = Buffer.concat(combinedChunks.map((c) => c.data)).toString('utf-8');\n const stdout = Buffer.concat(stdoutChunks).toString('utf-8');\n const stderr = Buffer.concat(stderrChunks).toString('utf-8');\n resolve({\n output,\n stdout,\n stderr,\n exitCode: code ?? 0,\n });\n });\n\n proc.on('error', (err) => {\n clearTimeout(timeoutId);\n reject(err);\n });\n });\n}\n","import stripAnsi from 'strip-ansi';\n\n/**\n * Escape special regex characters in a string.\n */\nfunction escapeRegex(str: string): string {\n return str.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&');\n}\n\n// Marker prefix for patterns (uses Unicode private use chars that won't appear in normal output)\nconst MARKER = '\\uE000';\n\n/**\n * Convert expected output with elision patterns to a regex.\n *\n * Handles (matching trycmd):\n * - [..] — matches any characters on the same line (trycmd: [^\\n]*?)\n * - ... — matches zero or more complete lines (trycmd: \\n(([^\\n]*\\n)*)?)\n * - [EXE] — matches .exe on Windows, empty otherwise\n * - [ROOT] — replaced with test root directory (pre-processed)\n * - [CWD] — replaced with current working directory (pre-processed)\n * - Custom [NAME] patterns from config (trycmd: TestCases::insert_var)\n */\nfunction patternToRegex(\n expected: string,\n customPatterns: Record<string, string | RegExp> = {},\n): RegExp {\n // Build a map of markers to their regex replacements\n const replacements = new Map<string, string>();\n let markerIndex = 0;\n\n const getMarker = (): string => {\n return `${MARKER}${markerIndex++}${MARKER}`;\n };\n\n let processed = expected;\n\n // Replace [..] with marker\n const dotdotMarker = getMarker();\n replacements.set(dotdotMarker, '[^\\\\n]*');\n processed = processed.replaceAll('[..]', dotdotMarker);\n\n // Replace ... (followed by newline) with marker\n const ellipsisMarker = getMarker();\n replacements.set(ellipsisMarker, '(?:[^\\\\n]*\\\\n)*');\n processed = processed.replace(/\\.\\.\\.\\n/g, ellipsisMarker);\n\n // Replace [EXE] with marker\n const exeMarker = getMarker();\n const exe = process.platform === 'win32' ? '\\\\.exe' : '';\n replacements.set(exeMarker, exe);\n processed = processed.replaceAll('[EXE]', exeMarker);\n\n // Replace custom patterns with markers\n for (const [name, pattern] of Object.entries(customPatterns)) {\n const placeholder = `[${name}]`;\n const patternStr = pattern instanceof RegExp ? pattern.source : pattern;\n const marker = getMarker();\n replacements.set(marker, `(${patternStr})`);\n processed = processed.replaceAll(placeholder, marker);\n }\n\n // Escape special regex characters\n let regex = escapeRegex(processed);\n\n // Restore markers to their regex replacements\n for (const [marker, replacement] of replacements) {\n regex = regex.replaceAll(escapeRegex(marker), replacement);\n }\n\n // Match the entire string (dotall mode for . to match newlines if needed)\n return new RegExp(`^${regex}$`, 's');\n}\n\n/**\n * Pre-process expected output to replace path placeholders with actual paths.\n * This happens BEFORE pattern matching.\n */\nfunction preprocessPaths(expected: string, context: { root: string; cwd: string }): string {\n let result = expected;\n // Normalize paths for comparison (use forward slashes)\n const normalizedRoot = context.root.replace(/\\\\/g, '/');\n const normalizedCwd = context.cwd.replace(/\\\\/g, '/');\n result = result.replaceAll('[ROOT]', normalizedRoot);\n result = result.replaceAll('[CWD]', normalizedCwd);\n return result;\n}\n\n/**\n * Normalize actual output for comparison.\n * - Remove ANSI escape codes (colors, etc.)\n * - Normalize line endings to \\n\n * - Normalize paths (Windows backslashes to forward slashes)\n * - Trim trailing whitespace from lines\n * - Ensure single trailing newline\n */\nexport function normalizeOutput(output: string): string {\n // Remove ANSI escape codes first\n let normalized = stripAnsi(output);\n\n normalized = normalized\n .replace(/\\r\\n/g, '\\n')\n .replace(/\\r/g, '\\n')\n .split('\\n')\n .map((line) => line.trimEnd())\n .join('\\n')\n .replace(/\\n+$/, '\\n');\n\n // Handle empty output\n if (normalized === '\\n') {\n normalized = '';\n }\n\n return normalized;\n}\n\n/**\n * Check if actual output matches expected pattern.\n */\nexport function matchOutput(\n actual: string,\n expected: string,\n context: { root: string; cwd: string },\n customPatterns: Record<string, string | RegExp> = {},\n): boolean {\n const normalizedActual = normalizeOutput(actual);\n const normalizedExpected = normalizeOutput(expected);\n\n // Empty expected matches empty actual\n if (normalizedExpected === '' && normalizedActual === '') {\n return true;\n }\n\n const preprocessed = preprocessPaths(normalizedExpected, context);\n const regex = patternToRegex(preprocessed, customPatterns);\n return regex.test(normalizedActual);\n}\n","// Public API exports\n\n// Version constant (injected at build time)\ndeclare const __VERSION__: string;\nexport const VERSION: string = typeof __VERSION__ !== 'undefined' ? __VERSION__ : 'development';\n\n// Config helper\nexport { defineConfig } from './lib/config.js';\nexport type { TryscriptConfig } from './lib/config.js';\n\n// Types\nexport type {\n TestConfig,\n TestBlock,\n TestFile,\n TestBlockResult,\n TestFileResult,\n TestRunSummary,\n CoverageConfig,\n CoverageContext,\n} from './lib/types.js';\n\n// Core functions (for programmatic use)\nexport { parseTestFile } from './lib/parser.js';\nexport { runBlock, createExecutionContext, cleanupExecutionContext } from './lib/runner.js';\nexport type { ExecutionContext } from './lib/runner.js';\nexport { matchOutput, normalizeOutput } from './lib/matcher.js';\n"],"mappings":"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"}
|