diff --git a/docs/angular/guides/configuration.md b/docs/angular/guides/configuration.md index 0faba7e2cb5..b7293ef8178 100644 --- a/docs/angular/guides/configuration.md +++ b/docs/angular/guides/configuration.md @@ -373,6 +373,7 @@ Tasks runners can accept different options. The following are the options suppor - `cacheDirectory` defines where the local cache is stored, which is `node_modules/.cache/nx` by default. - `encryptionKey` (when using `"@nrwl/nx-cloud"` only) defines an encryption key to support end-to-end encryption of your cloud cache. You may also provide an environment variable with the key `NX_CLOUD_ENCRYPTION_KEY` that contains an encryption key as its value. The Nx Cloud task runner normalizes the key length, so any length of key is acceptable. - `runtimeCacheInputs` defines the list of commands that are run by the runner to include into the computation hash value. +- `selectivelyHashTsConfig` only hash the path mapping of the active project in the `tsconfig.base.json` (e.g., adding/removing projects doesn't affect the hash of existing projects). Defaults to `false` `runtimeCacheInputs` are set as follows: diff --git a/docs/node/guides/configuration.md b/docs/node/guides/configuration.md index e27f8ca5e1a..835635933b1 100644 --- a/docs/node/guides/configuration.md +++ b/docs/node/guides/configuration.md @@ -397,6 +397,7 @@ Tasks runners can accept different options. The following are the options suppor - `cacheDirectory` defines where the local cache is stored, which is `node_modules/.cache/nx` by default. - `encryptionKey` (when using `"@nrwl/nx-cloud"` only) defines an encryption key to support end-to-end encryption of your cloud cache. You may also provide an environment variable with the key `NX_CLOUD_ENCRYPTION_KEY` that contains an encryption key as its value. The Nx Cloud task runner normalizes the key length, so any length of key is acceptable. - `runtimeCacheInputs` defines the list of commands that are run by the runner to include into the computation hash value. +- `selectivelyHashTsConfig` only hash the path mapping of the active project in the `tsconfig.base.json` (e.g., adding/removing projects doesn't affect the hash of existing projects). Defaults to `false` `runtimeCacheInputs` are set as follows: diff --git a/docs/react/guides/configuration.md b/docs/react/guides/configuration.md index a4214fbfdd1..53e11efdcc6 100644 --- a/docs/react/guides/configuration.md +++ b/docs/react/guides/configuration.md @@ -393,6 +393,7 @@ Tasks runners can accept different options. The following are the options suppor - `cacheDirectory` defines where the local cache is stored, which is `node_modules/.cache/nx` by default. - `encryptionKey` (when using `"@nrwl/nx-cloud"` only) defines an encryption key to support end-to-end encryption of your cloud cache. You may also provide an environment variable with the key `NX_CLOUD_ENCRYPTION_KEY` that contains an encryption key as its value. The Nx Cloud task runner normalizes the key length, so any length of key is acceptable. - `runtimeCacheInputs` defines the list of commands that are run by the runner to include into the computation hash value. +- `selectivelyHashTsConfig` only hash the path mapping of the active project in the `tsconfig.base.json` (e.g., adding/removing projects doesn't affect the hash of existing projects). Defaults to `false` `runtimeCacheInputs` are set as follows: diff --git a/packages/workspace/src/core/hasher/hasher.spec.ts b/packages/workspace/src/core/hasher/hasher.spec.ts index 61e936c6748..884b1c075d0 100644 --- a/packages/workspace/src/core/hasher/hasher.spec.ts +++ b/packages/workspace/src/core/hasher/hasher.spec.ts @@ -4,13 +4,36 @@ import fs = require('fs'); jest.mock('fs'); describe('Hasher', () => { + const nxJson = { + npmScope: 'nrwl', + projects: { + parent: { implicitDependencies: [], tags: [] }, + child: { implicitDependencies: [], tags: [] }, + }, + }; + + const workSpaceJson = { + projects: { + parent: { root: 'libs/parent' }, + child: { root: 'libs/child' }, + }, + }; + + const tsConfigBaseJsonHash = JSON.stringify({ + compilerOptions: { + paths: { + '@nrwl/parent': ['libs/parent/src/index.ts'], + '@nrwl/child': ['libs/child/src/index.ts'], + }, + }, + }); let hashes = { 'yarn.lock': 'yarn.lock.hash', 'nx.json': 'nx.json.hash', 'package-lock.json': 'package-lock.json.hash', 'package.json': 'package.json.hash', 'pnpm-lock.yaml': 'pnpm-lock.yaml.hash', - 'tsconfig.base.json': 'tsconfig.base.json.hash', + 'tsconfig.base.json': tsConfigBaseJsonHash, 'workspace.json': 'workspace.json.hash', global1: 'global1.hash', global2: 'global2.hash', @@ -23,24 +46,84 @@ describe('Hasher', () => { }; } - it('should create project hash', async () => { + beforeAll(() => { fs.readFileSync = (file) => { if (file === 'workspace.json') { - return JSON.stringify({ - projects: { proj: { root: 'proj-from-workspace.json' } }, - }); + return JSON.stringify(workSpaceJson); } if (file === 'nx.json') { - return JSON.stringify({ projects: { proj: 'proj-from-nx.json' } }); + return JSON.stringify(nxJson); + } + if (file === 'tsconfig.base.json') { + return tsConfigBaseJsonHash; } return file; }; + }); + + it('should create project hash', async () => { + hashes['/file'] = 'file.hash'; + const hasher = new Hasher( + { + nodes: { + parent: { + name: 'parent', + type: 'lib', + data: { + root: '', + files: [{ file: '/file', ext: '.ts', hash: 'file.hash' }], + }, + }, + }, + dependencies: { + parent: [], + }, + }, + {} as any, + { + runtimeCacheInputs: ['echo runtime123', 'echo runtime456'], + }, + createHashing() + ); + + const hash = await hasher.hashTaskWithDepsAndContext({ + target: { project: 'parent', target: 'build' }, + id: 'parent-build', + overrides: { prop: 'prop-value' }, + }); + + expect(hash.value).toContain('yarn.lock.hash'); //implicits + expect(hash.value).toContain('file.hash'); //project files + expect(hash.value).toContain('prop-value'); //overrides + expect(hash.value).toContain('parent'); //project + expect(hash.value).toContain('build'); //target + expect(hash.value).toContain('runtime123'); //target + expect(hash.value).toContain('runtime456'); //target + + expect(hash.details.command).toEqual('parent|build||{"prop":"prop-value"}'); + expect(hash.details.nodes).toEqual({ + parent: + '/file|file.hash|{"root":"libs/parent"}|{"implicitDependencies":[],"tags":[]}|{"compilerOptions":{"paths":{"@nrwl/parent":["libs/parent/src/index.ts"],"@nrwl/child":["libs/child/src/index.ts"]}}}', + }); + expect(hash.details.implicitDeps).toEqual({ + 'nx.json': '{"npmScope":"nrwl"}', + 'yarn.lock': 'yarn.lock.hash', + 'package-lock.json': 'package-lock.json.hash', + 'pnpm-lock.yaml': 'pnpm-lock.yaml.hash', + }); + expect(hash.details.runtime).toEqual({ + 'echo runtime123': 'runtime123', + 'echo runtime456': 'runtime456', + }); + }); + + it('should create project hash with tsconfig.base.json cache', async () => { hashes['/file'] = 'file.hash'; const hasher = new Hasher( { nodes: { - proj: { - name: 'proj', + parent: { + name: 'parent', type: 'lib', data: { root: '', @@ -49,40 +132,41 @@ describe('Hasher', () => { }, }, dependencies: { - proj: [], + parent: [], }, }, {} as any, { runtimeCacheInputs: ['echo runtime123', 'echo runtime456'], + selectivelyHashTsConfig: true, }, createHashing() ); const hash = await hasher.hashTaskWithDepsAndContext({ - target: { project: 'proj', target: 'build' }, - id: 'proj-build', + target: { project: 'parent', target: 'build' }, + id: 'parent-build', overrides: { prop: 'prop-value' }, }); expect(hash.value).toContain('yarn.lock.hash'); //implicits expect(hash.value).toContain('file.hash'); //project files expect(hash.value).toContain('prop-value'); //overrides - expect(hash.value).toContain('proj'); //project + expect(hash.value).toContain('parent'); //project expect(hash.value).toContain('build'); //target expect(hash.value).toContain('runtime123'); //target expect(hash.value).toContain('runtime456'); //target - expect(hash.details.command).toEqual('proj|build||{"prop":"prop-value"}'); + expect(hash.details.command).toEqual('parent|build||{"prop":"prop-value"}'); expect(hash.details.nodes).toEqual({ - proj: '/file.ts|file.hash|{"root":"proj-from-workspace.json"}|"proj-from-nx.json"', + parent: + '/file.ts|file.hash|{"root":"libs/parent"}|{"implicitDependencies":[],"tags":[]}|{"compilerOptions":{"paths":{"@nrwl/parent":["libs/parent/src/index.ts"]}}}', }); expect(hash.details.implicitDeps).toEqual({ + 'nx.json': '{"npmScope":"nrwl"}', 'yarn.lock': 'yarn.lock.hash', - 'nx.json': '{}', 'package-lock.json': 'package-lock.json.hash', 'pnpm-lock.yaml': 'pnpm-lock.yaml.hash', - 'tsconfig.base.json': 'tsconfig.base.json.hash', }); expect(hash.details.runtime).toEqual({ 'echo runtime123': 'runtime123', @@ -94,8 +178,8 @@ describe('Hasher', () => { const hasher = new Hasher( { nodes: { - proj: { - name: 'proj', + parent: { + name: 'parent', type: 'lib', data: { root: '', @@ -104,7 +188,7 @@ describe('Hasher', () => { }, }, dependencies: { - proj: [], + parent: [], }, }, {} as any, @@ -116,8 +200,8 @@ describe('Hasher', () => { try { await hasher.hashTaskWithDepsAndContext({ - target: { project: 'proj', target: 'build' }, - id: 'proj-build', + target: { project: 'parent', target: 'build' }, + id: 'parent-build', overrides: {}, }); fail('Should not be here'); @@ -172,8 +256,10 @@ describe('Hasher', () => { // note that the parent hash is based on parent source files only! expect(hash.details.nodes).toEqual({ - parent: '/filea.ts|a.hash|""|""', - child: '/fileb.ts|b.hash|""|""', + child: + '/fileb.ts|b.hash|{"root":"libs/child"}|{"implicitDependencies":[],"tags":[]}|{"compilerOptions":{"paths":{"@nrwl/parent":["libs/parent/src/index.ts"],"@nrwl/child":["libs/child/src/index.ts"]}}}', + parent: + '/filea.ts|a.hash|{"root":"libs/parent"}|{"implicitDependencies":[],"tags":[]}|{"compilerOptions":{"paths":{"@nrwl/parent":["libs/parent/src/index.ts"],"@nrwl/child":["libs/child/src/index.ts"]}}}', }); }); @@ -183,16 +269,16 @@ describe('Hasher', () => { const hasher = new Hasher( { nodes: { - proja: { - name: 'proja', + parent: { + name: 'parent', type: 'lib', data: { root: '', files: [{ file: '/filea.ts', hash: 'a.hash' }], }, }, - projb: { - name: 'projb', + child: { + name: 'child', type: 'lib', data: { root: '', @@ -201,8 +287,8 @@ describe('Hasher', () => { }, }, dependencies: { - proja: [{ source: 'proja', target: 'projb', type: 'static' }], - projb: [{ source: 'projb', target: 'proja', type: 'static' }], + parent: [{ source: 'parent', target: 'child', type: 'static' }], + child: [{ source: 'child', target: 'parent', type: 'static' }], }, }, {} as any, @@ -211,8 +297,8 @@ describe('Hasher', () => { ); const tasksHash = await hasher.hashTaskWithDepsAndContext({ - target: { project: 'proja', target: 'build' }, - id: 'proja-build', + target: { project: 'parent', target: 'build' }, + id: 'parent-build', overrides: { prop: 'prop-value' }, }); @@ -220,16 +306,18 @@ describe('Hasher', () => { expect(tasksHash.value).toContain('a.hash'); //project files expect(tasksHash.value).toContain('b.hash'); //project files expect(tasksHash.value).toContain('prop-value'); //overrides - expect(tasksHash.value).toContain('proj'); //project + expect(tasksHash.value).toContain('parent|build'); //project and target expect(tasksHash.value).toContain('build'); //target expect(tasksHash.details.nodes).toEqual({ - proja: '/filea.ts|a.hash|""|""', - projb: '/fileb.ts|b.hash|""|""', + child: + '/fileb.ts|b.hash|{"root":"libs/child"}|{"implicitDependencies":[],"tags":[]}|{"compilerOptions":{"paths":{"@nrwl/parent":["libs/parent/src/index.ts"],"@nrwl/child":["libs/child/src/index.ts"]}}}', + parent: + '/filea.ts|a.hash|{"root":"libs/parent"}|{"implicitDependencies":[],"tags":[]}|{"compilerOptions":{"paths":{"@nrwl/parent":["libs/parent/src/index.ts"],"@nrwl/child":["libs/child/src/index.ts"]}}}', }); const hashb = await hasher.hashTaskWithDepsAndContext({ - target: { project: 'projb', target: 'build' }, - id: 'projb-build', + target: { project: 'child', target: 'build' }, + id: 'child-build', overrides: { prop: 'prop-value' }, }); @@ -237,11 +325,13 @@ describe('Hasher', () => { expect(hashb.value).toContain('a.hash'); //project files expect(hashb.value).toContain('b.hash'); //project files expect(hashb.value).toContain('prop-value'); //overrides - expect(hashb.value).toContain('proj'); //project + expect(hashb.value).toContain('child|build'); //project and target expect(hashb.value).toContain('build'); //target expect(hashb.details.nodes).toEqual({ - proja: '/filea.ts|a.hash|""|""', - projb: '/fileb.ts|b.hash|""|""', + child: + '/fileb.ts|b.hash|{"root":"libs/child"}|{"implicitDependencies":[],"tags":[]}|{"compilerOptions":{"paths":{"@nrwl/parent":["libs/parent/src/index.ts"],"@nrwl/child":["libs/child/src/index.ts"]}}}', + parent: + '/filea.ts|a.hash|{"root":"libs/parent"}|{"implicitDependencies":[],"tags":[]}|{"compilerOptions":{"paths":{"@nrwl/parent":["libs/parent/src/index.ts"],"@nrwl/child":["libs/child/src/index.ts"]}}}', }); }); @@ -251,8 +341,8 @@ describe('Hasher', () => { const hasher = new Hasher( { nodes: { - proja: { - name: 'proja', + parent: { + name: 'parent', type: 'lib', data: { root: '', @@ -282,8 +372,8 @@ describe('Hasher', () => { ); const tasksHash = await hasher.hashTaskWithDepsAndContext({ - target: { project: 'proja', target: 'build' }, - id: 'proja-build', + target: { project: 'parent', target: 'build' }, + id: 'parent-build', overrides: { prop: 'prop-value' }, }); diff --git a/packages/workspace/src/core/hasher/hasher.ts b/packages/workspace/src/core/hasher/hasher.ts index a07823cf2ab..6d9b290defe 100644 --- a/packages/workspace/src/core/hasher/hasher.ts +++ b/packages/workspace/src/core/hasher/hasher.ts @@ -38,6 +38,14 @@ interface RuntimeHashResult { runtime: { [input: string]: string }; } +interface CompilerOptions { + paths: Record; +} + +interface TsconfigJsonConfiguration { + compilerOptions: CompilerOptions; +} + export class Hasher { static version = '2.0'; private implicitDependencies: Promise; @@ -61,7 +69,9 @@ export class Hasher { this.fileHasher = new FileHasher(hashing); this.fileHasher.clear(); } - this.projectHashes = new ProjectHasher(this.projectGraph, this.hashing); + this.projectHashes = new ProjectHasher(this.projectGraph, this.hashing, { + selectivelyHashTsConfig: this.options.selectivelyHashTsConfig ?? false, + }); } async hashTaskWithDepsAndContext(task: Task): Promise { @@ -210,7 +220,6 @@ export class Hasher { ...implicitDepsFromPatterns, //TODO: vsavkin move the special cases into explicit ts support - 'tsconfig.base.json', 'package-lock.json', 'yarn.lock', 'pnpm-lock.yaml', @@ -227,6 +236,7 @@ export class Hasher { }), ...this.hashGlobalConfig(), ]; + const combinedHash = this.hashing.hashArray( fileHashes.map((v) => v.hash) ); @@ -269,13 +279,16 @@ class ProjectHasher { private sourceHashes: { [projectName: string]: Promise } = {}; private workspaceJson: WorkspaceJsonConfiguration; private nxJson: NxJsonConfiguration; + private tsConfigJson: TsconfigJsonConfiguration; constructor( private readonly projectGraph: ProjectGraph, - private readonly hashing: HashingImpl + private readonly hashing: HashingImpl, + private readonly options: { selectivelyHashTsConfig: boolean } ) { this.workspaceJson = this.readWorkspaceConfigFile(workspaceFileName()); this.nxJson = this.readNxJsonConfigFile('nx.json'); + this.tsConfigJson = this.readTsConfig(); } async hashProject( @@ -323,12 +336,21 @@ class ProjectHasher { ); const nxJson = JSON.stringify(this.nxJson.projects[projectName] ?? ''); + let tsConfig: string; + + if (this.options.selectivelyHashTsConfig) { + tsConfig = this.removeOtherProjectsPathRecords(projectName); + } else { + tsConfig = JSON.stringify(this.tsConfigJson); + } + res( this.hashing.hashArray([ ...fileNames, ...values, workspaceJson, nxJson, + tsConfig, ]) ); }); @@ -336,6 +358,35 @@ class ProjectHasher { return this.sourceHashes[projectName]; } + private removeOtherProjectsPathRecords(projectName: string) { + const { paths, ...compilerOptions } = this.tsConfigJson.compilerOptions; + + const rootPath = this.workspaceJson.projects[projectName].root.split('/'); + rootPath.shift(); + const pathAlias = `@${this.nxJson.npmScope}/${rootPath.join('/')}`; + + return JSON.stringify({ + compilerOptions: { + ...compilerOptions, + paths: { + [pathAlias]: paths[pathAlias] ?? [], + }, + }, + }); + } + + private readTsConfig() { + try { + const res = readJsonFile('tsconfig.base.json'); + res.compilerOptions.paths ??= {}; + return res; + } catch { + return { + compilerOptions: { paths: {} }, + }; + } + } + private readWorkspaceConfigFile(path: string): WorkspaceJsonConfiguration { try { const res = readJsonFile(path);