From 27bfc927d92d622621ba867b2ac22f8db1bd9c05 Mon Sep 17 00:00:00 2001 From: Andrew Bradley Date: Sun, 6 Feb 2022 15:08:24 -0500 Subject: [PATCH 1/6] tweak `swc` jsdoc to link to ts-node website, since it requires installing additional deps --- src/index.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/index.ts b/src/index.ts index ee542e733..d83176eff 100644 --- a/src/index.ts +++ b/src/index.ts @@ -265,6 +265,8 @@ export interface CreateOptions { * Transpile with swc instead of the TypeScript compiler, and skip typechecking. * * Equivalent to setting both `transpileOnly: true` and `transpiler: 'ts-node/transpilers/swc'` + * + * For complete instructions: https://typestrong.org/ts-node/docs/transpilers */ swc?: boolean; /** From 3398558e01b15c2ce8b14a12711bf89d24d1f467 Mon Sep 17 00:00:00 2001 From: Andrew Bradley Date: Sun, 6 Feb 2022 19:20:28 -0500 Subject: [PATCH 2/6] WIP --- src/configuration.ts | 12 +++++++--- src/index.ts | 47 ++++++++++++++++++++++----------------- src/resolver-functions.ts | 13 +++++------ src/transpilers/swc.ts | 10 ++++----- src/transpilers/types.ts | 3 ++- src/util.ts | 26 ++++++++++++++++++++++ 6 files changed, 73 insertions(+), 38 deletions(-) diff --git a/src/configuration.ts b/src/configuration.ts index b536926dd..e961265b1 100644 --- a/src/configuration.ts +++ b/src/configuration.ts @@ -10,7 +10,7 @@ import { import type { TSInternal } from './ts-compiler-types'; import { createTsInternals } from './ts-internals'; import { getDefaultTsconfigJsonForNodeVersion } from './tsconfigs'; -import { assign, createRequire } from './util'; +import { assign, createProjectLocalResolveHelper } from './util'; /** * TypeScript compiler option values required by `ts-node` which cannot be overridden. @@ -172,9 +172,9 @@ export function readConfig( // Some options are relative to the config file, so must be converted to absolute paths here if (options.require) { // Modules are found relative to the tsconfig file, not the `dir` option - const tsconfigRelativeRequire = createRequire(configPath); + const tsconfigRelativeResolver = createProjectLocalResolveHelper(configPath); options.require = options.require.map((path: string) => - tsconfigRelativeRequire.resolve(path) + tsconfigRelativeResolver(path, false) ); } if (options.scopeDir) { @@ -185,6 +185,12 @@ export function readConfig( if (options.moduleTypes) { optionBasePaths.moduleTypes = basePath; } + if (options.transpiler != null) { + optionBasePaths.transpiler = basePath; + } + if (options.compiler != null) { + optionBasePaths.compiler = basePath; + } assign(tsNodeOptionsFromTsconfig, options); } diff --git a/src/index.ts b/src/index.ts index d83176eff..738ad19db 100644 --- a/src/index.ts +++ b/src/index.ts @@ -12,8 +12,11 @@ import { assign, attemptRequireWithV8CompileCache, cachedLookup, + createProjectLocalResolveHelper, + getBasePathForProjectLocalDependencyResolution, normalizeSlashes, parse, + ProjectLocalResolveHelper, split, yn, } from './util'; @@ -373,6 +376,8 @@ type ModuleTypes = Record; /** @internal */ export interface OptionBasePaths { moduleTypes?: string; + transpiler?: string; + compiler?: string; } /** @@ -501,6 +506,8 @@ export interface Service { enableExperimentalEsmLoaderInterop(): void; /** @internal */ transpileOnly: boolean; + /** @internal */ + projectLocalResolveHelper: ProjectLocalResolveHelper; } /** @@ -589,17 +596,16 @@ export function create(rawOptions: CreateOptions = {}): Service { * be changed by the tsconfig, so we have to do this twice. */ function loadCompiler(name: string | undefined, relativeToPath: string) { - const compiler = require.resolve(name || 'typescript', { - paths: [relativeToPath, __dirname], - }); + const projectLocalResolveHelper = createProjectLocalResolveHelper(relativeToPath); + const compiler = projectLocalResolveHelper(name || 'typescript', true); const ts: typeof _ts = attemptRequireWithV8CompileCache(require, compiler); - return { compiler, ts }; + return { compiler, ts, projectLocalResolveHelper }; } // Compute minimum options to read the config file. - let { compiler, ts } = loadCompiler( + let { compiler, ts, projectLocalResolveHelper } = loadCompiler( compilerName, - rawOptions.projectSearchDir ?? rawOptions.project ?? cwd + getBasePathForProjectLocalDependencyResolution(undefined, rawOptions.projectSearchDir, rawOptions.project, cwd) ); // Read config file and merge new options between env and CLI options. @@ -617,6 +623,14 @@ export function create(rawOptions: CreateOptions = {}): Service { ...(rawOptions.require || []), ]; + // Re-load the compiler in case it has changed. + // Compiler is loaded relative to tsconfig.json, so tsconfig discovery may cause us to load a + // different compiler than we did above, even if the name has not changed. + if (configFilePath) { + ({ compiler, ts, projectLocalResolveHelper } = loadCompiler(options.compiler, + getBasePathForProjectLocalDependencyResolution(configFilePath, rawOptions.projectSearchDir, rawOptions.project, cwd))); + } + // Experimental REPL await is not compatible targets lower than ES2018 const targetSupportsTla = config.options.target! >= ts.ScriptTarget.ES2018; if (options.experimentalReplAwait === true && !targetSupportsTla) { @@ -637,13 +651,6 @@ export function create(rawOptions: CreateOptions = {}): Service { tsVersionSupportsTla && targetSupportsTla; - // Re-load the compiler in case it has changed. - // Compiler is loaded relative to tsconfig.json, so tsconfig discovery may cause us to load a - // different compiler than we did above, even if the name has not changed. - if (configFilePath) { - ({ compiler, ts } = loadCompiler(options.compiler, configFilePath)); - } - // swc implies two other options // typeCheck option was implemented specifically to allow overriding tsconfig transpileOnly from the command-line // So we should allow using typeCheck to override swc @@ -735,14 +742,11 @@ export function create(rawOptions: CreateOptions = {}): Service { typeof transpiler === 'string' ? transpiler : transpiler[0]; const transpilerOptions = typeof transpiler === 'string' ? {} : transpiler[1] ?? {}; - // TODO mimic fixed resolution logic from loadCompiler main - // TODO refactor into a more generic "resolve dep relative to project" helper - const transpilerPath = require.resolve(transpilerName, { - paths: [cwd, __dirname], - }); + // TODO mimic fixed resolution logic from loadCompiler main (I forget what this comment is talking about) + const transpilerPath = projectLocalResolveHelper(transpilerName, true); const transpilerFactory: TranspilerFactory = require(transpilerPath).create; customTranspiler = transpilerFactory({ - service: { options, config }, + service: { options, config, projectLocalResolveHelper }, ...transpilerOptions, }); } @@ -927,7 +931,7 @@ export function create(rawOptions: CreateOptions = {}): Service { ts, cwd, config, - configFilePath, + projectLocalResolveHelper, }); serviceHost.resolveModuleNames = resolveModuleNames; serviceHost.getResolvedModuleWithFailedLookupLocationsFromCache = @@ -1078,10 +1082,10 @@ export function create(rawOptions: CreateOptions = {}): Service { } = createResolverFunctions({ host, cwd, - configFilePath, config, ts, getCanonicalFileName, + projectLocalResolveHelper, }); host.resolveModuleNames = resolveModuleNames; host.resolveTypeReferenceDirectives = resolveTypeReferenceDirectives; @@ -1358,6 +1362,7 @@ export function create(rawOptions: CreateOptions = {}): Service { installSourceMapSupport, enableExperimentalEsmLoaderInterop, transpileOnly, + projectLocalResolveHelper, }; } diff --git a/src/resolver-functions.ts b/src/resolver-functions.ts index a624e6940..b525c20fc 100644 --- a/src/resolver-functions.ts +++ b/src/resolver-functions.ts @@ -1,5 +1,6 @@ import { resolve } from 'path'; import type * as _ts from 'typescript'; +import type { ProjectLocalResolveHelper } from './util'; /** * @internal @@ -11,9 +12,9 @@ export function createResolverFunctions(kwargs: { cwd: string; getCanonicalFileName: (filename: string) => string; config: _ts.ParsedCommandLine; - configFilePath: string | undefined; + projectLocalResolveHelper: ProjectLocalResolveHelper; }) { - const { host, ts, config, cwd, getCanonicalFileName, configFilePath } = + const { host, ts, config, cwd, getCanonicalFileName, projectLocalResolveHelper } = kwargs; const moduleResolutionCache = ts.createModuleResolutionCache( cwd, @@ -136,12 +137,8 @@ export function createResolverFunctions(kwargs: { // Resolve @types/node relative to project first, then __dirname (copy logic from elsewhere / refactor into reusable function) let typesNodePackageJsonPath: string | undefined; try { - typesNodePackageJsonPath = require.resolve( - '@types/node/package.json', - { - paths: [configFilePath ?? cwd, __dirname], - } - ); + // TODO unify this require.resolve with generic fallback helper? + typesNodePackageJsonPath = projectLocalResolveHelper('@types/node/package.json', true); } catch {} // gracefully do nothing when @types/node is not installed for any reason if (typesNodePackageJsonPath) { const typeRoots = [resolve(typesNodePackageJsonPath, '../..')]; diff --git a/src/transpilers/swc.ts b/src/transpilers/swc.ts index 3111bfc16..c0f28ef12 100644 --- a/src/transpilers/swc.ts +++ b/src/transpilers/swc.ts @@ -15,23 +15,23 @@ export interface SwcTranspilerOptions extends CreateTranspilerOptions { export function create(createOptions: SwcTranspilerOptions): Transpiler { const { swc, - service: { config }, + service: { config, projectLocalResolveHelper }, } = createOptions; // Load swc compiler let swcInstance: typeof swcWasm; if (typeof swc === 'string') { - swcInstance = require(swc) as typeof swcWasm; + swcInstance = require(projectLocalResolveHelper(swc, true)) as typeof swcWasm; } else if (swc == null) { let swcResolved; try { - swcResolved = require.resolve('@swc/core'); + swcResolved = projectLocalResolveHelper('@swc/core', true); } catch (e) { try { - swcResolved = require.resolve('@swc/wasm'); + swcResolved = projectLocalResolveHelper('@swc/wasm', true); } catch (e) { throw new Error( - 'swc compiler requires either @swc/core or @swc/wasm to be installed as dependencies' + 'swc compiler requires either @swc/core or @swc/wasm to be installed as a dependency. See https://typestrong.org/ts-node/docs/transpilers' ); } } diff --git a/src/transpilers/types.ts b/src/transpilers/types.ts index 3c1e7afc3..425005c38 100644 --- a/src/transpilers/types.ts +++ b/src/transpilers/types.ts @@ -16,7 +16,8 @@ export type TranspilerFactory = ( ) => Transpiler; export interface CreateTranspilerOptions { // TODO this is confusing because its only a partial Service. Rename? - service: Pick; + // Careful: must avoid stripInternal breakage by guarding with Extract<> + service: Pick>; } export interface Transpiler { // TODOs diff --git a/src/util.ts b/src/util.ts index 217dc6669..87217667f 100644 --- a/src/util.ts +++ b/src/util.ts @@ -113,3 +113,29 @@ export function attemptRequireWithV8CompileCache( return requireFn(specifier); } } + +/** + * Helper to discover dependencies relative to a user's project, optionally + * falling back to relative to ts-node. This supports global installations of + * ts-node, for example where someone does `#!/usr/bin/env -S ts-node --swc` and + * we need to fallback to a global install of @swc/core + * @internal + */ +export function createProjectLocalResolveHelper(localDirectory: string) { + return function projectLocalResolveHelper(specifier: string, fallbackToTsNodeRelative: boolean) { + return require.resolve(specifier, { + paths: fallbackToTsNodeRelative ? [localDirectory, __dirname] : [localDirectory] + }); + } +} +/** @internal */ +export type ProjectLocalResolveHelper = ReturnType; + +/** + * Used as a reminder of all the factors we must consider when finding project-local dependencies and when a config file + * on disk may or may not exist. + * @internal + */ +export function getBasePathForProjectLocalDependencyResolution(configFilePath: string | undefined, projectSearchDirOption: string | undefined, projectOption: string | undefined, cwdOption: string) { + return configFilePath ?? projectSearchDirOption ?? projectOption ?? cwdOption; +} From ae7690539364b3f8d8297b351d0a8a91cea7e218 Mon Sep 17 00:00:00 2001 From: Andrew Bradley Date: Sun, 6 Feb 2022 19:49:35 -0500 Subject: [PATCH 3/6] WIP --- src/index.ts | 1 - src/resolver-functions.ts | 1 - 2 files changed, 2 deletions(-) diff --git a/src/index.ts b/src/index.ts index 738ad19db..45006cc97 100644 --- a/src/index.ts +++ b/src/index.ts @@ -742,7 +742,6 @@ export function create(rawOptions: CreateOptions = {}): Service { typeof transpiler === 'string' ? transpiler : transpiler[0]; const transpilerOptions = typeof transpiler === 'string' ? {} : transpiler[1] ?? {}; - // TODO mimic fixed resolution logic from loadCompiler main (I forget what this comment is talking about) const transpilerPath = projectLocalResolveHelper(transpilerName, true); const transpilerFactory: TranspilerFactory = require(transpilerPath).create; customTranspiler = transpilerFactory({ diff --git a/src/resolver-functions.ts b/src/resolver-functions.ts index b525c20fc..f478646b1 100644 --- a/src/resolver-functions.ts +++ b/src/resolver-functions.ts @@ -137,7 +137,6 @@ export function createResolverFunctions(kwargs: { // Resolve @types/node relative to project first, then __dirname (copy logic from elsewhere / refactor into reusable function) let typesNodePackageJsonPath: string | undefined; try { - // TODO unify this require.resolve with generic fallback helper? typesNodePackageJsonPath = projectLocalResolveHelper('@types/node/package.json', true); } catch {} // gracefully do nothing when @types/node is not installed for any reason if (typesNodePackageJsonPath) { From d96591e59da4cd66d30d016ac5fe6c7d50174758 Mon Sep 17 00:00:00 2001 From: Andrew Bradley Date: Sun, 6 Feb 2022 19:55:58 -0500 Subject: [PATCH 4/6] lint-fix --- src/configuration.ts | 3 ++- src/index.ts | 21 +++++++++++++++++---- src/resolver-functions.ts | 15 ++++++++++++--- src/transpilers/swc.ts | 5 ++++- src/transpilers/types.ts | 5 ++++- src/util.ts | 22 +++++++++++++++++----- 6 files changed, 56 insertions(+), 15 deletions(-) diff --git a/src/configuration.ts b/src/configuration.ts index e961265b1..5e2b9927c 100644 --- a/src/configuration.ts +++ b/src/configuration.ts @@ -172,7 +172,8 @@ export function readConfig( // Some options are relative to the config file, so must be converted to absolute paths here if (options.require) { // Modules are found relative to the tsconfig file, not the `dir` option - const tsconfigRelativeResolver = createProjectLocalResolveHelper(configPath); + const tsconfigRelativeResolver = + createProjectLocalResolveHelper(configPath); options.require = options.require.map((path: string) => tsconfigRelativeResolver(path, false) ); diff --git a/src/index.ts b/src/index.ts index 45006cc97..6e0104020 100644 --- a/src/index.ts +++ b/src/index.ts @@ -596,7 +596,8 @@ export function create(rawOptions: CreateOptions = {}): Service { * be changed by the tsconfig, so we have to do this twice. */ function loadCompiler(name: string | undefined, relativeToPath: string) { - const projectLocalResolveHelper = createProjectLocalResolveHelper(relativeToPath); + const projectLocalResolveHelper = + createProjectLocalResolveHelper(relativeToPath); const compiler = projectLocalResolveHelper(name || 'typescript', true); const ts: typeof _ts = attemptRequireWithV8CompileCache(require, compiler); return { compiler, ts, projectLocalResolveHelper }; @@ -605,7 +606,12 @@ export function create(rawOptions: CreateOptions = {}): Service { // Compute minimum options to read the config file. let { compiler, ts, projectLocalResolveHelper } = loadCompiler( compilerName, - getBasePathForProjectLocalDependencyResolution(undefined, rawOptions.projectSearchDir, rawOptions.project, cwd) + getBasePathForProjectLocalDependencyResolution( + undefined, + rawOptions.projectSearchDir, + rawOptions.project, + cwd + ) ); // Read config file and merge new options between env and CLI options. @@ -627,8 +633,15 @@ export function create(rawOptions: CreateOptions = {}): Service { // Compiler is loaded relative to tsconfig.json, so tsconfig discovery may cause us to load a // different compiler than we did above, even if the name has not changed. if (configFilePath) { - ({ compiler, ts, projectLocalResolveHelper } = loadCompiler(options.compiler, - getBasePathForProjectLocalDependencyResolution(configFilePath, rawOptions.projectSearchDir, rawOptions.project, cwd))); + ({ compiler, ts, projectLocalResolveHelper } = loadCompiler( + options.compiler, + getBasePathForProjectLocalDependencyResolution( + configFilePath, + rawOptions.projectSearchDir, + rawOptions.project, + cwd + ) + )); } // Experimental REPL await is not compatible targets lower than ES2018 diff --git a/src/resolver-functions.ts b/src/resolver-functions.ts index f478646b1..f032bf0a8 100644 --- a/src/resolver-functions.ts +++ b/src/resolver-functions.ts @@ -14,8 +14,14 @@ export function createResolverFunctions(kwargs: { config: _ts.ParsedCommandLine; projectLocalResolveHelper: ProjectLocalResolveHelper; }) { - const { host, ts, config, cwd, getCanonicalFileName, projectLocalResolveHelper } = - kwargs; + const { + host, + ts, + config, + cwd, + getCanonicalFileName, + projectLocalResolveHelper, + } = kwargs; const moduleResolutionCache = ts.createModuleResolutionCache( cwd, getCanonicalFileName, @@ -137,7 +143,10 @@ export function createResolverFunctions(kwargs: { // Resolve @types/node relative to project first, then __dirname (copy logic from elsewhere / refactor into reusable function) let typesNodePackageJsonPath: string | undefined; try { - typesNodePackageJsonPath = projectLocalResolveHelper('@types/node/package.json', true); + typesNodePackageJsonPath = projectLocalResolveHelper( + '@types/node/package.json', + true + ); } catch {} // gracefully do nothing when @types/node is not installed for any reason if (typesNodePackageJsonPath) { const typeRoots = [resolve(typesNodePackageJsonPath, '../..')]; diff --git a/src/transpilers/swc.ts b/src/transpilers/swc.ts index c0f28ef12..fedc6a3af 100644 --- a/src/transpilers/swc.ts +++ b/src/transpilers/swc.ts @@ -21,7 +21,10 @@ export function create(createOptions: SwcTranspilerOptions): Transpiler { // Load swc compiler let swcInstance: typeof swcWasm; if (typeof swc === 'string') { - swcInstance = require(projectLocalResolveHelper(swc, true)) as typeof swcWasm; + swcInstance = require(projectLocalResolveHelper( + swc, + true + )) as typeof swcWasm; } else if (swc == null) { let swcResolved; try { diff --git a/src/transpilers/types.ts b/src/transpilers/types.ts index 425005c38..ab524cbdc 100644 --- a/src/transpilers/types.ts +++ b/src/transpilers/types.ts @@ -17,7 +17,10 @@ export type TranspilerFactory = ( export interface CreateTranspilerOptions { // TODO this is confusing because its only a partial Service. Rename? // Careful: must avoid stripInternal breakage by guarding with Extract<> - service: Pick>; + service: Pick< + Service, + Extract<'config' | 'options' | 'projectLocalResolveHelper', keyof Service> + >; } export interface Transpiler { // TODOs diff --git a/src/util.ts b/src/util.ts index 87217667f..ca8536d8a 100644 --- a/src/util.ts +++ b/src/util.ts @@ -122,20 +122,32 @@ export function attemptRequireWithV8CompileCache( * @internal */ export function createProjectLocalResolveHelper(localDirectory: string) { - return function projectLocalResolveHelper(specifier: string, fallbackToTsNodeRelative: boolean) { + return function projectLocalResolveHelper( + specifier: string, + fallbackToTsNodeRelative: boolean + ) { return require.resolve(specifier, { - paths: fallbackToTsNodeRelative ? [localDirectory, __dirname] : [localDirectory] + paths: fallbackToTsNodeRelative + ? [localDirectory, __dirname] + : [localDirectory], }); - } + }; } /** @internal */ -export type ProjectLocalResolveHelper = ReturnType; +export type ProjectLocalResolveHelper = ReturnType< + typeof createProjectLocalResolveHelper +>; /** * Used as a reminder of all the factors we must consider when finding project-local dependencies and when a config file * on disk may or may not exist. * @internal */ -export function getBasePathForProjectLocalDependencyResolution(configFilePath: string | undefined, projectSearchDirOption: string | undefined, projectOption: string | undefined, cwdOption: string) { +export function getBasePathForProjectLocalDependencyResolution( + configFilePath: string | undefined, + projectSearchDirOption: string | undefined, + projectOption: string | undefined, + cwdOption: string +) { return configFilePath ?? projectSearchDirOption ?? projectOption ?? cwdOption; } From e5ac0e82db2cd320de5da773fd0c495228029097 Mon Sep 17 00:00:00 2001 From: Andrew Bradley Date: Sun, 6 Feb 2022 20:18:02 -0500 Subject: [PATCH 5/6] fix --- src/configuration.ts | 2 +- src/util.ts | 7 ++++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/src/configuration.ts b/src/configuration.ts index 5e2b9927c..176d92ee3 100644 --- a/src/configuration.ts +++ b/src/configuration.ts @@ -173,7 +173,7 @@ export function readConfig( if (options.require) { // Modules are found relative to the tsconfig file, not the `dir` option const tsconfigRelativeResolver = - createProjectLocalResolveHelper(configPath); + createProjectLocalResolveHelper(dirname(configPath)); options.require = options.require.map((path: string) => tsconfigRelativeResolver(path, false) ); diff --git a/src/util.ts b/src/util.ts index ca8536d8a..bc79d6f8e 100644 --- a/src/util.ts +++ b/src/util.ts @@ -4,6 +4,7 @@ import { } from 'module'; import type _createRequire from 'create-require'; import * as ynModule from 'yn'; +import { dirname } from 'path'; /** @internal */ export const createRequire = @@ -149,5 +150,9 @@ export function getBasePathForProjectLocalDependencyResolution( projectOption: string | undefined, cwdOption: string ) { - return configFilePath ?? projectSearchDirOption ?? projectOption ?? cwdOption; + if(configFilePath != null) return dirname(configFilePath); + return projectSearchDirOption ?? projectOption ?? cwdOption; + // TODO technically breaks if projectOption is path to a file, not a directory, + // and we attempt to resolve relative specifiers. By the time we resolve relative specifiers, + // should have configFilePath, so not reach this codepath. } From 37b77dd9c731c4014551c2032e82785648a92b37 Mon Sep 17 00:00:00 2001 From: Andrew Bradley Date: Sun, 6 Feb 2022 20:23:38 -0500 Subject: [PATCH 6/6] lint-fix --- src/configuration.ts | 5 +++-- src/util.ts | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/configuration.ts b/src/configuration.ts index 176d92ee3..ff38ddd44 100644 --- a/src/configuration.ts +++ b/src/configuration.ts @@ -172,8 +172,9 @@ export function readConfig( // Some options are relative to the config file, so must be converted to absolute paths here if (options.require) { // Modules are found relative to the tsconfig file, not the `dir` option - const tsconfigRelativeResolver = - createProjectLocalResolveHelper(dirname(configPath)); + const tsconfigRelativeResolver = createProjectLocalResolveHelper( + dirname(configPath) + ); options.require = options.require.map((path: string) => tsconfigRelativeResolver(path, false) ); diff --git a/src/util.ts b/src/util.ts index bc79d6f8e..adc5eaca9 100644 --- a/src/util.ts +++ b/src/util.ts @@ -150,7 +150,7 @@ export function getBasePathForProjectLocalDependencyResolution( projectOption: string | undefined, cwdOption: string ) { - if(configFilePath != null) return dirname(configFilePath); + if (configFilePath != null) return dirname(configFilePath); return projectSearchDirOption ?? projectOption ?? cwdOption; // TODO technically breaks if projectOption is path to a file, not a directory, // and we attempt to resolve relative specifiers. By the time we resolve relative specifiers,