Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Resolve #1613: transpiler / swc things to double-check #1627

Merged
merged 6 commits into from Feb 7, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
14 changes: 11 additions & 3 deletions src/configuration.ts
Expand Up @@ -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.
Expand Down Expand Up @@ -172,9 +172,11 @@ 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(
dirname(configPath)
);
options.require = options.require.map((path: string) =>
tsconfigRelativeRequire.resolve(path)
tsconfigRelativeResolver(path, false)
);
}
if (options.scopeDir) {
Expand All @@ -185,6 +187,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);
}
Expand Down
61 changes: 40 additions & 21 deletions src/index.ts
Expand Up @@ -12,8 +12,11 @@ import {
assign,
attemptRequireWithV8CompileCache,
cachedLookup,
createProjectLocalResolveHelper,
getBasePathForProjectLocalDependencyResolution,
normalizeSlashes,
parse,
ProjectLocalResolveHelper,
split,
yn,
} from './util';
Expand Down Expand Up @@ -265,6 +268,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;
/**
Expand Down Expand Up @@ -371,6 +376,8 @@ type ModuleTypes = Record<string, 'cjs' | 'esm' | 'package'>;
/** @internal */
export interface OptionBasePaths {
moduleTypes?: string;
transpiler?: string;
compiler?: string;
}

/**
Expand Down Expand Up @@ -499,6 +506,8 @@ export interface Service {
enableExperimentalEsmLoaderInterop(): void;
/** @internal */
transpileOnly: boolean;
/** @internal */
projectLocalResolveHelper: ProjectLocalResolveHelper;
}

/**
Expand Down Expand Up @@ -587,17 +596,22 @@ 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.
Expand All @@ -615,6 +629,21 @@ 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) {
Expand All @@ -635,13 +664,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
Expand Down Expand Up @@ -733,14 +755,10 @@ 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],
});
const transpilerPath = projectLocalResolveHelper(transpilerName, true);
const transpilerFactory: TranspilerFactory = require(transpilerPath).create;
customTranspiler = transpilerFactory({
service: { options, config },
service: { options, config, projectLocalResolveHelper },
...transpilerOptions,
});
}
Expand Down Expand Up @@ -925,7 +943,7 @@ export function create(rawOptions: CreateOptions = {}): Service {
ts,
cwd,
config,
configFilePath,
projectLocalResolveHelper,
});
serviceHost.resolveModuleNames = resolveModuleNames;
serviceHost.getResolvedModuleWithFailedLookupLocationsFromCache =
Expand Down Expand Up @@ -1076,10 +1094,10 @@ export function create(rawOptions: CreateOptions = {}): Service {
} = createResolverFunctions({
host,
cwd,
configFilePath,
config,
ts,
getCanonicalFileName,
projectLocalResolveHelper,
});
host.resolveModuleNames = resolveModuleNames;
host.resolveTypeReferenceDirectives = resolveTypeReferenceDirectives;
Expand Down Expand Up @@ -1356,6 +1374,7 @@ export function create(rawOptions: CreateOptions = {}): Service {
installSourceMapSupport,
enableExperimentalEsmLoaderInterop,
transpileOnly,
projectLocalResolveHelper,
};
}

Expand Down
19 changes: 12 additions & 7 deletions src/resolver-functions.ts
@@ -1,5 +1,6 @@
import { resolve } from 'path';
import type * as _ts from 'typescript';
import type { ProjectLocalResolveHelper } from './util';

/**
* @internal
Expand All @@ -11,10 +12,16 @@ 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 } =
kwargs;
const {
host,
ts,
config,
cwd,
getCanonicalFileName,
projectLocalResolveHelper,
} = kwargs;
const moduleResolutionCache = ts.createModuleResolutionCache(
cwd,
getCanonicalFileName,
Expand Down Expand Up @@ -136,11 +143,9 @@ 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(
typesNodePackageJsonPath = projectLocalResolveHelper(
'@types/node/package.json',
{
paths: [configFilePath ?? cwd, __dirname],
}
true
);
} catch {} // gracefully do nothing when @types/node is not installed for any reason
if (typesNodePackageJsonPath) {
Expand Down
13 changes: 8 additions & 5 deletions src/transpilers/swc.ts
Expand Up @@ -15,23 +15,26 @@ 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'
);
}
}
Expand Down
6 changes: 5 additions & 1 deletion src/transpilers/types.ts
Expand Up @@ -16,7 +16,11 @@ export type TranspilerFactory = (
) => Transpiler;
export interface CreateTranspilerOptions {
// TODO this is confusing because its only a partial Service. Rename?
service: Pick<Service, 'config' | 'options'>;
// Careful: must avoid stripInternal breakage by guarding with Extract<>
service: Pick<
Service,
Extract<'config' | 'options' | 'projectLocalResolveHelper', keyof Service>
>;
}
export interface Transpiler {
// TODOs
Expand Down
43 changes: 43 additions & 0 deletions src/util.ts
Expand Up @@ -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 =
Expand Down Expand Up @@ -113,3 +114,45 @@ 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<
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
) {
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.
}