diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml index 64af4bda5..6ba18a7de 100644 --- a/.github/workflows/continuous-integration.yml +++ b/.github/workflows/continuous-integration.yml @@ -51,46 +51,27 @@ jobs: matrix: os: [ubuntu, windows] # Don't forget to add all new flavors to this list! - flavor: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14] + flavor: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11] include: - # Node 12.15 - - flavor: 1 - node: 12.15 - nodeFlag: 12_15 - typescript: latest - typescriptFlag: latest - # Node 12.16 - # Earliest version that supports getFormat, etc hooks: https://github.com/nodejs/node/blob/master/doc/changelogs/CHANGELOG_V12.md#12.16.0 - - flavor: 2 - node: 12.16 - nodeFlag: 12_16 - typescript: latest - typescriptFlag: latest - # Node 12 - - flavor: 3 - node: 12 - nodeFlag: 12 - typescript: latest - typescriptFlag: latest # Node 14.13.0 # To test ESM builtin module resolution immediately before a node behavioral change: https://github.com/TypeStrong/ts-node/issues/1130 - - flavor: 4 + - flavor: 1 node: 14.13.0 nodeFlag: 14_13_0 typescript: latest typescriptFlag: latest # Node 14 - - flavor: 5 + - flavor: 2 node: 14 nodeFlag: 14 typescript: latest typescriptFlag: latest - - flavor: 6 + - flavor: 3 node: 14 nodeFlag: 14 - typescript: 2.7 - typescriptFlag: 2_7 - - flavor: 7 + typescript: 4.0 + typescriptFlag: 4_0 + - flavor: 4 node: 14 nodeFlag: 14 typescript: next @@ -98,44 +79,44 @@ jobs: # Node 16 # Node 16.11.1 # Earliest version that supports old ESM Loader Hooks API: https://github.com/TypeStrong/ts-node/pull/1522 - - flavor: 8 + - flavor: 5 node: 16.11.1 nodeFlag: 16_11_1 typescript: latest typescriptFlag: latest - - flavor: 9 + - flavor: 6 node: 16 nodeFlag: 16 typescript: latest typescriptFlag: latest downgradeNpm: true - - flavor: 10 + - flavor: 7 node: 16 nodeFlag: 16 - typescript: 2.7 - typescriptFlag: 2_7 + typescript: 4.0 + typescriptFlag: 4_0 downgradeNpm: true - - flavor: 11 + - flavor: 8 node: 16 nodeFlag: 16 typescript: next typescriptFlag: next downgradeNpm: true # Node 18 - - flavor: 12 + - flavor: 9 node: 18 nodeFlag: 18 typescript: latest typescriptFlag: latest downgradeNpm: true - - flavor: 13 + - flavor: 10 node: 18 nodeFlag: 18 typescript: next typescriptFlag: next downgradeNpm: true # Node nightly - - flavor: 14 + - flavor: 11 node: nightly nodeFlag: nightly typescript: latest diff --git a/ava.config.cjs b/ava.config.cjs index f6dc6951c..5ca2c1c7a 100644 --- a/ava.config.cjs +++ b/ava.config.cjs @@ -15,9 +15,7 @@ module.exports = { NODE_PATH: '' }, require: ['./src/test/remove-env-var-force-color.js'], - nodeArguments: semver.gte(process.version, '14.0.0') - ? ['--loader', './src/test/test-loader.mjs', '--no-warnings'] - : [], + nodeArguments: ['--loader', './src/test/test-loader.mjs', '--no-warnings'], timeout: '300s', concurrency: 1, }; diff --git a/dist-raw/README.md b/dist-raw/README.md index 9eeaed31d..a85dc568d 100644 --- a/dist-raw/README.md +++ b/dist-raw/README.md @@ -10,7 +10,7 @@ in a factory function, we will not indent the function body, to avoid whitespace One obvious problem with this approach: the code has been pulled from one version of node, whereas users of ts-node run multiple versions of node. -Users running node 12 may see that ts-node behaves like node 14, for example. +Users running node 14 may see that ts-node behaves like node 18, for example. ## `raw` directory diff --git a/dist-raw/node-internal-modules-cjs-helpers.js b/dist-raw/node-internal-modules-cjs-helpers.js index bd4f70204..ad188c543 100644 --- a/dist-raw/node-internal-modules-cjs-helpers.js +++ b/dist-raw/node-internal-modules-cjs-helpers.js @@ -63,8 +63,7 @@ function addBuiltinLibsToObject(object, dummyModuleName) { ObjectDefineProperty(object, name, { get: () => { - // Node 12 hack; remove when we drop node12 support - const lib = (dummyModule.require || require)(name); + const lib = dummyModule.require(name); // Disable the current getter/setter and set up a new // non-enumerable property. diff --git a/dist-raw/node-internal-modules-cjs-loader.js b/dist-raw/node-internal-modules-cjs-loader.js index cb83c3532..545747fb2 100644 --- a/dist-raw/node-internal-modules-cjs-loader.js +++ b/dist-raw/node-internal-modules-cjs-loader.js @@ -471,15 +471,11 @@ const Module_resolveFilename = function _resolveFilename(request, parent, isMain paths = Module._resolveLookupPaths(request, parent); } - // if (parent?.filename) { - // node 12 hack - if (parent != null && parent.filename) { + if (parent?.filename) { if (request[0] === '#') { const pkg = readPackageScope(parent.filename) || {}; - // if (pkg.data?.imports != null) { - // node 12 hack - if (pkg.data != null && pkg.data.imports != null) { + if (pkg.data?.imports != null) { try { return finalizeEsmResolution( packageImportsResolve(request, pathToFileURL(parent.filename), @@ -559,11 +555,15 @@ return { /** * copied from Module._extensions['.js'] * https://github.com/nodejs/node/blob/v15.3.0/lib/internal/modules/cjs/loader.js#L1113-L1120 + + * Assert that script can be loaded as CommonJS when we attempt to require it. + * If it should be loaded as ESM, throw ERR_REQUIRE_ESM like node does. + * * @param {import('../src/index').Service} service * @param {NodeJS.Module} module * @param {string} filename */ -function assertScriptCanLoadAsCJSImpl(service, module, filename) { +function assertScriptCanLoadAsCJS(service, module, filename) { const pkg = readPackageScope(filename); // ts-node modification: allow our configuration to override @@ -588,6 +588,6 @@ function assertScriptCanLoadAsCJSImpl(service, module, filename) { module.exports = { createCjsLoader, - assertScriptCanLoadAsCJSImpl, + assertScriptCanLoadAsCJS, readPackageScope }; diff --git a/dist-raw/node-options.js b/dist-raw/node-options.js index 22722755d..8039a711c 100644 --- a/dist-raw/node-options.js +++ b/dist-raw/node-options.js @@ -30,8 +30,6 @@ function parseArgv(argv) { '--preserve-symlinks-main': Boolean, '--input-type': String, '--experimental-specifier-resolution': String, - // Legacy alias for node versions prior to 12.16 - '--es-module-specifier-resolution': '--experimental-specifier-resolution', '--experimental-policy': String, '--conditions': [String], '--pending-deprecation': Boolean, diff --git a/node18/tsconfig.json b/node18/tsconfig.json new file mode 100644 index 000000000..8dcfdf373 --- /dev/null +++ b/node18/tsconfig.json @@ -0,0 +1,3 @@ +{ + "extends": "@tsconfig/node18/tsconfig.json" +} diff --git a/package-lock.json b/package-lock.json index 79b93f769..234223cde 100644 --- a/package-lock.json +++ b/package-lock.json @@ -758,12 +758,14 @@ "@tsconfig/node10": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.7.tgz", - "integrity": "sha512-aBvUmXLQbayM4w3A8TrjwrXs4DZ8iduJnuJLLRGdkWlyakCf1q6uHZJBzXoRA/huAEknG5tcUyQxN3A+In5euQ==" + "integrity": "sha512-aBvUmXLQbayM4w3A8TrjwrXs4DZ8iduJnuJLLRGdkWlyakCf1q6uHZJBzXoRA/huAEknG5tcUyQxN3A+In5euQ==", + "dev": true }, "@tsconfig/node12": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.7.tgz", - "integrity": "sha512-dgasobK/Y0wVMswcipr3k0HpevxFJLijN03A8mYfEPvWvOs14v0ZlYTR4kIgMx8g4+fTyTFv8/jLCIfRqLDJ4A==" + "integrity": "sha512-dgasobK/Y0wVMswcipr3k0HpevxFJLijN03A8mYfEPvWvOs14v0ZlYTR4kIgMx8g4+fTyTFv8/jLCIfRqLDJ4A==", + "dev": true }, "@tsconfig/node14": { "version": "1.0.0", @@ -775,6 +777,11 @@ "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.2.tgz", "integrity": "sha512-eZxlbI8GZscaGS7kkc/trHTT5xgrjH3/1n2JDwusC9iahPKWMRvRjJSAN5mCXviuTGQ/lHnhvv8Q1YTpnfz9gA==" }, + "@tsconfig/node18": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@tsconfig/node18/-/node18-1.0.0.tgz", + "integrity": "sha512-YEk7sAKXE0jJBiv5zsnw/MxXSqi4RM/Z12CTq+OnNMt+rG4zegu1OngM9Qatfc3KSyw7s107mheSJzysVeEnWA==" + }, "@types/argparse": { "version": "1.0.38", "resolved": "https://registry.npmjs.org/@types/argparse/-/argparse-1.0.38.tgz", diff --git a/package.json b/package.json index ae0924a17..9b4688a58 100644 --- a/package.json +++ b/package.json @@ -28,10 +28,9 @@ "./child-loader.mjs": "./child-loader.mjs", "./transpilers/swc": "./transpilers/swc.js", "./transpilers/swc-experimental": "./transpilers/swc-experimental.js", - "./node10/tsconfig.json": "./node10/tsconfig.json", - "./node12/tsconfig.json": "./node12/tsconfig.json", "./node14/tsconfig.json": "./node14/tsconfig.json", - "./node16/tsconfig.json": "./node16/tsconfig.json" + "./node16/tsconfig.json": "./node16/tsconfig.json", + "./node18/tsconfig.json": "./node18/tsconfig.json" }, "types": "dist/index.d.ts", "bin": { @@ -39,8 +38,7 @@ "ts-node-cwd": "dist/bin-cwd.js", "ts-node-esm": "dist/bin-esm.js", "ts-node-script": "dist/bin-script.js", - "ts-node-transpile-only": "dist/bin-transpile.js", - "ts-script": "dist/bin-script-deprecated.js" + "ts-node-transpile-only": "dist/bin-transpile.js" }, "files": [ "/transpilers/", @@ -55,10 +53,9 @@ "/LICENSE", "/tsconfig.schema.json", "/tsconfig.schemastore-schema.json", - "/node10/", - "/node12/", "/node14/", - "/node16/" + "/node16/", + "/node18/" ], "scripts": { "lint": "dprint check", @@ -159,10 +156,9 @@ }, "dependencies": { "@cspotcode/source-map-support": "^0.8.0", - "@tsconfig/node10": "^1.0.7", - "@tsconfig/node12": "^1.0.7", - "@tsconfig/node14": "^1.0.0", - "@tsconfig/node16": "^1.0.2", + "@tsconfig/node14": "*", + "@tsconfig/node16": "*", + "@tsconfig/node18": "*", "acorn": "^8.4.1", "acorn-walk": "^8.1.1", "arg": "^4.1.0", diff --git a/src/bin-script-deprecated.ts b/src/bin-script-deprecated.ts deleted file mode 100644 index a4dcdb91a..000000000 --- a/src/bin-script-deprecated.ts +++ /dev/null @@ -1,10 +0,0 @@ -#!/usr/bin/env node - -import { main } from './bin'; - -console.warn( - 'ts-script has been deprecated and will be removed in the next major release.', - 'Please use ts-node-script instead' -); - -main(undefined, { '--scriptMode': true }); diff --git a/src/bin.ts b/src/bin.ts index f470a2c83..203717754 100644 --- a/src/bin.ts +++ b/src/bin.ts @@ -39,6 +39,8 @@ import { findAndReadConfig } from './configuration'; * * The functions are intentionally given uncreative names and left in the same order as the original code, to make a * smaller git diff. + * + * @internal */ export function main( argv: string[] = process.argv.slice(2), @@ -97,19 +99,6 @@ export function bootstrap(state: BootstrapState) { function parseArgv(argv: string[], entrypointArgs: Record) { arg ??= require('arg'); - // HACK: technically, this function is not marked @internal so it's possible - // that libraries in the wild are doing `require('ts-node/dist/bin').main({'--transpile-only': true})` - // We can mark this function @internal in next major release. - // For now, rewrite args to avoid a breaking change. - entrypointArgs = { ...entrypointArgs }; - for (const key of Object.keys(entrypointArgs)) { - entrypointArgs[ - key.replace( - /([a-z])-([a-z])/g, - (_$0, $1, $2: string) => `${$1}${$2.toUpperCase()}` - ) - ] = entrypointArgs[key]; - } const args = { ...entrypointArgs, @@ -742,13 +731,6 @@ let guaranteedNonexistentDirectorySuffix = 0; * https://stackoverflow.com/questions/59865584/how-to-invalidate-cached-require-resolve-results */ function requireResolveNonCached(absoluteModuleSpecifier: string) { - // node <= 12.1.x fallback: The trick below triggers a node bug on old versions. - // On these old versions, pollute the require cache instead. This is a deliberate - // ts-node limitation that will *rarely* manifest, and will not matter once node 12 - // is end-of-life'd on 2022-04-30 - const isSupportedNodeVersion = versionGteLt(process.versions.node, '12.2.0'); - if (!isSupportedNodeVersion) return require.resolve(absoluteModuleSpecifier); - const { dir, base } = parsePath(absoluteModuleSpecifier); const relativeModuleSpecifier = `./${base}`; diff --git a/src/child/spawn-child.ts b/src/child/spawn-child.ts index 618b8190a..2859a61d9 100644 --- a/src/child/spawn-child.ts +++ b/src/child/spawn-child.ts @@ -1,7 +1,6 @@ import type { BootstrapState } from '../bin'; import { spawn } from 'child_process'; import { pathToFileURL } from 'url'; -import { versionGteLt } from '../util'; import { argPrefix, compress } from './argv-payload'; /** @@ -11,11 +10,6 @@ import { argPrefix, compress } from './argv-payload'; * the child process. */ export function callInChild(state: BootstrapState) { - if (!versionGteLt(process.versions.node, '12.17.0')) { - throw new Error( - '`ts-node-esm` and `ts-node --esm` require node version 12.17.0 or newer.' - ); - } const child = spawn( process.execPath, [ diff --git a/src/esm.ts b/src/esm.ts index cb1280451..5c27a5d5c 100644 --- a/src/esm.ts +++ b/src/esm.ts @@ -119,8 +119,6 @@ export function registerAndCreateEsmHooks(opts?: RegisterOptions) { } export function createEsmHooks(tsNodeService: Service) { - tsNodeService.enableExperimentalEsmLoaderInterop(); - // Custom implementation that considers additional file extensions and automatically adds file extensions const nodeResolveImplementation = tsNodeService.getNodeEsmResolver(); const nodeGetFormatImplementation = tsNodeService.getNodeEsmGetFormat(); diff --git a/src/index.ts b/src/index.ts index cf9d7eef8..58a47cc9e 100644 --- a/src/index.ts +++ b/src/index.ts @@ -38,6 +38,7 @@ import type * as _nodeInternalModulesEsmGetFormat from '../dist-raw/node-interna import type * as _nodeInternalModulesCjsLoader from '../dist-raw/node-internal-modules-cjs-loader'; import { Extensions, getExtensions } from './file-extensions'; import { createTsTranspileModule } from './ts-transpile-module'; +import { assertScriptCanLoadAsCJS } from '../dist-raw/node-internal-modules-cjs-loader'; export { TSCommon }; export { @@ -60,30 +61,7 @@ export type { NodeLoaderHooksFormat, } from './esm'; -/** - * Does this version of node obey the package.json "type" field - * and throw ERR_REQUIRE_ESM when attempting to require() an ESM modules. - */ -const engineSupportsPackageTypeField = - parseInt(process.versions.node.split('.')[0], 10) >= 12; - -/** - * Assert that script can be loaded as CommonJS when we attempt to require it. - * If it should be loaded as ESM, throw ERR_REQUIRE_ESM like node does. - * - * Loaded conditionally so we don't need to support older node versions - */ -let assertScriptCanLoadAsCJS: ( - service: Service, - module: NodeJS.Module, - filename: string -) => void = engineSupportsPackageTypeField - ? ( - require('../dist-raw/node-internal-modules-cjs-loader') as typeof _nodeInternalModulesCjsLoader - ).assertScriptCanLoadAsCJSImpl - : () => { - /* noop */ - }; +const engineSupportsPackageTypeField = true; /** * Registered `ts-node` instance information. @@ -531,8 +509,6 @@ export interface Service { /** @internal */ installSourceMapSupport(): void; /** @internal */ - enableExperimentalEsmLoaderInterop(): void; - /** @internal */ transpileOnly: boolean; /** @internal */ projectLocalResolveHelper: ProjectLocalResolveHelper; @@ -606,6 +582,8 @@ export function register( installCommonjsResolveHooksIfNecessary(service); + service.installSourceMapSupport(); + // Require specified modules before start-up. (Module as ModuleConstructorWithInternals)._preloadModules( service.options.require @@ -651,18 +629,9 @@ export function createFromPreloadedConfig( 'Experimental REPL await is not compatible with targets lower than ES2018' ); } - // Top-level await was added in TS 3.8 - const tsVersionSupportsTla = versionGteLt(ts.version, '3.8.0'); - if (options.experimentalReplAwait === true && !tsVersionSupportsTla) { - throw new Error( - 'Experimental REPL await is not compatible with TypeScript versions older than 3.8' - ); - } const shouldReplAwait = - options.experimentalReplAwait !== false && - tsVersionSupportsTla && - targetSupportsTla; + options.experimentalReplAwait !== false && targetSupportsTla; // swc implies two other options // typeCheck option was implemented specifically to allow overriding tsconfig transpileOnly from the command-line @@ -798,17 +767,7 @@ export function createFromPreloadedConfig( } } - /** - * True if require() hooks should interop with experimental ESM loader. - * Enabled explicitly via a flag since it is a breaking change. - */ - let experimentalEsmLoader = false; - function enableExperimentalEsmLoaderInterop() { - experimentalEsmLoader = true; - } - // Install source map support and read from memory cache. - installSourceMapSupport(); function installSourceMapSupport() { const sourceMapSupport = require('@cspotcode/source-map-support') as typeof _sourceMapSupport; @@ -817,9 +776,8 @@ export function createFromPreloadedConfig( retrieveFile(pathOrUrl: string) { let path = pathOrUrl; // If it's a file URL, convert to local path - // Note: fileURLToPath does not exist on early node v10 // I could not find a way to handle non-URLs except to swallow an error - if (experimentalEsmLoader && path.startsWith('file://')) { + if (path.startsWith('file://')) { try { path = fileURLToPath(path); } catch (e) { @@ -1131,25 +1089,10 @@ export function createFromPreloadedConfig( : undefined, }; - const host: _ts.CompilerHost = ts.createIncrementalCompilerHost - ? ts.createIncrementalCompilerHost(config.options, sys) - : { - ...sys, - getSourceFile: (fileName, languageVersion) => { - const contents = sys.readFile(fileName); - if (contents === undefined) return; - return ts.createSourceFile(fileName, contents, languageVersion); - }, - getDefaultLibLocation: () => normalizeSlashes(dirname(compiler)), - getDefaultLibFileName: () => - normalizeSlashes( - join( - dirname(compiler), - ts.getDefaultLibFileName(config.options) - ) - ), - useCaseSensitiveFileNames: () => sys.useCaseSensitiveFileNames, - }; + const host: _ts.CompilerHost = ts.createIncrementalCompilerHost( + config.options, + sys + ); host.trace = options.tsTrace; const { resolveModuleNames, @@ -1169,23 +1112,13 @@ export function createFromPreloadedConfig( host.resolveModuleNames = resolveModuleNames; host.resolveTypeReferenceDirectives = resolveTypeReferenceDirectives; - // Fallback for older TypeScript releases without incremental API. - let builderProgram = ts.createIncrementalProgram - ? ts.createIncrementalProgram({ - rootNames: Array.from(rootFileNames), - options: config.options, - host, - configFileParsingDiagnostics: config.errors, - projectReferences: config.projectReferences, - }) - : ts.createEmitAndSemanticDiagnosticsBuilderProgram( - Array.from(rootFileNames), - config.options, - host, - undefined, - config.errors, - config.projectReferences - ); + let builderProgram = ts.createIncrementalProgram({ + rootNames: Array.from(rootFileNames), + options: config.options, + host, + configFileParsingDiagnostics: config.errors, + projectReferences: config.projectReferences, + }); // Read and cache custom transformers. const customTransformers = @@ -1531,7 +1464,6 @@ export function createFromPreloadedConfig( shouldReplAwait, addDiagnosticFilter, installSourceMapSupport, - enableExperimentalEsmLoaderInterop, transpileOnly, projectLocalResolveHelper, getNodeEsmResolver, diff --git a/src/repl.ts b/src/repl.ts index 3137daa49..f139f6fad 100644 --- a/src/repl.ts +++ b/src/repl.ts @@ -90,8 +90,7 @@ export interface ReplService { */ nodeEval( code: string, - // TODO change to `Context` in a future release? Technically a breaking change - context: any, + context: Context, _filename: string, callback: (err: Error | null, result?: any) => any ): void; @@ -230,7 +229,7 @@ export function createRepl(options: CreateReplOptions = {}) { function nodeEval( code: string, - context: any, + context: Context, _filename: string, callback: (err: Error | null, result?: any) => any ) { diff --git a/src/test/diagnostics.spec.ts b/src/test/diagnostics.spec.ts index 994f6921b..db3c82e0f 100644 --- a/src/test/diagnostics.spec.ts +++ b/src/test/diagnostics.spec.ts @@ -8,8 +8,10 @@ const test = context(ctxTsNode); test.suite('TSError diagnostics', ({ context }) => { const test = context( once(async (t) => { + // Locking to es2021, because es2022 -- default in @tsconfig/bases for node18 -- + // changes this diagnostic to be a composite "No overload matches this call." const service = t.context.tsNodeUnderTest.create({ - compilerOptions: { target: 'es5' }, + compilerOptions: { target: 'es5', lib: ['es2021'] }, skipProject: true, }); try { @@ -22,11 +24,9 @@ test.suite('TSError diagnostics', ({ context }) => { ); const diagnosticCode = 2345; - const diagnosticMessage = semver.satisfies(ts.version, '2.7') - ? "Argument of type '123' " + - "is not assignable to parameter of type 'string | undefined'." - : "Argument of type 'number' " + - "is not assignable to parameter of type 'string'."; + const diagnosticMessage = + "Argument of type 'number' " + + "is not assignable to parameter of type 'string'."; const diagnosticErrorMessage = `TS${diagnosticCode}: ${diagnosticMessage}`; const cwdBefore = process.cwd(); diff --git a/src/test/esm-loader.spec.ts b/src/test/esm-loader.spec.ts index 375012a76..06b818b7a 100644 --- a/src/test/esm-loader.spec.ts +++ b/src/test/esm-loader.spec.ts @@ -12,16 +12,12 @@ import { CMD_TS_NODE_WITHOUT_PROJECT_FLAG, ctxTsNode, delay, - EXPERIMENTAL_MODULES_FLAG, - nodeSupportsEsmHooks, nodeSupportsImportAssertions, nodeSupportsUnflaggedJsonImports, - nodeSupportsSpawningChildProcess, nodeUsesNewHooksApi, resetNodeEnvironment, TEST_DIR, tsSupportsImportAssertions, - tsSupportsResolveJsonModule, tsSupportsStableNodeNextNode16, } from './helpers'; import { createExec, createSpawn, ExecReturn } from './exec-helpers'; @@ -40,321 +36,351 @@ const spawn = createSpawn({ }); test.suite('esm', (test) => { - test.suite('when node supports loader hooks', (test) => { - test.runIf(nodeSupportsEsmHooks); - test('should compile and execute as ESM', async () => { + 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'); + }); + 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 expectedModuleUrl = pathToFileURL( + join(TEST_DIR, './esm/throw error.ts') + ).toString(); + expect(err!.message).toMatch( + [ + `${expectedModuleUrl}:100`, + " bar() { throw new Error('this is a demo'); }", + ' ^', + 'Error: this is a demo', + ` at Foo.bar (${expectedModuleUrl}:100:17)`, + ].join('\n') + ); + }); + + test.suite('supports experimental-specifier-resolution=node', (test) => { + test('via --experimental-specifier-resolution', async () => { const { err, stdout } = await exec( - `${CMD_ESM_LOADER_WITHOUT_PROJECT} index.ts`, - { - cwd: join(TEST_DIR, './esm'), - } + `${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'); }); - test('should use source maps', async (t) => { - const { err, stdout, stderr } = await exec( - `${CMD_ESM_LOADER_WITHOUT_PROJECT} "throw error.ts"`, + test('via NODE_OPTIONS', async () => { + const { err, stdout } = await exec( + `${CMD_ESM_LOADER_WITHOUT_PROJECT} index.ts`, { - cwd: join(TEST_DIR, './esm'), + cwd: join(TEST_DIR, './esm-node-resolver'), + env: { + ...process.env, + NODE_OPTIONS: `--experimental-specifier-resolution=node`, + }, } ); - expect(err).not.toBe(null); - const expectedModuleUrl = pathToFileURL( - join(TEST_DIR, './esm/throw error.ts') - ).toString(); - expect(err!.message).toMatch( - [ - `${expectedModuleUrl}:100`, - " bar() { throw new Error('this is a demo'); }", - ' ^', - 'Error: this is a demo', - ` at Foo.bar (${expectedModuleUrl}:100:17)`, - ].join('\n') - ); + expect(err).toBe(null); + expect(stdout).toBe('foo bar baz biff libfoo\n'); }); + }); - test.suite('supports experimental-specifier-resolution=node', (test) => { - test('via --experimental-specifier-resolution', async () => { - const { err, stdout } = 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'); - }); - test('via --es-module-specifier-resolution alias', async () => { - const { err, stdout } = await exec( - `${CMD_ESM_LOADER_WITHOUT_PROJECT} ${EXPERIMENTAL_MODULES_FLAG} --es-module-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'); - }); - 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_MODULES_FLAG} --experimental-specifier-resolution=node`, - }, - } - ); - expect(err).toBe(null); - expect(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( + 'Error [ERR_REQUIRE_ESM]: Must use import to load ES Module:' + ); + }); - 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( - '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); + // expect error from node's default resolver + expect(stderr).toMatch( + /Error \[ERR_UNSUPPORTED_ESM_URL_SCHEME\]:.*(?:\n.*){0,2}\n *at defaultResolve/ + ); + }); - 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); - // expect error from node's default resolver - expect(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'); + }); - 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'); - }); + test('should support transpile only mode via dedicated loader entrypoint', async () => { + const { err, stdout } = 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(''); + }); + 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) { + throw new Error('Command was expected to fail, but it succeeded.'); + } + + expect(err.message).toMatch('Unable to compile TypeScript'); + expect(err.message).toMatch( + new RegExp( + "TS2345: Argument of type '(?:number|1101)' is not assignable to parameter of type 'string'\\." + ) + ); + expect(err.message).toMatch( + new RegExp( + "TS2322: Type '(?:\"hello world\"|string)' is not assignable to type 'number'\\." + ) + ); + expect(stdout).toBe(''); + }); - test('should support transpile only mode via dedicated loader entrypoint', async () => { - const { err, stdout } = await exec( - `${CMD_ESM_LOADER_WITHOUT_PROJECT}/transpile-only index.ts`, + test.suite('moduleTypes', (test) => { + suite('with vanilla ts transpilation', 'tsconfig.json'); + suite('with third-party-transpiler', 'tsconfig-swc.json'); + function suite(name: string, tsconfig: string) { + test.suite(name, (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( + `${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(``); + }); + test('should allow importing CJS in an otherwise ESM project', async (t) => { + await run('override-to-cjs', tsconfig, 'cjs'); + if (semver.gte(process.version, '14.13.1')) + await run('override-to-cjs', tsconfig, 'mjs'); + }); + test('should allow importing ESM in an otherwise CJS project', async (t) => { + await run('override-to-esm', tsconfig, 'cjs'); + // Node 14.13.0 has a bug(?) where it checks for ESM-only syntax *before* we transform the code. + if (semver.gte(process.version, '14.13.1')) + await run('override-to-esm', tsconfig, 'mjs'); + }); + }); + } + async function run(project: string, config: string, ext: string) { + const { err, stderr, stdout } = await exec( + `${CMD_ESM_LOADER_WITHOUT_PROJECT} ./module-types/${project}/test.${ext}`, { - cwd: join(TEST_DIR, './esm-transpile-only'), + env: { + ...process.env, + TS_NODE_PROJECT: `./module-types/${project}/${config}`, + }, } ); expect(err).toBe(null); - expect(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'), - } - ); + expect(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`, { + cwd: join(TEST_DIR, './esm-custom-loader'), + }); + if (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( - new RegExp( - "TS2345: Argument of type '(?:number|1101)' is not assignable to parameter of type 'string'\\." - ) - ); - expect(err.message).toMatch( - new RegExp( - "TS2322: Type '(?:\"hello world\"|string)' is not assignable to type 'number'\\." - ) - ); - expect(stdout).toBe(''); + expect(err.message).toMatch(/TS6133:\s+'unusedVar'/); }); + }); - test.suite('moduleTypes', (test) => { - suite('with vanilla ts transpilation', 'tsconfig.json'); - suite('with third-party-transpiler', 'tsconfig-swc.json'); - function suite(name: string, tsconfig: string) { - test.suite(name, (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( - `${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(``); - }); - test('should allow importing CJS in an otherwise ESM project', async (t) => { - await run('override-to-cjs', tsconfig, 'cjs'); - if (semver.gte(process.version, '14.13.1')) - await run('override-to-cjs', tsconfig, 'mjs'); - }); - test('should allow importing ESM in an otherwise CJS project', async (t) => { - await run('override-to-esm', tsconfig, 'cjs'); - // Node 14.13.0 has a bug(?) where it checks for ESM-only syntax *before* we transform the code. - if (semver.gte(process.version, '14.13.1')) - await run('override-to-esm', tsconfig, 'mjs'); - }); - }); - } - async function run(project: string, config: string, ext: string) { - const { err, stderr, stdout } = await exec( - `${CMD_ESM_LOADER_WITHOUT_PROJECT} ./module-types/${project}/test.${ext}`, - { - env: { - ...process.env, - TS_NODE_PROJECT: `./module-types/${project}/${config}`, - }, - } - ); - expect(err).toBe(null); - expect(stdout).toBe(`Failures: 0\n`); - } + test.suite('unit test hooks', ({ context }) => { + const test = context(async (t) => { + const service = t.context.tsNodeUnderTest.create({ + cwd: TEST_DIR, + }); + t.teardown(() => { + resetNodeEnvironment(); + }); + return { + service, + hooks: t.context.tsNodeUnderTest.createEsmHooks(service), + }; }); - test.suite('createEsmHooks()', (test) => { - test('should create proper hooks with provided instance', async () => { - const { err } = await exec( - `node ${EXPERIMENTAL_MODULES_FLAG} --loader ./loader.mjs index.ts`, - { - cwd: join(TEST_DIR, './esm-custom-loader'), + test.suite('data URIs', (test) => { + test.runIf(nodeUsesNewHooksApi); + + test('Correctly determines format of data URIs', async (t) => { + const { hooks } = t.context; + const url = 'data:text/javascript,console.log("hello world");'; + const result = await (hooks as NodeLoaderHooksAPI2).load( + url, + { format: undefined }, + async (url, context, _ignored) => { + return { format: context.format!, source: '' }; } ); - - if (err === null) { - throw new Error('Command was expected to fail, but it succeeded.'); - } - - expect(err.message).toMatch(/TS6133:\s+'unusedVar'/); + expect(result.format).toBe('module'); }); }); + }); - test.suite('unit test hooks', ({ context }) => { - const test = context(async (t) => { - const service = t.context.tsNodeUnderTest.create({ - cwd: TEST_DIR, - }); - t.teardown(() => { - resetNodeEnvironment(); - }); - return { - service, - hooks: t.context.tsNodeUnderTest.createEsmHooks(service), - }; - }); + test.suite('supports import assertions', (test) => { + test.runIf(nodeSupportsImportAssertions && tsSupportsImportAssertions); - test.suite('data URIs', (test) => { - test.runIf(nodeUsesNewHooksApi); - - test('Correctly determines format of data URIs', async (t) => { - const { hooks } = t.context; - const url = 'data:text/javascript,console.log("hello world");'; - const result = await (hooks as NodeLoaderHooksAPI2).load( - url, - { format: undefined }, - async (url, context, _ignored) => { - return { format: context.format!, source: '' }; - } - ); - expect(result.format).toBe('module'); - }); - }); + const macro = test.macro((flags: string) => async (t) => { + const { err, stdout } = 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( + 'A fuchsia car has 2 seats and the doors are open.\nDone!' + ); }); - test.suite('supports import assertions', (test) => { - test.runIf( - nodeSupportsImportAssertions && - tsSupportsImportAssertions && - tsSupportsResolveJsonModule + test.suite( + 'when node does not require --experimental-json-modules', + (test) => { + test.runIf(nodeSupportsUnflaggedJsonImports); + test('Can import JSON modules with appropriate assertion', macro, ''); + } + ); + test.suite('when node requires --experimental-json-modules', (test) => { + test.runIf(!nodeSupportsUnflaggedJsonImports); + test( + 'Can import JSON using the appropriate flag and assertion', + macro, + '--experimental-json-modules' ); + }); + }); - const macro = test.macro((flags: string) => async (t) => { + test.suite( + 'Entrypoint resolution falls back to CommonJS resolver and format', + (test) => { + test('extensionless entrypoint', async (t) => { const { err, stdout } = await exec( - `${CMD_ESM_LOADER_WITHOUT_PROJECT} ${flags} ./importJson.ts`, - { - cwd: resolve(TEST_DIR, 'esm-import-assertions'), - } + `${CMD_ESM_LOADER_WITHOUT_PROJECT} ./esm-loader-entrypoint-cjs-fallback/extensionless-entrypoint` ); expect(err).toBe(null); - expect(stdout.trim()).toBe( - 'A fuchsia car has 2 seats and the doors are open.\nDone!' + expect(stdout.trim()).toBe('Hello world!'); + }); + test('relies upon CommonJS resolution', async (t) => { + const { err, stdout } = 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!'); }); - - test.suite( - 'when node does not require --experimental-json-modules', - (test) => { - test.runIf(nodeSupportsUnflaggedJsonImports); - test('Can import JSON modules with appropriate assertion', macro, ''); - } - ); - test.suite('when node requires --experimental-json-modules', (test) => { - test.runIf(!nodeSupportsUnflaggedJsonImports); - test( - 'Can import JSON using the appropriate flag and assertion', - macro, - '--experimental-json-modules' + test('fails as expected when entrypoint does not exist at all', async (t) => { + const { err, stderr } = await exec( + `${CMD_ESM_LOADER_WITHOUT_PROJECT} ./esm-loader-entrypoint-cjs-fallback/does-not-exist` ); + expect(err).toBeDefined(); + expect(stderr).toContain(`Cannot find module `); }); - }); + } + ); - test.suite( - 'Entrypoint resolution falls back to CommonJS resolver and format', - (test) => { - test('extensionless entrypoint', async (t) => { - const { err, stdout } = await exec( - `${CMD_ESM_LOADER_WITHOUT_PROJECT} ./esm-loader-entrypoint-cjs-fallback/extensionless-entrypoint` - ); - expect(err).toBe(null); - expect(stdout.trim()).toBe('Hello world!'); - }); - test('relies upon CommonJS resolution', async (t) => { - const { err, stdout } = 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!'); - }); - test('fails as expected when entrypoint does not exist at all', async (t) => { - const { err, stderr } = await exec( - `${CMD_ESM_LOADER_WITHOUT_PROJECT} ./esm-loader-entrypoint-cjs-fallback/does-not-exist` - ); - expect(err).toBeDefined(); - expect(stderr).toContain(`Cannot find module `); - }); - } + test.suite('spawns child process', async (test) => { + basic('ts-node-esm executable', () => + exec(`${BIN_ESM_PATH} ./esm-child-process/via-flag/index.ts foo bar`) + ); + basic('ts-node --esm flag', () => + exec(`${BIN_PATH} --esm ./esm-child-process/via-flag/index.ts foo bar`) + ); + basic('ts-node w/tsconfig esm:true', () => + exec( + `${BIN_PATH} --esm ./esm-child-process/via-tsconfig/index.ts foo bar` + ) ); - test.suite('spawns child process', async (test) => { - test.runIf(nodeSupportsSpawningChildProcess); - - basic('ts-node-esm executable', () => - exec(`${BIN_ESM_PATH} ./esm-child-process/via-flag/index.ts foo bar`) - ); - basic('ts-node --esm flag', () => - exec(`${BIN_PATH} --esm ./esm-child-process/via-flag/index.ts foo bar`) - ); - basic('ts-node w/tsconfig esm:true', () => - exec( - `${BIN_PATH} --esm ./esm-child-process/via-tsconfig/index.ts foo bar` - ) - ); - - 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'); + 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(''); + }); + } + + test.suite('parent passes signals to child', (test) => { + test.runSerially(); + + signalTest('SIGINT'); + signalTest('SIGTERM'); + + function signalTest(signal: string) { + test(signal, async (t) => { + const childP = spawn([ + // exec lets us run the shims on windows; spawn does not + process.execPath, + BIN_PATH_JS, + `./esm-child-process/via-tsconfig/sleep.ts`, + ]); + let code: number | null | undefined = undefined; + childP.child.on('exit', (_code) => (code = _code)); + await delay(6e3); + const codeAfter6Seconds = code; + process.kill(childP.child.pid, signal); + await delay(2e3); + const codeAfter8Seconds = code; + const { stdoutP, stderrP } = await childP; + const stdout = await stdoutP; + const stderr = await stderrP; + t.log({ + stdout, + stderr, + codeAfter6Seconds, + codeAfter8Seconds, + code, + }); + expect(codeAfter6Seconds).toBeUndefined(); + if (process.platform === 'win32') { + // Windows doesn't have signals, and node attempts an imperfect facsimile. + // In Windows, SIGINT and SIGTERM kill the process immediately with exit + // code 1, and the process can't catch or prevent this. + expect(codeAfter8Seconds).toBe(1); + expect(code).toBe(1); + } else { + expect(codeAfter8Seconds).toBe(undefined); + expect(code).toBe(123); + expect(stdout.trim()).toBe( + `child registered signal handlers\nchild received signal: ${signal}\nchild exiting` + ); + } expect(stderr).toBe(''); }); } @@ -458,29 +484,15 @@ test.suite('esm', (test) => { }); }); - test.suite('node >= 12.x.x', (test) => { - test.runIf(semver.gte(process.version, '12.0.0')); - test('throws ERR_REQUIRE_ESM when attempting to require() an ESM script when ESM loader is *not* enabled and node version is >= 12', 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`, { - cwd: join(TEST_DIR, './esm-err-require-esm'), - }); - expect(err).not.toBe(null); - expect(stderr).toMatch( - 'Error [ERR_REQUIRE_ESM]: Must use import to load ES Module:' - ); - }); - }); - test.suite('node < 12.x.x', (test) => { - test.runIf(semver.lt(process.version, '12.0.0')); - test('Loads as CommonJS when attempting to require() an ESM script when ESM loader is *not* enabled and node version is < 12', async () => { - // Node versions less than 12 do not support package.json "type" field and so will load ESM as CommonJS - const { err, stdout } = await exec(`${BIN_PATH} ./index.js`, { - cwd: join(TEST_DIR, './esm-err-require-esm'), - }); - expect(err).toBe(null); - expect(stdout).toMatch('CommonJS'); + 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`, { + cwd: join(TEST_DIR, './esm-err-require-esm'), }); + expect(err).not.toBe(null); + expect(stderr).toMatch( + 'Error [ERR_REQUIRE_ESM]: Must use import to load ES Module:' + ); }); }); @@ -505,7 +517,7 @@ test.suite("Catch unexpected changes to node's loader context", (test) => { rows.forEach((row) => { const json = JSON.parse(row) as { resolveContextKeys?: string[]; - loadContextKeys?: string; + loadContextKeys?: string[]; }; if (json.resolveContextKeys) { expect(json.resolveContextKeys).toEqual([ @@ -514,20 +526,7 @@ test.suite("Catch unexpected changes to node's loader context", (test) => { 'parentURL', ]); } else if (json.loadContextKeys) { - try { - expect(json.loadContextKeys).toEqual(['format', 'importAssertions']); - } catch (e) { - // HACK for https://github.com/TypeStrong/ts-node/issues/1641 - if (process.version.includes('nightly')) { - expect(json.loadContextKeys).toEqual([ - 'format', - 'importAssertions', - 'parentURL', - ]); - } else { - throw e; - } - } + expect(json.loadContextKeys).toEqual(['format', 'importAssertions']); } else { throw new Error('Unexpected stdout in test.'); } diff --git a/src/test/helpers.ts b/src/test/helpers.ts index da86bddc2..c9130c23b 100644 --- a/src/test/helpers.ts +++ b/src/test/helpers.ts @@ -46,10 +46,7 @@ export const CMD_TS_NODE_WITH_PROJECT_FLAG = `"${BIN_PATH}" --project "${PROJECT 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 EXPERIMENTAL_MODULES_FLAG = semver.gte(process.version, '12.17.0') - ? '' - : '--experimental-modules'; -export const CMD_ESM_LOADER_WITHOUT_PROJECT = `node ${EXPERIMENTAL_MODULES_FLAG} --loader ts-node/esm`; +export const CMD_ESM_LOADER_WITHOUT_PROJECT = `node --loader ts-node/esm`; //#endregion // `createRequire` does not exist on older node versions @@ -58,11 +55,6 @@ export const testsDirRequire = createRequire(join(TEST_DIR, 'index.js')); export const ts = testsDirRequire('typescript'); //#region version checks -export const nodeSupportsEsmHooks = semver.gte(process.version, '12.16.0'); -export const nodeSupportsSpawningChildProcess = semver.gte( - process.version, - '12.17.0' -); 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 @@ -85,14 +77,6 @@ export const nodeSupportsImportingTransformedCjsFromEsm = semver.gte( process.version, '14.13.1' ); -export const tsSupportsResolveJsonModule = semver.gte(ts.version, '2.9.0'); -/** Supports tsconfig "extends" >= v3.2.0 */ -export const tsSupportsTsconfigInheritanceViaNodePackages = semver.gte( - ts.version, - '3.2.0' -); -/** Supports --showConfig: >= v3.2.0 */ -export const tsSupportsShowConfig = semver.gte(ts.version, '3.2.0'); /** Supports module:nodenext and module:node16 as *stable* features */ export const tsSupportsStableNodeNextNode16 = ts.version.startsWith('4.7.') || semver.gte(ts.version, '4.7.0'); diff --git a/src/test/index.spec.ts b/src/test/index.spec.ts index f085a3639..4df34e236 100644 --- a/src/test/index.spec.ts +++ b/src/test/index.spec.ts @@ -6,13 +6,9 @@ import semver = require('semver'); import { BIN_PATH_JS, CMD_TS_NODE_WITH_PROJECT_TRANSPILE_ONLY_FLAG, - nodeSupportsEsmHooks, - nodeSupportsSpawningChildProcess, ts, tsSupportsMtsCtsExtensions, - tsSupportsShowConfig, tsSupportsStableNodeNextNode16, - tsSupportsTsconfigInheritanceViaNodePackages, } from './helpers'; import { lstatSync, mkdtempSync } from 'fs'; import { npath } from '@yarnpkg/fslib'; @@ -73,21 +69,18 @@ test.suite('ts-node', (test) => { testsDirRequire.resolve('ts-node/register/transpile-only'); testsDirRequire.resolve('ts-node/register/type-check'); - if (semver.gte(process.version, '12.17.0')) { - // `node --loader ts-node/esm` - testsDirRequire.resolve('ts-node/esm'); - testsDirRequire.resolve('ts-node/esm.mjs'); - testsDirRequire.resolve('ts-node/esm/transpile-only'); - testsDirRequire.resolve('ts-node/esm/transpile-only.mjs'); - } + // `node --loader ts-node/esm` + testsDirRequire.resolve('ts-node/esm'); + testsDirRequire.resolve('ts-node/esm.mjs'); + testsDirRequire.resolve('ts-node/esm/transpile-only'); + testsDirRequire.resolve('ts-node/esm/transpile-only.mjs'); testsDirRequire.resolve('ts-node/transpilers/swc'); testsDirRequire.resolve('ts-node/transpilers/swc-experimental'); - testsDirRequire.resolve('ts-node/node10/tsconfig.json'); - testsDirRequire.resolve('ts-node/node12/tsconfig.json'); testsDirRequire.resolve('ts-node/node14/tsconfig.json'); testsDirRequire.resolve('ts-node/node16/tsconfig.json'); + testsDirRequire.resolve('ts-node/node18/tsconfig.json'); }); test('should not load typescript outside of loadConfig', async () => { @@ -215,9 +208,7 @@ test.suite('ts-node', (test) => { }); test.suite('should support mts when module = ESNext', (test) => { - test.runIf( - nodeSupportsSpawningChildProcess && tsSupportsMtsCtsExtensions - ); + test.runIf(tsSupportsMtsCtsExtensions); test('test', async () => { const { err, stdout } = await exec( [CMD_TS_NODE_WITHOUT_PROJECT_FLAG, './entrypoint.mjs'].join(' '), @@ -396,18 +387,16 @@ test.suite('ts-node', (test) => { }); }); - if (nodeSupportsEsmHooks) { - 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://'); - }); - } + 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://'); + }); test('should pipe into `ts-node` and evaluate', async () => { const execPromise = exec(CMD_TS_NODE_WITH_PROJECT_TRANSPILE_ONLY_FLAG); @@ -529,10 +518,6 @@ test.suite('ts-node', (test) => { }); test.suite('issue #884', (test) => { - // TODO disabled because it consistently fails on Windows on TS 2.7 - test.skipIf( - process.platform === 'win32' && semver.satisfies(ts.version, '2.7') - ); test('should compile', async (t) => { const { err, stdout } = await exec( `"${BIN_PATH}" --project issue-884/tsconfig.json issue-884` @@ -753,22 +738,20 @@ test.suite('ts-node', (test) => { ]); }); - if (tsSupportsTsconfigInheritanceViaNodePackages) { - test('should pull ts-node options from extended `tsconfig.json`', async () => { - const { err, stdout } = await exec( - `${BIN_PATH} --show-config --project ./tsconfig-extends/tsconfig.json` - ); - expect(err).toBe(null); - const config = JSON.parse(stdout); - expect(config['ts-node'].require).toEqual([ - resolve(TEST_DIR, 'tsconfig-extends/other/require-hook.js'), - ]); - expect(config['ts-node'].scopeDir).toBe( - resolve(TEST_DIR, 'tsconfig-extends/other/scopedir') - ); - expect(config['ts-node'].preferTsExts).toBe(true); - }); - } + test('should pull ts-node options from extended `tsconfig.json`', async () => { + const { err, stdout } = await exec( + `${BIN_PATH} --show-config --project ./tsconfig-extends/tsconfig.json` + ); + expect(err).toBe(null); + const config = JSON.parse(stdout); + expect(config['ts-node'].require).toEqual([ + resolve(TEST_DIR, 'tsconfig-extends/other/require-hook.js'), + ]); + expect(config['ts-node'].scopeDir).toBe( + resolve(TEST_DIR, 'tsconfig-extends/other/scopedir') + ); + expect(config['ts-node'].preferTsExts).toBe(true); + }); }); test.suite( @@ -777,51 +760,35 @@ test.suite('ts-node', (test) => { const test = context(async (t) => ({ tempDir: mkdtempSync(join(tmpdir(), 'ts-node-spec')), })); - if ( - semver.gte(ts.version, '3.5.0') && - semver.gte(process.versions.node, '14.0.0') - ) { - const libAndTarget = semver.gte(process.versions.node, '16.0.0') - ? 'es2021' - : 'es2020'; - test('implicitly uses @tsconfig/node14 or @tsconfig/node16 compilerOptions when both TS and node versions support it', async (t) => { - // node14 and node16 configs are identical, hence the "or" - 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), { - 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'); - }); - } else { - test('implicitly uses @tsconfig/* lower than node14 (node12) when either TS or node versions do not support @tsconfig/node14', async ({ + const libAndTarget = semver.gte(process.versions.node, '18.0.0') + ? 'es2022' + : semver.gte(process.versions.node, '16.0.0') + ? '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 }, - }) => { - const { err, stdout, stderr } = await exec(`${BIN_PATH} -pe 10n`, { - cwd: tempDir, - }); - expect(err).not.toBe(null); - expect(stderr).toMatch( - /BigInt literals are not available when targeting lower than|error TS2304: Cannot find name 'n'/ - ); + } = t; + const { + err: err1, + stdout: stdout1, + stderr: stderr1, + } = await exec(`${BIN_PATH} --showConfig`, { cwd: tempDir }); + expect(err1).toBe(null); + t.like(JSON.parse(stdout1), { + 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'); + }); test('implicitly loads @types/node even when not installed within local directory', async ({ context: { tempDir }, }) => { @@ -860,8 +827,6 @@ test.suite('ts-node', (test) => { test.suite( 'should bundle @tsconfig/bases to be used in your own tsconfigs', (test) => { - test.runIf(tsSupportsTsconfigInheritanceViaNodePackages); - const macro = test.macro((nodeVersion: string) => async (t) => { const config = require(`@tsconfig/${nodeVersion}/tsconfig.json`); const { err, stdout, stderr } = await exec( @@ -878,10 +843,9 @@ test.suite('ts-node', (test) => { }, }); }); - test(`ts-node/node10/tsconfig.json`, macro, 'node10'); - test(`ts-node/node12/tsconfig.json`, macro, 'node12'); test(`ts-node/node14/tsconfig.json`, macro, 'node14'); test(`ts-node/node16/tsconfig.json`, macro, 'node16'); + test(`ts-node/node18/tsconfig.json`, macro, 'node18'); } ); @@ -943,58 +907,48 @@ test.suite('ts-node', (test) => { }); }); - if (tsSupportsShowConfig) { - test('--showConfig should log resolved configuration', async (t) => { - function native(path: string) { - return path.replace(/\/|\\/g, pathSep); - } - function posix(path: string) { - return path.replace(/\/|\\/g, '/'); - } - const { err, stdout } = await exec( - `${CMD_TS_NODE_WITH_PROJECT_FLAG} --showConfig` - ); - expect(err).toBe(null); - t.is( - stdout, - JSON.stringify( - { - 'ts-node': { - cwd: native(`${ROOT_DIR}/tests`), - projectSearchDir: native(`${ROOT_DIR}/tests`), - project: native(`${ROOT_DIR}/tests/tsconfig.json`), - }, - compilerOptions: { - target: 'es6', - jsx: 'react', - noEmit: false, - strict: true, - typeRoots: [ - posix(`${ROOT_DIR}/tests/typings`), - posix(`${ROOT_DIR}/node_modules/@types`), - ], - sourceMap: true, - inlineSourceMap: false, - inlineSources: true, - declaration: false, - outDir: './.ts-node', - module: 'commonjs', - }, + test('--showConfig should log resolved configuration', async (t) => { + function native(path: string) { + return path.replace(/\/|\\/g, pathSep); + } + function posix(path: string) { + return path.replace(/\/|\\/g, '/'); + } + const { err, stdout } = await exec( + `${CMD_TS_NODE_WITH_PROJECT_FLAG} --showConfig` + ); + expect(err).toBe(null); + t.is( + stdout, + JSON.stringify( + { + 'ts-node': { + cwd: native(`${ROOT_DIR}/tests`), + projectSearchDir: native(`${ROOT_DIR}/tests`), + project: native(`${ROOT_DIR}/tests/tsconfig.json`), }, - null, - 2 - ) + '\n' - ); - }); - } else { - test('--show-config should log error message when used with old typescript versions', async (t) => { - const { err, stderr } = await exec( - `${CMD_TS_NODE_WITH_PROJECT_FLAG} --showConfig` - ); - expect(err).not.toBe(null); - expect(stderr).toMatch('Error: --showConfig requires'); - }); - } + compilerOptions: { + target: 'es6', + jsx: 'react', + noEmit: false, + strict: true, + typeRoots: [ + posix(`${ROOT_DIR}/tests/typings`), + posix(`${ROOT_DIR}/node_modules/@types`), + ], + sourceMap: true, + inlineSourceMap: false, + inlineSources: true, + declaration: false, + outDir: './.ts-node', + module: 'commonjs', + }, + }, + null, + 2 + ) + '\n' + ); + }); test('should support compiler scope specified via tsconfig.json', async (t) => { const { err, stderr, stdout } = await exec( diff --git a/src/test/module-node/1778.spec.ts b/src/test/module-node/1778.spec.ts index 43d65e0f7..4ec96bf8c 100644 --- a/src/test/module-node/1778.spec.ts +++ b/src/test/module-node/1778.spec.ts @@ -1,11 +1,9 @@ import { createExec } from '../exec-helpers'; import { ctxTsNode, - nodeSupportsEsmHooks, TEST_DIR, tsSupportsStableNodeNextNode16, CMD_TS_NODE_WITHOUT_PROJECT_FLAG, - nodeSupportsSpawningChildProcess, } from '../helpers'; import { context, expect } from '../testlib'; import { join } from 'path'; @@ -19,11 +17,7 @@ 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( - nodeSupportsEsmHooks && - nodeSupportsSpawningChildProcess && - tsSupportsStableNodeNextNode16 - ); + test.runIf(tsSupportsStableNodeNextNode16); test('test', async () => { const { err, stdout } = await exec( `${CMD_TS_NODE_WITHOUT_PROJECT_FLAG} ./index.ts`, diff --git a/src/test/pluggable-dep-resolution.spec.ts b/src/test/pluggable-dep-resolution.spec.ts index 480a40bbc..398442f86 100644 --- a/src/test/pluggable-dep-resolution.spec.ts +++ b/src/test/pluggable-dep-resolution.spec.ts @@ -1,10 +1,5 @@ import { context } from './testlib'; -import { - ctxTsNode, - resetNodeEnvironment, - TEST_DIR, - tsSupportsTsconfigInheritanceViaNodePackages, -} from './helpers'; +import { ctxTsNode, resetNodeEnvironment, TEST_DIR } from './helpers'; import * as expect from 'expect'; import { resolve } from 'path'; @@ -91,8 +86,6 @@ test.suite( ); test.suite('"extends"', (test) => { - test.runIf(tsSupportsTsconfigInheritanceViaNodePackages); - test( macro, 'tsconfig-extend-custom-compiler.json', diff --git a/src/test/repl/node-repl-tla.ts b/src/test/repl/node-repl-tla.ts index 2f30a06f2..c9bc3f92a 100644 --- a/src/test/repl/node-repl-tla.ts +++ b/src/test/repl/node-repl-tla.ts @@ -4,7 +4,6 @@ import { Stream } from 'stream'; import semver = require('semver'); import { ts } from '../helpers'; import type { ctxTsNode } from '../helpers'; -import { nodeSupportsEsmHooks } from '../helpers'; interface SharedObjects extends ctxTsNode.Ctx { TEST_DIR: string; @@ -32,11 +31,7 @@ export async function upstreamTopLevelAwaitTests({ experimentalReplAwait: true, transpileOnly: true, compilerOptions: { - target: semver.gte(ts.version, '3.0.1') - ? 'es2018' - : // TS 2.7 is using polyfill for async interator even though they - // were added in es2018 - 'esnext', + target: 'es2018', }, }); replService.setService(service); @@ -114,26 +109,17 @@ export async function upstreamTopLevelAwaitTests({ ['foo', '[Function: foo]'], ['class Foo {}; await 1;', '1'], - [ - 'Foo', - // Adjusted since ts-node supports older versions of node - semver.gte(process.version, '12.18.0') - ? '[class Foo]' - : '[Function: Foo]', - ], + ['Foo', '[class Foo]'], ['if (await true) { function fooz() {}; }'], ['fooz', '[Function: fooz]'], ['if (await true) { class Bar {}; }'], [ 'Bar', - // Adjusted since ts-node supports older versions of node - nodeSupportsEsmHooks - ? 'Uncaught ReferenceError: Bar is not defined' - : 'ReferenceError: Bar is not defined', + 'Uncaught ReferenceError: Bar is not defined', // Line increased due to TS added lines { - line: nodeSupportsEsmHooks ? 4 : 5, + line: 4, }, ], @@ -144,13 +130,10 @@ export async function upstreamTopLevelAwaitTests({ [ 'j', - // Adjusted since ts-node supports older versions of node - nodeSupportsEsmHooks - ? 'Uncaught ReferenceError: j is not defined' - : 'ReferenceError: j is not defined', + 'Uncaught ReferenceError: j is not defined', // Line increased due to TS added lines { - line: nodeSupportsEsmHooks ? 4 : 5, + line: 4, }, ], @@ -158,13 +141,10 @@ export async function upstreamTopLevelAwaitTests({ [ 'return 42; await 5;', - // Adjusted since ts-node supports older versions of node - nodeSupportsEsmHooks - ? 'Uncaught SyntaxError: Illegal return statement' - : 'SyntaxError: Illegal return statement', + 'Uncaught SyntaxError: Illegal return statement', // Line increased due to TS added lines { - line: nodeSupportsEsmHooks ? 4 : 5, + line: 4, }, ], diff --git a/src/test/repl/repl.spec.ts b/src/test/repl/repl.spec.ts index 55f49cb68..086bc37cf 100644 --- a/src/test/repl/repl.spec.ts +++ b/src/test/repl/repl.spec.ts @@ -148,10 +148,9 @@ test.suite('top level await', ({ context }) => { } }); - if (semver.gte(ts.version, '3.8.0')) { - // Serial because it's timing-sensitive - test.serial('should allow evaluating top level await', async (t) => { - const script = ` + // Serial because it's timing-sensitive + test.serial('should allow evaluating top level await', async (t) => { + const script = ` const x: number = await new Promise((r) => r(1)); for await (const x of [1,2,3]) { console.log(x) }; for (const x of ['a', 'b']) { await x; console.log(x) }; @@ -162,123 +161,113 @@ 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 { stdout, stderr } = await t.context.executeInTlaRepl( + script, + '6\n> ' + ); + expect(stderr).toBe(''); + expect(stdout).toBe('> 1\n2\n3\na\nb\n6\n> '); + }); - // Serial because it's timing-sensitive - test.serial( - 'should wait until promise is settled when awaiting at top level', - async (t) => { - const awaitMs = 500; - const script = ` + // Serial because it's timing-sensitive + test.serial( + 'should wait until promise is settled when awaiting at top level', + async (t) => { + const awaitMs = 500; + const script = ` const startTime = new Date().getTime(); await new Promise((r) => setTimeout(() => r(1), ${awaitMs})); const endTime = new Date().getTime(); endTime - startTime; `; - const { stdout, stderr } = await t.context.executeInTlaRepl( - script, - /\d+\n/ - ); - - expect(stderr).toBe(''); - - const elapsedTimeString = stdout - .split('\n')[0] - .replace('> ', '') - .trim(); - expect(elapsedTimeString).toMatch(/^\d+$/); - const elapsedTime = Number(elapsedTimeString); - expect(elapsedTime).toBeGreaterThanOrEqual(awaitMs - 50); - // When CI is taxed, the time may be *much* greater than expected. - // I can't think of a case where the time being *too high* is a bug - // that this test can catch. So I've made this check very loose. - expect(elapsedTime).toBeLessThanOrEqual(awaitMs + 10e3); - } - ); + const { stdout, stderr } = await t.context.executeInTlaRepl( + script, + /\d+\n/ + ); + + expect(stderr).toBe(''); - // Serial because it's timing-sensitive - test.serial( - 'should not wait until promise is settled when not using await at top level', - async (t) => { - const script = ` + const elapsedTimeString = stdout.split('\n')[0].replace('> ', '').trim(); + expect(elapsedTimeString).toMatch(/^\d+$/); + const elapsedTime = Number(elapsedTimeString); + expect(elapsedTime).toBeGreaterThanOrEqual(awaitMs - 50); + // When CI is taxed, the time may be *much* greater than expected. + // I can't think of a case where the time being *too high* is a bug + // that this test can catch. So I've made this check very loose. + expect(elapsedTime).toBeLessThanOrEqual(awaitMs + 10e3); + } + ); + + // Serial because it's timing-sensitive + test.serial( + 'should not wait until promise is settled when not using await at top level', + async (t) => { + const script = ` const startTime = new Date().getTime(); (async () => await new Promise((r) => setTimeout(() => r(1), ${5000})))(); const endTime = new Date().getTime(); endTime - startTime; `; - const { stdout, stderr } = await t.context.executeInTlaRepl( - script, - /\d+\n/ - ); - - expect(stderr).toBe(''); - - const ellapsedTime = Number( - stdout.split('\n')[0].replace('> ', '').trim() - ); - expect(ellapsedTime).toBeGreaterThanOrEqual(0); - // Should ideally be instantaneous; leave wiggle-room for slow CI - expect(ellapsedTime).toBeLessThanOrEqual(100); - } - ); + const { stdout, stderr } = await t.context.executeInTlaRepl( + script, + /\d+\n/ + ); - // Serial because it's timing-sensitive - test.serial( - 'should error with typing information when awaited result has type mismatch', - async (t) => { - const { stdout, stderr } = await t.context.executeInTlaRepl( - 'const x: string = await 1', - 'error' - ); - - expect(stdout).toBe('> > '); - expect(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` - : `Type '1' is not assignable to type 'string'.\n`) + - '\n' - ); - } - ); + expect(stderr).toBe(''); - // Serial because it's timing-sensitive - test.serial( - 'should error with typing information when importing a file with type errors', - async (t) => { - const { stdout, stderr } = await t.context.executeInTlaRepl( - `const {foo} = await import('./repl/tla-import');`, - 'error' - ); - - expect(stdout).toBe('> > '); - expect(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` - : `Type '1' is not assignable to type 'string'.\n`) + - '\n' - ); - } - ); + const ellapsedTime = Number( + stdout.split('\n')[0].replace('> ', '').trim() + ); + expect(ellapsedTime).toBeGreaterThanOrEqual(0); + // Should ideally be instantaneous; leave wiggle-room for slow CI + expect(ellapsedTime).toBeLessThanOrEqual(100); + } + ); - test('should pass upstream test cases', async (t) => { - const { tsNodeUnderTest } = t.context; - await upstreamTopLevelAwaitTests({ TEST_DIR, tsNodeUnderTest }); - }); - } else { - test('should throw error when attempting to use top level await on TS < 3.8', async (t) => { - expect(t.context.executeInTlaRepl('')).rejects.toThrow( - 'Experimental REPL await is not compatible with TypeScript versions older than 3.8' + // Serial because it's timing-sensitive + test.serial( + 'should error with typing information when awaited result has type mismatch', + async (t) => { + const { stdout, stderr } = await t.context.executeInTlaRepl( + 'const x: string = await 1', + 'error' ); - }); - } + + expect(stdout).toBe('> > '); + expect(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` + : `Type '1' is not assignable to type 'string'.\n`) + + '\n' + ); + } + ); + + // Serial because it's timing-sensitive + test.serial( + 'should error with typing information when importing a file with type errors', + async (t) => { + const { stdout, stderr } = await t.context.executeInTlaRepl( + `const {foo} = await import('./repl/tla-import');`, + 'error' + ); + + expect(stdout).toBe('> > '); + expect(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` + : `Type '1' is not assignable to type 'string'.\n`) + + '\n' + ); + } + ); + + test('should pass upstream test cases', async (t) => { + const { tsNodeUnderTest } = t.context; + await upstreamTopLevelAwaitTests({ TEST_DIR, tsNodeUnderTest }); + }); }); test.suite( @@ -562,9 +551,7 @@ test.suite('REPL treats object literals and block scopes correctly', (test) => { 'repl should treat ({ let v = 0; v; }) as object literal and error', macroReplStderrContains, '({ let v = 0; v; })', - semver.satisfies(ts.version, '2.7') - ? 'error TS2304' - : 'No value exists in scope for the shorthand property' + 'No value exists in scope for the shorthand property' ); test( 'repl should treat { let v = 0; v; } as block scope', @@ -573,7 +560,6 @@ test.suite('REPL treats object literals and block scopes correctly', (test) => { '0' ); test.suite('extra', (test) => { - test.skipIf(semver.satisfies(ts.version, '2.7')); test( 'repl should treat { key: 123 }; as block scope', macroReplNoErrorsAndStdoutContains, @@ -610,9 +596,7 @@ test.suite('REPL treats object literals and block scopes correctly', (test) => { 'repl should treat ({\\nlet v = 0;\\nv;\\n}) as object literal and error', macroReplStderrContains, '({\nlet v = 0;\nv;\n})', - semver.satisfies(ts.version, '2.7') - ? 'error TS2304' - : 'No value exists in scope for the shorthand property' + 'No value exists in scope for the shorthand property' ); test( 'repl should treat {\\nlet v = 0;\\nv;\\n} as block scope', @@ -644,9 +628,7 @@ test.suite('REPL treats object literals and block scopes correctly', (test) => { 'repl should treat { key: 123 }["foo"] as object literal non-existent indexed access', macroReplStderrContains, '{ key: 123 }["foo"]', - semver.satisfies(ts.version, '2.7') - ? 'error TS7017' - : "Property 'foo' does not exist on type" + "Property 'foo' does not exist on type" ); }); }); diff --git a/src/test/resolver.spec.ts b/src/test/resolver.spec.ts index 96d6cd8cf..508331739 100755 --- a/src/test/resolver.spec.ts +++ b/src/test/resolver.spec.ts @@ -4,6 +4,7 @@ import { isOneOf, resetNodeEnvironment, ts, + tsSupportsMtsCtsExtensions, tsSupportsStableNodeNextNode16, } from './helpers'; import { project as fsProject, Project as FsProject } from './fs-helpers'; @@ -157,10 +158,7 @@ const targetPackageStyles = [ test.suite('Resolver hooks', (test) => { test.runSerially(); - test.runIf( - semver.gte(process.version, '14.0.0') && - !semver.satisfies(ts.version, '2.7.x') - ); + test.runIf(tsSupportsMtsCtsExtensions); // // Generate all permutations of projects diff --git a/src/tsconfigs.ts b/src/tsconfigs.ts index cc104fd75..71c3ffaf1 100644 --- a/src/tsconfigs.ts +++ b/src/tsconfigs.ts @@ -8,19 +8,15 @@ const nodeMajor = parseInt(process.versions.node.split('.')[0], 10); */ export function getDefaultTsconfigJsonForNodeVersion(ts: TSCommon): any { const tsInternal = ts as any as TSInternal; - if (nodeMajor >= 16) { - const config = require('@tsconfig/node16/tsconfig.json'); - if (configCompatible(config)) return config; - } - if (nodeMajor >= 14) { - const config = require('@tsconfig/node14/tsconfig.json'); + if (nodeMajor >= 18) { + const config = require('@tsconfig/node18/tsconfig.json'); if (configCompatible(config)) return config; } - if (nodeMajor >= 12) { - const config = require('@tsconfig/node12/tsconfig.json'); + if (nodeMajor >= 16) { + const config = require('@tsconfig/node16/tsconfig.json'); if (configCompatible(config)) return config; } - return require('@tsconfig/node10/tsconfig.json'); + return require('@tsconfig/node14/tsconfig.json'); // Verify that tsconfig target and lib options are compatible with TypeScript compiler function configCompatible(config: { diff --git a/tests/tsconfig-bases/node10/tsconfig.json b/tests/tsconfig-bases/node10/tsconfig.json deleted file mode 100644 index f8b881e4c..000000000 --- a/tests/tsconfig-bases/node10/tsconfig.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "extends": "ts-node/node10/tsconfig.json" -} diff --git a/tests/tsconfig-bases/node12/tsconfig.json b/tests/tsconfig-bases/node12/tsconfig.json deleted file mode 100644 index eda168e10..000000000 --- a/tests/tsconfig-bases/node12/tsconfig.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "extends": "ts-node/node12/tsconfig.json" -} diff --git a/tests/tsconfig-bases/node18/tsconfig.json b/tests/tsconfig-bases/node18/tsconfig.json new file mode 100644 index 000000000..4c34d08cf --- /dev/null +++ b/tests/tsconfig-bases/node18/tsconfig.json @@ -0,0 +1,3 @@ +{ + "extends": "ts-node/node18/tsconfig.json" +} diff --git a/tsconfig.json b/tsconfig.json index 0b879a942..83c6df2eb 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -2,9 +2,9 @@ { "$schema": "./tsconfig.schemastore-schema.json", "compilerOptions": { - // `target` and `lib` match @tsconfig/bases for node12, since that's the oldest node LTS, so it's the oldest node we support - "target": "es2019", - "lib": ["es2019", "es2020.promise", "es2020.bigint", "es2020.string", "dom"], + // `target` and `lib` match @tsconfig/bases for node14, since that's the oldest node LTS, so it's the oldest node we support + "target": "es2020", + "lib": ["es2020"], "rootDir": ".", "outDir": "temp", "tsBuildInfoFile": "temp/tsconfig.tsbuildinfo",