Skip to content

Commit

Permalink
Resolve #1613: transpiler / swc things to double-check (#1627)
Browse files Browse the repository at this point in the history
* tweak `swc` jsdoc to link to ts-node website, since it requires installing additional deps

* WIP

* WIP

* lint-fix

* fix

* lint-fix
  • Loading branch information
cspotcode committed Feb 7, 2022
1 parent 32aaffe commit 39e2392
Show file tree
Hide file tree
Showing 6 changed files with 119 additions and 37 deletions.
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.
}

0 comments on commit 39e2392

Please sign in to comment.