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:
parent
6bb0c2e5f0
commit
0720f3f9b4
@ -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 () => {
|
||||
|
||||
@ -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,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,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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: {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user