From 6f66541461b7c70336a8a3f1e21a88e56fb23e64 Mon Sep 17 00:00:00 2001 From: Andrew Bradley Date: Fri, 20 May 2022 17:58:13 -0400 Subject: [PATCH] Nodenext polish (#1757) * Add experimentalSpecifierResolution to CLI flags, tsconfig, public API * Make moduleTypes page link to https://www.typescriptlang.org/docs/handbook/esm-node.html * Allow .jsx imports to remap to .tsx, the same way .js can map to .tsx * improve experimentalResolver docs * again tweak moduleTypeOverrides doc * lint-fix * tweak `experimentalResolver` docs --- dist-raw/node-internal-modules-cjs-loader.js | 8 ++++-- dist-raw/node-internal-modules-esm-resolve.js | 5 ++-- src/bin.ts | 13 +++++++++ src/file-extensions.ts | 2 ++ src/index.ts | 9 ++++-- website/docs/module-type-overrides.md | 4 +-- website/docs/options.md | 28 +++++++++++++++++-- 7 files changed, 57 insertions(+), 12 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 e863d9b8d..2fbd832e1 100644 --- a/dist-raw/node-internal-modules-esm-resolve.js +++ b/dist-raw/node-internal-modules-esm-resolve.js @@ -92,7 +92,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/bin.ts b/src/bin.ts index 693045707..8b5f91767 100644 --- a/src/bin.ts +++ b/src/bin.ts @@ -24,6 +24,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'; @@ -140,6 +141,7 @@ function parseArgv(argv: string[], entrypointArgs: Record) { '--scope': Boolean, '--scopeDir': String, '--noExperimentalReplAwait': Boolean, + '--experimentalSpecifierResolution': String, // Aliases. '-e': '--eval', @@ -173,6 +175,8 @@ function parseArgv(argv: string[], entrypointArgs: Record) { '--log-error': '--logError', '--scope-dir': '--scopeDir', '--no-experimental-repl-await': '--noExperimentalReplAwait', + '--experimental-specifier-resolution': + '--experimentalSpecifierResolution', }, { argv, @@ -215,6 +219,7 @@ function parseArgv(argv: string[], entrypointArgs: Record) { '--scope': scope = undefined, '--scopeDir': scopeDir = undefined, '--noExperimentalReplAwait': noExperimentalReplAwait, + '--experimentalSpecifierResolution': experimentalSpecifierResolution, '--esm': esm, _: restArgs, } = args; @@ -253,6 +258,7 @@ function parseArgv(argv: string[], entrypointArgs: Record) { scope, scopeDir, noExperimentalReplAwait, + experimentalSpecifierResolution, esm, }; } @@ -300,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); @@ -361,6 +369,8 @@ function phase3(payload: BootstrapState) { argsRequire, scope, scopeDir, + esm, + experimentalSpecifierResolution, } = payload.parseArgvResult; const { cwd, scriptPath } = payload.phase2Result!; @@ -388,6 +398,9 @@ function phase3(payload: BootstrapState) { scope, scopeDir, preferTsExts, + esm, + experimentalSpecifierResolution: + experimentalSpecifierResolution as ExperimentalSpecifierResolution, }); if (preloadedConfig.options.esm) payload.shouldUseChildProcess = true; diff --git a/src/file-extensions.ts b/src/file-extensions.ts index 3ae1097e6..87e8be1c6 100644 --- a/src/file-extensions.ts +++ b/src/file-extensions.ts @@ -100,6 +100,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) => @@ -143,6 +144,7 @@ export function getExtensions( legacyMainResolveAddsIfOmitted, replacementsForMjs, replacementsForCjs, + replacementsForJsx, replacementsForJs, }; } diff --git a/src/index.ts b/src/index.ts index b4ca47e47..607d5976d 100644 --- a/src/index.ts +++ b/src/index.ts @@ -367,6 +367,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; @@ -394,9 +400,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/module-type-overrides.md b/website/docs/module-type-overrides.md index 5247a0218..c8701a935 100644 --- a/website/docs/module-type-overrides.md +++ b/website/docs/module-type-overrides.md @@ -2,8 +2,8 @@ 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 -in this section. `NodeNext`, `.mts`, and `.cts` should work well for most projects. +> 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. 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"` diff --git a/website/docs/options.md b/website/docs/options.md index bcfce5563..6fec6b573 100644 --- a/website/docs/options.md +++ b/website/docs/options.md @@ -369,11 +369,35 @@ 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: -*Default:* `false`
+* 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: + +* `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`, but will likely be enabled by default in a future version
*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.