feat(core): hash only relevant parts of global config files

This commit is contained in:
vsavkin 2021-04-05 14:26:15 -04:00 committed by Victor Savkin
parent 76bc890f34
commit b5efa6bfee
16 changed files with 128 additions and 103 deletions

View File

@ -787,19 +787,3 @@ describe('cache', () => {
expect(matchingProjects).toEqual(expectedProjects); expect(matchingProjects).toEqual(expectedProjects);
} }
}); });
describe('workspace structure', () => {
beforeEach(() => newProject());
afterEach(() => removeProject({ onlyOnCI: true }));
it('should have a vscode/extensions.json file created', () => {
const extensions = readJson('.vscode/extensions.json');
expect(extensions).toEqual({
recommendations: [
'ms-vscode.vscode-typescript-tslint-plugin',
'esbenp.prettier-vscode',
],
});
});
});

View File

@ -1,6 +1,6 @@
{ {
"name": "@nrwl/nx-source", "name": "@nrwl/nx-source",
"version": "11.6.0", "version": "12.0.0-beta.1",
"description": "Powerful, Extensible Dev Tools", "description": "Powerful, Extensible Dev Tools",
"homepage": "https://nx.dev", "homepage": "https://nx.dev",
"private": true, "private": true,

View File

@ -30,6 +30,7 @@ import {
} from '@nrwl/workspace/src/core/file-utils'; } from '@nrwl/workspace/src/core/file-utils';
import { TargetProjectLocator } from '@nrwl/workspace/src/core/target-project-locator'; import { TargetProjectLocator } from '@nrwl/workspace/src/core/target-project-locator';
import { checkCircularPath } from '@nrwl/workspace/src/utils/graph-utils'; import { checkCircularPath } from '@nrwl/workspace/src/utils/graph-utils';
import { readCurrentProjectGraph } from '@nrwl/workspace/src/core/project-graph/project-graph';
type Options = [ type Options = [
{ {
@ -106,10 +107,9 @@ export default createESLintRule<Options, MessageIds>({
(global as any).projectPath || appRootPath (global as any).projectPath || appRootPath
); );
if (!(global as any).projectGraph) { if (!(global as any).projectGraph) {
const workspaceJson = readWorkspaceJson();
const nxJson = readNxJson(); const nxJson = readNxJson();
(global as any).npmScope = nxJson.npmScope; (global as any).npmScope = nxJson.npmScope;
(global as any).projectGraph = createProjectGraph(workspaceJson, nxJson); (global as any).projectGraph = readCurrentProjectGraph();
} }
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 ProjectGraph;
@ -123,6 +123,8 @@ export default createESLintRule<Options, MessageIds>({
.targetProjectLocator as TargetProjectLocator; .targetProjectLocator as TargetProjectLocator;
function run(node: TSESTree.ImportDeclaration | TSESTree.ImportExpression) { function run(node: TSESTree.ImportDeclaration | TSESTree.ImportExpression) {
if (!projectGraph) return;
// accept only literals because template literals have no value // accept only literals because template literals have no value
if (node.source.type !== AST_NODE_TYPES.Literal) { if (node.source.type !== AST_NODE_TYPES.Literal) {
return; return;

View File

@ -1,5 +1,4 @@
import { dirname, extname } from 'path'; import { dirname, extname } from 'path';
import * as ts from 'typescript';
import defaultResolver from 'jest-resolve/build/defaultResolver'; import defaultResolver from 'jest-resolve/build/defaultResolver';
interface ResolveOptions { interface ResolveOptions {
@ -12,6 +11,9 @@ interface ResolveOptions {
defaultResolver: typeof defaultResolver; defaultResolver: typeof defaultResolver;
} }
let compilerSetup;
let ts;
function getCompilerSetup(rootDir: string) { function getCompilerSetup(rootDir: string) {
const tsConfigPath = const tsConfigPath =
ts.findConfigFile(rootDir, ts.sys.fileExists, 'tsconfig.spec.json') || ts.findConfigFile(rootDir, ts.sys.fileExists, 'tsconfig.spec.json') ||
@ -35,8 +37,6 @@ function getCompilerSetup(rootDir: string) {
return { compilerOptions, host }; return { compilerOptions, host };
} }
let compilerSetup;
module.exports = function (path: string, options: ResolveOptions) { module.exports = function (path: string, options: ResolveOptions) {
const ext = extname(path); const ext = extname(path);
if ( if (
@ -52,7 +52,14 @@ module.exports = function (path: string, options: ResolveOptions) {
try { try {
return defaultResolver(path, options); return defaultResolver(path, options);
} catch (e) { } catch (e) {
if (
path === 'jest-sequencer-@jest/test-sequencer' ||
path === '@jest/test-sequencer'
) {
return;
}
// Fallback to using typescript // Fallback to using typescript
ts = ts || require('typescript');
compilerSetup = compilerSetup || getCompilerSetup(options.rootDir); compilerSetup = compilerSetup || getCompilerSetup(options.rootDir);
const { compilerOptions, host } = compilerSetup; const { compilerOptions, host } = compilerSetup;
return ts.resolveModuleName(path, options.basedir, compilerOptions, host) return ts.resolveModuleName(path, options.basedir, compilerOptions, host)

View File

@ -6,6 +6,8 @@
# Nx CLI # Nx CLI
This is an alias of the [@nrwl/cli](https://www.npmjs.com/package/@nrwl/cli) package. Please see [@nrwl/cli](https://www.npmjs.com/package/@nrwl/cli) for more information.
{{what-is-nx}} {{what-is-nx}}
{{getting-started}} {{getting-started}}

View File

@ -25,6 +25,17 @@ describe('Hasher', () => {
} }
it('should create project hash', async (done) => { it('should create project hash', async (done) => {
fs.readFileSync = (file) => {
if (file === 'workspace.json') {
return JSON.stringify({
projects: { proj: 'proj-from-workspace.json' },
});
}
if (file === 'nx.json') {
return JSON.stringify({ projects: { proj: 'proj-from-nx.json' } });
}
return file;
};
hashes['/file'] = 'file.hash'; hashes['/file'] = 'file.hash';
const hasher = new Hasher( const hasher = new Hasher(
{ {
@ -69,16 +80,14 @@ describe('Hasher', () => {
expect(hash.details.command).toEqual('proj|build||{"prop":"prop-value"}'); expect(hash.details.command).toEqual('proj|build||{"prop":"prop-value"}');
expect(hash.details.sources).toEqual({ expect(hash.details.sources).toEqual({
proj: '/file|file.hash', proj: '/file|file.hash|"proj-from-workspace.json"|"proj-from-nx.json"',
}); });
expect(hash.details.implicitDeps).toEqual({ expect(hash.details.implicitDeps).toEqual({
'yarn.lock': 'yarn.lock.hash', 'yarn.lock': 'yarn.lock.hash',
'nx.json': 'nx.json.hash', 'nx.json': '{}',
'package-lock.json': 'package-lock.json.hash', 'package-lock.json': 'package-lock.json.hash',
'package.json': 'package.json.hash',
'pnpm-lock.yaml': 'pnpm-lock.yaml.hash', 'pnpm-lock.yaml': 'pnpm-lock.yaml.hash',
'tsconfig.base.json': 'tsconfig.base.json.hash', 'tsconfig.base.json': 'tsconfig.base.json.hash',
'workspace.json': 'workspace.json.hash',
}); });
expect(hash.details.runtime).toEqual({ expect(hash.details.runtime).toEqual({
'echo runtime123': 'runtime123', 'echo runtime123': 'runtime123',
@ -175,8 +184,8 @@ describe('Hasher', () => {
// note that the parent hash is based on parent source files only! // note that the parent hash is based on parent source files only!
expect(hash.details.sources).toEqual({ expect(hash.details.sources).toEqual({
parent: '/filea|a.hash', parent: '/filea|a.hash|""|""',
child: '/fileb|b.hash', child: '/fileb|b.hash|""|""',
}); });
done(); done();
@ -232,8 +241,8 @@ describe('Hasher', () => {
expect(tasksHash.value).toContain('proj'); //project expect(tasksHash.value).toContain('proj'); //project
expect(tasksHash.value).toContain('build'); //target expect(tasksHash.value).toContain('build'); //target
expect(tasksHash.details.sources).toEqual({ expect(tasksHash.details.sources).toEqual({
proja: '/filea|a.hash', proja: '/filea|a.hash|""|""',
projb: '/fileb|b.hash', projb: '/fileb|b.hash|""|""',
}); });
const hashb = ( const hashb = (
@ -253,8 +262,8 @@ describe('Hasher', () => {
expect(hashb.value).toContain('proj'); //project expect(hashb.value).toContain('proj'); //project
expect(hashb.value).toContain('build'); //target expect(hashb.value).toContain('build'); //target
expect(hashb.details.sources).toEqual({ expect(hashb.details.sources).toEqual({
proja: '/filea|a.hash', proja: '/filea|a.hash|""|""',
projb: '/fileb|b.hash', projb: '/fileb|b.hash|""|""',
}); });
done(); done();

View File

@ -2,18 +2,13 @@ import { ProjectGraph } from '../project-graph';
import { NxJson } from '../shared-interfaces'; import { NxJson } from '../shared-interfaces';
import { Task } from '../../tasks-runner/tasks-runner'; import { Task } from '../../tasks-runner/tasks-runner';
import { readFileSync } from 'fs'; import { readFileSync } from 'fs';
import { rootWorkspaceFileNames } from '../file-utils'; import { workspaceFileName } from '../file-utils';
import { exec, execSync } from 'child_process'; import { exec } from 'child_process';
import { import { defaultFileHasher, FileHasher } from './file-hasher';
defaultFileHasher,
extractNameAndVersion,
FileHasher,
} from './file-hasher';
import { defaultHashing, HashingImp } from './hashing-impl'; import { defaultHashing, HashingImp } from './hashing-impl';
import * as minimatch from 'minimatch'; import * as minimatch from 'minimatch';
import { performance } from 'perf_hooks'; import { performance } from 'perf_hooks';
import * as stripJsonComments from 'strip-json-comments';
const resolve = require('resolve');
export interface Hash { export interface Hash {
value: string; value: string;
@ -47,7 +42,6 @@ interface NodeModulesResult {
export class Hasher { export class Hasher {
static version = '1.0'; static version = '1.0';
private implicitDependencies: Promise<ImplicitHashResult>; private implicitDependencies: Promise<ImplicitHashResult>;
private nodeModules: Promise<NodeModulesResult>;
private runtimeInputs: Promise<RuntimeHashResult>; private runtimeInputs: Promise<RuntimeHashResult>;
private fileHasher: FileHasher; private fileHasher: FileHasher;
private projectHashes: ProjectHasher; private projectHashes: ProjectHasher;
@ -93,7 +87,6 @@ export class Hasher {
]), ]),
this.implicitDepsHash(), this.implicitDepsHash(),
this.runtimeInputsHash(), this.runtimeInputsHash(),
// this.nodeModulesHash(),
])) as [ ])) as [
ProjectHashResult, ProjectHashResult,
ImplicitHashResult, ImplicitHashResult,
@ -192,18 +185,22 @@ export class Hasher {
const fileNames = [ const fileNames = [
...filesWithoutPatterns, ...filesWithoutPatterns,
...implicitDepsFromPatterns, ...implicitDepsFromPatterns,
...rootWorkspaceFileNames(),
//TODO: vsavkin move the special cases into explicit ts support
'tsconfig.base.json',
'package-lock.json', 'package-lock.json',
'yarn.lock', 'yarn.lock',
'pnpm-lock.yaml', 'pnpm-lock.yaml',
]; ];
this.implicitDependencies = Promise.resolve().then(async () => { this.implicitDependencies = Promise.resolve().then(async () => {
const fileHashes = fileNames.map((file) => { const fileHashes = [
...fileNames.map((file) => {
const hash = this.fileHasher.hashFile(file); const hash = this.fileHasher.hashFile(file);
return { file, hash }; return { file, hash };
}); }),
...this.hashGlobalConfig(),
];
const combinedHash = this.hashing.hashArray( const combinedHash = this.hashing.hashArray(
fileHashes.map((v) => v.hash) fileHashes.map((v) => v.hash)
); );
@ -214,6 +211,7 @@ export class Hasher {
'hasher:implicit deps hash:start', 'hasher:implicit deps hash:start',
'hasher:implicit deps hash:end' 'hasher:implicit deps hash:end'
); );
return { return {
value: combinedHash, value: combinedHash,
sources: fileHashes.reduce((m, c) => ((m[c.file] = c.hash), m), {}), sources: fileHashes.reduce((m, c) => ((m[c.file] = c.hash), m), {}),
@ -222,43 +220,36 @@ export class Hasher {
return this.implicitDependencies; return this.implicitDependencies;
} }
private async nodeModulesHash() { private hashGlobalConfig() {
if (this.nodeModules) return this.nodeModules; return [
{
this.nodeModules = Promise.resolve().then(async () => { hash: this.fileHasher.hashFile('nx.json', (file) => {
try { try {
const j = JSON.parse(readFileSync('package.json').toString()); const r = JSON.parse(stripJsonComments(file));
const allPackages = [ delete r.projects;
...Object.keys(j.dependencies), return JSON.stringify(r);
...Object.keys(j.devDependencies),
];
const packageJsonHashes = allPackages.map((d) => {
try {
const path = resolve.sync(`${d}/package.json`, {
basedir: process.cwd(),
});
return this.fileHasher.hashFile(path, extractNameAndVersion);
} catch (e) { } catch (e) {
return ''; return '';
} }
}); }),
return { value: this.hashing.hashArray(packageJsonHashes) }; file: 'nx.json',
} catch (e) { },
return { value: '' }; ];
}
});
return this.nodeModules;
} }
} }
class ProjectHasher { class ProjectHasher {
private sourceHashes: { [projectName: string]: Promise<string> } = {}; private sourceHashes: { [projectName: string]: Promise<string> } = {};
private workspaceJson: any;
private nxJson: any;
constructor( constructor(
private readonly projectGraph: ProjectGraph, private readonly projectGraph: ProjectGraph,
private readonly hashing: HashingImp private readonly hashing: HashingImp
) {} ) {
this.workspaceJson = this.readConfigFile(workspaceFileName());
this.nxJson = this.readConfigFile('nx.json');
}
async hashProject( async hashProject(
projectName: string, projectName: string,
@ -299,9 +290,32 @@ class ProjectHasher {
const p = this.projectGraph.nodes[projectName]; const p = this.projectGraph.nodes[projectName];
const fileNames = p.data.files.map((f) => f.file); const fileNames = p.data.files.map((f) => f.file);
const values = p.data.files.map((f) => f.hash); const values = p.data.files.map((f) => f.hash);
res(this.hashing.hashArray([...fileNames, ...values]));
const workspaceJson = JSON.stringify(
this.workspaceJson.projects[projectName] || ''
);
const nxJson = JSON.stringify(this.nxJson.projects[projectName] || '');
res(
this.hashing.hashArray([
...fileNames,
...values,
workspaceJson,
nxJson,
])
);
}); });
} }
return this.sourceHashes[projectName]; return this.sourceHashes[projectName];
} }
private readConfigFile(name: string) {
try {
const res = JSON.parse(stripJsonComments(readFileSync(name).toString()));
if (!res.projects) res.projects = {};
return res;
} catch (e) {
return { projects: {} };
}
}
} }

View File

@ -80,6 +80,11 @@ export function createProjectGraph(
} }
} }
export function readCurrentProjectGraph(): ProjectGraph | null {
const cache = readCache();
return cache === false ? null : cache;
}
function addWorkspaceFiles( function addWorkspaceFiles(
projectGraph: ProjectGraph, projectGraph: ProjectGraph,
allWorkspaceFiles: FileData[] allWorkspaceFiles: FileData[]

View File

@ -3,6 +3,9 @@ import { relative } from 'path';
import { dirSync, fileSync } from 'tmp'; import { dirSync, fileSync } from 'tmp';
import runCommands, { LARGE_BUFFER } from './run-commands.impl'; import runCommands, { LARGE_BUFFER } from './run-commands.impl';
function normalize(p: string) {
return p.startsWith('/private') ? p.substring(8) : p;
}
function readFile(f: string) { function readFile(f: string) {
return readFileSync(f).toString().replace(/\s/g, ''); return readFileSync(f).toString().replace(/\s/g, '');
} }
@ -59,7 +62,7 @@ describe('Command Runner Builder', () => {
}); });
}); });
it('ssss should forward args by default when using commands (plural)', async () => { it('should forward args by default when using commands (plural)', async () => {
const exec = spyOn(require('child_process'), 'exec').and.callThrough(); const exec = spyOn(require('child_process'), 'exec').and.callThrough();
await runCommands( await runCommands(
@ -285,7 +288,7 @@ describe('Command Runner Builder', () => {
); );
expect(result).toEqual(jasmine.objectContaining({ success: true })); expect(result).toEqual(jasmine.objectContaining({ success: true }));
expect(readFile(f)).toBe(root); expect(normalize(readFile(f))).toBe(root);
}); });
it('should run the task in the specified cwd relative to the workspace root when cwd is not an absolute path', async () => { it('should run the task in the specified cwd relative to the workspace root when cwd is not an absolute path', async () => {
@ -308,7 +311,7 @@ describe('Command Runner Builder', () => {
); );
expect(result).toEqual(jasmine.objectContaining({ success: true })); expect(result).toEqual(jasmine.objectContaining({ success: true }));
expect(readFile(f)).toBe(childFolder); expect(normalize(readFile(f))).toBe(childFolder);
}); });
it('should run the task in the specified absolute cwd', async () => { it('should run the task in the specified absolute cwd', async () => {
@ -330,7 +333,7 @@ describe('Command Runner Builder', () => {
); );
expect(result).toEqual(jasmine.objectContaining({ success: true })); expect(result).toEqual(jasmine.objectContaining({ success: true }));
expect(readFile(f)).toBe(childFolder); expect(normalize(readFile(f))).toBe(childFolder);
}); });
}); });

View File

@ -148,7 +148,6 @@ Object {
"dependencies": "*", "dependencies": "*",
"devDependencies": "*", "devDependencies": "*",
}, },
"tslint.json": "*",
}, },
"npmScope": "npmScope", "npmScope": "npmScope",
"projects": Object {}, "projects": Object {},

View File

@ -5,3 +5,18 @@ exports[`@nrwl/workspace:workspace should create a prettierrc file 1`] = `
\\"singleQuote\\": true \\"singleQuote\\": true
}" }"
`; `;
exports[`@nrwl/workspace:workspace should recommend vscode extensions (angular) 1`] = `
Array [
"angular.ng-template",
"nrwl.angular-console",
"esbenp.prettier-vscode",
]
`;
exports[`@nrwl/workspace:workspace should recommend vscode extensions 1`] = `
Array [
"nrwl.angular-console",
"esbenp.prettier-vscode",
]
`;

View File

@ -1,10 +1,9 @@
{ {
"recommendations": [ "recommendations": [
<% if(cli === 'angular') { %> <% if(cli === 'angular') { %>
"nrwl.angular-console",
"angular.ng-template",<% } "angular.ng-template",<% }
%> %>
"ms-vscode.vscode-typescript-tslint-plugin", "nrwl.angular-console",
"esbenp.prettier-vscode" "esbenp.prettier-vscode"
] ]
} }

View File

@ -8,7 +8,6 @@
"dependencies": "*", "dependencies": "*",
"devDependencies": "*" "devDependencies": "*"
}, },
"tslint.json": "*",
".eslintrc.json": "*" ".eslintrc.json": "*"
}, },
"tasksRunnerOptions": { "tasksRunnerOptions": {

View File

@ -43,7 +43,6 @@ describe('@nrwl/workspace:workspace', () => {
dependencies: '*', dependencies: '*',
devDependencies: '*', devDependencies: '*',
}, },
'tslint.json': '*',
'.eslintrc.json': '*', '.eslintrc.json': '*',
}, },
tasksRunnerOptions: { tasksRunnerOptions: {
@ -82,10 +81,7 @@ describe('@nrwl/workspace:workspace', () => {
'proj/.vscode/extensions.json' 'proj/.vscode/extensions.json'
).recommendations; ).recommendations;
expect(recommendations).toEqual([ expect(recommendations).toMatchSnapshot();
'ms-vscode.vscode-typescript-tslint-plugin',
'esbenp.prettier-vscode',
]);
}); });
it('should recommend vscode extensions (angular)', async () => { it('should recommend vscode extensions (angular)', async () => {
@ -101,12 +97,7 @@ describe('@nrwl/workspace:workspace', () => {
'proj/.vscode/extensions.json' 'proj/.vscode/extensions.json'
).recommendations; ).recommendations;
expect(recommendations).toEqual([ expect(recommendations).toMatchSnapshot();
'nrwl.angular-console',
'angular.ng-template',
'ms-vscode.vscode-typescript-tslint-plugin',
'esbenp.prettier-vscode',
]);
}); });
it('should add decorate-angular-cli when used with angular cli', async () => { it('should add decorate-angular-cli when used with angular cli', async () => {

View File

@ -22,12 +22,10 @@ import {
onlyLoadChildren, onlyLoadChildren,
} from '../utils/runtime-lint-utils'; } from '../utils/runtime-lint-utils';
import { normalize } from 'path'; import { normalize } from 'path';
import { import { readNxJson } from '@nrwl/workspace/src/core/file-utils';
readNxJson,
readWorkspaceJson,
} from '@nrwl/workspace/src/core/file-utils';
import { TargetProjectLocator } from '../core/target-project-locator'; import { TargetProjectLocator } from '../core/target-project-locator';
import { checkCircularPath } from '@nrwl/workspace/src/utils/graph-utils'; import { checkCircularPath } from '@nrwl/workspace/src/utils/graph-utils';
import { readCurrentProjectGraph } from '@nrwl/workspace/src/core/project-graph/project-graph';
export class Rule extends Lint.Rules.AbstractRule { export class Rule extends Lint.Rules.AbstractRule {
constructor( constructor(
@ -42,13 +40,9 @@ export class Rule extends Lint.Rules.AbstractRule {
if (!projectPath) { if (!projectPath) {
this.projectPath = normalize(appRootPath); this.projectPath = normalize(appRootPath);
if (!(global as any).projectGraph) { if (!(global as any).projectGraph) {
const workspaceJson = readWorkspaceJson();
const nxJson = readNxJson(); const nxJson = readNxJson();
(global as any).npmScope = nxJson.npmScope; (global as any).npmScope = nxJson.npmScope;
(global as any).projectGraph = createProjectGraph( (global as any).projectGraph = readCurrentProjectGraph();
workspaceJson,
nxJson
);
} }
this.npmScope = (global as any).npmScope; this.npmScope = (global as any).npmScope;
this.projectGraph = (global as any).projectGraph; this.projectGraph = (global as any).projectGraph;
@ -63,6 +57,7 @@ export class Rule extends Lint.Rules.AbstractRule {
} }
public apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] { public apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] {
if (!this.projectGraph) return [];
return this.applyWithWalker( return this.applyWithWalker(
new EnforceModuleBoundariesWalker( new EnforceModuleBoundariesWalker(
sourceFile, sourceFile,

View File

@ -38,6 +38,7 @@ if (
} }
module.exports = function (path, options) { module.exports = function (path, options) {
if (path === 'jest-sequencer-@jest/test-sequencer') return;
const ext = path_1.extname(path); const ext = path_1.extname(path);
if ( if (
ext === '.css' || ext === '.css' ||