feat(core): nx-plugin-checks accounts for outDir and rootDir of projects when checking file existence (#29391)

For Nx plugins that use the the new TS solution setup, we need to
account for `generators.json`, `executors.json`, and `migrations.json`
pointing to `dist` rather than source.

This PR adds two options, `rootDir` and `outDir`, that allows the lint
rule to check the source files rather than depend on build artifacts.
The defaults are what we generate our plugins with.


<!-- 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 -->

## Expected Behavior
<!-- This is the behavior we should expect with the changes in this PR
-->

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

Fixes #
This commit is contained in:
Jack Hsu 2024-12-18 10:05:55 -05:00 committed by GitHub
parent 6bb0c2e5f0
commit 0720f3f9b4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 73 additions and 26 deletions

View File

@ -69,7 +69,7 @@ describe('Nx Plugin (TS solution)', () => {
const executor = uniq('executor');
const generatedProject = uniq('project');
runCLI(`generate @nx/plugin:plugin packages/${plugin}`);
runCLI(`generate @nx/plugin:plugin packages/${plugin} --linter eslint`);
runCLI(
`generate @nx/plugin:generator --name ${generator} --path packages/${plugin}/src/generators/${generator}/generator`
@ -97,6 +97,7 @@ describe('Nx Plugin (TS solution)', () => {
expect(() => checkFilesExist(`libs/${generatedProject}`)).not.toThrow();
expect(() => runCLI(`execute ${generatedProject}`)).not.toThrow();
expect(() => runCLI(`lint ${generatedProject}`)).not.toThrow();
});
it('should be able to resolve local generators and executors using package.json development condition export', async () => {

View File

@ -9,7 +9,6 @@ import {
} from '@nx/devkit';
import { getRootTsConfigPath } from '@nx/js';
import { registerTsProject } from '@nx/js/src/internal';
import { existsSync } from 'fs';
import * as path from 'path';
import { valid } from 'semver';
import { readProjectGraph } from '../utils/project-graph-utils';
@ -26,15 +25,22 @@ type Options = [
migrationsJson?: string;
packageJson?: string;
allowedVersionStrings: string[];
tsConfig?: string;
}
];
type NormalizedOptions = Options[0] & {
rootDir?: string;
outDir?: string;
};
const DEFAULT_OPTIONS: Options[0] = {
generatorsJson: 'generators.json',
executorsJson: 'executors.json',
migrationsJson: 'migrations.json',
packageJson: 'package.json',
allowedVersionStrings: ['*', 'latest', 'next'],
tsConfig: 'tsconfig.lib.json',
};
export type MessageIds =
@ -88,6 +94,11 @@ export default ESLintUtils.RuleCreator(() => ``)<Options, MessageIds>({
'A list of specifiers that are valid for versions within package group. Defaults to ["*", "latest", "next"]',
items: { type: 'string' },
},
tsConfig: {
type: 'string',
description:
'The path to the tsconfig file used to build the plugin. Defaults to "tsconfig.lib.json".',
},
},
additionalProperties: false,
},
@ -159,11 +170,11 @@ export default ESLintUtils.RuleCreator(() => ``)<Options, MessageIds>({
node: AST.JSONObjectExpression
) {
if (sourceFilePath === generatorsJson) {
checkCollectionFileNode(node, 'generator', context);
checkCollectionFileNode(node, 'generator', context, options);
} else if (sourceFilePath === migrationsJson) {
checkCollectionFileNode(node, 'migration', context);
checkCollectionFileNode(node, 'migration', context, options);
} else if (sourceFilePath === executorsJson) {
checkCollectionFileNode(node, 'executor', context);
checkCollectionFileNode(node, 'executor', context, options);
} else if (sourceFilePath === packageJson) {
validatePackageGroup(node, context);
}
@ -175,8 +186,21 @@ export default ESLintUtils.RuleCreator(() => ``)<Options, MessageIds>({
function normalizeOptions(
sourceProject: ProjectGraphProjectNode,
options: Options[0]
): Options[0] {
): NormalizedOptions {
let rootDir: string;
let outDir: string;
const base = { ...DEFAULT_OPTIONS, ...options };
let runtimeTsConfig: string;
try {
runtimeTsConfig = require.resolve(
path.join(sourceProject.data.root, base.tsConfig)
);
const tsConfig = readJsonFile(runtimeTsConfig);
rootDir = tsConfig.compilerOptions?.rootDir;
outDir = tsConfig.compilerOptions?.outDir;
} catch {
// nothing
}
const pathPrefix =
sourceProject.data.root !== '.' ? `${sourceProject.data.root}/` : '';
return {
@ -193,13 +217,16 @@ function normalizeOptions(
packageJson: base.packageJson
? `${pathPrefix}${base.packageJson}`
: undefined,
rootDir: rootDir ? path.join(sourceProject.data.root, rootDir) : undefined,
outDir: outDir ? path.join(sourceProject.data.root, outDir) : undefined,
};
}
export function checkCollectionFileNode(
baseNode: AST.JSONObjectExpression,
mode: 'migration' | 'generator' | 'executor',
context: TSESLint.RuleContext<MessageIds, Options>
context: TSESLint.RuleContext<MessageIds, Options>,
options: NormalizedOptions
) {
const schematicsRootNode = baseNode.properties.find(
(x) => x.key.type === 'JSONLiteral' && x.key.value === 'schematics'
@ -246,7 +273,7 @@ export function checkCollectionFileNode(
node: schematicsRootNode as any,
});
} else {
checkCollectionNode(collectionNode.value, mode, context);
checkCollectionNode(collectionNode.value, mode, context, options);
}
}
}
@ -254,7 +281,8 @@ export function checkCollectionFileNode(
export function checkCollectionNode(
baseNode: AST.JSONObjectExpression,
mode: 'migration' | 'generator' | 'executor',
context: TSESLint.RuleContext<MessageIds, Options>
context: TSESLint.RuleContext<MessageIds, Options>,
options: NormalizedOptions
) {
const entries = baseNode.properties;
@ -270,7 +298,8 @@ export function checkCollectionNode(
entryNode.value,
entryNode.key.value.toString(),
mode,
context
context,
options
);
}
}
@ -280,8 +309,9 @@ export function validateEntry(
baseNode: AST.JSONObjectExpression,
key: string,
mode: 'migration' | 'generator' | 'executor',
context: TSESLint.RuleContext<MessageIds, Options>
) {
context: TSESLint.RuleContext<MessageIds, Options>,
options: NormalizedOptions
): void {
const schemaNode = baseNode.properties.find(
(x) => x.key.type === 'JSONLiteral' && x.key.value === 'schema'
);
@ -303,19 +333,25 @@ export function validateEntry(
node: schemaNode.value as any,
});
} else {
let validJsonFound = false;
const schemaFilePath = path.join(
path.dirname(context.filename ?? context.getFilename()),
schemaNode.value.value
);
if (!existsSync(schemaFilePath)) {
context.report({
messageId: 'invalidSchemaPath',
node: schemaNode.value as any,
});
} else {
try {
readJsonFile(schemaFilePath);
} catch (e) {
validJsonFound = true;
} catch {
try {
// Try to map back to source, which will be the case with TS solution setup.
readJsonFile(schemaFilePath.replace(options.outDir, options.rootDir));
validJsonFound = true;
} catch {
// nothing, will be reported below
}
}
if (!validJsonFound) {
context.report({
messageId: 'invalidSchemaPath',
node: schemaNode.value as any,
@ -323,7 +359,6 @@ export function validateEntry(
}
}
}
}
const implementationNode = baseNode.properties.find(
(x) =>
@ -339,7 +374,7 @@ export function validateEntry(
node: baseNode as any,
});
} else {
validateImplemenationNode(implementationNode, key, context);
validateImplementationNode(implementationNode, key, context, options);
}
if (mode === 'migration') {
@ -380,10 +415,11 @@ export function validateEntry(
}
}
export function validateImplemenationNode(
export function validateImplementationNode(
implementationNode: AST.JSONProperty,
key: string,
context: TSESLint.RuleContext<MessageIds, Options>
context: TSESLint.RuleContext<MessageIds, Options>,
options: NormalizedOptions
) {
if (
implementationNode.value.type !== 'JSONLiteral' ||
@ -408,7 +444,17 @@ export function validateImplemenationNode(
try {
resolvedPath = require.resolve(modulePath);
} catch (e) {
} catch {
try {
resolvedPath = require.resolve(
modulePath.replace(options.outDir, options.rootDir)
);
} catch {
// nothing, will be reported below
}
}
if (!resolvedPath) {
context.report({
messageId: 'invalidImplementationPath',
data: {