Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
merged 1 commit into from
Dec 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading