Skip to content

Commit 39e2392

Browse files
authored
Resolve #1613: transpiler / swc things to double-check (#1627)
* tweak `swc` jsdoc to link to ts-node website, since it requires installing additional deps * WIP * WIP * lint-fix * fix * lint-fix
1 parent 32aaffe commit 39e2392

File tree

6 files changed

+119
-37
lines changed

6 files changed

+119
-37
lines changed

src/configuration.ts

+11-3
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import {
1010
import type { TSInternal } from './ts-compiler-types';
1111
import { createTsInternals } from './ts-internals';
1212
import { getDefaultTsconfigJsonForNodeVersion } from './tsconfigs';
13-
import { assign, createRequire } from './util';
13+
import { assign, createProjectLocalResolveHelper } from './util';
1414

1515
/**
1616
* TypeScript compiler option values required by `ts-node` which cannot be overridden.
@@ -172,9 +172,11 @@ export function readConfig(
172172
// Some options are relative to the config file, so must be converted to absolute paths here
173173
if (options.require) {
174174
// Modules are found relative to the tsconfig file, not the `dir` option
175-
const tsconfigRelativeRequire = createRequire(configPath);
175+
const tsconfigRelativeResolver = createProjectLocalResolveHelper(
176+
dirname(configPath)
177+
);
176178
options.require = options.require.map((path: string) =>
177-
tsconfigRelativeRequire.resolve(path)
179+
tsconfigRelativeResolver(path, false)
178180
);
179181
}
180182
if (options.scopeDir) {
@@ -185,6 +187,12 @@ export function readConfig(
185187
if (options.moduleTypes) {
186188
optionBasePaths.moduleTypes = basePath;
187189
}
190+
if (options.transpiler != null) {
191+
optionBasePaths.transpiler = basePath;
192+
}
193+
if (options.compiler != null) {
194+
optionBasePaths.compiler = basePath;
195+
}
188196

189197
assign(tsNodeOptionsFromTsconfig, options);
190198
}

src/index.ts

+40-21
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,11 @@ import {
1212
assign,
1313
attemptRequireWithV8CompileCache,
1414
cachedLookup,
15+
createProjectLocalResolveHelper,
16+
getBasePathForProjectLocalDependencyResolution,
1517
normalizeSlashes,
1618
parse,
19+
ProjectLocalResolveHelper,
1720
split,
1821
yn,
1922
} from './util';
@@ -265,6 +268,8 @@ export interface CreateOptions {
265268
* Transpile with swc instead of the TypeScript compiler, and skip typechecking.
266269
*
267270
* Equivalent to setting both `transpileOnly: true` and `transpiler: 'ts-node/transpilers/swc'`
271+
*
272+
* For complete instructions: https://typestrong.org/ts-node/docs/transpilers
268273
*/
269274
swc?: boolean;
270275
/**
@@ -371,6 +376,8 @@ type ModuleTypes = Record<string, 'cjs' | 'esm' | 'package'>;
371376
/** @internal */
372377
export interface OptionBasePaths {
373378
moduleTypes?: string;
379+
transpiler?: string;
380+
compiler?: string;
374381
}
375382

376383
/**
@@ -499,6 +506,8 @@ export interface Service {
499506
enableExperimentalEsmLoaderInterop(): void;
500507
/** @internal */
501508
transpileOnly: boolean;
509+
/** @internal */
510+
projectLocalResolveHelper: ProjectLocalResolveHelper;
502511
}
503512

504513
/**
@@ -587,17 +596,22 @@ export function create(rawOptions: CreateOptions = {}): Service {
587596
* be changed by the tsconfig, so we have to do this twice.
588597
*/
589598
function loadCompiler(name: string | undefined, relativeToPath: string) {
590-
const compiler = require.resolve(name || 'typescript', {
591-
paths: [relativeToPath, __dirname],
592-
});
599+
const projectLocalResolveHelper =
600+
createProjectLocalResolveHelper(relativeToPath);
601+
const compiler = projectLocalResolveHelper(name || 'typescript', true);
593602
const ts: typeof _ts = attemptRequireWithV8CompileCache(require, compiler);
594-
return { compiler, ts };
603+
return { compiler, ts, projectLocalResolveHelper };
595604
}
596605

597606
// Compute minimum options to read the config file.
598-
let { compiler, ts } = loadCompiler(
607+
let { compiler, ts, projectLocalResolveHelper } = loadCompiler(
599608
compilerName,
600-
rawOptions.projectSearchDir ?? rawOptions.project ?? cwd
609+
getBasePathForProjectLocalDependencyResolution(
610+
undefined,
611+
rawOptions.projectSearchDir,
612+
rawOptions.project,
613+
cwd
614+
)
601615
);
602616

603617
// Read config file and merge new options between env and CLI options.
@@ -615,6 +629,21 @@ export function create(rawOptions: CreateOptions = {}): Service {
615629
...(rawOptions.require || []),
616630
];
617631

632+
// Re-load the compiler in case it has changed.
633+
// Compiler is loaded relative to tsconfig.json, so tsconfig discovery may cause us to load a
634+
// different compiler than we did above, even if the name has not changed.
635+
if (configFilePath) {
636+
({ compiler, ts, projectLocalResolveHelper } = loadCompiler(
637+
options.compiler,
638+
getBasePathForProjectLocalDependencyResolution(
639+
configFilePath,
640+
rawOptions.projectSearchDir,
641+
rawOptions.project,
642+
cwd
643+
)
644+
));
645+
}
646+
618647
// Experimental REPL await is not compatible targets lower than ES2018
619648
const targetSupportsTla = config.options.target! >= ts.ScriptTarget.ES2018;
620649
if (options.experimentalReplAwait === true && !targetSupportsTla) {
@@ -635,13 +664,6 @@ export function create(rawOptions: CreateOptions = {}): Service {
635664
tsVersionSupportsTla &&
636665
targetSupportsTla;
637666

638-
// Re-load the compiler in case it has changed.
639-
// Compiler is loaded relative to tsconfig.json, so tsconfig discovery may cause us to load a
640-
// different compiler than we did above, even if the name has not changed.
641-
if (configFilePath) {
642-
({ compiler, ts } = loadCompiler(options.compiler, configFilePath));
643-
}
644-
645667
// swc implies two other options
646668
// typeCheck option was implemented specifically to allow overriding tsconfig transpileOnly from the command-line
647669
// So we should allow using typeCheck to override swc
@@ -733,14 +755,10 @@ export function create(rawOptions: CreateOptions = {}): Service {
733755
typeof transpiler === 'string' ? transpiler : transpiler[0];
734756
const transpilerOptions =
735757
typeof transpiler === 'string' ? {} : transpiler[1] ?? {};
736-
// TODO mimic fixed resolution logic from loadCompiler main
737-
// TODO refactor into a more generic "resolve dep relative to project" helper
738-
const transpilerPath = require.resolve(transpilerName, {
739-
paths: [cwd, __dirname],
740-
});
758+
const transpilerPath = projectLocalResolveHelper(transpilerName, true);
741759
const transpilerFactory: TranspilerFactory = require(transpilerPath).create;
742760
customTranspiler = transpilerFactory({
743-
service: { options, config },
761+
service: { options, config, projectLocalResolveHelper },
744762
...transpilerOptions,
745763
});
746764
}
@@ -925,7 +943,7 @@ export function create(rawOptions: CreateOptions = {}): Service {
925943
ts,
926944
cwd,
927945
config,
928-
configFilePath,
946+
projectLocalResolveHelper,
929947
});
930948
serviceHost.resolveModuleNames = resolveModuleNames;
931949
serviceHost.getResolvedModuleWithFailedLookupLocationsFromCache =
@@ -1076,10 +1094,10 @@ export function create(rawOptions: CreateOptions = {}): Service {
10761094
} = createResolverFunctions({
10771095
host,
10781096
cwd,
1079-
configFilePath,
10801097
config,
10811098
ts,
10821099
getCanonicalFileName,
1100+
projectLocalResolveHelper,
10831101
});
10841102
host.resolveModuleNames = resolveModuleNames;
10851103
host.resolveTypeReferenceDirectives = resolveTypeReferenceDirectives;
@@ -1356,6 +1374,7 @@ export function create(rawOptions: CreateOptions = {}): Service {
13561374
installSourceMapSupport,
13571375
enableExperimentalEsmLoaderInterop,
13581376
transpileOnly,
1377+
projectLocalResolveHelper,
13591378
};
13601379
}
13611380

src/resolver-functions.ts

+12-7
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { resolve } from 'path';
22
import type * as _ts from 'typescript';
3+
import type { ProjectLocalResolveHelper } from './util';
34

45
/**
56
* @internal
@@ -11,10 +12,16 @@ export function createResolverFunctions(kwargs: {
1112
cwd: string;
1213
getCanonicalFileName: (filename: string) => string;
1314
config: _ts.ParsedCommandLine;
14-
configFilePath: string | undefined;
15+
projectLocalResolveHelper: ProjectLocalResolveHelper;
1516
}) {
16-
const { host, ts, config, cwd, getCanonicalFileName, configFilePath } =
17-
kwargs;
17+
const {
18+
host,
19+
ts,
20+
config,
21+
cwd,
22+
getCanonicalFileName,
23+
projectLocalResolveHelper,
24+
} = kwargs;
1825
const moduleResolutionCache = ts.createModuleResolutionCache(
1926
cwd,
2027
getCanonicalFileName,
@@ -136,11 +143,9 @@ export function createResolverFunctions(kwargs: {
136143
// Resolve @types/node relative to project first, then __dirname (copy logic from elsewhere / refactor into reusable function)
137144
let typesNodePackageJsonPath: string | undefined;
138145
try {
139-
typesNodePackageJsonPath = require.resolve(
146+
typesNodePackageJsonPath = projectLocalResolveHelper(
140147
'@types/node/package.json',
141-
{
142-
paths: [configFilePath ?? cwd, __dirname],
143-
}
148+
true
144149
);
145150
} catch {} // gracefully do nothing when @types/node is not installed for any reason
146151
if (typesNodePackageJsonPath) {

src/transpilers/swc.ts

+8-5
Original file line numberDiff line numberDiff line change
@@ -15,23 +15,26 @@ export interface SwcTranspilerOptions extends CreateTranspilerOptions {
1515
export function create(createOptions: SwcTranspilerOptions): Transpiler {
1616
const {
1717
swc,
18-
service: { config },
18+
service: { config, projectLocalResolveHelper },
1919
} = createOptions;
2020

2121
// Load swc compiler
2222
let swcInstance: typeof swcWasm;
2323
if (typeof swc === 'string') {
24-
swcInstance = require(swc) as typeof swcWasm;
24+
swcInstance = require(projectLocalResolveHelper(
25+
swc,
26+
true
27+
)) as typeof swcWasm;
2528
} else if (swc == null) {
2629
let swcResolved;
2730
try {
28-
swcResolved = require.resolve('@swc/core');
31+
swcResolved = projectLocalResolveHelper('@swc/core', true);
2932
} catch (e) {
3033
try {
31-
swcResolved = require.resolve('@swc/wasm');
34+
swcResolved = projectLocalResolveHelper('@swc/wasm', true);
3235
} catch (e) {
3336
throw new Error(
34-
'swc compiler requires either @swc/core or @swc/wasm to be installed as dependencies'
37+
'swc compiler requires either @swc/core or @swc/wasm to be installed as a dependency. See https://typestrong.org/ts-node/docs/transpilers'
3538
);
3639
}
3740
}

src/transpilers/types.ts

+5-1
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,11 @@ export type TranspilerFactory = (
1616
) => Transpiler;
1717
export interface CreateTranspilerOptions {
1818
// TODO this is confusing because its only a partial Service. Rename?
19-
service: Pick<Service, 'config' | 'options'>;
19+
// Careful: must avoid stripInternal breakage by guarding with Extract<>
20+
service: Pick<
21+
Service,
22+
Extract<'config' | 'options' | 'projectLocalResolveHelper', keyof Service>
23+
>;
2024
}
2125
export interface Transpiler {
2226
// TODOs

src/util.ts

+43
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {
44
} from 'module';
55
import type _createRequire from 'create-require';
66
import * as ynModule from 'yn';
7+
import { dirname } from 'path';
78

89
/** @internal */
910
export const createRequire =
@@ -113,3 +114,45 @@ export function attemptRequireWithV8CompileCache(
113114
return requireFn(specifier);
114115
}
115116
}
117+
118+
/**
119+
* Helper to discover dependencies relative to a user's project, optionally
120+
* falling back to relative to ts-node. This supports global installations of
121+
* ts-node, for example where someone does `#!/usr/bin/env -S ts-node --swc` and
122+
* we need to fallback to a global install of @swc/core
123+
* @internal
124+
*/
125+
export function createProjectLocalResolveHelper(localDirectory: string) {
126+
return function projectLocalResolveHelper(
127+
specifier: string,
128+
fallbackToTsNodeRelative: boolean
129+
) {
130+
return require.resolve(specifier, {
131+
paths: fallbackToTsNodeRelative
132+
? [localDirectory, __dirname]
133+
: [localDirectory],
134+
});
135+
};
136+
}
137+
/** @internal */
138+
export type ProjectLocalResolveHelper = ReturnType<
139+
typeof createProjectLocalResolveHelper
140+
>;
141+
142+
/**
143+
* Used as a reminder of all the factors we must consider when finding project-local dependencies and when a config file
144+
* on disk may or may not exist.
145+
* @internal
146+
*/
147+
export function getBasePathForProjectLocalDependencyResolution(
148+
configFilePath: string | undefined,
149+
projectSearchDirOption: string | undefined,
150+
projectOption: string | undefined,
151+
cwdOption: string
152+
) {
153+
if (configFilePath != null) return dirname(configFilePath);
154+
return projectSearchDirOption ?? projectOption ?? cwdOption;
155+
// TODO technically breaks if projectOption is path to a file, not a directory,
156+
// and we attempt to resolve relative specifiers. By the time we resolve relative specifiers,
157+
// should have configFilePath, so not reach this codepath.
158+
}

0 commit comments

Comments
 (0)