feat(linter): add option to ignore files based on pattern (#18863)
This commit is contained in:
parent
29850b0745
commit
90ca436d81
@ -202,6 +202,75 @@ describe('Dependency checks (eslint)', () => {
|
|||||||
expect(failures.length).toEqual(0);
|
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', () => {
|
it('should report missing dependencies section and fix it', () => {
|
||||||
const packageJson = {
|
const packageJson = {
|
||||||
name: '@mycompany/liba',
|
name: '@mycompany/liba',
|
||||||
|
|||||||
@ -22,6 +22,7 @@ export type Options = [
|
|||||||
checkVersionMismatches?: boolean;
|
checkVersionMismatches?: boolean;
|
||||||
checkMissingPackageJson?: boolean;
|
checkMissingPackageJson?: boolean;
|
||||||
ignoredDependencies?: string[];
|
ignoredDependencies?: string[];
|
||||||
|
ignoredFiles?: string[];
|
||||||
includeTransitiveDependencies?: boolean;
|
includeTransitiveDependencies?: boolean;
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
@ -49,6 +50,7 @@ export default createESLintRule<Options, MessageIds>({
|
|||||||
properties: {
|
properties: {
|
||||||
buildTargets: [{ type: 'string' }],
|
buildTargets: [{ type: 'string' }],
|
||||||
ignoredDependencies: [{ type: 'string' }],
|
ignoredDependencies: [{ type: 'string' }],
|
||||||
|
ignoredFiles: [{ type: 'string' }],
|
||||||
checkMissingDependencies: { type: 'boolean' },
|
checkMissingDependencies: { type: 'boolean' },
|
||||||
checkObsoleteDependencies: { type: 'boolean' },
|
checkObsoleteDependencies: { type: 'boolean' },
|
||||||
checkVersionMismatches: { type: 'boolean' },
|
checkVersionMismatches: { type: 'boolean' },
|
||||||
@ -71,6 +73,7 @@ export default createESLintRule<Options, MessageIds>({
|
|||||||
checkObsoleteDependencies: true,
|
checkObsoleteDependencies: true,
|
||||||
checkVersionMismatches: true,
|
checkVersionMismatches: true,
|
||||||
ignoredDependencies: [],
|
ignoredDependencies: [],
|
||||||
|
ignoredFiles: [],
|
||||||
includeTransitiveDependencies: false,
|
includeTransitiveDependencies: false,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@ -80,6 +83,7 @@ export default createESLintRule<Options, MessageIds>({
|
|||||||
{
|
{
|
||||||
buildTargets,
|
buildTargets,
|
||||||
ignoredDependencies,
|
ignoredDependencies,
|
||||||
|
ignoredFiles,
|
||||||
checkMissingDependencies,
|
checkMissingDependencies,
|
||||||
checkObsoleteDependencies,
|
checkObsoleteDependencies,
|
||||||
checkVersionMismatches,
|
checkVersionMismatches,
|
||||||
@ -133,6 +137,7 @@ export default createESLintRule<Options, MessageIds>({
|
|||||||
buildTarget, // TODO: What if child library has a build target different from the parent?
|
buildTarget, // TODO: What if child library has a build target different from the parent?
|
||||||
{
|
{
|
||||||
includeTransitiveDependencies,
|
includeTransitiveDependencies,
|
||||||
|
ignoredFiles,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
const expectedDependencyNames = Object.keys(npmDependencies);
|
const expectedDependencyNames = Object.keys(npmDependencies);
|
||||||
|
|||||||
@ -1137,6 +1137,20 @@ describe('lib', () => {
|
|||||||
executor: '@nx/vite:test',
|
executor: '@nx/vite:test',
|
||||||
});
|
});
|
||||||
expect(tree.exists('libs/my-lib/vite.config.ts')).toBeTruthy();
|
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`
|
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', () => {
|
describe('--minimal', () => {
|
||||||
it('should generate a README.md when minimal is set to false', async () => {
|
it('should generate a README.md when minimal is set to false', async () => {
|
||||||
await libraryGenerator(tree, {
|
await libraryGenerator(tree, {
|
||||||
|
|||||||
@ -10,6 +10,7 @@ import {
|
|||||||
names,
|
names,
|
||||||
offsetFromRoot,
|
offsetFromRoot,
|
||||||
ProjectConfiguration,
|
ProjectConfiguration,
|
||||||
|
readProjectConfiguration,
|
||||||
runTasksInSerial,
|
runTasksInSerial,
|
||||||
toJS,
|
toJS,
|
||||||
Tree,
|
Tree,
|
||||||
@ -236,12 +237,14 @@ export type AddLintOptions = Pick<
|
|||||||
| 'js'
|
| 'js'
|
||||||
| 'setParserOptionsProject'
|
| 'setParserOptionsProject'
|
||||||
| 'rootProject'
|
| 'rootProject'
|
||||||
|
| 'bundler'
|
||||||
>;
|
>;
|
||||||
export async function addLint(
|
export async function addLint(
|
||||||
tree: Tree,
|
tree: Tree,
|
||||||
options: AddLintOptions
|
options: AddLintOptions
|
||||||
): Promise<GeneratorCallback> {
|
): Promise<GeneratorCallback> {
|
||||||
const { lintProjectGenerator } = ensurePackage('@nx/linter', nxVersion);
|
const { lintProjectGenerator } = ensurePackage('@nx/linter', nxVersion);
|
||||||
|
const projectConfiguration = readProjectConfiguration(tree, options.name);
|
||||||
const task = lintProjectGenerator(tree, {
|
const task = lintProjectGenerator(tree, {
|
||||||
project: options.name,
|
project: options.name,
|
||||||
linter: options.linter,
|
linter: options.linter,
|
||||||
@ -256,15 +259,17 @@ export async function addLint(
|
|||||||
setParserOptionsProject: options.setParserOptionsProject,
|
setParserOptionsProject: options.setParserOptionsProject,
|
||||||
rootProject: options.rootProject,
|
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.
|
// Also update the root ESLint config. The lintProjectGenerator will not generate it for root projects.
|
||||||
// But we need to set the package.json checks.
|
// But we need to set the package.json checks.
|
||||||
if (options.rootProject) {
|
if (options.rootProject) {
|
||||||
const {
|
|
||||||
addOverrideToLintConfig,
|
|
||||||
isEslintConfigSupported,
|
|
||||||
// nx-ignore-next-line
|
|
||||||
} = require('@nx/linter/src/generators/utils/eslint-file');
|
|
||||||
|
|
||||||
if (isEslintConfigSupported(tree)) {
|
if (isEslintConfigSupported(tree)) {
|
||||||
addOverrideToLintConfig(tree, '', {
|
addOverrideToLintConfig(tree, '', {
|
||||||
files: ['*.json'],
|
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;
|
return task;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -397,4 +397,57 @@ describe('findNpmDependencies', () => {
|
|||||||
'@acme/lib3': '*',
|
'@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({});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -27,6 +27,7 @@ export function findNpmDependencies(
|
|||||||
buildTarget: string,
|
buildTarget: string,
|
||||||
options: {
|
options: {
|
||||||
includeTransitiveDependencies?: boolean;
|
includeTransitiveDependencies?: boolean;
|
||||||
|
ignoredFiles?: string[];
|
||||||
} = {}
|
} = {}
|
||||||
): Record<string, string> {
|
): Record<string, string> {
|
||||||
let seen: null | Set<string> = null;
|
let seen: null | Set<string> = null;
|
||||||
@ -41,6 +42,7 @@ export function findNpmDependencies(
|
|||||||
collectedDeps: Record<string, string>
|
collectedDeps: Record<string, string>
|
||||||
): void {
|
): void {
|
||||||
if (seen?.has(currentProject.name)) return;
|
if (seen?.has(currentProject.name)) return;
|
||||||
|
seen?.add(currentProject.name);
|
||||||
|
|
||||||
collectDependenciesFromFileMap(
|
collectDependenciesFromFileMap(
|
||||||
workspaceRoot,
|
workspaceRoot,
|
||||||
@ -48,6 +50,7 @@ export function findNpmDependencies(
|
|||||||
projectGraph,
|
projectGraph,
|
||||||
projectFileMap,
|
projectFileMap,
|
||||||
buildTarget,
|
buildTarget,
|
||||||
|
options.ignoredFiles,
|
||||||
collectedDeps
|
collectedDeps
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -82,19 +85,22 @@ function collectDependenciesFromFileMap(
|
|||||||
projectGraph: ProjectGraph,
|
projectGraph: ProjectGraph,
|
||||||
projectFileMap: ProjectFileMap,
|
projectFileMap: ProjectFileMap,
|
||||||
buildTarget: string,
|
buildTarget: string,
|
||||||
|
ignoredFiles: string[],
|
||||||
npmDeps: Record<string, string>
|
npmDeps: Record<string, string>
|
||||||
): void {
|
): void {
|
||||||
const rawFiles = projectFileMap[sourceProject.name];
|
const rawFiles = projectFileMap[sourceProject.name];
|
||||||
if (!rawFiles) return;
|
if (!rawFiles) return;
|
||||||
|
|
||||||
// Cannot read inputs if the target does not exist on the project.
|
// If build target does not exist in project, use all files as input.
|
||||||
if (!sourceProject.data.targets[buildTarget]) return;
|
// This is needed for transitive dependencies for apps -- where libs may not be buildable.
|
||||||
|
const inputs = sourceProject.data.targets[buildTarget]
|
||||||
const inputs = getTargetInputs(
|
? getTargetInputs(readNxJson(), sourceProject, buildTarget).selfInputs
|
||||||
readNxJson(),
|
: ['{projectRoot}/**/*'];
|
||||||
sourceProject,
|
if (ignoredFiles) {
|
||||||
buildTarget
|
for (const pattern of ignoredFiles) {
|
||||||
).selfInputs;
|
inputs.push(`!${pattern}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
const files = filterUsingGlobPatterns(
|
const files = filterUsingGlobPatterns(
|
||||||
sourceProject.data.root,
|
sourceProject.data.root,
|
||||||
projectFileMap[sourceProject.name] || [],
|
projectFileMap[sourceProject.name] || [],
|
||||||
@ -128,7 +134,12 @@ function collectDependenciesFromFileMap(
|
|||||||
npmDeps[cached.name] = cached.version;
|
npmDeps[cached.name] = cached.version;
|
||||||
} else {
|
} else {
|
||||||
const packageJson = readPackageJson(workspaceDep, workspaceRoot);
|
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.
|
// 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.
|
// ASSUMPTION: Most users will use '*' for workspace lib versions. Otherwise, they can manually update it.
|
||||||
npmDeps[packageJson.name] = '*';
|
npmDeps[packageJson.name] = '*';
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user