diff --git a/src/bin.ts b/src/bin.ts index 900383d71..95c7f9320 100644 --- a/src/bin.ts +++ b/src/bin.ts @@ -299,7 +299,6 @@ export function main( ['ts-node']: { ...service.options, optionBasePaths: undefined, - experimentalEsmLoader: undefined, compilerOptions: undefined, project: service.configFilePath ?? service.options.project, }, diff --git a/src/configuration.ts b/src/configuration.ts index 6bc8e1113..a970b49c4 100644 --- a/src/configuration.ts +++ b/src/configuration.ts @@ -251,7 +251,10 @@ export function readConfig( */ function filterRecognizedTsConfigTsNodeOptions( jsonObject: any -): { recognized: TsConfigOptions; unrecognized: any } { +): { + recognized: TsConfigOptions; + unrecognized: any; +} { if (jsonObject == null) return { recognized: {}, unrecognized: {} }; const { compiler, diff --git a/src/esm.ts b/src/esm.ts index 53e14fd71..ab1638eb1 100644 --- a/src/esm.ts +++ b/src/esm.ts @@ -1,4 +1,4 @@ -import { register, getExtensions, RegisterOptions } from './index'; +import { getExtensions, register, RegisterOptions, Service } from './index'; import { parse as parseUrl, format as formatUrl, @@ -15,17 +15,21 @@ const { // Note: On Windows, URLs look like this: file:///D:/dev/@TypeStrong/ts-node-examples/foo.ts +/** @internal */ export function registerAndCreateEsmHooks(opts?: RegisterOptions) { // Automatically performs registration just like `-r ts-node/register` - const tsNodeInstance = register({ - ...opts, - experimentalEsmLoader: true, - }); + const tsNodeInstance = register(opts); + + return createEsmHooks(tsNodeInstance); +} + +export function createEsmHooks(tsNodeService: Service) { + tsNodeService.enableExperimentalEsmLoaderInterop(); // Custom implementation that considers additional file extensions and automatically adds file extensions const nodeResolveImplementation = createResolve({ - ...getExtensions(tsNodeInstance.config), - preferTsExts: tsNodeInstance.options.preferTsExts, + ...getExtensions(tsNodeService.config), + preferTsExts: tsNodeService.options.preferTsExts, }); return { resolve, getFormat, transformSource }; @@ -98,17 +102,17 @@ export function registerAndCreateEsmHooks(opts?: RegisterOptions) { // If file has .ts, .tsx, or .jsx extension, then ask node how it would treat this file if it were .js const ext = extname(nativePath); let nodeSays: { format: Format }; - if (ext !== '.js' && !tsNodeInstance.ignored(nativePath)) { + if (ext !== '.js' && !tsNodeService.ignored(nativePath)) { nodeSays = await defer(formatUrl(pathToFileURL(nativePath + '.js'))); } else { nodeSays = await defer(); } // For files compiled by ts-node that node believes are either CJS or ESM, check if we should override that classification if ( - !tsNodeInstance.ignored(nativePath) && + !tsNodeService.ignored(nativePath) && (nodeSays.format === 'commonjs' || nodeSays.format === 'module') ) { - const { moduleType } = tsNodeInstance.moduleTypeClassifier.classifyModule( + const { moduleType } = tsNodeService.moduleTypeClassifier.classifyModule( normalizeSlashes(nativePath) ); if (moduleType === 'cjs') { @@ -139,11 +143,11 @@ export function registerAndCreateEsmHooks(opts?: RegisterOptions) { } const nativePath = fileURLToPath(url); - if (tsNodeInstance.ignored(nativePath)) { + if (tsNodeService.ignored(nativePath)) { return defer(); } - const emittedJs = tsNodeInstance.compile(sourceAsString, nativePath); + const emittedJs = tsNodeService.compile(sourceAsString, nativePath); return { source: emittedJs }; } diff --git a/src/index.ts b/src/index.ts index dee5fa1b4..5f9c5c9aa 100644 --- a/src/index.ts +++ b/src/index.ts @@ -294,12 +294,6 @@ export interface CreateOptions { transformers?: | _ts.CustomTransformers | ((p: _ts.Program) => _ts.CustomTransformers); - /** - * True if require() hooks should interop with experimental ESM loader. - * Enabled explicitly via a flag since it is a breaking change. - * @internal - */ - experimentalEsmLoader?: boolean; /** * Allows the usage of top level await in REPL. * @@ -369,7 +363,6 @@ export interface TsConfigOptions | 'dir' | 'cwd' | 'projectSearchDir' - | 'experimentalEsmLoader' | 'optionBasePaths' > {} @@ -405,7 +398,6 @@ export const DEFAULTS: RegisterOptions = { typeCheck: yn(env.TS_NODE_TYPE_CHECK), compilerHost: yn(env.TS_NODE_COMPILER_HOST), logError: yn(env.TS_NODE_LOG_ERROR), - experimentalEsmLoader: false, experimentalReplAwait: yn(env.TS_NODE_EXPERIMENTAL_REPL_AWAIT) ?? undefined, }; @@ -452,6 +444,8 @@ export interface Service { addDiagnosticFilter(filter: DiagnosticFilter): void; /** @internal */ installSourceMapSupport(): void; + /** @internal */ + enableExperimentalEsmLoaderInterop(): void; } /** @@ -688,7 +682,7 @@ export function create(rawOptions: CreateOptions = {}): Service { // If it's a file URL, convert to local path // Note: fileURLToPath does not exist on early node v10 // I could not find a way to handle non-URLs except to swallow an error - if (options.experimentalEsmLoader && path.startsWith('file://')) { + if (experimentalEsmLoader && path.startsWith('file://')) { try { path = fileURLToPath(path); } catch (e) { @@ -1260,6 +1254,15 @@ export function create(rawOptions: CreateOptions = {}): Service { }); } + /** + * True if require() hooks should interop with experimental ESM loader. + * Enabled explicitly via a flag since it is a breaking change. + */ + let experimentalEsmLoader = false; + function enableExperimentalEsmLoaderInterop() { + experimentalEsmLoader = true; + } + return { [TS_NODE_SERVICE_BRAND]: true, ts, @@ -1274,6 +1277,7 @@ export function create(rawOptions: CreateOptions = {}): Service { shouldReplAwait, addDiagnosticFilter, installSourceMapSupport, + enableExperimentalEsmLoaderInterop, }; } @@ -1468,3 +1472,8 @@ function getTokenAtPosition( return current; } } + +import type { createEsmHooks as createEsmHooksFn } from './esm'; +export const createEsmHooks: typeof createEsmHooksFn = ( + tsNodeService: Service +) => require('./esm').createEsmHooks(tsNodeService); diff --git a/src/test/esm-loader.spec.ts b/src/test/esm-loader.spec.ts new file mode 100644 index 000000000..e117e1ef8 --- /dev/null +++ b/src/test/esm-loader.spec.ts @@ -0,0 +1,39 @@ +// ESM loader hook tests +// TODO: at the time of writing, other ESM loader hook tests have not been moved into this file. +// Should consolidate them here. + +import { context } from './testlib'; +import semver = require('semver'); +import { + contextTsNodeUnderTest, + EXPERIMENTAL_MODULES_FLAG, + TEST_DIR, +} from './helpers'; +import { createExec } from './exec-helpers'; +import { join } from 'path'; +import * as expect from 'expect'; + +const test = context(contextTsNodeUnderTest); + +const exec = createExec({ + cwd: TEST_DIR, +}); + +test.suite('createEsmHooks', (test) => { + if (semver.gte(process.version, '12.16.0')) { + test('should create proper hooks with provided instance', async () => { + const { err } = await exec( + `node ${EXPERIMENTAL_MODULES_FLAG} --loader ./loader.mjs index.ts`, + { + cwd: join(TEST_DIR, './esm-custom-loader'), + } + ); + + if (err === null) { + throw new Error('Command was expected to fail, but it succeeded.'); + } + + expect(err.message).toMatch(/TS6133:\s+'unusedVar'/); + }); + } +}); diff --git a/tests/esm-custom-loader/index.ts b/tests/esm-custom-loader/index.ts new file mode 100755 index 000000000..89efb1cf9 --- /dev/null +++ b/tests/esm-custom-loader/index.ts @@ -0,0 +1,4 @@ +export function abc() { + let unusedVar: string; + return true; +} diff --git a/tests/esm-custom-loader/loader.mjs b/tests/esm-custom-loader/loader.mjs new file mode 100755 index 000000000..3b0ee683c --- /dev/null +++ b/tests/esm-custom-loader/loader.mjs @@ -0,0 +1,16 @@ +import { fileURLToPath } from 'url'; +import { createRequire } from 'module'; +const require = createRequire(fileURLToPath(import.meta.url)); + +/** @type {import('../../dist')} **/ +const { createEsmHooks, register } = require('ts-node'); + +const tsNodeInstance = register({ + compilerOptions: { + noUnusedLocals: true, + }, +}); + +export const { resolve, getFormat, transformSource } = createEsmHooks( + tsNodeInstance +); diff --git a/tests/esm-custom-loader/package.json b/tests/esm-custom-loader/package.json new file mode 100755 index 000000000..3dbc1ca59 --- /dev/null +++ b/tests/esm-custom-loader/package.json @@ -0,0 +1,3 @@ +{ + "type": "module" +} diff --git a/tests/esm-custom-loader/tsconfig.json b/tests/esm-custom-loader/tsconfig.json new file mode 100755 index 000000000..ad01eee33 --- /dev/null +++ b/tests/esm-custom-loader/tsconfig.json @@ -0,0 +1,7 @@ +{ + "compilerOptions": { + "module": "ESNext", + "moduleResolution": "node", + "noUnusedLocals": false + } +}