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 { TsConfigOptions } from 'ts-node';
|
||||||
import type { CompilerOptions } from 'typescript';
|
import type { CompilerOptions } from 'typescript';
|
||||||
import { logger, NX_PREFIX, stripIndent } from '../../../utils/logger';
|
import { logger, NX_PREFIX, stripIndent } from '../../../utils/logger';
|
||||||
|
import { readTsConfigWithoutFiles } from './typescript';
|
||||||
|
|
||||||
const swcNodeInstalled = packageIsInstalled('@swc-node/register');
|
const swcNodeInstalled = packageIsInstalled('@swc-node/register');
|
||||||
const tsNodeInstalled = packageIsInstalled('ts-node/register');
|
const tsNodeInstalled = packageIsInstalled('ts-node/register');
|
||||||
@ -79,11 +80,11 @@ export function registerTsProject(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const tsConfigPath = configFilename ? join(path, configFilename) : path;
|
const tsConfigPath = configFilename ? join(path, configFilename) : path;
|
||||||
const compilerOptions: CompilerOptions = readCompilerOptions(tsConfigPath);
|
const { compilerOptions, tsConfigRaw } = readCompilerOptions(tsConfigPath);
|
||||||
|
|
||||||
const cleanupFunctions: ((...args: unknown[]) => unknown)[] = [
|
const cleanupFunctions: ((...args: unknown[]) => unknown)[] = [
|
||||||
registerTsConfigPaths(tsConfigPath),
|
registerTsConfigPaths(tsConfigPath),
|
||||||
registerTranspiler(compilerOptions),
|
registerTranspiler(compilerOptions, tsConfigRaw),
|
||||||
];
|
];
|
||||||
|
|
||||||
// Add ESM support for `.ts` files.
|
// Add ESM support for `.ts` files.
|
||||||
@ -128,13 +129,18 @@ export function getSwcTranspiler(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function getTsNodeTranspiler(
|
export function getTsNodeTranspiler(
|
||||||
compilerOptions: CompilerOptions
|
compilerOptions: CompilerOptions,
|
||||||
|
tsNodeOptions?: TsConfigOptions
|
||||||
): (...args: unknown[]) => unknown {
|
): (...args: unknown[]) => unknown {
|
||||||
const { register } = require('ts-node') as typeof import('ts-node');
|
const { register } = require('ts-node') as typeof import('ts-node');
|
||||||
// ts-node doesn't provide a cleanup method
|
// ts-node doesn't provide a cleanup method
|
||||||
const service = register({
|
const service = register({
|
||||||
|
...tsNodeOptions,
|
||||||
transpileOnly: true,
|
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
|
// we already read and provide the compiler options, so prevent ts-node from reading them again
|
||||||
skipProject: true,
|
skipProject: true,
|
||||||
});
|
});
|
||||||
@ -250,24 +256,36 @@ export function getTranspiler(
|
|||||||
compilerOptions.inlineSourceMap = true;
|
compilerOptions.inlineSourceMap = true;
|
||||||
compilerOptions.skipLibCheck = 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.
|
// Just return if transpiler was already registered before.
|
||||||
const registrationKey = JSON.stringify(compilerOptions);
|
|
||||||
const registrationEntry = registered.get(registrationKey);
|
const registrationEntry = registered.get(registrationKey);
|
||||||
if (registered.has(registrationKey)) {
|
if (registered.has(registrationKey)) {
|
||||||
registrationEntry.refCount++;
|
registrationEntry.refCount++;
|
||||||
return registrationEntry.cleanup;
|
return registrationEntry.cleanup;
|
||||||
}
|
}
|
||||||
|
|
||||||
const _getTranspiler =
|
|
||||||
swcNodeInstalled && !preferTsNode
|
|
||||||
? getSwcTranspiler
|
|
||||||
: tsNodeInstalled
|
|
||||||
? // We can fall back on ts-node if it's available
|
|
||||||
getTsNodeTranspiler
|
|
||||||
: undefined;
|
|
||||||
|
|
||||||
if (_getTranspiler) {
|
if (_getTranspiler) {
|
||||||
const transpilerCleanup = _getTranspiler(compilerOptions);
|
const transpilerCleanup = _getTranspiler(compilerOptions, tsNodeOptions);
|
||||||
const currRegistrationEntry = {
|
const currRegistrationEntry = {
|
||||||
refCount: 1,
|
refCount: 1,
|
||||||
cleanup: () => {
|
cleanup: () => {
|
||||||
@ -294,10 +312,11 @@ export function getTranspiler(
|
|||||||
* @returns cleanup method
|
* @returns cleanup method
|
||||||
*/
|
*/
|
||||||
export function registerTranspiler(
|
export function registerTranspiler(
|
||||||
compilerOptions: CompilerOptions
|
compilerOptions: CompilerOptions,
|
||||||
|
tsConfigRaw?: unknown
|
||||||
): () => void {
|
): () => void {
|
||||||
// Function to register transpiler that returns cleanup function
|
// Function to register transpiler that returns cleanup function
|
||||||
const transpiler = getTranspiler(compilerOptions);
|
const transpiler = getTranspiler(compilerOptions, tsConfigRaw);
|
||||||
|
|
||||||
if (!transpiler) {
|
if (!transpiler) {
|
||||||
warnNoTranspiler();
|
warnNoTranspiler();
|
||||||
@ -336,11 +355,16 @@ export function registerTsConfigPaths(tsConfigPath): () => void {
|
|||||||
throw new Error(`Unable to load ${tsConfigPath}`);
|
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';
|
const preferTsNode = process.env.NX_PREFER_TS_NODE === 'true';
|
||||||
|
|
||||||
if (swcNodeInstalled && !preferTsNode) {
|
if (swcNodeInstalled && !preferTsNode) {
|
||||||
return readCompilerOptionsWithSwc(tsConfigPath);
|
return {
|
||||||
|
compilerOptions: readCompilerOptionsWithSwc(tsConfigPath),
|
||||||
|
};
|
||||||
} else {
|
} else {
|
||||||
return readCompilerOptionsWithTypescript(tsConfigPath);
|
return readCompilerOptionsWithTypescript(tsConfigPath);
|
||||||
}
|
}
|
||||||
@ -359,20 +383,15 @@ function readCompilerOptionsWithSwc(tsConfigPath) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function readCompilerOptionsWithTypescript(tsConfigPath) {
|
function readCompilerOptionsWithTypescript(tsConfigPath) {
|
||||||
if (!ts) {
|
const { options, raw } = readTsConfigWithoutFiles(tsConfigPath);
|
||||||
ts = require('typescript');
|
|
||||||
}
|
|
||||||
const { readConfigFile, parseJsonConfigFileContent, sys } = ts;
|
|
||||||
const jsonContent = readConfigFile(tsConfigPath, sys.readFile);
|
|
||||||
const { options } = parseJsonConfigFileContent(
|
|
||||||
jsonContent.config,
|
|
||||||
sys,
|
|
||||||
dirname(tsConfigPath)
|
|
||||||
);
|
|
||||||
// This property is returned in compiler options for some reason, but not part of the typings.
|
// 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.
|
// ts-node fails on unknown props, so we have to remove it.
|
||||||
delete options.configFilePath;
|
delete options.configFilePath;
|
||||||
return options;
|
|
||||||
|
return {
|
||||||
|
compilerOptions: options,
|
||||||
|
tsConfigRaw: raw,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function loadTsConfigPaths(): typeof import('tsconfig-paths') | null {
|
function loadTsConfigPaths(): typeof import('tsconfig-paths') | null {
|
||||||
|
|||||||
@ -8,43 +8,45 @@ const normalizedAppRoot = workspaceRoot.replace(/\\/g, '/');
|
|||||||
|
|
||||||
let tsModule: typeof import('typescript');
|
let tsModule: typeof import('typescript');
|
||||||
|
|
||||||
export function readTsConfig(tsConfigPath: string) {
|
export function readTsConfig(
|
||||||
|
tsConfigPath: string,
|
||||||
|
sys?: ts.System
|
||||||
|
): ts.ParsedCommandLine {
|
||||||
if (!tsModule) {
|
if (!tsModule) {
|
||||||
tsModule = require('typescript');
|
tsModule = require('typescript');
|
||||||
}
|
}
|
||||||
const readResult = tsModule.readConfigFile(
|
|
||||||
tsConfigPath,
|
sys ??= tsModule.sys;
|
||||||
tsModule.sys.readFile
|
|
||||||
);
|
const readResult = tsModule.readConfigFile(tsConfigPath, sys.readFile);
|
||||||
return tsModule.parseJsonConfigFileContent(
|
return tsModule.parseJsonConfigFileContent(
|
||||||
readResult.config,
|
readResult.config,
|
||||||
tsModule.sys,
|
sys,
|
||||||
dirname(tsConfigPath)
|
dirname(tsConfigPath)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function readTsConfigOptions(tsConfigPath: string) {
|
export function readTsConfigWithoutFiles(
|
||||||
|
tsConfigPath: string
|
||||||
|
): ts.ParsedCommandLine {
|
||||||
if (!tsModule) {
|
if (!tsModule) {
|
||||||
tsModule = require('typescript');
|
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
|
// We only care about options, so we don't need to scan source files, and thus
|
||||||
// `readDirectory` is stubbed for performance.
|
// `readDirectory` is stubbed for performance.
|
||||||
const host = {
|
const sys = {
|
||||||
...tsModule.sys,
|
...tsModule.sys,
|
||||||
readDirectory: () => [],
|
readDirectory: () => [],
|
||||||
};
|
};
|
||||||
|
|
||||||
return tsModule.parseJsonConfigFileContent(
|
return readTsConfig(tsConfigPath, sys);
|
||||||
readResult.config,
|
}
|
||||||
host,
|
|
||||||
dirname(tsConfigPath)
|
export function readTsConfigOptions(tsConfigPath: string): ts.CompilerOptions {
|
||||||
).options;
|
const { options } = readTsConfigWithoutFiles(tsConfigPath);
|
||||||
|
|
||||||
|
return options;
|
||||||
}
|
}
|
||||||
|
|
||||||
let compilerHost: {
|
let compilerHost: {
|
||||||
|
|||||||
@ -1,12 +1,12 @@
|
|||||||
import { existsSync } from 'node:fs';
|
import { existsSync } from 'node:fs';
|
||||||
import { join } from 'node:path/posix';
|
import { join } from 'node:path/posix';
|
||||||
import { workspaceRoot } from '../../utils/workspace-root';
|
import type * as ts from 'typescript';
|
||||||
import {
|
import {
|
||||||
registerTranspiler,
|
registerTranspiler,
|
||||||
registerTsConfigPaths,
|
registerTsConfigPaths,
|
||||||
} from '../../plugins/js/utils/register';
|
} from '../../plugins/js/utils/register';
|
||||||
import { readTsConfigOptions } from '../../plugins/js/utils/typescript';
|
import { readTsConfigWithoutFiles } from '../../plugins/js/utils/typescript';
|
||||||
import type * as ts from 'typescript';
|
import { workspaceRoot } from '../../utils/workspace-root';
|
||||||
|
|
||||||
export let unregisterPluginTSTranspiler: (() => void) | null = null;
|
export let unregisterPluginTSTranspiler: (() => void) | null = null;
|
||||||
|
|
||||||
@ -25,16 +25,19 @@ export function registerPluginTSTranspiler() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const tsConfigOptions: Partial<ts.CompilerOptions> = tsConfigName
|
const tsConfig: Partial<ts.ParsedCommandLine> = tsConfigName
|
||||||
? readTsConfigOptions(tsConfigName)
|
? readTsConfigWithoutFiles(tsConfigName)
|
||||||
: {};
|
: {};
|
||||||
const cleanupFns = [
|
const cleanupFns = [
|
||||||
registerTsConfigPaths(tsConfigName),
|
registerTsConfigPaths(tsConfigName),
|
||||||
registerTranspiler({
|
registerTranspiler(
|
||||||
experimentalDecorators: true,
|
{
|
||||||
emitDecoratorMetadata: true,
|
experimentalDecorators: true,
|
||||||
...tsConfigOptions,
|
emitDecoratorMetadata: true,
|
||||||
}),
|
...tsConfig.options,
|
||||||
|
},
|
||||||
|
tsConfig.raw
|
||||||
|
),
|
||||||
];
|
];
|
||||||
unregisterPluginTSTranspiler = () => {
|
unregisterPluginTSTranspiler = () => {
|
||||||
cleanupFns.forEach((fn) => fn?.());
|
cleanupFns.forEach((fn) => fn?.());
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user