Skip to content

Commit

Permalink
feat(core): nx-plugin-checks accounts for outDir and rootDir of proje…
Browse files Browse the repository at this point in the history
…cts 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 #
  • Loading branch information
jaysoo authored Dec 18, 2024
1 parent 6bb0c2e commit 0720f3f
Show file tree
Hide file tree
Showing 2 changed files with 73 additions and 26 deletions.
3 changes: 2 additions & 1 deletion e2e/plugin/src/nx-plugin-ts-solution.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand Down Expand Up @@ -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 () => {
Expand Down
96 changes: 71 additions & 25 deletions packages/eslint-plugin/src/rules/nx-plugin-checks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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 =
Expand Down Expand Up @@ -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,
},
Expand Down Expand Up @@ -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);
}
Expand All @@ -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 {
Expand All @@ -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'
Expand Down Expand Up @@ -246,15 +273,16 @@ export function checkCollectionFileNode(
node: schematicsRootNode as any,
});
} else {
checkCollectionNode(collectionNode.value, mode, context);
checkCollectionNode(collectionNode.value, mode, context, options);
}
}
}

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;

Expand All @@ -270,7 +298,8 @@ export function checkCollectionNode(
entryNode.value,
entryNode.key.value.toString(),
mode,
context
context,
options
);
}
}
Expand All @@ -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'
);
Expand All @@ -303,24 +333,29 @@ 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)) {
try {
readJsonFile(schemaFilePath);
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,
});
} else {
try {
readJsonFile(schemaFilePath);
} catch (e) {
context.report({
messageId: 'invalidSchemaPath',
node: schemaNode.value as any,
});
}
}
}
}
Expand All @@ -339,7 +374,7 @@ export function validateEntry(
node: baseNode as any,
});
} else {
validateImplemenationNode(implementationNode, key, context);
validateImplementationNode(implementationNode, key, context, options);
}

if (mode === 'migration') {
Expand Down Expand Up @@ -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' ||
Expand All @@ -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: {
Expand Down

0 comments on commit 0720f3f

Please sign in to comment.