feat(linter): migrate to create-nodes-v2 (#26302)
<!-- Please make sure you have read the submission guidelines before posting an PR --> <!-- https://github.com/nrwl/nx/blob/master/CONTRIBUTING.md#-submitting-a-pr --> <!-- Please make sure that your commit message follows our format --> <!-- Example: `fix(nx): must begin with lowercase` --> ## Current Behavior No cache for eslint nodes ## Expected Behavior v2 cache for eslint nodes ## Related Issue(s) <!-- Please link the issue being fixed so it gets closed when this is merged. --> Fixes # --------- Co-authored-by: FrozenPandaz <jasonjean1993@gmail.com>
This commit is contained in:
parent
cf0142d711
commit
0594debfef
@ -5,8 +5,8 @@ import {
|
|||||||
type TargetConfiguration,
|
type TargetConfiguration,
|
||||||
type Tree,
|
type Tree,
|
||||||
} from '@nx/devkit';
|
} from '@nx/devkit';
|
||||||
import { createNodes, EslintPluginOptions } from '../../plugins/plugin';
|
import { createNodesV2, EslintPluginOptions } from '../../plugins/plugin';
|
||||||
import { migrateExecutorToPluginV1 } from '@nx/devkit/src/generators/plugin-migrations/executor-to-plugin-migrator';
|
import { migrateExecutorToPlugin } from '@nx/devkit/src/generators/plugin-migrations/executor-to-plugin-migrator';
|
||||||
import { targetOptionsToCliMap } from './lib/target-options-map';
|
import { targetOptionsToCliMap } from './lib/target-options-map';
|
||||||
import { interpolate } from 'nx/src/tasks-runner/utils';
|
import { interpolate } from 'nx/src/tasks-runner/utils';
|
||||||
|
|
||||||
@ -19,26 +19,26 @@ export async function convertToInferred(tree: Tree, options: Schema) {
|
|||||||
const projectGraph = await createProjectGraphAsync();
|
const projectGraph = await createProjectGraphAsync();
|
||||||
|
|
||||||
const migratedProjectsModern =
|
const migratedProjectsModern =
|
||||||
await migrateExecutorToPluginV1<EslintPluginOptions>(
|
await migrateExecutorToPlugin<EslintPluginOptions>(
|
||||||
tree,
|
tree,
|
||||||
projectGraph,
|
projectGraph,
|
||||||
'@nx/eslint:lint',
|
'@nx/eslint:lint',
|
||||||
'@nx/eslint/plugin',
|
'@nx/eslint/plugin',
|
||||||
(targetName) => ({ targetName }),
|
(targetName) => ({ targetName }),
|
||||||
postTargetTransformer,
|
postTargetTransformer,
|
||||||
createNodes,
|
createNodesV2,
|
||||||
options.project
|
options.project
|
||||||
);
|
);
|
||||||
|
|
||||||
const migratedProjectsLegacy =
|
const migratedProjectsLegacy =
|
||||||
await migrateExecutorToPluginV1<EslintPluginOptions>(
|
await migrateExecutorToPlugin<EslintPluginOptions>(
|
||||||
tree,
|
tree,
|
||||||
projectGraph,
|
projectGraph,
|
||||||
'@nrwl/linter:eslint',
|
'@nrwl/linter:eslint',
|
||||||
'@nx/eslint/plugin',
|
'@nx/eslint/plugin',
|
||||||
(targetName) => ({ targetName }),
|
(targetName) => ({ targetName }),
|
||||||
postTargetTransformer,
|
postTargetTransformer,
|
||||||
createNodes,
|
createNodesV2,
|
||||||
options.project
|
options.project
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@ -8,10 +8,10 @@ import {
|
|||||||
Tree,
|
Tree,
|
||||||
updateNxJson,
|
updateNxJson,
|
||||||
} from '@nx/devkit';
|
} from '@nx/devkit';
|
||||||
import { addPluginV1 } from '@nx/devkit/src/utils/add-plugin';
|
import { addPlugin } from '@nx/devkit/src/utils/add-plugin';
|
||||||
import { eslintVersion, nxVersion } from '../../utils/versions';
|
import { eslintVersion, nxVersion } from '../../utils/versions';
|
||||||
import { findEslintFile } from '../utils/eslint-file';
|
import { findEslintFile } from '../utils/eslint-file';
|
||||||
import { createNodes } from '../../plugins/plugin';
|
import { createNodesV2 } from '../../plugins/plugin';
|
||||||
import { hasEslintPlugin } from '../utils/plugin';
|
import { hasEslintPlugin } from '../utils/plugin';
|
||||||
|
|
||||||
export interface LinterInitOptions {
|
export interface LinterInitOptions {
|
||||||
@ -73,11 +73,11 @@ export async function initEsLint(
|
|||||||
];
|
];
|
||||||
|
|
||||||
if (rootEslintFile && options.addPlugin && !hasPlugin) {
|
if (rootEslintFile && options.addPlugin && !hasPlugin) {
|
||||||
await addPluginV1(
|
await addPlugin(
|
||||||
tree,
|
tree,
|
||||||
graph,
|
graph,
|
||||||
'@nx/eslint/plugin',
|
'@nx/eslint/plugin',
|
||||||
createNodes,
|
createNodesV2,
|
||||||
{
|
{
|
||||||
targetName: lintTargetNames,
|
targetName: lintTargetNames,
|
||||||
},
|
},
|
||||||
@ -94,11 +94,11 @@ export async function initEsLint(
|
|||||||
updateProductionFileset(tree);
|
updateProductionFileset(tree);
|
||||||
|
|
||||||
if (options.addPlugin) {
|
if (options.addPlugin) {
|
||||||
await addPluginV1(
|
await addPlugin(
|
||||||
tree,
|
tree,
|
||||||
graph,
|
graph,
|
||||||
'@nx/eslint/plugin',
|
'@nx/eslint/plugin',
|
||||||
createNodes,
|
createNodesV2,
|
||||||
{
|
{
|
||||||
targetName: lintTargetNames,
|
targetName: lintTargetNames,
|
||||||
},
|
},
|
||||||
|
|||||||
@ -1,13 +1,21 @@
|
|||||||
import { CreateNodesContext } from '@nx/devkit';
|
import { CreateNodesContextV2 } from '@nx/devkit';
|
||||||
import { minimatch } from 'minimatch';
|
import { minimatch } from 'minimatch';
|
||||||
import { TempFs } from 'nx/src/internal-testing-utils/temp-fs';
|
import { TempFs } from 'nx/src/internal-testing-utils/temp-fs';
|
||||||
import { createNodes } from './plugin';
|
import { createNodesV2 } from './plugin';
|
||||||
|
import { mkdirSync, rmdirSync } from 'fs';
|
||||||
|
|
||||||
|
jest.mock('nx/src/utils/cache-directory', () => ({
|
||||||
|
...jest.requireActual('nx/src/utils/cache-directory'),
|
||||||
|
projectGraphCacheDirectory: 'tmp/project-graph-cache',
|
||||||
|
}));
|
||||||
|
|
||||||
describe('@nx/eslint/plugin', () => {
|
describe('@nx/eslint/plugin', () => {
|
||||||
let context: CreateNodesContext;
|
let context: CreateNodesContextV2;
|
||||||
let tempFs: TempFs;
|
let tempFs: TempFs;
|
||||||
|
let configFiles: string[] = [];
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
|
mkdirSync('tmp/project-graph-cache', { recursive: true });
|
||||||
tempFs = new TempFs('eslint-plugin');
|
tempFs = new TempFs('eslint-plugin');
|
||||||
context = {
|
context = {
|
||||||
nxJsonConfiguration: {
|
nxJsonConfiguration: {
|
||||||
@ -24,7 +32,6 @@ describe('@nx/eslint/plugin', () => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
workspaceRoot: tempFs.tempDir,
|
workspaceRoot: tempFs.tempDir,
|
||||||
configFiles: [],
|
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -32,6 +39,7 @@ describe('@nx/eslint/plugin', () => {
|
|||||||
jest.resetModules();
|
jest.resetModules();
|
||||||
tempFs.cleanup();
|
tempFs.cleanup();
|
||||||
tempFs = null;
|
tempFs = null;
|
||||||
|
rmdirSync('tmp/project-graph-cache', { recursive: true });
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not create any nodes when there are no eslint configs', async () => {
|
it('should not create any nodes when there are no eslint configs', async () => {
|
||||||
@ -617,27 +625,30 @@ describe('@nx/eslint/plugin', () => {
|
|||||||
|
|
||||||
function createFiles(fileSys: Record<string, string>) {
|
function createFiles(fileSys: Record<string, string>) {
|
||||||
tempFs.createFilesSync(fileSys);
|
tempFs.createFilesSync(fileSys);
|
||||||
// @ts-expect-error update otherwise readonly property for testing
|
configFiles = getMatchingFiles(Object.keys(fileSys));
|
||||||
context.configFiles = getMatchingFiles(Object.keys(fileSys));
|
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
|
||||||
function getMatchingFiles(allConfigFiles: string[]): string[] {
|
function getMatchingFiles(allConfigFiles: string[]): string[] {
|
||||||
return allConfigFiles.filter((file) =>
|
return allConfigFiles.filter((file) =>
|
||||||
minimatch(file, createNodes[0], { dot: true })
|
minimatch(file, createNodesV2[0], { dot: true })
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function invokeCreateNodesOnMatchingFiles(
|
async function invokeCreateNodesOnMatchingFiles(
|
||||||
context: CreateNodesContext,
|
context: CreateNodesContextV2,
|
||||||
targetName: string
|
targetName: string
|
||||||
) {
|
) {
|
||||||
const aggregateProjects: Record<string, any> = {};
|
const aggregateProjects: Record<string, any> = {};
|
||||||
for (const file of context.configFiles) {
|
const results = await createNodesV2[1](
|
||||||
const nodes = await createNodes[1](file, { targetName }, context);
|
configFiles,
|
||||||
|
{ targetName },
|
||||||
|
context
|
||||||
|
);
|
||||||
|
for (const [, nodes] of results) {
|
||||||
Object.assign(aggregateProjects, nodes.projects);
|
Object.assign(aggregateProjects, nodes.projects);
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
projects: aggregateProjects,
|
projects: aggregateProjects,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
|||||||
@ -2,12 +2,20 @@ import {
|
|||||||
CreateNodes,
|
CreateNodes,
|
||||||
CreateNodesContext,
|
CreateNodesContext,
|
||||||
CreateNodesResult,
|
CreateNodesResult,
|
||||||
|
CreateNodesV2,
|
||||||
TargetConfiguration,
|
TargetConfiguration,
|
||||||
|
createNodesFromFiles,
|
||||||
|
logger,
|
||||||
|
readJsonFile,
|
||||||
|
writeJsonFile,
|
||||||
} from '@nx/devkit';
|
} from '@nx/devkit';
|
||||||
import { existsSync } from 'node:fs';
|
import { existsSync } from 'node:fs';
|
||||||
import { dirname, join, normalize, sep } from 'node:path';
|
import { dirname, join, normalize, sep } from 'node:path';
|
||||||
import { combineGlobPatterns } from 'nx/src/utils/globs';
|
import { combineGlobPatterns } from 'nx/src/utils/globs';
|
||||||
import { globWithWorkspaceContext } from 'nx/src/utils/workspace-context';
|
import {
|
||||||
|
globWithWorkspaceContext,
|
||||||
|
hashWithWorkspaceContext,
|
||||||
|
} from 'nx/src/utils/workspace-context';
|
||||||
import {
|
import {
|
||||||
ESLINT_CONFIG_FILENAMES,
|
ESLINT_CONFIG_FILENAMES,
|
||||||
baseEsLintConfigFile,
|
baseEsLintConfigFile,
|
||||||
@ -16,6 +24,9 @@ import {
|
|||||||
} from '../utils/config-file';
|
} from '../utils/config-file';
|
||||||
import { resolveESLintClass } from '../utils/resolve-eslint-class';
|
import { resolveESLintClass } from '../utils/resolve-eslint-class';
|
||||||
import { gte } from 'semver';
|
import { gte } from 'semver';
|
||||||
|
import { projectGraphCacheDirectory } from 'nx/src/utils/cache-directory';
|
||||||
|
import { hashObject } from 'nx/src/hasher/file-hasher';
|
||||||
|
import { calculateHashForCreateNodes } from '@nx/devkit/src/utils/calculate-hash-for-create-nodes';
|
||||||
|
|
||||||
export interface EslintPluginOptions {
|
export interface EslintPluginOptions {
|
||||||
targetName?: string;
|
targetName?: string;
|
||||||
@ -23,14 +34,31 @@ export interface EslintPluginOptions {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const DEFAULT_EXTENSIONS = ['ts', 'tsx', 'js', 'jsx', 'html', 'vue'];
|
const DEFAULT_EXTENSIONS = ['ts', 'tsx', 'js', 'jsx', 'html', 'vue'];
|
||||||
|
const ESLINT_CONFIG_GLOB = combineGlobPatterns([
|
||||||
export const createNodes: CreateNodes<EslintPluginOptions> = [
|
|
||||||
combineGlobPatterns([
|
|
||||||
...ESLINT_CONFIG_FILENAMES.map((f) => `**/${f}`),
|
...ESLINT_CONFIG_FILENAMES.map((f) => `**/${f}`),
|
||||||
baseEsLintConfigFile,
|
baseEsLintConfigFile,
|
||||||
baseEsLintFlatConfigFile,
|
baseEsLintFlatConfigFile,
|
||||||
]),
|
]);
|
||||||
async (configFilePath, options, context) => {
|
|
||||||
|
type EslintProjects = Awaited<ReturnType<typeof getProjectsUsingESLintConfig>>;
|
||||||
|
|
||||||
|
function readTargetsCache(cachePath: string): Record<string, EslintProjects> {
|
||||||
|
return existsSync(cachePath) ? readJsonFile(cachePath) : {};
|
||||||
|
}
|
||||||
|
|
||||||
|
function writeTargetsToCache(
|
||||||
|
cachePath: string,
|
||||||
|
results: Record<string, EslintProjects>
|
||||||
|
) {
|
||||||
|
writeJsonFile(cachePath, results);
|
||||||
|
}
|
||||||
|
|
||||||
|
const internalCreateNodes = async (
|
||||||
|
configFilePath: string,
|
||||||
|
options: EslintPluginOptions,
|
||||||
|
context: CreateNodesContext,
|
||||||
|
projectsCache: Record<string, CreateNodesResult['projects']>
|
||||||
|
): Promise<CreateNodesResult> => {
|
||||||
options = normalizeOptions(options);
|
options = normalizeOptions(options);
|
||||||
const configDir = dirname(configFilePath);
|
const configDir = dirname(configFilePath);
|
||||||
|
|
||||||
@ -53,12 +81,9 @@ export const createNodes: CreateNodes<EslintPluginOptions> = [
|
|||||||
|
|
||||||
const projectFiles = await globWithWorkspaceContext(
|
const projectFiles = await globWithWorkspaceContext(
|
||||||
context.workspaceRoot,
|
context.workspaceRoot,
|
||||||
[
|
['project.json', 'package.json', '**/project.json', '**/package.json'].map(
|
||||||
'project.json',
|
(f) => join(configDir, f)
|
||||||
'package.json',
|
),
|
||||||
'**/project.json',
|
|
||||||
'**/package.json',
|
|
||||||
].map((f) => join(configDir, f)),
|
|
||||||
nestedEslintRootPatterns.length ? nestedEslintRootPatterns : undefined
|
nestedEslintRootPatterns.length ? nestedEslintRootPatterns : undefined
|
||||||
);
|
);
|
||||||
// dedupe and sort project roots by depth for more efficient traversal
|
// dedupe and sort project roots by depth for more efficient traversal
|
||||||
@ -71,6 +96,7 @@ export const createNodes: CreateNodes<EslintPluginOptions> = [
|
|||||||
const eslintVersion = ESLint.version;
|
const eslintVersion = ESLint.version;
|
||||||
const childProjectRoots = new Set<string>();
|
const childProjectRoots = new Set<string>();
|
||||||
|
|
||||||
|
const projects: CreateNodesResult['projects'] = {};
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
dedupedProjectRoots.map(async (childProjectRoot, index) => {
|
dedupedProjectRoots.map(async (childProjectRoot, index) => {
|
||||||
// anything after is either a nested project or a sibling project, can be excluded
|
// anything after is either a nested project or a sibling project, can be excluded
|
||||||
@ -83,31 +109,85 @@ export const createNodes: CreateNodes<EslintPluginOptions> = [
|
|||||||
// exclude nested eslint roots and nested project roots
|
// exclude nested eslint roots and nested project roots
|
||||||
[...nestedEslintRootPatterns, ...nestedProjectRootPatterns]
|
[...nestedEslintRootPatterns, ...nestedProjectRootPatterns]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const parentConfigs = context.configFiles.filter((eslintConfig) =>
|
||||||
|
isSubDir(childProjectRoot, dirname(eslintConfig))
|
||||||
|
);
|
||||||
|
const hash = await calculateHashForCreateNodes(
|
||||||
|
childProjectRoot,
|
||||||
|
options,
|
||||||
|
context,
|
||||||
|
[...parentConfigs, join(childProjectRoot, '.eslintignore')]
|
||||||
|
);
|
||||||
|
|
||||||
|
if (projectsCache[hash]) {
|
||||||
|
// We can reuse the projects in the cache.
|
||||||
|
Object.assign(projects, projectsCache[hash]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
const eslint = new ESLint({
|
const eslint = new ESLint({
|
||||||
cwd: join(context.workspaceRoot, childProjectRoot),
|
cwd: join(context.workspaceRoot, childProjectRoot),
|
||||||
});
|
});
|
||||||
for (const file of lintableFiles) {
|
for (const file of lintableFiles) {
|
||||||
if (
|
if (!(await eslint.isPathIgnored(join(context.workspaceRoot, file)))) {
|
||||||
!(await eslint.isPathIgnored(join(context.workspaceRoot, file)))
|
|
||||||
) {
|
|
||||||
childProjectRoots.add(childProjectRoot);
|
childProjectRoots.add(childProjectRoot);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
const uniqueChildProjectRoots = Array.from(childProjectRoots);
|
const uniqueChildProjectRoots = Array.from(childProjectRoots);
|
||||||
|
|
||||||
return {
|
const projectsForRoot = getProjectsUsingESLintConfig(
|
||||||
projects: getProjectsUsingESLintConfig(
|
|
||||||
configFilePath,
|
configFilePath,
|
||||||
uniqueChildProjectRoots,
|
uniqueChildProjectRoots,
|
||||||
eslintVersion,
|
eslintVersion,
|
||||||
options,
|
options,
|
||||||
context
|
context
|
||||||
),
|
);
|
||||||
|
|
||||||
|
if (Object.keys(projectsForRoot).length > 0) {
|
||||||
|
Object.assign(projects, projectsForRoot);
|
||||||
|
// Store those projects into the cache;
|
||||||
|
projectsCache[hash] = projectsForRoot;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
projects,
|
||||||
};
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const createNodesV2: CreateNodesV2<EslintPluginOptions> = [
|
||||||
|
ESLINT_CONFIG_GLOB,
|
||||||
|
async (configFiles, options, context) => {
|
||||||
|
const optionsHash = hashObject(options);
|
||||||
|
const cachePath = join(
|
||||||
|
projectGraphCacheDirectory,
|
||||||
|
`eslint-${optionsHash}.hash`
|
||||||
|
);
|
||||||
|
const targetsCache = readTargetsCache(cachePath);
|
||||||
|
try {
|
||||||
|
return await createNodesFromFiles(
|
||||||
|
(configFile, options, context) =>
|
||||||
|
internalCreateNodes(configFile, options, context, targetsCache),
|
||||||
|
configFiles,
|
||||||
|
options,
|
||||||
|
context
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
writeTargetsToCache(cachePath, targetsCache);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const createNodes: CreateNodes<EslintPluginOptions> = [
|
||||||
|
ESLINT_CONFIG_GLOB,
|
||||||
|
(configFilePath, options, context) => {
|
||||||
|
logger.warn(
|
||||||
|
'`createNodes` is deprecated. Update your plugin to utilize createNodesV2 instead. In Nx 20, this will change to the createNodesV2 API.'
|
||||||
|
);
|
||||||
|
return internalCreateNodes(configFilePath, options, context, {});
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user