fix(js): Update typescript plugin check for buildable projects (#29431)

<!-- Please make sure you have read the submission guidelines before
posting an PR -->
<!--
https://github.com/nrwl/nx/blob/master/CONTRIBUTING.md#-submitting-a-pr
-->

<!-- Please make sure that your commit message follows our format -->
<!-- Example: `fix(nx): must begin with lowercase` -->

<!-- If this is a particularly complex change or feature addition, you
can request a dedicated Nx release for this pull request branch. Mention
someone from the Nx team or the `@nrwl/nx-pipelines-reviewers` and they
will confirm if the PR warrants its own release for testing purposes,
and generate it for you if appropriate. -->

## Current Behavior
<!-- This is the behavior we have today -->
Currently, we use `rootDir` to check if the project is buildable. This
might not be correct in the case where the transpiled files are inside
source.

## Expected Behavior
<!-- This is the behavior we should expect with the changes in this PR
-->
It should work the for projects as long as the main / exports files are
not source files.

## Related Issue(s)
<!-- Please link the issue being fixed so it gets closed when this is
merged. -->

Fixes #
This commit is contained in:
Nicholas Cunningham 2025-01-09 09:03:33 -07:00 committed by GitHub
parent 325b9f6471
commit a77e3ef083
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 101 additions and 98 deletions

View File

@ -1819,7 +1819,7 @@ describe(`Plugin: ${PLUGIN_NAME}`, () => {
// Sibling package.json
await applyFilesToTempFsAndContext(tempFs, context, {
'libs/my-lib/tsconfig.json': `{}`,
'libs/my-lib/tsconfig.lib.json': `{}`,
'libs/my-lib/tsconfig.lib.json': `{"compilerOptions": {"outDir": "dist"}}`,
'libs/my-lib/tsconfig.build.json': `{}`,
'libs/my-lib/package.json': `{}`,
});
@ -1867,7 +1867,7 @@ describe(`Plugin: ${PLUGIN_NAME}`, () => {
// Sibling package.json
await applyFilesToTempFsAndContext(tempFs, context, {
'libs/my-lib/tsconfig.json': `{}`,
'libs/my-lib/tsconfig.lib.json': `{}`,
'libs/my-lib/tsconfig.lib.json': `{"compilerOptions": {"outDir": "dist"}}`,
'libs/my-lib/tsconfig.build.json': `{}`,
'libs/my-lib/package.json': `{}`,
});
@ -1917,7 +1917,7 @@ describe(`Plugin: ${PLUGIN_NAME}`, () => {
// Sibling package.json
await applyFilesToTempFsAndContext(tempFs, context, {
'libs/my-lib/tsconfig.json': `{}`,
'libs/my-lib/tsconfig.lib.json': `{"compilerOptions": {"rootDir": "src"}}`,
'libs/my-lib/tsconfig.lib.json': `{"compilerOptions": {"outDir": "dist"}}`,
'libs/my-lib/tsconfig.build.json': `{}`,
'libs/my-lib/package.json': `{"main": "dist/index.js"}`,
});
@ -1965,7 +1965,9 @@ describe(`Plugin: ${PLUGIN_NAME}`, () => {
"options": {
"cwd": "libs/my-lib",
},
"outputs": [],
"outputs": [
"{projectRoot}/dist",
],
"syncGenerators": [
"@nx/js:typescript-sync",
],
@ -1978,8 +1980,8 @@ describe(`Plugin: ${PLUGIN_NAME}`, () => {
// Sibling project.json
await applyFilesToTempFsAndContext(tempFs, context, {
'libs/my-lib/tsconfig.json': `{"compilerOptions": {"rootDir": "src"}}`,
'libs/my-lib/tsconfig.lib.json': `{"compilerOptions": {"rootDir": "src"}}`,
'libs/my-lib/tsconfig.json': `{}`,
'libs/my-lib/tsconfig.lib.json': `{"compilerOptions": {"outDir": "dist"}}`,
'libs/my-lib/tsconfig.build.json': `{}`,
'libs/my-lib/project.json': `{}`,
});
@ -2027,7 +2029,9 @@ describe(`Plugin: ${PLUGIN_NAME}`, () => {
"options": {
"cwd": "libs/my-lib",
},
"outputs": [],
"outputs": [
"{projectRoot}/dist",
],
"syncGenerators": [
"@nx/js:typescript-sync",
],
@ -2043,7 +2047,7 @@ describe(`Plugin: ${PLUGIN_NAME}`, () => {
// Sibling package.json
await applyFilesToTempFsAndContext(tempFs, context, {
'libs/my-lib/tsconfig.json': `{}`,
'libs/my-lib/tsconfig.lib.json': `{"compilerOptions": {"rootDir": "src"}}`,
'libs/my-lib/tsconfig.lib.json': `{"compilerOptions": {"outDir": "dist"}}`,
'libs/my-lib/tsconfig.build.json': `{}`,
'libs/my-lib/package.json': `{ "main": "dist/index.js" }`,
});
@ -2093,7 +2097,9 @@ describe(`Plugin: ${PLUGIN_NAME}`, () => {
"options": {
"cwd": "libs/my-lib",
},
"outputs": [],
"outputs": [
"{projectRoot}/dist",
],
"syncGenerators": [
"@nx/js:typescript-sync",
],
@ -2110,7 +2116,7 @@ describe(`Plugin: ${PLUGIN_NAME}`, () => {
await applyFilesToTempFsAndContext(tempFs, context, {
'libs/my-lib/tsconfig.json': `{}`,
'libs/my-lib/tsconfig.lib.json': `{}`,
'libs/my-lib/tsconfig.build.json': `{"compilerOptions": {"rootDir": "src"}}`,
'libs/my-lib/tsconfig.build.json': `{"compilerOptions": {"outDir": "dist"}}`,
'libs/my-lib/project.json': `{}`,
});
expect(
@ -2159,7 +2165,9 @@ describe(`Plugin: ${PLUGIN_NAME}`, () => {
"options": {
"cwd": "libs/my-lib",
},
"outputs": [],
"outputs": [
"{projectRoot}/dist",
],
"syncGenerators": [
"@nx/js:typescript-sync",
],
@ -2193,7 +2201,7 @@ describe(`Plugin: ${PLUGIN_NAME}`, () => {
await applyFilesToTempFsAndContext(tempFs, context, {
'libs/my-lib/tsconfig.lib.json': JSON.stringify({
compilerOptions: {
rootDir: 'src',
outDir: 'dist',
},
include: ['src/**/*.ts'],
exclude: ['src/**/*.spec.ts'],
@ -2247,7 +2255,9 @@ describe(`Plugin: ${PLUGIN_NAME}`, () => {
"options": {
"cwd": "libs/my-lib",
},
"outputs": [],
"outputs": [
"{projectRoot}/dist",
],
"syncGenerators": [
"@nx/js:typescript-sync",
],
@ -2272,7 +2282,7 @@ describe(`Plugin: ${PLUGIN_NAME}`, () => {
extends: '../../tsconfig.foo.json',
include: ['src/**/*.ts'],
compilerOptions: {
rootDir: 'src',
outDir: 'dist',
},
}),
'libs/my-lib/package.json': `{"main": "dist/index.js"}`,
@ -2326,7 +2336,9 @@ describe(`Plugin: ${PLUGIN_NAME}`, () => {
"options": {
"cwd": "libs/my-lib",
},
"outputs": [],
"outputs": [
"{projectRoot}/dist",
],
"syncGenerators": [
"@nx/js:typescript-sync",
],
@ -2351,7 +2363,7 @@ describe(`Plugin: ${PLUGIN_NAME}`, () => {
'libs/my-lib/tsconfig.lib.json': JSON.stringify({
extends: '../../tsconfig.foo.json',
compilerOptions: {
rootDir: 'src',
outDir: 'dist',
},
include: ['src/**/*.ts'],
}),
@ -2412,7 +2424,9 @@ describe(`Plugin: ${PLUGIN_NAME}`, () => {
"options": {
"cwd": "libs/my-lib",
},
"outputs": [],
"outputs": [
"{projectRoot}/dist",
],
"syncGenerators": [
"@nx/js:typescript-sync",
],
@ -2428,7 +2442,7 @@ describe(`Plugin: ${PLUGIN_NAME}`, () => {
await applyFilesToTempFsAndContext(tempFs, context, {
'libs/my-lib/tsconfig.json': '{}',
'libs/my-lib/tsconfig.lib.json': JSON.stringify({
compilerOptions: { rootDir: 'src' },
compilerOptions: { outDir: 'dist' },
include: ['src/**/*.ts'],
exclude: ['src/**/foo.ts'], // should be ignored because a referenced internal project includes this same pattern
references: [
@ -2494,7 +2508,9 @@ describe(`Plugin: ${PLUGIN_NAME}`, () => {
"options": {
"cwd": "libs/my-lib",
},
"outputs": [],
"outputs": [
"{projectRoot}/dist",
],
"syncGenerators": [
"@nx/js:typescript-sync",
],
@ -2511,7 +2527,7 @@ describe(`Plugin: ${PLUGIN_NAME}`, () => {
await applyFilesToTempFsAndContext(tempFs, context, {
'libs/my-lib/tsconfig.json': '{}',
'libs/my-lib/tsconfig.lib.json': JSON.stringify({
compilerOptions: { rootDir: 'src' }, // rootDir is required to determine if the project is buildable
compilerOptions: { outDir: 'dist' }, // outDir is required to determine if the project is buildable
include: ['src/**/*.ts'],
exclude: ['src/**/foo.ts'], // should be ignored
references: [{ path: './tsconfig.other.json' }],
@ -2548,7 +2564,7 @@ describe(`Plugin: ${PLUGIN_NAME}`, () => {
await applyFilesToTempFsAndContext(tempFs, context, {
'libs/my-lib/tsconfig.json': '{}',
'libs/my-lib/tsconfig.lib.json': JSON.stringify({
compilerOptions: { rootDir: 'src' },
compilerOptions: { outDir: 'dist' },
include: ['**/*.ts'],
exclude: ['**/foo.ts'], // should be ignored
references: [{ path: './tsconfig.other.json' }],
@ -2584,7 +2600,7 @@ describe(`Plugin: ${PLUGIN_NAME}`, () => {
await applyFilesToTempFsAndContext(tempFs, context, {
'libs/my-lib/tsconfig.json': '{}',
'libs/my-lib/tsconfig.lib.json': JSON.stringify({
compilerOptions: { rootDir: 'src' }, // rooDir is required to determine if the project is buildable
compilerOptions: { outDir: 'dist' }, // outDir is required to determine if the project is buildable
include: ['src/**/*.ts'],
exclude: ['src/**/foo.ts'], // should be ignored
references: [{ path: './tsconfig.other.json' }],
@ -2620,7 +2636,7 @@ describe(`Plugin: ${PLUGIN_NAME}`, () => {
await applyFilesToTempFsAndContext(tempFs, context, {
'libs/my-lib/tsconfig.json': '{}',
'libs/my-lib/tsconfig.lib.json': JSON.stringify({
compilerOptions: { rootDir: 'src' },
compilerOptions: { outDir: 'dist' },
include: ['src/**/*.ts'],
exclude: ['src/**/foo.ts'], // should be ignored
references: [{ path: './tsconfig.other.json' }],
@ -2656,7 +2672,7 @@ describe(`Plugin: ${PLUGIN_NAME}`, () => {
await applyFilesToTempFsAndContext(tempFs, context, {
'libs/my-lib/tsconfig.json': '{}',
'libs/my-lib/tsconfig.lib.json': JSON.stringify({
compilerOptions: { rootDir: 'src' },
compilerOptions: { outDir: 'dist' },
include: ['src/**/*.ts'],
exclude: [
'src/**/foo.ts', // should be ignored
@ -2696,7 +2712,7 @@ describe(`Plugin: ${PLUGIN_NAME}`, () => {
it('should fall back to named inputs when not using include', async () => {
await applyFilesToTempFsAndContext(tempFs, context, {
'libs/my-lib/tsconfig.lib.json': JSON.stringify({
compilerOptions: { rootDir: 'src' },
compilerOptions: { outDir: 'dist' },
files: ['main.ts'],
}),
'libs/my-lib/tsconfig.json': `{}`,
@ -2746,19 +2762,7 @@ describe(`Plugin: ${PLUGIN_NAME}`, () => {
"cwd": "libs/my-lib",
},
"outputs": [
"{projectRoot}/**/*.js",
"{projectRoot}/**/*.cjs",
"{projectRoot}/**/*.mjs",
"{projectRoot}/**/*.jsx",
"{projectRoot}/**/*.js.map",
"{projectRoot}/**/*.jsx.map",
"{projectRoot}/**/*.d.ts",
"{projectRoot}/**/*.d.cts",
"{projectRoot}/**/*.d.mts",
"{projectRoot}/**/*.d.ts.map",
"{projectRoot}/**/*.d.cts.map",
"{projectRoot}/**/*.d.mts.map",
"{projectRoot}/tsconfig.lib.tsbuildinfo",
"{projectRoot}/dist",
],
"syncGenerators": [
"@nx/js:typescript-sync",
@ -2778,12 +2782,11 @@ describe(`Plugin: ${PLUGIN_NAME}`, () => {
'libs/my-lib/tsconfig.lib.json': JSON.stringify({
compilerOptions: {
outFile: '../../dist/libs/my-lib/index.js',
rootDir: 'src',
},
files: ['main.ts'],
}),
'libs/my-lib/tsconfig.json': `{}`,
'libs/my-lib/package.json': `{"main": "dist/libs/my-lib/index.js"}`,
'libs/my-lib/package.json': `{}`,
});
expect(
await invokeCreateNodesOnMatchingFiles(context, {
@ -2851,12 +2854,11 @@ describe(`Plugin: ${PLUGIN_NAME}`, () => {
'libs/my-lib/tsconfig.lib.json': JSON.stringify({
compilerOptions: {
outDir: '../../dist/libs/my-lib',
rootDir: 'src',
},
files: ['main.ts'],
}),
'libs/my-lib/tsconfig.json': `{}`,
'libs/my-lib/package.json': `{"main": "dist/libs/my-lib/index.js"}`,
'libs/my-lib/package.json': `{"main": "../../dist/libs/my-lib/index.js"}`,
});
expect(
await invokeCreateNodesOnMatchingFiles(context, {
@ -2919,7 +2921,6 @@ describe(`Plugin: ${PLUGIN_NAME}`, () => {
it('should add the inline output files when `outDir` is not defined', async () => {
await applyFilesToTempFsAndContext(tempFs, context, {
'libs/my-lib/tsconfig.lib.json': JSON.stringify({
compilerOptions: { rootDir: 'src' },
files: ['main.ts'],
}),
'libs/my-lib/tsconfig.json': `{}`,
@ -3000,7 +3001,6 @@ describe(`Plugin: ${PLUGIN_NAME}`, () => {
'libs/my-lib/tsconfig.lib.json': JSON.stringify({
compilerOptions: {
outFile: '../../dist/libs/my-lib/lib.js',
rootDir: 'src',
},
files: ['main.ts'],
references: [{ path: './tsconfig.other.json' }],
@ -3008,11 +3008,10 @@ describe(`Plugin: ${PLUGIN_NAME}`, () => {
'libs/my-lib/tsconfig.other.json': JSON.stringify({
compilerOptions: {
outDir: '../../dist/libs/my-lib/other',
rootDir: 'src',
},
include: ['other/**/*.ts'],
}),
'libs/my-lib/package.json': `{"main": "dist/libs/my-lib/lib.js"}`,
'libs/my-lib/package.json': `{"main": "../../dist/libs/my-lib/lib.js"}`,
});
expect(
await invokeCreateNodesOnMatchingFiles(context, {
@ -3087,12 +3086,11 @@ describe(`Plugin: ${PLUGIN_NAME}`, () => {
'libs/my-lib/tsconfig.lib.json': JSON.stringify({
compilerOptions: {
outFile: '../../dist/libs/my-lib/index.js',
rootDir: 'src',
tsBuildInfoFile: '../../dist/libs/my-lib/my-lib.tsbuildinfo',
},
files: ['main.ts'],
}),
'libs/my-lib/package.json': `{"main": "dist/libs/my-lib/index.js"}`,
'libs/my-lib/package.json': `{"main": "../../dist/libs/my-lib/index.js"}`,
});
expect(
await invokeCreateNodesOnMatchingFiles(context, {
@ -3159,7 +3157,6 @@ describe(`Plugin: ${PLUGIN_NAME}`, () => {
'libs/my-lib/tsconfig.json': '{}',
'libs/my-lib/tsconfig.lib.json': JSON.stringify({
compilerOptions: {
rootDir: 'src',
tsBuildInfoFile: '../../dist/libs/my-lib/my-lib.tsbuildinfo',
},
files: ['main.ts'],
@ -3239,7 +3236,6 @@ describe(`Plugin: ${PLUGIN_NAME}`, () => {
await applyFilesToTempFsAndContext(tempFs, context, {
'libs/my-lib/tsconfig.lib.json': JSON.stringify({
compilerOptions: {
rootDir: 'src',
outDir: 'dist',
tsBuildInfoFile: 'my-lib.tsbuildinfo',
},
@ -3312,7 +3308,6 @@ describe(`Plugin: ${PLUGIN_NAME}`, () => {
'libs/my-lib/tsconfig.lib.json': JSON.stringify({
compilerOptions: {
outDir: 'dist',
rootDir: 'src',
tsBuildInfoFile: 'dist/my-lib.tsbuildinfo',
},
files: ['main.ts'],

View File

@ -22,6 +22,7 @@ import { existsSync, readdirSync, statSync } from 'node:fs';
import {
basename,
dirname,
extname,
join,
normalize,
relative,
@ -210,6 +211,11 @@ async function createNodesInternal(
projectRoot
);
const packageJsonPath = joinPathFragments(projectRoot, 'package.json');
const packageJson = existsSync(packageJsonPath)
? readJsonFile(packageJsonPath)
: null;
const nodeHash = hashArray([
...[
fullConfigPath,
@ -219,6 +225,7 @@ async function createNodesInternal(
join(context.workspaceRoot, lockFileName),
].map(hashFile),
hashObject(options),
...(packageJson ? [hashObject(packageJson)] : []),
]);
const cacheKey = `${nodeHash}_${configFilePath}`;
@ -325,12 +332,7 @@ function buildTscTargets(
if (
options.build &&
basename(configFilePath) === options.build.configName &&
isValidPackageJsonBuildConfig(
tsConfig,
context.workspaceRoot,
projectRoot,
configFilePath
)
isValidPackageJsonBuildConfig(tsConfig, context.workspaceRoot, projectRoot)
) {
internalProjectReferences ??= resolveInternalProjectReferences(
tsConfig,
@ -618,21 +620,22 @@ function getOutputs(
}
/**
* Checks whether a `package.json` file has a valid build configuration by ensuring
* that the `main`, `module`, or `exports` do not include paths from the `rootDir`.
* Or if `outFile` is defined, it should not be within the `rootDir`.
* Validates the build configuration of a `package.json` file by ensuring that paths in the `exports`, `module`,
* and `main` fields reference valid output paths within the `outDir` defined in the TypeScript configuration.
* Priority is given to the `exports` field, specifically the `.` export if defined. If `exports` is not defined,
* the function falls back to validating `main` and `module` fields. If `outFile` is specified, it validates that the file
* is located within the output directory.
* If no `package.json` file exists, it assumes the configuration is valid.
*
* @param tsConfig The TypeScript configuration object.
* @param workspaceRoot The workspace root path.
* @param projectRoot The project root path.
* @param tsConfigPath The path to the TypeScript configuration file.
* @returns `true` if the package has a valid build configuration; otherwise, `false`.
*/
function isValidPackageJsonBuildConfig(
tsConfig,
workspaceRoot: string,
projectRoot: string,
tsConfigPath: string
projectRoot: string
): boolean {
if (!existsSync(joinPathFragments(projectRoot, 'package.json'))) {
// If the package.json file does not exist.
@ -643,42 +646,30 @@ function isValidPackageJsonBuildConfig(
joinPathFragments(projectRoot, 'package.json')
);
const rootDir = tsConfig.options.rootDir ?? 'src/';
if (!tsConfig.options.rootDir) {
console.warn(
`The 'rootDir' option is not set in the tsconfig file at ${tsConfigPath}. Assuming 'src/' as the root directory.`
);
}
const outDir = tsConfig.options.outFile
? dirname(tsConfig.options.outFile)
: tsConfig.options.outDir;
const resolvedOutDir = outDir
? resolve(workspaceRoot, projectRoot, outDir)
: undefined;
const isPathWithinSrc = (path: string): boolean => {
const resolvedRootDir = resolve(workspaceRoot, projectRoot, rootDir);
const pathToCheck = resolve(workspaceRoot, projectRoot, path);
const isPathSourceFile = (path: string): boolean => {
if (resolvedOutDir) {
const pathToCheck = resolve(workspaceRoot, projectRoot, path);
return !pathToCheck.startsWith(resolvedOutDir);
}
return pathToCheck.startsWith(resolvedRootDir);
const ext = extname(path);
// Check that the file extension is a TS file extension. As the source files are in the same directory as the output files.
return ['.ts', '.tsx', '.cts', '.mts'].includes(ext);
};
// If `outFile` is defined, check the validity of the path.
if (tsConfig.options.outFile) {
if (isPathWithinSrc(tsConfig.options.outFile)) {
return false;
}
}
const buildPaths = ['main', 'module'];
for (const field of buildPaths) {
if (packageJson[field] && isPathWithinSrc(packageJson[field])) {
return false;
}
}
const exports = packageJson?.exports;
// Checks if the value is a path within the `src` directory.
const containsInvalidPath = (
value: string | Record<string, string>
): boolean => {
if (typeof value === 'string') {
return isPathWithinSrc(value);
return isPathSourceFile(value);
} else if (typeof value === 'object') {
return Object.entries(value).some(([currentKey, subValue]) => {
// Skip types field
@ -686,7 +677,7 @@ function isValidPackageJsonBuildConfig(
return false;
}
if (typeof subValue === 'string') {
return isPathWithinSrc(subValue);
return isPathSourceFile(subValue);
}
return false;
});
@ -694,17 +685,34 @@ function isValidPackageJsonBuildConfig(
return false;
};
if (typeof exports === 'string' && isPathWithinSrc(exports)) {
return false;
}
const exports = packageJson?.exports;
// Check nested exports if `exports` is an object.
if (typeof exports === 'object') {
for (const key in exports) {
if (containsInvalidPath(exports[key])) {
// Check the `.` export if `exports` is defined.
if (exports) {
if (typeof exports === 'string') {
return !isPathSourceFile(exports);
} else if (typeof exports === 'object' && '.' in exports) {
if (containsInvalidPath(exports['.'])) {
return false;
}
}
// Check other exports if `.` is not defined or valid.
for (const key in exports) {
if (key !== '.' && containsInvalidPath(exports[key])) {
return false;
}
}
return true;
}
// If `exports` is not defined, fallback to `main` and `module` fields.
const buildPaths = ['main', 'module'];
for (const field of buildPaths) {
if (packageJson[field] && isPathSourceFile(packageJson[field])) {
return false;
}
}
return true;