fix(linter): refactor pcv3 plugin, expose configFiles on context (#21677)

This commit is contained in:
James Henry 2024-03-16 00:29:13 +04:00 committed by GitHub
parent cea7e93c86
commit 1fe5b98f45
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
24 changed files with 668 additions and 248 deletions

View File

@ -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)

View File

@ -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`[] \| `"*"`\>

View File

@ -34,6 +34,7 @@ describe('@nx/cypress/plugin', () => {
},
},
workspaceRoot: tempFs.tempDir,
configFiles: [],
};
});

View File

@ -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]];

View File

@ -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);

View File

@ -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,
};
}

View File

@ -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;
}

View File

@ -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');
}

View File

@ -20,6 +20,7 @@ describe('@nx/jest/plugin', () => {
},
},
workspaceRoot: tempFs.tempDir,
configFiles: [],
};
await tempFs.createFiles({

View File

@ -18,6 +18,7 @@ describe('@nx/next/plugin', () => {
},
},
workspaceRoot: '',
configFiles: [],
};
});
@ -53,6 +54,7 @@ describe('@nx/next/plugin', () => {
},
},
workspaceRoot: tempFs.tempDir,
configFiles: [],
};
tempFs.createFileSync(

View File

@ -38,6 +38,7 @@ describe('@nx/nuxt/plugin', () => {
},
},
workspaceRoot: '',
configFiles: [],
};
});
@ -72,6 +73,7 @@ describe('@nx/nuxt/plugin', () => {
},
},
workspaceRoot: tempFs.tempDir,
configFiles: [],
};
tempFs.createFileSync(

View File

@ -14,6 +14,7 @@ describe('nx project.json plugin', () => {
context = {
nxJsonConfiguration: {},
workspaceRoot: '/root',
configFiles: [],
};
});

View File

@ -12,6 +12,7 @@ describe('nx project.json plugin', () => {
context = {
nxJsonConfiguration: {},
workspaceRoot: '/root',
configFiles: [],
};
});

View File

@ -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['.'];

View File

@ -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 = {

View File

@ -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) => {

View File

@ -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,

View File

@ -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;
}

View File

@ -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>
];

View File

@ -24,6 +24,7 @@ describe('@nx/playwright/plugin', () => {
},
},
workspaceRoot: tempFs.tempDir,
configFiles: [],
};
});

View File

@ -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(

View File

@ -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(

View File

@ -17,6 +17,7 @@ describe('@nx/storybook/plugin', () => {
},
},
workspaceRoot: tempFs.tempDir,
configFiles: [],
};
tempFs.createFileSync(
'my-app/project.json',

View File

@ -19,6 +19,7 @@ describe('@nx/webpack/plugin', () => {
},
},
workspaceRoot: tempFs.tempDir,
configFiles: [],
};
tempFs.createFileSync(