feat(linter): add option to ignore files based on pattern (#18863)

This commit is contained in:
Jack Hsu 2023-08-28 16:27:14 -04:00 committed by GitHub
parent 29850b0745
commit 90ca436d81
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 282 additions and 15 deletions

View File

@ -202,6 +202,75 @@ describe('Dependency checks (eslint)', () => {
expect(failures.length).toEqual(0);
});
it('should exclude files that are ignored', () => {
const packageJson = {
name: '@mycompany/liba',
dependencies: {},
};
const fileSys = {
'./libs/liba/package.json': JSON.stringify(packageJson, null, 2),
'./libs/liba/vite.config.ts': '',
'./libs/liba/project.json': JSON.stringify(
{
name: 'liba',
targets: {
build: {
command: 'tsc -p tsconfig.lib.json',
},
},
},
null,
2
),
'./nx.json': JSON.stringify({
targetDefaults: {
build: {
inputs: [
'{projectRoot}/**/*',
'!{projectRoot}/**/?(*.)+(spec|test).[jt]s?(x)?(.snap)',
],
},
},
}),
'./package.json': JSON.stringify(rootPackageJson, null, 2),
};
vol.fromJSON(fileSys, '/root');
const failures = runRule(
{
ignoredFiles: ['{projectRoot}/vite.config.ts'],
},
`/root/libs/liba/package.json`,
JSON.stringify(packageJson, null, 2),
{
nodes: {
liba: {
name: 'liba',
type: 'lib',
data: {
root: 'libs/liba',
targets: {
build: {},
},
},
},
},
externalNodes,
dependencies: {
liba: [{ source: 'liba', target: 'npm:external1', type: 'static' }],
},
},
{
liba: [
createFile(`libs/liba/vite.config.ts`, ['npm:external1']),
createFile(`libs/liba/package.json`, []),
],
}
);
expect(failures.length).toEqual(0);
});
it('should report missing dependencies section and fix it', () => {
const packageJson = {
name: '@mycompany/liba',

View File

@ -22,6 +22,7 @@ export type Options = [
checkVersionMismatches?: boolean;
checkMissingPackageJson?: boolean;
ignoredDependencies?: string[];
ignoredFiles?: string[];
includeTransitiveDependencies?: boolean;
}
];
@ -49,6 +50,7 @@ export default createESLintRule<Options, MessageIds>({
properties: {
buildTargets: [{ type: 'string' }],
ignoredDependencies: [{ type: 'string' }],
ignoredFiles: [{ type: 'string' }],
checkMissingDependencies: { type: 'boolean' },
checkObsoleteDependencies: { type: 'boolean' },
checkVersionMismatches: { type: 'boolean' },
@ -71,6 +73,7 @@ export default createESLintRule<Options, MessageIds>({
checkObsoleteDependencies: true,
checkVersionMismatches: true,
ignoredDependencies: [],
ignoredFiles: [],
includeTransitiveDependencies: false,
},
],
@ -80,6 +83,7 @@ export default createESLintRule<Options, MessageIds>({
{
buildTargets,
ignoredDependencies,
ignoredFiles,
checkMissingDependencies,
checkObsoleteDependencies,
checkVersionMismatches,
@ -133,6 +137,7 @@ export default createESLintRule<Options, MessageIds>({
buildTarget, // TODO: What if child library has a build target different from the parent?
{
includeTransitiveDependencies,
ignoredFiles,
}
);
const expectedDependencyNames = Object.keys(npmDependencies);

View File

@ -1137,6 +1137,20 @@ describe('lib', () => {
executor: '@nx/vite:test',
});
expect(tree.exists('libs/my-lib/vite.config.ts')).toBeTruthy();
expect(
readJson(tree, 'libs/my-lib/.eslintrc.json').overrides
).toContainEqual({
files: ['*.json'],
parser: 'jsonc-eslint-parser',
rules: {
'@nx/dependency-checks': [
'error',
{
ignoredFiles: ['{projectRoot}/vite.config.{js,ts,mjs,mts}'],
},
],
},
});
});
it.each`
@ -1159,6 +1173,66 @@ describe('lib', () => {
);
});
describe('--bundler=esbuild', () => {
it('should add build with esbuild', async () => {
await libraryGenerator(tree, {
...defaultOptions,
name: 'myLib',
bundler: 'esbuild',
unitTestRunner: 'none',
});
const project = readProjectConfiguration(tree, 'my-lib');
expect(project.targets.build).toMatchObject({
executor: '@nx/esbuild:esbuild',
});
expect(
readJson(tree, 'libs/my-lib/.eslintrc.json').overrides
).toContainEqual({
files: ['*.json'],
parser: 'jsonc-eslint-parser',
rules: {
'@nx/dependency-checks': [
'error',
{
ignoredFiles: ['{projectRoot}/esbuild.config.{js,ts,mjs,mts}'],
},
],
},
});
});
});
describe('--bundler=rollup', () => {
it('should add build with rollup', async () => {
await libraryGenerator(tree, {
...defaultOptions,
name: 'myLib',
bundler: 'rollup',
unitTestRunner: 'none',
});
const project = readProjectConfiguration(tree, 'my-lib');
expect(project.targets.build).toMatchObject({
executor: '@nx/rollup:rollup',
});
expect(
readJson(tree, 'libs/my-lib/.eslintrc.json').overrides
).toContainEqual({
files: ['*.json'],
parser: 'jsonc-eslint-parser',
rules: {
'@nx/dependency-checks': [
'error',
{
ignoredFiles: ['{projectRoot}/rollup.config.{js,ts,mjs,mts}'],
},
],
},
});
});
});
describe('--minimal', () => {
it('should generate a README.md when minimal is set to false', async () => {
await libraryGenerator(tree, {

View File

@ -10,6 +10,7 @@ import {
names,
offsetFromRoot,
ProjectConfiguration,
readProjectConfiguration,
runTasksInSerial,
toJS,
Tree,
@ -236,12 +237,14 @@ export type AddLintOptions = Pick<
| 'js'
| 'setParserOptionsProject'
| 'rootProject'
| 'bundler'
>;
export async function addLint(
tree: Tree,
options: AddLintOptions
): Promise<GeneratorCallback> {
const { lintProjectGenerator } = ensurePackage('@nx/linter', nxVersion);
const projectConfiguration = readProjectConfiguration(tree, options.name);
const task = lintProjectGenerator(tree, {
project: options.name,
linter: options.linter,
@ -256,15 +259,17 @@ export async function addLint(
setParserOptionsProject: options.setParserOptionsProject,
rootProject: options.rootProject,
});
const {
addOverrideToLintConfig,
lintConfigHasOverride,
isEslintConfigSupported,
updateOverrideInLintConfig,
// nx-ignore-next-line
} = require('@nx/linter/src/generators/utils/eslint-file');
// Also update the root ESLint config. The lintProjectGenerator will not generate it for root projects.
// But we need to set the package.json checks.
if (options.rootProject) {
const {
addOverrideToLintConfig,
isEslintConfigSupported,
// nx-ignore-next-line
} = require('@nx/linter/src/generators/utils/eslint-file');
if (isEslintConfigSupported(tree)) {
addOverrideToLintConfig(tree, '', {
files: ['*.json'],
@ -275,6 +280,56 @@ export async function addLint(
});
}
}
// If project lints package.json with @nx/dependency-checks, then add ignore files for
// build configuration files such as vite.config.ts. These config files need to be
// ignored, otherwise we will errors on missing dependencies that are for dev only.
if (
lintConfigHasOverride(
tree,
projectConfiguration.root,
(o) =>
Array.isArray(o.files)
? o.files.some((f) => f.match(/\.json$/))
: !!o.files?.match(/\.json$/),
true
)
) {
updateOverrideInLintConfig(
tree,
projectConfiguration.root,
(o) => o.rules?.['@nx/dependency-checks'],
(o) => {
const value = o.rules['@nx/dependency-checks'];
let ruleSeverity: string;
let ruleOptions: any;
if (Array.isArray(value)) {
ruleSeverity = value[0];
ruleOptions = value[1];
} else {
ruleSeverity = value;
ruleOptions = {};
}
if (options.bundler === 'vite' || options.unitTestRunner === 'vitest') {
ruleOptions.ignoredFiles = [
'{projectRoot}/vite.config.{js,ts,mjs,mts}',
];
o.rules['@nx/dependency-checks'] = [ruleSeverity, ruleOptions];
} else if (options.bundler === 'rollup') {
ruleOptions.ignoredFiles = [
'{projectRoot}/rollup.config.{js,ts,mjs,mts}',
];
o.rules['@nx/dependency-checks'] = [ruleSeverity, ruleOptions];
} else if (options.bundler === 'esbuild') {
ruleOptions.ignoredFiles = [
'{projectRoot}/esbuild.config.{js,ts,mjs,mts}',
];
o.rules['@nx/dependency-checks'] = [ruleSeverity, ruleOptions];
}
return o;
}
);
}
return task;
}

View File

@ -397,4 +397,57 @@ describe('findNpmDependencies', () => {
'@acme/lib3': '*',
});
});
it('should support ignoring extra file patterns in addition to task input', () => {
vol.fromJSON(
{
'./nx.json': JSON.stringify(nxJson),
},
'/root'
);
const lib = {
name: 'my-lib',
type: 'lib' as const,
data: {
root: 'libs/my-lib',
targets: { build: {} },
},
};
const projectGraph = {
nodes: {
'my-lib': lib,
},
externalNodes: {
'npm:foo': {
name: 'npm:foo' as const,
type: 'npm' as const,
data: {
packageName: 'foo',
version: '1.0.0',
},
},
},
dependencies: {},
};
const projectFileMap = {
'my-lib': [
{
file: 'libs/my-lib/vite.config.ts',
hash: '123',
deps: ['npm:foo'],
},
],
};
const results = findNpmDependencies(
'/root',
lib,
projectGraph,
projectFileMap,
'build',
{ ignoredFiles: ['{projectRoot}/vite.config.ts'] }
);
expect(results).toEqual({});
});
});

View File

@ -27,6 +27,7 @@ export function findNpmDependencies(
buildTarget: string,
options: {
includeTransitiveDependencies?: boolean;
ignoredFiles?: string[];
} = {}
): Record<string, string> {
let seen: null | Set<string> = null;
@ -41,6 +42,7 @@ export function findNpmDependencies(
collectedDeps: Record<string, string>
): void {
if (seen?.has(currentProject.name)) return;
seen?.add(currentProject.name);
collectDependenciesFromFileMap(
workspaceRoot,
@ -48,6 +50,7 @@ export function findNpmDependencies(
projectGraph,
projectFileMap,
buildTarget,
options.ignoredFiles,
collectedDeps
);
@ -82,19 +85,22 @@ function collectDependenciesFromFileMap(
projectGraph: ProjectGraph,
projectFileMap: ProjectFileMap,
buildTarget: string,
ignoredFiles: string[],
npmDeps: Record<string, string>
): void {
const rawFiles = projectFileMap[sourceProject.name];
if (!rawFiles) return;
// Cannot read inputs if the target does not exist on the project.
if (!sourceProject.data.targets[buildTarget]) return;
const inputs = getTargetInputs(
readNxJson(),
sourceProject,
buildTarget
).selfInputs;
// If build target does not exist in project, use all files as input.
// This is needed for transitive dependencies for apps -- where libs may not be buildable.
const inputs = sourceProject.data.targets[buildTarget]
? getTargetInputs(readNxJson(), sourceProject, buildTarget).selfInputs
: ['{projectRoot}/**/*'];
if (ignoredFiles) {
for (const pattern of ignoredFiles) {
inputs.push(`!${pattern}`);
}
}
const files = filterUsingGlobPatterns(
sourceProject.data.root,
projectFileMap[sourceProject.name] || [],
@ -128,7 +134,12 @@ function collectDependenciesFromFileMap(
npmDeps[cached.name] = cached.version;
} else {
const packageJson = readPackageJson(workspaceDep, workspaceRoot);
if (packageJson) {
if (
// Check that this is a buildable project, otherwise it cannot be a dependency in package.json.
workspaceDep.data.targets[buildTarget] &&
// Make sure package.json exists and has a valid name.
packageJson?.name
) {
// This is a workspace lib so we can't reliably read in a specific version since it depends on how the workspace is set up.
// ASSUMPTION: Most users will use '*' for workspace lib versions. Otherwise, they can manually update it.
npmDeps[packageJson.name] = '*';