fix(linter): refactor pcv3 plugin, expose configFiles on context (#21677)
This commit is contained in:
parent
cea7e93c86
commit
1fe5b98f45
@ -1,6 +1,6 @@
|
||||
# Type alias: CreateNodes\<T\>
|
||||
|
||||
Ƭ **CreateNodes**\<`T`\>: readonly [projectFilePattern: string, createNodesFunction: CreateNodesFunction\<T\>]
|
||||
Ƭ **CreateNodes**\<`T`\>: readonly [configFilePattern: string, createNodesFunction: CreateNodesFunction\<T\>]
|
||||
|
||||
A pair of file patterns and [CreateNodesFunction](../../devkit/documents/CreateNodesFunction)
|
||||
|
||||
|
||||
@ -6,11 +6,20 @@ Context for [CreateNodesFunction](../../devkit/documents/CreateNodesFunction)
|
||||
|
||||
### Properties
|
||||
|
||||
- [configFiles](../../devkit/documents/CreateNodesContext#configfiles): string[]
|
||||
- [nxJsonConfiguration](../../devkit/documents/CreateNodesContext#nxjsonconfiguration): NxJsonConfiguration<string[] | "\*">
|
||||
- [workspaceRoot](../../devkit/documents/CreateNodesContext#workspaceroot): string
|
||||
|
||||
## Properties
|
||||
|
||||
### configFiles
|
||||
|
||||
• `Readonly` **configFiles**: `string`[]
|
||||
|
||||
The subset of configuration files which match the createNodes pattern
|
||||
|
||||
---
|
||||
|
||||
### nxJsonConfiguration
|
||||
|
||||
• `Readonly` **nxJsonConfiguration**: [`NxJsonConfiguration`](../../devkit/documents/NxJsonConfiguration)\<`string`[] \| `"*"`\>
|
||||
|
||||
@ -34,6 +34,7 @@ describe('@nx/cypress/plugin', () => {
|
||||
},
|
||||
},
|
||||
workspaceRoot: tempFs.tempDir,
|
||||
configFiles: [],
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
@ -48,6 +48,7 @@ export async function replaceProjectConfigurationsWithPlugin<T = unknown>(
|
||||
const nodes = await createNodesFunction(configFile, pluginOptions, {
|
||||
workspaceRoot: tree.root,
|
||||
nxJsonConfiguration: readNxJson(tree),
|
||||
configFiles,
|
||||
});
|
||||
const node = nodes.projects[Object.keys(nodes.projects)[0]];
|
||||
|
||||
|
||||
@ -25,11 +25,18 @@ export async function updatePackageScripts(
|
||||
const nxJson = readNxJson(tree);
|
||||
|
||||
const [pattern, createNodes] = createNodesTuple;
|
||||
const files = glob(tree, [pattern]);
|
||||
const matchingFiles = glob(tree, [pattern]);
|
||||
|
||||
for (const file of files) {
|
||||
for (const file of matchingFiles) {
|
||||
const projectRoot = getProjectRootFromConfigFile(file);
|
||||
await processProject(tree, projectRoot, file, createNodes, nxJson);
|
||||
await processProject(
|
||||
tree,
|
||||
projectRoot,
|
||||
file,
|
||||
createNodes,
|
||||
nxJson,
|
||||
matchingFiles
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -38,7 +45,8 @@ async function processProject(
|
||||
projectRoot: string,
|
||||
projectConfigurationFile: string,
|
||||
createNodesFunction: CreateNodesFunction,
|
||||
nxJsonConfiguration: NxJsonConfiguration
|
||||
nxJsonConfiguration: NxJsonConfiguration,
|
||||
configFiles: string[]
|
||||
) {
|
||||
const packageJsonPath = `${projectRoot}/package.json`;
|
||||
if (!tree.exists(packageJsonPath)) {
|
||||
@ -52,7 +60,11 @@ async function processProject(
|
||||
const result = await createNodesFunction(
|
||||
projectConfigurationFile,
|
||||
{},
|
||||
{ nxJsonConfiguration, workspaceRoot }
|
||||
{
|
||||
nxJsonConfiguration,
|
||||
workspaceRoot,
|
||||
configFiles,
|
||||
}
|
||||
);
|
||||
|
||||
const targetCommands = getInferredTargetCommands(result);
|
||||
|
||||
@ -1,23 +1,30 @@
|
||||
import { CreateNodesContext } from '@nx/devkit';
|
||||
import { createNodes } from './plugin';
|
||||
import { vol } from 'memfs';
|
||||
import 'nx/src/internal-testing-utils/mock-fs';
|
||||
|
||||
jest.mock('fs', () => {
|
||||
const memFs = require('memfs').fs;
|
||||
jest.mock(
|
||||
'nx/src/utils/workspace-context',
|
||||
(): Partial<typeof import('nx/src/utils/workspace-context')> => {
|
||||
const glob = require('fast-glob');
|
||||
return {
|
||||
...memFs,
|
||||
existsSync: (p) => (p.endsWith('.node') ? true : memFs.existsSync(p)),
|
||||
globWithWorkspaceContext(workspaceRoot: string, patterns: string[]) {
|
||||
// This glob will operate on memfs thanks to 'nx/src/internal-testing-utils/mock-fs'
|
||||
return glob.sync(patterns, { cwd: workspaceRoot });
|
||||
},
|
||||
};
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
import { CreateNodesContext } from '@nx/devkit';
|
||||
import { vol } from 'memfs';
|
||||
import { minimatch } from 'minimatch';
|
||||
import { createNodes } from './plugin';
|
||||
|
||||
describe('@nx/eslint/plugin', () => {
|
||||
let createNodesFunction = createNodes[1];
|
||||
let context: CreateNodesContext;
|
||||
|
||||
beforeEach(async () => {
|
||||
context = {
|
||||
nxJsonConfiguration: {
|
||||
// These defaults should be overridden by plugin
|
||||
// These defaults should be overridden by the plugin
|
||||
targetDefaults: {
|
||||
lint: {
|
||||
cache: false,
|
||||
@ -30,6 +37,7 @@ describe('@nx/eslint/plugin', () => {
|
||||
},
|
||||
},
|
||||
workspaceRoot: '',
|
||||
configFiles: [],
|
||||
};
|
||||
});
|
||||
|
||||
@ -38,23 +46,110 @@ describe('@nx/eslint/plugin', () => {
|
||||
jest.resetModules();
|
||||
});
|
||||
|
||||
it('should create nodes with default configuration for nested project', () => {
|
||||
const fileSys = {
|
||||
'apps/my-app/.eslintrc.json': `{}`,
|
||||
'apps/my-app/project.json': `{}`,
|
||||
'.eslintrc.json': `{}`,
|
||||
'package.json': `{}`,
|
||||
};
|
||||
vol.fromJSON(fileSys, '');
|
||||
const nodes = createNodesFunction(
|
||||
'apps/my-app/project.json',
|
||||
it('should not create any nodes when there are no eslint configs', async () => {
|
||||
applyFilesToVolAndContext(
|
||||
{
|
||||
targetName: 'lint',
|
||||
'package.json': `{}`,
|
||||
'project.json': `{}`,
|
||||
},
|
||||
context
|
||||
);
|
||||
expect(await invokeCreateNodesOnMatchingFiles(context, 'lint'))
|
||||
.toMatchInlineSnapshot(`
|
||||
{
|
||||
"projects": {},
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
expect(nodes).toMatchInlineSnapshot(`
|
||||
describe('root eslint config only', () => {
|
||||
it('should not create any nodes for just a package.json and root level eslint config', async () => {
|
||||
applyFilesToVolAndContext(
|
||||
{
|
||||
'.eslintrc.json': `{}`,
|
||||
'package.json': `{}`,
|
||||
},
|
||||
context
|
||||
);
|
||||
expect(await invokeCreateNodesOnMatchingFiles(context, 'lint'))
|
||||
.toMatchInlineSnapshot(`
|
||||
{
|
||||
"projects": {},
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('should not create a node for a root level eslint config when accompanied by a project.json, if no src directory is present', async () => {
|
||||
applyFilesToVolAndContext(
|
||||
{
|
||||
'eslint.config.js': `module.exports = {};`,
|
||||
'project.json': `{}`,
|
||||
},
|
||||
context
|
||||
);
|
||||
// NOTE: It should set ESLINT_USE_FLAT_CONFIG to true because of the use of eslint.config.js
|
||||
expect(await invokeCreateNodesOnMatchingFiles(context, 'lint'))
|
||||
.toMatchInlineSnapshot(`
|
||||
{
|
||||
"projects": {},
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
// Standalone Nx workspace style setup
|
||||
it('should create a node for just a package.json and root level eslint config if accompanied by a src directory', async () => {
|
||||
applyFilesToVolAndContext(
|
||||
{
|
||||
'.eslintrc.json': `{}`,
|
||||
'package.json': `{}`,
|
||||
'src/index.ts': `console.log('hello world')`,
|
||||
},
|
||||
context
|
||||
);
|
||||
// NOTE: The command is specifically targeting the src directory in the case of a standalone Nx workspace
|
||||
expect(await invokeCreateNodesOnMatchingFiles(context, 'lint'))
|
||||
.toMatchInlineSnapshot(`
|
||||
{
|
||||
"projects": {
|
||||
".": {
|
||||
"targets": {
|
||||
"lint": {
|
||||
"cache": true,
|
||||
"command": "eslint ./src",
|
||||
"inputs": [
|
||||
"default",
|
||||
"^default",
|
||||
"{projectRoot}/eslintrc.json",
|
||||
"{workspaceRoot}/tools/eslint-rules/**/*",
|
||||
{
|
||||
"externalDependencies": [
|
||||
"eslint",
|
||||
],
|
||||
},
|
||||
],
|
||||
"options": {
|
||||
"cwd": ".",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('should create a node for a nested project (with a project.json and any lintable file) which does not have its own eslint config if accompanied by a root level eslint config', async () => {
|
||||
applyFilesToVolAndContext(
|
||||
{
|
||||
'.eslintrc.json': `{}`,
|
||||
'apps/my-app/project.json': `{}`,
|
||||
// This file is lintable so create the target
|
||||
'apps/my-app/index.ts': `console.log('hello world')`,
|
||||
},
|
||||
context
|
||||
);
|
||||
expect(await invokeCreateNodesOnMatchingFiles(context, 'lint'))
|
||||
.toMatchInlineSnapshot(`
|
||||
{
|
||||
"projects": {
|
||||
"apps/my-app": {
|
||||
@ -64,8 +159,8 @@ describe('@nx/eslint/plugin', () => {
|
||||
"command": "eslint .",
|
||||
"inputs": [
|
||||
"default",
|
||||
"^default",
|
||||
"{workspaceRoot}/.eslintrc.json",
|
||||
"{workspaceRoot}/apps/my-app/.eslintrc.json",
|
||||
"{workspaceRoot}/tools/eslint-rules/**/*",
|
||||
{
|
||||
"externalDependencies": [
|
||||
@ -84,34 +179,29 @@ describe('@nx/eslint/plugin', () => {
|
||||
`);
|
||||
});
|
||||
|
||||
it('should create nodes with default configuration for standalone project', () => {
|
||||
const fileSys = {
|
||||
'apps/my-app/eslint.config.js': `module.exports = []`,
|
||||
'apps/my-app/project.json': `{}`,
|
||||
'eslint.config.js': `module.exports = []`,
|
||||
'src/index.ts': `console.log('hello world')`,
|
||||
'package.json': `{}`,
|
||||
};
|
||||
vol.fromJSON(fileSys, '');
|
||||
const nodes = createNodesFunction(
|
||||
'package.json',
|
||||
it('should create a node for a nested project (with a package.json and any lintable file) which does not have its own eslint config if accompanied by a root level eslint config', async () => {
|
||||
applyFilesToVolAndContext(
|
||||
{
|
||||
targetName: 'lint',
|
||||
'.eslintrc.json': `{}`,
|
||||
'apps/my-app/package.json': `{}`,
|
||||
// This file is lintable so create the target
|
||||
'apps/my-app/index.ts': `console.log('hello world')`,
|
||||
},
|
||||
context
|
||||
);
|
||||
|
||||
expect(nodes).toMatchInlineSnapshot(`
|
||||
expect(await invokeCreateNodesOnMatchingFiles(context, 'lint'))
|
||||
.toMatchInlineSnapshot(`
|
||||
{
|
||||
"projects": {
|
||||
".": {
|
||||
"apps/my-app": {
|
||||
"targets": {
|
||||
"lint": {
|
||||
"cache": true,
|
||||
"command": "eslint ./src",
|
||||
"command": "eslint .",
|
||||
"inputs": [
|
||||
"default",
|
||||
"{workspaceRoot}/eslint.config.js",
|
||||
"^default",
|
||||
"{workspaceRoot}/.eslintrc.json",
|
||||
"{workspaceRoot}/tools/eslint-rules/**/*",
|
||||
{
|
||||
"externalDependencies": [
|
||||
@ -120,10 +210,7 @@ describe('@nx/eslint/plugin', () => {
|
||||
},
|
||||
],
|
||||
"options": {
|
||||
"cwd": ".",
|
||||
"env": {
|
||||
"ESLINT_USE_FLAT_CONFIG": "true",
|
||||
},
|
||||
"cwd": "apps/my-app",
|
||||
},
|
||||
},
|
||||
},
|
||||
@ -133,22 +220,259 @@ describe('@nx/eslint/plugin', () => {
|
||||
`);
|
||||
});
|
||||
|
||||
it('should not create nodes if no src folder for root', () => {
|
||||
const fileSys = {
|
||||
'apps/my-app/eslint.config.js': `module.exports = []`,
|
||||
'apps/my-app/project.json': `{}`,
|
||||
'eslint.config.js': `module.exports = []`,
|
||||
'package.json': `{}`,
|
||||
};
|
||||
vol.fromJSON(fileSys, '');
|
||||
const nodes = createNodesFunction(
|
||||
'package.json',
|
||||
it('should not create a node for a nested project (with a package.json and no lintable files) which does not have its own eslint config if accompanied by a root level eslint config', async () => {
|
||||
applyFilesToVolAndContext(
|
||||
{
|
||||
targetName: 'lint',
|
||||
'.eslintrc.json': `{}`,
|
||||
'apps/my-app/package.json': `{}`,
|
||||
// These files are not lintable so do not create the target
|
||||
'apps/my-app/one.png': `...`,
|
||||
'apps/my-app/two.mov': `...`,
|
||||
'apps/my-app/three.css': `...`,
|
||||
'apps/my-app/config-one.yaml': `...`,
|
||||
'apps/my-app/config-two.yml': `...`,
|
||||
},
|
||||
context
|
||||
);
|
||||
expect(await invokeCreateNodesOnMatchingFiles(context, 'lint'))
|
||||
.toMatchInlineSnapshot(`
|
||||
{
|
||||
"projects": {},
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
expect(nodes).toMatchInlineSnapshot(`{}`);
|
||||
it('should not create a node for a nested project (with a project.json and no lintable files) which does not have its own eslint config if accompanied by a root level eslint config', async () => {
|
||||
applyFilesToVolAndContext(
|
||||
{
|
||||
'.eslintrc.json': `{}`,
|
||||
'apps/my-app/project.json': `{}`,
|
||||
// These files are not lintable so do not create the target
|
||||
'apps/my-app/one.png': `...`,
|
||||
'apps/my-app/two.mov': `...`,
|
||||
'apps/my-app/three.css': `...`,
|
||||
'apps/my-app/config-one.yaml': `...`,
|
||||
'apps/my-app/config-two.yml': `...`,
|
||||
},
|
||||
context
|
||||
);
|
||||
expect(await invokeCreateNodesOnMatchingFiles(context, 'lint'))
|
||||
.toMatchInlineSnapshot(`
|
||||
{
|
||||
"projects": {},
|
||||
}
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('nested eslint configs only', () => {
|
||||
it('should create appropriate nodes for nested projects without a root level eslint config', async () => {
|
||||
applyFilesToVolAndContext(
|
||||
{
|
||||
'apps/my-app/.eslintrc.json': `{}`,
|
||||
'apps/my-app/project.json': `{}`,
|
||||
'apps/my-app/index.ts': `console.log('hello world')`,
|
||||
'libs/my-lib/.eslintrc.json': `{}`,
|
||||
'libs/my-lib/project.json': `{}`,
|
||||
'libs/my-lib/index.ts': `console.log('hello world')`,
|
||||
},
|
||||
context
|
||||
);
|
||||
expect(await invokeCreateNodesOnMatchingFiles(context, 'lint'))
|
||||
.toMatchInlineSnapshot(`
|
||||
{
|
||||
"projects": {
|
||||
"apps/my-app": {
|
||||
"targets": {
|
||||
"lint": {
|
||||
"cache": true,
|
||||
"command": "eslint .",
|
||||
"inputs": [
|
||||
"default",
|
||||
"^default",
|
||||
"{projectRoot}/.eslintrc.json",
|
||||
"{workspaceRoot}/tools/eslint-rules/**/*",
|
||||
{
|
||||
"externalDependencies": [
|
||||
"eslint",
|
||||
],
|
||||
},
|
||||
],
|
||||
"options": {
|
||||
"cwd": "apps/my-app",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"libs/my-lib": {
|
||||
"targets": {
|
||||
"lint": {
|
||||
"cache": true,
|
||||
"command": "eslint .",
|
||||
"inputs": [
|
||||
"default",
|
||||
"^default",
|
||||
"{projectRoot}/.eslintrc.json",
|
||||
"{workspaceRoot}/tools/eslint-rules/**/*",
|
||||
{
|
||||
"externalDependencies": [
|
||||
"eslint",
|
||||
],
|
||||
},
|
||||
],
|
||||
"options": {
|
||||
"cwd": "libs/my-lib",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('root eslint config and nested eslint configs', () => {
|
||||
it('should create appropriate nodes for just a package.json and root level eslint config combined with nested eslint configs', async () => {
|
||||
applyFilesToVolAndContext(
|
||||
{
|
||||
'.eslintrc.json': `{}`,
|
||||
'package.json': `{}`,
|
||||
'apps/my-app/.eslintrc.json': `{}`,
|
||||
'apps/my-app/project.json': `{}`,
|
||||
'apps/my-app/index.ts': `console.log('hello world')`,
|
||||
'libs/my-lib/.eslintrc.json': `{}`,
|
||||
'libs/my-lib/project.json': `{}`,
|
||||
'libs/my-lib/index.ts': `console.log('hello world')`,
|
||||
},
|
||||
context
|
||||
);
|
||||
// NOTE: The nested projects have the root level config as an input to their lint targets
|
||||
expect(await invokeCreateNodesOnMatchingFiles(context, 'lint'))
|
||||
.toMatchInlineSnapshot(`
|
||||
{
|
||||
"projects": {
|
||||
"apps/my-app": {
|
||||
"targets": {
|
||||
"lint": {
|
||||
"cache": true,
|
||||
"command": "eslint .",
|
||||
"inputs": [
|
||||
"default",
|
||||
"^default",
|
||||
"{workspaceRoot}/.eslintrc.json",
|
||||
"{projectRoot}/.eslintrc.json",
|
||||
"{workspaceRoot}/tools/eslint-rules/**/*",
|
||||
{
|
||||
"externalDependencies": [
|
||||
"eslint",
|
||||
],
|
||||
},
|
||||
],
|
||||
"options": {
|
||||
"cwd": "apps/my-app",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"libs/my-lib": {
|
||||
"targets": {
|
||||
"lint": {
|
||||
"cache": true,
|
||||
"command": "eslint .",
|
||||
"inputs": [
|
||||
"default",
|
||||
"^default",
|
||||
"{workspaceRoot}/.eslintrc.json",
|
||||
"{projectRoot}/.eslintrc.json",
|
||||
"{workspaceRoot}/tools/eslint-rules/**/*",
|
||||
{
|
||||
"externalDependencies": [
|
||||
"eslint",
|
||||
],
|
||||
},
|
||||
],
|
||||
"options": {
|
||||
"cwd": "libs/my-lib",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('should create appropriate nodes for a nested project without its own eslint config but with an orphaned eslint config in its parent hierarchy', async () => {
|
||||
applyFilesToVolAndContext(
|
||||
{
|
||||
'.eslintrc.json': '{}',
|
||||
'apps/.eslintrc.json': '{}',
|
||||
'apps/myapp/project.json': '{}',
|
||||
'apps/myapp/index.ts': 'console.log("hello world")',
|
||||
},
|
||||
context
|
||||
);
|
||||
// NOTE: The nested projects have the root level config as an input to their lint targets
|
||||
expect(await invokeCreateNodesOnMatchingFiles(context, 'lint'))
|
||||
.toMatchInlineSnapshot(`
|
||||
{
|
||||
"projects": {
|
||||
"apps/myapp": {
|
||||
"targets": {
|
||||
"lint": {
|
||||
"cache": true,
|
||||
"command": "eslint .",
|
||||
"inputs": [
|
||||
"default",
|
||||
"^default",
|
||||
"{workspaceRoot}/.eslintrc.json",
|
||||
"{workspaceRoot}/apps/.eslintrc.json",
|
||||
"{workspaceRoot}/tools/eslint-rules/**/*",
|
||||
{
|
||||
"externalDependencies": [
|
||||
"eslint",
|
||||
],
|
||||
},
|
||||
],
|
||||
"options": {
|
||||
"cwd": "apps/myapp",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
`);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function getMatchingFiles(allConfigFiles: string[]): string[] {
|
||||
return allConfigFiles.filter((file) =>
|
||||
minimatch(file, createNodes[0], { dot: true })
|
||||
);
|
||||
}
|
||||
|
||||
function applyFilesToVolAndContext(
|
||||
fileSys: Record<string, string>,
|
||||
context: CreateNodesContext
|
||||
) {
|
||||
vol.fromJSON(fileSys, '');
|
||||
// @ts-expect-error update otherwise readonly property for testing
|
||||
context.configFiles = getMatchingFiles(Object.keys(fileSys));
|
||||
}
|
||||
|
||||
async function invokeCreateNodesOnMatchingFiles(
|
||||
context: CreateNodesContext,
|
||||
targetName: string
|
||||
) {
|
||||
const aggregateProjects: Record<string, any> = {};
|
||||
for (const file of context.configFiles) {
|
||||
const nodes = await createNodes[1](file, { targetName }, context);
|
||||
Object.assign(aggregateProjects, nodes.projects);
|
||||
}
|
||||
return {
|
||||
projects: aggregateProjects,
|
||||
};
|
||||
}
|
||||
|
||||
@ -1,110 +1,158 @@
|
||||
import { CreateNodes, TargetConfiguration } from '@nx/devkit';
|
||||
import { dirname, join } from 'path';
|
||||
import { readdirSync } from 'fs';
|
||||
import {
|
||||
CreateNodes,
|
||||
CreateNodesContext,
|
||||
CreateNodesResult,
|
||||
TargetConfiguration,
|
||||
} from '@nx/devkit';
|
||||
import { existsSync } from 'node:fs';
|
||||
import { dirname, join } from 'node:path';
|
||||
import { combineGlobPatterns } from 'nx/src/utils/globs';
|
||||
import { globWithWorkspaceContext } from 'nx/src/utils/workspace-context';
|
||||
import {
|
||||
ESLINT_CONFIG_FILENAMES,
|
||||
findBaseEslintFile,
|
||||
baseEsLintConfigFile,
|
||||
baseEsLintFlatConfigFile,
|
||||
isFlatConfig,
|
||||
} from '../utils/config-file';
|
||||
|
||||
export interface EslintPluginOptions {
|
||||
targetName?: string;
|
||||
extensions?: string[];
|
||||
}
|
||||
|
||||
export const createNodes: CreateNodes<EslintPluginOptions> = [
|
||||
combineGlobPatterns(['**/project.json', '**/package.json']),
|
||||
(configFilePath, options, context) => {
|
||||
const projectRoot = dirname(configFilePath);
|
||||
const DEFAULT_EXTENSIONS = ['ts', 'tsx', 'js', 'jsx', 'html', 'vue'];
|
||||
|
||||
export const createNodes: CreateNodes<EslintPluginOptions> = [
|
||||
combineGlobPatterns([
|
||||
...ESLINT_CONFIG_FILENAMES.map((f) => `**/${f}`),
|
||||
baseEsLintConfigFile,
|
||||
baseEsLintFlatConfigFile,
|
||||
]),
|
||||
(configFilePath, options, context) => {
|
||||
options = normalizeOptions(options);
|
||||
|
||||
const eslintConfigs = getEslintConfigsForProject(
|
||||
projectRoot,
|
||||
context.workspaceRoot
|
||||
);
|
||||
if (!eslintConfigs.length) {
|
||||
return {};
|
||||
// Ensure that configFiles are set, e2e-run fails due to them being undefined in CI (does not occur locally)
|
||||
// TODO(JamesHenry): Further troubleshoot this in CI
|
||||
(context as any).configFiles = context.configFiles ?? [];
|
||||
|
||||
// Create a Set of all the directories containing eslint configs
|
||||
const eslintRoots = new Set(context.configFiles.map(dirname));
|
||||
const configDir = dirname(configFilePath);
|
||||
|
||||
const childProjectRoots = globWithWorkspaceContext(
|
||||
context.workspaceRoot,
|
||||
[
|
||||
'project.json',
|
||||
'package.json',
|
||||
'**/project.json',
|
||||
'**/package.json',
|
||||
].map((f) => join(configDir, f))
|
||||
)
|
||||
.map((f) => dirname(f))
|
||||
.filter((childProjectRoot) => {
|
||||
// Filter out projects under other eslint configs
|
||||
let root = childProjectRoot;
|
||||
// Traverse up from the childProjectRoot to either the workspaceRoot or the dir of this config file
|
||||
while (root !== dirname(root) && root !== dirname(configFilePath)) {
|
||||
if (eslintRoots.has(root)) {
|
||||
return false;
|
||||
}
|
||||
root = dirname(root);
|
||||
}
|
||||
return true;
|
||||
})
|
||||
.filter((dir) => {
|
||||
// Ignore project roots where the project does not contain any lintable files
|
||||
const lintableFiles = globWithWorkspaceContext(context.workspaceRoot, [
|
||||
join(dir, `**/*.{${options.extensions.join(',')}}`),
|
||||
]);
|
||||
return lintableFiles.length > 0;
|
||||
});
|
||||
|
||||
const uniqueChildProjectRoots = Array.from(new Set(childProjectRoots));
|
||||
|
||||
return {
|
||||
projects: {
|
||||
[projectRoot]: {
|
||||
targets: buildEslintTargets(eslintConfigs, projectRoot, options),
|
||||
},
|
||||
},
|
||||
projects: getProjectsUsingESLintConfig(
|
||||
configFilePath,
|
||||
uniqueChildProjectRoots,
|
||||
options,
|
||||
context
|
||||
),
|
||||
};
|
||||
},
|
||||
];
|
||||
|
||||
function getEslintConfigsForProject(
|
||||
projectRoot: string,
|
||||
workspaceRoot: string
|
||||
): string[] {
|
||||
const detectedConfigs = new Set<string>();
|
||||
const baseConfig = findBaseEslintFile(workspaceRoot);
|
||||
if (baseConfig) {
|
||||
detectedConfigs.add(baseConfig);
|
||||
function getProjectsUsingESLintConfig(
|
||||
configFilePath: string,
|
||||
childProjectRoots: string[],
|
||||
options: EslintPluginOptions,
|
||||
context: CreateNodesContext
|
||||
): CreateNodesResult['projects'] {
|
||||
const projects: CreateNodesResult['projects'] = {};
|
||||
|
||||
const rootEslintConfig = context.configFiles.find(
|
||||
(f) =>
|
||||
f === baseEsLintConfigFile ||
|
||||
f === baseEsLintFlatConfigFile ||
|
||||
ESLINT_CONFIG_FILENAMES.includes(f)
|
||||
);
|
||||
|
||||
// Add a lint target for each child project without an eslint config, with the root level config as an input
|
||||
for (const projectRoot of childProjectRoots) {
|
||||
// If there's no src folder, it's not a standalone project, do not add the target at all
|
||||
const isStandaloneWorkspace =
|
||||
projectRoot === '.' &&
|
||||
existsSync(join(context.workspaceRoot, projectRoot, 'src')) &&
|
||||
existsSync(join(context.workspaceRoot, projectRoot, 'package.json'));
|
||||
if (projectRoot === '.' && !isStandaloneWorkspace) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let siblingFiles = readdirSync(join(workspaceRoot, projectRoot));
|
||||
const eslintConfigs = [configFilePath];
|
||||
|
||||
if (projectRoot === '.') {
|
||||
// If there's no src folder, it's not a standalone project
|
||||
if (!siblingFiles.includes('src')) {
|
||||
return [];
|
||||
if (rootEslintConfig && !eslintConfigs.includes(rootEslintConfig)) {
|
||||
eslintConfigs.unshift(rootEslintConfig);
|
||||
}
|
||||
// If it's standalone but doesn't have eslint config, it's not a lintable
|
||||
const config = siblingFiles.find((f) =>
|
||||
ESLINT_CONFIG_FILENAMES.includes(f)
|
||||
);
|
||||
if (!config) {
|
||||
return [];
|
||||
|
||||
projects[projectRoot] = {
|
||||
targets: buildEslintTargets(
|
||||
eslintConfigs,
|
||||
projectRoot,
|
||||
options,
|
||||
isStandaloneWorkspace
|
||||
),
|
||||
};
|
||||
}
|
||||
detectedConfigs.add(config);
|
||||
return Array.from(detectedConfigs);
|
||||
}
|
||||
while (projectRoot !== '.') {
|
||||
// if it has an eslint config it's lintable
|
||||
const config = siblingFiles.find((f) =>
|
||||
ESLINT_CONFIG_FILENAMES.includes(f)
|
||||
);
|
||||
if (config) {
|
||||
detectedConfigs.add(`${projectRoot}/${config}`);
|
||||
return Array.from(detectedConfigs);
|
||||
}
|
||||
projectRoot = dirname(projectRoot);
|
||||
siblingFiles = readdirSync(join(workspaceRoot, projectRoot));
|
||||
}
|
||||
// check whether the root has an eslint config
|
||||
const config = readdirSync(workspaceRoot).find((f) =>
|
||||
ESLINT_CONFIG_FILENAMES.includes(f)
|
||||
);
|
||||
if (config) {
|
||||
detectedConfigs.add(config);
|
||||
return Array.from(detectedConfigs);
|
||||
}
|
||||
return [];
|
||||
|
||||
return projects;
|
||||
}
|
||||
|
||||
function buildEslintTargets(
|
||||
eslintConfigs: string[],
|
||||
projectRoot: string,
|
||||
options: EslintPluginOptions
|
||||
options: EslintPluginOptions,
|
||||
isStandaloneWorkspace = false
|
||||
) {
|
||||
const isRootProject = projectRoot === '.';
|
||||
|
||||
const targets: Record<string, TargetConfiguration> = {};
|
||||
|
||||
const targetConfig: TargetConfiguration = {
|
||||
command: `eslint ${isRootProject ? './src' : '.'}`,
|
||||
command: `eslint ${isRootProject && isStandaloneWorkspace ? './src' : '.'}`,
|
||||
cache: true,
|
||||
options: {
|
||||
cwd: projectRoot,
|
||||
},
|
||||
inputs: [
|
||||
'default',
|
||||
...eslintConfigs.map((config) => `{workspaceRoot}/${config}`),
|
||||
// Certain lint rules can be impacted by changes to dependencies
|
||||
'^default',
|
||||
...eslintConfigs.map((config) =>
|
||||
`{workspaceRoot}/${config}`.replace(
|
||||
`{workspaceRoot}/${projectRoot}`,
|
||||
isRootProject ? '{projectRoot}/' : '{projectRoot}'
|
||||
)
|
||||
),
|
||||
'{workspaceRoot}/tools/eslint-rules/**/*',
|
||||
{ externalDependencies: ['eslint'] },
|
||||
],
|
||||
@ -123,5 +171,13 @@ function buildEslintTargets(
|
||||
function normalizeOptions(options: EslintPluginOptions): EslintPluginOptions {
|
||||
options ??= {};
|
||||
options.targetName ??= 'lint';
|
||||
|
||||
// Normalize user input for extensions (strip leading . characters)
|
||||
if (Array.isArray(options.extensions)) {
|
||||
options.extensions = options.extensions.map((f) => f.replace(/^\.+/, ''));
|
||||
} else {
|
||||
options.extensions = DEFAULT_EXTENSIONS;
|
||||
}
|
||||
|
||||
return options;
|
||||
}
|
||||
|
||||
@ -14,22 +14,6 @@ export const ESLINT_CONFIG_FILENAMES = [
|
||||
export const baseEsLintConfigFile = '.eslintrc.base.json';
|
||||
export const baseEsLintFlatConfigFile = 'eslint.base.config.js';
|
||||
|
||||
export function findBaseEslintFile(workspaceRoot = ''): string | null {
|
||||
if (existsSync(joinPathFragments(workspaceRoot, baseEsLintConfigFile))) {
|
||||
return baseEsLintConfigFile;
|
||||
}
|
||||
if (existsSync(joinPathFragments(workspaceRoot, baseEsLintFlatConfigFile))) {
|
||||
return baseEsLintFlatConfigFile;
|
||||
}
|
||||
for (const file of ESLINT_CONFIG_FILENAMES) {
|
||||
if (existsSync(joinPathFragments(workspaceRoot, file))) {
|
||||
return file;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export function isFlatConfig(configFilePath: string): boolean {
|
||||
return configFilePath.endsWith('.config.js');
|
||||
}
|
||||
|
||||
@ -20,6 +20,7 @@ describe('@nx/jest/plugin', () => {
|
||||
},
|
||||
},
|
||||
workspaceRoot: tempFs.tempDir,
|
||||
configFiles: [],
|
||||
};
|
||||
|
||||
await tempFs.createFiles({
|
||||
|
||||
@ -18,6 +18,7 @@ describe('@nx/next/plugin', () => {
|
||||
},
|
||||
},
|
||||
workspaceRoot: '',
|
||||
configFiles: [],
|
||||
};
|
||||
});
|
||||
|
||||
@ -53,6 +54,7 @@ describe('@nx/next/plugin', () => {
|
||||
},
|
||||
},
|
||||
workspaceRoot: tempFs.tempDir,
|
||||
configFiles: [],
|
||||
};
|
||||
|
||||
tempFs.createFileSync(
|
||||
|
||||
@ -38,6 +38,7 @@ describe('@nx/nuxt/plugin', () => {
|
||||
},
|
||||
},
|
||||
workspaceRoot: '',
|
||||
configFiles: [],
|
||||
};
|
||||
});
|
||||
|
||||
@ -72,6 +73,7 @@ describe('@nx/nuxt/plugin', () => {
|
||||
},
|
||||
},
|
||||
workspaceRoot: tempFs.tempDir,
|
||||
configFiles: [],
|
||||
};
|
||||
|
||||
tempFs.createFileSync(
|
||||
|
||||
@ -14,6 +14,7 @@ describe('nx project.json plugin', () => {
|
||||
context = {
|
||||
nxJsonConfiguration: {},
|
||||
workspaceRoot: '/root',
|
||||
configFiles: [],
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
@ -12,6 +12,7 @@ describe('nx project.json plugin', () => {
|
||||
context = {
|
||||
nxJsonConfiguration: {},
|
||||
workspaceRoot: '/root',
|
||||
configFiles: [],
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
@ -20,6 +20,7 @@ describe('target-defaults plugin', () => {
|
||||
},
|
||||
},
|
||||
workspaceRoot: '/root',
|
||||
configFiles: [],
|
||||
};
|
||||
});
|
||||
|
||||
@ -109,6 +110,7 @@ describe('target-defaults plugin', () => {
|
||||
},
|
||||
},
|
||||
workspaceRoot: '/root',
|
||||
configFiles: [],
|
||||
})
|
||||
).toMatchInlineSnapshot(`
|
||||
{
|
||||
@ -156,6 +158,7 @@ describe('target-defaults plugin', () => {
|
||||
},
|
||||
},
|
||||
workspaceRoot: '/root',
|
||||
configFiles: [],
|
||||
})
|
||||
).toMatchInlineSnapshot(`
|
||||
{
|
||||
@ -200,6 +203,7 @@ describe('target-defaults plugin', () => {
|
||||
},
|
||||
},
|
||||
workspaceRoot: '/root',
|
||||
configFiles: [],
|
||||
})
|
||||
).toMatchInlineSnapshot(`{}`);
|
||||
});
|
||||
@ -230,6 +234,7 @@ describe('target-defaults plugin', () => {
|
||||
},
|
||||
},
|
||||
workspaceRoot: '/root',
|
||||
configFiles: [],
|
||||
})
|
||||
).toMatchInlineSnapshot(`
|
||||
{
|
||||
@ -278,6 +283,7 @@ describe('target-defaults plugin', () => {
|
||||
},
|
||||
},
|
||||
workspaceRoot: '/root',
|
||||
configFiles: [],
|
||||
});
|
||||
|
||||
const { targets } = result.projects['.'];
|
||||
@ -321,6 +327,7 @@ describe('target-defaults plugin', () => {
|
||||
},
|
||||
},
|
||||
workspaceRoot: '/root',
|
||||
configFiles: [],
|
||||
});
|
||||
|
||||
const { targets } = result.projects['.'];
|
||||
|
||||
@ -182,7 +182,7 @@ export { readNxJson, workspaceLayout } from '../config/configuration';
|
||||
* TODO(v19): Remove this function.
|
||||
*/
|
||||
function getProjectsSyncNoInference(root: string, nxJson: NxJsonConfiguration) {
|
||||
const projectFiles = retrieveProjectConfigurationPaths(
|
||||
const allConfigFiles = retrieveProjectConfigurationPaths(
|
||||
root,
|
||||
getDefaultPluginsSync(root)
|
||||
);
|
||||
@ -199,11 +199,15 @@ function getProjectsSyncNoInference(root: string, nxJson: NxJsonConfiguration) {
|
||||
if (!pattern) {
|
||||
continue;
|
||||
}
|
||||
for (const file of projectFiles) {
|
||||
const matchingConfigFiles = allConfigFiles.filter((file) =>
|
||||
minimatch(file, pattern, { dot: true })
|
||||
);
|
||||
for (const file of matchingConfigFiles) {
|
||||
if (minimatch(file, pattern, { dot: true })) {
|
||||
let r = createNodes(file, options, {
|
||||
nxJsonConfiguration: nxJson,
|
||||
workspaceRoot: root,
|
||||
configFiles: matchingConfigFiles,
|
||||
}) as CreateNodesResult;
|
||||
for (const node in r.projects) {
|
||||
const project = {
|
||||
|
||||
@ -193,13 +193,13 @@ export type ConfigurationResult = {
|
||||
* Transforms a list of project paths into a map of project configurations.
|
||||
*
|
||||
* @param nxJson The NxJson configuration
|
||||
* @param projectFiles A list of files identified as projects
|
||||
* @param workspaceFiles A list of non-ignored workspace files
|
||||
* @param plugins The plugins that should be used to infer project configuration
|
||||
* @param root The workspace root
|
||||
*/
|
||||
export function buildProjectsConfigurationsFromProjectPathsAndPlugins(
|
||||
nxJson: NxJsonConfiguration,
|
||||
projectFiles: string[], // making this parameter allows devkit to pick up newly created projects
|
||||
workspaceFiles: string[], // making this parameter allows devkit to pick up newly created projects
|
||||
plugins: LoadedNxPlugin[],
|
||||
root: string = workspaceRoot
|
||||
): Promise<ConfigurationResult> {
|
||||
@ -222,13 +222,17 @@ export function buildProjectsConfigurationsFromProjectPathsAndPlugins(
|
||||
continue;
|
||||
}
|
||||
|
||||
for (const file of projectFiles) {
|
||||
const matchingConfigFiles: string[] = workspaceFiles.filter(
|
||||
minimatch.filter(pattern, { dot: true })
|
||||
);
|
||||
|
||||
for (const file of matchingConfigFiles) {
|
||||
performance.mark(`${plugin.name}:createNodes:${file} - start`);
|
||||
if (minimatch(file, pattern, { dot: true })) {
|
||||
try {
|
||||
let r = createNodes(file, options, {
|
||||
nxJsonConfiguration: nxJson,
|
||||
workspaceRoot: root,
|
||||
configFiles: matchingConfigFiles,
|
||||
});
|
||||
|
||||
if (r instanceof Promise) {
|
||||
@ -271,7 +275,6 @@ export function buildProjectsConfigurationsFromProjectPathsAndPlugins(
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
// If there are no promises (counter undefined) or all promises have resolved (counter === 0)
|
||||
results.push(
|
||||
Promise.all(pluginResults).then((results) => {
|
||||
|
||||
@ -116,12 +116,12 @@ function _retrieveProjectConfigurations(
|
||||
plugins: LoadedNxPlugin[]
|
||||
): Promise<RetrievedGraphNodes> {
|
||||
const globPatterns = configurationGlobs(plugins);
|
||||
const projectFiles = globWithWorkspaceContext(workspaceRoot, globPatterns);
|
||||
const workspaceFiles = globWithWorkspaceContext(workspaceRoot, globPatterns);
|
||||
|
||||
return createProjectConfigurations(
|
||||
workspaceRoot,
|
||||
nxJson,
|
||||
projectFiles,
|
||||
workspaceFiles,
|
||||
plugins
|
||||
);
|
||||
}
|
||||
@ -152,7 +152,8 @@ export async function retrieveProjectConfigurationsWithoutPluginInference(
|
||||
return projectsWithoutPluginCache.get(cacheKey);
|
||||
}
|
||||
|
||||
const projectFiles = globWithWorkspaceContext(root, projectGlobPatterns);
|
||||
const projectFiles =
|
||||
globWithWorkspaceContext(root, projectGlobPatterns) ?? [];
|
||||
const { projects } = await createProjectConfigurations(
|
||||
root,
|
||||
nxJson,
|
||||
|
||||
@ -32,7 +32,7 @@ export function getIgnoredGlobs(
|
||||
}
|
||||
|
||||
export function getAlwaysIgnore(root?: string) {
|
||||
const paths = ['node_modules', '**/node_modules', '.git'];
|
||||
const paths = ['node_modules', '**/node_modules', '.git', '.nx', '.vscode'];
|
||||
return root ? paths.map((x) => joinPathFragments(root, x)) : paths;
|
||||
}
|
||||
|
||||
|
||||
@ -50,6 +50,10 @@ import { TargetDefaultsPlugin } from '../plugins/target-defaults/target-defaults
|
||||
export interface CreateNodesContext {
|
||||
readonly nxJsonConfiguration: NxJsonConfiguration;
|
||||
readonly workspaceRoot: string;
|
||||
/**
|
||||
* The subset of configuration files which match the createNodes pattern
|
||||
*/
|
||||
readonly configFiles: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
@ -78,7 +82,7 @@ export interface CreateNodesResult {
|
||||
* A pair of file patterns and {@link CreateNodesFunction}
|
||||
*/
|
||||
export type CreateNodes<T = unknown> = readonly [
|
||||
projectFilePattern: string,
|
||||
configFilePattern: string,
|
||||
createNodesFunction: CreateNodesFunction<T>
|
||||
];
|
||||
|
||||
|
||||
@ -24,6 +24,7 @@ describe('@nx/playwright/plugin', () => {
|
||||
},
|
||||
},
|
||||
workspaceRoot: tempFs.tempDir,
|
||||
configFiles: [],
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
@ -34,6 +34,7 @@ describe('@nx/remix/plugin', () => {
|
||||
},
|
||||
},
|
||||
workspaceRoot: tempFs.tempDir,
|
||||
configFiles: [],
|
||||
};
|
||||
tempFs.createFileSync(
|
||||
'package.json',
|
||||
@ -89,6 +90,7 @@ module.exports = {
|
||||
},
|
||||
},
|
||||
workspaceRoot: tempFs.tempDir,
|
||||
configFiles: [],
|
||||
};
|
||||
|
||||
tempFs.createFileSync(
|
||||
|
||||
@ -25,6 +25,7 @@ describe('@nx/rollup/plugin', () => {
|
||||
},
|
||||
},
|
||||
workspaceRoot: tempFs.tempDir,
|
||||
configFiles: [],
|
||||
};
|
||||
|
||||
tempFs.createFileSync('package.json', JSON.stringify({ name: 'mylib' }));
|
||||
@ -93,6 +94,7 @@ module.exports = config;
|
||||
},
|
||||
},
|
||||
workspaceRoot: tempFs.tempDir,
|
||||
configFiles: [],
|
||||
};
|
||||
|
||||
tempFs.createFileSync(
|
||||
|
||||
@ -17,6 +17,7 @@ describe('@nx/storybook/plugin', () => {
|
||||
},
|
||||
},
|
||||
workspaceRoot: tempFs.tempDir,
|
||||
configFiles: [],
|
||||
};
|
||||
tempFs.createFileSync(
|
||||
'my-app/project.json',
|
||||
|
||||
@ -19,6 +19,7 @@ describe('@nx/webpack/plugin', () => {
|
||||
},
|
||||
},
|
||||
workspaceRoot: tempFs.tempDir,
|
||||
configFiles: [],
|
||||
};
|
||||
|
||||
tempFs.createFileSync(
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user