From 91110cdaf4a67f3b915c67221145fa3f677405eb Mon Sep 17 00:00:00 2001 From: Andrew Bradley Date: Sat, 2 Jul 2022 15:47:45 -0400 Subject: [PATCH 01/12] 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 5142a358..266f2d92 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 87e8be1c..b5fd0355 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 607d5976..7167dbe1 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 afe13b46..83568669 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 00000000..39c4cc29 --- /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 00000000..3a850c17 --- /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 00000000..62d968e8 --- /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 00000000..2f1444fb --- /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 00000000..098594e5 --- /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 02/12] 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 237b2d06..f1f3cb36 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 7fe9bad6..0d7958f0 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 c4ea9e8a..da86bddc 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 57174f5e..1d64186a 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 246b70f4..89e9bd49 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 03/12] 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 eed95a0d..3137daa4 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 04/12] 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 266f2d92..4ab0a7cc 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 05/12] 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 00000000..b249da6d --- /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 06/12] 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 8b5f9176..fb3208c4 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 00000000..abe6da9d --- /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 03a02d2e..0550170b 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 12368fce..618b8190 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 41c421fd..375012a7 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 ca4c2cf8..f085a363 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 00000000..88a3bd61 --- /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 00000000..3dbc1ca5 --- /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 00000000..04e93e5c --- /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 00000000..820d10b2 --- /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 00000000..ec94e846 --- /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 00000000..3dbc1ca5 --- /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 00000000..4114d5ab --- /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 00000000..04e93e5c --- /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 00000000..2d59e0aa --- /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 00000000..3dbc1ca5 --- /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 00000000..4114d5ab --- /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 00000000..04e93e5c --- /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 00000000..f3ba1b30 --- /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 00000000..21230f9d --- /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 00000000..3dbc1ca5 --- /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 00000000..04e93e5c --- /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 00000000..45ff8afd --- /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 00000000..4114d5ab --- /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 00000000..484405d0 --- /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 07/12] 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 21704761..a54979a8 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 08/12] 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 a54979a8..e5940e6a 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 09/12] rebuild readme --- README.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index a08d3648..63625d5e 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 10/12] 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 9e16ce1d..6d42f4a4 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 11/12] 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 7167dbe1..cf9d7eef 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 12/12] 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 f1f3cb36..90ef1939 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 0d7958f0..a6842cf7 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": {