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 executor = uniq('executor');
|
||||||
const generatedProject = uniq('project');
|
const generatedProject = uniq('project');
|
||||||
|
|
||||||
runCLI(`generate @nx/plugin:plugin packages/${plugin}`);
|
runCLI(`generate @nx/plugin:plugin packages/${plugin} --linter eslint`);
|
||||||
|
|
||||||
runCLI(
|
runCLI(
|
||||||
`generate @nx/plugin:generator --name ${generator} --path packages/${plugin}/src/generators/${generator}/generator`
|
`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(() => checkFilesExist(`libs/${generatedProject}`)).not.toThrow();
|
||||||
expect(() => runCLI(`execute ${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 () => {
|
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';
|
} from '@nx/devkit';
|
||||||
import { getRootTsConfigPath } from '@nx/js';
|
import { getRootTsConfigPath } from '@nx/js';
|
||||||
import { registerTsProject } from '@nx/js/src/internal';
|
import { registerTsProject } from '@nx/js/src/internal';
|
||||||
import { existsSync } from 'fs';
|
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
import { valid } from 'semver';
|
import { valid } from 'semver';
|
||||||
import { readProjectGraph } from '../utils/project-graph-utils';
|
import { readProjectGraph } from '../utils/project-graph-utils';
|
||||||
@ -26,15 +25,22 @@ type Options = [
|
|||||||
migrationsJson?: string;
|
migrationsJson?: string;
|
||||||
packageJson?: string;
|
packageJson?: string;
|
||||||
allowedVersionStrings: string[];
|
allowedVersionStrings: string[];
|
||||||
|
tsConfig?: string;
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
|
type NormalizedOptions = Options[0] & {
|
||||||
|
rootDir?: string;
|
||||||
|
outDir?: string;
|
||||||
|
};
|
||||||
|
|
||||||
const DEFAULT_OPTIONS: Options[0] = {
|
const DEFAULT_OPTIONS: Options[0] = {
|
||||||
generatorsJson: 'generators.json',
|
generatorsJson: 'generators.json',
|
||||||
executorsJson: 'executors.json',
|
executorsJson: 'executors.json',
|
||||||
migrationsJson: 'migrations.json',
|
migrationsJson: 'migrations.json',
|
||||||
packageJson: 'package.json',
|
packageJson: 'package.json',
|
||||||
allowedVersionStrings: ['*', 'latest', 'next'],
|
allowedVersionStrings: ['*', 'latest', 'next'],
|
||||||
|
tsConfig: 'tsconfig.lib.json',
|
||||||
};
|
};
|
||||||
|
|
||||||
export type MessageIds =
|
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"]',
|
'A list of specifiers that are valid for versions within package group. Defaults to ["*", "latest", "next"]',
|
||||||
items: { type: 'string' },
|
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,
|
additionalProperties: false,
|
||||||
},
|
},
|
||||||
@ -159,11 +170,11 @@ export default ESLintUtils.RuleCreator(() => ``)<Options, MessageIds>({
|
|||||||
node: AST.JSONObjectExpression
|
node: AST.JSONObjectExpression
|
||||||
) {
|
) {
|
||||||
if (sourceFilePath === generatorsJson) {
|
if (sourceFilePath === generatorsJson) {
|
||||||
checkCollectionFileNode(node, 'generator', context);
|
checkCollectionFileNode(node, 'generator', context, options);
|
||||||
} else if (sourceFilePath === migrationsJson) {
|
} else if (sourceFilePath === migrationsJson) {
|
||||||
checkCollectionFileNode(node, 'migration', context);
|
checkCollectionFileNode(node, 'migration', context, options);
|
||||||
} else if (sourceFilePath === executorsJson) {
|
} else if (sourceFilePath === executorsJson) {
|
||||||
checkCollectionFileNode(node, 'executor', context);
|
checkCollectionFileNode(node, 'executor', context, options);
|
||||||
} else if (sourceFilePath === packageJson) {
|
} else if (sourceFilePath === packageJson) {
|
||||||
validatePackageGroup(node, context);
|
validatePackageGroup(node, context);
|
||||||
}
|
}
|
||||||
@ -175,8 +186,21 @@ export default ESLintUtils.RuleCreator(() => ``)<Options, MessageIds>({
|
|||||||
function normalizeOptions(
|
function normalizeOptions(
|
||||||
sourceProject: ProjectGraphProjectNode,
|
sourceProject: ProjectGraphProjectNode,
|
||||||
options: Options[0]
|
options: Options[0]
|
||||||
): Options[0] {
|
): NormalizedOptions {
|
||||||
|
let rootDir: string;
|
||||||
|
let outDir: string;
|
||||||
const base = { ...DEFAULT_OPTIONS, ...options };
|
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 =
|
const pathPrefix =
|
||||||
sourceProject.data.root !== '.' ? `${sourceProject.data.root}/` : '';
|
sourceProject.data.root !== '.' ? `${sourceProject.data.root}/` : '';
|
||||||
return {
|
return {
|
||||||
@ -193,13 +217,16 @@ function normalizeOptions(
|
|||||||
packageJson: base.packageJson
|
packageJson: base.packageJson
|
||||||
? `${pathPrefix}${base.packageJson}`
|
? `${pathPrefix}${base.packageJson}`
|
||||||
: undefined,
|
: undefined,
|
||||||
|
rootDir: rootDir ? path.join(sourceProject.data.root, rootDir) : undefined,
|
||||||
|
outDir: outDir ? path.join(sourceProject.data.root, outDir) : undefined,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function checkCollectionFileNode(
|
export function checkCollectionFileNode(
|
||||||
baseNode: AST.JSONObjectExpression,
|
baseNode: AST.JSONObjectExpression,
|
||||||
mode: 'migration' | 'generator' | 'executor',
|
mode: 'migration' | 'generator' | 'executor',
|
||||||
context: TSESLint.RuleContext<MessageIds, Options>
|
context: TSESLint.RuleContext<MessageIds, Options>,
|
||||||
|
options: NormalizedOptions
|
||||||
) {
|
) {
|
||||||
const schematicsRootNode = baseNode.properties.find(
|
const schematicsRootNode = baseNode.properties.find(
|
||||||
(x) => x.key.type === 'JSONLiteral' && x.key.value === 'schematics'
|
(x) => x.key.type === 'JSONLiteral' && x.key.value === 'schematics'
|
||||||
@ -246,7 +273,7 @@ export function checkCollectionFileNode(
|
|||||||
node: schematicsRootNode as any,
|
node: schematicsRootNode as any,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
checkCollectionNode(collectionNode.value, mode, context);
|
checkCollectionNode(collectionNode.value, mode, context, options);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -254,7 +281,8 @@ export function checkCollectionFileNode(
|
|||||||
export function checkCollectionNode(
|
export function checkCollectionNode(
|
||||||
baseNode: AST.JSONObjectExpression,
|
baseNode: AST.JSONObjectExpression,
|
||||||
mode: 'migration' | 'generator' | 'executor',
|
mode: 'migration' | 'generator' | 'executor',
|
||||||
context: TSESLint.RuleContext<MessageIds, Options>
|
context: TSESLint.RuleContext<MessageIds, Options>,
|
||||||
|
options: NormalizedOptions
|
||||||
) {
|
) {
|
||||||
const entries = baseNode.properties;
|
const entries = baseNode.properties;
|
||||||
|
|
||||||
@ -270,7 +298,8 @@ export function checkCollectionNode(
|
|||||||
entryNode.value,
|
entryNode.value,
|
||||||
entryNode.key.value.toString(),
|
entryNode.key.value.toString(),
|
||||||
mode,
|
mode,
|
||||||
context
|
context,
|
||||||
|
options
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -280,8 +309,9 @@ export function validateEntry(
|
|||||||
baseNode: AST.JSONObjectExpression,
|
baseNode: AST.JSONObjectExpression,
|
||||||
key: string,
|
key: string,
|
||||||
mode: 'migration' | 'generator' | 'executor',
|
mode: 'migration' | 'generator' | 'executor',
|
||||||
context: TSESLint.RuleContext<MessageIds, Options>
|
context: TSESLint.RuleContext<MessageIds, Options>,
|
||||||
) {
|
options: NormalizedOptions
|
||||||
|
): void {
|
||||||
const schemaNode = baseNode.properties.find(
|
const schemaNode = baseNode.properties.find(
|
||||||
(x) => x.key.type === 'JSONLiteral' && x.key.value === 'schema'
|
(x) => x.key.type === 'JSONLiteral' && x.key.value === 'schema'
|
||||||
);
|
);
|
||||||
@ -303,24 +333,29 @@ export function validateEntry(
|
|||||||
node: schemaNode.value as any,
|
node: schemaNode.value as any,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
|
let validJsonFound = false;
|
||||||
const schemaFilePath = path.join(
|
const schemaFilePath = path.join(
|
||||||
path.dirname(context.filename ?? context.getFilename()),
|
path.dirname(context.filename ?? context.getFilename()),
|
||||||
schemaNode.value.value
|
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({
|
context.report({
|
||||||
messageId: 'invalidSchemaPath',
|
messageId: 'invalidSchemaPath',
|
||||||
node: schemaNode.value as any,
|
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,
|
node: baseNode as any,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
validateImplemenationNode(implementationNode, key, context);
|
validateImplementationNode(implementationNode, key, context, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mode === 'migration') {
|
if (mode === 'migration') {
|
||||||
@ -380,10 +415,11 @@ export function validateEntry(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function validateImplemenationNode(
|
export function validateImplementationNode(
|
||||||
implementationNode: AST.JSONProperty,
|
implementationNode: AST.JSONProperty,
|
||||||
key: string,
|
key: string,
|
||||||
context: TSESLint.RuleContext<MessageIds, Options>
|
context: TSESLint.RuleContext<MessageIds, Options>,
|
||||||
|
options: NormalizedOptions
|
||||||
) {
|
) {
|
||||||
if (
|
if (
|
||||||
implementationNode.value.type !== 'JSONLiteral' ||
|
implementationNode.value.type !== 'JSONLiteral' ||
|
||||||
@ -408,7 +444,17 @@ export function validateImplemenationNode(
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
resolvedPath = require.resolve(modulePath);
|
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({
|
context.report({
|
||||||
messageId: 'invalidImplementationPath',
|
messageId: 'invalidImplementationPath',
|
||||||
data: {
|
data: {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user