From ba741fa3ecdd86cdb64bb3cc6a269ce34bb56d07 Mon Sep 17 00:00:00 2001 From: Andrew Bradley Date: Sun, 12 Feb 2023 21:48:08 -0500 Subject: [PATCH] Maintenance (#1956) * Get rid of tons of unnecessary destructuring in tests * control in-process concurrency w/env var formatting in testlib split `context` into `context` and `contextEach`; stop sprinkling `once` around `beforeAll` -> `before` `runIf` -> `if` `runSerially` -> `serial`, makes `serial` overloaded * extract testlib to git dep * extract getStream/expectStream to git dep * Replace bundled fs-fixture-builder with git dep * fix lockfile * try to make yarn happy grrr * test against node 20 nightly, since 19 seems not to be building every night * bump git deps * lockfile * try to fix install on node nightly * organize test helpers into multiple files, remove @yarnpkg/fslib dep, extract ctxTmpDir to its own context builder * fix bug in tests supposed to run *outside* of the git clone directory * fix transitive dep crash * fix fs-fixture-builder bug --- .github/workflows/continuous-integration.yml | 4 +- .gitignore | 1 + ava.config.cjs | 3 +- justfile | 6 + package.json | 7 +- scripts/node-nightly.sh | 27 + src/test/ci-node-and-ts-versions.spec.ts | 2 +- src/test/diagnostics.spec.ts | 34 +- src/test/esm-loader.spec.ts | 220 +++---- src/test/exec-helpers.ts | 26 +- src/test/fs-helpers.ts | 103 --- src/test/helpers.ts | 373 ----------- src/test/helpers/command-lines.ts | 12 + src/test/helpers/ctx-tmp-dir.ts | 39 ++ src/test/helpers/ctx-ts-node.ts | 98 +++ src/test/helpers/index.ts | 7 + src/test/helpers/misc.ts | 20 + src/test/helpers/paths.ts | 24 + src/test/helpers/reset-node-environment.ts | 109 ++++ src/test/helpers/version-checks.ts | 44 ++ src/test/index.spec.ts | 648 +++++++++---------- src/test/module-node/1778.spec.ts | 15 +- src/test/module-node/module-node.spec.ts | 22 +- src/test/pluggable-dep-resolution.spec.ts | 2 +- src/test/register.spec.ts | 18 +- src/test/regression.spec.ts | 8 +- src/test/repl/helpers.ts | 15 +- src/test/repl/repl-environment.spec.ts | 48 +- src/test/repl/repl.spec.ts | 222 +++---- src/test/resolver.spec.ts | 9 +- src/test/sourcemaps.spec.ts | 5 +- src/test/testlib.ts | 344 +--------- src/test/transpilers.spec.ts | 6 +- src/test/ts-import-specifiers.spec.ts | 13 +- yarn.lock | 95 ++- 35 files changed, 1044 insertions(+), 1585 deletions(-) create mode 100755 scripts/node-nightly.sh delete mode 100644 src/test/fs-helpers.ts delete mode 100644 src/test/helpers.ts create mode 100644 src/test/helpers/command-lines.ts create mode 100644 src/test/helpers/ctx-tmp-dir.ts create mode 100644 src/test/helpers/ctx-ts-node.ts create mode 100644 src/test/helpers/index.ts create mode 100644 src/test/helpers/misc.ts create mode 100644 src/test/helpers/paths.ts create mode 100644 src/test/helpers/reset-node-environment.ts create mode 100644 src/test/helpers/version-checks.ts diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml index 68cd3d47a..f54553da4 100644 --- a/.github/workflows/continuous-integration.yml +++ b/.github/workflows/continuous-integration.yml @@ -89,8 +89,8 @@ jobs: typescriptFlag: next # Node nightly - flavor: 5 - node: 19-nightly - nodeFlag: 19_nightly + node: 20-nightly + nodeFlag: 20_nightly typescript: latest typescriptFlag: latest steps: diff --git a/.gitignore b/.gitignore index ee612bb98..730f0ff3d 100644 --- a/.gitignore +++ b/.gitignore @@ -13,6 +13,7 @@ npm-debug.log /website/static/api /tsconfig.tsbuildinfo /temp +/tmp /yarn-error.log /.yarn/install-state.gz /tests/.yarn/install-state.gz diff --git a/ava.config.cjs b/ava.config.cjs index 4b9fef2d4..5532195d4 100644 --- a/ava.config.cjs +++ b/ava.config.cjs @@ -12,7 +12,8 @@ module.exports = { // This avoids passing it to spawned processes under test, which would negatively affect // their behavior. FORCE_COLOR: '3', - NODE_PATH: '' + NODE_PATH: '', + CONCURRENT_TESTS: '4' }, require: ['./src/test/remove-env-var-force-color.js'], nodeArguments: ['--loader', './src/test/test-loader.mjs', '--no-warnings'], diff --git a/justfile b/justfile index 5c70d933c..4ed6cdf8b 100644 --- a/justfile +++ b/justfile @@ -21,6 +21,12 @@ regenerate: install: yarn +yarn *ARGS: + yarn "$@" + +node *ARGS: + node "$@" + # EVERYTHING BELOW THIS LINE IS AUTO-GENERATED FROM PACKAGE.JSON # DO NOT MODIFY BY HAND diff --git a/package.json b/package.json index 03a10b370..9206ee289 100644 --- a/package.json +++ b/package.json @@ -108,6 +108,9 @@ }, "homepage": "https://typestrong.org/ts-node", "devDependencies": { + "@TypeStrong/fs-fixture-builder": "github:TypeStrong/fs-fixture-builder#8abd1494280116ff5318dde2c50ad01e1663790c", + "@cspotcode/ava-lib": "github:cspotcode/ava-lib#edcc1885d192d08d5af83490af9341468402ec41", + "@cspotcode/expect-stream": "github:cspotcode/node-expect-stream#4e425ff1eef240003af8716291e80fbaf3e3ae8f", "@microsoft/api-extractor": "^7.19.4", "@swc/core": ">=1.3.32", "@swc/wasm": ">=1.3.32", @@ -119,12 +122,10 @@ "@types/react": "^16.14.19", "@types/rimraf": "^3.0.0", "@types/semver": "^7.1.0", - "@yarnpkg/fslib": "^2.4.0", "ava": "^5.1.1", "axios": "^0.21.1", "dprint": "^0.25.0", "expect": "27.0.2", - "get-stream": "^6.0.0", "lodash": "^4.17.15", "nyc": "^15.0.1", "outdent": "^0.8.0", @@ -135,7 +136,7 @@ "throat": "^6.0.1", "typedoc": "^0.22.10", "typescript": "4.7.4", - "typescript-json-schema": "^0.53.0" + "typescript-json-schema": "^0.54.0" }, "peerDependencies": { "@swc/core": ">=1.2.50", diff --git a/scripts/node-nightly.sh b/scripts/node-nightly.sh new file mode 100755 index 000000000..6f37dce40 --- /dev/null +++ b/scripts/node-nightly.sh @@ -0,0 +1,27 @@ +#!/usr/bin/env bash + +# Hacky script to grab node nightly and plonk it into node_modules/.bin, for +# locally testing against nightly builds. + +set -euo pipefail +shopt -s inherit_errexit +__dirname="$(CDPATH= cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +cd "$__dirname/.." + +set -x + +mkdir -p tmp + +version=$(curl https://nodejs.org/download/nightly/index.json | jq -r '.[0].version') +[ -e "tmp/$version.tar.xz" ] || \ + curl -o "tmp/$version.tar.xz" "https://nodejs.org/download/nightly/$version/node-$version-linux-x64.tar.xz" + +[ -e "tmp/$version.tar" ] || \ + unxz "tmp/$version.tar.xz" + +{ + cd tmp + tar -xvf "$version.tar" +} + +ln -s "../../tmp/node-$version-linux-x64/bin/node" ./node_modules/.bin/node diff --git a/src/test/ci-node-and-ts-versions.spec.ts b/src/test/ci-node-and-ts-versions.spec.ts index 5fc4cb2aa..c819a2e4a 100644 --- a/src/test/ci-node-and-ts-versions.spec.ts +++ b/src/test/ci-node-and-ts-versions.spec.ts @@ -8,7 +8,7 @@ import { context, expect } from './testlib'; const test = context(ctxTsNode); test.suite('Confirm node and typescript versions on CI', (test) => { - test.runIf(!!process.env.CI); + test.if(!!process.env.CI); test('node version is correct', async (t) => { const expectedVersion = process.env.TEST_MATRIX_NODE_VERSION!; const actualVersion = process.versions.node; diff --git a/src/test/diagnostics.spec.ts b/src/test/diagnostics.spec.ts index 4fea00c3e..cdb0423cf 100644 --- a/src/test/diagnostics.spec.ts +++ b/src/test/diagnostics.spec.ts @@ -6,24 +6,22 @@ import { once } from 'lodash'; const test = context(ctxTsNode); test.suite('TSError diagnostics', ({ context }) => { - const test = context( - once(async (t) => { - // Locking to es2020, because: - // 1) es2022 -- default in @tsconfig/bases for node18 -- changes this diagnostic - // to be a composite "No overload matches this call." - // 2) TS 4.2 doesn't support es2021 or higher - const service = t.context.tsNodeUnderTest.create({ - compilerOptions: { target: 'es5', lib: ['es2020'] }, - skipProject: true, - }); - try { - service.compile('new Error(123)', 'test.ts'); - } catch (err) { - return { err: err as TSError }; - } - return { err: undefined }; - }) - ); + const test = context(async (t) => { + // Locking to es2020, because: + // 1) es2022 -- default in @tsconfig/bases for node18 -- changes this diagnostic + // to be a composite "No overload matches this call." + // 2) TS 4.2 doesn't support es2021 or higher + const service = t.context.tsNodeUnderTest.create({ + compilerOptions: { target: 'es5', lib: ['es2020'] }, + skipProject: true, + }); + try { + service.compile('new Error(123)', 'test.ts'); + } catch (err) { + return { err: err as TSError }; + } + return { err: undefined }; + }); const diagnosticCode = 2345; const diagnosticMessage = diff --git a/src/test/esm-loader.spec.ts b/src/test/esm-loader.spec.ts index 40c3a0b9d..b35788df1 100644 --- a/src/test/esm-loader.spec.ts +++ b/src/test/esm-loader.spec.ts @@ -37,27 +37,21 @@ const spawn = createSpawn({ test.suite('esm', (test) => { test('should compile and execute as ESM', async () => { - const { err, stdout } = await exec( - `${CMD_ESM_LOADER_WITHOUT_PROJECT} index.ts`, - { - cwd: join(TEST_DIR, './esm'), - } - ); - expect(err).toBe(null); - expect(stdout).toBe('foo bar baz biff libfoo\n'); + const r = await exec(`${CMD_ESM_LOADER_WITHOUT_PROJECT} index.ts`, { + cwd: join(TEST_DIR, './esm'), + }); + expect(r.err).toBe(null); + expect(r.stdout).toBe('foo bar baz biff libfoo\n'); }); test('should use source maps', async (t) => { - const { err, stdout, stderr } = await exec( - `${CMD_ESM_LOADER_WITHOUT_PROJECT} "throw error.ts"`, - { - cwd: join(TEST_DIR, './esm'), - } - ); - expect(err).not.toBe(null); + const r = await exec(`${CMD_ESM_LOADER_WITHOUT_PROJECT} "throw error.ts"`, { + cwd: join(TEST_DIR, './esm'), + }); + expect(r.err).not.toBe(null); const expectedModuleUrl = pathToFileURL( join(TEST_DIR, './esm/throw error.ts') ).toString(); - expect(err!.message).toMatch( + expect(r.err!.message).toMatch( [ `${expectedModuleUrl}:100`, " bar() { throw new Error('this is a demo'); }", @@ -70,100 +64,85 @@ test.suite('esm', (test) => { test.suite('supports experimental-specifier-resolution=node', (test) => { test('via --experimental-specifier-resolution', async () => { - const { err, stdout } = await exec( + const r = await exec( `${CMD_ESM_LOADER_WITHOUT_PROJECT} --experimental-specifier-resolution=node index.ts`, { cwd: join(TEST_DIR, './esm-node-resolver') } ); - expect(err).toBe(null); - expect(stdout).toBe('foo bar baz biff libfoo\n'); + expect(r.err).toBe(null); + expect(r.stdout).toBe('foo bar baz biff libfoo\n'); }); test('via NODE_OPTIONS', async () => { - const { err, stdout } = await exec( - `${CMD_ESM_LOADER_WITHOUT_PROJECT} index.ts`, - { - cwd: join(TEST_DIR, './esm-node-resolver'), - env: { - ...process.env, - NODE_OPTIONS: `--experimental-specifier-resolution=node`, - }, - } - ); - expect(err).toBe(null); - expect(stdout).toBe('foo bar baz biff libfoo\n'); + const r = await exec(`${CMD_ESM_LOADER_WITHOUT_PROJECT} index.ts`, { + cwd: join(TEST_DIR, './esm-node-resolver'), + env: { + ...process.env, + NODE_OPTIONS: `--experimental-specifier-resolution=node`, + }, + }); + expect(r.err).toBe(null); + expect(r.stdout).toBe('foo bar baz biff libfoo\n'); }); }); test('throws ERR_REQUIRE_ESM when attempting to require() an ESM script when ESM loader is enabled', async () => { - const { err, stderr } = await exec( - `${CMD_ESM_LOADER_WITHOUT_PROJECT} ./index.js`, - { - cwd: join(TEST_DIR, './esm-err-require-esm'), - } - ); - expect(err).not.toBe(null); - expect(stderr).toMatch( + const r = await exec(`${CMD_ESM_LOADER_WITHOUT_PROJECT} ./index.js`, { + cwd: join(TEST_DIR, './esm-err-require-esm'), + }); + expect(r.err).not.toBe(null); + expect(r.stderr).toMatch( 'Error [ERR_REQUIRE_ESM]: Must use import to load ES Module:' ); }); test('defers to fallback loaders when URL should not be handled by ts-node', async () => { - const { err, stdout, stderr } = await exec( - `${CMD_ESM_LOADER_WITHOUT_PROJECT} index.mjs`, - { - cwd: join(TEST_DIR, './esm-import-http-url'), - } - ); - expect(err).not.toBe(null); + const r = await exec(`${CMD_ESM_LOADER_WITHOUT_PROJECT} index.mjs`, { + cwd: join(TEST_DIR, './esm-import-http-url'), + }); + expect(r.err).not.toBe(null); // expect error from node's default resolver - expect(stderr).toMatch( + expect(r.stderr).toMatch( /Error \[ERR_UNSUPPORTED_ESM_URL_SCHEME\]:.*(?:\n.*){0,2}\n *at defaultResolve/ ); }); test('should bypass import cache when changing search params', async () => { - const { err, stdout } = await exec( - `${CMD_ESM_LOADER_WITHOUT_PROJECT} index.ts`, - { - cwd: join(TEST_DIR, './esm-import-cache'), - } - ); - expect(err).toBe(null); - expect(stdout).toBe('log1\nlog2\nlog2\n'); + const r = await exec(`${CMD_ESM_LOADER_WITHOUT_PROJECT} index.ts`, { + cwd: join(TEST_DIR, './esm-import-cache'), + }); + expect(r.err).toBe(null); + expect(r.stdout).toBe('log1\nlog2\nlog2\n'); }); test('should support transpile only mode via dedicated loader entrypoint', async () => { - const { err, stdout } = await exec( + const r = await exec( `${CMD_ESM_LOADER_WITHOUT_PROJECT}/transpile-only index.ts`, { cwd: join(TEST_DIR, './esm-transpile-only'), } ); - expect(err).toBe(null); - expect(stdout).toBe(''); + expect(r.err).toBe(null); + expect(r.stdout).toBe(''); }); test('should throw type errors without transpile-only enabled', async () => { - const { err, stdout } = await exec( - `${CMD_ESM_LOADER_WITHOUT_PROJECT} index.ts`, - { - cwd: join(TEST_DIR, './esm-transpile-only'), - } - ); - if (err === null) { + const r = await exec(`${CMD_ESM_LOADER_WITHOUT_PROJECT} index.ts`, { + cwd: join(TEST_DIR, './esm-transpile-only'), + }); + if (r.err === null) { throw new Error('Command was expected to fail, but it succeeded.'); } - expect(err.message).toMatch('Unable to compile TypeScript'); - expect(err.message).toMatch( + expect(r.err.message).toMatch('Unable to compile TypeScript'); + expect(r.err.message).toMatch( new RegExp( "TS2345: Argument of type '(?:number|1101)' is not assignable to parameter of type 'string'\\." ) ); - expect(err.message).toMatch( + expect(r.err.message).toMatch( new RegExp( "TS2322: Type '(?:\"hello world\"|string)' is not assignable to type 'number'\\." ) ); - expect(stdout).toBe(''); + expect(r.stdout).toBe(''); }); test.suite('moduleTypes', (test) => { @@ -174,11 +153,11 @@ test.suite('esm', (test) => { test('supports CJS webpack.config.ts in an otherwise ESM project', async (t) => { // A notable case where you can use ts-node's CommonJS loader, not the ESM loader, in an ESM project: // when loading a webpack.config.ts or similar config - const { err, stdout } = await exec( + const r = await exec( `${CMD_TS_NODE_WITHOUT_PROJECT_FLAG} --project ./module-types/override-to-cjs/${tsconfig} ./module-types/override-to-cjs/test-webpack-config.cjs` ); - expect(err).toBe(null); - expect(stdout).toBe(``); + expect(r.err).toBe(null); + expect(r.stdout).toBe(``); }); test('should allow importing CJS in an otherwise ESM project', async (t) => { await run('override-to-cjs', tsconfig, 'cjs'); @@ -194,7 +173,7 @@ test.suite('esm', (test) => { }); } async function run(project: string, config: string, ext: string) { - const { err, stderr, stdout } = await exec( + const r = await exec( `${CMD_ESM_LOADER_WITHOUT_PROJECT} ./module-types/${project}/test.${ext}`, { env: { @@ -203,27 +182,27 @@ test.suite('esm', (test) => { }, } ); - expect(err).toBe(null); - expect(stdout).toBe(`Failures: 0\n`); + expect(r.err).toBe(null); + expect(r.stdout).toBe(`Failures: 0\n`); } }); test.suite('createEsmHooks()', (test) => { test('should create proper hooks with provided instance', async () => { - const { err } = await exec(`node --loader ./loader.mjs index.ts`, { + const r = await exec(`node --loader ./loader.mjs index.ts`, { cwd: join(TEST_DIR, './esm-custom-loader'), }); - if (err === null) { + if (r.err === null) { throw new Error('Command was expected to fail, but it succeeded.'); } - expect(err.message).toMatch(/TS6133:\s+'unusedVar'/); + expect(r.err.message).toMatch(/TS6133:\s+'unusedVar'/); }); }); - test.suite('unit test hooks', ({ context }) => { - const test = context(async (t) => { + test.suite('unit test hooks', ({ contextEach }) => { + const test = contextEach(async (t) => { const service = t.context.tsNodeUnderTest.create({ cwd: TEST_DIR, }); @@ -237,7 +216,7 @@ test.suite('esm', (test) => { }); test.suite('data URIs', (test) => { - test.runIf(nodeUsesNewHooksApi); + test.if(nodeUsesNewHooksApi); test('Correctly determines format of data URIs', async (t) => { const { hooks } = t.context; @@ -255,17 +234,17 @@ test.suite('esm', (test) => { }); test.suite('supports import assertions', (test) => { - test.runIf(nodeSupportsImportAssertions && tsSupportsImportAssertions); + test.if(nodeSupportsImportAssertions && tsSupportsImportAssertions); const macro = test.macro((flags: string) => async (t) => { - const { err, stdout } = await exec( + const r = await exec( `${CMD_ESM_LOADER_WITHOUT_PROJECT} ${flags} ./importJson.ts`, { cwd: resolve(TEST_DIR, 'esm-import-assertions'), } ); - expect(err).toBe(null); - expect(stdout.trim()).toBe( + expect(r.err).toBe(null); + expect(r.stdout.trim()).toBe( 'A fuchsia car has 2 seats and the doors are open.\nDone!' ); }); @@ -273,12 +252,12 @@ test.suite('esm', (test) => { test.suite( 'when node does not require --experimental-json-modules', (test) => { - test.runIf(nodeSupportsUnflaggedJsonImports); + test.if(nodeSupportsUnflaggedJsonImports); test('Can import JSON modules with appropriate assertion', macro, ''); } ); test.suite('when node requires --experimental-json-modules', (test) => { - test.runIf(!nodeSupportsUnflaggedJsonImports); + test.if(!nodeSupportsUnflaggedJsonImports); test( 'Can import JSON using the appropriate flag and assertion', macro, @@ -291,25 +270,25 @@ test.suite('esm', (test) => { 'Entrypoint resolution falls back to CommonJS resolver and format', (test) => { test('extensionless entrypoint', async (t) => { - const { err, stdout } = await exec( + const r = await exec( `${CMD_ESM_LOADER_WITHOUT_PROJECT} ./esm-loader-entrypoint-cjs-fallback/extensionless-entrypoint` ); - expect(err).toBe(null); - expect(stdout.trim()).toBe('Hello world!'); + expect(r.err).toBe(null); + expect(r.stdout.trim()).toBe('Hello world!'); }); test('relies upon CommonJS resolution', async (t) => { - const { err, stdout } = await exec( + const r = await exec( `${CMD_ESM_LOADER_WITHOUT_PROJECT} ./esm-loader-entrypoint-cjs-fallback/relies-upon-cjs-resolution` ); - expect(err).toBe(null); - expect(stdout.trim()).toBe('Hello world!'); + expect(r.err).toBe(null); + expect(r.stdout.trim()).toBe('Hello world!'); }); test('fails as expected when entrypoint does not exist at all', async (t) => { - const { err, stderr } = await exec( + const r = await exec( `${CMD_ESM_LOADER_WITHOUT_PROJECT} ./esm-loader-entrypoint-cjs-fallback/does-not-exist` ); - expect(err).toBeDefined(); - expect(stderr).toContain(`Cannot find module `); + expect(r.err).toBeDefined(); + expect(r.stderr).toContain(`Cannot find module `); }); } ); @@ -329,19 +308,19 @@ test.suite('esm', (test) => { function basic(title: string, cb: () => ExecReturn) { test(title, async (t) => { - const { err, stdout, stderr } = await cb(); - expect(err).toBe(null); - expect(stdout.trim()).toBe('CLI args: foo bar'); - expect(stderr).toBe(''); + const r = await cb(); + expect(r.err).toBe(null); + expect(r.stdout.trim()).toBe('CLI args: foo bar'); + expect(r.stderr).toBe(''); }); } test('extensionless entrypoint, regression test for #1943', async (t) => { - const { err, stdout } = await exec( + const r = await exec( `${BIN_ESM_PATH} ./esm-loader-entrypoint-cjs-fallback/extensionless-entrypoint` ); - expect(err).toBe(null); - expect(stdout.trim()).toBe('Hello world!'); + expect(r.err).toBe(null); + expect(r.stdout.trim()).toBe('Hello world!'); }); test.suite('parent passes signals to child', (test) => { @@ -388,26 +367,23 @@ test.suite('esm', (test) => { test.suite('esm child process working directory', (test) => { test('should have the correct working directory in the user entry-point', async () => { - const { err, stdout, stderr } = await exec( - `${BIN_PATH} --esm --cwd ./esm/ index.ts`, - { - cwd: resolve(TEST_DIR, 'working-dir'), - } - ); + const r = await exec(`${BIN_PATH} --esm --cwd ./esm/ index.ts`, { + cwd: resolve(TEST_DIR, 'working-dir'), + }); - expect(err).toBe(null); - expect(stdout.trim()).toBe('Passing'); - expect(stderr).toBe(''); + expect(r.err).toBe(null); + expect(r.stdout.trim()).toBe('Passing'); + expect(r.stderr).toBe(''); }); }); test.suite('esm child process and forking', (test) => { const macro = test.macro((command: string) => async (t) => { - const { err, stdout, stderr } = await exec(command); + const r = await exec(command); - expect(err).toBe(null); - expect(stdout.trim()).toBe('Passing: from main'); - expect(stderr).toBe(''); + expect(r.err).toBe(null); + expect(r.stdout.trim()).toBe('Passing: from main'); + expect(r.stderr).toBe(''); }); test( @@ -430,11 +406,11 @@ test.suite('esm', (test) => { test('throws ERR_REQUIRE_ESM when attempting to require() an ESM script when ESM loader is *not* enabled', async () => { // Node versions >= 12 support package.json "type" field and so will throw an error when attempting to load ESM as CJS - const { err, stderr } = await exec(`${BIN_PATH} ./index.js`, { + const r = await exec(`${BIN_PATH} ./index.js`, { cwd: join(TEST_DIR, './esm-err-require-esm'), }); - expect(err).not.toBe(null); - expect(stderr).toMatch( + expect(r.err).not.toBe(null); + expect(r.stderr).toMatch( 'Error [ERR_REQUIRE_ESM]: Must use import to load ES Module:' ); }); @@ -442,7 +418,7 @@ test.suite('esm', (test) => { test.suite("Catch unexpected changes to node's loader context", (test) => { // loader context includes import assertions, therefore this test requires support for import assertions - test.runIf(nodeSupportsImportAssertions); + test.if(nodeSupportsImportAssertions); /* * This does not test ts-node. @@ -453,10 +429,10 @@ test.suite("Catch unexpected changes to node's loader context", (test) => { * modifying them, or suppressing them. */ test('Ensure context passed to loader by node has only expected properties', async (t) => { - const { stdout, stderr } = await exec( + const r = await exec( `node --loader ./esm-loader-context/loader.mjs --experimental-json-modules ./esm-loader-context/index.mjs` ); - const rows = stdout.split('\n').filter((v) => v[0] === '{'); + const rows = r.stdout.split('\n').filter((v) => v[0] === '{'); expect(rows.length).toBe(14); rows.forEach((row) => { const json = JSON.parse(row) as { diff --git a/src/test/exec-helpers.ts b/src/test/exec-helpers.ts index 1e420a908..57ef77251 100644 --- a/src/test/exec-helpers.ts +++ b/src/test/exec-helpers.ts @@ -8,7 +8,7 @@ import { exec as childProcessExec, spawn as childProcessSpawn, } from 'child_process'; -import { GetStream, getStream } from './helpers'; +import { ExpectStream, expectStream } from '@cspotcode/expect-stream'; import { expect } from './testlib'; export type ExecReturn = Promise & { child: ChildProcess }; @@ -55,8 +55,8 @@ export function createExec>( export type SpawnReturn = Promise & SpawnResult; export interface SpawnResult { - stdout: GetStream; - stderr: GetStream; + stdout: ExpectStream; + stderr: ExpectStream; code: number | null; child: ChildProcess; } @@ -77,16 +77,16 @@ export function createSpawn>( Partial> ): SpawnReturn { let child!: ChildProcess; - let stdout!: GetStream; - let stderr!: GetStream; + let stdout!: ExpectStream; + let stderr!: ExpectStream; const promise = Object.assign( new Promise((resolve, reject) => { child = childProcessSpawn(cmd[0], cmd.slice(1), { ...preBoundOptions, ...opts, }); - stdout = getStream(child.stdout!); - stderr = getStream(child.stderr!); + stdout = expectStream(child.stdout!); + stderr = expectStream(child.stderr!); child.on('exit', (code) => { promise.code = code; resolve({ stdout, stderr, code, child }); @@ -143,18 +143,18 @@ export function createExecTester>( ...preBoundOptions, ...options, }; - const execPromise = exec(`${cmd} ${flags}`, { + const p = exec(`${cmd} ${flags}`, { env: { ...process.env, ...env }, }); if (stdin !== undefined) { - execPromise.child.stdin!.end(stdin); + p.child.stdin!.end(stdin); } - const { err, stdout, stderr } = await execPromise; + const r = await p; if (expectError) { - expect(err).toBeDefined(); + expect(r.err).toBeDefined(); } else { - expect(err).toBeNull(); + expect(r.err).toBeNull(); } - return { stdout, stderr, err }; + return r; }; } diff --git a/src/test/fs-helpers.ts b/src/test/fs-helpers.ts deleted file mode 100644 index e34d2d816..000000000 --- a/src/test/fs-helpers.ts +++ /dev/null @@ -1,103 +0,0 @@ -import { TEST_DIR } from './helpers'; -import * as fs from 'fs'; -import * as Path from 'path'; - -// Helpers to describe a bunch of files in a project programmatically, -// then write them to disk in a temp directory. - -export interface File { - path: string; - content: string; -} -export interface JsonFile extends File { - obj: T; -} -export interface DirectoryApi { - add(file: File): File; - addFile(...args: Parameters): File; - addJsonFile(...args: Parameters): JsonFile; - dir(dirPath: string, cb?: (dir: DirectoryApi) => void): DirectoryApi; -} - -export type ProjectAPI = ReturnType; - -export function file(path: string, content = '') { - return { path, content }; -} -export function jsonFile(path: string, obj: T) { - const file: JsonFile = { - path, - obj, - get content() { - return JSON.stringify(obj, null, 2); - }, - }; - return file; -} - -export function tempdirProject(name = '') { - const rootTmpDir = `${TEST_DIR}/tmp/`; - fs.mkdirSync(rootTmpDir, { recursive: true }); - const tmpdir = fs.mkdtempSync(`${TEST_DIR}/tmp/${name}`); - return projectInternal(tmpdir); -} - -export type Project = ReturnType; -export function project(name: string) { - return projectInternal(`${TEST_DIR}/tmp/${name}`); -} - -function projectInternal(cwd: string) { - const files: File[] = []; - function write() { - for (const file of files) { - fs.mkdirSync(Path.dirname(file.path), { recursive: true }); - fs.writeFileSync(file.path, file.content); - } - } - function rm() { - try { - fs.rmdirSync(cwd, { recursive: true }); - } catch (err) { - if (fs.existsSync(cwd)) throw err; - } - } - const { add, addFile, addJsonFile, dir } = createDirectory(cwd); - function createDirectory( - dirPath: string, - cb?: (dir: DirectoryApi) => void - ): DirectoryApi { - function add(file: File) { - file.path = Path.join(dirPath, file.path); - files.push(file); - return file; - } - function addFile(...args: Parameters) { - return add(file(...args)); - } - function addJsonFile(...args: Parameters) { - return add(jsonFile(...args)) as JsonFile; - } - function dir(path: string, cb?: (dir: DirectoryApi) => void) { - return createDirectory(Path.join(dirPath, path), cb); - } - const _dir: DirectoryApi = { - add, - addFile, - addJsonFile, - dir, - }; - cb?.(_dir); - return _dir; - } - return { - cwd, - files: [], - dir, - add, - addFile, - addJsonFile, - write, - rm, - }; -} diff --git a/src/test/helpers.ts b/src/test/helpers.ts deleted file mode 100644 index 28afe43eb..000000000 --- a/src/test/helpers.ts +++ /dev/null @@ -1,373 +0,0 @@ -import { NodeFS } from '@yarnpkg/fslib'; -import { exec as childProcessExec } from 'child_process'; -import { promisify } from 'util'; -import { sync as rimrafSync } from 'rimraf'; -import { existsSync, readFileSync, unlinkSync, writeFileSync } from 'fs'; -import { join, resolve } from 'path'; -import * as fs from 'fs'; -import { lock } from 'proper-lockfile'; -import type { Readable } from 'stream'; -/** - * types from ts-node under test - */ -import type * as tsNodeTypes from '../index'; -import type _createRequire from 'create-require'; -import { has, mapValues, once, sortBy } from 'lodash'; -import semver = require('semver'); -import type { ExecutionContext } from './testlib'; -const createRequire: typeof _createRequire = require('create-require'); -export { tsNodeTypes }; - -//#region Paths -export const ROOT_DIR = resolve(__dirname, '../..'); -export const DIST_DIR = resolve(__dirname, '..'); -export const TEST_DIR = join(__dirname, '../../tests'); -export const PROJECT = join(TEST_DIR, 'tsconfig.json'); -export const PROJECT_TRANSPILE_ONLY = join( - TEST_DIR, - 'tsconfig-transpile-only.json' -); -export const BIN_PATH = join(TEST_DIR, 'node_modules/.bin/ts-node'); -export const BIN_PATH_JS = join(TEST_DIR, 'node_modules/ts-node/dist/bin.js'); -export const BIN_SCRIPT_PATH = join( - TEST_DIR, - 'node_modules/.bin/ts-node-script' -); -export const BIN_CWD_PATH = join(TEST_DIR, 'node_modules/.bin/ts-node-cwd'); -export const BIN_ESM_PATH = join(TEST_DIR, 'node_modules/.bin/ts-node-esm'); - -process.chdir(TEST_DIR); -//#endregion - -//#region command lines -/** Default `ts-node --project` invocation */ -export const CMD_TS_NODE_WITH_PROJECT_FLAG = `"${BIN_PATH}" --project "${PROJECT}"`; -/** Default `ts-node --project` invocation with transpile-only */ -export const CMD_TS_NODE_WITH_PROJECT_TRANSPILE_ONLY_FLAG = `"${BIN_PATH}" --project "${PROJECT_TRANSPILE_ONLY}"`; -/** Default `ts-node` invocation without `--project` */ -export const CMD_TS_NODE_WITHOUT_PROJECT_FLAG = `"${BIN_PATH}"`; -export const CMD_ESM_LOADER_WITHOUT_PROJECT = `node --loader ts-node/esm`; -//#endregion - -// `createRequire` does not exist on older node versions -export const testsDirRequire = createRequire(join(TEST_DIR, 'index.js')); - -export const ts = testsDirRequire('typescript') as typeof import('typescript'); - -//#region version checks -export const nodeUsesNewHooksApi = semver.gte(process.version, '16.12.0'); -// 16.14.0: https://github.com/nodejs/node/blob/main/doc/changelogs/CHANGELOG_V16.md#notable-changes-4 -// 17.1.0: https://github.com/nodejs/node/blob/main/doc/changelogs/CHANGELOG_V17.md#2021-11-09-version-1710-current-targos -export const nodeSupportsImportAssertions = - (semver.gte(process.version, '16.14.0') && - semver.lt(process.version, '17.0.0')) || - semver.gte(process.version, '17.1.0'); -// These versions do not require `--experimental-json-modules` -// 16.15.0: https://github.com/nodejs/node/blob/main/doc/changelogs/CHANGELOG_V16.md#2022-04-26-version-16150-gallium-lts-danielleadams -// 17.5.0: https://github.com/nodejs/node/blob/main/doc/changelogs/CHANGELOG_V17.md#2022-02-10-version-1750-current-ruyadorno -export const nodeSupportsUnflaggedJsonImports = - (semver.gte(process.version, '16.15.0') && - semver.lt(process.version, '17.0.0')) || - semver.gte(process.version, '17.5.0'); -// Node 14.13.0 has a bug where it tries to lex CJS files to discover named exports *before* -// we transform the code. -// In other words, it tries to parse raw TS as CJS and balks at `export const foo =`, expecting to see `exports.foo =` -// This lexing only happens when CJS TS is imported from the ESM loader. -export const nodeSupportsImportingTransformedCjsFromEsm = semver.gte( - process.version, - '14.13.1' -); -/** Supports module:nodenext and module:node16 as *stable* features */ -export const tsSupportsStableNodeNextNode16 = - ts.version.startsWith('4.7.') || semver.gte(ts.version, '4.7.0'); -// TS 4.5 is first version to understand .cts, .mts, .cjs, and .mjs extensions -export const tsSupportsMtsCtsExtensions = semver.gte(ts.version, '4.5.0'); -export const tsSupportsImportAssertions = semver.gte(ts.version, '4.5.0'); -// TS 4.1 added jsx=react-jsx and react-jsxdev: https://devblogs.microsoft.com/typescript/announcing-typescript-4-1/#react-17-jsx-factories -export const tsSupportsReact17JsxFactories = semver.gte(ts.version, '4.1.0'); -// TS 5.0 added "allowImportingTsExtensions" -export const tsSupportsAllowImportingTsExtensions = semver.gte( - ts.version, - '4.999.999' -); -// Relevant when @tsconfig/bases refers to es2021 and we run tests against -// old TS versions. -export const tsSupportsEs2021 = semver.gte(ts.version, '4.3.0'); -export const tsSupportsEs2022 = semver.gte(ts.version, '4.6.0'); -//#endregion - -export const xfs = new NodeFS(fs); - -/** Pass to `test.context()` to get access to the ts-node API under test */ -export const ctxTsNode = once(async () => { - await installTsNode(); - const tsNodeUnderTest: typeof tsNodeTypes = testsDirRequire('ts-node'); - return { - tsNodeUnderTest, - }; -}); -export namespace ctxTsNode { - export type Ctx = Awaited>; - export type T = ExecutionContext; -} - -//#region install ts-node tarball -const ts_node_install_lock = process.env.ts_node_install_lock as string; -const lockPath = join(__dirname, ts_node_install_lock); - -interface InstallationResult { - error: string | null; -} - -/** - * Pack and install ts-node locally, necessary to test package "exports" - * FS locking b/c tests run in separate processes - */ -export async function installTsNode() { - await lockedMemoizedOperation(lockPath, async () => { - const totalTries = process.platform === 'win32' ? 5 : 1; - let tries = 0; - while (true) { - try { - rimrafSync(join(TEST_DIR, '.yarn/cache/ts-node-file-*')); - const result = await promisify(childProcessExec)( - `yarn --no-immutable`, - { - cwd: TEST_DIR, - } - ); - // You can uncomment this to aid debugging - // console.log(result.stdout, result.stderr); - rimrafSync(join(TEST_DIR, '.yarn/cache/ts-node-file-*')); - writeFileSync(join(TEST_DIR, 'yarn.lock'), ''); - break; - } catch (e) { - tries++; - if (tries >= totalTries) throw e; - } - } - }); -} - -/** - * Attempt an operation once across multiple processes, using filesystem locking. - * If it was executed already by another process, and it errored, throw the same error message. - */ -async function lockedMemoizedOperation( - lockPath: string, - operation: () => Promise -) { - const releaseLock = await lock(lockPath, { - realpath: false, - stale: 120e3, - retries: { - retries: 120, - maxTimeout: 1000, - }, - }); - try { - const operationHappened = existsSync(lockPath); - if (operationHappened) { - const result: InstallationResult = JSON.parse( - readFileSync(lockPath, 'utf8') - ); - if (result.error) throw result.error; - } else { - const result: InstallationResult = { error: null }; - try { - await operation(); - } catch (e) { - result.error = `${e}`; - throw e; - } finally { - writeFileSync(lockPath, JSON.stringify(result)); - } - } - } finally { - releaseLock(); - } -} -//#endregion - -export type GetStream = ReturnType; -/** - * Get a stream into a string. - * Wait for the stream to end, or wait till some pattern appears in the stream. - */ -export function getStream(stream: Readable) { - let resolve: (value: string) => void; - const promise = new Promise((res) => { - resolve = res; - }); - const received: Buffer[] = []; - let combinedBuffer: Buffer = Buffer.concat([]); - let combinedString: string = ''; - let waitForStart = 0; - - stream.on('data', (data) => { - received.push(data); - combine(); - }); - stream.on('end', () => { - resolve(combinedString); - }); - - return Object.assign(promise, { - get, - wait, - stream, - }); - - function get() { - return combinedString; - } - - function wait(pattern: string | RegExp, required = false) { - return new Promise((resolve, reject) => { - const start = waitForStart; - stream.on('checkWaitFor', checkWaitFor); - stream.on('end', endOrTimeout); - checkWaitFor(); - - function checkWaitFor() { - if (typeof pattern === 'string') { - const index = combinedString.indexOf(pattern, start); - if (index >= 0) { - waitForStart = index + pattern.length; - resolve(combinedString.slice(index, waitForStart)); - } - } else if (pattern instanceof RegExp) { - const match = combinedString.slice(start).match(pattern); - if (match != null) { - waitForStart = start + match.index!; - resolve(match[0]); - } - } - } - - function endOrTimeout() { - required ? reject() : resolve(undefined); - } - }); - } - - function combine() { - combinedBuffer = Buffer.concat(received); - combinedString = combinedBuffer.toString('utf8'); - stream.emit('checkWaitFor'); - } -} - -//#region Reset node environment - -const defaultRequireExtensions = captureObjectState(require.extensions); -// Avoid node deprecation warning for accessing _channel -const defaultProcess = captureObjectState(process, ['_channel']); -const defaultModule = captureObjectState(require('module')); -const defaultError = captureObjectState(Error); -const defaultGlobal = captureObjectState(global); - -/** - * Undo all of ts-node & co's installed hooks, resetting the node environment to default - * so we can run multiple test cases which `.register()` ts-node. - * - * Must also play nice with `nyc`'s environmental mutations. - */ -export function resetNodeEnvironment() { - const sms = - require('@cspotcode/source-map-support') as typeof import('@cspotcode/source-map-support'); - // We must uninstall so that it resets its internal state; otherwise it won't know it needs to reinstall in the next test. - sms.uninstall(); - // Must remove handlers to avoid a memory leak - sms.resetRetrieveHandlers(); - - // Modified by ts-node hooks - resetObject( - require.extensions, - defaultRequireExtensions, - undefined, - undefined, - undefined, - true - ); - - // ts-node attaches a property when it registers an instance - // source-map-support monkey-patches the emit function - // Avoid node deprecation warnings for setting process.config or accessing _channel - resetObject(process, defaultProcess, undefined, ['_channel'], ['config']); - - // source-map-support swaps out the prepareStackTrace function - resetObject(Error, defaultError); - - // _resolveFilename et.al. are modified by ts-node, tsconfig-paths, source-map-support, yarn, maybe other things? - resetObject(require('module'), defaultModule, undefined, ['wrap', 'wrapper']); - - // May be modified by REPL tests, since the REPL sets globals. - // Avoid deleting nyc's coverage data. - resetObject(global, defaultGlobal, ['__coverage__']); - - // Reset our ESM hooks - process.__test_setloader__?.(undefined); -} - -function captureObjectState(object: any, avoidGetters: string[] = []) { - const descriptors = Object.getOwnPropertyDescriptors(object); - const values = mapValues(descriptors, (_d, key) => { - if (avoidGetters.includes(key)) return descriptors[key].value; - return object[key]; - }); - return { - descriptors, - values, - }; -} -// Redefine all property descriptors and delete any new properties -function resetObject( - object: any, - state: ReturnType, - doNotDeleteTheseKeys: string[] = [], - doNotSetTheseKeys: true | string[] = [], - avoidSetterIfUnchanged: string[] = [], - reorderProperties = false -) { - const currentDescriptors = Object.getOwnPropertyDescriptors(object); - for (const key of Object.keys(currentDescriptors)) { - if (doNotDeleteTheseKeys.includes(key)) continue; - if (has(state.descriptors, key)) continue; - delete object[key]; - } - // Trigger nyc's setter functions - for (const [key, value] of Object.entries(state.values)) { - try { - if (doNotSetTheseKeys === true || doNotSetTheseKeys.includes(key)) - continue; - if (avoidSetterIfUnchanged.includes(key) && object[key] === value) - continue; - state.descriptors[key].set?.call(object, value); - } catch {} - } - // Reset descriptors - Object.defineProperties(object, state.descriptors); - - if (reorderProperties) { - // Delete and re-define each property so that they are in original order - const originalOrder = Object.keys(state.descriptors); - const properties = Object.getOwnPropertyDescriptors(object); - const sortedKeys = sortBy(Object.keys(properties), (name) => - originalOrder.includes(name) ? originalOrder.indexOf(name) : 999 - ); - for (const key of sortedKeys) { - delete object[key]; - Object.defineProperty(object, key, properties[key]); - } - } -} - -//#endregion - -export const delay = promisify(setTimeout); - -/** Essentially Array:includes, but with tweaked types for checks on enums */ -export function isOneOf(value: V, arrayOfPossibilities: ReadonlyArray) { - return arrayOfPossibilities.includes(value as any); -} diff --git a/src/test/helpers/command-lines.ts b/src/test/helpers/command-lines.ts new file mode 100644 index 000000000..e47923422 --- /dev/null +++ b/src/test/helpers/command-lines.ts @@ -0,0 +1,12 @@ +// Command lines + +import { BIN_PATH, PROJECT, PROJECT_TRANSPILE_ONLY } from './paths'; + +/** Default `ts-node --project` invocation */ +export const CMD_TS_NODE_WITH_PROJECT_FLAG = `"${BIN_PATH}" --project "${PROJECT}"`; +/** Default `ts-node --project` invocation with transpile-only */ +export const CMD_TS_NODE_WITH_PROJECT_TRANSPILE_ONLY_FLAG = `"${BIN_PATH}" --project "${PROJECT_TRANSPILE_ONLY}"`; +/** Default `ts-node` invocation without `--project` */ +export const CMD_TS_NODE_WITHOUT_PROJECT_FLAG = `"${BIN_PATH}"`; +export const CMD_ESM_LOADER_WITHOUT_PROJECT = `node --loader ts-node/esm`; +//#endregion diff --git a/src/test/helpers/ctx-tmp-dir.ts b/src/test/helpers/ctx-tmp-dir.ts new file mode 100644 index 000000000..9c6c2c094 --- /dev/null +++ b/src/test/helpers/ctx-tmp-dir.ts @@ -0,0 +1,39 @@ +import { tmpdir } from 'os'; +import type { ExecutionContext } from '../testlib'; +import { tempdirProject } from '@TypeStrong/fs-fixture-builder'; + +/** + * This helpers gives you an empty directory in the OS temp directory, *outside* + * of the git clone. + * + * Some tests must run in a directory that is *outside* of the git clone. + * When TS and ts-node search for a tsconfig, they traverse up the filesystem. + * If they run inside our git clone, they will find the root tsconfig.json, and + * we do not always want that. + */ +export async function ctxTmpDirOutsideCheckout(t: ExecutionContext) { + const fixture = tempdirProject({ + name: 'ts-node-spec', + rootDir: tmpdir(), + }); + return { + tmpDir: fixture.cwd, + fixture, + }; +} +export namespace ctxTmpDirOutsideCheckout { + export type Ctx = Awaited>; + export type T = ExecutionContext; +} + +export async function ctxTmpDir(t: ExecutionContext) { + const fixture = tempdirProject('ts-node-spec'); + return { + fixture, + tmpDir: fixture.cwd, + }; +} +export namespace ctxTmpDir { + export type Ctx = Awaited>; + export type T = ExecutionContext; +} diff --git a/src/test/helpers/ctx-ts-node.ts b/src/test/helpers/ctx-ts-node.ts new file mode 100644 index 000000000..012cb8682 --- /dev/null +++ b/src/test/helpers/ctx-ts-node.ts @@ -0,0 +1,98 @@ +import { exec as childProcessExec } from 'child_process'; +import { lock } from 'proper-lockfile'; +import { promisify } from 'util'; +import { existsSync, readFileSync, writeFileSync } from 'fs'; +import { join } from 'path'; +import type { ExecutionContext } from '../testlib'; +import { sync as rimrafSync } from 'rimraf'; +import { TEST_DIR } from './paths'; +import { testsDirRequire, tsNodeTypes } from './misc'; + +/** Pass to `test.context()` to get access to the ts-node API under test */ +export async function ctxTsNode() { + await installTsNode(); + const tsNodeUnderTest: typeof tsNodeTypes = testsDirRequire('ts-node'); + return { + tsNodeUnderTest, + }; +} +export namespace ctxTsNode { + export type Ctx = Awaited>; + export type T = ExecutionContext; +} + +const ts_node_install_lock = process.env.ts_node_install_lock as string; +const lockPath = join(__dirname, ts_node_install_lock); + +interface InstallationResult { + error: string | null; +} + +/** + * Pack and install ts-node locally, necessary to test package "exports" + * FS locking b/c tests run in separate processes + */ +export async function installTsNode() { + await lockedMemoizedOperation(lockPath, async () => { + const totalTries = process.platform === 'win32' ? 5 : 1; + let tries = 0; + while (true) { + try { + rimrafSync(join(TEST_DIR, '.yarn/cache/ts-node-file-*')); + const result = await promisify(childProcessExec)( + `yarn --no-immutable`, + { + cwd: TEST_DIR, + } + ); + // You can uncomment this to aid debugging + // console.log(result.stdout, result.stderr); + rimrafSync(join(TEST_DIR, '.yarn/cache/ts-node-file-*')); + writeFileSync(join(TEST_DIR, 'yarn.lock'), ''); + break; + } catch (e) { + tries++; + if (tries >= totalTries) throw e; + } + } + }); +} + +/** + * Attempt an operation once across multiple processes, using filesystem locking. + * If it was executed already by another process, and it errored, throw the same error message. + */ +async function lockedMemoizedOperation( + lockPath: string, + operation: () => Promise +) { + const releaseLock = await lock(lockPath, { + realpath: false, + stale: 120e3, + retries: { + retries: 120, + maxTimeout: 1000, + }, + }); + try { + const operationHappened = existsSync(lockPath); + if (operationHappened) { + const result: InstallationResult = JSON.parse( + readFileSync(lockPath, 'utf8') + ); + if (result.error) throw result.error; + } else { + const result: InstallationResult = { error: null }; + try { + await operation(); + } catch (e) { + result.error = `${e}`; + throw e; + } finally { + writeFileSync(lockPath, JSON.stringify(result)); + } + } + } finally { + releaseLock(); + } +} diff --git a/src/test/helpers/index.ts b/src/test/helpers/index.ts new file mode 100644 index 000000000..fbb64d072 --- /dev/null +++ b/src/test/helpers/index.ts @@ -0,0 +1,7 @@ +export * from './misc'; +export * from './ctx-tmp-dir'; +export * from './ctx-ts-node'; +export * from './paths'; +export * from './command-lines'; +export * from './reset-node-environment'; +export * from './version-checks'; diff --git a/src/test/helpers/misc.ts b/src/test/helpers/misc.ts new file mode 100644 index 000000000..e310b9327 --- /dev/null +++ b/src/test/helpers/misc.ts @@ -0,0 +1,20 @@ +/** types from ts-node under test */ +import type * as tsNodeTypes from '../../index'; +import type _createRequire from 'create-require'; +import { TEST_DIR } from './paths'; +import { join } from 'path'; +import { promisify } from 'util'; +const createRequire: typeof _createRequire = require('create-require'); +export { tsNodeTypes }; + +// `createRequire` does not exist on older node versions +export const testsDirRequire = createRequire(join(TEST_DIR, 'index.js')); + +export const ts = testsDirRequire('typescript') as typeof import('typescript'); + +export const delay = promisify(setTimeout); + +/** Essentially Array:includes, but with tweaked types for checks on enums */ +export function isOneOf(value: V, arrayOfPossibilities: ReadonlyArray) { + return arrayOfPossibilities.includes(value as any); +} diff --git a/src/test/helpers/paths.ts b/src/test/helpers/paths.ts new file mode 100644 index 000000000..a2ebe3a3c --- /dev/null +++ b/src/test/helpers/paths.ts @@ -0,0 +1,24 @@ +import { setFixturesRootDir } from '@TypeStrong/fs-fixture-builder'; +import { join, resolve } from 'path'; + +//#region Paths +export const ROOT_DIR = resolve(__dirname, '../../..'); +export const DIST_DIR = resolve(__dirname, '../..'); +export const TEST_DIR = join(__dirname, '../../../tests'); +export const PROJECT = join(TEST_DIR, 'tsconfig.json'); +export const PROJECT_TRANSPILE_ONLY = join( + TEST_DIR, + 'tsconfig-transpile-only.json' +); +export const BIN_PATH = join(TEST_DIR, 'node_modules/.bin/ts-node'); +export const BIN_PATH_JS = join(TEST_DIR, 'node_modules/ts-node/dist/bin.js'); +export const BIN_SCRIPT_PATH = join( + TEST_DIR, + 'node_modules/.bin/ts-node-script' +); +export const BIN_CWD_PATH = join(TEST_DIR, 'node_modules/.bin/ts-node-cwd'); +export const BIN_ESM_PATH = join(TEST_DIR, 'node_modules/.bin/ts-node-esm'); + +process.chdir(TEST_DIR); +setFixturesRootDir(TEST_DIR); +//#endregion diff --git a/src/test/helpers/reset-node-environment.ts b/src/test/helpers/reset-node-environment.ts new file mode 100644 index 000000000..8ab7dd355 --- /dev/null +++ b/src/test/helpers/reset-node-environment.ts @@ -0,0 +1,109 @@ +import { has, mapValues, sortBy } from 'lodash'; + +// Reset node environment +// Useful because ts-node installation necessarily must mutate the node environment. +// Yet we want to run tests in-process for speed. +// So we need to reliably reset everything changed by ts-node installation. + +const defaultRequireExtensions = captureObjectState(require.extensions); +// Avoid node deprecation warning for accessing _channel +const defaultProcess = captureObjectState(process, ['_channel']); +const defaultModule = captureObjectState(require('module')); +const defaultError = captureObjectState(Error); +const defaultGlobal = captureObjectState(global); + +/** + * Undo all of ts-node & co's installed hooks, resetting the node environment to default + * so we can run multiple test cases which `.register()` ts-node. + * + * Must also play nice with `nyc`'s environmental mutations. + */ +export function resetNodeEnvironment() { + const sms = + require('@cspotcode/source-map-support') as typeof import('@cspotcode/source-map-support'); + // We must uninstall so that it resets its internal state; otherwise it won't know it needs to reinstall in the next test. + sms.uninstall(); + // Must remove handlers to avoid a memory leak + sms.resetRetrieveHandlers(); + + // Modified by ts-node hooks + resetObject( + require.extensions, + defaultRequireExtensions, + undefined, + undefined, + undefined, + true + ); + + // ts-node attaches a property when it registers an instance + // source-map-support monkey-patches the emit function + // Avoid node deprecation warnings for setting process.config or accessing _channel + resetObject(process, defaultProcess, undefined, ['_channel'], ['config']); + + // source-map-support swaps out the prepareStackTrace function + resetObject(Error, defaultError); + + // _resolveFilename et.al. are modified by ts-node, tsconfig-paths, source-map-support, yarn, maybe other things? + resetObject(require('module'), defaultModule, undefined, ['wrap', 'wrapper']); + + // May be modified by REPL tests, since the REPL sets globals. + // Avoid deleting nyc's coverage data. + resetObject(global, defaultGlobal, ['__coverage__']); + + // Reset our ESM hooks + process.__test_setloader__?.(undefined); +} + +function captureObjectState(object: any, avoidGetters: string[] = []) { + const descriptors = Object.getOwnPropertyDescriptors(object); + const values = mapValues(descriptors, (_d, key) => { + if (avoidGetters.includes(key)) return descriptors[key].value; + return object[key]; + }); + return { + descriptors, + values, + }; +} +// Redefine all property descriptors and delete any new properties +function resetObject( + object: any, + state: ReturnType, + doNotDeleteTheseKeys: string[] = [], + doNotSetTheseKeys: true | string[] = [], + avoidSetterIfUnchanged: string[] = [], + reorderProperties = false +) { + const currentDescriptors = Object.getOwnPropertyDescriptors(object); + for (const key of Object.keys(currentDescriptors)) { + if (doNotDeleteTheseKeys.includes(key)) continue; + if (has(state.descriptors, key)) continue; + delete object[key]; + } + // Trigger nyc's setter functions + for (const [key, value] of Object.entries(state.values)) { + try { + if (doNotSetTheseKeys === true || doNotSetTheseKeys.includes(key)) + continue; + if (avoidSetterIfUnchanged.includes(key) && object[key] === value) + continue; + state.descriptors[key].set?.call(object, value); + } catch {} + } + // Reset descriptors + Object.defineProperties(object, state.descriptors); + + if (reorderProperties) { + // Delete and re-define each property so that they are in original order + const originalOrder = Object.keys(state.descriptors); + const properties = Object.getOwnPropertyDescriptors(object); + const sortedKeys = sortBy(Object.keys(properties), (name) => + originalOrder.includes(name) ? originalOrder.indexOf(name) : 999 + ); + for (const key of sortedKeys) { + delete object[key]; + Object.defineProperty(object, key, properties[key]); + } + } +} diff --git a/src/test/helpers/version-checks.ts b/src/test/helpers/version-checks.ts new file mode 100644 index 000000000..a783158ad --- /dev/null +++ b/src/test/helpers/version-checks.ts @@ -0,0 +1,44 @@ +import semver = require('semver'); +import { ts } from './misc'; + +// Version checks, used to conditionally enable tests. + +export const nodeUsesNewHooksApi = semver.gte(process.version, '16.12.0'); +// 16.14.0: https://github.com/nodejs/node/blob/main/doc/changelogs/CHANGELOG_V16.md#notable-changes-4 +// 17.1.0: https://github.com/nodejs/node/blob/main/doc/changelogs/CHANGELOG_V17.md#2021-11-09-version-1710-current-targos +export const nodeSupportsImportAssertions = + (semver.gte(process.version, '16.14.0') && + semver.lt(process.version, '17.0.0')) || + semver.gte(process.version, '17.1.0'); +// These versions do not require `--experimental-json-modules` +// 16.15.0: https://github.com/nodejs/node/blob/main/doc/changelogs/CHANGELOG_V16.md#2022-04-26-version-16150-gallium-lts-danielleadams +// 17.5.0: https://github.com/nodejs/node/blob/main/doc/changelogs/CHANGELOG_V17.md#2022-02-10-version-1750-current-ruyadorno +export const nodeSupportsUnflaggedJsonImports = + (semver.gte(process.version, '16.15.0') && + semver.lt(process.version, '17.0.0')) || + semver.gte(process.version, '17.5.0'); +// Node 14.13.0 has a bug where it tries to lex CJS files to discover named exports *before* +// we transform the code. +// In other words, it tries to parse raw TS as CJS and balks at `export const foo =`, expecting to see `exports.foo =` +// This lexing only happens when CJS TS is imported from the ESM loader. +export const nodeSupportsImportingTransformedCjsFromEsm = semver.gte( + process.version, + '14.13.1' +); +/** Supports module:nodenext and module:node16 as *stable* features */ +export const tsSupportsStableNodeNextNode16 = + ts.version.startsWith('4.7.') || semver.gte(ts.version, '4.7.0'); +// TS 4.5 is first version to understand .cts, .mts, .cjs, and .mjs extensions +export const tsSupportsMtsCtsExtensions = semver.gte(ts.version, '4.5.0'); +export const tsSupportsImportAssertions = semver.gte(ts.version, '4.5.0'); +// TS 4.1 added jsx=react-jsx and react-jsxdev: https://devblogs.microsoft.com/typescript/announcing-typescript-4-1/#react-17-jsx-factories +export const tsSupportsReact17JsxFactories = semver.gte(ts.version, '4.1.0'); +// TS 5.0 added "allowImportingTsExtensions" +export const tsSupportsAllowImportingTsExtensions = semver.gte( + ts.version, + '4.999.999' +); +// Relevant when @tsconfig/bases refers to es2021 and we run tests against +// old TS versions. +export const tsSupportsEs2021 = semver.gte(ts.version, '4.3.0'); +export const tsSupportsEs2022 = semver.gte(ts.version, '4.6.0'); diff --git a/src/test/index.spec.ts b/src/test/index.spec.ts index 7ae9b257d..5ba5f7f31 100644 --- a/src/test/index.spec.ts +++ b/src/test/index.spec.ts @@ -1,19 +1,19 @@ import { context, ExecutionContext } from './testlib'; import * as expect from 'expect'; import { join, resolve, sep as pathSep } from 'path'; -import { tmpdir } from 'os'; import semver = require('semver'); +import { project as fsProject } from '@TypeStrong/fs-fixture-builder'; import { BIN_PATH_JS, CMD_TS_NODE_WITH_PROJECT_TRANSPILE_ONLY_FLAG, + ctxTmpDirOutsideCheckout, ts, tsSupportsEs2021, tsSupportsEs2022, tsSupportsMtsCtsExtensions, tsSupportsStableNodeNextNode16, } from './helpers'; -import { lstatSync, mkdtempSync } from 'fs'; -import { npath } from '@yarnpkg/fslib'; +import { lstatSync } from 'fs'; import type _createRequire from 'create-require'; import { createExec } from './exec-helpers'; import { @@ -24,8 +24,6 @@ import { ROOT_DIR, TEST_DIR, testsDirRequire, - tsNodeTypes, - xfs, ctxTsNode, CMD_TS_NODE_WITH_PROJECT_FLAG, CMD_TS_NODE_WITHOUT_PROJECT_FLAG, @@ -86,44 +84,36 @@ test.suite('ts-node', (test) => { }); test('should not load typescript outside of loadConfig', async () => { - const { err, stdout } = await exec( + const r = await exec( `node -e "require('ts-node'); console.dir(Object.keys(require.cache).filter(k => k.includes('node_modules/typescript')).length)"` ); - expect(err).toBe(null); - expect(stdout).toBe('0\n'); + expect(r.err).toBe(null); + expect(r.stdout).toBe('0\n'); }); test.suite('cli', (test) => { test('should execute cli', async () => { - const { err, stdout } = await exec( - `${CMD_TS_NODE_WITH_PROJECT_FLAG} hello-world` - ); - expect(err).toBe(null); - expect(stdout).toBe('Hello, world!\n'); + const r = await exec(`${CMD_TS_NODE_WITH_PROJECT_FLAG} hello-world`); + expect(r.err).toBe(null); + expect(r.stdout).toBe('Hello, world!\n'); }); test('shows usage via --help', async () => { - const { err, stdout } = await exec( - `${CMD_TS_NODE_WITHOUT_PROJECT_FLAG} --help` - ); - expect(err).toBe(null); - expect(stdout).toMatch(/Usage: ts-node /); + const r = await exec(`${CMD_TS_NODE_WITHOUT_PROJECT_FLAG} --help`); + expect(r.err).toBe(null); + expect(r.stdout).toMatch(/Usage: ts-node /); }); test('shows version via -v', async () => { - const { err, stdout } = await exec( - `${CMD_TS_NODE_WITHOUT_PROJECT_FLAG} -v` - ); - expect(err).toBe(null); - expect(stdout.trim()).toBe( + const r = await exec(`${CMD_TS_NODE_WITHOUT_PROJECT_FLAG} -v`); + expect(r.err).toBe(null); + expect(r.stdout.trim()).toBe( 'v' + testsDirRequire('ts-node/package').version ); }); test('shows version of compiler via -vv', async () => { - const { err, stdout } = await exec( - `${CMD_TS_NODE_WITHOUT_PROJECT_FLAG} -vv` - ); - expect(err).toBe(null); - expect(stdout.trim()).toBe( + const r = await exec(`${CMD_TS_NODE_WITHOUT_PROJECT_FLAG} -vv`); + expect(r.err).toBe(null); + expect(r.stdout.trim()).toBe( `ts-node v${testsDirRequire('ts-node/package').version}\n` + `node ${process.version}\n` + `compiler v${testsDirRequire('typescript/package').version}` @@ -131,71 +121,68 @@ test.suite('ts-node', (test) => { }); test('should register via cli', async () => { - const { err, stdout } = await exec( - `node -r ts-node/register hello-world.ts`, - { - cwd: TEST_DIR, - } - ); - expect(err).toBe(null); - expect(stdout).toBe('Hello, world!\n'); + const r = await exec(`node -r ts-node/register hello-world.ts`, { + cwd: TEST_DIR, + }); + expect(r.err).toBe(null); + expect(r.stdout).toBe('Hello, world!\n'); }); test('should execute cli with absolute path', async () => { - const { err, stdout } = await exec( + const r = await exec( `${CMD_TS_NODE_WITH_PROJECT_TRANSPILE_ONLY_FLAG} "${join( TEST_DIR, 'hello-world' )}"` ); - expect(err).toBe(null); - expect(stdout).toBe('Hello, world!\n'); + expect(r.err).toBe(null); + expect(r.stdout).toBe('Hello, world!\n'); }); test('should print scripts', async () => { - const { err, stdout } = await exec( + const r = await exec( `${CMD_TS_NODE_WITH_PROJECT_FLAG} -pe "import { example } from './complex/index';example()"` ); - expect(err).toBe(null); - expect(stdout).toBe('example\n'); + expect(r.err).toBe(null); + expect(r.stdout).toBe('example\n'); }); test("should expose ts-node Service as a symbol property on Node's `process` object", async () => { - const { err, stdout } = await exec( + const r = await exec( `${CMD_TS_NODE_WITH_PROJECT_TRANSPILE_ONLY_FLAG} env` ); - expect(err).toBe(null); - expect(stdout).toBe('object\n'); + expect(r.err).toBe(null); + expect(r.stdout).toBe('object\n'); }); test('should allow js', async () => { - const { err, stdout } = await exec( + const r = await exec( [ CMD_TS_NODE_WITH_PROJECT_TRANSPILE_ONLY_FLAG, '-O "{\\"allowJs\\":true}"', '-pe "import { main } from \'./allow-js/run\';main()"', ].join(' ') ); - expect(err).toBe(null); - expect(stdout).toBe('hello world\n'); + expect(r.err).toBe(null); + expect(r.stdout).toBe('hello world\n'); }); test('should include jsx when `allow-js` true', async () => { - const { err, stdout } = await exec( + const r = await exec( [ CMD_TS_NODE_WITH_PROJECT_TRANSPILE_ONLY_FLAG, '-O "{\\"allowJs\\":true}"', '-pe "import { Foo2 } from \'./allow-js/with-jsx\'; Foo2.sayHi()"', ].join(' ') ); - expect(err).toBe(null); - expect(stdout).toBe('hello world\n'); + expect(r.err).toBe(null); + expect(r.stdout).toBe('hello world\n'); }); test.suite('should support cts when module = CommonJS', (test) => { - test.runIf(tsSupportsMtsCtsExtensions); + test.if(tsSupportsMtsCtsExtensions); test('test', async (t) => { - const { err, stdout } = await exec( + const r = await exec( [ CMD_TS_NODE_WITHOUT_PROJECT_FLAG, '-pe "import { main } from \'./index.cjs\';main()"', @@ -204,50 +191,50 @@ test.suite('ts-node', (test) => { cwd: join(TEST_DIR, 'ts45-ext/ext-cts'), } ); - expect(err).toBe(null); - expect(stdout).toBe('hello world\n'); + expect(r.err).toBe(null); + expect(r.stdout).toBe('hello world\n'); }); }); test.suite('should support mts when module = ESNext', (test) => { - test.runIf(tsSupportsMtsCtsExtensions); + test.if(tsSupportsMtsCtsExtensions); test('test', async () => { - const { err, stdout } = await exec( + const r = await exec( [CMD_TS_NODE_WITHOUT_PROJECT_FLAG, './entrypoint.mjs'].join(' '), { cwd: join(TEST_DIR, 'ts45-ext/ext-mts'), } ); - expect(err).toBe(null); - expect(stdout).toBe('hello world\n'); + expect(r.err).toBe(null); + expect(r.stdout).toBe('hello world\n'); }); }); test('should eval code', async () => { - const { err, stdout } = await exec( + const r = await exec( `${CMD_TS_NODE_WITH_PROJECT_TRANSPILE_ONLY_FLAG} -e "import * as m from './module';console.log(m.example('test'))"` ); - expect(err).toBe(null); - expect(stdout).toBe('TEST\n'); + expect(r.err).toBe(null); + expect(r.stdout).toBe('TEST\n'); }); test('should import empty files', async () => { - const { err, stdout } = await exec( + const r = await exec( `${CMD_TS_NODE_WITH_PROJECT_TRANSPILE_ONLY_FLAG} -e "import './empty'"` ); - expect(err).toBe(null); - expect(stdout).toBe(''); + expect(r.err).toBe(null); + expect(r.stdout).toBe(''); }); test('should throw typechecking errors', async () => { - const { err } = await exec( + const r = await exec( `${CMD_TS_NODE_WITH_PROJECT_FLAG} -e "import * as m from './module';console.log(m.example(123))"` ); - if (err === null) { + if (r.err === null) { throw new Error('Command was expected to fail, but it succeeded.'); } - expect(err.message).toMatch( + expect(r.err.message).toMatch( new RegExp( "TS2345: Argument of type '(?:number|123)' " + "is not assignable to parameter of type 'string'\\." @@ -256,27 +243,25 @@ test.suite('ts-node', (test) => { }); test('should be able to ignore diagnostic', async () => { - const { err } = await exec( + const r = await exec( `${CMD_TS_NODE_WITH_PROJECT_FLAG} --ignore-diagnostics 2345 -e "import * as m from './module';console.log(m.example(123))"` ); - if (err === null) { + if (r.err === null) { throw new Error('Command was expected to fail, but it succeeded.'); } - expect(err.message).toMatch( + expect(r.err.message).toMatch( /TypeError: (?:(?:undefined|foo\.toUpperCase) is not a function|.*has no method \'toUpperCase\')/ ); }); test('should work with source maps', async () => { - const { err } = await exec( - `${CMD_TS_NODE_WITH_PROJECT_FLAG} "throw error"` - ); - if (err === null) { + const r = await exec(`${CMD_TS_NODE_WITH_PROJECT_FLAG} "throw error"`); + if (r.err === null) { throw new Error('Command was expected to fail, but it succeeded.'); } - expect(err.message).toMatch( + expect(r.err.message).toMatch( [ `${join(TEST_DIR, 'throw error.ts')}:100`, " bar() { throw new Error('this is a demo'); }", @@ -287,14 +272,14 @@ test.suite('ts-node', (test) => { }); test('should work with source maps in --transpile-only mode', async () => { - const { err } = await exec( + const r = await exec( `${CMD_TS_NODE_WITH_PROJECT_FLAG} --transpile-only "throw error"` ); - if (err === null) { + if (r.err === null) { throw new Error('Command was expected to fail, but it succeeded.'); } - expect(err.message).toMatch( + expect(r.err.message).toMatch( [ `${join(TEST_DIR, 'throw error.ts')}:100`, " bar() { throw new Error('this is a demo'); }", @@ -305,14 +290,14 @@ test.suite('ts-node', (test) => { }); test('eval should work with source maps', async () => { - const { err } = await exec( + const r = await exec( `${CMD_TS_NODE_WITH_PROJECT_FLAG} -pe "import './throw error'"` ); - if (err === null) { + if (r.err === null) { throw new Error('Command was expected to fail, but it succeeded.'); } - expect(err.message).toMatch( + expect(r.err.message).toMatch( [ `${join(TEST_DIR, 'throw error.ts')}:100`, " bar() { throw new Error('this is a demo'); }", @@ -322,25 +307,25 @@ test.suite('ts-node', (test) => { }); test('should support transpile only mode', async () => { - const { err } = await exec( + const r = await exec( `${CMD_TS_NODE_WITH_PROJECT_FLAG} --transpile-only -pe "x"` ); - if (err === null) { + if (r.err === null) { throw new Error('Command was expected to fail, but it succeeded.'); } - expect(err.message).toMatch('ReferenceError: x is not defined'); + expect(r.err.message).toMatch('ReferenceError: x is not defined'); }); test('should throw error even in transpileOnly mode', async () => { - const { err } = await exec( + const r = await exec( `${CMD_TS_NODE_WITH_PROJECT_FLAG} --transpile-only -pe "console."` ); - if (err === null) { + if (r.err === null) { throw new Error('Command was expected to fail, but it succeeded.'); } - expect(err.message).toMatch('error TS1003: Identifier expected'); + expect(r.err.message).toMatch('error TS1003: Identifier expected'); }); for (const flavor of [ @@ -351,19 +336,16 @@ test.suite('ts-node', (test) => { 'transpile-only-swc-shorthand-via-tsconfig', ]) { test(`should support swc and third-party transpilers: ${flavor}`, async () => { - const { err, stdout } = await exec( - `${CMD_TS_NODE_WITHOUT_PROJECT_FLAG} ${flavor}`, - { - env: { - ...process.env, - NODE_OPTIONS: `${ - process.env.NODE_OPTIONS || '' - } --require ${require.resolve('../../tests/spy-swc-transpiler')}`, - }, - } - ); - expect(err).toBe(null); - expect(stdout).toMatch( + const r = await exec(`${CMD_TS_NODE_WITHOUT_PROJECT_FLAG} ${flavor}`, { + env: { + ...process.env, + NODE_OPTIONS: `${ + process.env.NODE_OPTIONS || '' + } --require ${require.resolve('../../tests/spy-swc-transpiler')}`, + }, + }); + expect(r.err).toBe(null); + expect(r.stdout).toMatch( 'Hello World! swc transpiler invocation count: 1\n' ); }); @@ -371,85 +353,78 @@ test.suite('ts-node', (test) => { test.suite('should support `traceResolution` compiler option', (test) => { test('prints traces before running code when enabled', async () => { - const { err, stdout } = await exec( + const r = await exec( `${BIN_PATH} --compiler-options="{ \\"traceResolution\\": true }" -e "console.log('ok')"` ); - expect(err).toBeNull(); - expect(stdout).toContain('======== Resolving module'); - expect(stdout.endsWith('ok\n')).toBe(true); + expect(r.err).toBeNull(); + expect(r.stdout).toContain('======== Resolving module'); + expect(r.stdout.endsWith('ok\n')).toBe(true); }); test('does NOT print traces when not enabled', async () => { - const { err, stdout } = await exec( - `${BIN_PATH} -e "console.log('ok')"` - ); - expect(err).toBeNull(); - expect(stdout).not.toContain('======== Resolving module'); - expect(stdout.endsWith('ok\n')).toBe(true); + const r = await exec(`${BIN_PATH} -e "console.log('ok')"`); + expect(r.err).toBeNull(); + expect(r.stdout).not.toContain('======== Resolving module'); + expect(r.stdout.endsWith('ok\n')).toBe(true); }); }); test('swc transpiler supports native ESM emit', async () => { - const { err, stdout } = await exec( - `${CMD_ESM_LOADER_WITHOUT_PROJECT} ./index.ts`, - { - cwd: resolve(TEST_DIR, 'transpile-only-swc-native-esm'), - } - ); - expect(err).toBe(null); - expect(stdout).toMatch('Hello file://'); + const r = await exec(`${CMD_ESM_LOADER_WITHOUT_PROJECT} ./index.ts`, { + cwd: resolve(TEST_DIR, 'transpile-only-swc-native-esm'), + }); + expect(r.err).toBe(null); + expect(r.stdout).toMatch('Hello file://'); }); test('should pipe into `ts-node` and evaluate', async () => { - const execPromise = exec(CMD_TS_NODE_WITH_PROJECT_TRANSPILE_ONLY_FLAG); - execPromise.child.stdin!.end("console.log('hello')"); - const { err, stdout } = await execPromise; - expect(err).toBe(null); - expect(stdout).toBe('hello\n'); + const p = exec(CMD_TS_NODE_WITH_PROJECT_TRANSPILE_ONLY_FLAG); + p.child.stdin!.end("console.log('hello')"); + const r = await p; + expect(r.err).toBe(null); + expect(r.stdout).toBe('hello\n'); }); test('should pipe into `ts-node`', async () => { - const execPromise = exec( - `${CMD_TS_NODE_WITH_PROJECT_TRANSPILE_ONLY_FLAG} -p` - ); - execPromise.child.stdin!.end('true'); - const { err, stdout } = await execPromise; - expect(err).toBe(null); - expect(stdout).toBe('true\n'); + const p = exec(`${CMD_TS_NODE_WITH_PROJECT_TRANSPILE_ONLY_FLAG} -p`); + p.child.stdin!.end('true'); + const r = await p; + expect(r.err).toBe(null); + expect(r.stdout).toBe('true\n'); }); test('should pipe into an eval script', async () => { - const execPromise = exec( + const p = exec( `${CMD_TS_NODE_WITH_PROJECT_FLAG} --transpile-only -pe "process.stdin.isTTY"` ); - execPromise.child.stdin!.end('true'); - const { err, stdout } = await execPromise; - expect(err).toBe(null); - expect(stdout).toBe('undefined\n'); + p.child.stdin!.end('true'); + const r = await p; + expect(r.err).toBe(null); + expect(r.stdout).toBe('undefined\n'); }); test('should support require flags', async () => { - const { err, stdout } = await exec( + const r = await exec( `${CMD_TS_NODE_WITH_PROJECT_TRANSPILE_ONLY_FLAG} -r ./hello-world -pe "console.log('success')"` ); - expect(err).toBe(null); - expect(stdout).toBe('Hello, world!\nsuccess\nundefined\n'); + expect(r.err).toBe(null); + expect(r.stdout).toBe('Hello, world!\nsuccess\nundefined\n'); }); test('should support require from node modules', async () => { - const { err, stdout } = await exec( + const r = await exec( `${CMD_TS_NODE_WITH_PROJECT_TRANSPILE_ONLY_FLAG} -r typescript -e "console.log('success')"` ); - expect(err).toBe(null); - expect(stdout).toBe('success\n'); + expect(r.err).toBe(null); + expect(r.stdout).toBe('success\n'); }); test('should use source maps with react tsx', async () => { - const { err, stdout } = await exec( + const r = await exec( `${CMD_TS_NODE_WITH_PROJECT_FLAG} "throw error react tsx.tsx"` ); - expect(err).not.toBe(null); - expect(err!.message).toMatch( + expect(r.err).not.toBe(null); + expect(r.err!.message).toMatch( [ `${join(TEST_DIR, './throw error react tsx.tsx')}:100`, " bar() { throw new Error('this is a demo'); }", @@ -460,11 +435,11 @@ test.suite('ts-node', (test) => { }); test('should use source maps with react tsx in --transpile-only mode', async () => { - const { err, stdout } = await exec( + const r = await exec( `${CMD_TS_NODE_WITH_PROJECT_FLAG} --transpile-only "throw error react tsx.tsx"` ); - expect(err).not.toBe(null); - expect(err!.message).toMatch( + expect(r.err).not.toBe(null); + expect(r.err!.message).toMatch( [ `${join(TEST_DIR, './throw error react tsx.tsx')}:100`, " bar() { throw new Error('this is a demo'); }", @@ -475,117 +450,112 @@ test.suite('ts-node', (test) => { }); test('should allow custom typings', async () => { - const { err, stdout } = await exec( - `${CMD_TS_NODE_WITH_PROJECT_FLAG} custom-types` - ); + const r = await exec(`${CMD_TS_NODE_WITH_PROJECT_FLAG} custom-types`); // This error comes from *node*, meaning TypeScript respected the custom types (good) but *node* could not find the non-existent module (expected) - expect(err?.message).toMatch( + expect(r.err?.message).toMatch( /Error: Cannot find module 'does-not-exist'/ ); }); test('should import js before ts by default', async () => { - const { err, stdout } = await exec( + const r = await exec( `${CMD_TS_NODE_WITH_PROJECT_TRANSPILE_ONLY_FLAG} import-order/compiled` ); - expect(err).toBe(null); - expect(stdout).toBe('Hello, JavaScript!\n'); + expect(r.err).toBe(null); + expect(r.stdout).toBe('Hello, JavaScript!\n'); }); test('should import ts before js when --prefer-ts-exts flag is present', async () => { - const { err, stdout } = await exec( + const r = await exec( `${CMD_TS_NODE_WITH_PROJECT_TRANSPILE_ONLY_FLAG} --prefer-ts-exts import-order/compiled` ); - expect(err).toBe(null); - expect(stdout).toBe('Hello, TypeScript!\n'); + expect(r.err).toBe(null); + expect(r.stdout).toBe('Hello, TypeScript!\n'); }); test('should import ts before js when TS_NODE_PREFER_TS_EXTS env is present', async () => { - const { err, stdout } = await exec( + const r = await exec( `${CMD_TS_NODE_WITH_PROJECT_TRANSPILE_ONLY_FLAG} import-order/compiled`, { env: { ...process.env, TS_NODE_PREFER_TS_EXTS: 'true' }, } ); - expect(err).toBe(null); - expect(stdout).toBe('Hello, TypeScript!\n'); + expect(r.err).toBe(null); + expect(r.stdout).toBe('Hello, TypeScript!\n'); }); test('should ignore .d.ts files', async () => { - const { err, stdout } = await exec( + const r = await exec( `${CMD_TS_NODE_WITH_PROJECT_TRANSPILE_ONLY_FLAG} import-order/importer` ); - expect(err).toBe(null); - expect(stdout).toBe('Hello, World!\n'); + expect(r.err).toBe(null); + expect(r.stdout).toBe('Hello, World!\n'); }); test.suite('issue #884', (test) => { test('should compile', async (t) => { - const { err, stdout } = await exec( + const r = await exec( `"${BIN_PATH}" --project issue-884/tsconfig.json issue-884` ); - expect(err).toBe(null); - expect(stdout).toBe(''); + expect(r.err).toBe(null); + expect(r.stdout).toBe(''); }); }); test.suite('issue #986', (test) => { test('should not compile', async () => { - const { err, stdout, stderr } = await exec( + const r = await exec( `"${BIN_PATH}" --project issue-986/tsconfig.json issue-986` ); - expect(err).not.toBe(null); - expect(stderr).toMatch("Cannot find name 'TEST'"); // TypeScript error. - expect(stdout).toBe(''); + expect(r.err).not.toBe(null); + expect(r.stderr).toMatch("Cannot find name 'TEST'"); // TypeScript error. + expect(r.stdout).toBe(''); }); test('should compile with `--files`', async () => { - const { err, stdout, stderr } = await exec( + const r = await exec( `"${BIN_PATH}" --files --project issue-986/tsconfig.json issue-986` ); - expect(err).not.toBe(null); - expect(stderr).toMatch('ReferenceError: TEST is not defined'); // Runtime error. - expect(stdout).toBe(''); + expect(r.err).not.toBe(null); + expect(r.stderr).toMatch('ReferenceError: TEST is not defined'); // Runtime error. + expect(r.stdout).toBe(''); }); }); test('should locate tsconfig relative to entry-point by default', async () => { - const { err, stdout } = await exec(`${BIN_PATH} ../a/index`, { + const r = await exec(`${BIN_PATH} ../a/index`, { cwd: join(TEST_DIR, 'cwd-and-script-mode/b'), }); - expect(err).toBe(null); - expect(stdout).toMatch(/plugin-a/); + expect(r.err).toBe(null); + expect(r.stdout).toMatch(/plugin-a/); }); test('should locate tsconfig relative to entry-point via ts-node-script', async () => { - const { err, stdout } = await exec(`${BIN_SCRIPT_PATH} ../a/index`, { + const r = await exec(`${BIN_SCRIPT_PATH} ../a/index`, { cwd: join(TEST_DIR, 'cwd-and-script-mode/b'), }); - expect(err).toBe(null); - expect(stdout).toMatch(/plugin-a/); + expect(r.err).toBe(null); + expect(r.stdout).toMatch(/plugin-a/); }); test('should locate tsconfig relative to entry-point with --script-mode', async () => { - const { err, stdout } = await exec( - `${BIN_PATH} --script-mode ../a/index`, - { - cwd: join(TEST_DIR, 'cwd-and-script-mode/b'), - } - ); - expect(err).toBe(null); - expect(stdout).toMatch(/plugin-a/); + const r = await exec(`${BIN_PATH} --script-mode ../a/index`, { + cwd: join(TEST_DIR, 'cwd-and-script-mode/b'), + }); + expect(r.err).toBe(null); + expect(r.stdout).toMatch(/plugin-a/); }); test('should locate tsconfig relative to cwd via ts-node-cwd', async () => { - const { err, stdout } = await exec(`${BIN_CWD_PATH} ../a/index`, { + const r = await exec(`${BIN_CWD_PATH} ../a/index`, { cwd: join(TEST_DIR, 'cwd-and-script-mode/b'), }); - expect(err).toBe(null); - expect(stdout).toMatch(/plugin-b/); + expect(r.err).toBe(null); + expect(r.stdout).toMatch(/plugin-b/); }); test('should locate tsconfig relative to cwd in --cwd-mode', async () => { - const { err, stdout } = await exec(`${BIN_PATH} --cwd-mode ../a/index`, { + const r = await exec(`${BIN_PATH} --cwd-mode ../a/index`, { cwd: join(TEST_DIR, 'cwd-and-script-mode/b'), }); - expect(err).toBe(null); - expect(stdout).toMatch(/plugin-b/); + expect(r.err).toBe(null); + expect(r.stdout).toMatch(/plugin-b/); }); test('should locate tsconfig relative to realpath, not symlink, when entrypoint is a symlink', async (t) => { if ( @@ -593,11 +563,9 @@ test.suite('ts-node', (test) => { join(TEST_DIR, 'main-realpath/symlink/symlink.tsx') ).isSymbolicLink() ) { - const { err, stdout } = await exec( - `${BIN_PATH} main-realpath/symlink/symlink.tsx` - ); - expect(err).toBe(null); - expect(stdout).toBe(''); + const r = await exec(`${BIN_PATH} main-realpath/symlink/symlink.tsx`); + expect(r.err).toBe(null); + expect(r.stdout).toBe(''); } else { t.log('Skipping'); return; @@ -605,16 +573,13 @@ test.suite('ts-node', (test) => { }); test('should have the correct working directory in the user entry-point', async () => { - const { err, stdout, stderr } = await exec( - `${BIN_PATH} --cwd ./cjs index.ts`, - { - cwd: resolve(TEST_DIR, 'working-dir'), - } - ); + const r = await exec(`${BIN_PATH} --cwd ./cjs index.ts`, { + cwd: resolve(TEST_DIR, 'working-dir'), + }); - expect(err).toBe(null); - expect(stdout.trim()).toBe('Passing'); - expect(stderr).toBe(''); + expect(r.err).toBe(null); + expect(r.stdout.trim()).toBe('Passing'); + expect(r.stderr).toBe(''); }); // Disabled due to bug: @@ -622,30 +587,25 @@ test.suite('ts-node', (test) => { // tracked/fixed by either https://github.com/TypeStrong/ts-node/issues/1834 // or https://github.com/TypeStrong/ts-node/issues/1831 test.skip('should be able to fork into a nested TypeScript script with a modified working directory', async () => { - const { err, stdout, stderr } = await exec( - `${BIN_PATH} --cwd ./working-dir/forking/ index.ts` - ); + const r = await exec(`${BIN_PATH} --cwd ./working-dir/forking/ index.ts`); - expect(err).toBe(null); - expect(stdout.trim()).toBe('Passing: from main'); - expect(stderr).toBe(''); + expect(r.err).toBe(null); + expect(r.stdout.trim()).toBe('Passing: from main'); + expect(r.stderr).toBe(''); }); test.suite('should read ts-node options from tsconfig.json', (test) => { const BIN_EXEC = `"${BIN_PATH}" --project tsconfig-options/tsconfig.json`; test('should override compiler options from env', async () => { - const { err, stdout } = await exec( - `${BIN_EXEC} tsconfig-options/log-options1.js`, - { - env: { - ...process.env, - TS_NODE_COMPILER_OPTIONS: '{"typeRoots": ["env-typeroots"]}', - }, - } - ); - expect(err).toBe(null); - const { config } = JSON.parse(stdout); + const r = await exec(`${BIN_EXEC} tsconfig-options/log-options1.js`, { + env: { + ...process.env, + TS_NODE_COMPILER_OPTIONS: '{"typeRoots": ["env-typeroots"]}', + }, + }); + expect(r.err).toBe(null); + const { config } = JSON.parse(r.stdout); expect(config.options.typeRoots).toEqual([ join(TEST_DIR, './tsconfig-options/env-typeroots').replace( /\\/g, @@ -655,11 +615,9 @@ test.suite('ts-node', (test) => { }); test('should use options from `tsconfig.json`', async () => { - const { err, stdout } = await exec( - `${BIN_EXEC} tsconfig-options/log-options1.js` - ); - expect(err).toBe(null); - const { options, config } = JSON.parse(stdout); + const r = await exec(`${BIN_EXEC} tsconfig-options/log-options1.js`); + expect(r.err).toBe(null); + const { options, config } = JSON.parse(r.stdout); expect(config.options.typeRoots).toEqual([ join(TEST_DIR, './tsconfig-options/tsconfig-typeroots').replace( /\\/g, @@ -676,26 +634,23 @@ test.suite('ts-node', (test) => { }); test('should ignore empty strings in the array options', async () => { - const { err, stdout } = await exec( - `${BIN_EXEC} tsconfig-options/log-options1.js`, - { - env: { - ...process.env, - TS_NODE_IGNORE: '', - }, - } - ); - expect(err).toBe(null); - const { options } = JSON.parse(stdout); + const r = await exec(`${BIN_EXEC} tsconfig-options/log-options1.js`, { + env: { + ...process.env, + TS_NODE_IGNORE: '', + }, + }); + expect(r.err).toBe(null); + const { options } = JSON.parse(r.stdout); expect(options.ignore).toEqual([]); }); test('should have flags override / merge with `tsconfig.json`', async () => { - const { err, stdout } = await exec( + const r = await exec( `${BIN_EXEC} --skip-ignore --compiler-options "{\\"types\\":[\\"flags-types\\"]}" --require ./tsconfig-options/required2.js tsconfig-options/log-options2.js` ); - expect(err).toBe(null); - const { options, config } = JSON.parse(stdout); + expect(r.err).toBe(null); + const { options, config } = JSON.parse(r.stdout); expect(config.options.typeRoots).toEqual([ join(TEST_DIR, './tsconfig-options/tsconfig-typeroots').replace( /\\/g, @@ -713,18 +668,15 @@ test.suite('ts-node', (test) => { }); test('should have `tsconfig.json` override environment', async () => { - const { err, stdout } = await exec( - `${BIN_EXEC} tsconfig-options/log-options1.js`, - { - env: { - ...process.env, - TS_NODE_PRETTY: 'true', - TS_NODE_SKIP_IGNORE: 'true', - }, - } - ); - expect(err).toBe(null); - const { options, config } = JSON.parse(stdout); + const r = await exec(`${BIN_EXEC} tsconfig-options/log-options1.js`, { + env: { + ...process.env, + TS_NODE_PRETTY: 'true', + TS_NODE_SKIP_IGNORE: 'true', + }, + }); + expect(r.err).toBe(null); + const { options, config } = JSON.parse(r.stdout); expect(config.options.typeRoots).toEqual([ join(TEST_DIR, './tsconfig-options/tsconfig-typeroots').replace( /\\/g, @@ -741,11 +693,11 @@ test.suite('ts-node', (test) => { }); test('should pull ts-node options from extended `tsconfig.json`', async () => { - const { err, stdout } = await exec( + const r = await exec( `${BIN_PATH} --show-config --project ./tsconfig-extends/tsconfig.json` ); - expect(err).toBe(null); - const config = JSON.parse(stdout); + expect(r.err).toBe(null); + const config = JSON.parse(r.stdout); expect(config['ts-node'].require).toEqual([ resolve(TEST_DIR, 'tsconfig-extends/other/require-hook.js'), ]); @@ -758,10 +710,8 @@ test.suite('ts-node', (test) => { test.suite( 'should use implicit @tsconfig/bases config when one is not loaded from disk', - ({ context }) => { - const test = context(async (t) => ({ - tempDir: mkdtempSync(join(tmpdir(), 'ts-node-spec')), - })); + ({ contextEach }) => { + const test = contextEach(ctxTmpDirOutsideCheckout); const libAndTarget = semver.gte(process.versions.node, '18.0.0') && tsSupportsEs2022 ? 'es2022' @@ -769,58 +719,43 @@ test.suite('ts-node', (test) => { ? 'es2021' : 'es2020'; test('implicitly uses @tsconfig/node14, @tsconfig/node16, or @tsconfig/node18 compilerOptions when both TS and node versions support it', async (t) => { - const { - context: { tempDir }, - } = t; - const { - err: err1, - stdout: stdout1, - stderr: stderr1, - } = await exec(`${BIN_PATH} --showConfig`, { cwd: tempDir }); - expect(err1).toBe(null); - t.like(JSON.parse(stdout1), { + const r1 = await exec(`${BIN_PATH} --showConfig`, { + cwd: t.context.tmpDir, + }); + expect(r1.err).toBe(null); + t.like(JSON.parse(r1.stdout), { compilerOptions: { target: libAndTarget, lib: [libAndTarget], }, }); - const { - err: err2, - stdout: stdout2, - stderr: stderr2, - } = await exec(`${BIN_PATH} -pe 10n`, { cwd: tempDir }); - expect(err2).toBe(null); - expect(stdout2).toBe('10n\n'); + const r2 = await exec(`${BIN_PATH} -pe 10n`, { + cwd: t.context.tmpDir, + }); + expect(r2.err).toBe(null); + expect(r2.stdout).toBe('10n\n'); }); - test('implicitly loads @types/node even when not installed within local directory', async ({ - context: { tempDir }, - }) => { - const { err, stdout, stderr } = await exec( - `${BIN_PATH} -pe process.env.foo`, - { - cwd: tempDir, - env: { ...process.env, foo: 'hello world' }, - } - ); - expect(err).toBe(null); - expect(stdout).toBe('hello world\n'); + test('implicitly loads @types/node even when not installed within local directory', async (t) => { + const r = await exec(`${BIN_PATH} -pe process.env.foo`, { + cwd: t.context.tmpDir, + env: { ...process.env, foo: 'hello world' }, + }); + expect(r.err).toBe(null); + expect(r.stdout).toBe('hello world\n'); }); - test('implicitly loads local @types/node', async ({ - context: { tempDir }, - }) => { - await xfs.copyPromise( - npath.toPortablePath(tempDir), - npath.toPortablePath(join(TEST_DIR, 'local-types-node')) + test('implicitly loads local @types/node', async (t) => { + t.context.fixture.readFrom( + join(TEST_DIR, 'local-types-node'), + undefined, + [] ); - const { err, stdout, stderr } = await exec( - `${BIN_PATH} -pe process.env.foo`, - { - cwd: tempDir, - env: { ...process.env, foo: 'hello world' }, - } - ); - expect(err).not.toBe(null); - expect(stderr).toMatch( + t.context.fixture.write(); + const r = await exec(`${BIN_PATH} -pe process.env.foo`, { + cwd: t.context.fixture.cwd, + env: { ...process.env, foo: 'hello world' }, + }); + expect(r.err).not.toBe(null); + expect(r.stderr).toMatch( "Property 'env' does not exist on type 'LocalNodeTypes_Process'" ); }); @@ -831,17 +766,14 @@ test.suite('ts-node', (test) => { 'should bundle @tsconfig/bases to be used in your own tsconfigs', (test) => { // Older TS versions will complain about newer `target` and `lib` options - test.runIf(tsSupportsEs2022); + test.if(tsSupportsEs2022); const macro = test.macro((nodeVersion: string) => async (t) => { const config = require(`@tsconfig/${nodeVersion}/tsconfig.json`); - const { err, stdout, stderr } = await exec( - `${BIN_PATH} --showConfig -e 10n`, - { - cwd: join(TEST_DIR, 'tsconfig-bases', nodeVersion), - } - ); - expect(err).toBe(null); - t.like(JSON.parse(stdout), { + const r = await exec(`${BIN_PATH} --showConfig -e 10n`, { + cwd: join(TEST_DIR, 'tsconfig-bases', nodeVersion), + }); + expect(r.err).toBe(null); + t.like(JSON.parse(r.stdout), { compilerOptions: { target: config.compilerOptions.target, lib: config.compilerOptions.lib, @@ -856,23 +788,23 @@ test.suite('ts-node', (test) => { test.suite('compiler host', (test) => { test('should execute cli', async () => { - const { err, stdout } = await exec( + const r = await exec( `${CMD_TS_NODE_WITH_PROJECT_FLAG} --compiler-host hello-world` ); - expect(err).toBe(null); - expect(stdout).toBe('Hello, world!\n'); + expect(r.err).toBe(null); + expect(r.stdout).toBe('Hello, world!\n'); }); }); test('should transpile files inside a node_modules directory when not ignored', async () => { - const { err, stdout, stderr } = await exec( + const r = await exec( `${CMD_TS_NODE_WITHOUT_PROJECT_FLAG} from-node-modules/from-node-modules` ); - if (err) + if (r.err) throw new Error( - `Unexpected error: ${err}\nstdout:\n${stdout}\nstderr:\n${stderr}` + `Unexpected error: ${r.err}\nstdout:\n${r.stdout}\nstderr:\n${r.stderr}` ); - expect(JSON.parse(stdout)).toEqual({ + expect(JSON.parse(r.stdout)).toEqual({ external: { tsmri: { name: 'typescript-module-required-internally' }, jsmri: { name: 'javascript-module-required-internally' }, @@ -888,11 +820,11 @@ test.suite('ts-node', (test) => { test.suite('should respect maxNodeModulesJsDepth', (test) => { test('for unscoped modules', async () => { - const { err, stdout, stderr } = await exec( + const r = await exec( `${CMD_TS_NODE_WITHOUT_PROJECT_FLAG} maxnodemodulesjsdepth` ); - expect(err).not.toBe(null); - expect(stderr.replace(/\r\n/g, '\n')).toMatch( + expect(r.err).not.toBe(null); + expect(r.stderr.replace(/\r\n/g, '\n')).toMatch( 'TSError: ⨯ Unable to compile TypeScript:\n' + "maxnodemodulesjsdepth/other.ts(4,7): error TS2322: Type 'string' is not assignable to type 'boolean'.\n" + '\n' @@ -900,11 +832,11 @@ test.suite('ts-node', (test) => { }); test('for @scoped modules', async () => { - const { err, stdout, stderr } = await exec( + const r = await exec( `${CMD_TS_NODE_WITHOUT_PROJECT_FLAG} maxnodemodulesjsdepth-scoped` ); - expect(err).not.toBe(null); - expect(stderr.replace(/\r\n/g, '\n')).toMatch( + expect(r.err).not.toBe(null); + expect(r.stderr.replace(/\r\n/g, '\n')).toMatch( 'TSError: ⨯ Unable to compile TypeScript:\n' + "maxnodemodulesjsdepth-scoped/other.ts(7,7): error TS2322: Type 'string' is not assignable to type 'boolean'.\n" + '\n' @@ -919,12 +851,10 @@ test.suite('ts-node', (test) => { function posix(path: string) { return path.replace(/\/|\\/g, '/'); } - const { err, stdout } = await exec( - `${CMD_TS_NODE_WITH_PROJECT_FLAG} --showConfig` - ); - expect(err).toBe(null); + const r = await exec(`${CMD_TS_NODE_WITH_PROJECT_FLAG} --showConfig`); + expect(r.err).toBe(null); t.is( - stdout, + r.stdout, JSON.stringify( { 'ts-node': { @@ -956,16 +886,16 @@ test.suite('ts-node', (test) => { }); test('should support compiler scope specified via tsconfig.json', async (t) => { - const { err, stderr, stdout } = await exec( + const r = await exec( `${CMD_TS_NODE_WITHOUT_PROJECT_FLAG} --project ./scope/c/config/tsconfig.json ./scope/c/index.js` ); - expect(err).toBe(null); - expect(stdout).toBe(`value\nFailures: 0\n`); + expect(r.err).toBe(null); + expect(r.stdout).toBe(`value\nFailures: 0\n`); }); }); - test.suite('create', ({ context }) => { - const test = context(async (t) => { + test.suite('create', ({ contextEach }) => { + const test = contextEach(async (t) => { return { service: t.context.tsNodeUnderTest.create({ compilerOptions: { target: 'es5' }, @@ -974,27 +904,31 @@ test.suite('ts-node', (test) => { }; }); - test('should create generic compiler instances', ({ - context: { service }, - }) => { - const output = service.compile('const x = 10', 'test.ts'); + test('should create generic compiler instances', (t) => { + const output = t.context.service.compile('const x = 10', 'test.ts'); expect(output).toMatch('var x = 10;'); }); test.suite('should get type information', (test) => { - test('given position of identifier', ({ context: { service } }) => { + test('given position of identifier', (t) => { expect( - service.getTypeInfo('/**jsdoc here*/const x = 10', 'test.ts', 21) + t.context.service.getTypeInfo( + '/**jsdoc here*/const x = 10', + 'test.ts', + 21 + ) ).toEqual({ comment: 'jsdoc here', name: 'const x: 10', }); }); - test('given position that does not point to an identifier', ({ - context: { service }, - }) => { + test('given position that does not point to an identifier', (t) => { expect( - service.getTypeInfo('/**jsdoc here*/const x = 10', 'test.ts', 0) + t.context.service.getTypeInfo( + '/**jsdoc here*/const x = 10', + 'test.ts', + 0 + ) ).toEqual({ comment: '', name: '', @@ -1079,14 +1013,14 @@ test.suite('ts-node', (test) => { }); test('Falls back to transpileOnly when ts compiler returns emitSkipped', async () => { - const { err, stdout } = await exec( + const r = await exec( `${CMD_TS_NODE_WITHOUT_PROJECT_FLAG} --project tsconfig.json ./outside-rootDir/foo.js`, { cwd: join(TEST_DIR, 'emit-skipped-fallback'), } ); - expect(err).toBe(null); - expect(stdout).toBe('foo\n'); + expect(r.err).toBe(null); + expect(r.stdout).toBe('foo\n'); }); test.suite('node environment', (test) => { @@ -1103,12 +1037,12 @@ test.suite('node environment', (test) => { nodeFlag?: string ) { test(command, async (t) => { - const { err, stderr, stdout } = await exec( + const r = await exec( `${command} --skipIgnore ./recursive-fork/index.ts argv2` ); - expect(err).toBeNull(); - expect(stderr).toBe(''); - const generations = stdout.split('\n'); + expect(r.err).toBeNull(); + expect(r.stderr).toBe(''); + const generations = r.stdout.split('\n'); const expectation = { execArgv: [nodeFlag, BIN_PATH_JS, '--skipIgnore'].filter((v) => v), argv: [ diff --git a/src/test/module-node/1778.spec.ts b/src/test/module-node/1778.spec.ts index 4ec96bf8c..33e673702 100644 --- a/src/test/module-node/1778.spec.ts +++ b/src/test/module-node/1778.spec.ts @@ -17,16 +17,13 @@ const test = context(ctxTsNode); test.suite( 'Issue #1778: typechecker resolver should take importer\'s module type -- cjs or esm -- into account when resolving package.json "exports"', (test) => { - test.runIf(tsSupportsStableNodeNextNode16); + test.if(tsSupportsStableNodeNextNode16); test('test', async () => { - const { err, stdout } = await exec( - `${CMD_TS_NODE_WITHOUT_PROJECT_FLAG} ./index.ts`, - { - cwd: join(TEST_DIR, '1778'), - } - ); - expect(err).toBe(null); - expect(stdout).toBe('{ esm: true }\n'); + const r = await exec(`${CMD_TS_NODE_WITHOUT_PROJECT_FLAG} ./index.ts`, { + cwd: join(TEST_DIR, '1778'), + }); + expect(r.err).toBe(null); + expect(r.stdout).toBe('{ esm: true }\n'); }); } ); diff --git a/src/test/module-node/module-node.spec.ts b/src/test/module-node/module-node.spec.ts index 7ebb3fd4c..183a7dba0 100644 --- a/src/test/module-node/module-node.spec.ts +++ b/src/test/module-node/module-node.spec.ts @@ -9,7 +9,12 @@ import { import * as Path from 'path'; import { ctxTsNode } from '../helpers'; import { exec } from '../exec-helpers'; -import { file, project, ProjectAPI as ProjectAPI } from '../fs-helpers'; +import { + file, + project, + ProjectAPI as ProjectAPI, + StringFile, +} from '@TypeStrong/fs-fixture-builder'; const test = context(ctxTsNode); test.beforeEach(async () => { @@ -19,7 +24,7 @@ type Test = typeof test; // Declare one test case for each permutations of project configuration test.suite('TypeScript module=NodeNext and Node16', (test) => { - test.runIf( + test.if( tsSupportsStableNodeNextNode16 && nodeSupportsImportingTransformedCjsFromEsm ); @@ -59,14 +64,14 @@ function declareTest(test: Test, testParams: TestParams) { // All assertions happen within the fixture scripts // Zero exit code indicates a passing test - const { stdout, stderr, err } = await exec( + const r = await exec( `${CMD_TS_NODE_WITHOUT_PROJECT_FLAG} --esm ./index.mjs`, { cwd: proj.cwd } ); - t.log(stdout); - t.log(stderr); - expect(err).toBe(null); - expect(stdout).toMatch(/done\n$/); + t.log(r.stdout); + t.log(r.stderr); + expect(r.err).toBe(null); + expect(r.stdout).toMatch(/done\n$/); }); } @@ -254,6 +259,7 @@ function createImporter( return; const importer = { + type: 'string', path: `${name.replace(/ /g, '_')}.${importerExtension.ext}`, imports: '', assertions: '', @@ -267,7 +273,7 @@ function createImporter( `; }, }; - proj.add(importer); + proj.add(importer as StringFile); if (!importerExtension.isJs) importer.imports += `export {};\n`; diff --git a/src/test/pluggable-dep-resolution.spec.ts b/src/test/pluggable-dep-resolution.spec.ts index 398442f86..98c12f16e 100644 --- a/src/test/pluggable-dep-resolution.spec.ts +++ b/src/test/pluggable-dep-resolution.spec.ts @@ -8,7 +8,7 @@ const test = context(ctxTsNode); test.suite( 'Pluggable dependency (compiler, transpiler, swc backend) is require()d relative to the tsconfig file that declares it', (test) => { - test.runSerially(); + test.serial(); // The use-case we want to support: // diff --git a/src/test/register.spec.ts b/src/test/register.spec.ts index 721e6d032..6187f495c 100644 --- a/src/test/register.spec.ts +++ b/src/test/register.spec.ts @@ -21,14 +21,12 @@ const createOptions: tsNodeTypes.CreateOptions = { }, }; -const test = context(ctxTsNode).context( - once(async (t) => { - return { - moduleTestPath: resolve(__dirname, '../../tests/module.ts'), - service: t.context.tsNodeUnderTest.create(createOptions), - }; - }) -); +const test = context(ctxTsNode).context(async (t) => { + return { + moduleTestPath: resolve(__dirname, '../../tests/module.ts'), + service: t.context.tsNodeUnderTest.create(createOptions), + }; +}); test.beforeEach(async (t) => { // Un-install all hook and remove our test module from cache resetNodeEnvironment(); @@ -38,7 +36,7 @@ test.beforeEach(async (t) => { "Unexpected token 'export'" ); }); -test.runSerially(); +test.serial(); test('create() does not register()', async (t) => { // nyc sets its own `require.extensions` hooks; to truly detect if we're @@ -136,7 +134,7 @@ test.suite('register(create(options))', (test) => { test.suite('JSX preserve', (test) => { let compiled: string; - test.beforeAll(async () => { + test.before(async () => { const old = require.extensions['.tsx']!; require.extensions['.tsx'] = (m: any, fileName) => { const _compile = m._compile; diff --git a/src/test/regression.spec.ts b/src/test/regression.spec.ts index ddcf179eb..077dd88c8 100644 --- a/src/test/regression.spec.ts +++ b/src/test/regression.spec.ts @@ -26,14 +26,16 @@ test('#1488 regression test', async () => { // `./package.json` needs to be fetched into cache via `assertScriptCanLoadAsCJS` which caused a recursive `require()` call // Circular dependency warning is emitted by node - const { stdout, stderr } = await exec({ + const r = await exec({ exec: createExec({ cwd: join(TEST_DIR, '1488'), }), cmd: `${CMD_TS_NODE_WITHOUT_PROJECT_FLAG} ./index.js`, }); + exp(r.err).toBeNull(); + // Assert that we do *not* get `Warning: Accessing non-existent property 'getOptionValue' of module exports inside circular dependency` - exp(stdout).toBe('foo\n'); // prove that it ran - exp(stderr).toBe(''); // prove that no warnings + exp(r.stdout).toBe('foo\n'); // prove that it ran + exp(r.stderr).toBe(''); // prove that no warnings }); diff --git a/src/test/repl/helpers.ts b/src/test/repl/helpers.ts index 1656010aa..46b3101ae 100644 --- a/src/test/repl/helpers.ts +++ b/src/test/repl/helpers.ts @@ -1,7 +1,8 @@ import { PassThrough } from 'stream'; -import { delay, getStream, TEST_DIR, tsNodeTypes, ctxTsNode } from '../helpers'; +import { delay, TEST_DIR, tsNodeTypes, ctxTsNode } from '../helpers'; import type { ExecutionContext } from 'ava'; import { test, expect } from '../testlib'; +import { expectStream } from '@cspotcode/expect-stream'; export interface CreateReplViaApiOptions { registerHooks: boolean; @@ -78,8 +79,8 @@ export async function ctxRepl(t: ctxTsNode.T) { stdin.write(input); stdin.end(); - const stdoutPromise = getStream(stdout); - const stderrPromise = getStream(stderr); + const stdoutPromise = expectStream(stdout); + const stderrPromise = expectStream(stderr); // Wait for expected output pattern or timeout, whichever comes first await Promise.race([ delay(waitMs), @@ -129,13 +130,13 @@ async function macroReplInternal( waitPattern: string, options?: Partial ) { - const { stdout, stderr } = await t.context.executeInRepl(script, { + const r = await t.context.executeInRepl(script, { registerHooks: true, startInternalOptions: { useGlobal: false }, waitPattern, ...options, }); - if (stderrContains) expect(stderr).toContain(stderrContains); - else expect(stderr).toBe(''); - if (stdoutContains) expect(stdout).toContain(stdoutContains); + if (stderrContains) expect(r.stderr).toContain(stderrContains); + else expect(r.stderr).toBe(''); + if (stdoutContains) expect(r.stdout).toContain(stdoutContains); } diff --git a/src/test/repl/repl-environment.spec.ts b/src/test/repl/repl-environment.spec.ts index d02341f1e..bcc1bf672 100644 --- a/src/test/repl/repl-environment.spec.ts +++ b/src/test/repl/repl-environment.spec.ts @@ -4,7 +4,7 @@ */ import { context, expect } from '../testlib'; -import * as getStream from 'get-stream'; +import { expectStream } from '@cspotcode/expect-stream'; import { CMD_TS_NODE_WITH_PROJECT_FLAG, ctxTsNode, @@ -16,7 +16,7 @@ import { createExec, createExecTester } from '../exec-helpers'; import { homedir } from 'os'; import { ctxRepl } from './helpers'; -const test = context(ctxTsNode).context(ctxRepl); +const test = context(ctxTsNode).contextEach(ctxRepl); const exec = createExec({ cwd: TEST_DIR, @@ -84,8 +84,8 @@ test.suite( done = true; stdout.end(); stderr.end(); - expect(await getStream(stderr)).toBe(''); - await assertions(await getStream(stdout)); + expect(await expectStream(stderr)).toBe(''); + await assertions(await expectStream(stdout)); } ); @@ -149,11 +149,11 @@ test.suite( const tsNodeExe = expect.stringMatching(/\b(ts-node|bin.js)$/); test('stdin', async (t) => { - const { stdout } = await execTester({ + const r = await execTester({ stdin: `${setReportGlobal('stdin')};${printReports}`, flags: '', }); - const report = parseStdout(stdout); + const report = parseStdout(r.stdout); expect(report).toMatchObject({ stdinReport: { __filename: '[stdin]', @@ -176,11 +176,11 @@ test.suite( }); }); test('repl', async (t) => { - const { stdout } = await execTester({ + const r = await execTester({ stdin: `${setReportGlobal('repl')};${printReports}`, flags: '-i', }); - const report = parseStdoutStripReplPrompt(stdout); + const report = parseStdoutStripReplPrompt(r.stdout); expect(report).toMatchObject({ stdinReport: false, evalReport: false, @@ -214,11 +214,11 @@ test.suite( // Should ignore -i and run the entrypoint test('-i w/entrypoint ignores -i', async (t) => { - const { stdout } = await execTester({ + const r = await execTester({ stdin: `${setReportGlobal('repl')};${printReports}`, flags: '-i ./repl/script.js', }); - const report = parseStdout(stdout); + const report = parseStdout(r.stdout); expect(report).toMatchObject({ stdinReport: false, evalReport: false, @@ -229,11 +229,11 @@ test.suite( // Should not execute stdin // Should not interpret positional arg as an entrypoint script test('-e', async (t) => { - const { stdout } = await execTester({ + const r = await execTester({ stdin: `throw new Error()`, flags: `-e "${setReportGlobal('eval')};${printReports}"`, }); - const report = parseStdout(stdout); + const report = parseStdout(r.stdout); expect(report).toMatchObject({ stdinReport: false, evalReport: { @@ -256,13 +256,13 @@ test.suite( }); }); test('-e w/entrypoint arg does not execute entrypoint', async (t) => { - const { stdout } = await execTester({ + const r = await execTester({ stdin: `throw new Error()`, flags: `-e "${setReportGlobal( 'eval' )};${printReports}" ./repl/script.js`, }); - const report = parseStdout(stdout); + const report = parseStdout(r.stdout); expect(report).toMatchObject({ stdinReport: false, evalReport: { @@ -285,13 +285,13 @@ test.suite( }); }); test('-e w/non-path arg', async (t) => { - const { stdout } = await execTester({ + const r = await execTester({ stdin: `throw new Error()`, flags: `-e "${setReportGlobal( 'eval' )};${printReports}" ./does-not-exist.js`, }); - const report = parseStdout(stdout); + const report = parseStdout(r.stdout); expect(report).toMatchObject({ stdinReport: false, evalReport: { @@ -314,11 +314,11 @@ test.suite( }); }); test('-e -i', async (t) => { - const { stdout } = await execTester({ + const r = await execTester({ stdin: `${setReportGlobal('repl')};${printReports}`, flags: `-e "${setReportGlobal('eval')}" -i`, }); - const report = parseStdoutStripReplPrompt(stdout); + const report = parseStdoutStripReplPrompt(r.stdout); expect(report).toMatchObject({ stdinReport: false, evalReport: { @@ -366,11 +366,11 @@ test.suite( }); test('-e -i w/entrypoint ignores -e and -i, runs entrypoint', async (t) => { - const { stdout } = await execTester({ + const r = await execTester({ stdin: `throw new Error()`, flags: '-e "throw new Error()" -i ./repl/script.js', }); - const report = parseStdout(stdout); + const report = parseStdout(r.stdout); expect(report).toMatchObject({ stdinReport: false, evalReport: false, @@ -379,14 +379,14 @@ test.suite( }); test('-e -i when -e throws error, -i does not run', async (t) => { - const { stdout, stderr, err } = await execTester({ + const r = await execTester({ stdin: `console.log('hello')`, flags: `-e "throw new Error('error from -e')" -i`, expectError: true, }); - expect(err).toBeDefined(); - expect(stdout).toBe(''); - expect(stderr).toContain('error from -e'); + expect(r.err).toBeDefined(); + expect(r.stdout).toBe(''); + expect(r.stderr).toContain('error from -e'); }); // Serial because it's timing-sensitive diff --git a/src/test/repl/repl.spec.ts b/src/test/repl/repl.spec.ts index 62062f66f..1000958f4 100644 --- a/src/test/repl/repl.spec.ts +++ b/src/test/repl/repl.spec.ts @@ -1,12 +1,7 @@ import { context, expect } from '../testlib'; import { delay, resetNodeEnvironment, ts } from '../helpers'; import semver = require('semver'); -import { - CMD_TS_NODE_WITH_PROJECT_FLAG, - ctxTsNode, - getStream, - TEST_DIR, -} from '../helpers'; +import { CMD_TS_NODE_WITH_PROJECT_FLAG, ctxTsNode, TEST_DIR } from '../helpers'; import { createExec, createExecTester } from '../exec-helpers'; import { upstreamTopLevelAwaitTests } from './node-repl-tla'; import { @@ -14,9 +9,10 @@ import { macroReplNoErrorsAndStdoutContains, macroReplStderrContains, } from './helpers'; +import { expectStream } from '@cspotcode/expect-stream'; -const test = context(ctxTsNode).context(ctxRepl); -test.runSerially(); +const test = context(ctxTsNode).contextEach(ctxRepl); +test.serial(); test.beforeEach(async (t) => { t.teardown(() => { resetNodeEnvironment(); @@ -36,29 +32,29 @@ const execTester = createExecTester({ }); test('should run REPL when --interactive passed and stdin is not a TTY', async () => { - const execPromise = exec(`${CMD_TS_NODE_WITH_PROJECT_FLAG} --interactive`); - execPromise.child.stdin!.end('console.log("123")\n'); - const { err, stdout } = await execPromise; - expect(err).toBe(null); - expect(stdout).toBe('> 123\n' + 'undefined\n' + '> '); + const p = exec(`${CMD_TS_NODE_WITH_PROJECT_FLAG} --interactive`); + p.child.stdin!.end('console.log("123")\n'); + const r = await p; + expect(r.err).toBe(null); + expect(r.stdout).toBe('> 123\n' + 'undefined\n' + '> '); }); test('should echo a value when using the swc transpiler', async () => { - const execPromise = exec( + const p = exec( `${CMD_TS_NODE_WITH_PROJECT_FLAG} --interactive --transpiler ts-node/transpilers/swc-experimental` ); - execPromise.child.stdin!.end('400\n401\n'); - const { err, stdout } = await execPromise; - expect(err).toBe(null); - expect(stdout).toBe('> 400\n> 401\n> '); + p.child.stdin!.end('400\n401\n'); + const r = await p; + expect(r.err).toBe(null); + expect(r.stdout).toBe('> 400\n> 401\n> '); }); test('REPL has command to get type information', async () => { - const execPromise = exec(`${CMD_TS_NODE_WITH_PROJECT_FLAG} --interactive`); - execPromise.child.stdin!.end('\nconst a = 123\n.type a'); - const { err, stdout } = await execPromise; - expect(err).toBe(null); - expect(stdout).toBe( + const p = exec(`${CMD_TS_NODE_WITH_PROJECT_FLAG} --interactive`); + p.child.stdin!.end('\nconst a = 123\n.type a'); + const r = await p; + expect(r.err).toBe(null); + expect(r.stdout).toBe( '> undefined\n' + '> undefined\n' + '> const a: 123\n' + '> ' ); }); @@ -67,27 +63,24 @@ test('REPL has command to get type information', async () => { test.serial('REPL can be configured on `start`', async (t) => { const prompt = '#> '; - const { stdout, stderr } = await t.context.executeInRepl( - `const x = 3\n'done'`, - { - waitPattern: "'done'\n#> ", - registerHooks: true, - startInternalOptions: { - prompt, - ignoreUndefined: true, - }, - } - ); + const r = await t.context.executeInRepl(`const x = 3\n'done'`, { + waitPattern: "'done'\n#> ", + registerHooks: true, + startInternalOptions: { + prompt, + ignoreUndefined: true, + }, + }); - expect(stderr).toBe(''); - expect(stdout).toBe(`${prompt}${prompt}'done'\n#> `); + expect(r.stderr).toBe(''); + expect(r.stdout).toBe(`${prompt}${prompt}'done'\n#> `); }); // Serial because it's timing-sensitive test.serial( 'REPL uses a different context when `useGlobal` is false', async (t) => { - const { stdout, stderr } = await t.context.executeInRepl( + const r = await t.context.executeInRepl( // No error when re-declaring x 'const x = 3\n' + // console.log ouput will end up in the stream and not in test output @@ -101,31 +94,28 @@ test.serial( } ); - expect(stderr).toBe(''); - expect(stdout).toBe(`> undefined\n> 1\nundefined\n> `); + expect(r.stderr).toBe(''); + expect(r.stdout).toBe(`> undefined\n> 1\nundefined\n> `); } ); // Serial because it's timing-sensitive test.serial('REPL can be created via API', async (t) => { - const { stdout, stderr } = await t.context.executeInRepl( - `\nconst a = 123\n.type a\n`, - { - registerHooks: true, - waitPattern: '123\n> ', - } - ); - expect(stderr).toBe(''); - expect(stdout).toBe( + const r = await t.context.executeInRepl(`\nconst a = 123\n.type a\n`, { + registerHooks: true, + waitPattern: '123\n> ', + }); + expect(r.stderr).toBe(''); + expect(r.stdout).toBe( '> undefined\n' + '> undefined\n' + '> const a: 123\n' + '> ' ); }); -test.suite('top level await', ({ context }) => { +test.suite('top level await', ({ contextEach }) => { const compilerOptions = { target: 'es2018', }; - const test = context(async (t) => { + const test = contextEach(async (t) => { return { executeInTlaRepl }; function executeInTlaRepl(input: string, waitPattern?: string | RegExp) { @@ -161,12 +151,9 @@ test.suite('top level await', ({ context }) => { x + y + z; `; - const { stdout, stderr } = await t.context.executeInTlaRepl( - script, - '6\n> ' - ); - expect(stderr).toBe(''); - expect(stdout).toBe('> 1\n2\n3\na\nb\n6\n> '); + const r = await t.context.executeInTlaRepl(script, '6\n> '); + expect(r.stderr).toBe(''); + expect(r.stdout).toBe('> 1\n2\n3\na\nb\n6\n> '); }); // Serial because it's timing-sensitive @@ -180,14 +167,14 @@ test.suite('top level await', ({ context }) => { const endTime = new Date().getTime(); endTime - startTime; `; - const { stdout, stderr } = await t.context.executeInTlaRepl( - script, - /\d+\n/ - ); + const r = await t.context.executeInTlaRepl(script, /\d+\n/); - expect(stderr).toBe(''); + expect(r.stderr).toBe(''); - const elapsedTimeString = stdout.split('\n')[0].replace('> ', '').trim(); + const elapsedTimeString = r.stdout + .split('\n')[0] + .replace('> ', '') + .trim(); expect(elapsedTimeString).toMatch(/^\d+$/); const elapsedTime = Number(elapsedTimeString); expect(elapsedTime).toBeGreaterThanOrEqual(awaitMs - 50); @@ -208,15 +195,12 @@ test.suite('top level await', ({ context }) => { const endTime = new Date().getTime(); endTime - startTime; `; - const { stdout, stderr } = await t.context.executeInTlaRepl( - script, - /\d+\n/ - ); + const r = await t.context.executeInTlaRepl(script, /\d+\n/); - expect(stderr).toBe(''); + expect(r.stderr).toBe(''); const ellapsedTime = Number( - stdout.split('\n')[0].replace('> ', '').trim() + r.stdout.split('\n')[0].replace('> ', '').trim() ); expect(ellapsedTime).toBeGreaterThanOrEqual(0); // Should ideally be instantaneous; leave wiggle-room for slow CI @@ -228,13 +212,13 @@ test.suite('top level await', ({ context }) => { test.serial( 'should error with typing information when awaited result has type mismatch', async (t) => { - const { stdout, stderr } = await t.context.executeInTlaRepl( + const r = await t.context.executeInTlaRepl( 'const x: string = await 1', 'error' ); - expect(stdout).toBe('> > '); - expect(stderr.replace(/\r\n/g, '\n')).toBe( + expect(r.stdout).toBe('> > '); + expect(r.stderr.replace(/\r\n/g, '\n')).toBe( '.ts(4,7): error TS2322: ' + (semver.gte(ts.version, '4.0.0') ? `Type 'number' is not assignable to type 'string'.\n` @@ -248,13 +232,13 @@ test.suite('top level await', ({ context }) => { test.serial( 'should error with typing information when importing a file with type errors', async (t) => { - const { stdout, stderr } = await t.context.executeInTlaRepl( + const r = await t.context.executeInTlaRepl( `const {foo} = await import('./repl/tla-import');`, 'error' ); - expect(stdout).toBe('> > '); - expect(stderr.replace(/\r\n/g, '\n')).toBe( + expect(r.stdout).toBe('> > '); + expect(r.stderr.replace(/\r\n/g, '\n')).toBe( 'repl/tla-import.ts(1,14): error TS2322: ' + (semver.gte(ts.version, '4.0.0') ? `Type 'number' is not assignable to type 'string'.\n` @@ -276,32 +260,32 @@ test.suite( const code = `function foo() {};\nfunction foo() {return 123};\nconsole.log(foo());\n`; const diagnosticMessage = `Duplicate function implementation`; test('interactive repl should ignore them', async (t) => { - const { stdout, stderr } = await execTester({ + const r = await execTester({ flags: '-i', stdin: code, }); - expect(stdout).not.toContain(diagnosticMessage); + expect(r.stdout).not.toContain(diagnosticMessage); }); test('interactive repl should not ignore them if they occur in other files', async (t) => { - const { stdout, stderr } = await execTester({ + const r = await execTester({ flags: '-i', stdin: `import './repl-ignored-diagnostics/index';\n`, }); - expect(stderr).toContain(diagnosticMessage); + expect(r.stderr).toContain(diagnosticMessage); }); test('[stdin] should not ignore them', async (t) => { - const { stdout, stderr } = await execTester({ + const r = await execTester({ stdin: code, expectError: true, }); - expect(stderr).toContain(diagnosticMessage); + expect(r.stderr).toContain(diagnosticMessage); }); test('[eval] should not ignore them', async (t) => { - const { stdout, stderr } = await execTester({ + const r = await execTester({ flags: `-e "${code.replace(/\n/g, '')}"`, expectError: true, }); - expect(stderr).toContain(diagnosticMessage); + expect(r.stderr).toContain(diagnosticMessage); }); } ); @@ -310,10 +294,10 @@ test.suite( 'REPL inputs are syntactically independent of each other', (test) => { // Serial because they're timing-sensitive - test.runSerially(); + test.serial(); test('arithmetic operators are independent of previous values', async (t) => { - const { stdout, stderr } = await t.context.executeInRepl( + const r = await t.context.executeInRepl( `9 + 3 7 @@ -332,17 +316,17 @@ test.suite( waitPattern: 'done!\nundefined\n>', } ); - expect(stdout).not.toContain('12'); - expect(stdout).not.toContain('4'); - expect(stdout).not.toContain('21'); - expect(stdout).not.toContain('50'); - expect(stdout).not.toContain('25'); - expect(stdout).toContain('3'); - expect(stdout).toContain('-3'); + expect(r.stdout).not.toContain('12'); + expect(r.stdout).not.toContain('4'); + expect(r.stdout).not.toContain('21'); + expect(r.stdout).not.toContain('50'); + expect(r.stdout).not.toContain('25'); + expect(r.stdout).toContain('3'); + expect(r.stdout).toContain('-3'); }); test('automatically inserted semicolons do not appear in error messages at the end', async (t) => { - const { stdout, stderr } = await t.context.executeInRepl( + const r = await t.context.executeInRepl( `( a console.log('done!')`, @@ -352,12 +336,12 @@ test.suite( waitPattern: 'done!\nundefined\n>', } ); - expect(stderr).toContain("error TS1005: ')' expected."); - expect(stderr).not.toContain(';'); + expect(r.stderr).toContain("error TS1005: ')' expected."); + expect(r.stderr).not.toContain(';'); }); test('automatically inserted semicolons do not appear in error messages at the start', async (t) => { - const { stdout, stderr } = await t.context.executeInRepl( + const r = await t.context.executeInRepl( `) console.log('done!')`, { @@ -366,15 +350,15 @@ test.suite( waitPattern: 'done!\nundefined\n>', } ); - expect(stderr).toContain( + expect(r.stderr).toContain( 'error TS1128: Declaration or statement expected.' ); - expect(stderr).toContain(')'); - expect(stderr).not.toContain(';'); + expect(r.stderr).toContain(')'); + expect(r.stderr).not.toContain(';'); }); test('automatically inserted semicolons do not break function calls', async (t) => { - const { stdout, stderr } = await t.context.executeInRepl( + const r = await t.context.executeInRepl( `function foo(a: number) { return a + 1; } @@ -387,15 +371,15 @@ test.suite( waitPattern: '2\n>', } ); - expect(stderr).toBe(''); - expect(stdout).toContain('2'); + expect(r.stderr).toBe(''); + expect(r.stdout).toContain('2'); }); test('automatically inserted semicolons do not affect subsequent line numbers', async (t) => { // If first line of input ends in a semicolon, should not add a second semicolon. // That will cause an extra blank line in the compiled output which will // offset the stack line number. - const { stdout, stderr } = await t.context.executeInRepl( + const r = await t.context.executeInRepl( `1; new Error().stack!.split('\\n')[1] console.log('done!')`, @@ -405,14 +389,14 @@ test.suite( waitPattern: 'done!', } ); - expect(stderr).toBe(''); - expect(stdout).toContain(":1:1'\n"); + expect(r.stderr).toBe(''); + expect(r.stdout).toContain(":1:1'\n"); }); } ); test.suite('Multiline inputs and RECOVERY_CODES', (test) => { - test.runSerially(); + test.serial(); test( 'multiline function args declaration', macroReplNoErrorsAndStdoutContains, @@ -461,7 +445,7 @@ test.suite('REPL works with traceResolution', (test) => { await delay(3e3); repl.stdout.end(); - const stdout = await getStream(repl.stdout); + const stdout = await expectStream(repl.stdout); expect(stdout).toContain('======== Resolving module'); expect(stdout.endsWith('> ')).toBe(true); @@ -471,21 +455,21 @@ test.suite('REPL works with traceResolution', (test) => { test.serial( 'traces should NOT appear when traceResolution is not enabled', async (t) => { - const { stdout, stderr } = await t.context.executeInRepl('1', { + const r = await t.context.executeInRepl('1', { registerHooks: true, startInternalOptions: { useGlobal: false }, waitPattern: '1\n>', }); - expect(stderr).toBe(''); - expect(stdout).not.toContain('======== Resolving module'); + expect(r.stderr).toBe(''); + expect(r.stdout).not.toContain('======== Resolving module'); } ); }); test.suite('REPL declares types for node built-ins within REPL', (test) => { - test.runSerially(); + test.serial(); test('enabled when typechecking', async (t) => { - const { stdout, stderr } = await t.context.executeInRepl( + const r = await t.context.executeInRepl( `util.promisify(setTimeout)("should not be a string" as string) type Duplex = stream.Duplex const s = stream @@ -501,18 +485,18 @@ test.suite('REPL declares types for node built-ins within REPL', (test) => { // Assert that we receive a typechecking error about improperly using // `util.promisify` but *not* an error about the absence of `util` - expect(stderr).not.toMatch("Cannot find name 'util'"); - expect(stderr).toMatch( + expect(r.stderr).not.toMatch("Cannot find name 'util'"); + expect(r.stderr).toMatch( "Argument of type 'string' is not assignable to parameter of type 'number'" ); // Assert that both types and values can be used without error - expect(stderr).not.toMatch("Cannot find namespace 'stream'"); - expect(stderr).not.toMatch("Cannot find name 'stream'"); - expect(stdout).toMatch(`done`); + expect(r.stderr).not.toMatch("Cannot find namespace 'stream'"); + expect(r.stderr).not.toMatch("Cannot find name 'stream'"); + expect(r.stdout).toMatch(`done`); }); test('disabled in transpile-only mode, to avoid breaking third-party SWC transpiler which rejects `declare import` syntax', async (t) => { - const { stdout, stderr } = await t.context.executeInRepl( + const r = await t.context.executeInRepl( `type Duplex = stream.Duplex const s = stream 'done'`, @@ -529,8 +513,8 @@ test.suite('REPL declares types for node built-ins within REPL', (test) => { ); // Assert that we do not get errors about `declare import` syntax from swc - expect(stdout).toBe("> undefined\n> undefined\n> 'done'\n> "); - expect(stderr).toBe(''); + expect(r.stdout).toBe("> undefined\n> undefined\n> 'done'\n> "); + expect(r.stderr).toBe(''); }); }); diff --git a/src/test/resolver.spec.ts b/src/test/resolver.spec.ts index 508331739..9bf1e031b 100755 --- a/src/test/resolver.spec.ts +++ b/src/test/resolver.spec.ts @@ -7,7 +7,10 @@ import { tsSupportsMtsCtsExtensions, tsSupportsStableNodeNextNode16, } from './helpers'; -import { project as fsProject, Project as FsProject } from './fs-helpers'; +import { + project as fsProject, + Project as FsProject, +} from '@TypeStrong/fs-fixture-builder'; import { join } from 'path'; import * as semver from 'semver'; import { padStart } from 'lodash'; @@ -157,8 +160,8 @@ const targetPackageStyles = [ ] as const; test.suite('Resolver hooks', (test) => { - test.runSerially(); - test.runIf(tsSupportsMtsCtsExtensions); + test.serial(); + test.if(tsSupportsMtsCtsExtensions); // // Generate all permutations of projects diff --git a/src/test/sourcemaps.spec.ts b/src/test/sourcemaps.spec.ts index 26a0cb044..136872b5e 100644 --- a/src/test/sourcemaps.spec.ts +++ b/src/test/sourcemaps.spec.ts @@ -12,10 +12,11 @@ const exec = createExecTester({ }); test('Redirects source-map-support to @cspotcode/source-map-support so that third-party libraries get correct source-mapped locations', async () => { - const { stdout } = await exec({ + const r = await exec({ flags: `./legacy-source-map-support-interop/index.ts`, }); - expect(stdout.split('\n')).toMatchObject([ + expect(r.err).toBeNull(); + expect(r.stdout.split('\n')).toMatchObject([ expect.stringContaining('.ts:2 '), 'true', 'true', diff --git a/src/test/testlib.ts b/src/test/testlib.ts index dcc716c17..0dac4834a 100644 --- a/src/test/testlib.ts +++ b/src/test/testlib.ts @@ -1,346 +1,4 @@ -/* - * Extensions to ava, for declaring and running test cases and suites - * Utilities specific to testing ts-node, for example handling streams and exec-ing processes, - * should go in a separate module. - */ - -import avaTest, { - ExecutionContext, - Implementation, - ImplementationFn, - Macro, - MacroDeclarationOptions, - MacroFn, - TestFn, -} from 'ava'; -import * as assert from 'assert'; -import throat from 'throat'; -import * as expect from 'expect'; - -export { ExecutionContext, expect }; - // HACK ensure ts-node-specific bootstrapping is executed import './helpers'; -// NOTE: this limits concurrency within a single process, but AVA launches -// each .spec file in its own process, so actual concurrency is higher. -const concurrencyLimiter = throat(4); - -function errorPostprocessor(fn: T): T { - return async function (this: any) { - try { - return await fn.call(this, arguments); - } catch (error: any) { - delete error?.matcherResult; - // delete error?.matcherResult?.message; - if (error?.message) error.message = `\n${error.message}\n`; - throw error; - } - } as any; -} - -function once(func: T): T { - let run = false; - let ret: any = undefined; - return function (...args: any[]) { - if (run) return ret; - run = true; - ret = func(...args); - return ret; - } as any as T; -} - -export const test = createTestInterface({ - beforeEachFunctions: [], - mustDoSerial: false, - automaticallyDoSerial: false, - automaticallySkip: false, - // The little right chevron used by ava - separator: ' \u203a ', - titlePrefix: undefined, -}); -// In case someone wants to `const test = context()` -export const context = test.context; - -export type SimpleTitleFn = (providedTitle: string | undefined) => string; -export type SimpleImplementationFn = ( - t: ExecutionContext -) => PromiseLike; -export type SimpleContextFn = ( - t: ExecutionContext -) => Promise; - -export interface TestInterface< - Context -> /*extends Omit, 'before' | 'beforeEach' | 'after' | 'afterEach' | 'failing' | 'serial'>*/ { - //#region copy-pasted from ava's .d.ts - /** Declare a concurrent test. */ - (title: string, implementation: Implementation): void; - /** Declare a concurrent test that uses one or more macros. Additional arguments are passed to the macro. */ - ( - title: string, - implementation: Implementation, - ...rest: T - ): void; - /** Declare a concurrent test that uses one or more macros. The macro is responsible for generating a unique test title. */ - (macro: Implementation, ...rest: T): void; - //#endregion - - serial( - title: string, - implementation: Implementation - ): void; - /** Declare a concurrent test that uses one or more macros. Additional arguments are passed to the macro. */ - serial( - title: string, - implementation: Implementation, - ...rest: T - ): void; - /** Declare a concurrent test that uses one or more macros. The macro is responsible for generating a unique test title. */ - serial( - implementation: Implementation, - ...rest: T - ): void; - skip(title: string, implementation: Implementation): void; - /** Declare a concurrent test that uses one or more macros. Additional arguments are passed to the macro. */ - skip( - title: string, - implementation: Implementation, - ...rest: T - ): void; - /** Declare a concurrent test that uses one or more macros. The macro is responsible for generating a unique test title. */ - skip( - implementation: Implementation, - ...rest: T - ): void; - - macro( - cb: ( - ...args: Args - ) => - | [SimpleTitleFn | string, SimpleImplementationFn] - | SimpleImplementationFn - ): Macro; - - avaMacro: MacroFn; - - beforeAll(cb: SimpleImplementationFn): void; - beforeEach(cb: SimpleImplementationFn): void; - context( - cb: SimpleContextFn - ): TestInterface; - suite(title: string, cb: (test: TestInterface) => void): void; - - runSerially(): void; - - /** Skip tests unless this condition is met */ - skipUnless(conditional: boolean): void; - /** If conditional is true, run tests, otherwise skip them */ - runIf(conditional: boolean): void; - /** If conditional is false, skip tests */ - skipIf(conditional: boolean): void; - - // TODO add teardownEach -} - -function createTestInterface(opts: { - titlePrefix: string | undefined; - separator: string | undefined; - mustDoSerial: boolean; - automaticallyDoSerial: boolean; - automaticallySkip: boolean; - beforeEachFunctions: Function[]; -}): TestInterface { - const { titlePrefix, separator = ' > ' } = opts; - const beforeEachFunctions = [...(opts.beforeEachFunctions ?? [])]; - let { mustDoSerial, automaticallyDoSerial, automaticallySkip } = opts; - let hookDeclared = false; - let suiteOrTestDeclared = false; - function computeTitle( - title: string | undefined, - impl?: Implementation, - ...args: Args - ) { - if (isMacroWithTitle(impl)) { - title = impl.title!(title, ...args); - } - assert(title); - // return `${ titlePrefix }${ separator }${ title }`; - if (titlePrefix != null && title != null) { - return `${titlePrefix}${separator}${title}`; - } - if (titlePrefix == null && title != null) { - return title; - } - } - function parseArgs(args: any[]) { - const title = - typeof args[0] === 'string' ? (args.shift() as string) : undefined; - const impl = args.shift() as Implementation; - return { title, impl, args }; - } - function assertOrderingForDeclaringTest() { - suiteOrTestDeclared = true; - } - function assertOrderingForDeclaringHook() { - if (suiteOrTestDeclared) { - throw new Error( - 'Hooks must be declared before declaring sub-suites or tests' - ); - } - hookDeclared = true; - } - function assertOrderingForDeclaringSkipUnless() { - if (suiteOrTestDeclared) { - throw new Error( - 'skipUnless or runIf must be declared before declaring sub-suites or tests' - ); - } - } - /** - * @param avaDeclareFunction either test or test.serial - */ - function declareTest( - title: string | undefined, - impl: Implementation, - avaDeclareFunction: Function & { skip: Function }, - args: any[], - skip = false - ) { - const wrapped = async function ( - t: ExecutionContext, - ...args: any[] - ) { - return concurrencyLimiter( - errorPostprocessor(async () => { - let i = 0; - for (const func of beforeEachFunctions) { - await func(t); - i++; - } - return isMacro(impl) - ? impl.exec(t, ...args) - : (impl as ImplementationFn)(t, ...args); - }) - ); - }; - const computedTitle = computeTitle(title, impl, ...args); - (automaticallySkip || skip ? avaDeclareFunction.skip : avaDeclareFunction)( - computedTitle, - wrapped, - ...args - ); - } - function test(...inputArgs: any[]) { - assertOrderingForDeclaringTest(); - // TODO is this safe to disable? - // X parallel tests will each invoke the beforeAll hook, but once()ification means each invocation will return the same promise, and tests cannot - // start till it finishes. - // HOWEVER if it returns a single shared state, can tests concurrently use this shared state? - // if(!automaticallyDoSerial && mustDoSerial) throw new Error('Cannot declare non-serial tests because you have declared a beforeAll() hook for this test suite.'); - const { args, impl, title } = parseArgs(inputArgs); - return declareTest( - title, - impl, - automaticallyDoSerial ? avaTest.serial : avaTest, - args - ); - } - test.serial = function (...inputArgs: any[]) { - assertOrderingForDeclaringTest(); - const { args, impl, title } = parseArgs(inputArgs); - return declareTest(title, impl, avaTest.serial, args); - }; - test.skip = function (...inputArgs: any[]) { - assertOrderingForDeclaringTest(); - const { args, impl, title } = parseArgs(inputArgs); - return declareTest(title, impl, avaTest, args, true); - }; - test.beforeEach = function ( - cb: (test: ExecutionContext) => Promise - ) { - assertOrderingForDeclaringHook(); - beforeEachFunctions.push(cb); - }; - test.context = function ( - cb: (test: ExecutionContext) => Promise - ) { - assertOrderingForDeclaringHook(); - beforeEachFunctions.push(async (t: ExecutionContext) => { - const addedContextFields = await cb(t); - Object.assign(t.context, addedContextFields); - }); - return test; - }; - test.beforeAll = function ( - cb: (test: ExecutionContext) => Promise - ) { - assertOrderingForDeclaringHook(); - mustDoSerial = true; - beforeEachFunctions.push(once(cb)); - }; - test.macro = function ( - cb: ( - ...args: Args - ) => - | [SimpleTitleFn | string, SimpleImplementationFn] - | SimpleImplementationFn - ) { - function title(givenTitle: string | undefined, ...args: Args) { - const ret = cb(...args); - return Array.isArray(ret) - ? typeof ret[0] === 'string' - ? ret[0] - : ret[0](givenTitle) - : givenTitle ?? 'UNKNOWN'; - } - function exec(testInterface: ExecutionContext, ...args: Args) { - const ret = cb(...args); - const impl = Array.isArray(ret) ? ret[1] : ret; - return impl(testInterface); - } - const declaration: MacroDeclarationOptions = { - title, - exec, - }; - return (avaTest as TestFn).macro(declaration); - }; - test.avaMacro = (avaTest as TestFn).macro; - test.suite = function ( - title: string, - cb: (test: TestInterface) => void - ) { - suiteOrTestDeclared = true; - const newApi = createTestInterface({ - mustDoSerial, - automaticallyDoSerial, - automaticallySkip, - separator, - titlePrefix: computeTitle(title), - beforeEachFunctions, - }); - cb(newApi); - }; - test.runSerially = function () { - automaticallyDoSerial = true; - }; - test.skipUnless = test.runIf = function (runIfTrue: boolean) { - assertOrderingForDeclaringSkipUnless(); - automaticallySkip = automaticallySkip || !runIfTrue; - }; - test.skipIf = function (skipIfTrue: boolean) { - test.runIf(!skipIfTrue); - }; - return test as any; -} - -function isMacro( - implementation?: Implementation -): implementation is Macro { - return implementation != null && typeof implementation !== 'function'; -} -function isMacroWithTitle( - implementation?: Implementation -): implementation is Macro { - return !!(implementation && (implementation as Macro<[]>)?.title); -} +export * from '@cspotcode/ava-lib'; diff --git a/src/test/transpilers.spec.ts b/src/test/transpilers.spec.ts index 1d64186a0..a25a00678 100644 --- a/src/test/transpilers.spec.ts +++ b/src/test/transpilers.spec.ts @@ -82,7 +82,7 @@ test.suite('swc', (test) => { test(macro, 'react', undefined, undefined); test.suite('react 17 jsx factories', (test) => { - test.runIf(tsSupportsReact17JsxFactories); + test.if(tsSupportsReact17JsxFactories); test(macro, 'react-jsx', 'automatic', undefined); test(macro, 'react-jsxdev', 'automatic', true); }); @@ -122,7 +122,7 @@ test.suite('swc', (test) => { `const div = /*#__PURE__*/ React.createElement("div", null);` ); test.suite('react 17 jsx factories', (test) => { - test.runIf(tsSupportsReact17JsxFactories); + test.if(tsSupportsReact17JsxFactories); test( compileMacro, { jsx: 'react-jsx' }, @@ -149,7 +149,7 @@ test.suite('swc', (test) => { }); test.suite('preserves import assertions for json imports', (test) => { - test.runIf(tsSupportsImportAssertions); + test.if(tsSupportsImportAssertions); test( 'basic json import', compileMacro, diff --git a/src/test/ts-import-specifiers.spec.ts b/src/test/ts-import-specifiers.spec.ts index 5dd3c87dc..68d9215e0 100644 --- a/src/test/ts-import-specifiers.spec.ts +++ b/src/test/ts-import-specifiers.spec.ts @@ -7,7 +7,7 @@ import { CMD_TS_NODE_WITHOUT_PROJECT_FLAG, tsSupportsAllowImportingTsExtensions, } from './helpers'; -import { project as fsProject, Project as FsProject } from './fs-helpers'; +import { project as fsProject } from '@TypeStrong/fs-fixture-builder'; import { outdent as o } from 'outdent'; const exec = createExec({ @@ -54,10 +54,9 @@ test('Supports .ts extensions in import specifiers with typechecking, even thoug }); p.write(); - const { err, stdout } = await exec( - `${CMD_TS_NODE_WITHOUT_PROJECT_FLAG} ./index.ts`, - { cwd: p.cwd } - ); - expect(err).toBe(null); - expect(stdout.trim()).toBe('{ foo: true, bar: true }'); + const r = await exec(`${CMD_TS_NODE_WITHOUT_PROJECT_FLAG} ./index.ts`, { + cwd: p.cwd, + }); + expect(r.err).toBe(null); + expect(r.stdout.trim()).toBe('{ foo: true, bar: true }'); }); diff --git a/yarn.lock b/yarn.lock index 73900a1cd..5a4ed1632 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5,6 +5,15 @@ __metadata: version: 6 cacheKey: 8 +"@TypeStrong/fs-fixture-builder@github:TypeStrong/fs-fixture-builder#8abd1494280116ff5318dde2c50ad01e1663790c": + version: 0.0.0 + resolution: "@TypeStrong/fs-fixture-builder@https://github.com/TypeStrong/fs-fixture-builder.git#commit=8abd1494280116ff5318dde2c50ad01e1663790c" + bin: + capture-fs-fixture: ./dist/capture-fixture.ts + checksum: f9fc2cdf58b79470373fbac56449540c8c727553f13b9a402e5dcec47b0c909fca2d246cb09f6b55632ff823fa4da3332a593f18720e4227f88478be566898c1 + languageName: node + linkType: hard + "@ampproject/remapping@npm:^2.1.0": version: 2.2.0 resolution: "@ampproject/remapping@npm:2.2.0" @@ -232,6 +241,26 @@ __metadata: languageName: node linkType: hard +"@cspotcode/ava-lib@github:cspotcode/ava-lib#edcc1885d192d08d5af83490af9341468402ec41": + version: 0.0.1 + resolution: "@cspotcode/ava-lib@https://github.com/cspotcode/ava-lib.git#commit=edcc1885d192d08d5af83490af9341468402ec41" + dependencies: + "@types/node": "*" + throat: ^6.0.1 + peerDependencies: + ava: "*" + expect: "*" + checksum: 82a6ad95a7bb32d9183fdf77e2bd4b16cb4c7e97c9dfee34e30c8ffcabc4dd3c10fa5b2c5781cf4a443e12d43ad3b01df59814492d81777d4f3876fc4846d3f2 + languageName: node + linkType: hard + +"@cspotcode/expect-stream@github:cspotcode/node-expect-stream#4e425ff1eef240003af8716291e80fbaf3e3ae8f": + version: 0.0.0 + resolution: "@cspotcode/expect-stream@https://github.com/cspotcode/node-expect-stream.git#commit=4e425ff1eef240003af8716291e80fbaf3e3ae8f" + checksum: 32d78cccd0df519e7c6ac8a38d980d857877178e86ee5c82cce94ffbeecc11808c7eada0a83188bb1fe32852cc48a56c40b1f5a7bacb2a1d59881a214f999eaf + languageName: node + linkType: hard + "@cspotcode/source-map-support@npm:^0.8.0": version: 0.8.1 resolution: "@cspotcode/source-map-support@npm:0.8.1" @@ -639,13 +668,6 @@ __metadata: languageName: node linkType: hard -"@types/emscripten@npm:^1.38.0": - version: 1.39.6 - resolution: "@types/emscripten@npm:1.39.6" - checksum: 437f2f9cdfd9057255662508fa9a415fe704ba484c6198f3549c5b05feebcdcd612b1ec7b10026d2566935d05d3c36f9366087cb42bc90bd25772a88fcfc9343 - languageName: node - linkType: hard - "@types/glob@npm:*": version: 7.2.0 resolution: "@types/glob@npm:7.2.0" @@ -818,26 +840,6 @@ __metadata: languageName: node linkType: hard -"@yarnpkg/fslib@npm:^2.4.0": - version: 2.7.0 - resolution: "@yarnpkg/fslib@npm:2.7.0" - dependencies: - "@yarnpkg/libzip": ^2.2.4 - tslib: ^1.13.0 - checksum: ebb28ffbec67339a365fa97394f78a05c3c640e463b19d32fe1d435fd45b62a5c810e90d5b41941855c2717e16a04323d0307273349a71bed14b3f2129dc4246 - languageName: node - linkType: hard - -"@yarnpkg/libzip@npm:^2.2.4": - version: 2.2.4 - resolution: "@yarnpkg/libzip@npm:2.2.4" - dependencies: - "@types/emscripten": ^1.38.0 - tslib: ^1.13.0 - checksum: 974a286d4e7ff52bd924d56cb39492898a2306e95774362e4a3eb94690f180273a078243bf4044909e0fe29354552acc1cddd7d10d71ce332f7b1e1ff8eb54d9 - languageName: node - linkType: hard - "abbrev@npm:1": version: 1.1.1 resolution: "abbrev@npm:1.1.1" @@ -1980,13 +1982,6 @@ __metadata: languageName: node linkType: hard -"get-stream@npm:^6.0.0": - version: 6.0.1 - resolution: "get-stream@npm:6.0.1" - checksum: e04ecece32c92eebf5b8c940f51468cd53554dcbb0ea725b2748be583c9523d00128137966afce410b9b051eb2ef16d657cd2b120ca8edafcf5a65e81af63cad - languageName: node - linkType: hard - "glob-parent@npm:^5.1.2, glob-parent@npm:~5.1.2": version: 5.1.2 resolution: "glob-parent@npm:5.1.2" @@ -3124,10 +3119,10 @@ __metadata: languageName: node linkType: hard -"path-equal@npm:1.1.2": - version: 1.1.2 - resolution: "path-equal@npm:1.1.2" - checksum: 9dc43572eb8f438000c9e74ef87644a7cdae432dcdffd14b831cf1918ae20f907603fd5a6ade5c9e8273f43fbe8238d7af28d984e11242c827ba6c6e3965f716 +"path-equal@npm:^1.1.2": + version: 1.2.5 + resolution: "path-equal@npm:1.2.5" + checksum: 2bef7bcb98c7ae371c52c1562b2fc515bfd03bc1a5571df9a8591038db8d742ba2d1ff39aa5130853e6afb69e773ccba5095f54d2e6d17422ca03ef9047992d7 languageName: node linkType: hard @@ -3814,6 +3809,9 @@ __metadata: version: 0.0.0-use.local resolution: "ts-node@workspace:." dependencies: + "@TypeStrong/fs-fixture-builder": "github:TypeStrong/fs-fixture-builder#8abd1494280116ff5318dde2c50ad01e1663790c" + "@cspotcode/ava-lib": "github:cspotcode/ava-lib#edcc1885d192d08d5af83490af9341468402ec41" + "@cspotcode/expect-stream": "github:cspotcode/node-expect-stream#4e425ff1eef240003af8716291e80fbaf3e3ae8f" "@cspotcode/source-map-support": ^0.8.0 "@microsoft/api-extractor": ^7.19.4 "@swc/core": ">=1.3.32" @@ -3829,7 +3827,6 @@ __metadata: "@types/react": ^16.14.19 "@types/rimraf": ^3.0.0 "@types/semver": ^7.1.0 - "@yarnpkg/fslib": ^2.4.0 acorn: ^8.4.1 acorn-walk: ^8.1.1 arg: ^4.1.0 @@ -3839,7 +3836,6 @@ __metadata: diff: ^4.0.1 dprint: ^0.25.0 expect: 27.0.2 - get-stream: ^6.0.0 lodash: ^4.17.15 make-error: ^1.1.1 nyc: ^15.0.1 @@ -3851,7 +3847,7 @@ __metadata: throat: ^6.0.1 typedoc: ^0.22.10 typescript: 4.7.4 - typescript-json-schema: ^0.53.0 + typescript-json-schema: ^0.54.0 v8-compile-cache-lib: ^3.0.1 yn: 3.1.1 peerDependencies: @@ -3873,13 +3869,6 @@ __metadata: languageName: unknown linkType: soft -"tslib@npm:^1.13.0": - version: 1.14.1 - resolution: "tslib@npm:1.14.1" - checksum: dbe628ef87f66691d5d2959b3e41b9ca0045c3ee3c7c7b906cc1e328b39f199bb1ad9e671c39025bd56122ac57dfbf7385a94843b1cc07c60a4db74795829acd - languageName: node - linkType: hard - "type-fest@npm:^0.13.1": version: 0.13.1 resolution: "type-fest@npm:0.13.1" @@ -3920,21 +3909,21 @@ __metadata: languageName: node linkType: hard -"typescript-json-schema@npm:^0.53.0": - version: 0.53.1 - resolution: "typescript-json-schema@npm:0.53.1" +"typescript-json-schema@npm:^0.54.0": + version: 0.54.0 + resolution: "typescript-json-schema@npm:0.54.0" dependencies: "@types/json-schema": ^7.0.9 "@types/node": ^16.9.2 glob: ^7.1.7 - path-equal: 1.1.2 + path-equal: ^1.1.2 safe-stable-stringify: ^2.2.0 ts-node: ^10.2.1 typescript: ~4.6.0 yargs: ^17.1.1 bin: typescript-json-schema: bin/typescript-json-schema - checksum: 70fd5880b4b3c564cde114439ce8234b90dc192522bc35ca695372fe5c5d431be37964157abecf7da28cb42e7872f092b3945c7b002f01d899e0ed23d870eed2 + checksum: 49e03bd2612f79fe3ee9e9afcea34ae563da9aa799a8b4cf12b73feb60eb62a0786300eedb30261c69c482ed7e545acf1e5617d59861e6deee4f6570a658de88 languageName: node linkType: hard