feat(linter): optimize lint performance. resolves #5210 (#5576)

* feat(linter): optimize containsFile function

related to #5210

* feat(linter): optimize find project function

related to #5210

* feat(core): cleanup target project locator

* fix(core): fix false positive match on resolvedModule

* chore(core): mark old code for removal

* feat(linter): move npm check before expensive typescript resolution

* feat(linter): improve performance of extension removal

* feat(linter): improve I/O operations

* feat(linter): read ts config only once in project locator

* feat(linter): remove double path normalization from rule

normalization is already part of getSourceFilePath function

* feat(linter): run find source only once

* feat(linter): defer source file path calculation will after whitelist check

* feat(linter): map project graph node files to hash map

* fix(linter): map projectGraph only once per runtime

* feat(linter): introduce mapped project graph type
This commit is contained in:
Miroslav Jonaš 2021-05-18 16:41:12 +02:00 committed by GitHub
parent 75cbd54818
commit ff3cc38b0c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 139 additions and 86 deletions

View File

@ -19,8 +19,8 @@ export interface ProjectFileMap {
/** /**
* A Graph of projects in the workspace and dependencies between them * A Graph of projects in the workspace and dependencies between them
*/ */
export interface ProjectGraph { export interface ProjectGraph<T = any> {
nodes: Record<string, ProjectGraphNode>; nodes: Record<string, ProjectGraphNode<T>>;
dependencies: Record<string, ProjectGraphDependency[]>; dependencies: Record<string, ProjectGraphDependency[]>;
// this is optional otherwise it might break folks who use project graph creation // this is optional otherwise it might break folks who use project graph creation

View File

@ -9,8 +9,10 @@ import {
hasNoneOfTheseTags, hasNoneOfTheseTags,
isAbsoluteImportIntoAnotherProject, isAbsoluteImportIntoAnotherProject,
isRelativeImportIntoAnotherProject, isRelativeImportIntoAnotherProject,
mapProjectGraphFiles,
matchImportWithWildcard, matchImportWithWildcard,
onlyLoadChildren, onlyLoadChildren,
MappedProjectGraph,
} from '@nrwl/workspace/src/utils/runtime-lint-utils'; } from '@nrwl/workspace/src/utils/runtime-lint-utils';
import { import {
AST_NODE_TYPES, AST_NODE_TYPES,
@ -20,7 +22,6 @@ import { createESLintRule } from '../utils/create-eslint-rule';
import { normalizePath } from '@nrwl/devkit'; import { normalizePath } from '@nrwl/devkit';
import { import {
isNpmProject, isNpmProject,
ProjectGraph,
ProjectType, ProjectType,
} from '@nrwl/workspace/src/core/project-graph'; } from '@nrwl/workspace/src/core/project-graph';
import { readNxJson } from '@nrwl/workspace/src/core/file-utils'; import { readNxJson } from '@nrwl/workspace/src/core/file-utils';
@ -121,7 +122,9 @@ export default createESLintRule<Options, MessageIds>({
if (!(global as any).projectGraph) { if (!(global as any).projectGraph) {
const nxJson = readNxJson(); const nxJson = readNxJson();
(global as any).npmScope = nxJson.npmScope; (global as any).npmScope = nxJson.npmScope;
(global as any).projectGraph = readCurrentProjectGraph(); (global as any).projectGraph = mapProjectGraphFiles(
readCurrentProjectGraph()
);
} }
if (!(global as any).projectGraph) { if (!(global as any).projectGraph) {
@ -129,7 +132,7 @@ export default createESLintRule<Options, MessageIds>({
} }
const npmScope = (global as any).npmScope; const npmScope = (global as any).npmScope;
const projectGraph = (global as any).projectGraph as ProjectGraph; const projectGraph = (global as any).projectGraph as MappedProjectGraph;
if (!(global as any).targetProjectLocator) { if (!(global as any).targetProjectLocator) {
(global as any).targetProjectLocator = new TargetProjectLocator( (global as any).targetProjectLocator = new TargetProjectLocator(
@ -159,23 +162,25 @@ export default createESLintRule<Options, MessageIds>({
const imp = node.source.value as string; const imp = node.source.value as string;
const sourceFilePath = getSourceFilePath(
normalizePath(context.getFilename()),
projectPath
);
// whitelisted import // whitelisted import
if (allow.some((a) => matchImportWithWildcard(a, imp))) { if (allow.some((a) => matchImportWithWildcard(a, imp))) {
return; return;
} }
const sourceFilePath = getSourceFilePath(
context.getFilename(),
projectPath
);
// check for relative and absolute imports // check for relative and absolute imports
const sourceProject = findSourceProject(projectGraph, sourceFilePath);
if ( if (
isRelativeImportIntoAnotherProject( isRelativeImportIntoAnotherProject(
imp, imp,
projectPath, projectPath,
projectGraph, projectGraph,
sourceFilePath sourceFilePath,
sourceProject
) || ) ||
isAbsoluteImportIntoAnotherProject(imp) isAbsoluteImportIntoAnotherProject(imp)
) { ) {
@ -189,7 +194,6 @@ export default createESLintRule<Options, MessageIds>({
return; return;
} }
const sourceProject = findSourceProject(projectGraph, sourceFilePath);
const targetProject = findProjectUsingImport( const targetProject = findProjectUsingImport(
projectGraph, projectGraph,
targetProjectLocator, targetProjectLocator,

View File

@ -11,6 +11,7 @@ import enforceModuleBoundaries, {
RULE_NAME as enforceModuleBoundariesRuleName, RULE_NAME as enforceModuleBoundariesRuleName,
} from '../../src/rules/enforce-module-boundaries'; } from '../../src/rules/enforce-module-boundaries';
import { TargetProjectLocator } from '@nrwl/workspace/src/core/target-project-locator'; import { TargetProjectLocator } from '@nrwl/workspace/src/core/target-project-locator';
import { mapProjectGraphFiles } from '@nrwl/workspace/src/utils/runtime-lint-utils';
jest.mock('fs', () => require('memfs').fs); jest.mock('fs', () => require('memfs').fs);
jest.mock('../../../workspace/src/utilities/app-root', () => ({ jest.mock('../../../workspace/src/utilities/app-root', () => ({
appRootPath: '/root', appRootPath: '/root',
@ -72,7 +73,7 @@ const fileSys = {
'./tsconfig.base.json': JSON.stringify(tsconfig), './tsconfig.base.json': JSON.stringify(tsconfig),
}; };
describe('Enforce Module Boundaries', () => { describe('Enforce Module Boundaries (eslint)', () => {
beforeEach(() => { beforeEach(() => {
vol.fromJSON(fileSys, '/root'); vol.fromJSON(fileSys, '/root');
}); });
@ -1536,7 +1537,7 @@ function runRule(
): TSESLint.Linter.LintMessage[] { ): TSESLint.Linter.LintMessage[] {
(global as any).projectPath = `${process.cwd()}/proj`; (global as any).projectPath = `${process.cwd()}/proj`;
(global as any).npmScope = 'mycompany'; (global as any).npmScope = 'mycompany';
(global as any).projectGraph = projectGraph; (global as any).projectGraph = mapProjectGraphFiles(projectGraph);
(global as any).targetProjectLocator = new TargetProjectLocator( (global as any).targetProjectLocator = new TargetProjectLocator(
projectGraph.nodes projectGraph.nodes
); );

View File

@ -149,7 +149,7 @@ function projectsToRun(nxArgs: NxArgs, projectGraph: ProjectGraph) {
} }
function applyExclude( function applyExclude(
projects: Record<string, ProjectGraphNode<any>>, projects: Record<string, ProjectGraphNode>,
nxArgs: NxArgs nxArgs: NxArgs
) { ) {
return Object.keys(projects) return Object.keys(projects)
@ -161,7 +161,7 @@ function applyExclude(
} }
function applyOnlyFailed( function applyOnlyFailed(
projectsNotExcluded: Record<string, ProjectGraphNode<any>>, projectsNotExcluded: Record<string, ProjectGraphNode>,
nxArgs: NxArgs, nxArgs: NxArgs,
env: Environment env: Environment
) { ) {

View File

@ -80,7 +80,7 @@ function applyExclude(
} }
function applyOnlyFailed( function applyOnlyFailed(
projectsNotExcluded: Record<string, ProjectGraphNode<any>>, projectsNotExcluded: Record<string, ProjectGraphNode>,
nxArgs: NxArgs, nxArgs: NxArgs,
env: Environment env: Environment
) { ) {

View File

@ -151,7 +151,7 @@ function getIgnoredGlobs() {
return ig; return ig;
} }
function readFileIfExisting(path: string) { export function readFileIfExisting(path: string) {
return existsSync(path) ? readFileSync(path, 'utf-8') : ''; return existsSync(path) ? readFileSync(path, 'utf-8') : '';
} }

View File

@ -1,9 +1,5 @@
import { ProjectGraphBuilder } from './project-graph-builder'; import { ProjectGraphBuilder } from './project-graph-builder';
import { import { ProjectGraph, ProjectGraphNode } from './project-graph-models';
ProjectGraph,
ProjectGraphNode,
ProjectGraphNodeRecords,
} from './project-graph-models';
const reverseMemo = new Map<ProjectGraph, ProjectGraph>(); const reverseMemo = new Map<ProjectGraph, ProjectGraph>();
@ -61,7 +57,7 @@ export function isNpmProject(
return project.type === 'npm'; return project.type === 'npm';
} }
export function getSortedProjectNodes(nodes: ProjectGraphNodeRecords) { export function getSortedProjectNodes(nodes: Record<string, ProjectGraphNode>) {
return Object.values(nodes).sort((nodeA, nodeB) => { return Object.values(nodes).sort((nodeA, nodeB) => {
// If a or b is not a nx project, leave them in the same spot // If a or b is not a nx project, leave them in the same spot
if (!isWorkspaceProject(nodeA) && !isWorkspaceProject(nodeB)) { if (!isWorkspaceProject(nodeA) && !isWorkspaceProject(nodeB)) {

View File

@ -1,9 +1,6 @@
import { resolveModuleByImport } from '../utilities/typescript'; import { resolveModuleByImport } from '../utilities/typescript';
import { defaultFileRead, normalizedProjectRoot } from './file-utils'; import { normalizedProjectRoot, readFileIfExisting } from './file-utils';
import { import { ProjectGraphNode } from './project-graph/project-graph-models';
ProjectGraphNode,
ProjectGraphNodeRecords,
} from './project-graph/project-graph-models';
import { import {
getSortedProjectNodes, getSortedProjectNodes,
isNpmProject, isNpmProject,
@ -29,14 +26,12 @@ export class TargetProjectLocator {
} as ProjectGraphNode) } as ProjectGraphNode)
); );
private npmProjects = this.sortedProjects.filter(isNpmProject); private npmProjects = this.sortedProjects.filter(isNpmProject);
private tsConfigPath = this.getRootTsConfigPath(); private tsConfig = this.getRootTsConfig();
private absTsConfigPath = join(appRootPath, this.tsConfigPath); private paths = this.tsConfig.config?.compilerOptions?.paths;
private paths = parseJsonWithComments(defaultFileRead(this.tsConfigPath))
?.compilerOptions?.paths;
private typescriptResolutionCache = new Map<string, string | null>(); private typescriptResolutionCache = new Map<string, string | null>();
private npmResolutionCache = new Map<string, string | null>(); private npmResolutionCache = new Map<string, string | null>();
constructor(private nodes: ProjectGraphNodeRecords) {} constructor(private readonly nodes: Record<string, ProjectGraphNode>) {}
/** /**
* Find a project based on its import * Find a project based on its import
@ -73,6 +68,12 @@ export class TargetProjectLocator {
} }
} }
// try to find npm package before using expensive typescript resolution
const npmProject = this.findNpmPackage(importExpr);
if (npmProject || this.npmResolutionCache.has(importExpr)) {
return npmProject;
}
let resolvedModule: string; let resolvedModule: string;
if (this.typescriptResolutionCache.has(normalizedImportExpr)) { if (this.typescriptResolutionCache.has(normalizedImportExpr)) {
resolvedModule = this.typescriptResolutionCache.get(normalizedImportExpr); resolvedModule = this.typescriptResolutionCache.get(normalizedImportExpr);
@ -80,7 +81,7 @@ export class TargetProjectLocator {
resolvedModule = resolveModuleByImport( resolvedModule = resolveModuleByImport(
normalizedImportExpr, normalizedImportExpr,
filePath, filePath,
this.absTsConfigPath this.tsConfig.absolutePath
); );
this.typescriptResolutionCache.set( this.typescriptResolutionCache.set(
normalizedImportExpr, normalizedImportExpr,
@ -89,12 +90,13 @@ export class TargetProjectLocator {
} }
// TODO: vsavkin temporary workaround. Remove it once we reworking handling of npm packages. // TODO: vsavkin temporary workaround. Remove it once we reworking handling of npm packages.
if (resolvedModule && resolvedModule.indexOf('/node_modules/') === -1) { if (resolvedModule && resolvedModule.indexOf('node_modules/') === -1) {
const resolvedProject = this.findProjectOfResolvedModule(resolvedModule); const resolvedProject = this.findProjectOfResolvedModule(resolvedModule);
if (resolvedProject) { if (resolvedProject) {
return resolvedProject; return resolvedProject;
} }
} }
// TODO: meeroslav this block should be probably removed
const importedProject = this.sortedWorkspaceProjects.find((p) => { const importedProject = this.sortedWorkspaceProjects.find((p) => {
const projectImport = `@${npmScope}/${p.data.normalizedRoot}`; const projectImport = `@${npmScope}/${p.data.normalizedRoot}`;
return ( return (
@ -102,23 +104,28 @@ export class TargetProjectLocator {
normalizedImportExpr.startsWith(`${projectImport}/`) normalizedImportExpr.startsWith(`${projectImport}/`)
); );
}); });
if (importedProject) return importedProject.name; if (importedProject) {
return importedProject.name;
}
const npmProject = this.findNpmPackage(importExpr); // nothing found, cache for later
return npmProject ? npmProject : null; this.npmResolutionCache.set(importExpr, undefined);
return null;
} }
private findNpmPackage(npmImport: string) { private findNpmPackage(npmImport: string): string | undefined {
if (this.npmResolutionCache.has(npmImport)) { if (this.npmResolutionCache.has(npmImport)) {
return this.npmResolutionCache.get(npmImport); return this.npmResolutionCache.get(npmImport);
} else { } else {
const pkgName = this.npmProjects.find( const pkg = this.npmProjects.find(
(pkg) => (pkg) =>
npmImport === pkg.data.packageName || npmImport === pkg.data.packageName ||
npmImport.startsWith(`${pkg.data.packageName}/`) npmImport.startsWith(`${pkg.data.packageName}/`)
)?.name; );
this.npmResolutionCache.set(npmImport, pkgName); if (pkg) {
return pkgName; this.npmResolutionCache.set(npmImport, pkg.name);
return pkg.name;
}
} }
} }
@ -127,15 +134,18 @@ export class TargetProjectLocator {
return resolvedModule.startsWith(p.data.root); return resolvedModule.startsWith(p.data.root);
}); });
return importedProject?.name; return importedProject ? importedProject.name : void 0;
} }
private getRootTsConfigPath() { private getRootTsConfig() {
try { let path = 'tsconfig.base.json';
defaultFileRead('tsconfig.base.json'); let absolutePath = join(appRootPath, path);
return 'tsconfig.base.json'; let content = readFileIfExisting(absolutePath);
} catch (e) { if (!content) {
return 'tsconfig.json'; path = 'tsconfig.json';
absolutePath = join(appRootPath, path);
content = readFileIfExisting(absolutePath);
} }
return { path, absolutePath, config: parseJsonWithComments(content) };
} }
} }

View File

@ -9,6 +9,7 @@ import {
} from '../core/project-graph'; } from '../core/project-graph';
import { Rule } from './nxEnforceModuleBoundariesRule'; import { Rule } from './nxEnforceModuleBoundariesRule';
import { TargetProjectLocator } from '../core/target-project-locator'; import { TargetProjectLocator } from '../core/target-project-locator';
import { mapProjectGraphFiles } from '../utils/runtime-lint-utils';
jest.mock('fs', () => require('memfs').fs); jest.mock('fs', () => require('memfs').fs);
jest.mock('../utilities/app-root', () => ({ appRootPath: '/root' })); jest.mock('../utilities/app-root', () => ({ appRootPath: '/root' }));
@ -69,7 +70,7 @@ const fileSys = {
'./tsconfig.base.json': JSON.stringify(tsconfig), './tsconfig.base.json': JSON.stringify(tsconfig),
}; };
describe('Enforce Module Boundaries', () => { describe('Enforce Module Boundaries (tslint)', () => {
beforeEach(() => { beforeEach(() => {
vol.fromJSON(fileSys, '/root'); vol.fromJSON(fileSys, '/root');
}); });
@ -1177,12 +1178,14 @@ function runRule(
true true
); );
const mappedProjectGraph = mapProjectGraphFiles(projectGraph);
const rule = new Rule( const rule = new Rule(
options, options,
`${process.cwd()}/proj`, `${process.cwd()}/proj`,
'mycompany', 'mycompany',
projectGraph, mappedProjectGraph,
new TargetProjectLocator(projectGraph.nodes) new TargetProjectLocator(mappedProjectGraph.nodes)
); );
return rule.apply(sourceFile); return rule.apply(sourceFile);
} }

View File

@ -13,6 +13,8 @@ import {
hasNoneOfTheseTags, hasNoneOfTheseTags,
isAbsoluteImportIntoAnotherProject, isAbsoluteImportIntoAnotherProject,
isRelativeImportIntoAnotherProject, isRelativeImportIntoAnotherProject,
MappedProjectGraph,
mapProjectGraphFiles,
matchImportWithWildcard, matchImportWithWildcard,
onlyLoadChildren, onlyLoadChildren,
} from '../utils/runtime-lint-utils'; } from '../utils/runtime-lint-utils';
@ -28,7 +30,7 @@ export class Rule extends Lint.Rules.AbstractRule {
options: IOptions, options: IOptions,
private readonly projectPath?: string, private readonly projectPath?: string,
private readonly npmScope?: string, private readonly npmScope?: string,
private readonly projectGraph?: ProjectGraph, private readonly projectGraph?: MappedProjectGraph,
private readonly targetProjectLocator?: TargetProjectLocator private readonly targetProjectLocator?: TargetProjectLocator
) { ) {
super(options); super(options);
@ -38,10 +40,12 @@ export class Rule extends Lint.Rules.AbstractRule {
if (!(global as any).projectGraph) { if (!(global as any).projectGraph) {
const nxJson = readNxJson(); const nxJson = readNxJson();
(global as any).npmScope = nxJson.npmScope; (global as any).npmScope = nxJson.npmScope;
(global as any).projectGraph = readCurrentProjectGraph(); (global as any).projectGraph = mapProjectGraphFiles(
readCurrentProjectGraph()
);
} }
this.npmScope = (global as any).npmScope; this.npmScope = (global as any).npmScope;
this.projectGraph = (global as any).projectGraph; this.projectGraph = (global as any).projectGraph as MappedProjectGraph;
if (!(global as any).targetProjectLocator && this.projectGraph) { if (!(global as any).targetProjectLocator && this.projectGraph) {
(global as any).targetProjectLocator = new TargetProjectLocator( (global as any).targetProjectLocator = new TargetProjectLocator(
@ -109,16 +113,20 @@ class EnforceModuleBoundariesWalker extends Lint.RuleWalker {
return; return;
} }
const filePath = getSourceFilePath(
this.getSourceFile().fileName,
this.projectPath
);
const sourceProject = findSourceProject(this.projectGraph, filePath);
// check for relative and absolute imports // check for relative and absolute imports
if ( if (
isRelativeImportIntoAnotherProject( isRelativeImportIntoAnotherProject(
imp, imp,
this.projectPath, this.projectPath,
this.projectGraph, this.projectGraph,
getSourceFilePath( filePath,
normalize(this.getSourceFile().fileName), sourceProject
this.projectPath
)
) || ) ||
isAbsoluteImportIntoAnotherProject(imp) isAbsoluteImportIntoAnotherProject(imp)
) { ) {
@ -130,12 +138,6 @@ class EnforceModuleBoundariesWalker extends Lint.RuleWalker {
return; return;
} }
const filePath = getSourceFilePath(
this.getSourceFile().fileName,
this.projectPath
);
const sourceProject = findSourceProject(this.projectGraph, filePath);
const targetProject = findProjectUsingImport( const targetProject = findProjectUsingImport(
this.projectGraph, this.projectGraph,
this.targetProjectLocator, this.targetProjectLocator,

View File

@ -47,7 +47,11 @@ export function readJsonFile<T = any>(path: string): T {
} }
export function parseJsonWithComments<T = any>(content: string): T { export function parseJsonWithComments<T = any>(content: string): T {
return JSON.parse(stripJsonComments(content)); try {
return JSON.parse(content);
} catch {
return JSON.parse(stripJsonComments(content));
}
} }
export function writeJsonFile(path: string, json: any) { export function writeJsonFile(path: string, json: any) {

View File

@ -7,7 +7,22 @@ import {
ProjectGraphNode, ProjectGraphNode,
} from '../core/project-graph'; } from '../core/project-graph';
import { TargetProjectLocator } from '../core/target-project-locator'; import { TargetProjectLocator } from '../core/target-project-locator';
import { normalizePath } from '@nrwl/devkit'; import { normalizePath, TargetConfiguration } from '@nrwl/devkit';
export interface MappedProjectGraphNode<T = any> {
type: string;
name: string;
data: T & {
root?: string;
targets?: { [targetName: string]: TargetConfiguration };
files: Record<string, FileData>;
};
}
export interface MappedProjectGraph<T = any> {
nodes: Record<string, MappedProjectGraphNode<T>>;
dependencies: Record<string, ProjectGraphDependency[]>;
allWorkspaceFiles?: FileData[];
}
export type Deps = { [projectName: string]: ProjectGraphDependency[] }; export type Deps = { [projectName: string]: ProjectGraphDependency[] };
export type DepConstraint = { export type DepConstraint = {
@ -15,7 +30,10 @@ export type DepConstraint = {
onlyDependOnLibsWithTags: string[]; onlyDependOnLibsWithTags: string[];
}; };
export function hasNoneOfTheseTags(proj: ProjectGraphNode, tags: string[]) { export function hasNoneOfTheseTags(
proj: ProjectGraphNode<any>,
tags: string[]
) {
return tags.filter((allowedTag) => hasTag(proj, allowedTag)).length === 0; return tags.filter((allowedTag) => hasTag(proj, allowedTag)).length === 0;
} }
@ -23,15 +41,6 @@ function hasTag(proj: ProjectGraphNode, tag: string) {
return (proj.data.tags || []).indexOf(tag) > -1 || tag === '*'; return (proj.data.tags || []).indexOf(tag) > -1 || tag === '*';
} }
function containsFile(
files: FileData[],
targetFileWithoutExtension: string
): boolean {
return !!files.filter(
(f) => removeExt(f.file) === targetFileWithoutExtension
)[0];
}
function removeExt(file: string): string { function removeExt(file: string): string {
return file.replace(/\.[^/.]+$/, ''); return file.replace(/\.[^/.]+$/, '');
} }
@ -66,7 +75,8 @@ export function isRelativeImportIntoAnotherProject(
imp: string, imp: string,
projectPath: string, projectPath: string,
projectGraph: ProjectGraph, projectGraph: ProjectGraph,
sourceFilePath: string sourceFilePath: string,
sourceProject: ProjectGraphNode
): boolean { ): boolean {
if (!isRelative(imp)) return false; if (!isRelative(imp)) return false;
@ -74,19 +84,19 @@ export function isRelativeImportIntoAnotherProject(
path.resolve(path.join(projectPath, path.dirname(sourceFilePath)), imp) path.resolve(path.join(projectPath, path.dirname(sourceFilePath)), imp)
).substring(projectPath.length + 1); ).substring(projectPath.length + 1);
const sourceProject = findSourceProject(projectGraph, sourceFilePath);
const targetProject = findTargetProject(projectGraph, targetFile); const targetProject = findTargetProject(projectGraph, targetFile);
return sourceProject && targetProject && sourceProject !== targetProject; return sourceProject && targetProject && sourceProject !== targetProject;
} }
export function findProjectUsingFile(projectGraph: ProjectGraph, file: string) { export function findProjectUsingFile<T>(
return Object.values(projectGraph.nodes).filter((n) => projectGraph: MappedProjectGraph<T>,
containsFile(n.data.files, file) file: string
)[0]; ): MappedProjectGraphNode {
return Object.values(projectGraph.nodes).find((n) => n.data.files[file]);
} }
export function findSourceProject( export function findSourceProject(
projectGraph: ProjectGraph, projectGraph: MappedProjectGraph,
sourceFilePath: string sourceFilePath: string
) { ) {
const targetFile = removeExt(sourceFilePath); const targetFile = removeExt(sourceFilePath);
@ -180,3 +190,26 @@ export function hasBuildExecutor(projectGraph: ProjectGraphNode): boolean {
projectGraph.data.targets.build.executor !== '' projectGraph.data.targets.build.executor !== ''
); );
} }
export function mapProjectGraphFiles<T>(
projectGraph: ProjectGraph<T>
): MappedProjectGraph | null {
if (!projectGraph) {
return null;
}
const nodes: Record<string, MappedProjectGraphNode> = {};
Object.entries(projectGraph.nodes).forEach(([name, node]) => {
const files: Record<string, FileData> = {};
node.data.files.forEach(({ file, hash, ext }) => {
files[file.slice(0, -ext.length)] = { file, hash, ext };
});
const data = { ...node.data, files };
nodes[name] = { ...node, data };
});
return {
...projectGraph,
nodes,
};
}