diff --git a/package.json b/package.json index d30cff213..37ea98acf 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,7 @@ "./register/files": "./register/files.js", "./register/transpile-only": "./register/transpile-only.js", "./register/type-check": "./register/type-check.js", + "./register/try-ts-ext": "./register/try-ts-ext.js", "./esm": "./esm.mjs", "./esm.mjs": "./esm.mjs", "./esm/transpile-only": "./esm/transpile-only.mjs", diff --git a/register/try-ts-ext.js b/register/try-ts-ext.js new file mode 100644 index 000000000..e2d59618b --- /dev/null +++ b/register/try-ts-ext.js @@ -0,0 +1,3 @@ +require('../dist').register({ + tryTsExt: true, +}); diff --git a/src/configuration.ts b/src/configuration.ts index 6bc8e1113..c869365ab 100644 --- a/src/configuration.ts +++ b/src/configuration.ts @@ -263,6 +263,7 @@ function filterRecognizedTsConfigTsNodeOptions( ignoreDiagnostics, logError, preferTsExts, + tryTsExt, pretty, require, skipIgnore, @@ -286,6 +287,7 @@ function filterRecognizedTsConfigTsNodeOptions( ignoreDiagnostics, logError, preferTsExts, + tryTsExt, pretty, require, skipIgnore, diff --git a/src/index.ts b/src/index.ts index e1db16cac..e8fa75dd9 100644 --- a/src/index.ts +++ b/src/index.ts @@ -15,6 +15,7 @@ import { parse, split, yn, + isRelativeSpecifier, } from './util'; import { readConfig } from './configuration'; import type { TSCommon, TSInternal } from './ts-compiler-types'; @@ -117,6 +118,7 @@ export interface ProcessEnv { TS_NODE_SKIP_PROJECT?: string; TS_NODE_SKIP_IGNORE?: string; TS_NODE_PREFER_TS_EXTS?: string; + TS_NODE_TRY_TS_EXT?: string; TS_NODE_IGNORE_DIAGNOSTICS?: string; TS_NODE_TRANSPILE_ONLY?: string; TS_NODE_TYPE_CHECK?: string; @@ -353,6 +355,13 @@ export interface RegisterOptions extends CreateOptions { * @default false */ preferTsExts?: boolean; + + /** + * Attempt to resolve the typescript file when the js file cannot be found. + * + * @default false + */ + tryTsExt?: boolean } /** @@ -400,6 +409,7 @@ export const DEFAULTS: RegisterOptions = { skipProject: yn(env.TS_NODE_SKIP_PROJECT), skipIgnore: yn(env.TS_NODE_SKIP_IGNORE), preferTsExts: yn(env.TS_NODE_PREFER_TS_EXTS), + tryTsExt: yn(env.TS_NODE_TRY_TS_EXT), ignoreDiagnostics: split(env.TS_NODE_IGNORE_DIAGNOSTICS), transpileOnly: yn(env.TS_NODE_TRANSPILE_ONLY), typeCheck: yn(env.TS_NODE_TYPE_CHECK), @@ -477,6 +487,45 @@ export function getExtensions(config: _ts.ParsedCommandLine) { return { tsExtensions, jsExtensions }; } +function canDropJsExt(request: string, parentPath?: string) { + if (isRelativeSpecifier(request) && request.slice(-3) === '.js') { + if (!parentPath) return true + const paths = require.main?.paths || [] + for(let i = 0; i < paths.length; i++) { + if (parentPath.startsWith(paths[i])) { + return false + } + } + return true + } +} + + +function patchResolveFileName() { + const originalResolveFilename = (Module as any)._resolveFilename; + + (Module as any)._resolveFilename = function (...args: any[]) { + const [request, parent, isMain ] = args + if (isMain) { + return originalResolveFilename.apply(this, args); + } + if(canDropJsExt(request, parent?.path)) { + try { + return originalResolveFilename.call(this, request.slice(0, -3), ...args.slice(1)) + } catch(e) { + const mainFile = originalResolveFilename.apply(this, args); + if (mainFile.endsWith('.js')) { + //re-resolve with ts preference + //look-up tsconfig drop outDir?? + return originalResolveFilename.call(this, mainFile.slice(0, -3), ...args.slice(1)) + } + return mainFile + } + } + return originalResolveFilename.apply(this, args); + }; +} + /** * Register TypeScript compiler instance onto node.js */ @@ -497,6 +546,10 @@ export function register(opts: RegisterOptions = {}): Service { originalJsHandler ); + if (service.options.tryTsExt) { + patchResolveFileName(); + } + // Require specified modules before start-up. (Module as any)._preloadModules(service.options.require); @@ -548,6 +601,7 @@ export function create(rawOptions: CreateOptions = {}): Service { ...(tsNodeOptionsFromTsconfig.require || []), ...(rawOptions.require || []), ]; + options.preferTsExts = options.preferTsExts || options.tryTsExt // Experimental REPL await is not compatible targets lower than ES2018 const targetSupportsTla = config.options.target! >= ts.ScriptTarget.ES2018; diff --git a/src/util.ts b/src/util.ts index 9ea98a66d..829c2cce8 100644 --- a/src/util.ts +++ b/src/util.ts @@ -93,3 +93,18 @@ export function cachedLookup(fn: (arg: T) => R): (arg: T) => R { * `trace` options in hosts, I am using this placeholder. */ export function trace(s: string): void {} + +/** + * + * Determine if a specifier is relative (from node core) + * @internal + */ +export function isRelativeSpecifier(specifier: string) { + if (specifier[0] === '.') { + if (specifier.length === 1 || specifier[1] === '/') return true; + if (specifier[1] === '.') { + if (specifier.length === 2 || specifier[2] === '/') return true; + } + } + return false; +}