From 8f91b9ae22097c6e6e2b5a8627bdfe0df24b3b55 Mon Sep 17 00:00:00 2001 From: Andrew Bradley Date: Thu, 19 May 2022 12:30:33 -0400 Subject: [PATCH 1/7] Add experimentalSpecifierResolution to CLI flags, tsconfig, public API --- src/bin.ts | 11 +++++++++++ src/index.ts | 9 ++++++--- website/docs/options.md | 11 +++++++++++ 3 files changed, 28 insertions(+), 3 deletions(-) diff --git a/src/bin.ts b/src/bin.ts index 3256649e6..ee79dd6c9 100644 --- a/src/bin.ts +++ b/src/bin.ts @@ -25,6 +25,7 @@ import { createEsmHooks, createFromPreloadedConfig, DEFAULTS, + ExperimentalSpecifierResolution, } from './index'; import type { TSInternal } from './ts-compiler-types'; import { addBuiltinLibsToObject } from '../dist-raw/node-internal-modules-cjs-helpers'; @@ -141,6 +142,7 @@ function parseArgv(argv: string[], entrypointArgs: Record) { '--scope': Boolean, '--scopeDir': String, '--noExperimentalReplAwait': Boolean, + '--experimentalSpecifierResolution': String, // Aliases. '-e': '--eval', @@ -174,6 +176,7 @@ function parseArgv(argv: string[], entrypointArgs: Record) { '--log-error': '--logError', '--scope-dir': '--scopeDir', '--no-experimental-repl-await': '--noExperimentalReplAwait', + '--experimental-specifier-resolution': '--experimentalSpecifierResolution' }, { argv, @@ -216,6 +219,7 @@ function parseArgv(argv: string[], entrypointArgs: Record) { '--scope': scope = undefined, '--scopeDir': scopeDir = undefined, '--noExperimentalReplAwait': noExperimentalReplAwait, + '--experimentalSpecifierResolution': experimentalSpecifierResolution, '--esm': esm, _: restArgs, } = args; @@ -254,6 +258,7 @@ function parseArgv(argv: string[], entrypointArgs: Record) { scope, scopeDir, noExperimentalReplAwait, + experimentalSpecifierResolution, esm, }; } @@ -301,6 +306,8 @@ Options: --preferTsExts Prefer importing TypeScript files over JavaScript files --logError Logs TypeScript errors to stderr instead of throwing exceptions --noExperimentalReplAwait Disable top-level await in REPL. Equivalent to node's --no-experimental-repl-await + --experimentalSpecifierResolution [node|explicit] + Equivalent to node's --experimental-specifier-resolution `); process.exit(0); @@ -362,6 +369,8 @@ function phase3(payload: BootstrapState) { argsRequire, scope, scopeDir, + esm, + experimentalSpecifierResolution, } = payload.parseArgvResult; const { cwd, scriptPath } = payload.phase2Result!; @@ -389,6 +398,8 @@ function phase3(payload: BootstrapState) { scope, scopeDir, preferTsExts, + esm, + experimentalSpecifierResolution: experimentalSpecifierResolution as ExperimentalSpecifierResolution, }); if (preloadedConfig.options.esm) payload.shouldUseChildProcess = true; diff --git a/src/index.ts b/src/index.ts index a0079bd24..7b2819816 100644 --- a/src/index.ts +++ b/src/index.ts @@ -393,6 +393,12 @@ export interface CreateOptions { * @default false */ preferTsExts?: boolean; + /** + * Like node's `--experimental-specifier-resolution`, , but can also be set in your `tsconfig.json` for convenience. + * + * For details, see https://nodejs.org/dist/latest-v18.x/docs/api/esm.html#customizing-esm-specifier-resolution-algorithm + */ + experimentalSpecifierResolution?: 'node' | 'explicit'; } export type ModuleTypes = Record; @@ -420,9 +426,6 @@ export interface RegisterOptions extends CreateOptions { * For details, see https://github.com/TypeStrong/ts-node/issues/1514 */ experimentalResolver?: boolean; - - /** @internal */ - experimentalSpecifierResolution?: 'node' | 'explicit'; } export type ExperimentalSpecifierResolution = 'node' | 'explicit'; diff --git a/website/docs/options.md b/website/docs/options.md index bcfce5563..211f98976 100644 --- a/website/docs/options.md +++ b/website/docs/options.md @@ -374,6 +374,17 @@ Enable experimental features that re-map imports and require calls to support: ` *Default:* `false`
*Can only be specified via `tsconfig.json` or API.* +### experimentalSpecifierResolution + +```shell +ts-node --experimentalSpecifierResolution node +``` + +Like node's [`--experimental-specifier-resolution`](https://nodejs.org/dist/latest-v18.x/docs/api/esm.html#customizing-esm-specifier-resolution-algorithm), but can also be set in your `tsconfig.json` for convenience. +Requires `esm` to be enabled. + +*Default:* `explicit`
+ ## API Options The API includes [additional options](https://typestrong.org/ts-node/api/interfaces/RegisterOptions.html) not shown here. From eb60e6c4b549e7b684036e1af3f40cafd0552a07 Mon Sep 17 00:00:00 2001 From: Andrew Bradley Date: Thu, 19 May 2022 12:35:10 -0400 Subject: [PATCH 2/7] Make moduleTypes page link to https://www.typescriptlang.org/docs/handbook/esm-node.html --- website/docs/module-type-overrides.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/docs/module-type-overrides.md b/website/docs/module-type-overrides.md index 5247a0218..d171c9ac4 100644 --- a/website/docs/module-type-overrides.md +++ b/website/docs/module-type-overrides.md @@ -2,7 +2,7 @@ title: Module type overrides --- -> Wherever possible, it is recommended to use TypeScript's [`NodeNext` or `Node16` mode](https://devblogs.microsoft.com/typescript/announcing-typescript-4-7-rc/#ecmascript-module-support-in-node-js) instead of the options described +> Wherever possible, it is recommended to use TypeScript's [`NodeNext` or `Node16` mode](https://www.typescriptlang.org/docs/handbook/esm-node.html) instead of the options described in this section. `NodeNext`, `.mts`, and `.cts` should work well for most projects. When deciding how a file should be compiled and executed -- as either CommonJS or native ECMAScript module -- ts-node matches From 30f030e750277a719caadd75c519dacff0c669cd Mon Sep 17 00:00:00 2001 From: Andrew Bradley Date: Thu, 19 May 2022 12:41:44 -0400 Subject: [PATCH 3/7] Allow .jsx imports to remap to .tsx, the same way .js can map to .tsx --- dist-raw/node-internal-modules-cjs-loader.js | 8 +++++--- dist-raw/node-internal-modules-esm-resolve.js | 5 +++-- src/file-extensions.ts | 2 ++ 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/dist-raw/node-internal-modules-cjs-loader.js b/dist-raw/node-internal-modules-cjs-loader.js index e2ab5a2e7..cb83c3532 100644 --- a/dist-raw/node-internal-modules-cjs-loader.js +++ b/dist-raw/node-internal-modules-cjs-loader.js @@ -144,7 +144,7 @@ function readPackageScope(checkPath) { */ function createCjsLoader(opts) { const {nodeEsmResolver, preferTsExts} = opts; -const {replacementsForCjs, replacementsForJs, replacementsForMjs} = opts.extensions; +const {replacementsForCjs, replacementsForJs, replacementsForMjs, replacementsForJsx} = opts.extensions; const { encodedSepRegEx, packageExportsResolve, @@ -219,10 +219,11 @@ function statReplacementExtensions(p) { const lastDotIndex = p.lastIndexOf('.'); if(lastDotIndex >= 0) { const ext = p.slice(lastDotIndex); - if (ext === '.js' || ext === '.cjs' || ext === '.mjs') { + if (ext === '.js' || ext === '.jsx' || ext === '.mjs' || ext === '.cjs') { const pathnameWithoutExtension = p.slice(0, lastDotIndex); const replacementExts = ext === '.js' ? replacementsForJs + : ext === '.jsx' ? replacementsForJsx : ext === '.mjs' ? replacementsForMjs : replacementsForCjs; for (let i = 0; i < replacementExts.length; i++) { @@ -240,10 +241,11 @@ function tryReplacementExtensions(p, isMain) { const lastDotIndex = p.lastIndexOf('.'); if(lastDotIndex >= 0) { const ext = p.slice(lastDotIndex); - if (ext === '.js' || ext === '.cjs' || ext === '.mjs') { + if (ext === '.js' || ext === '.jsx' || ext === '.mjs' || ext === '.cjs') { const pathnameWithoutExtension = p.slice(0, lastDotIndex); const replacementExts = ext === '.js' ? replacementsForJs + : ext === '.jsx' ? replacementsForJsx : ext === '.mjs' ? replacementsForMjs : replacementsForCjs; for (let i = 0; i < replacementExts.length; i++) { diff --git a/dist-raw/node-internal-modules-esm-resolve.js b/dist-raw/node-internal-modules-esm-resolve.js index 24df6164c..d80c373c7 100644 --- a/dist-raw/node-internal-modules-esm-resolve.js +++ b/dist-raw/node-internal-modules-esm-resolve.js @@ -101,7 +101,7 @@ function createResolve(opts) { // TODO receive cached fs implementations here const {preferTsExts, tsNodeExperimentalSpecifierResolution, extensions} = opts; const esrnExtensions = extensions.experimentalSpecifierResolutionAddsIfOmitted; -const {legacyMainResolveAddsIfOmitted, replacementsForCjs, replacementsForJs, replacementsForMjs} = extensions; +const {legacyMainResolveAddsIfOmitted, replacementsForCjs, replacementsForJs, replacementsForMjs, replacementsForJsx} = extensions; // const experimentalSpecifierResolution = tsNodeExperimentalSpecifierResolution ?? getOptionValue('--experimental-specifier-resolution'); const experimentalSpecifierResolution = tsNodeExperimentalSpecifierResolution != null ? tsNodeExperimentalSpecifierResolution : getOptionValue('--experimental-specifier-resolution'); @@ -310,10 +310,11 @@ function resolveReplacementExtensions(search) { const lastDotIndex = search.pathname.lastIndexOf('.'); if(lastDotIndex >= 0) { const ext = search.pathname.slice(lastDotIndex); - if (ext === '.js' || ext === '.cjs' || ext === '.mjs') { + if (ext === '.js' || ext === '.jsx' || ext === '.mjs' || ext === '.cjs') { const pathnameWithoutExtension = search.pathname.slice(0, lastDotIndex); const replacementExts = ext === '.js' ? replacementsForJs + : ext === '.jsx' ? replacementsForJsx : ext === '.mjs' ? replacementsForMjs : replacementsForCjs; const guess = new URL(search.toString()); diff --git a/src/file-extensions.ts b/src/file-extensions.ts index 4f73413fb..104c2ccf9 100644 --- a/src/file-extensions.ts +++ b/src/file-extensions.ts @@ -99,6 +99,7 @@ export function getExtensions( const replacementsForJs = r.filter((ext) => ['.js', '.jsx', '.ts', '.tsx'].includes(ext) ); + const replacementsForJsx = r.filter((ext) => ['.jsx', '.tsx'].includes(ext)); const replacementsForMjs = r.filter((ext) => ['.mjs', '.mts'].includes(ext)); const replacementsForCjs = r.filter((ext) => ['.cjs', '.cts'].includes(ext)); const replacementsForJsOrMjs = r.filter((ext) => @@ -142,6 +143,7 @@ export function getExtensions( legacyMainResolveAddsIfOmitted, replacementsForMjs, replacementsForCjs, + replacementsForJsx, replacementsForJs, }; } From 7674c557b5e717c16fb82e970f24a346505cbea4 Mon Sep 17 00:00:00 2001 From: Andrew Bradley Date: Thu, 19 May 2022 22:30:30 -0400 Subject: [PATCH 4/7] improve experimentalResolver docs --- website/docs/options.md | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/website/docs/options.md b/website/docs/options.md index 211f98976..b02b4c4cb 100644 --- a/website/docs/options.md +++ b/website/docs/options.md @@ -369,7 +369,19 @@ Disable top-level await in REPL. Equivalent to node's [`--no-experimental-repl- ### experimentalResolver -Enable experimental features that re-map imports and require calls to support: `baseUrl`, `paths`, `rootDirs`, `.js` to `.ts` file extension mappings, `outDir` to `rootDir` mappings for composite projects and monorepos. For details, see [#1514](https://github.com/TypeStrong/ts-node/issues/1514) +Enable experimental hooks that re-map imports and require calls to support: + +* `.js` to `.ts` mappings, so that `import "foo.js"` can execute `foo.ts` +* `.cjs` to `.cts` mapping +* `.mjs` to `.mts` mapping + +In the future, this hook will also support: + +* `baseUrl`, `paths` +* `rootDirs` +* `outDir` to `rootDir` mappings for composite projects and monorepos + +For details, see [#1514](https://github.com/TypeStrong/ts-node/issues/1514). *Default:* `false`
*Can only be specified via `tsconfig.json` or API.* From fa17b6d152001d96977426133bbcc5e122f87677 Mon Sep 17 00:00:00 2001 From: Andrew Bradley Date: Thu, 19 May 2022 22:43:22 -0400 Subject: [PATCH 5/7] again tweak moduleTypeOverrides doc --- website/docs/module-type-overrides.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/docs/module-type-overrides.md b/website/docs/module-type-overrides.md index d171c9ac4..c8701a935 100644 --- a/website/docs/module-type-overrides.md +++ b/website/docs/module-type-overrides.md @@ -3,7 +3,7 @@ title: Module type overrides --- > Wherever possible, it is recommended to use TypeScript's [`NodeNext` or `Node16` mode](https://www.typescriptlang.org/docs/handbook/esm-node.html) instead of the options described -in this section. `NodeNext`, `.mts`, and `.cts` should work well for most projects. +in this section. Setting `"module": "NodeNext"` and using the `.cts` file extension should work well for most projects. When deciding how a file should be compiled and executed -- as either CommonJS or native ECMAScript module -- ts-node matches `node` and `tsc` behavior. This means TypeScript files are transformed according to your `tsconfig.json` `"module"` From 0f21f714c00e0f69700ec8a19e2b687dc6648213 Mon Sep 17 00:00:00 2001 From: Andrew Bradley Date: Thu, 19 May 2022 22:44:21 -0400 Subject: [PATCH 6/7] lint-fix --- src/bin.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/bin.ts b/src/bin.ts index ee79dd6c9..303af8d0d 100644 --- a/src/bin.ts +++ b/src/bin.ts @@ -176,7 +176,8 @@ function parseArgv(argv: string[], entrypointArgs: Record) { '--log-error': '--logError', '--scope-dir': '--scopeDir', '--no-experimental-repl-await': '--noExperimentalReplAwait', - '--experimental-specifier-resolution': '--experimentalSpecifierResolution' + '--experimental-specifier-resolution': + '--experimentalSpecifierResolution', }, { argv, @@ -399,7 +400,8 @@ function phase3(payload: BootstrapState) { scopeDir, preferTsExts, esm, - experimentalSpecifierResolution: experimentalSpecifierResolution as ExperimentalSpecifierResolution, + experimentalSpecifierResolution: + experimentalSpecifierResolution as ExperimentalSpecifierResolution, }); if (preloadedConfig.options.esm) payload.shouldUseChildProcess = true; From fb83487b61e358591c14e5a934cb8bef9f1028f5 Mon Sep 17 00:00:00 2001 From: Andrew Bradley Date: Fri, 20 May 2022 17:57:36 -0400 Subject: [PATCH 7/7] tweak `experimentalResolver` docs --- website/docs/options.md | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/website/docs/options.md b/website/docs/options.md index b02b4c4cb..6fec6b573 100644 --- a/website/docs/options.md +++ b/website/docs/options.md @@ -371,9 +371,10 @@ Disable top-level await in REPL. Equivalent to node's [`--no-experimental-repl- Enable experimental hooks that re-map imports and require calls to support: -* `.js` to `.ts` mappings, so that `import "foo.js"` can execute `foo.ts` -* `.cjs` to `.cts` mapping -* `.mjs` to `.mts` mapping +* resolves `.js` to `.ts`, so that `import "./foo.js"` will execute `foo.ts` +* resolves `.cjs` to `.cts` +* resolves `.mjs` to `.mts` +* allows including file extensions in CommonJS, for consistency with ESM where this is often mandatory In the future, this hook will also support: @@ -383,7 +384,7 @@ In the future, this hook will also support: For details, see [#1514](https://github.com/TypeStrong/ts-node/issues/1514). -*Default:* `false`
+*Default:* `false`, but will likely be enabled by default in a future version
*Can only be specified via `tsconfig.json` or API.* ### experimentalSpecifierResolution