fix(core): use ts-node option from tsconfig files when creating transpiler (#31469)
## Current Behavior When creating a `ts-node` transpiler, only `compilerOptions` are provided. Because we instruct `ts-node` to skip reading the tsconfig (this was previously done to avoid some edge cases), other options in the tsconfig files are lost (e.g. `ts-node` specific options). This was previously reported at https://github.com/nrwl/nx/issues/21695 and fixed by https://github.com/nrwl/nx/pull/21723, but a rework at a later point caused a regression. ## Expected Behavior When creating a `ts-node` transpiler, we should provide `compilerOptions` and the `ts-node` options.
This commit is contained in:
parent
6fe9d297e2
commit
93234039c9
@ -1,7 +1,8 @@
|
||||
import { dirname, join, sep } from 'path';
|
||||
import { join, sep } from 'path';
|
||||
import type { TsConfigOptions } from 'ts-node';
|
||||
import type { CompilerOptions } from 'typescript';
|
||||
import { logger, NX_PREFIX, stripIndent } from '../../../utils/logger';
|
||||
import { readTsConfigWithoutFiles } from './typescript';
|
||||
|
||||
const swcNodeInstalled = packageIsInstalled('@swc-node/register');
|
||||
const tsNodeInstalled = packageIsInstalled('ts-node/register');
|
||||
@ -79,11 +80,11 @@ export function registerTsProject(
|
||||
}
|
||||
|
||||
const tsConfigPath = configFilename ? join(path, configFilename) : path;
|
||||
const compilerOptions: CompilerOptions = readCompilerOptions(tsConfigPath);
|
||||
const { compilerOptions, tsConfigRaw } = readCompilerOptions(tsConfigPath);
|
||||
|
||||
const cleanupFunctions: ((...args: unknown[]) => unknown)[] = [
|
||||
registerTsConfigPaths(tsConfigPath),
|
||||
registerTranspiler(compilerOptions),
|
||||
registerTranspiler(compilerOptions, tsConfigRaw),
|
||||
];
|
||||
|
||||
// Add ESM support for `.ts` files.
|
||||
@ -128,13 +129,18 @@ export function getSwcTranspiler(
|
||||
}
|
||||
|
||||
export function getTsNodeTranspiler(
|
||||
compilerOptions: CompilerOptions
|
||||
compilerOptions: CompilerOptions,
|
||||
tsNodeOptions?: TsConfigOptions
|
||||
): (...args: unknown[]) => unknown {
|
||||
const { register } = require('ts-node') as typeof import('ts-node');
|
||||
// ts-node doesn't provide a cleanup method
|
||||
const service = register({
|
||||
...tsNodeOptions,
|
||||
transpileOnly: true,
|
||||
compilerOptions: getTsNodeCompilerOptions(compilerOptions),
|
||||
compilerOptions: getTsNodeCompilerOptions({
|
||||
...tsNodeOptions?.compilerOptions,
|
||||
...compilerOptions,
|
||||
}),
|
||||
// we already read and provide the compiler options, so prevent ts-node from reading them again
|
||||
skipProject: true,
|
||||
});
|
||||
@ -250,24 +256,36 @@ export function getTranspiler(
|
||||
compilerOptions.inlineSourceMap = true;
|
||||
compilerOptions.skipLibCheck = true;
|
||||
|
||||
let _getTranspiler: (
|
||||
compilerOptions: CompilerOptions,
|
||||
tsNodeOptions?: TsConfigOptions
|
||||
) => (...args: unknown[]) => unknown;
|
||||
|
||||
let registrationKey = JSON.stringify(compilerOptions);
|
||||
let tsNodeOptions: TsConfigOptions | undefined;
|
||||
if (swcNodeInstalled && !preferTsNode) {
|
||||
_getTranspiler = getSwcTranspiler;
|
||||
} else if (tsNodeInstalled) {
|
||||
// We can fall back on ts-node if it's available
|
||||
_getTranspiler = getTsNodeTranspiler;
|
||||
tsNodeOptions = filterRecognizedTsConfigTsNodeOptions(
|
||||
tsConfigRaw?.['ts-node']
|
||||
).recognized;
|
||||
// include ts-node options in the registration key
|
||||
registrationKey += JSON.stringify(tsNodeOptions);
|
||||
} else {
|
||||
_getTranspiler = undefined;
|
||||
}
|
||||
|
||||
// Just return if transpiler was already registered before.
|
||||
const registrationKey = JSON.stringify(compilerOptions);
|
||||
const registrationEntry = registered.get(registrationKey);
|
||||
if (registered.has(registrationKey)) {
|
||||
registrationEntry.refCount++;
|
||||
return registrationEntry.cleanup;
|
||||
}
|
||||
|
||||
const _getTranspiler =
|
||||
swcNodeInstalled && !preferTsNode
|
||||
? getSwcTranspiler
|
||||
: tsNodeInstalled
|
||||
? // We can fall back on ts-node if it's available
|
||||
getTsNodeTranspiler
|
||||
: undefined;
|
||||
|
||||
if (_getTranspiler) {
|
||||
const transpilerCleanup = _getTranspiler(compilerOptions);
|
||||
const transpilerCleanup = _getTranspiler(compilerOptions, tsNodeOptions);
|
||||
const currRegistrationEntry = {
|
||||
refCount: 1,
|
||||
cleanup: () => {
|
||||
@ -294,10 +312,11 @@ export function getTranspiler(
|
||||
* @returns cleanup method
|
||||
*/
|
||||
export function registerTranspiler(
|
||||
compilerOptions: CompilerOptions
|
||||
compilerOptions: CompilerOptions,
|
||||
tsConfigRaw?: unknown
|
||||
): () => void {
|
||||
// Function to register transpiler that returns cleanup function
|
||||
const transpiler = getTranspiler(compilerOptions);
|
||||
const transpiler = getTranspiler(compilerOptions, tsConfigRaw);
|
||||
|
||||
if (!transpiler) {
|
||||
warnNoTranspiler();
|
||||
@ -336,11 +355,16 @@ export function registerTsConfigPaths(tsConfigPath): () => void {
|
||||
throw new Error(`Unable to load ${tsConfigPath}`);
|
||||
}
|
||||
|
||||
function readCompilerOptions(tsConfigPath): CompilerOptions {
|
||||
function readCompilerOptions(tsConfigPath): {
|
||||
compilerOptions: CompilerOptions;
|
||||
tsConfigRaw?: unknown;
|
||||
} {
|
||||
const preferTsNode = process.env.NX_PREFER_TS_NODE === 'true';
|
||||
|
||||
if (swcNodeInstalled && !preferTsNode) {
|
||||
return readCompilerOptionsWithSwc(tsConfigPath);
|
||||
return {
|
||||
compilerOptions: readCompilerOptionsWithSwc(tsConfigPath),
|
||||
};
|
||||
} else {
|
||||
return readCompilerOptionsWithTypescript(tsConfigPath);
|
||||
}
|
||||
@ -359,20 +383,15 @@ function readCompilerOptionsWithSwc(tsConfigPath) {
|
||||
}
|
||||
|
||||
function readCompilerOptionsWithTypescript(tsConfigPath) {
|
||||
if (!ts) {
|
||||
ts = require('typescript');
|
||||
}
|
||||
const { readConfigFile, parseJsonConfigFileContent, sys } = ts;
|
||||
const jsonContent = readConfigFile(tsConfigPath, sys.readFile);
|
||||
const { options } = parseJsonConfigFileContent(
|
||||
jsonContent.config,
|
||||
sys,
|
||||
dirname(tsConfigPath)
|
||||
);
|
||||
const { options, raw } = readTsConfigWithoutFiles(tsConfigPath);
|
||||
// This property is returned in compiler options for some reason, but not part of the typings.
|
||||
// ts-node fails on unknown props, so we have to remove it.
|
||||
delete options.configFilePath;
|
||||
return options;
|
||||
|
||||
return {
|
||||
compilerOptions: options,
|
||||
tsConfigRaw: raw,
|
||||
};
|
||||
}
|
||||
|
||||
function loadTsConfigPaths(): typeof import('tsconfig-paths') | null {
|
||||
|
||||
@ -8,43 +8,45 @@ const normalizedAppRoot = workspaceRoot.replace(/\\/g, '/');
|
||||
|
||||
let tsModule: typeof import('typescript');
|
||||
|
||||
export function readTsConfig(tsConfigPath: string) {
|
||||
export function readTsConfig(
|
||||
tsConfigPath: string,
|
||||
sys?: ts.System
|
||||
): ts.ParsedCommandLine {
|
||||
if (!tsModule) {
|
||||
tsModule = require('typescript');
|
||||
}
|
||||
const readResult = tsModule.readConfigFile(
|
||||
tsConfigPath,
|
||||
tsModule.sys.readFile
|
||||
);
|
||||
|
||||
sys ??= tsModule.sys;
|
||||
|
||||
const readResult = tsModule.readConfigFile(tsConfigPath, sys.readFile);
|
||||
return tsModule.parseJsonConfigFileContent(
|
||||
readResult.config,
|
||||
tsModule.sys,
|
||||
sys,
|
||||
dirname(tsConfigPath)
|
||||
);
|
||||
}
|
||||
|
||||
export function readTsConfigOptions(tsConfigPath: string) {
|
||||
export function readTsConfigWithoutFiles(
|
||||
tsConfigPath: string
|
||||
): ts.ParsedCommandLine {
|
||||
if (!tsModule) {
|
||||
tsModule = require('typescript');
|
||||
}
|
||||
|
||||
const readResult = tsModule.readConfigFile(
|
||||
tsConfigPath,
|
||||
tsModule.sys.readFile
|
||||
);
|
||||
|
||||
// We only care about options, so we don't need to scan source files, and thus
|
||||
// `readDirectory` is stubbed for performance.
|
||||
const host = {
|
||||
const sys = {
|
||||
...tsModule.sys,
|
||||
readDirectory: () => [],
|
||||
};
|
||||
|
||||
return tsModule.parseJsonConfigFileContent(
|
||||
readResult.config,
|
||||
host,
|
||||
dirname(tsConfigPath)
|
||||
).options;
|
||||
return readTsConfig(tsConfigPath, sys);
|
||||
}
|
||||
|
||||
export function readTsConfigOptions(tsConfigPath: string): ts.CompilerOptions {
|
||||
const { options } = readTsConfigWithoutFiles(tsConfigPath);
|
||||
|
||||
return options;
|
||||
}
|
||||
|
||||
let compilerHost: {
|
||||
|
||||
@ -1,12 +1,12 @@
|
||||
import { existsSync } from 'node:fs';
|
||||
import { join } from 'node:path/posix';
|
||||
import { workspaceRoot } from '../../utils/workspace-root';
|
||||
import type * as ts from 'typescript';
|
||||
import {
|
||||
registerTranspiler,
|
||||
registerTsConfigPaths,
|
||||
} from '../../plugins/js/utils/register';
|
||||
import { readTsConfigOptions } from '../../plugins/js/utils/typescript';
|
||||
import type * as ts from 'typescript';
|
||||
import { readTsConfigWithoutFiles } from '../../plugins/js/utils/typescript';
|
||||
import { workspaceRoot } from '../../utils/workspace-root';
|
||||
|
||||
export let unregisterPluginTSTranspiler: (() => void) | null = null;
|
||||
|
||||
@ -25,16 +25,19 @@ export function registerPluginTSTranspiler() {
|
||||
return;
|
||||
}
|
||||
|
||||
const tsConfigOptions: Partial<ts.CompilerOptions> = tsConfigName
|
||||
? readTsConfigOptions(tsConfigName)
|
||||
const tsConfig: Partial<ts.ParsedCommandLine> = tsConfigName
|
||||
? readTsConfigWithoutFiles(tsConfigName)
|
||||
: {};
|
||||
const cleanupFns = [
|
||||
registerTsConfigPaths(tsConfigName),
|
||||
registerTranspiler({
|
||||
experimentalDecorators: true,
|
||||
emitDecoratorMetadata: true,
|
||||
...tsConfigOptions,
|
||||
}),
|
||||
registerTranspiler(
|
||||
{
|
||||
experimentalDecorators: true,
|
||||
emitDecoratorMetadata: true,
|
||||
...tsConfig.options,
|
||||
},
|
||||
tsConfig.raw
|
||||
),
|
||||
];
|
||||
unregisterPluginTSTranspiler = () => {
|
||||
cleanupFns.forEach((fn) => fn?.());
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user