From 8aedaed7548e814d70963d27d916a2a46788fab2 Mon Sep 17 00:00:00 2001 From: Andrew Bradley Date: Sat, 2 Jul 2022 12:50:04 -0400 Subject: [PATCH 01/19] Revert "Use file URL for source map paths (#1771)" (#1821) This reverts commit 599f28bbed574003aea08cffab098a3348475649. --- src/index.ts | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/src/index.ts b/src/index.ts index 799731f2a..607d5976d 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,7 +1,7 @@ -import { relative, basename, extname, dirname, join, isAbsolute } from 'path'; +import { relative, basename, extname, dirname, join } from 'path'; import { Module } from 'module'; import * as util from 'util'; -import { fileURLToPath, pathToFileURL } from 'url'; +import { fileURLToPath } from 'url'; import type * as _sourceMapSupport from '@cspotcode/source-map-support'; import { BaseError } from 'make-error'; @@ -1667,11 +1667,8 @@ function updateOutput( */ function updateSourceMap(sourceMapText: string, fileName: string) { const sourceMap = JSON.parse(sourceMapText); - const outputFileName = isAbsolute(fileName) - ? pathToFileURL(fileName).href - : fileName; - sourceMap.file = outputFileName; - sourceMap.sources = [outputFileName]; + sourceMap.file = fileName; + sourceMap.sources = [fileName]; delete sourceMap.sourceRoot; return JSON.stringify(sourceMap); } From 26d171693013445d4c0a676da97eee51723847eb Mon Sep 17 00:00:00 2001 From: Andrew Bradley Date: Sat, 2 Jul 2022 12:55:54 -0400 Subject: [PATCH 02/19] Docs merge for 10.8.2 release (#1823) * Update options.md (#1768) * Update options.md * Update options.md * Update configuration.md * Update performance.md * Update transpilers.md From 9c2d5d6889046ec5b033e96145f83ba568bd3136 Mon Sep 17 00:00:00 2001 From: Andrew Bradley Date: Sat, 2 Jul 2022 12:27:19 -0400 Subject: [PATCH 03/19] tweak contributing doc to explain the correct `np` invocation --- CONTRIBUTING.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 41f797264..a2ccaad09 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -109,6 +109,8 @@ We publish using `np`: https://npm.im/np 2. Rebuild the README (see instructions above, necessary because npmjs.com renders the readme) 3. (optional) Update the api-extractor report; check for unexpected changes. See below 4. Publish with `np` + - `np --branch main --no-tests` + - `--no-tests` because we must rely on CI to test ts-node. Even if you *did* run the tests locally, you would only be testing a single operating system, node version, and TypeScript version, so locally-run tests are insufficient. 5. Add changelog to the Github Release; match formatting from previous releases 6. Move `docs` branch to head of `main` - this rebuilds the website From ddd559dc0f1aec9a31c2f5aa6e868c59d43ba56c Mon Sep 17 00:00:00 2001 From: Andrew Bradley Date: Sat, 2 Jul 2022 13:56:50 -0400 Subject: [PATCH 04/19] 10.8.2 --- package-lock.json | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index 9722c5fd8..237b2d067 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "ts-node", - "version": "10.8.1", + "version": "10.8.2", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 96cc40187..7fe9bad62 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ts-node", - "version": "10.8.1", + "version": "10.8.2", "description": "TypeScript execution environment and REPL for node.js, with source map support", "main": "dist/index.js", "exports": { From 91110cdaf4a67f3b915c67221145fa3f677405eb Mon Sep 17 00:00:00 2001 From: Andrew Bradley Date: Sat, 2 Jul 2022 15:47:45 -0400 Subject: [PATCH 05/19] Allow opting-in to `.ts` import specifiers (#1815) * quick impl * fix * update * add a test * add jsdoc for new option --- src/configuration.ts | 2 ++ src/file-extensions.ts | 20 ++++++++++++++++++ src/index.ts | 23 +++++++++++++++++++- src/resolver-functions.ts | 27 +++++++++++++++++++++++- src/test/ts-import-specifiers.spec.ts | 22 +++++++++++++++++++ tests/ts-import-specifiers/bar.tsx | 1 + tests/ts-import-specifiers/foo.ts | 1 + tests/ts-import-specifiers/index.ts | 3 +++ tests/ts-import-specifiers/tsconfig.json | 10 +++++++++ 9 files changed, 107 insertions(+), 2 deletions(-) create mode 100644 src/test/ts-import-specifiers.spec.ts create mode 100644 tests/ts-import-specifiers/bar.tsx create mode 100644 tests/ts-import-specifiers/foo.ts create mode 100644 tests/ts-import-specifiers/index.ts create mode 100644 tests/ts-import-specifiers/tsconfig.json diff --git a/src/configuration.ts b/src/configuration.ts index 5142a3584..266f2d920 100644 --- a/src/configuration.ts +++ b/src/configuration.ts @@ -383,6 +383,7 @@ function filterRecognizedTsConfigTsNodeOptions(jsonObject: any): { experimentalResolver, esm, experimentalSpecifierResolution, + experimentalTsImportSpecifiers, ...unrecognized } = jsonObject as TsConfigOptions; const filteredTsConfigOptions = { @@ -409,6 +410,7 @@ function filterRecognizedTsConfigTsNodeOptions(jsonObject: any): { experimentalResolver, esm, experimentalSpecifierResolution, + experimentalTsImportSpecifiers, }; // Use the typechecker to make sure this implementation has the correct set of properties const catchExtraneousProps: keyof TsConfigOptions = diff --git a/src/file-extensions.ts b/src/file-extensions.ts index 87e8be1c6..b5fd03552 100644 --- a/src/file-extensions.ts +++ b/src/file-extensions.ts @@ -19,6 +19,13 @@ const nodeEquivalents = new Map([ ['.cts', '.cjs'], ]); +const tsResolverEquivalents = new Map([ + ['.ts', ['.js']], + ['.tsx', ['.js', '.jsx']], + ['.mts', ['.mjs']], + ['.cts', ['.cjs']], +]); + // All extensions understood by vanilla node const vanillaNodeExtensions: readonly string[] = [ '.js', @@ -129,6 +136,19 @@ export function getExtensions( * as far as getFormat is concerned. */ nodeEquivalents, + /** + * Mapping from extensions rejected by TSC in import specifiers, to the + * possible alternatives that TS's resolver will accept. + * + * When we allow users to opt-in to .ts extensions in import specifiers, TS's + * resolver requires us to replace the .ts extensions with .js alternatives. + * Otherwise, resolution fails. + * + * Note TS's resolver is only used by, and only required for, typechecking. + * This is separate from node's resolver, which we hook separately and which + * does not require this mapping. + */ + tsResolverEquivalents, /** * Extensions that we can support if the user upgrades their typescript version. * Used when raising hints. diff --git a/src/index.ts b/src/index.ts index 607d5976d..7167dbe1b 100644 --- a/src/index.ts +++ b/src/index.ts @@ -373,6 +373,17 @@ export interface CreateOptions { * For details, see https://nodejs.org/dist/latest-v18.x/docs/api/esm.html#customizing-esm-specifier-resolution-algorithm */ experimentalSpecifierResolution?: 'node' | 'explicit'; + /** + * Allow using voluntary `.ts` file extension in import specifiers. + * + * Typically, in ESM projects, import specifiers must hanve an emit extension, `.js`, `.cjs`, or `.mjs`, + * and we automatically map to the corresponding `.ts`, `.cts`, or `.mts` source file. This is the + * recommended approach. + * + * However, if you really want to use `.ts` in import specifiers, and are aware that this may + * break tooling, you can enable this flag. + */ + experimentalTsImportSpecifiers?: boolean; } export type ModuleTypes = Record; @@ -693,6 +704,11 @@ export function createFromPreloadedConfig( 6059, // "'rootDir' is expected to contain all source files." 18002, // "The 'files' list in config file is empty." 18003, // "No inputs were found in config file." + ...(options.experimentalTsImportSpecifiers + ? [ + 2691, // "An import path cannot end with a '.ts' extension. Consider importing '' instead." + ] + : []), ...(options.ignoreDiagnostics || []), ].map(Number), }, @@ -905,6 +921,8 @@ export function createFromPreloadedConfig( patterns: options.moduleTypes, }); + const extensions = getExtensions(config, options, ts.version); + // Use full language services when the fast option is disabled. if (!transpileOnly) { const fileContents = new Map(); @@ -985,6 +1003,8 @@ export function createFromPreloadedConfig( cwd, config, projectLocalResolveHelper, + options, + extensions, }); serviceHost.resolveModuleNames = resolveModuleNames; serviceHost.getResolvedModuleWithFailedLookupLocationsFromCache = @@ -1143,6 +1163,8 @@ export function createFromPreloadedConfig( ts, getCanonicalFileName, projectLocalResolveHelper, + options, + extensions, }); host.resolveModuleNames = resolveModuleNames; host.resolveTypeReferenceDirectives = resolveTypeReferenceDirectives; @@ -1448,7 +1470,6 @@ export function createFromPreloadedConfig( let active = true; const enabled = (enabled?: boolean) => enabled === undefined ? active : (active = !!enabled); - const extensions = getExtensions(config, options, ts.version); const ignored = (fileName: string) => { if (!active) return true; const ext = extname(fileName); diff --git a/src/resolver-functions.ts b/src/resolver-functions.ts index afe13b463..83568669c 100644 --- a/src/resolver-functions.ts +++ b/src/resolver-functions.ts @@ -1,4 +1,6 @@ import { resolve } from 'path'; +import type { CreateOptions } from '.'; +import type { Extensions } from './file-extensions'; import type { TSCommon, TSInternal } from './ts-compiler-types'; import type { ProjectLocalResolveHelper } from './util'; @@ -13,6 +15,8 @@ export function createResolverFunctions(kwargs: { getCanonicalFileName: (filename: string) => string; config: TSCommon.ParsedCommandLine; projectLocalResolveHelper: ProjectLocalResolveHelper; + options: CreateOptions; + extensions: Extensions; }) { const { host, @@ -21,6 +25,8 @@ export function createResolverFunctions(kwargs: { cwd, getCanonicalFileName, projectLocalResolveHelper, + options, + extensions, } = kwargs; const moduleResolutionCache = ts.createModuleResolutionCache( cwd, @@ -105,7 +111,7 @@ export function createResolverFunctions(kwargs: { i ) : undefined; - const { resolvedModule } = ts.resolveModuleName( + let { resolvedModule } = ts.resolveModuleName( moduleName, containingFile, config.options, @@ -114,6 +120,25 @@ export function createResolverFunctions(kwargs: { redirectedReference, mode ); + if (!resolvedModule && options.experimentalTsImportSpecifiers) { + const lastDotIndex = moduleName.lastIndexOf('.'); + const ext = lastDotIndex >= 0 ? moduleName.slice(lastDotIndex) : ''; + if (ext) { + const replacements = extensions.tsResolverEquivalents.get(ext); + for (const replacementExt of replacements ?? []) { + ({ resolvedModule } = ts.resolveModuleName( + moduleName.slice(0, -ext.length) + replacementExt, + containingFile, + config.options, + host, + moduleResolutionCache, + redirectedReference, + mode + )); + if (resolvedModule) break; + } + } + } if (resolvedModule) { fixupResolvedModule(resolvedModule); } diff --git a/src/test/ts-import-specifiers.spec.ts b/src/test/ts-import-specifiers.spec.ts new file mode 100644 index 000000000..39c4cc294 --- /dev/null +++ b/src/test/ts-import-specifiers.spec.ts @@ -0,0 +1,22 @@ +import { context } from './testlib'; +import * as expect from 'expect'; +import { createExec } from './exec-helpers'; +import { + TEST_DIR, + ctxTsNode, + CMD_TS_NODE_WITHOUT_PROJECT_FLAG, +} from './helpers'; + +const exec = createExec({ + cwd: TEST_DIR, +}); + +const test = context(ctxTsNode); + +test('Supports .ts extensions in import specifiers with typechecking, even though vanilla TS checker does not', async () => { + const { err, stdout } = await exec( + `${CMD_TS_NODE_WITHOUT_PROJECT_FLAG} ts-import-specifiers/index.ts` + ); + expect(err).toBe(null); + expect(stdout.trim()).toBe('{ foo: true, bar: true }'); +}); diff --git a/tests/ts-import-specifiers/bar.tsx b/tests/ts-import-specifiers/bar.tsx new file mode 100644 index 000000000..3a850c17c --- /dev/null +++ b/tests/ts-import-specifiers/bar.tsx @@ -0,0 +1 @@ +export const bar = true; diff --git a/tests/ts-import-specifiers/foo.ts b/tests/ts-import-specifiers/foo.ts new file mode 100644 index 000000000..62d968e82 --- /dev/null +++ b/tests/ts-import-specifiers/foo.ts @@ -0,0 +1 @@ +export const foo = true; diff --git a/tests/ts-import-specifiers/index.ts b/tests/ts-import-specifiers/index.ts new file mode 100644 index 000000000..2f1444fb5 --- /dev/null +++ b/tests/ts-import-specifiers/index.ts @@ -0,0 +1,3 @@ +import { foo } from './foo.ts'; +import { bar } from './bar.jsx'; +console.log({ foo, bar }); diff --git a/tests/ts-import-specifiers/tsconfig.json b/tests/ts-import-specifiers/tsconfig.json new file mode 100644 index 000000000..098594e5f --- /dev/null +++ b/tests/ts-import-specifiers/tsconfig.json @@ -0,0 +1,10 @@ +{ + "ts-node": { + // Can eventually make this a stable feature. For now, `experimental` flag allows me to iterate quickly + "experimentalTsImportSpecifiers": true, + "experimentalResolver": true + }, + "compilerOptions": { + "jsx": "react" + } +} From ab15063b0c367f49a1c9565781572cae7cc5c229 Mon Sep 17 00:00:00 2001 From: Andrew Bradley Date: Sat, 2 Jul 2022 15:49:56 -0400 Subject: [PATCH 06/19] swc plugin enhancements (#1802) * refactor swc plugin to make it more test-able; also throw helpful error when users swc dep is too old * fmt * fix * fix import assertions * fix --- package-lock.json | 150 ++++++++++--------- package.json | 7 +- src/test/helpers.ts | 2 + src/test/transpilers.spec.ts | 124 +++++++++++++++- src/transpilers/swc.ts | 271 +++++++++++++++++++++-------------- 5 files changed, 369 insertions(+), 185 deletions(-) diff --git a/package-lock.json b/package-lock.json index 237b2d067..f1f3cb36a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -508,21 +508,6 @@ } } }, - "@napi-rs/triples": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@napi-rs/triples/-/triples-1.0.3.tgz", - "integrity": "sha512-jDJTpta+P4p1NZTFVLHJ/TLFVYVcOqv6l8xwOeBKNPMgY/zDYH/YH7SJbvrr/h1RcS9GzbPcLKGzpuK9cV56UA==", - "dev": true - }, - "@node-rs/helper": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@node-rs/helper/-/helper-1.2.1.tgz", - "integrity": "sha512-R5wEmm8nbuQU0YGGmYVjEc0OHtYsuXdpRG+Ut/3wZ9XAvQWyThN08bTh2cBJgoZxHQUPtvRfeQuxcAgLuiBISg==", - "dev": true, - "requires": { - "@napi-rs/triples": "^1.0.3" - } - }, "@nodelib/fs.scandir": { "version": "2.1.4", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.4.tgz", @@ -644,114 +629,121 @@ "dev": true }, "@swc/core": { - "version": "1.2.106", - "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.2.106.tgz", - "integrity": "sha512-9uw8gqU+lsk7KROAcSNhsrnBgNiC5H4MIaps5LlnnEevJmKu/o1ws22tXc2qjJg+F4/V1ynUbh8E0rYlmo1XGw==", - "dev": true, - "requires": { - "@node-rs/helper": "^1.0.0", - "@swc/core-android-arm64": "^1.2.106", - "@swc/core-darwin-arm64": "^1.2.106", - "@swc/core-darwin-x64": "^1.2.106", - "@swc/core-freebsd-x64": "^1.2.106", - "@swc/core-linux-arm-gnueabihf": "^1.2.106", - "@swc/core-linux-arm64-gnu": "^1.2.106", - "@swc/core-linux-arm64-musl": "^1.2.106", - "@swc/core-linux-x64-gnu": "^1.2.106", - "@swc/core-linux-x64-musl": "^1.2.106", - "@swc/core-win32-arm64-msvc": "^1.2.106", - "@swc/core-win32-ia32-msvc": "^1.2.106", - "@swc/core-win32-x64-msvc": "^1.2.106" - } + "version": "1.2.205", + "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.2.205.tgz", + "integrity": "sha512-evq0/tFyYdYgOhKb//+G93fxe9zwFxtme7NL7wSiEF8+4/ON4Y5AI9eHLoqddXqs3W8Y0HQi+rJmlrkCibrseA==", + "dev": true, + "requires": { + "@swc/core-android-arm-eabi": "1.2.205", + "@swc/core-android-arm64": "1.2.205", + "@swc/core-darwin-arm64": "1.2.205", + "@swc/core-darwin-x64": "1.2.205", + "@swc/core-freebsd-x64": "1.2.205", + "@swc/core-linux-arm-gnueabihf": "1.2.205", + "@swc/core-linux-arm64-gnu": "1.2.205", + "@swc/core-linux-arm64-musl": "1.2.205", + "@swc/core-linux-x64-gnu": "1.2.205", + "@swc/core-linux-x64-musl": "1.2.205", + "@swc/core-win32-arm64-msvc": "1.2.205", + "@swc/core-win32-ia32-msvc": "1.2.205", + "@swc/core-win32-x64-msvc": "1.2.205" + } + }, + "@swc/core-android-arm-eabi": { + "version": "1.2.205", + "resolved": "https://registry.npmjs.org/@swc/core-android-arm-eabi/-/core-android-arm-eabi-1.2.205.tgz", + "integrity": "sha512-HfiuVA1JDHMSRQ8nE1DcemUgZ1PKaPwit4i7q3xin0NVbVHY1xkJyQFuLVh3VxTvGKKkF3hi8GJMVQgOXWL6kg==", + "dev": true, + "optional": true }, "@swc/core-android-arm64": { - "version": "1.2.106", - "resolved": "https://registry.npmjs.org/@swc/core-android-arm64/-/core-android-arm64-1.2.106.tgz", - "integrity": "sha512-F5T6kP3yV9S0/oXyco305QaAyE6rLNsNSdR0QI4CtACwKadiPwTOptwNIDCiTNLNgWlWLQmIRkPpxg+G4doT6Q==", + "version": "1.2.205", + "resolved": "https://registry.npmjs.org/@swc/core-android-arm64/-/core-android-arm64-1.2.205.tgz", + "integrity": "sha512-sRGZBV2dOnmh8gWWFo9HVOHdKa33zIsF8/8oYEGtq+2/s96UlAKltO2AA7HH9RaO/fT1tzBZStp+fEMUhDk/FA==", "dev": true, "optional": true }, "@swc/core-darwin-arm64": { - "version": "1.2.106", - "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.2.106.tgz", - "integrity": "sha512-bgKzzYLFnc+mv2mDS/DLwzBvx5DCC9ZCKYY46b4dAnBfasr+SMHj+v/WI84HtilbjLBMUfYZ2hgYKls3CebIIQ==", + "version": "1.2.205", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.2.205.tgz", + "integrity": "sha512-JwVDfKS7vp7zzOQXWNwwcF41h4r3DWEpK6DQjz18WJyS1VVOcpVQGyuE7kSPjcnG01ZxBL9JPwwT353i/8IwDg==", "dev": true, "optional": true }, "@swc/core-darwin-x64": { - "version": "1.2.106", - "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.2.106.tgz", - "integrity": "sha512-I5Uhit5RqbXaMIV2+v9jL+MIQeR3lT1DqVwzxZs1bTARclAheFZQpTmg+h6QmichjCiUT74SXQb6Apc/vqYKog==", + "version": "1.2.205", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.2.205.tgz", + "integrity": "sha512-malz2I+w6xFF1QyTmPGt0Y0NEMbUcrvfr5gUfZDGjxMhPPlS7k6fXucuZxVr9VVaM+JGq1SidVODmZ84jb1qHg==", "dev": true, "optional": true }, "@swc/core-freebsd-x64": { - "version": "1.2.106", - "resolved": "https://registry.npmjs.org/@swc/core-freebsd-x64/-/core-freebsd-x64-1.2.106.tgz", - "integrity": "sha512-ZSK3vgzbA2Pkpw2LgHlAkUdx4okIpdXXTbLXuc5jkZMw1KhRWpeQaDlwbrN7XVynAYjkj2qgGQ7wv1tD43vQig==", + "version": "1.2.205", + "resolved": "https://registry.npmjs.org/@swc/core-freebsd-x64/-/core-freebsd-x64-1.2.205.tgz", + "integrity": "sha512-/nZrG1z0T7h97AsOb/wOtYlnh4WEuNppv3XKQIMPj32YNQdMBVgpybVTVRIs1GQGZMd1/7jAy5BVQcwQjUbrLg==", "dev": true, "optional": true }, "@swc/core-linux-arm-gnueabihf": { - "version": "1.2.106", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.2.106.tgz", - "integrity": "sha512-WZh6XV8cQ9Fh3IQNX9z87Tv68+sLtfnT51ghMQxceRhfvc5gIaYW+PCppezDDdlPJnWXhybGWNPAl5SHppWb2g==", + "version": "1.2.205", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.2.205.tgz", + "integrity": "sha512-mTA3vETMdBmpecUyI9waZYsp7FABhew4e81psspmFpDyfty0SLISWZDnvPAn0pSnb2fWhzKwDC5kdXHKUmLJuA==", "dev": true, "optional": true }, "@swc/core-linux-arm64-gnu": { - "version": "1.2.106", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.2.106.tgz", - "integrity": "sha512-OSI9VUWPsRrCbUlRQ4KdYqdwV63VYBC5ahSNq+72DXhtRwVbLSFuF7MNsnXgUSMHidxbc0No3/bPPamshqHdsQ==", + "version": "1.2.205", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.2.205.tgz", + "integrity": "sha512-qGzFGryeQE+O5SFK7Nn2ESqCEnv00rnzhf11WZF9V71EZ15amIhmbcwHqvFpoRSDw8hZnqoGqfPRfoJbouptnA==", "dev": true, "optional": true }, "@swc/core-linux-arm64-musl": { - "version": "1.2.106", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.2.106.tgz", - "integrity": "sha512-de8AAUOP8D2/tZIpQ399xw+pGGKlR1+l5Jmy4lW7ixarEI4xKkBSF4bS9eXtC1jckmenzrLPiK/5sSbQSf6BWQ==", + "version": "1.2.205", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.2.205.tgz", + "integrity": "sha512-uLJoX9L/4Xg3sLMjAbIhzbTe5gD/MBA8VETBeEkLtgb7a0ys1kvn9xQ6qLw6A71amEPlI+VABnoTRdUEaBSV9Q==", "dev": true, "optional": true }, "@swc/core-linux-x64-gnu": { - "version": "1.2.106", - "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.2.106.tgz", - "integrity": "sha512-QzFC7+lBSuVBmX5tS2pdM+74voiJcGgIMJ+x9pcjUu3GkDl3ow6WC6ta2WUzlgGopCGNp6IdZaFemKRzjLr3lw==", + "version": "1.2.205", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.2.205.tgz", + "integrity": "sha512-gQsjcYlkWKP1kceQIsoHGrOrG7ygW3ojNsSnYoZ5DG5PipRA4eeUfO9YIfrmoa29LiVNjmRPfUJa8O1UHDG5ew==", "dev": true, "optional": true }, "@swc/core-linux-x64-musl": { - "version": "1.2.106", - "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.2.106.tgz", - "integrity": "sha512-QZ1gFqNiCJefkNMihbmYc7nr5stERyjoQpWgAIN6dzrgMUzRHXHGDRl/p1qsXW2VKos+okSdLwPFEmRT94H+1A==", + "version": "1.2.205", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.2.205.tgz", + "integrity": "sha512-LR5ukqBltQc++2eX3qEj/H8KtOt0V3CmtgXNOiNCUxvPDT8mYz/8MJhYOrofonND0RKfXyyPW7dRxg62ceTLSQ==", "dev": true, "optional": true }, "@swc/core-win32-arm64-msvc": { - "version": "1.2.106", - "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.2.106.tgz", - "integrity": "sha512-MbuQwk+s43bfBNnAZTKnoQlfo4UPSOsy6t9F15yU4P3rVUuFtcxdZg6CpDnUqNPbojILXujp8z4SSigRYh5cgg==", + "version": "1.2.205", + "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.2.205.tgz", + "integrity": "sha512-NjcLWm4mOy78LAEt7pqFl+SLcCyqnSlUP729XRd1uRvKwt1Cwch5SQRdoaFqwf1DaEQy4H4iuGPynkfarlb1kQ==", "dev": true, "optional": true }, "@swc/core-win32-ia32-msvc": { - "version": "1.2.106", - "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.2.106.tgz", - "integrity": "sha512-BFxWpcPxsG2LLQZ+8K8ma45rbTckjpPbnvOOhybQ0hEhLgoVzMVPp3RIUGmC+RMZe6DkGSaEQf/Rjn2cbMdQhw==", + "version": "1.2.205", + "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.2.205.tgz", + "integrity": "sha512-+6byrRxIXgZ0zmLL6ZeX1HBBrAqvCy8MR5Yz0SO26jR8OPZXJCgZXL9BTsZO+YEG4f32ZOlZh3nnHCl6Dcb4GA==", "dev": true, "optional": true }, "@swc/core-win32-x64-msvc": { - "version": "1.2.106", - "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.2.106.tgz", - "integrity": "sha512-Emn5akqApGXzPsA7ntSXEohL0AH0WjQMHy6mT3MS9Yil42yTJ96dJGf68ejKVptxwg7Iz798mT+J9r1JbAFBgg==", + "version": "1.2.205", + "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.2.205.tgz", + "integrity": "sha512-RRSkyAol0c7sU9gejtrpF8TLmdYdBjLutcmQHtLKbWTm74ZLidZpF28G0J2tD7HNmzQnMpLzyoT1jW9JgLwzVg==", "dev": true, "optional": true }, "@swc/wasm": { - "version": "1.2.58", - "resolved": "https://registry.npmjs.org/@swc/wasm/-/wasm-1.2.58.tgz", - "integrity": "sha512-3PAMVT+clB2xZsaVtQK2WjgeCftCxDO/0q8JCwW5g3CrLL/WBOCHyCKME3CzGz7V9zfw84QZZMpZY26gohhF/w==", + "version": "1.2.205", + "resolved": "https://registry.npmjs.org/@swc/wasm/-/wasm-1.2.205.tgz", + "integrity": "sha512-xRI7Lrg/v18EnJIHRDflD09bif4Ivc2W0dhRGtTmjdsSrCjJFrArDGaGttD2Fwuv7q1S4/uAWMLFHncwwSyO3Q==", "dev": true }, "@szmarczak/http-timer": { @@ -3583,6 +3575,12 @@ } } }, + "outdent": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/outdent/-/outdent-0.8.0.tgz", + "integrity": "sha512-KiOAIsdpUTcAXuykya5fnVVT+/5uS0Q1mrkRHcF89tpieSmY33O/tmc54CqwA+bfhbtEfZUNLHaPUiB9X3jt1A==", + "dev": true + }, "p-cancelable": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-1.1.0.tgz", @@ -4670,9 +4668,9 @@ } }, "typescript": { - "version": "4.7.2", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.7.2.tgz", - "integrity": "sha512-Mamb1iX2FDUpcTRzltPxgWMKy3fhg0TN378ylbktPGPK/99KbDtMQ4W1hwgsbPAsG3a0xKa1vmw4VKZQbkvz5A==", + "version": "4.7.4", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.7.4.tgz", + "integrity": "sha512-C0WQT0gezHuw6AdY1M2jxUO83Rjf0HP7Sk1DtXj6j1EwkQNZrHAg2XPWlq62oqEhYvONq5pkC2Y9oPljWToLmQ==", "dev": true }, "typescript-json-schema": { diff --git a/package.json b/package.json index 7fe9bad62..0d7958f04 100644 --- a/package.json +++ b/package.json @@ -112,8 +112,8 @@ "homepage": "https://typestrong.org/ts-node", "devDependencies": { "@microsoft/api-extractor": "^7.19.4", - "@swc/core": ">=1.2.50", - "@swc/wasm": ">=1.2.50", + "@swc/core": ">=1.2.205", + "@swc/wasm": ">=1.2.205", "@types/diff": "^4.0.2", "@types/lodash": "^4.14.151", "@types/node": "13.13.5", @@ -131,6 +131,7 @@ "lodash": "^4.17.15", "ntypescript": "^1.201507091536.1", "nyc": "^15.0.1", + "outdent": "^0.8.0", "proper-lockfile": "^4.1.2", "proxyquire": "^2.0.0", "react": "^16.14.0", @@ -138,7 +139,7 @@ "semver": "^7.1.3", "throat": "^6.0.1", "typedoc": "^0.22.10", - "typescript": "4.7.2", + "typescript": "4.7.4", "typescript-json-schema": "^0.53.0", "util.promisify": "^1.0.1" }, diff --git a/src/test/helpers.ts b/src/test/helpers.ts index c4ea9e8a4..da86bddc2 100644 --- a/src/test/helpers.ts +++ b/src/test/helpers.ts @@ -99,6 +99,8 @@ export const tsSupportsStableNodeNextNode16 = // 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'); //#endregion export const xfs = new NodeFS(fs); diff --git a/src/test/transpilers.spec.ts b/src/test/transpilers.spec.ts index 57174f5e8..1d64186a0 100644 --- a/src/test/transpilers.spec.ts +++ b/src/test/transpilers.spec.ts @@ -3,8 +3,15 @@ // Should consolidate them here. import { context } from './testlib'; -import { ctxTsNode, testsDirRequire } from './helpers'; +import { + ctxTsNode, + testsDirRequire, + tsSupportsImportAssertions, + tsSupportsReact17JsxFactories, +} from './helpers'; +import { createSwcOptions } from '../transpilers/swc'; import * as expect from 'expect'; +import { outdent } from 'outdent'; const test = context(ctxTsNode); @@ -44,4 +51,119 @@ test.suite('swc', (test) => { expect([...swcTranspiler.targetMapping.values()]).toContain(target); } }); + + test.suite('converts TS config to swc config', (test) => { + test.suite('jsx', (test) => { + const macro = test.macro( + (jsx: string, runtime?: string, development?: boolean) => [ + () => `jsx=${jsx}`, + async (t) => { + const tsNode = t.context.tsNodeUnderTest.create({ + compilerOptions: { + jsx, + }, + }); + const swcOptions = createSwcOptions( + tsNode.config.options, + undefined, + require('@swc/core'), + '@swc/core' + ); + expect(swcOptions.tsxOptions.jsc?.transform?.react).toBeDefined(); + expect( + swcOptions.tsxOptions.jsc?.transform?.react?.development + ).toBe(development); + expect(swcOptions.tsxOptions.jsc?.transform?.react?.runtime).toBe( + runtime + ); + }, + ] + ); + + test(macro, 'react', undefined, undefined); + test.suite('react 17 jsx factories', (test) => { + test.runIf(tsSupportsReact17JsxFactories); + test(macro, 'react-jsx', 'automatic', undefined); + test(macro, 'react-jsxdev', 'automatic', true); + }); + }); + }); + + const compileMacro = test.macro( + (compilerOptions: object, input: string, expectedOutput: string) => [ + (title?: string) => title ?? `${JSON.stringify(compilerOptions)}`, + async (t) => { + const code = t.context.tsNodeUnderTest + .create({ + swc: true, + skipProject: true, + compilerOptions: { + module: 'esnext', + ...compilerOptions, + }, + }) + .compile(input, 'input.tsx'); + expect(code.replace(/\/\/# sourceMappingURL.*/, '').trim()).toBe( + expectedOutput + ); + }, + ] + ); + + test.suite('transforms various forms of jsx', (test) => { + const input = outdent` + const div =
; + `; + + test( + compileMacro, + { jsx: 'react' }, + input, + `const div = /*#__PURE__*/ React.createElement("div", null);` + ); + test.suite('react 17 jsx factories', (test) => { + test.runIf(tsSupportsReact17JsxFactories); + test( + compileMacro, + { jsx: 'react-jsx' }, + input, + outdent` + import { jsx as _jsx } from "react/jsx-runtime"; + const div = /*#__PURE__*/ _jsx("div", {}); + ` + ); + test( + compileMacro, + { jsx: 'react-jsxdev' }, + input, + outdent` + import { jsxDEV as _jsxDEV } from "react/jsx-dev-runtime"; + const div = /*#__PURE__*/ _jsxDEV("div", {}, void 0, false, { + fileName: "input.tsx", + lineNumber: 1, + columnNumber: 13 + }, this); + ` + ); + }); + }); + + test.suite('preserves import assertions for json imports', (test) => { + test.runIf(tsSupportsImportAssertions); + test( + 'basic json import', + compileMacro, + { module: 'esnext' }, + outdent` + import document from './document.json' assert {type: 'json'}; + document; + `, + outdent` + import document from './document.json' assert { + type: 'json' + }; + document; + ` + ); + }); }); diff --git a/src/transpilers/swc.ts b/src/transpilers/swc.ts index 246b70f40..89e9bd496 100644 --- a/src/transpilers/swc.ts +++ b/src/transpilers/swc.ts @@ -2,7 +2,9 @@ import type * as ts from 'typescript'; import type * as swcWasm from '@swc/wasm'; import type * as swcTypes from '@swc/core'; import type { CreateTranspilerOptions, Transpiler } from './types'; +import type { NodeModuleEmitKind } from '..'; +type SwcInstance = typeof swcWasm; export interface SwcTranspilerOptions extends CreateTranspilerOptions { /** * swc compiler to use for compilation @@ -21,8 +23,11 @@ export function create(createOptions: SwcTranspilerOptions): Transpiler { } = createOptions; // Load swc compiler - let swcInstance: typeof swcWasm; + let swcInstance: SwcInstance; + // Used later in diagnostics; merely needs to be human-readable. + let swcDepName: string = 'swc'; if (typeof swc === 'string') { + swcDepName = swc; swcInstance = require(transpilerConfigLocalResolveHelper( swc, true @@ -30,10 +35,12 @@ export function create(createOptions: SwcTranspilerOptions): Transpiler { } else if (swc == null) { let swcResolved; try { - swcResolved = transpilerConfigLocalResolveHelper('@swc/core', true); + swcDepName = '@swc/core'; + swcResolved = transpilerConfigLocalResolveHelper(swcDepName, true); } catch (e) { try { - swcResolved = transpilerConfigLocalResolveHelper('@swc/wasm', true); + swcDepName = '@swc/wasm'; + swcResolved = transpilerConfigLocalResolveHelper(swcDepName, true); } catch (e) { throw new Error( 'swc compiler requires either @swc/core or @swc/wasm to be installed as a dependency. See https://typestrong.org/ts-node/docs/transpilers' @@ -46,107 +53,12 @@ export function create(createOptions: SwcTranspilerOptions): Transpiler { } // Prepare SWC options derived from typescript compiler options - const compilerOptions = config.options; - const { - esModuleInterop, - sourceMap, - importHelpers, - experimentalDecorators, - emitDecoratorMetadata, - target, - module, - jsxFactory, - jsxFragmentFactory, - strict, - alwaysStrict, - noImplicitUseStrict, - } = compilerOptions; - const nonTsxOptions = createSwcOptions(false); - const tsxOptions = createSwcOptions(true); - function createSwcOptions(isTsx: boolean): swcTypes.Options { - let swcTarget = targetMapping.get(target!) ?? 'es3'; - // Downgrade to lower target if swc does not support the selected target. - // Perhaps project has an older version of swc. - // TODO cache the results of this; slightly faster - let swcTargetIndex = swcTargets.indexOf(swcTarget); - for (; swcTargetIndex >= 0; swcTargetIndex--) { - try { - swcInstance.transformSync('', { - jsc: { target: swcTargets[swcTargetIndex] }, - }); - break; - } catch (e) {} - } - swcTarget = swcTargets[swcTargetIndex]; - const keepClassNames = target! >= /* ts.ScriptTarget.ES2016 */ 3; - const isNodeModuleKind = - module === ModuleKind.Node12 || module === ModuleKind.NodeNext; - // swc only supports these 4x module options [MUST_UPDATE_FOR_NEW_MODULEKIND] - const moduleType = - module === ModuleKind.CommonJS - ? 'commonjs' - : module === ModuleKind.AMD - ? 'amd' - : module === ModuleKind.UMD - ? 'umd' - : isNodeModuleKind && nodeModuleEmitKind === 'nodecjs' - ? 'commonjs' - : isNodeModuleKind && nodeModuleEmitKind === 'nodeesm' - ? 'es6' - : 'es6'; - // In swc: - // strictMode means `"use strict"` is *always* emitted for non-ES module, *never* for ES module where it is assumed it can be omitted. - // (this assumption is invalid, but that's the way swc behaves) - // tsc is a bit more complex: - // alwaysStrict will force emitting it always unless `import`/`export` syntax is emitted which implies it per the JS spec. - // if not alwaysStrict, will emit implicitly whenever module target is non-ES *and* transformed module syntax is emitted. - // For node, best option is to assume that all scripts are modules (commonjs or esm) and thus should get tsc's implicit strict behavior. - - // Always set strictMode, *unless* alwaysStrict is disabled and noImplicitUseStrict is enabled - const strictMode = - // if `alwaysStrict` is disabled, remembering that `strict` defaults `alwaysStrict` to true - (alwaysStrict === false || (alwaysStrict !== true && strict !== true)) && - // if noImplicitUseStrict is enabled - noImplicitUseStrict === true - ? false - : true; - return { - sourceMaps: sourceMap, - // isModule: true, - module: moduleType - ? ({ - noInterop: !esModuleInterop, - type: moduleType, - strictMode, - // For NodeNext and Node12, emit as CJS but do not transform dynamic imports - ignoreDynamic: nodeModuleEmitKind === 'nodecjs', - } as swcTypes.ModuleConfig) - : undefined, - swcrc: false, - jsc: { - externalHelpers: importHelpers, - parser: { - syntax: 'typescript', - tsx: isTsx, - decorators: experimentalDecorators, - dynamicImport: true, - }, - target: swcTarget, - transform: { - decoratorMetadata: emitDecoratorMetadata, - legacyDecorator: true, - react: { - throwIfNamespace: false, - development: false, - useBuiltins: false, - pragma: jsxFactory!, - pragmaFrag: jsxFragmentFactory!, - } as swcTypes.ReactConfig, - }, - keepClassNames, - } as swcTypes.JscConfig, - }; - } + const { nonTsxOptions, tsxOptions } = createSwcOptions( + config.options, + nodeModuleEmitKind, + swcInstance, + swcDepName + ); const transpile: Transpiler['transpile'] = (input, transpileOptions) => { const { fileName } = transpileOptions; @@ -207,6 +119,155 @@ const ModuleKind = { ES2015: 5, ES2020: 6, ESNext: 99, - Node12: 100, + Node16: 100, NodeNext: 199, } as const; + +const JsxEmit = { + ReactJSX: /* ts.JsxEmit.ReactJSX */ 4, + ReactJSXDev: /* ts.JsxEmit.ReactJSXDev */ 5, +} as const; + +/** + * Prepare SWC options derived from typescript compiler options. + * @internal exported for testing + */ +export function createSwcOptions( + compilerOptions: ts.CompilerOptions, + nodeModuleEmitKind: NodeModuleEmitKind | undefined, + swcInstance: SwcInstance, + swcDepName: string +) { + const { + esModuleInterop, + sourceMap, + importHelpers, + experimentalDecorators, + emitDecoratorMetadata, + target, + module, + jsx, + jsxFactory, + jsxFragmentFactory, + strict, + alwaysStrict, + noImplicitUseStrict, + } = compilerOptions; + + let swcTarget = targetMapping.get(target!) ?? 'es3'; + // Downgrade to lower target if swc does not support the selected target. + // Perhaps project has an older version of swc. + // TODO cache the results of this; slightly faster + let swcTargetIndex = swcTargets.indexOf(swcTarget); + for (; swcTargetIndex >= 0; swcTargetIndex--) { + try { + swcInstance.transformSync('', { + jsc: { target: swcTargets[swcTargetIndex] }, + }); + break; + } catch (e) {} + } + swcTarget = swcTargets[swcTargetIndex]; + const keepClassNames = target! >= /* ts.ScriptTarget.ES2016 */ 3; + const isNodeModuleKind = + module === ModuleKind.Node16 || module === ModuleKind.NodeNext; + // swc only supports these 4x module options [MUST_UPDATE_FOR_NEW_MODULEKIND] + const moduleType = + module === ModuleKind.CommonJS + ? 'commonjs' + : module === ModuleKind.AMD + ? 'amd' + : module === ModuleKind.UMD + ? 'umd' + : isNodeModuleKind && nodeModuleEmitKind === 'nodecjs' + ? 'commonjs' + : isNodeModuleKind && nodeModuleEmitKind === 'nodeesm' + ? 'es6' + : 'es6'; + // In swc: + // strictMode means `"use strict"` is *always* emitted for non-ES module, *never* for ES module where it is assumed it can be omitted. + // (this assumption is invalid, but that's the way swc behaves) + // tsc is a bit more complex: + // alwaysStrict will force emitting it always unless `import`/`export` syntax is emitted which implies it per the JS spec. + // if not alwaysStrict, will emit implicitly whenever module target is non-ES *and* transformed module syntax is emitted. + // For node, best option is to assume that all scripts are modules (commonjs or esm) and thus should get tsc's implicit strict behavior. + + // Always set strictMode, *unless* alwaysStrict is disabled and noImplicitUseStrict is enabled + const strictMode = + // if `alwaysStrict` is disabled, remembering that `strict` defaults `alwaysStrict` to true + (alwaysStrict === false || (alwaysStrict !== true && strict !== true)) && + // if noImplicitUseStrict is enabled + noImplicitUseStrict === true + ? false + : true; + + const jsxRuntime: swcTypes.ReactConfig['runtime'] = + jsx === JsxEmit.ReactJSX || jsx === JsxEmit.ReactJSXDev + ? 'automatic' + : undefined; + const jsxDevelopment: swcTypes.ReactConfig['development'] = + jsx === JsxEmit.ReactJSXDev ? true : undefined; + + const nonTsxOptions = createVariant(false); + const tsxOptions = createVariant(true); + return { nonTsxOptions, tsxOptions }; + + function createVariant(isTsx: boolean): swcTypes.Options { + const swcOptions: swcTypes.Options = { + sourceMaps: sourceMap, + // isModule: true, + module: moduleType + ? ({ + noInterop: !esModuleInterop, + type: moduleType, + strictMode, + // For NodeNext and Node12, emit as CJS but do not transform dynamic imports + ignoreDynamic: nodeModuleEmitKind === 'nodecjs', + } as swcTypes.ModuleConfig) + : undefined, + swcrc: false, + jsc: { + externalHelpers: importHelpers, + parser: { + syntax: 'typescript', + tsx: isTsx, + decorators: experimentalDecorators, + dynamicImport: true, + importAssertions: true, + }, + target: swcTarget, + transform: { + decoratorMetadata: emitDecoratorMetadata, + legacyDecorator: true, + react: { + throwIfNamespace: false, + development: jsxDevelopment, + useBuiltins: false, + pragma: jsxFactory!, + pragmaFrag: jsxFragmentFactory!, + runtime: jsxRuntime, + } as swcTypes.ReactConfig, + }, + keepClassNames, + experimental: { + keepImportAssertions: true, + }, + } as swcTypes.JscConfig, + }; + + // Throw a helpful error if swc version is old, for example, if it rejects `ignoreDynamic` + try { + swcInstance.transformSync('', swcOptions); + } catch (e) { + throw new Error( + `${swcDepName} threw an error when attempting to validate swc compiler options.\n` + + 'You may be using an old version of swc which does not support the options used by ts-node.\n' + + 'Try upgrading to the latest version of swc.\n' + + 'Error message from swc:\n' + + (e as Error)?.message + ); + } + + return swcOptions; + } +} From ad01f49b269e47ec56c213d682a236b4c0a6d164 Mon Sep 17 00:00:00 2001 From: Andrew Bradley Date: Sat, 2 Jul 2022 16:15:06 -0400 Subject: [PATCH 07/19] Fix #1764 (#1824) --- src/repl.ts | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/repl.ts b/src/repl.ts index eed95a0d7..3137daa49 100644 --- a/src/repl.ts +++ b/src/repl.ts @@ -207,6 +207,7 @@ export function createRepl(options: CreateReplOptions = {}) { state, input: code, context, + overrideIsCompletion: false, }); assert(result.containsTopLevelAwait === false); return result.value; @@ -512,6 +513,12 @@ function appendCompileAndEvalInput(options: { /** Enable top-level await but only if the TSNode service allows it. */ enableTopLevelAwait?: boolean; context: Context | undefined; + /** + * Added so that `evalCode` can be guaranteed *not* to trigger the `isCompletion` + * codepath. However, the `isCompletion` logic is ancient and maybe should be removed entirely. + * Nobody's looked at it in a long time. + */ + overrideIsCompletion?: boolean; }): AppendCompileAndEvalInputResult { const { service, @@ -519,6 +526,7 @@ function appendCompileAndEvalInput(options: { wrappedErr, enableTopLevelAwait = false, context, + overrideIsCompletion, } = options; let { input } = options; @@ -533,7 +541,7 @@ function appendCompileAndEvalInput(options: { } const lines = state.lines; - const isCompletion = !/\n$/.test(input); + const isCompletion = overrideIsCompletion ?? !/\n$/.test(input); const undo = appendToEvalState(state, input); let output: string; From aa5ec36526bf817b09345449492d5b9da11c0b93 Mon Sep 17 00:00:00 2001 From: Andrew Bradley Date: Sun, 10 Jul 2022 10:16:13 -0400 Subject: [PATCH 08/19] Make --project accept path to directory containing tsconfig, not just path to tsconfig (#1830) * fix #1829 * fix * fix --- src/configuration.ts | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/configuration.ts b/src/configuration.ts index 266f2d920..4ab0a7ccf 100644 --- a/src/configuration.ts +++ b/src/configuration.ts @@ -1,4 +1,4 @@ -import { resolve, dirname } from 'path'; +import { resolve, dirname, join } from 'path'; import type * as _ts from 'typescript'; import { CreateOptions, @@ -167,9 +167,13 @@ export function readConfig( // Read project configuration when available. if (!skipProject) { - configFilePath = project - ? resolve(cwd, project) - : ts.findConfigFile(projectSearchDir, fileExists); + if (project) { + const resolved = resolve(cwd, project); + const nested = join(resolved, 'tsconfig.json'); + configFilePath = fileExists(nested) ? nested : resolved; + } else { + configFilePath = ts.findConfigFile(projectSearchDir, fileExists); + } if (configFilePath) { let pathToNextConfigInChain = configFilePath; From 86b63bfde7b6ed234f55420d4d20c1e691075e53 Mon Sep 17 00:00:00 2001 From: Andrew Bradley Date: Tue, 12 Jul 2022 21:32:36 -0400 Subject: [PATCH 09/19] try adding config to ignore formatting commits in blame view (#1835) --- .git-blame-ignore-revs | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 .git-blame-ignore-revs diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs new file mode 100644 index 000000000..b249da6d7 --- /dev/null +++ b/.git-blame-ignore-revs @@ -0,0 +1,3 @@ +# Prettier formatting +9d05cb684fc3a6e492832100a125ea07d1cc98c5 + From 32d07e2b2fcbaab97c11e71ee5fc3a79fc20c802 Mon Sep 17 00:00:00 2001 From: Paul Gschwendtner Date: Wed, 13 Jul 2022 21:36:36 +0200 Subject: [PATCH 10/19] Fix ESM node processes being unable to fork into other scripts (#1814) * Fix ESM node processes being unable to fork into other scripts Currently, Node processes instantiated through the `--esm` flag result in a child process being created so that the ESM loader can be registered. This works fine and is reasonable. The child process approach to register ESM hooks currently prevents the NodeJS `fork` method from being used because the `execArgv` propagated into forked processes causes `ts-node` (which is also propagated as child exec script -- this is good because it allows nested type resolution to work) to always execute the original entry-point, causing potential infinite loops because the designated fork module script is not executed as expected. This commit fixes this by not encoding the entry-point information into the state that is captured as part of the `execArgv`. Instead the entry-point information is always retrieved from the parsed rest command line arguments in the final stage (`phase4`). Fixes #1812. * Fix `--cwd` to actually set the working directory and work with ESM child process Currently the `--esm` option does not necessarily do what the documentation suggests. i.e. the script does not run as if the working directory is the specified directory. This commit fixes this, so that the option is useful for TSConfig resolution, as well as for controlling the script working directory. Also fixes that the CWD encoded in the bootstrap brotli state for the ESM child process messes with the entry-point resolution, if e.g. the entry-point in `child_process.fork` is relative to a specified `cwd`. * changes based on review * lint-fix * enable transpileOnly in new tests for performance * Tweak basic working dir tests to verify that --cwd affects entrypoint resolution but not process.cwd() * update forking tests: disable non --esm test with comment about known bug and link to tickets make tests set cwd for fork() call, to be sure it is respected and not overridden by --cwd * use swc compiler to avoid issue with ancient TS versions not understanding import.meta.url syntax * Remove tests that I think are redundant (but I've asked for confirmation in code review) * fix another issue with old TS * final review updates Co-authored-by: Andrew Bradley --- src/bin.ts | 131 +++++++++++++----- src/child/argv-payload.ts | 18 +++ src/child/child-entrypoint.ts | 28 ++-- src/child/spawn-child.ts | 15 +- src/test/esm-loader.spec.ts | 48 +++++++ src/test/index.spec.ts | 27 ++++ .../process-forking-js/index.ts | 24 ++++ .../process-forking-js/package.json | 3 + .../process-forking-js/tsconfig.json | 8 ++ .../process-forking-js/worker.js | 1 + .../process-forking-ts-abs/index.ts | 26 ++++ .../process-forking-ts-abs/package.json | 3 + .../subfolder/worker.ts | 3 + .../process-forking-ts-abs/tsconfig.json | 8 ++ .../process-forking-ts/index.ts | 24 ++++ .../process-forking-ts/package.json | 3 + .../process-forking-ts/subfolder/worker.ts | 3 + .../process-forking-ts/tsconfig.json | 8 ++ tests/working-dir/cjs/index.ts | 7 + tests/working-dir/esm/index.ts | 11 ++ tests/working-dir/esm/package.json | 3 + tests/working-dir/esm/tsconfig.json | 8 ++ tests/working-dir/forking/index.ts | 22 +++ tests/working-dir/forking/subfolder/worker.ts | 3 + tests/working-dir/tsconfig.json | 6 + 25 files changed, 390 insertions(+), 51 deletions(-) create mode 100644 src/child/argv-payload.ts create mode 100644 tests/esm-child-process/process-forking-js/index.ts create mode 100644 tests/esm-child-process/process-forking-js/package.json create mode 100644 tests/esm-child-process/process-forking-js/tsconfig.json create mode 100644 tests/esm-child-process/process-forking-js/worker.js create mode 100644 tests/esm-child-process/process-forking-ts-abs/index.ts create mode 100644 tests/esm-child-process/process-forking-ts-abs/package.json create mode 100644 tests/esm-child-process/process-forking-ts-abs/subfolder/worker.ts create mode 100644 tests/esm-child-process/process-forking-ts-abs/tsconfig.json create mode 100644 tests/esm-child-process/process-forking-ts/index.ts create mode 100644 tests/esm-child-process/process-forking-ts/package.json create mode 100644 tests/esm-child-process/process-forking-ts/subfolder/worker.ts create mode 100644 tests/esm-child-process/process-forking-ts/tsconfig.json create mode 100644 tests/working-dir/cjs/index.ts create mode 100644 tests/working-dir/esm/index.ts create mode 100644 tests/working-dir/esm/package.json create mode 100644 tests/working-dir/esm/tsconfig.json create mode 100644 tests/working-dir/forking/index.ts create mode 100644 tests/working-dir/forking/subfolder/worker.ts create mode 100644 tests/working-dir/tsconfig.json diff --git a/src/bin.ts b/src/bin.ts index 8b5f91767..fb3208c48 100644 --- a/src/bin.ts +++ b/src/bin.ts @@ -48,7 +48,8 @@ export function main( const state: BootstrapState = { shouldUseChildProcess: false, isInChildProcess: false, - entrypoint: __filename, + isCli: true, + tsNodeScript: __filename, parseArgvResult: args, }; return bootstrap(state); @@ -62,7 +63,12 @@ export function main( export interface BootstrapState { isInChildProcess: boolean; shouldUseChildProcess: boolean; - entrypoint: string; + /** + * True if bootstrapping the ts-node CLI process or the direct child necessitated by `--esm`. + * false if bootstrapping a subsequently `fork()`ed child. + */ + isCli: boolean; + tsNodeScript: string; parseArgvResult: ReturnType; phase2Result?: ReturnType; phase3Result?: ReturnType; @@ -73,12 +79,16 @@ export function bootstrap(state: BootstrapState) { if (!state.phase2Result) { state.phase2Result = phase2(state); if (state.shouldUseChildProcess && !state.isInChildProcess) { + // Note: When transitioning into the child-process after `phase2`, + // the updated working directory needs to be preserved. return callInChild(state); } } if (!state.phase3Result) { state.phase3Result = phase3(state); if (state.shouldUseChildProcess && !state.isInChildProcess) { + // Note: When transitioning into the child-process after `phase2`, + // the updated working directory needs to be preserved. return callInChild(state); } } @@ -264,8 +274,7 @@ function parseArgv(argv: string[], entrypointArgs: Record) { } function phase2(payload: BootstrapState) { - const { help, version, code, interactive, cwdArg, restArgs, esm } = - payload.parseArgvResult; + const { help, version, cwdArg, esm } = payload.parseArgvResult; if (help) { console.log(` @@ -319,28 +328,14 @@ Options: process.exit(0); } - // Figure out which we are executing: piped stdin, --eval, REPL, and/or entrypoint - // This is complicated because node's behavior is complicated - // `node -e code -i ./script.js` ignores -e - const executeEval = code != null && !(interactive && restArgs.length); - const executeEntrypoint = !executeEval && restArgs.length > 0; - const executeRepl = - !executeEntrypoint && - (interactive || (process.stdin.isTTY && !executeEval)); - const executeStdin = !executeEval && !executeRepl && !executeEntrypoint; - - const cwd = cwdArg || process.cwd(); - /** Unresolved. May point to a symlink, not realpath. May be missing file extension */ - const scriptPath = executeEntrypoint ? resolve(cwd, restArgs[0]) : undefined; + const cwd = cwdArg ? resolve(cwdArg) : process.cwd(); + // If ESM is explicitly enabled through the flag, stage3 should be run in a child process + // with the ESM loaders configured. if (esm) payload.shouldUseChildProcess = true; + return { - executeEval, - executeEntrypoint, - executeRepl, - executeStdin, cwd, - scriptPath, }; } @@ -372,7 +367,15 @@ function phase3(payload: BootstrapState) { esm, experimentalSpecifierResolution, } = payload.parseArgvResult; - const { cwd, scriptPath } = payload.phase2Result!; + const { cwd } = payload.phase2Result!; + + // NOTE: When we transition to a child process for ESM, the entry-point script determined + // here might not be the one used later in `phase4`. This can happen when we execute the + // original entry-point but then the process forks itself using e.g. `child_process.fork`. + // We will always use the original TS project in forked processes anyway, so it is + // expected and acceptable to retrieve the entry-point information here in `phase2`. + // See: https://github.com/TypeStrong/ts-node/issues/1812. + const { entryPointPath } = getEntryPointInfo(payload); const preloadedConfig = findAndReadConfig({ cwd, @@ -387,7 +390,12 @@ function phase3(payload: BootstrapState) { compilerHost, ignore, logError, - projectSearchDir: getProjectSearchDir(cwd, scriptMode, cwdMode, scriptPath), + projectSearchDir: getProjectSearchDir( + cwd, + scriptMode, + cwdMode, + entryPointPath + ), project, skipProject, skipIgnore, @@ -403,23 +411,77 @@ function phase3(payload: BootstrapState) { experimentalSpecifierResolution as ExperimentalSpecifierResolution, }); + // If ESM is enabled through the parsed tsconfig, stage4 should be run in a child + // process with the ESM loaders configured. if (preloadedConfig.options.esm) payload.shouldUseChildProcess = true; + return { preloadedConfig }; } +/** + * Determines the entry-point information from the argv and phase2 result. This + * method will be invoked in two places: + * + * 1. In phase 3 to be able to find a project from the potential entry-point script. + * 2. In phase 4 to determine the actual entry-point script. + * + * Note that we need to explicitly re-resolve the entry-point information in the final + * stage because the previous stage information could be modified when the bootstrap + * invocation transitioned into a child process for ESM. + * + * Stages before (phase 4) can and will be cached by the child process through the Brotli + * configuration and entry-point information is only reliable in the final phase. More + * details can be found in here: https://github.com/TypeStrong/ts-node/issues/1812. + */ +function getEntryPointInfo(state: BootstrapState) { + const { code, interactive, restArgs } = state.parseArgvResult!; + const { cwd } = state.phase2Result!; + const { isCli } = state; + + // Figure out which we are executing: piped stdin, --eval, REPL, and/or entrypoint + // This is complicated because node's behavior is complicated + // `node -e code -i ./script.js` ignores -e + const executeEval = code != null && !(interactive && restArgs.length); + const executeEntrypoint = !executeEval && restArgs.length > 0; + const executeRepl = + !executeEntrypoint && + (interactive || (process.stdin.isTTY && !executeEval)); + const executeStdin = !executeEval && !executeRepl && !executeEntrypoint; + + /** + * Unresolved. May point to a symlink, not realpath. May be missing file extension + * NOTE: resolution relative to cwd option (not `process.cwd()`) is legacy backwards-compat; should be changed in next major: https://github.com/TypeStrong/ts-node/issues/1834 + */ + const entryPointPath = executeEntrypoint + ? isCli + ? resolve(cwd, restArgs[0]) + : resolve(restArgs[0]) + : undefined; + + return { + executeEval, + executeEntrypoint, + executeRepl, + executeStdin, + entryPointPath, + }; +} + function phase4(payload: BootstrapState) { - const { isInChildProcess, entrypoint } = payload; + const { isInChildProcess, tsNodeScript } = payload; const { version, showConfig, restArgs, code, print, argv } = payload.parseArgvResult; + const { cwd } = payload.phase2Result!; + const { preloadedConfig } = payload.phase3Result!; + const { + entryPointPath, + executeEntrypoint, executeEval, - cwd, - executeStdin, executeRepl, - executeEntrypoint, - scriptPath, - } = payload.phase2Result!; - const { preloadedConfig } = payload.phase3Result!; + executeStdin, + } = getEntryPointInfo(payload); + /** * , [stdin], and [eval] are all essentially virtual files that do not exist on disc and are backed by a REPL * service to handle eval-ing of code. @@ -566,12 +628,13 @@ function phase4(payload: BootstrapState) { // Prepend `ts-node` arguments to CLI for child processes. process.execArgv.push( - entrypoint, + tsNodeScript, ...argv.slice(2, argv.length - restArgs.length) ); - // TODO this comes from BoostrapState + + // TODO this comes from BootstrapState process.argv = [process.argv[1]] - .concat(executeEntrypoint ? ([scriptPath] as string[]) : []) + .concat(executeEntrypoint ? ([entryPointPath] as string[]) : []) .concat(restArgs.slice(executeEntrypoint ? 1 : 0)); // Execute the main contents (either eval, script or piped). diff --git a/src/child/argv-payload.ts b/src/child/argv-payload.ts new file mode 100644 index 000000000..abe6da9db --- /dev/null +++ b/src/child/argv-payload.ts @@ -0,0 +1,18 @@ +import { brotliCompressSync, brotliDecompressSync, constants } from 'zlib'; + +/** @internal */ +export const argPrefix = '--brotli-base64-config='; + +/** @internal */ +export function compress(object: any) { + return brotliCompressSync(Buffer.from(JSON.stringify(object), 'utf8'), { + [constants.BROTLI_PARAM_QUALITY]: constants.BROTLI_MIN_QUALITY, + }).toString('base64'); +} + +/** @internal */ +export function decompress(str: string) { + return JSON.parse( + brotliDecompressSync(Buffer.from(str, 'base64')).toString() + ); +} diff --git a/src/child/child-entrypoint.ts b/src/child/child-entrypoint.ts index 03a02d2e9..0550170bf 100644 --- a/src/child/child-entrypoint.ts +++ b/src/child/child-entrypoint.ts @@ -1,16 +1,24 @@ import { BootstrapState, bootstrap } from '../bin'; -import { brotliDecompressSync } from 'zlib'; +import { argPrefix, compress, decompress } from './argv-payload'; const base64ConfigArg = process.argv[2]; -const argPrefix = '--brotli-base64-config='; if (!base64ConfigArg.startsWith(argPrefix)) throw new Error('unexpected argv'); const base64Payload = base64ConfigArg.slice(argPrefix.length); -const payload = JSON.parse( - brotliDecompressSync(Buffer.from(base64Payload, 'base64')).toString() -) as BootstrapState; -payload.isInChildProcess = true; -payload.entrypoint = __filename; -payload.parseArgvResult.argv = process.argv; -payload.parseArgvResult.restArgs = process.argv.slice(3); +const state = decompress(base64Payload) as BootstrapState; -bootstrap(payload); +state.isInChildProcess = true; +state.tsNodeScript = __filename; +state.parseArgvResult.argv = process.argv; +state.parseArgvResult.restArgs = process.argv.slice(3); + +// Modify and re-compress the payload delivered to subsequent child processes. +// This logic may be refactored into bin.ts by https://github.com/TypeStrong/ts-node/issues/1831 +if (state.isCli) { + const stateForChildren: BootstrapState = { + ...state, + isCli: false, + }; + state.parseArgvResult.argv[2] = `${argPrefix}${compress(stateForChildren)}`; +} + +bootstrap(state); diff --git a/src/child/spawn-child.ts b/src/child/spawn-child.ts index 12368fcef..618b8190a 100644 --- a/src/child/spawn-child.ts +++ b/src/child/spawn-child.ts @@ -1,12 +1,15 @@ import type { BootstrapState } from '../bin'; import { spawn } from 'child_process'; -import { brotliCompressSync } from 'zlib'; import { pathToFileURL } from 'url'; import { versionGteLt } from '../util'; +import { argPrefix, compress } from './argv-payload'; -const argPrefix = '--brotli-base64-config='; - -/** @internal */ +/** + * @internal + * @param state Bootstrap state to be transferred into the child process. + * @param targetCwd Working directory to be preserved when transitioning to + * the child process. + */ export function callInChild(state: BootstrapState) { if (!versionGteLt(process.versions.node, '12.17.0')) { throw new Error( @@ -22,9 +25,7 @@ export function callInChild(state: BootstrapState) { // Node on Windows doesn't like `c:\` absolute paths here; must be `file:///c:/` pathToFileURL(require.resolve('../../child-loader.mjs')).toString(), require.resolve('./child-entrypoint.js'), - `${argPrefix}${brotliCompressSync( - Buffer.from(JSON.stringify(state), 'utf8') - ).toString('base64')}`, + `${argPrefix}${compress(state)}`, ...state.parseArgvResult.restArgs, ], { diff --git a/src/test/esm-loader.spec.ts b/src/test/esm-loader.spec.ts index 41c421fd6..375012a76 100644 --- a/src/test/esm-loader.spec.ts +++ b/src/test/esm-loader.spec.ts @@ -22,6 +22,7 @@ import { TEST_DIR, tsSupportsImportAssertions, tsSupportsResolveJsonModule, + tsSupportsStableNodeNextNode16, } from './helpers'; import { createExec, createSpawn, ExecReturn } from './exec-helpers'; import { join, resolve } from 'path'; @@ -358,6 +359,53 @@ 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'), + } + ); + + expect(err).toBe(null); + expect(stdout.trim()).toBe('Passing'); + expect(stderr).toBe(''); + }); + }); + + test.suite('esm child process and forking', (test) => { + test('should be able to fork vanilla NodeJS script', async () => { + const { err, stdout, stderr } = await exec( + `${BIN_PATH} --esm --cwd ./esm-child-process/ ./process-forking-js/index.ts` + ); + + expect(err).toBe(null); + expect(stdout.trim()).toBe('Passing: from main'); + expect(stderr).toBe(''); + }); + + test('should be able to fork TypeScript script', async () => { + const { err, stdout, stderr } = await exec( + `${BIN_PATH} --esm --cwd ./esm-child-process/ ./process-forking-ts/index.ts` + ); + + expect(err).toBe(null); + expect(stdout.trim()).toBe('Passing: from main'); + expect(stderr).toBe(''); + }); + + test('should be able to fork TypeScript script by absolute path', async () => { + const { err, stdout, stderr } = await exec( + `${BIN_PATH} --esm --cwd ./esm-child-process/ ./process-forking-ts-abs/index.ts` + ); + + expect(err).toBe(null); + expect(stdout.trim()).toBe('Passing: from main'); + expect(stderr).toBe(''); + }); + }); + test.suite('parent passes signals to child', (test) => { test.runSerially(); diff --git a/src/test/index.spec.ts b/src/test/index.spec.ts index ca4c2cf85..f085a3639 100644 --- a/src/test/index.spec.ts +++ b/src/test/index.spec.ts @@ -617,6 +617,33 @@ 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'), + } + ); + + expect(err).toBe(null); + expect(stdout.trim()).toBe('Passing'); + expect(stderr).toBe(''); + }); + + // Disabled due to bug: + // --cwd is passed to forked children when not using --esm, erroneously affects their entrypoint resolution. + // 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` + ); + + expect(err).toBe(null); + expect(stdout.trim()).toBe('Passing: from main'); + expect(stderr).toBe(''); + }); + test.suite('should read ts-node options from tsconfig.json', (test) => { const BIN_EXEC = `"${BIN_PATH}" --project tsconfig-options/tsconfig.json`; diff --git a/tests/esm-child-process/process-forking-js/index.ts b/tests/esm-child-process/process-forking-js/index.ts new file mode 100644 index 000000000..88a3bd61a --- /dev/null +++ b/tests/esm-child-process/process-forking-js/index.ts @@ -0,0 +1,24 @@ +import { fork } from 'child_process'; +import { dirname } from 'path'; +import { fileURLToPath } from 'url'; + +// Initially set the exit code to non-zero. We only set it to `0` when the +// worker process finishes properly with the expected stdout message. +process.exitCode = 1; + +process.chdir(dirname(fileURLToPath(import.meta.url))); + +const workerProcess = fork('./worker.js', [], { + stdio: 'pipe', +}); + +let stdout = ''; + +workerProcess.stdout.on('data', (chunk) => (stdout += chunk.toString('utf8'))); +workerProcess.on('error', () => (process.exitCode = 1)); +workerProcess.on('close', (status, signal) => { + if (status === 0 && signal === null && stdout.trim() === 'Works') { + console.log('Passing: from main'); + process.exitCode = 0; + } +}); diff --git a/tests/esm-child-process/process-forking-js/package.json b/tests/esm-child-process/process-forking-js/package.json new file mode 100644 index 000000000..3dbc1ca59 --- /dev/null +++ b/tests/esm-child-process/process-forking-js/package.json @@ -0,0 +1,3 @@ +{ + "type": "module" +} diff --git a/tests/esm-child-process/process-forking-js/tsconfig.json b/tests/esm-child-process/process-forking-js/tsconfig.json new file mode 100644 index 000000000..04e93e5c7 --- /dev/null +++ b/tests/esm-child-process/process-forking-js/tsconfig.json @@ -0,0 +1,8 @@ +{ + "compilerOptions": { + "module": "ESNext" + }, + "ts-node": { + "swc": true + } +} diff --git a/tests/esm-child-process/process-forking-js/worker.js b/tests/esm-child-process/process-forking-js/worker.js new file mode 100644 index 000000000..820d10b2e --- /dev/null +++ b/tests/esm-child-process/process-forking-js/worker.js @@ -0,0 +1 @@ +console.log('Works'); diff --git a/tests/esm-child-process/process-forking-ts-abs/index.ts b/tests/esm-child-process/process-forking-ts-abs/index.ts new file mode 100644 index 000000000..ec94e846d --- /dev/null +++ b/tests/esm-child-process/process-forking-ts-abs/index.ts @@ -0,0 +1,26 @@ +import { fork } from 'child_process'; +import { dirname, join } from 'path'; +import { fileURLToPath } from 'url'; + +// Initially set the exit code to non-zero. We only set it to `0` when the +// worker process finishes properly with the expected stdout message. +process.exitCode = 1; + +const workerProcess = fork( + join(dirname(fileURLToPath(import.meta.url)), 'subfolder/worker.ts'), + [], + { + stdio: 'pipe', + } +); + +let stdout = ''; + +workerProcess.stdout.on('data', (chunk) => (stdout += chunk.toString('utf8'))); +workerProcess.on('error', () => (process.exitCode = 1)); +workerProcess.on('close', (status, signal) => { + if (status === 0 && signal === null && stdout.trim() === 'Works') { + console.log('Passing: from main'); + process.exitCode = 0; + } +}); diff --git a/tests/esm-child-process/process-forking-ts-abs/package.json b/tests/esm-child-process/process-forking-ts-abs/package.json new file mode 100644 index 000000000..3dbc1ca59 --- /dev/null +++ b/tests/esm-child-process/process-forking-ts-abs/package.json @@ -0,0 +1,3 @@ +{ + "type": "module" +} diff --git a/tests/esm-child-process/process-forking-ts-abs/subfolder/worker.ts b/tests/esm-child-process/process-forking-ts-abs/subfolder/worker.ts new file mode 100644 index 000000000..4114d5ab0 --- /dev/null +++ b/tests/esm-child-process/process-forking-ts-abs/subfolder/worker.ts @@ -0,0 +1,3 @@ +const message: string = 'Works'; + +console.log(message); diff --git a/tests/esm-child-process/process-forking-ts-abs/tsconfig.json b/tests/esm-child-process/process-forking-ts-abs/tsconfig.json new file mode 100644 index 000000000..04e93e5c7 --- /dev/null +++ b/tests/esm-child-process/process-forking-ts-abs/tsconfig.json @@ -0,0 +1,8 @@ +{ + "compilerOptions": { + "module": "ESNext" + }, + "ts-node": { + "swc": true + } +} diff --git a/tests/esm-child-process/process-forking-ts/index.ts b/tests/esm-child-process/process-forking-ts/index.ts new file mode 100644 index 000000000..2d59e0aab --- /dev/null +++ b/tests/esm-child-process/process-forking-ts/index.ts @@ -0,0 +1,24 @@ +import { fork } from 'child_process'; +import { dirname, join } from 'path'; +import { fileURLToPath } from 'url'; + +// Initially set the exit code to non-zero. We only set it to `0` when the +// worker process finishes properly with the expected stdout message. +process.exitCode = 1; + +process.chdir(join(dirname(fileURLToPath(import.meta.url)), 'subfolder')); + +const workerProcess = fork('./worker.ts', [], { + stdio: 'pipe', +}); + +let stdout = ''; + +workerProcess.stdout.on('data', (chunk) => (stdout += chunk.toString('utf8'))); +workerProcess.on('error', () => (process.exitCode = 1)); +workerProcess.on('close', (status, signal) => { + if (status === 0 && signal === null && stdout.trim() === 'Works') { + console.log('Passing: from main'); + process.exitCode = 0; + } +}); diff --git a/tests/esm-child-process/process-forking-ts/package.json b/tests/esm-child-process/process-forking-ts/package.json new file mode 100644 index 000000000..3dbc1ca59 --- /dev/null +++ b/tests/esm-child-process/process-forking-ts/package.json @@ -0,0 +1,3 @@ +{ + "type": "module" +} diff --git a/tests/esm-child-process/process-forking-ts/subfolder/worker.ts b/tests/esm-child-process/process-forking-ts/subfolder/worker.ts new file mode 100644 index 000000000..4114d5ab0 --- /dev/null +++ b/tests/esm-child-process/process-forking-ts/subfolder/worker.ts @@ -0,0 +1,3 @@ +const message: string = 'Works'; + +console.log(message); diff --git a/tests/esm-child-process/process-forking-ts/tsconfig.json b/tests/esm-child-process/process-forking-ts/tsconfig.json new file mode 100644 index 000000000..04e93e5c7 --- /dev/null +++ b/tests/esm-child-process/process-forking-ts/tsconfig.json @@ -0,0 +1,8 @@ +{ + "compilerOptions": { + "module": "ESNext" + }, + "ts-node": { + "swc": true + } +} diff --git a/tests/working-dir/cjs/index.ts b/tests/working-dir/cjs/index.ts new file mode 100644 index 000000000..f3ba1b30a --- /dev/null +++ b/tests/working-dir/cjs/index.ts @@ -0,0 +1,7 @@ +import { strictEqual } from 'assert'; +import { normalize, dirname } from 'path'; + +// Expect the working directory to be the parent directory. +strictEqual(normalize(process.cwd()), normalize(dirname(__dirname))); + +console.log('Passing'); diff --git a/tests/working-dir/esm/index.ts b/tests/working-dir/esm/index.ts new file mode 100644 index 000000000..21230f9d8 --- /dev/null +++ b/tests/working-dir/esm/index.ts @@ -0,0 +1,11 @@ +import { strictEqual } from 'assert'; +import { normalize, dirname } from 'path'; +import { fileURLToPath } from 'url'; + +// Expect the working directory to be the parent directory. +strictEqual( + normalize(process.cwd()), + normalize(dirname(dirname(fileURLToPath(import.meta.url)))) +); + +console.log('Passing'); diff --git a/tests/working-dir/esm/package.json b/tests/working-dir/esm/package.json new file mode 100644 index 000000000..3dbc1ca59 --- /dev/null +++ b/tests/working-dir/esm/package.json @@ -0,0 +1,3 @@ +{ + "type": "module" +} diff --git a/tests/working-dir/esm/tsconfig.json b/tests/working-dir/esm/tsconfig.json new file mode 100644 index 000000000..04e93e5c7 --- /dev/null +++ b/tests/working-dir/esm/tsconfig.json @@ -0,0 +1,8 @@ +{ + "compilerOptions": { + "module": "ESNext" + }, + "ts-node": { + "swc": true + } +} diff --git a/tests/working-dir/forking/index.ts b/tests/working-dir/forking/index.ts new file mode 100644 index 000000000..45ff8afd7 --- /dev/null +++ b/tests/working-dir/forking/index.ts @@ -0,0 +1,22 @@ +import { fork } from 'child_process'; +import { join } from 'path'; + +// Initially set the exit code to non-zero. We only set it to `0` when the +// worker process finishes properly with the expected stdout message. +process.exitCode = 1; + +const workerProcess = fork('./worker.ts', [], { + stdio: 'pipe', + cwd: join(__dirname, 'subfolder'), +}); + +let stdout = ''; + +workerProcess.stdout!.on('data', (chunk) => (stdout += chunk.toString('utf8'))); +workerProcess.on('error', () => (process.exitCode = 1)); +workerProcess.on('close', (status, signal) => { + if (status === 0 && signal === null && stdout.trim() === 'Works') { + console.log('Passing: from main'); + process.exitCode = 0; + } +}); diff --git a/tests/working-dir/forking/subfolder/worker.ts b/tests/working-dir/forking/subfolder/worker.ts new file mode 100644 index 000000000..4114d5ab0 --- /dev/null +++ b/tests/working-dir/forking/subfolder/worker.ts @@ -0,0 +1,3 @@ +const message: string = 'Works'; + +console.log(message); diff --git a/tests/working-dir/tsconfig.json b/tests/working-dir/tsconfig.json new file mode 100644 index 000000000..484405d0e --- /dev/null +++ b/tests/working-dir/tsconfig.json @@ -0,0 +1,6 @@ +{ + "extends": "../tsconfig.json", + "ts-node": { + "transpileOnly": true + } +} From 3333005f43ef32c13eedc6d4deec4db9b7b4501a Mon Sep 17 00:00:00 2001 From: Andrew Bradley Date: Wed, 13 Jul 2022 15:46:07 -0400 Subject: [PATCH 11/19] Update performance.md (#1837) --- website/docs/performance.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/website/docs/performance.md b/website/docs/performance.md index 21704761e..a54979a84 100644 --- a/website/docs/performance.md +++ b/website/docs/performance.md @@ -6,7 +6,9 @@ These tricks will make ts-node faster. ## Skip typechecking -It is often better to use `tsc --noEmit` to typecheck as part of your tests or linting. In these cases, ts-node can skip typechecking. +It is often better to typecheck as part of your tests or linting. You can use `tsc --noEmit` to do this. In these cases, ts-node can skip typechecking making it much faster. + +To skip typechecking in ts-node, do one of the following: * Enable [swc](./swc.md) * This is by far the fastest option @@ -14,6 +16,8 @@ It is often better to use `tsc --noEmit` to typecheck as part of your tests or l ## With typechecking +If you absolutely must use ts-node for typechecking: + * Avoid dynamic `require()` which may trigger repeated typechecking; prefer `import` * Try with and without `--files`; one may be faster depending on your project * Check `tsc --showConfig`; make sure all executed files are included From 604b2aa7bba341351ac43c8d042a4843632ea8bf Mon Sep 17 00:00:00 2001 From: Andrew Bradley Date: Wed, 13 Jul 2022 15:51:31 -0400 Subject: [PATCH 12/19] docs tweak ugh --- website/docs/performance.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/website/docs/performance.md b/website/docs/performance.md index a54979a84..e5940e6aa 100644 --- a/website/docs/performance.md +++ b/website/docs/performance.md @@ -6,7 +6,7 @@ These tricks will make ts-node faster. ## Skip typechecking -It is often better to typecheck as part of your tests or linting. You can use `tsc --noEmit` to do this. In these cases, ts-node can skip typechecking making it much faster. +It is often better to typecheck as part of your tests or linting. You can run `tsc --noEmit` to do this. In these cases, ts-node can skip typechecking, making it much faster. To skip typechecking in ts-node, do one of the following: @@ -16,7 +16,7 @@ To skip typechecking in ts-node, do one of the following: ## With typechecking -If you absolutely must use ts-node for typechecking: +If you absolutely must typecheck in ts-node: * Avoid dynamic `require()` which may trigger repeated typechecking; prefer `import` * Try with and without `--files`; one may be faster depending on your project From ee3e37ad52f7a87ebeb3e40ca19bdb8b1548ec6f Mon Sep 17 00:00:00 2001 From: Andrew Bradley Date: Wed, 13 Jul 2022 15:54:13 -0400 Subject: [PATCH 13/19] rebuild readme --- README.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index a08d36481..63625d5ee 100644 --- a/README.md +++ b/README.md @@ -1057,7 +1057,9 @@ These tricks will make ts-node faster. ## Skip typechecking -It is often better to use `tsc --noEmit` to typecheck as part of your tests or linting. In these cases, ts-node can skip typechecking. +It is often better to typecheck as part of your tests or linting. You can run `tsc --noEmit` to do this. In these cases, ts-node can skip typechecking, making it much faster. + +To skip typechecking in ts-node, do one of the following: * Enable [swc](#swc) * This is by far the fastest option @@ -1065,6 +1067,8 @@ It is often better to use `tsc --noEmit` to typecheck as part of your tests or l ## With typechecking +If you absolutely must typecheck in ts-node: + * Avoid dynamic `require()` which may trigger repeated typechecking; prefer `import` * Try with and without `--files`; one may be faster depending on your project * Check `tsc --showConfig`; make sure all executed files are included From b30ad7e9a1527c673ec2426fe02477c4c85eb036 Mon Sep 17 00:00:00 2001 From: Andrew Bradley Date: Wed, 13 Jul 2022 15:55:38 -0400 Subject: [PATCH 14/19] update api report --- api-extractor/ts-node.api.md | 1 + 1 file changed, 1 insertion(+) diff --git a/api-extractor/ts-node.api.md b/api-extractor/ts-node.api.md index 9e16ce1d0..6d42f4a42 100644 --- a/api-extractor/ts-node.api.md +++ b/api-extractor/ts-node.api.md @@ -29,6 +29,7 @@ export interface CreateOptions { esm?: boolean; experimentalReplAwait?: boolean; experimentalSpecifierResolution?: 'node' | 'explicit'; + experimentalTsImportSpecifiers?: boolean; // (undocumented) fileExists?: (path: string) => boolean; files?: boolean; From 7e41cb7c312d159c518d0ab37eee581d54c792d3 Mon Sep 17 00:00:00 2001 From: Andrew Bradley Date: Wed, 13 Jul 2022 16:31:19 -0400 Subject: [PATCH 15/19] fix jsdoc typo --- src/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/index.ts b/src/index.ts index 7167dbe1b..cf9d7eef8 100644 --- a/src/index.ts +++ b/src/index.ts @@ -376,7 +376,7 @@ export interface CreateOptions { /** * Allow using voluntary `.ts` file extension in import specifiers. * - * Typically, in ESM projects, import specifiers must hanve an emit extension, `.js`, `.cjs`, or `.mjs`, + * Typically, in ESM projects, import specifiers must have an emit extension, `.js`, `.cjs`, or `.mjs`, * and we automatically map to the corresponding `.ts`, `.cts`, or `.mts` source file. This is the * recommended approach. * From 11424e06ac360c4aea26f94e1bacaa3b74b7e57f Mon Sep 17 00:00:00 2001 From: Andrew Bradley Date: Wed, 13 Jul 2022 19:33:45 -0400 Subject: [PATCH 16/19] 10.9.0 --- package-lock.json | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index f1f3cb36a..90ef1939b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "ts-node", - "version": "10.8.2", + "version": "10.9.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 0d7958f04..a6842cf72 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ts-node", - "version": "10.8.2", + "version": "10.9.0", "description": "TypeScript execution environment and REPL for node.js, with source map support", "main": "dist/index.js", "exports": { From c02af136b062574081d355a768ceddd3b0bb1903 Mon Sep 17 00:00:00 2001 From: Andrew Bradley Date: Wed, 13 Jul 2022 21:45:45 -0400 Subject: [PATCH 17/19] Attempt workaround for node regression (#1838) * attempt workaround for node regression * lint-fix * fix --- dist-raw/runmain-hack.js | 9 +++++++++ src/bin.ts | 10 +++++++++- 2 files changed, 18 insertions(+), 1 deletion(-) create mode 100644 dist-raw/runmain-hack.js diff --git a/dist-raw/runmain-hack.js b/dist-raw/runmain-hack.js new file mode 100644 index 000000000..6c0688e14 --- /dev/null +++ b/dist-raw/runmain-hack.js @@ -0,0 +1,9 @@ +const {pathToFileURL} = require('url'); + +// Hack to avoid Module.runMain on node 18.6.0 +// Keeping it simple for now, isolated in this file. +// Could theoretically probe `getFormat` impl to determine if `import()` or `Module._load()` is best +// Note that I attempted a try-catch around `Module._load`, but it poisons some sort of cache such that subsequent `import()` is impossible. +exports.run = function(entryPointPath) { + import(pathToFileURL(entryPointPath)); +} diff --git a/src/bin.ts b/src/bin.ts index fb3208c48..f470a2c83 100644 --- a/src/bin.ts +++ b/src/bin.ts @@ -639,7 +639,15 @@ function phase4(payload: BootstrapState) { // Execute the main contents (either eval, script or piped). if (executeEntrypoint) { - Module.runMain(); + if ( + payload.isInChildProcess && + versionGteLt(process.versions.node, '18.6.0') + ) { + // HACK workaround node regression + require('../dist-raw/runmain-hack.js').run(entryPointPath); + } else { + Module.runMain(); + } } else { // Note: eval and repl may both run, but never with stdin. // If stdin runs, eval and repl will not. From 0e0da590b3c104ee515ae7b603c29d3d87620ce5 Mon Sep 17 00:00:00 2001 From: Andrew Bradley Date: Wed, 13 Jul 2022 22:32:39 -0400 Subject: [PATCH 18/19] 10.9.1 --- package-lock.json | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index 90ef1939b..79b93f769 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "ts-node", - "version": "10.9.0", + "version": "10.9.1", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index a6842cf72..ae0924a17 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ts-node", - "version": "10.9.0", + "version": "10.9.1", "description": "TypeScript execution environment and REPL for node.js, with source map support", "main": "dist/index.js", "exports": { From 97f9afd046b66a0fe05a7d76e7a32f94b872016f Mon Sep 17 00:00:00 2001 From: Andrew Bradley Date: Thu, 14 Jul 2022 14:31:26 -0400 Subject: [PATCH 19/19] V11 breaking changes (#1760) * remove node12 stuff * bump minimum supported TS version to 3.8 to match DT * install source-map-support in register, not create * Remove ts.createIncrementalProgram fallback to ts.createEmitAndSemanticDiagnosticsBuilderProgram * Make enableExperimentalEsmLoaderInterop() on by default; remove conditional code * Remove deprecated `ts-script` bin * make bin.ts `main()` export @internal * change `nodeEval` function signature to accept `context: Context` instead of `: any` * fix version check * Add node18 @tsconfig/bases; remove node10 and node12; change dependencies to be wildcard; will respect whatever version you want to use * More stuff for bumping minimum supported node12 -> node14 * remove test workaround that avoided old bug in node nightly * fix test failing due to `lib` change in @tsconfig/bases node18 * merge fixes * fix --- .github/workflows/continuous-integration.yml | 51 +- ava.config.cjs | 4 +- dist-raw/README.md | 2 +- dist-raw/node-internal-modules-cjs-helpers.js | 3 +- dist-raw/node-internal-modules-cjs-loader.js | 16 +- dist-raw/node-options.js | 2 - node18/tsconfig.json | 3 + package-lock.json | 11 +- package.json | 20 +- src/bin-script-deprecated.ts | 10 - src/bin.ts | 22 +- src/child/spawn-child.ts | 6 - src/esm.ts | 2 - src/index.ts | 102 +-- src/repl.ts | 5 +- src/test/diagnostics.spec.ts | 12 +- src/test/esm-loader.spec.ts | 619 +++++++++--------- src/test/helpers.ts | 18 +- src/test/index.spec.ts | 246 +++---- src/test/module-node/1778.spec.ts | 8 +- src/test/pluggable-dep-resolution.spec.ts | 9 +- src/test/repl/node-repl-tla.ts | 36 +- src/test/repl/repl.spec.ts | 212 +++--- src/test/resolver.spec.ts | 6 +- src/tsconfigs.ts | 14 +- tests/tsconfig-bases/node10/tsconfig.json | 3 - tests/tsconfig-bases/node12/tsconfig.json | 3 - tests/tsconfig-bases/node18/tsconfig.json | 3 + tsconfig.json | 6 +- 29 files changed, 604 insertions(+), 850 deletions(-) create mode 100644 node18/tsconfig.json delete mode 100644 src/bin-script-deprecated.ts delete mode 100644 tests/tsconfig-bases/node10/tsconfig.json delete mode 100644 tests/tsconfig-bases/node12/tsconfig.json create mode 100644 tests/tsconfig-bases/node18/tsconfig.json 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",