From 801b113113c940b5d4b3b531e8398a19f07aac9c Mon Sep 17 00:00:00 2001 From: bluelovers Date: Thu, 9 Dec 2021 23:19:39 +0800 Subject: [PATCH 1/3] feat: support .mts .cts --- src/index.ts | 14 ++++++++++++-- src/test/index.spec.ts | 24 ++++++++++++++++++++++++ tests/ts45-ext/ext-cts/index.cts | 3 +++ tests/ts45-ext/ext-cts/tsconfig.json | 5 +++++ tests/ts45-ext/ext-mts/index.mts | 3 +++ tests/ts45-ext/ext-mts/tsconfig.json | 5 +++++ 6 files changed, 52 insertions(+), 2 deletions(-) create mode 100644 tests/ts45-ext/ext-cts/index.cts create mode 100644 tests/ts45-ext/ext-cts/tsconfig.json create mode 100644 tests/ts45-ext/ext-mts/index.mts create mode 100644 tests/ts45-ext/ext-mts/tsconfig.json diff --git a/src/index.ts b/src/index.ts index b3506a4cd..2bf4be322 100644 --- a/src/index.ts +++ b/src/index.ts @@ -24,6 +24,7 @@ import { } from './module-type-classifier'; import { createResolverFunctions } from './resolver-functions'; import type { createEsmHooks as createEsmHooksFn } from './esm'; +import { ModuleKind } from 'typescript'; export { TSCommon }; export { @@ -502,11 +503,20 @@ export interface DiagnosticFilter { export function getExtensions(config: _ts.ParsedCommandLine) { const tsExtensions = ['.ts']; const jsExtensions = []; + const useESNext = [ModuleKind.ES2015, ModuleKind.ES2020, 7 as any, ModuleKind.ESNext, 199 as any].indexOf(config.options.module) !== -1; + const useCommonJS = [ModuleKind.CommonJS, 100 as any].indexOf(config.options.module) !== -1; // Enable additional extensions when JSX or `allowJs` is enabled. if (config.options.jsx) tsExtensions.push('.tsx'); - if (config.options.allowJs) jsExtensions.push('.js'); - if (config.options.jsx && config.options.allowJs) jsExtensions.push('.jsx'); + // Support .mts .cts + if (useESNext) tsExtensions.push('.mts'); + if (useCommonJS) tsExtensions.push('.cts'); + if (config.options.allowJs) { + jsExtensions.push('.js'); + if (config.options.jsx) jsExtensions.push('.jsx'); + if (useESNext) tsExtensions.push('.mjs'); + if (useCommonJS) tsExtensions.push('.cjs'); + } return { tsExtensions, jsExtensions }; } diff --git a/src/test/index.spec.ts b/src/test/index.spec.ts index 3b128afdc..b5f8adad4 100644 --- a/src/test/index.spec.ts +++ b/src/test/index.spec.ts @@ -183,6 +183,30 @@ test.suite('ts-node', (test) => { expect(err).toBe(null); expect(stdout).toBe('hello world\n'); }); + + test('should support cts when module = CommonJS', async () => { + const { err, stdout } = await exec( + [ + CMD_TS_NODE_WITH_PROJECT_FLAG, + '-O "{\\"module\\":"CommonJS"}"', + '-pe "import { main } from \'./ts45-ext/ext-cts/index\';main()"', + ].join(' ') + ); + expect(err).toBe(null); + expect(stdout).toBe('hello world\n'); + }); + + test('should support cts when module = ESNext', async () => { + const { err, stdout } = await exec( + [ + CMD_TS_NODE_WITH_PROJECT_FLAG, + '-O "{\\"module\\":"ESNext"}"', + '-pe "import { main } from \'./ts45-ext/ext-mts/index\';main()"', + ].join(' ') + ); + expect(err).toBe(null); + expect(stdout).toBe('hello world\n'); + }); } test('should eval code', async () => { diff --git a/tests/ts45-ext/ext-cts/index.cts b/tests/ts45-ext/ext-cts/index.cts new file mode 100644 index 000000000..9001625ad --- /dev/null +++ b/tests/ts45-ext/ext-cts/index.cts @@ -0,0 +1,3 @@ +export function main() { + return 'hello world'; +} diff --git a/tests/ts45-ext/ext-cts/tsconfig.json b/tests/ts45-ext/ext-cts/tsconfig.json new file mode 100644 index 000000000..28900bb1b --- /dev/null +++ b/tests/ts45-ext/ext-cts/tsconfig.json @@ -0,0 +1,5 @@ +{ + "compilerOptions": { + "module": "CommonJS" + } +} diff --git a/tests/ts45-ext/ext-mts/index.mts b/tests/ts45-ext/ext-mts/index.mts new file mode 100644 index 000000000..9001625ad --- /dev/null +++ b/tests/ts45-ext/ext-mts/index.mts @@ -0,0 +1,3 @@ +export function main() { + return 'hello world'; +} diff --git a/tests/ts45-ext/ext-mts/tsconfig.json b/tests/ts45-ext/ext-mts/tsconfig.json new file mode 100644 index 000000000..1ac61592b --- /dev/null +++ b/tests/ts45-ext/ext-mts/tsconfig.json @@ -0,0 +1,5 @@ +{ + "compilerOptions": { + "module": "ESNext" + } +} From 9b1d42ddb9c674a6322b14e2a4d5f8270f2c7c34 Mon Sep 17 00:00:00 2001 From: Andrew Bradley Date: Wed, 26 Jan 2022 22:45:12 -0500 Subject: [PATCH 2/3] stuff --- src/esm.ts | 23 ++++++++++++++++++----- src/index.ts | 27 +++++++++++++++------------ 2 files changed, 33 insertions(+), 17 deletions(-) diff --git a/src/esm.ts b/src/esm.ts index c83fd22c4..f95987231 100644 --- a/src/esm.ts +++ b/src/esm.ts @@ -96,7 +96,7 @@ export function createEsmHooks(tsNodeService: Service) { // Custom implementation that considers additional file extensions and automatically adds file extensions const nodeResolveImplementation = createResolve({ - ...getExtensions(tsNodeService.config), + ...tsNodeService.extensions, preferTsExts: tsNodeService.options.preferTsExts, }); @@ -112,7 +112,6 @@ export function createEsmHooks(tsNodeService: Service) { const hooksAPI: NodeLoaderHooksAPI1 | NodeLoaderHooksAPI2 = newHooksAPI ? { resolve, load, getFormat: undefined, transformSource: undefined } : { resolve, getFormat, transformSource, load: undefined }; - return hooksAPI; function isFileUrlOrNodeStyleSpecifier(parsed: UrlWithStringQuery) { // We only understand file:// URLs, but in node, the specifier can be a node-style `./foo` or `foo` @@ -205,6 +204,16 @@ export function createEsmHooks(tsNodeService: Service) { return { format, source }; } + // Mapping from extensions understood by tsc to the equivalent for node, + // as far as getFormat is concerned. + const nodeEquivalentExtensions = new Map([ + ['.ts', '.js'], + ['.tsx', '.js'], + ['.jsx', '.js'], + ['.mts', '.mjs'], + ['.cts', '.cjs'] + ]); + async function getFormat( url: string, context: {}, @@ -227,11 +236,13 @@ export function createEsmHooks(tsNodeService: Service) { const nativePath = fileURLToPath(url); - // If file has .ts, .tsx, or .jsx extension, then ask node how it would treat this file if it were .js + // If file has extension not understood by node, then ask node how it would treat the emitted extension. + // E.g. .mts compiles to .mjs, so ask node how to classify an .mjs file. const ext = extname(nativePath); let nodeSays: { format: NodeLoaderHooksFormat }; - if (ext !== '.js' && !tsNodeService.ignored(nativePath)) { - nodeSays = await defer(formatUrl(pathToFileURL(nativePath + '.js'))); + const nodeEquivalentExt = nodeEquivalentExtensions.get(ext); + if (nodeEquivalentExt && !tsNodeService.ignored(nativePath)) { + nodeSays = await defer(formatUrl(pathToFileURL(nativePath + nodeEquivalentExt))); } else { nodeSays = await defer(); } @@ -283,4 +294,6 @@ export function createEsmHooks(tsNodeService: Service) { return { source: emittedJs }; } + + return hooksAPI; } diff --git a/src/index.ts b/src/index.ts index 2bf4be322..679d047fc 100644 --- a/src/index.ts +++ b/src/index.ts @@ -24,7 +24,6 @@ import { } from './module-type-classifier'; import { createResolverFunctions } from './resolver-functions'; import type { createEsmHooks as createEsmHooksFn } from './esm'; -import { ModuleKind } from 'typescript'; export { TSCommon }; export { @@ -480,6 +479,8 @@ export interface Service { installSourceMapSupport(): void; /** @internal */ enableExperimentalEsmLoaderInterop(): void; + /** @internal */ + extensions: Extensions; } /** @@ -500,22 +501,23 @@ export interface DiagnosticFilter { } /** @internal */ -export function getExtensions(config: _ts.ParsedCommandLine) { +export type Extensions = ReturnType; + +/** @internal */ +export function getExtensions(config: _ts.ParsedCommandLine, tsVersion: string) { + // TS 4.5 is first version to understand .cts, .mts, .cjs, and .mjs extensions + const tsSupportsMtsCtsExts = versionGteLt(tsVersion, '4.5.0'); const tsExtensions = ['.ts']; const jsExtensions = []; - const useESNext = [ModuleKind.ES2015, ModuleKind.ES2020, 7 as any, ModuleKind.ESNext, 199 as any].indexOf(config.options.module) !== -1; - const useCommonJS = [ModuleKind.CommonJS, 100 as any].indexOf(config.options.module) !== -1; + + if(tsSupportsMtsCtsExts) tsExtensions.push('.mts', '.cts'); // Enable additional extensions when JSX or `allowJs` is enabled. if (config.options.jsx) tsExtensions.push('.tsx'); - // Support .mts .cts - if (useESNext) tsExtensions.push('.mts'); - if (useCommonJS) tsExtensions.push('.cts'); if (config.options.allowJs) { - jsExtensions.push('.js'); + jsExtensions.push('.js', '.mjs', '.cjs'); if (config.options.jsx) jsExtensions.push('.jsx'); - if (useESNext) tsExtensions.push('.mjs'); - if (useCommonJS) tsExtensions.push('.cjs'); + if (tsSupportsMtsCtsExts) tsExtensions.push('.mjs', '.cjs'); } return { tsExtensions, jsExtensions }; } @@ -539,7 +541,7 @@ export function register( } const originalJsHandler = require.extensions['.js']; - const { tsExtensions, jsExtensions } = getExtensions(service.config); + const { tsExtensions, jsExtensions } = service.extensions; const extensions = [...tsExtensions, ...jsExtensions]; // Expose registered instance globally. @@ -1306,7 +1308,7 @@ export function create(rawOptions: CreateOptions = {}): Service { let active = true; const enabled = (enabled?: boolean) => enabled === undefined ? active : (active = !!enabled); - const extensions = getExtensions(config); + const extensions = getExtensions(config, ts.version); const ignored = (fileName: string) => { if (!active) return true; const ext = extname(fileName); @@ -1343,6 +1345,7 @@ export function create(rawOptions: CreateOptions = {}): Service { addDiagnosticFilter, installSourceMapSupport, enableExperimentalEsmLoaderInterop, + extensions, }; } From 49832c9643240e1fa07b7c6ae4b117f15cddfad0 Mon Sep 17 00:00:00 2001 From: Andrew Bradley Date: Thu, 27 Jan 2022 00:23:44 -0500 Subject: [PATCH 3/3] WIP with TODOs --- dist-raw/node-cjs-loader-utils.js | 2 ++ dist-raw/node-esm-resolve-implementation.js | 14 +++++++++++--- src/index.ts | 19 +++++++++++++++++-- 3 files changed, 30 insertions(+), 5 deletions(-) diff --git a/dist-raw/node-cjs-loader-utils.js b/dist-raw/node-cjs-loader-utils.js index b7ec0d531..8ddeca084 100644 --- a/dist-raw/node-cjs-loader-utils.js +++ b/dist-raw/node-cjs-loader-utils.js @@ -23,6 +23,8 @@ function assertScriptCanLoadAsCJSImpl(service, module, filename) { const tsNodeClassification = service.moduleTypeClassifier.classifyModule(normalizeSlashes(filename)); if(tsNodeClassification.moduleType === 'cjs') return; + // TODO modify to ignore package.json when file extension is ESM-only + // Function require shouldn't be used in ES modules. if (tsNodeClassification.moduleType === 'esm' || (pkg && pkg.data && pkg.data.type === 'module')) { const parentPath = module.parent && module.parent.filename; diff --git a/dist-raw/node-esm-resolve-implementation.js b/dist-raw/node-esm-resolve-implementation.js index 04ea84668..ce2ffad16 100644 --- a/dist-raw/node-esm-resolve-implementation.js +++ b/dist-raw/node-esm-resolve-implementation.js @@ -330,11 +330,19 @@ function resolveExtensions(search) { * TS's resolver can resolve foo.js to foo.ts, by replacing .js extension with several source extensions. * IMPORTANT: preserve ordering according to preferTsExts; this affects resolution behavior! */ -const replacementExtensions = extensions.filter(ext => ['.js', '.jsx', '.ts', '.tsx'].includes(ext)); +const replacementExtensionsForJs = extensions.filter(ext => ['.js', '.jsx', '.ts', '.tsx'].includes(ext)); +const replacementExtensionsForMjs = extensions.filter(ext => ['.mjs', '.mts'].includes(ext)); +const replacementExtensionsForCjs = extensions.filter(ext => ['.cjs', '.cts'].includes(ext)); function resolveReplacementExtensions(search) { - if (search.pathname.match(/\.js$/)) { - const pathnameWithoutExtension = search.pathname.slice(0, search.pathname.length - 3); + const lastDotIndex = search.pathname.lastIndexOf('.'); + const ext = search.pathname.slice(lastDotIndex); + if (ext === '.js' || ext === '.cjs' || ext === '.mjs') { + const pathnameWithoutExtension = search.pathname.slice(0, lastDotIndex); + const replacementExtensions = + ext === '.js' ? replacementExtensionsForJs + : ext === '.mjs' ? replacementExtensionsForMjs + : replacementExtensionsForCjs; for (let i = 0; i < replacementExtensions.length; i++) { const extension = replacementExtensions[i]; const guess = new URL(search.toString()); diff --git a/src/index.ts b/src/index.ts index 679d047fc..86e254dcd 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1381,14 +1381,29 @@ function registerExtensions( service: Service, originalJsHandler: (m: NodeModule, filename: string) => any ) { + const exts = new Set(extensions); + // Only way to transform .mts and .cts is via the .js extension. + // Can't register those extensions cuz would allow omitting file extension; node requires ext for .cjs and .mjs + if(exts.has('.mts') || exts.has('.cts')) exts.add('.js'); + // Filter extensions which should not be added to `require.extensions` + // They may still be handled via the `.js` extension handler. + exts.delete('.mts'); + exts.delete('.cts'); + exts.delete('.mjs'); + exts.delete('.cjs'); + + // TODO do we care about overriding moduleType for mjs? No, I don't think so. + // Could conditionally register `.mjs` extension when moduleType overrides are configured, + // since that is the only situation where we want to avoid node throwing an error. + // Register new extensions. - for (const ext of extensions) { + for (const ext of exts) { registerExtension(ext, service, originalJsHandler); } if (preferTsExts) { const preferredExtensions = new Set([ - ...extensions, + ...exts, ...Object.keys(require.extensions), ]);