fix(misc): override customConditions when using an incompatible module resolution (#30477)

## Current Behavior

In a few different places (Crystal plugins, executors, generators,
helpers) where `ts-node` compiler options are overridden and
`moduleResolution` is being set to something other than `node16`,
`nodenext`, or `bundler`, an error can occur if the `customConditions`
compiler option is being used.

## Expected Behavior

When overriding the `ts-node` compiler options and changing forcing
`moduleResolution` to have a value that's incompatible with
`customConditions`, the latter should be unset (set to `null`) to avoid
errors.

## Related Issue(s)

Fixes #
This commit is contained in:
Leosvel Pérez Espinosa 2025-03-25 12:51:02 +01:00 committed by GitHub
parent 30781f7fe3
commit 5dbe040374
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 230 additions and 62 deletions

View File

@ -484,4 +484,164 @@ describe('convert-to-rspack', () => {
"
`);
});
it('should configure ts-node in the tsconfig.json file', async () => {
const tree = createTreeWithEmptyWorkspace();
addProjectConfiguration(tree, 'app', {
root: 'apps/app',
sourceRoot: 'apps/app/src',
projectType: 'application',
targets: {
build: {
executor: '@angular-devkit/build-angular:browser',
options: {
outputPath: 'dist/apps/app',
index: 'apps/app/src/index.html',
main: 'apps/app/src/main.ts',
polyfills: ['tslib'], // zone.js is not in nx repo's node_modules so simulating it with a package that is
tsConfig: 'apps/app/tsconfig.app.json',
assets: [
'apps/app/src/favicon.ico',
'apps/app/src/assets',
{ input: 'apps/app/public', glob: '**/*' },
],
styles: ['apps/app/src/styles.scss'],
scripts: [],
},
},
},
});
writeJson(tree, 'apps/app/tsconfig.json', {});
updateJson(tree, 'package.json', (json) => {
json.scripts ??= {};
json.scripts.build = 'nx build';
return json;
});
await convertToRspack(tree, { project: 'app', skipFormat: true });
expect(tree.read('apps/app/tsconfig.json', 'utf-8')).toMatchInlineSnapshot(`
"{
"ts-node": {
"compilerOptions": {
"module": "CommonJS",
"moduleResolution": "Node10"
}
}
}
"
`);
});
it('should configure ts-node in the tsconfig.json file to unset "customConditions" when it is defined in the root tsconfig.json file', async () => {
const tree = createTreeWithEmptyWorkspace();
updateJson(tree, 'tsconfig.base.json', (json) => {
json.compilerOptions ??= {};
json.compilerOptions.customConditions = ['development'];
return json;
});
addProjectConfiguration(tree, 'app', {
root: 'apps/app',
sourceRoot: 'apps/app/src',
projectType: 'application',
targets: {
build: {
executor: '@angular-devkit/build-angular:browser',
options: {
outputPath: 'dist/apps/app',
index: 'apps/app/src/index.html',
main: 'apps/app/src/main.ts',
polyfills: ['tslib'], // zone.js is not in nx repo's node_modules so simulating it with a package that is
tsConfig: 'apps/app/tsconfig.app.json',
assets: [
'apps/app/src/favicon.ico',
'apps/app/src/assets',
{ input: 'apps/app/public', glob: '**/*' },
],
styles: ['apps/app/src/styles.scss'],
scripts: [],
},
},
},
});
writeJson(tree, 'apps/app/tsconfig.json', {});
updateJson(tree, 'package.json', (json) => {
json.scripts ??= {};
json.scripts.build = 'nx build';
return json;
});
await convertToRspack(tree, { project: 'app', skipFormat: true });
expect(tree.read('apps/app/tsconfig.json', 'utf-8')).toMatchInlineSnapshot(`
"{
"ts-node": {
"compilerOptions": {
"module": "CommonJS",
"moduleResolution": "Node10",
"customConditions": null
}
}
}
"
`);
});
it('should configure ts-node in the tsconfig.json file to unset "customConditions" when it is defined in the project tsconfig.json file', async () => {
const tree = createTreeWithEmptyWorkspace();
addProjectConfiguration(tree, 'app', {
root: 'apps/app',
sourceRoot: 'apps/app/src',
projectType: 'application',
targets: {
build: {
executor: '@angular-devkit/build-angular:browser',
options: {
outputPath: 'dist/apps/app',
index: 'apps/app/src/index.html',
main: 'apps/app/src/main.ts',
polyfills: ['tslib'], // zone.js is not in nx repo's node_modules so simulating it with a package that is
tsConfig: 'apps/app/tsconfig.app.json',
assets: [
'apps/app/src/favicon.ico',
'apps/app/src/assets',
{ input: 'apps/app/public', glob: '**/*' },
],
styles: ['apps/app/src/styles.scss'],
scripts: [],
},
},
},
});
writeJson(tree, 'apps/app/tsconfig.json', {
compilerOptions: {
customConditions: ['development'],
},
});
updateJson(tree, 'package.json', (json) => {
json.scripts ??= {};
json.scripts.build = 'nx build';
return json;
});
await convertToRspack(tree, { project: 'app', skipFormat: true });
expect(tree.read('apps/app/tsconfig.json', 'utf-8')).toMatchInlineSnapshot(`
"{
"compilerOptions": {
"customConditions": [
"development"
]
},
"ts-node": {
"compilerOptions": {
"module": "CommonJS",
"moduleResolution": "Node10",
"customConditions": null
}
}
}
"
`);
});
});

View File

@ -1,12 +1,26 @@
import { joinPathFragments, readJson, Tree, writeJson } from '@nx/devkit';
import { getRootTsConfigFileName } from '@nx/js';
export function updateTsconfig(tree: Tree, projectRoot: string) {
const tsconfigPath = joinPathFragments(projectRoot, 'tsconfig.json');
const tsconfig = readJson(tree, tsconfigPath);
tsconfig['ts-node'] = {
compilerOptions: {
module: 'CommonJS',
},
};
tsconfig['ts-node'] ??= {};
tsconfig['ts-node'].compilerOptions ??= {};
tsconfig['ts-node'].compilerOptions.module = 'CommonJS';
tsconfig['ts-node'].compilerOptions.moduleResolution = 'Node10';
if (tsconfig.compilerOptions?.customConditions) {
tsconfig['ts-node'].compilerOptions.customConditions = null;
} else {
const rootTsconfigFile = getRootTsConfigFileName(tree);
if (rootTsconfigFile) {
const rootTsconfigJson = readJson(tree, rootTsconfigFile);
if (rootTsconfigJson.compilerOptions?.customConditions) {
tsconfig['ts-node'].compilerOptions.customConditions = null;
}
}
}
writeJson(tree, tsconfigPath, tsconfig);
}

View File

@ -23,7 +23,12 @@ export async function jestExecutor(
): Promise<{ success: boolean }> {
// Jest registers ts-node with module CJS https://github.com/SimenB/jest/blob/v29.6.4/packages/jest-config/src/readConfigFileAndSetRootDir.ts#L117-L119
// We want to support of ESM via 'module':'nodenext', we need to override the resolution until Jest supports it.
process.env.TS_NODE_COMPILER_OPTIONS ??= '{"moduleResolution":"node10"}';
const existingValue = process.env['TS_NODE_COMPILER_OPTIONS'];
process.env['TS_NODE_COMPILER_OPTIONS'] = JSON.stringify({
...(existingValue ? JSON.parse(existingValue) : {}),
moduleResolution: 'Node10',
customConditions: null,
});
const config = await parseJestConfig(options, context);

View File

@ -98,7 +98,7 @@ describe.each([true, false])('@nx/jest/plugin', (disableJestRuntime) => {
"options": {
"cwd": "proj",
"env": {
"TS_NODE_COMPILER_OPTIONS": "{"moduleResolution":"node10"}",
"TS_NODE_COMPILER_OPTIONS": "{"moduleResolution":"node10","customConditions":null}",
},
},
"outputs": [
@ -179,7 +179,7 @@ describe.each([true, false])('@nx/jest/plugin', (disableJestRuntime) => {
"options": {
"cwd": "proj",
"env": {
"TS_NODE_COMPILER_OPTIONS": "{"moduleResolution":"node10"}",
"TS_NODE_COMPILER_OPTIONS": "{"moduleResolution":"node10","customConditions":null}",
},
},
"outputs": [
@ -249,7 +249,7 @@ describe.each([true, false])('@nx/jest/plugin', (disableJestRuntime) => {
"options": {
"cwd": "proj",
"env": {
"TS_NODE_COMPILER_OPTIONS": "{"moduleResolution":"node10"}",
"TS_NODE_COMPILER_OPTIONS": "{"moduleResolution":"node10","customConditions":null}",
},
},
"outputs": [
@ -318,7 +318,7 @@ describe.each([true, false])('@nx/jest/plugin', (disableJestRuntime) => {
"options": {
"cwd": "proj",
"env": {
"TS_NODE_COMPILER_OPTIONS": "{"moduleResolution":"node10"}",
"TS_NODE_COMPILER_OPTIONS": "{"moduleResolution":"node10","customConditions":null}",
},
},
"outputs": [
@ -398,7 +398,7 @@ describe.each([true, false])('@nx/jest/plugin', (disableJestRuntime) => {
"options": {
"cwd": "proj",
"env": {
"TS_NODE_COMPILER_OPTIONS": "{"moduleResolution":"node10"}",
"TS_NODE_COMPILER_OPTIONS": "{"moduleResolution":"node10","customConditions":null}",
},
},
"outputs": [
@ -482,7 +482,7 @@ describe.each([true, false])('@nx/jest/plugin', (disableJestRuntime) => {
"options": {
"cwd": "proj",
"env": {
"TS_NODE_COMPILER_OPTIONS": "{"moduleResolution":"node10"}",
"TS_NODE_COMPILER_OPTIONS": "{"moduleResolution":"node10","customConditions":null}",
},
},
"outputs": [
@ -552,7 +552,7 @@ describe.each([true, false])('@nx/jest/plugin', (disableJestRuntime) => {
"options": {
"cwd": "proj",
"env": {
"TS_NODE_COMPILER_OPTIONS": "{"moduleResolution":"node10"}",
"TS_NODE_COMPILER_OPTIONS": "{"moduleResolution":"node10","customConditions":null}",
},
},
"outputs": [
@ -632,7 +632,7 @@ describe.each([true, false])('@nx/jest/plugin', (disableJestRuntime) => {
"options": {
"cwd": "proj",
"env": {
"TS_NODE_COMPILER_OPTIONS": "{"moduleResolution":"node10"}",
"TS_NODE_COMPILER_OPTIONS": "{"moduleResolution":"node10","customConditions":null}",
},
},
"outputs": [
@ -702,7 +702,7 @@ describe.each([true, false])('@nx/jest/plugin', (disableJestRuntime) => {
"options": {
"cwd": "proj",
"env": {
"TS_NODE_COMPILER_OPTIONS": "{"moduleResolution":"node10"}",
"TS_NODE_COMPILER_OPTIONS": "{"moduleResolution":"node10","customConditions":null}",
},
},
"outputs": [
@ -782,7 +782,7 @@ describe.each([true, false])('@nx/jest/plugin', (disableJestRuntime) => {
"options": {
"cwd": "proj",
"env": {
"TS_NODE_COMPILER_OPTIONS": "{"moduleResolution":"node10"}",
"TS_NODE_COMPILER_OPTIONS": "{"moduleResolution":"node10","customConditions":null}",
},
},
"outputs": [
@ -852,7 +852,7 @@ describe.each([true, false])('@nx/jest/plugin', (disableJestRuntime) => {
"options": {
"cwd": "proj",
"env": {
"TS_NODE_COMPILER_OPTIONS": "{"moduleResolution":"node10"}",
"TS_NODE_COMPILER_OPTIONS": "{"moduleResolution":"node10","customConditions":null}",
},
},
"outputs": [

View File

@ -253,13 +253,21 @@ async function buildJestTargets(
const targets: Record<string, TargetConfiguration> = {};
const namedInputs = getNamedInputs(projectRoot, context);
const existingTsNodeCompilerOptions = process.env['TS_NODE_COMPILER_OPTIONS'];
const tsNodeCompilerOptions = JSON.stringify({
...(existingTsNodeCompilerOptions
? JSON.parse(existingTsNodeCompilerOptions)
: {}),
moduleResolution: 'node10',
customConditions: null,
});
const target: TargetConfiguration = (targets[options.targetName] = {
command: 'jest',
options: {
cwd: projectRoot,
// Jest registers ts-node with module CJS https://github.com/SimenB/jest/blob/v29.6.4/packages/jest-config/src/readConfigFileAndSetRootDir.ts#L117-L119
// We want to support of ESM via 'module':'nodenext', we need to override the resolution until Jest supports it.
env: { TS_NODE_COMPILER_OPTIONS: '{"moduleResolution":"node10"}' },
env: { TS_NODE_COMPILER_OPTIONS: tsNodeCompilerOptions },
},
metadata: {
technologies: ['jest'],
@ -341,7 +349,7 @@ async function buildJestTargets(
outputs,
options: {
cwd: projectRoot,
env: { TS_NODE_COMPILER_OPTIONS: '{"moduleResolution":"node10"}' },
env: { TS_NODE_COMPILER_OPTIONS: tsNodeCompilerOptions },
},
metadata: {
technologies: ['jest'],
@ -481,9 +489,7 @@ async function buildJestTargets(
outputs,
options: {
cwd: projectRoot,
env: {
TS_NODE_COMPILER_OPTIONS: '{"moduleResolution":"node10"}',
},
env: { TS_NODE_COMPILER_OPTIONS: tsNodeCompilerOptions },
},
metadata: {
technologies: ['jest'],

View File

@ -60,8 +60,6 @@
"semver": "^7.5.3",
"source-map-support": "0.5.19",
"tinyglobby": "^0.2.12",
"ts-node": "10.9.1",
"tsconfig-paths": "^4.1.2",
"tslib": "^2.3.0"
},
"peerDependencies": {

View File

@ -1,21 +0,0 @@
export function tsNodeRegister(file: string, tsConfig?: string) {
if (!file?.endsWith('.ts')) return;
// Register TS compiler lazily
require('ts-node').register({
project: tsConfig,
compilerOptions: {
module: 'CommonJS',
types: ['node'],
},
});
if (!tsConfig) return;
// Register paths in tsConfig
const tsconfigPaths = require('tsconfig-paths');
const { absoluteBaseUrl: baseUrl, paths } =
tsconfigPaths.loadConfig(tsConfig);
if (baseUrl && paths) {
tsconfigPaths.register({ baseUrl, paths });
}
}

View File

@ -245,6 +245,7 @@ export function getTranspiler(
// use NodeJs module resolution until support for TS 4.x is dropped and then
// we can switch to Node10
compilerOptions.moduleResolution = ts.ModuleResolutionKind.NodeJs;
compilerOptions.customConditions = null;
compilerOptions.target = ts.ScriptTarget.ES2021;
compilerOptions.inlineSourceMap = true;
compilerOptions.skipLibCheck = true;

View File

@ -88,7 +88,7 @@ describe('@nx/rspack', () => {
],
"cwd": "my-app",
"env": {
"TS_NODE_COMPILER_OPTIONS": "{"moduleResolution":"Node10","module":"CommonJS"}",
"TS_NODE_COMPILER_OPTIONS": "{"moduleResolution":"Node10","module":"CommonJS","customConditions":null}",
},
},
"outputs": [],
@ -106,7 +106,7 @@ describe('@nx/rspack', () => {
],
"cwd": "my-app",
"env": {
"TS_NODE_COMPILER_OPTIONS": "{"moduleResolution":"Node10","module":"CommonJS"}",
"TS_NODE_COMPILER_OPTIONS": "{"moduleResolution":"Node10","module":"CommonJS","customConditions":null}",
},
},
},
@ -118,7 +118,7 @@ describe('@nx/rspack', () => {
],
"cwd": "my-app",
"env": {
"TS_NODE_COMPILER_OPTIONS": "{"moduleResolution":"Node10","module":"CommonJS"}",
"TS_NODE_COMPILER_OPTIONS": "{"moduleResolution":"Node10","module":"CommonJS","customConditions":null}",
},
},
},

View File

@ -187,6 +187,7 @@ async function createRspackTargets(
...(existingValue ? JSON.parse(existingValue) : {}),
module: 'CommonJS',
moduleResolution: 'Node10',
customConditions: null,
});
}

View File

@ -844,6 +844,7 @@ describe('@nx/storybook:configuration for Storybook v7', () => {
"ts-node": {
"compilerOptions": {
"module": "commonjs",
"moduleResolution": "node10",
},
},
}

View File

@ -1,4 +1,5 @@
import { updateJson, type Tree } from '@nx/devkit';
import { readJson, updateJson, type Tree } from '@nx/devkit';
import { getRootTsConfigFileName } from '@nx/js';
/**
* This is a temporary fix for Storybook to support TypeScript configuration files.
@ -12,21 +13,23 @@ export function editRootTsConfig(tree: Tree) {
}
updateJson(tree, 'tsconfig.json', (json) => {
if (json['ts-node']) {
json['ts-node'] = {
...json['ts-node'],
compilerOptions: {
...(json['ts-node'].compilerOptions ?? {}),
module: 'commonjs',
},
};
json['ts-node'] ??= {};
json['ts-node'].compilerOptions ??= {};
json['ts-node'].compilerOptions.module = 'commonjs';
json['ts-node'].compilerOptions.moduleResolution = 'node10';
if (json.compilerOptions?.customConditions) {
json['ts-node'].compilerOptions.customConditions = null;
} else {
json['ts-node'] = {
compilerOptions: {
module: 'commonjs',
},
};
const rootTsconfigFile = getRootTsConfigFileName(tree);
if (rootTsconfigFile) {
const rootTsconfigJson = readJson(tree, rootTsconfigFile);
if (rootTsconfigJson.compilerOptions?.customConditions) {
json['ts-node'].compilerOptions.customConditions = null;
}
}
}
return json;
});
}