feat(core): finalize the input api

This commit is contained in:
Victor Savkin 2022-06-28 13:33:27 -04:00
parent dc7301634a
commit c52a8c2e9b
25 changed files with 320 additions and 446 deletions

View File

@ -204,9 +204,7 @@ jobs:
mainmacos: mainmacos:
executor: macos executor: macos
environment: environment:
NX_CLOUD_DISTRIBUTED_EXECUTION: 'true'
NX_E2E_CI_CACHE_KEY: e2e-circleci-macos NX_E2E_CI_CACHE_KEY: e2e-circleci-macos
NX_CLOUD_DISTRIBUTED_EXECUTION_AGENT_COUNT: 2
steps: steps:
- run: - run:
name: Set dynamic nx run variable name: Set dynamic nx run variable
@ -249,14 +247,6 @@ workflows:
name: 'agent7' name: 'agent7'
- agent: - agent:
name: 'agent8' name: 'agent8'
- agent:
name: 'agent9'
pm: 'npm'
os: 'macos'
- agent:
name: 'agent10'
pm: 'npm'
os: 'macos'
- main-linux - main-linux
- mainmacos: - mainmacos:
name: main-macos-e2e name: main-macos-e2e

View File

@ -33,45 +33,6 @@ chunk {main} main.js, main.js.map (main) 4.17 KiB [entry] [rendered]
Based on the state of the source code and the environment, Nx was able to figure out that it had already run this exact command. Nx found the artifact in the local cache and replayed the output and restored the necessary files. Based on the state of the source code and the environment, Nx was able to figure out that it had already run this exact command. Nx found the artifact in the local cache and replayed the output and restored the necessary files.
## --with-deps
Nx is smart, so it knows how applications and libraries in the workspace depend on each other.
**Run `nx lint todos --with-deps`, and you see that Nx lints both the `todos` app and the libraries it depends on.**
```bash
> NX Running target lint for project todos and its 2 deps.
———————————————————————————————————————————————
> nx run data:lint
Linting "data"...
All files pass linting.
> nx run auth:lint
Linting "auth"...
All files pass linting.
> nx run todos:lint
Linting "todos"...
All files pass linting.
———————————————————————————————————————————————
> NX SUCCESS Running target "lint" succeeded
```
> Add --parallel to any command, and Nx does most of the work in parallel.
## What's Next ## What's Next
- Continue to [Step 7: Test affected projects](/node-tutorial/07-test-affected-projects) - Continue to [Step 7: Test affected projects](/node-tutorial/07-test-affected-projects)

View File

@ -188,7 +188,7 @@ describe('Extra Nx Misc Tests', () => {
expect(resultArgs).toContain('camel: d'); expect(resultArgs).toContain('camel: d');
}, 120000); }, 120000);
it('should fail when a process exits non-zero', () => { it('ttt should fail when a process exits non-zero', () => {
updateProjectConfig(mylib, (config) => { updateProjectConfig(mylib, (config) => {
config.targets.error = { config.targets.error = {
executor: '@nrwl/workspace:run-commands', executor: '@nrwl/workspace:run-commands',
@ -203,7 +203,7 @@ describe('Extra Nx Misc Tests', () => {
runCLI(`run ${mylib}:error`); runCLI(`run ${mylib}:error`);
fail('Should error if process errors'); fail('Should error if process errors');
} catch (e) { } catch (e) {
expect(e.stderr.toString()).toContain( expect(e.stdout.toString()).toContain(
'Something went wrong in run-commands - Command failed: exit 1' 'Something went wrong in run-commands - Command failed: exit 1'
); );
} }

View File

@ -831,10 +831,10 @@ describe('Workspace Tests', () => {
} }
expect(error).toBeDefined(); expect(error).toBeDefined();
expect(error.stderr.toString()).toContain( expect(error.stdout.toString()).toContain(
`${lib1} is still depended on by the following projects` `${lib1} is still depended on by the following projects`
); );
expect(error.stderr.toString()).toContain(lib2); expect(error.stdout.toString()).toContain(lib2);
/** /**
* Try force removing the project * Try force removing the project

View File

@ -339,44 +339,6 @@ describe('Nx Affected and Graph Tests', () => {
}); });
compareTwoArrays(resWithTarget.projects, [`${myapp}-e2e`, myapp]); compareTwoArrays(resWithTarget.projects, [`${myapp}-e2e`, myapp]);
const resWithDeps = JSON.parse(
(
await runCLIAsync(
`print-affected --files=apps/${myapp}/src/app/app.element.spec.ts --target=build --with-deps`,
{ silent: true }
)
).stdout
);
expect(resWithDeps.tasks[0]).toMatchObject({
id: `${myapp}:build:production`,
overrides: {},
target: {
project: myapp,
target: 'build',
},
command: `${runNx} run ${myapp}:build:production`,
outputs: [`dist/apps/${myapp}`],
});
expect(resWithDeps.tasks[2]).toMatchObject({
id: `${mypublishablelib}:build`,
overrides: {},
target: {
project: mypublishablelib,
target: 'build',
},
command: `${runNx} run ${mypublishablelib}:build`,
outputs: [`dist/libs/${mypublishablelib}`],
});
compareTwoArrays(resWithDeps.projects, [
mylib,
mypublishablelib,
myapp,
`${myapp}-e2e`,
]);
const resWithTargetWithSelect1 = ( const resWithTargetWithSelect1 = (
await runCLIAsync( await runCLIAsync(
`print-affected --files=apps/${myapp}/src/app/app.element.spec.ts --target=test --select=projects`, `print-affected --files=apps/${myapp}/src/app/app.element.spec.ts --target=test --select=projects`,

View File

@ -172,7 +172,10 @@ describe('cache', () => {
runCLI(`generate @nrwl/js:lib ${child1}`); runCLI(`generate @nrwl/js:lib ${child1}`);
runCLI(`generate @nrwl/js:lib ${child2}`); runCLI(`generate @nrwl/js:lib ${child2}`);
updateJson(`nx.json`, (c) => { updateJson(`nx.json`, (c) => {
c.namedInputs = { prod: ['!**/*.spec.ts'] }; c.namedInputs = {
default: ['{projectRoot}/**/*'],
prod: ['!{projectRoot}/**/*.spec.ts'],
};
c.targetDefaults = { c.targetDefaults = {
test: { test: {
inputs: ['default', '^prod'], inputs: ['default', '^prod'],
@ -187,7 +190,7 @@ describe('cache', () => {
}); });
updateJson(`libs/${child1}/project.json`, (c) => { updateJson(`libs/${child1}/project.json`, (c) => {
c.namedInputs = { prod: ['**/*.ts'] }; c.namedInputs = { prod: ['{projectRoot}/**/*.ts'] };
return c; return c;
}); });

View File

@ -49,9 +49,6 @@ function getHttpServerArgs(options: Schema) {
function getBuildTargetCommand(options: Schema) { function getBuildTargetCommand(options: Schema) {
const cmd = ['nx', 'run', options.buildTarget]; const cmd = ['nx', 'run', options.buildTarget];
if (options.withDeps) {
cmd.push(`--with-deps`);
}
if (options.parallel) { if (options.parallel) {
cmd.push(`--parallel`); cmd.push(`--parallel`);
} }

View File

@ -19,6 +19,6 @@ export function updateJestConfig(host: Tree, options: JestProjectSchema) {
host, host,
findRootJestConfig(host), findRootJestConfig(host),
'projects', 'projects',
`<rootDir>/${project.root}` `<rootDir>/$"14.4.0-beta.5"root}`
); );
} }

View File

@ -24,8 +24,6 @@ export function assertDependentProjectsHaveBeenBuilt(
Please build these libraries first: Please build these libraries first:
${missing.map((x) => ` - ${x.node.name}`).join('\n')} ${missing.map((x) => ` - ${x.node.name}`).join('\n')}
Try: ${chalk.bold(`nx build ${context.projectName} --with-deps`)}
`) `)
); );
} }

View File

@ -1,7 +1,4 @@
{ {
"implicitDependencies": {
"package.json": "*"
},
"tasksRunnerOptions": { "tasksRunnerOptions": {
"default": { "default": {
"runner": "nx/tasks-runners/default", "runner": "nx/tasks-runners/default",

View File

@ -12,7 +12,6 @@ import {
} from '../utils/command-line-utils'; } from '../utils/command-line-utils';
import { performance } from 'perf_hooks'; import { performance } from 'perf_hooks';
import { createProjectGraphAsync } from '../project-graph/project-graph'; import { createProjectGraphAsync } from '../project-graph/project-graph';
import { withDeps } from '../project-graph/operators';
import { ProjectGraph, ProjectGraphProjectNode } from '../config/project-graph'; import { ProjectGraph, ProjectGraphProjectNode } from '../config/project-graph';
import { projectHasTarget } from '../utils/project-graph-utils'; import { projectHasTarget } from '../utils/project-graph-utils';
import { filterAffected } from '../project-graph/affected/affected-project-graph'; import { filterAffected } from '../project-graph/affected/affected-project-graph';
@ -147,12 +146,6 @@ function projectsToRun(
nxArgs nxArgs
) )
); );
if (!nxArgs.all && nxArgs.withDeps) {
affectedGraph = withDeps(
projectGraph,
Object.values(affectedGraph.nodes) as ProjectGraphProjectNode[]
);
}
if (nxArgs.exclude) { if (nxArgs.exclude) {
const excludedProjects = new Set(nxArgs.exclude); const excludedProjects = new Set(nxArgs.exclude);

View File

@ -8,7 +8,6 @@ jest.doMock('../utils/workspace-root', () => {
}); });
jest.mock('fs', () => require('memfs').fs); jest.mock('fs', () => require('memfs').fs);
require('fs').existsSync = () => true;
jest.mock('../utils/typescript'); jest.mock('../utils/typescript');
import { vol } from 'memfs'; import { vol } from 'memfs';
@ -28,22 +27,21 @@ describe('Hasher', () => {
}, },
}, },
}); });
let hashes = { const allWorkspaceFiles = [
'/root/yarn.lock': 'yarn.lock.hash', { file: 'yarn.lock', hash: 'yarn.lock.hash' },
'/root/nx.json': 'nx.json.hash', { file: 'nx.json', hash: 'nx.json.hash' },
'/root/package-lock.json': 'package-lock.json.hash', { file: 'package-lock.json', hash: 'package-lock.json.hash' },
'/root/package.json': 'package.json.hash', { file: 'package.json', hash: 'package.json.hash' },
'/root/pnpm-lock.yaml': 'pnpm-lock.yaml.hash', { file: 'pnpm-lock.yaml', hash: 'pnpm-lock.yaml.hash' },
'/root/tsconfig.base.json': tsConfigBaseJson, { file: 'tsconfig.base.json', hash: tsConfigBaseJson },
'/root/workspace.json': 'workspace.json.hash', { file: 'workspace.json', hash: 'workspace.json.hash' },
'/root/global1': 'global1.hash', { file: 'global1', hash: 'global1.hash' },
'/root/global2': 'global2.hash', { file: 'global2', hash: 'global2.hash' },
}; ];
function createHashing(): any { function createHashing(): any {
return { return {
hashArray: (values: string[]) => values.join('|'), hashArray: (values: string[]) => values.join('|'),
hashFile: (path: string) => hashes[path],
}; };
} }
@ -100,6 +98,7 @@ describe('Hasher', () => {
dependencies: { dependencies: {
parent: [], parent: [],
}, },
allWorkspaceFiles,
}, },
{} as any, {} as any,
{ {
@ -172,6 +171,7 @@ describe('Hasher', () => {
dependencies: { dependencies: {
parent: [{ source: 'parent', target: 'child', type: 'static' }], parent: [{ source: 'parent', target: 'child', type: 'static' }],
}, },
allWorkspaceFiles,
}, },
{} as any, {} as any,
{}, {},
@ -208,8 +208,8 @@ describe('Hasher', () => {
}, },
}, },
files: [ files: [
{ file: '/filea.ts', hash: 'a.hash' }, { file: 'libs/parent/filea.ts', hash: 'a.hash' },
{ file: '/filea.spec.ts', hash: 'a.spec.hash' }, { file: 'libs/parent/filea.spec.ts', hash: 'a.spec.hash' },
], ],
}, },
}, },
@ -223,8 +223,8 @@ describe('Hasher', () => {
}, },
targets: { build: {} }, targets: { build: {} },
files: [ files: [
{ file: '/fileb.ts', hash: 'b.hash' }, { file: 'libs/child/fileb.ts', hash: 'b.hash' },
{ file: '/fileb.spec.ts', hash: 'b.spec.hash' }, { file: 'libs/child/fileb.spec.ts', hash: 'b.spec.hash' },
], ],
}, },
}, },
@ -232,10 +232,11 @@ describe('Hasher', () => {
dependencies: { dependencies: {
parent: [{ source: 'parent', target: 'child', type: 'static' }], parent: [{ source: 'parent', target: 'child', type: 'static' }],
}, },
allWorkspaceFiles,
}, },
{ {
namedInputs: { namedInputs: {
prod: ['!**/*.spec.ts'], prod: ['!{projectRoot}/**/*.spec.ts'],
}, },
} as any, } as any,
{}, {},
@ -250,13 +251,13 @@ describe('Hasher', () => {
expect(onlySourceNodes(hash.details.nodes)).toEqual({ expect(onlySourceNodes(hash.details.nodes)).toEqual({
'child:$filesets': 'child:$filesets':
'/fileb.ts|/fileb.spec.ts|b.hash|b.spec.hash|{"root":"libs/child","namedInputs":{"prod":["default"]},"targets":{"build":{}}}|{"compilerOptions":{"paths":{"@nrwl/parent":["libs/parent/src/index.ts"],"@nrwl/child":["libs/child/src/index.ts"]}}}', 'libs/child/fileb.ts|libs/child/fileb.spec.ts|b.hash|b.spec.hash|{"root":"libs/child","namedInputs":{"prod":["default"]},"targets":{"build":{}}}|{"compilerOptions":{"paths":{"@nrwl/parent":["libs/parent/src/index.ts"],"@nrwl/child":["libs/child/src/index.ts"]}}}',
'parent:$filesets': 'parent:$filesets':
'/filea.ts|a.hash|{"root":"libs/parent","targets":{"build":{"inputs":["prod","^prod"]}}}|{"compilerOptions":{"paths":{"@nrwl/parent":["libs/parent/src/index.ts"],"@nrwl/child":["libs/child/src/index.ts"]}}}', 'libs/parent/filea.ts|a.hash|{"root":"libs/parent","targets":{"build":{"inputs":["prod","^prod"]}}}|{"compilerOptions":{"paths":{"@nrwl/parent":["libs/parent/src/index.ts"],"@nrwl/child":["libs/child/src/index.ts"]}}}',
}); });
}); });
it('should use targetDefaults from nx.json', async () => { it('should use targetdefaults from nx.json', async () => {
const hasher = new Hasher( const hasher = new Hasher(
{ {
nodes: { nodes: {
@ -269,8 +270,8 @@ describe('Hasher', () => {
build: {}, build: {},
}, },
files: [ files: [
{ file: '/filea.ts', hash: 'a.hash' }, { file: 'libs/parent/filea.ts', hash: 'a.hash' },
{ file: '/filea.spec.ts', hash: 'a.spec.hash' }, { file: 'libs/parent/filea.spec.ts', hash: 'a.spec.hash' },
], ],
}, },
}, },
@ -281,8 +282,8 @@ describe('Hasher', () => {
root: 'libs/child', root: 'libs/child',
targets: { build: {} }, targets: { build: {} },
files: [ files: [
{ file: '/fileb.ts', hash: 'b.hash' }, { file: 'libs/child/fileb.ts', hash: 'b.hash' },
{ file: '/fileb.spec.ts', hash: 'b.spec.hash' }, { file: 'libs/child/fileb.spec.ts', hash: 'b.spec.hash' },
], ],
}, },
}, },
@ -290,10 +291,11 @@ describe('Hasher', () => {
dependencies: { dependencies: {
parent: [{ source: 'parent', target: 'child', type: 'static' }], parent: [{ source: 'parent', target: 'child', type: 'static' }],
}, },
allWorkspaceFiles,
}, },
{ {
namedInputs: { namedInputs: {
prod: ['!**/*.spec.ts'], prod: ['!{projectRoot}/**/*.spec.ts'],
}, },
targetDefaults: { targetDefaults: {
build: { build: {
@ -313,9 +315,9 @@ describe('Hasher', () => {
expect(onlySourceNodes(hash.details.nodes)).toEqual({ expect(onlySourceNodes(hash.details.nodes)).toEqual({
'child:$filesets': 'child:$filesets':
'/fileb.ts|b.hash|{"root":"libs/child","targets":{"build":{}}}|{"compilerOptions":{"paths":{"@nrwl/parent":["libs/parent/src/index.ts"],"@nrwl/child":["libs/child/src/index.ts"]}}}', 'libs/child/fileb.ts|b.hash|{"root":"libs/child","targets":{"build":{}}}|{"compilerOptions":{"paths":{"@nrwl/parent":["libs/parent/src/index.ts"],"@nrwl/child":["libs/child/src/index.ts"]}}}',
'parent:$filesets': 'parent:$filesets':
'/filea.ts|a.hash|{"root":"libs/parent","targets":{"build":{}}}|{"compilerOptions":{"paths":{"@nrwl/parent":["libs/parent/src/index.ts"],"@nrwl/child":["libs/child/src/index.ts"]}}}', 'libs/parent/filea.ts|a.hash|{"root":"libs/parent","targets":{"build":{}}}|{"compilerOptions":{"paths":{"@nrwl/parent":["libs/parent/src/index.ts"],"@nrwl/child":["libs/child/src/index.ts"]}}}',
}); });
}); });
@ -336,6 +338,7 @@ describe('Hasher', () => {
dependencies: { dependencies: {
parent: [], parent: [],
}, },
allWorkspaceFiles,
}, },
{ npmScope: 'nrwl' } as any, { npmScope: 'nrwl' } as any,
{ {
@ -393,6 +396,7 @@ describe('Hasher', () => {
parent: [{ source: 'parent', target: 'child', type: 'static' }], parent: [{ source: 'parent', target: 'child', type: 'static' }],
child: [{ source: 'child', target: 'parent', type: 'static' }], child: [{ source: 'child', target: 'parent', type: 'static' }],
}, },
allWorkspaceFiles,
}, },
{} as any, {} as any,
{}, {},
@ -455,6 +459,7 @@ describe('Hasher', () => {
dependencies: { dependencies: {
parent: [], parent: [],
}, },
allWorkspaceFiles,
}, },
{} as any, {} as any,
{ {
@ -494,16 +499,7 @@ describe('Hasher', () => {
}, },
}, },
dependencies: {}, dependencies: {},
allWorkspaceFiles: [ allWorkspaceFiles,
{
file: 'global1',
hash: 'hash1',
},
{
file: 'global2',
hash: 'hash2',
},
],
}, },
{ {
implicitDependencies: { implicitDependencies: {
@ -554,6 +550,7 @@ describe('Hasher', () => {
{ source: 'app', target: 'npm:react', type: DependencyType.static }, { source: 'app', target: 'npm:react', type: DependencyType.static },
], ],
}, },
allWorkspaceFiles,
}, },
{} as any, {} as any,
{}, {},
@ -599,6 +596,7 @@ describe('Hasher', () => {
}, },
], ],
}, },
allWorkspaceFiles,
}, },
{} as any, {} as any,
{}, {},

View File

@ -1,6 +1,5 @@
import { exec } from 'child_process'; import { exec } from 'child_process';
import * as minimatch from 'minimatch'; import * as minimatch from 'minimatch';
import { existsSync } from 'fs';
import { getRootTsConfigFileName } from '../utils/typescript'; import { getRootTsConfigFileName } from '../utils/typescript';
import { defaultHashing, HashingImpl } from './hashing-impl'; import { defaultHashing, HashingImpl } from './hashing-impl';
import { import {
@ -14,8 +13,6 @@ import { Task } from '../config/task-graph';
import { readJsonFile } from '../utils/fileutils'; import { readJsonFile } from '../utils/fileutils';
import { InputDefinition } from '../config/workspace-json-project-json'; import { InputDefinition } from '../config/workspace-json-project-json';
import { getImportPath } from '../utils/path'; import { getImportPath } from '../utils/path';
import { workspaceRoot } from '../utils/workspace-root';
import { join } from 'path';
type ExpandedSelfInput = type ExpandedSelfInput =
| { fileset: string } | { fileset: string }
@ -183,7 +180,7 @@ export class Hasher {
const DEFAULT_INPUTS = [ const DEFAULT_INPUTS = [
{ {
projects: 'self', projects: 'self',
fileset: 'default', fileset: '{projectRoot}/**/*',
}, },
{ {
projects: 'dependencies', projects: 'dependencies',
@ -283,13 +280,15 @@ class TaskHasher {
task: Task, task: Task,
projectNode: ProjectGraphProjectNode<any> projectNode: ProjectGraphProjectNode<any>
): { depsInputs: { input: string }[]; selfInputs: ExpandedSelfInput[] } { ): { depsInputs: { input: string }[]; selfInputs: ExpandedSelfInput[] } {
const namedInputs = {
default: [{ fileset: '{projectRoot}/**/*' }],
...this.nxJson.namedInputs,
...projectNode.data.namedInputs,
};
if (task.target.target === '$input') { if (task.target.target === '$input') {
return { return {
depsInputs: [{ input: task.target.configuration }], depsInputs: [{ input: task.target.configuration }],
selfInputs: expandNamedInput(task.target.configuration, { selfInputs: expandNamedInput(task.target.configuration, namedInputs),
...this.nxJson.namedInputs,
...projectNode.data.namedInputs,
}),
}; };
} else { } else {
const targetData = projectNode.data.targets[task.target.target]; const targetData = projectNode.data.targets[task.target.target];
@ -299,7 +298,7 @@ class TaskHasher {
// task from TaskGraph can be added here // task from TaskGraph can be added here
return splitInputsIntoSelfAndDependencies( return splitInputsIntoSelfAndDependencies(
targetData.inputs || targetDefaults?.inputs || DEFAULT_INPUTS, targetData.inputs || targetDefaults?.inputs || DEFAULT_INPUTS,
{ ...this.nxJson.namedInputs, ...projectNode.data.namedInputs } namedInputs
); );
} }
} }
@ -340,25 +339,42 @@ class TaskHasher {
.filter((r) => !!r['fileset']) .filter((r) => !!r['fileset'])
.map((r) => r['fileset']); .map((r) => r['fileset']);
const projectFilesets = filesets.filter( const projectFilesets = [];
(r) => !r.startsWith('{workspaceRoot}') const workspaceFilesets = [];
); let invalidFileset = null;
const rootFilesets = filesets.filter((r) =>
r.startsWith('{workspaceRoot}/')
);
for (let f of filesets) {
if (f.startsWith('{projectRoot}/') || f.startsWith('!{projectRoot}/')) {
projectFilesets.push(f);
} else if (
f.startsWith('{workspaceRoot}/') ||
f.startsWith('!{workspaceRoot}/')
) {
workspaceFilesets.push(f);
} else {
invalidFileset = f;
}
}
if (invalidFileset) {
throw new Error(
[
`"${invalidFileset}" is an invalid fileset.`,
'All filesets have to start with either {workspaceRoot} or {projectRoot}.',
'For instance: "!{projectRoot}/**/*.spec.ts" or "{workspaceRoot}/package.json".',
`If "${invalidFileset}" is a named input, make sure it is defined in, for instance, nx.json.`,
].join('\n')
);
}
const notFilesets = inputs.filter((r) => !r['fileset']);
return Promise.all([ return Promise.all([
this.hashTaskFileset(task, projectFilesets), this.hashTaskFileset(task, projectFilesets),
...[ ...[
...rootFilesets, ...workspaceFilesets,
...this.legacyFilesetInputs.map((r) => r.fileset), ...this.legacyFilesetInputs.map((r) => r.fileset),
].map((fileset) => this.hashRootFileset(fileset)), ].map((fileset) => this.hashRootFileset(fileset)),
...[...inputs, ...this.legacyRuntimeInputs] ...[...notFilesets, ...this.legacyRuntimeInputs].map((r) =>
.filter((r) => !r['fileset']) r['runtime'] ? this.hashRuntime(r['runtime']) : this.hashEnv(r['env'])
.map((r) => ),
r['runtime'] ? this.hashRuntime(r['runtime']) : this.hashEnv(r['env'])
),
]); ]);
} }
@ -372,13 +388,14 @@ class TaskHasher {
this.projectGraph.allWorkspaceFiles this.projectGraph.allWorkspaceFiles
.filter((f) => minimatch(f.file, withoutWorkspaceRoot)) .filter((f) => minimatch(f.file, withoutWorkspaceRoot))
.forEach((f) => { .forEach((f) => {
parts.push(this.hashing.hashFile(join(workspaceRoot, f.file))); parts.push(f.hash);
}); });
} else { } else {
if (existsSync(join(workspaceRoot, withoutWorkspaceRoot))) { const matchingFile = this.projectGraph.allWorkspaceFiles.find(
parts.push( (t) => t.file === withoutWorkspaceRoot
this.hashing.hashFile(join(workspaceRoot, withoutWorkspaceRoot)) );
); if (matchingFile) {
parts.push(matchingFile.hash);
} }
} }
const value = this.hashing.hashArray(parts); const value = this.hashing.hashArray(parts);
@ -399,7 +416,14 @@ class TaskHasher {
if (!this.filesetHashes[mapKey]) { if (!this.filesetHashes[mapKey]) {
this.filesetHashes[mapKey] = new Promise(async (res) => { this.filesetHashes[mapKey] = new Promise(async (res) => {
const p = this.projectGraph.nodes[task.target.project]; const p = this.projectGraph.nodes[task.target.project];
const filteredFiles = this.filterFiles(p.data.files, filesetPatterns); const filesetWithExpandedProjectRoot = filesetPatterns.map((f) =>
f.replace('{projectRoot}', p.data.root)
);
const filteredFiles = this.filterFiles(
p.data.root,
p.data.files,
filesetWithExpandedProjectRoot
);
const fileNames = filteredFiles.map((f) => f.file); const fileNames = filteredFiles.map((f) => f.file);
const values = filteredFiles.map((f) => f.hash); const values = filteredFiles.map((f) => f.hash);
@ -450,9 +474,12 @@ class TaskHasher {
}; };
} }
private filterFiles(files: FileData[], patterns: string[]) { private filterFiles(
patterns = patterns.filter((p) => p !== 'default'); projectRoot: string,
if (patterns.length === 0) return files; files: FileData[],
patterns: string[]
) {
if (patterns.indexOf(`${projectRoot}/**/*`) > -1) return files;
return files.filter( return files.filter(
(f) => !!patterns.find((pattern) => minimatch(f.file, pattern)) (f) => !!patterns.find((pattern) => minimatch(f.file, pattern))
); );
@ -545,7 +572,6 @@ export function expandNamedInput(
input: string, input: string,
namedInputs: { [inputName: string]: (InputDefinition | string)[] } namedInputs: { [inputName: string]: (InputDefinition | string)[] }
): ExpandedSelfInput[] { ): ExpandedSelfInput[] {
if (input === 'default') return [{ fileset: 'default' }];
namedInputs ||= {}; namedInputs ||= {};
if (!namedInputs[input]) throw new Error(`Input '${input}' is not defined`); if (!namedInputs[input]) throw new Error(`Input '${input}' is not defined`);
return expandSelfInputs(namedInputs[input], namedInputs); return expandSelfInputs(namedInputs[input], namedInputs);

View File

@ -7,6 +7,7 @@ import { WholeFileChange } from '../../file-utils';
import { import {
getTouchedProjects, getTouchedProjects,
getImplicitlyTouchedProjects, getImplicitlyTouchedProjects,
extractGlobalFilesFromInputs,
} from './workspace-projects'; } from './workspace-projects';
function getFileChanges(files: string[]) { function getFileChanges(files: string[]) {
@ -147,6 +148,56 @@ describe('getImplicitlyTouchedProjects', () => {
}); });
}); });
describe('extractGlobalFilesFromInputs', () => {
it('should return list of global files from nx.json', () => {
const globalFiles = extractGlobalFilesFromInputs(
{
namedInputs: {
one: [
'{workspaceRoot}/global1.txt',
{ fileset: '{workspaceRoot}/global2.txt' },
'{projectRoot}/local.txt',
],
},
targetDefaults: {
build: {
inputs: ['{workspaceRoot}/global3.txt'],
},
},
},
{}
);
expect(globalFiles).toEqual(['global1.txt', 'global2.txt', 'global3.txt']);
});
it('should return list of global files from project configuration', () => {
const globalFiles = extractGlobalFilesFromInputs(
{},
{
one: {
name: 'one',
type: 'lib',
data: {
namedInputs: {
one: [
'{workspaceRoot}/global1.txt',
{ fileset: '{workspaceRoot}/global2.txt' },
'{projectRoot}/local.txt',
],
},
targets: {
build: {
inputs: ['{workspaceRoot}/global3.txt'],
},
},
},
},
}
);
expect(globalFiles).toEqual(['global1.txt', 'global2.txt', 'global3.txt']);
});
});
function buildProjectGraphNodes( function buildProjectGraphNodes(
projects: Record<string, ProjectConfiguration> projects: Record<string, ProjectConfiguration>
): ProjectGraph['nodes'] { ): ProjectGraph['nodes'] {

View File

@ -1,5 +1,7 @@
import * as minimatch from 'minimatch'; import * as minimatch from 'minimatch';
import { TouchedProjectLocator } from '../affected-project-graph-models'; import { TouchedProjectLocator } from '../affected-project-graph-models';
import { NxJsonConfiguration } from '../../../config/nx-json';
import { ProjectGraphProjectNode } from '../../../config/project-graph';
export const getTouchedProjects: TouchedProjectLocator = ( export const getTouchedProjects: TouchedProjectLocator = (
touchedFiles, touchedFiles,
@ -26,18 +28,22 @@ export const getTouchedProjects: TouchedProjectLocator = (
export const getImplicitlyTouchedProjects: TouchedProjectLocator = ( export const getImplicitlyTouchedProjects: TouchedProjectLocator = (
fileChanges, fileChanges,
workspaceJson, projectGraphNodes,
nxJson nxJson
): string[] => { ): string[] => {
if (!nxJson.implicitDependencies) { const implicits = { ...nxJson.implicitDependencies };
return []; const globalFiles = [
} ...extractGlobalFilesFromInputs(nxJson, projectGraphNodes),
'nx.json',
'package.json',
];
globalFiles.forEach((file) => {
implicits[file] = '*' as any;
});
const touched = new Set<string>(); const touched = new Set<string>();
for (const [pattern, projects] of Object.entries( for (const [pattern, projects] of Object.entries(implicits)) {
nxJson.implicitDependencies
)) {
const implicitDependencyWasChanged = fileChanges.some((f) => const implicitDependencyWasChanged = fileChanges.some((f) =>
minimatch(f.file, pattern) minimatch(f.file, pattern)
); );
@ -53,3 +59,49 @@ export const getImplicitlyTouchedProjects: TouchedProjectLocator = (
return Array.from(touched); return Array.from(touched);
}; };
export function extractGlobalFilesFromInputs(
nxJson: NxJsonConfiguration,
projectGraphNodes: Record<string, ProjectGraphProjectNode>
) {
const globalFiles = [];
globalFiles.push(...extractGlobalFilesFromNamedInputs(nxJson.namedInputs));
globalFiles.push(...extractGlobalFilesFromTargets(nxJson.targetDefaults));
Object.values(projectGraphNodes || {}).forEach((node) => {
globalFiles.push(
...extractGlobalFilesFromNamedInputs(node.data.namedInputs)
);
globalFiles.push(...extractGlobalFilesFromTargets(node.data.targets));
});
return globalFiles;
}
function extractGlobalFilesFromNamedInputs(namedInputs: any) {
const globalFiles = [];
for (const inputs of Object.values(namedInputs || {})) {
globalFiles.push(...extractGlobalFiles(inputs));
}
return globalFiles;
}
function extractGlobalFilesFromTargets(targets: any) {
const globalFiles = [];
for (const target of Object.values(targets || {})) {
if ((target as any).inputs) {
globalFiles.push(...extractGlobalFiles((target as any).inputs));
}
}
return globalFiles;
}
function extractGlobalFiles(inputs: any) {
const globalFiles = [];
for (const input of inputs) {
if (typeof input === 'string' && input.startsWith('{workspaceRoot}/')) {
globalFiles.push(input.substring('{workspaceRoot}/'.length));
} else if (input.fileset && input.fileset.startsWith('{workspaceRoot}/')) {
globalFiles.push(input.fileset.substring('{workspaceRoot}/'.length));
}
}
return globalFiles;
}

View File

@ -1,4 +1,4 @@
import { reverse, withDeps, filterNodes } from './operators'; import { reverse, filterNodes } from './operators';
import { import {
DependencyType, DependencyType,
ProjectGraph, ProjectGraph,
@ -163,144 +163,6 @@ describe('reverse', () => {
}); });
}); });
describe('withDeps', () => {
it('should return a new graph with all dependencies included from original', () => {
const affectedNodes: ProjectGraphProjectNode[] = [
{ name: 'app1-e2e', type: 'app', data: null },
{ name: 'app1', type: 'app', data: null },
{ name: 'lib1', type: 'lib', data: null },
];
const result = withDeps(graph, affectedNodes);
expect(result).toEqual({
nodes: {
lib3: {
name: 'lib3',
type: 'lib',
data: null,
},
lib2: {
name: 'lib2',
type: 'lib',
data: null,
},
lib1: {
name: 'lib1',
type: 'lib',
data: null,
},
app1: {
name: 'app1',
type: 'app',
data: null,
},
'app1-e2e': {
name: 'app1-e2e',
type: 'app',
data: null,
},
},
dependencies: {
lib2: [
{
type: 'static',
source: 'lib2',
target: 'lib3',
},
],
lib1: [
{
type: 'static',
source: 'lib1',
target: 'lib2',
},
{
type: 'static',
source: 'lib1',
target: 'lib3',
},
],
app1: [
{
type: 'static',
source: 'app1',
target: 'lib1',
},
],
'app1-e2e': [
{
type: 'implicit',
source: 'app1-e2e',
target: 'app1',
},
],
lib3: [],
},
});
});
it('should handle circular deps', () => {
const graph: ProjectGraph = {
nodes: {
lib1: { name: 'lib1', type: 'lib', data: null },
lib2: { name: 'lib2', type: 'lib', data: null },
},
dependencies: {
lib1: [
{
type: DependencyType.static,
source: 'lib1',
target: 'lib2',
},
],
lib2: [
{
type: DependencyType.static,
source: 'lib2',
target: 'lib1',
},
],
},
};
const affectedNodes: ProjectGraphProjectNode[] = [
{ name: 'lib1', type: 'lib', data: null },
];
const result = withDeps(graph, affectedNodes);
expect(result).toEqual({
nodes: {
lib1: {
name: 'lib1',
type: 'lib',
data: null,
},
lib2: {
name: 'lib2',
type: 'lib',
data: null,
},
},
dependencies: {
lib2: [
{
type: 'static',
source: 'lib2',
target: 'lib1',
},
],
lib1: [
{
type: 'static',
source: 'lib1',
target: 'lib2',
},
],
},
});
});
});
describe('filterNodes', () => { describe('filterNodes', () => {
it('filters out nodes based on predicate', () => { it('filters out nodes based on predicate', () => {
const result = filterNodes((n) => n.type === 'app')(graph); const result = filterNodes((n) => n.type === 'app')(graph);

View File

@ -219,7 +219,12 @@ function interpolateOverrides<T = any>(
Object.entries(interpolatedArgs).forEach(([name, value]) => { Object.entries(interpolatedArgs).forEach(([name, value]) => {
interpolatedArgs[name] = interpolatedArgs[name] =
typeof value === 'string' typeof value === 'string'
? interpolate(value, { project: { ...project, name: projectName } }) ? interpolate(value, {
workspaceRoot: '',
projectRoot: project.root,
projectName: project.name,
project: { ...project, name: projectName }, // this is legacy
})
: value; : value;
}); });
return interpolatedArgs; return interpolatedArgs;

View File

@ -55,10 +55,6 @@ export const defaultTasksRunner: TasksRunner<
options.lifeCycle.startCommand(); options.lifeCycle.startCommand();
try { try {
return await runAllTasks(tasks, options, context); return await runAllTasks(tasks, options, context);
} catch (e) {
console.error('Unexpected error:');
console.error(e);
process.exit(1);
} finally { } finally {
options.lifeCycle.endCommand(); options.lifeCycle.endCommand();
} }

View File

@ -22,7 +22,8 @@ import {
import { Task } from '../config/task-graph'; import { Task } from '../config/task-graph';
import { createTaskGraph } from './create-task-graph'; import { createTaskGraph } from './create-task-graph';
import { findCycle, makeAcyclic } from './task-graph-utils'; import { findCycle, makeAcyclic } from './task-graph-utils';
import { TargetDependencyConfig } from 'nx/src/config/workspace-json-project-json'; import { TargetDependencyConfig } from '../config/workspace-json-project-json';
import { handleErrors } from '../utils/params';
async function getTerminalOutputLifeCycle( async function getTerminalOutputLifeCycle(
initiatingProject: string, initiatingProject: string,
@ -90,81 +91,80 @@ export async function runCommand(
initiatingProject: string | null, initiatingProject: string | null,
extraTargetDependencies: Record<string, (TargetDependencyConfig | string)[]> extraTargetDependencies: Record<string, (TargetDependencyConfig | string)[]>
) { ) {
const { tasksRunner, runnerOptions } = getRunner(nxArgs, nxJson); const status = await handleErrors(overrides['verbose'] === true, async () => {
const { tasksRunner, runnerOptions } = getRunner(nxArgs, nxJson);
const defaultDependencyConfigs = mergeTargetDependencies( const defaultDependencyConfigs = mergeTargetDependencies(
nxJson.targetDefaults, nxJson.targetDefaults,
extraTargetDependencies extraTargetDependencies
); );
const projectNames = projectsToRun.map((t) => t.name); const projectNames = projectsToRun.map((t) => t.name);
const taskGraph = createTaskGraph( const taskGraph = createTaskGraph(
projectGraph,
defaultDependencyConfigs,
projectNames,
[nxArgs.target],
nxArgs.configuration,
overrides
);
const cycle = findCycle(taskGraph);
if (cycle) {
if (nxArgs.nxIgnoreCycles) {
output.warn({
title: `The task graph has a circular dependency`,
bodyLines: [`${cycle.join(' --> ')}`],
});
makeAcyclic(taskGraph);
} else {
output.error({
title: `Could not execute command because the task graph has a circular dependency`,
bodyLines: [`${cycle.join(' --> ')}`],
});
process.exit(1);
}
}
const tasks = Object.values(taskGraph.tasks);
if (nxArgs.outputStyle == 'stream') {
process.env.NX_STREAM_OUTPUT = 'true';
process.env.NX_PREFIX_OUTPUT = 'true';
}
if (nxArgs.outputStyle == 'stream-without-prefixes') {
process.env.NX_STREAM_OUTPUT = 'true';
}
const { lifeCycle, renderIsDone } = await getTerminalOutputLifeCycle(
initiatingProject,
projectNames,
tasks,
nxArgs,
overrides,
runnerOptions
);
const lifeCycles = [lifeCycle] as LifeCycle[];
if (process.env.NX_PERF_LOGGING) {
lifeCycles.push(new TaskTimingsLifeCycle());
}
if (process.env.NX_PROFILE) {
lifeCycles.push(new TaskProfilingLifeCycle(process.env.NX_PROFILE));
}
const promiseOrObservable = tasksRunner(
tasks,
{ ...runnerOptions, lifeCycle: new CompositeLifeCycle(lifeCycles) },
{
initiatingProject:
nxArgs.outputStyle === 'compact' ? null : initiatingProject,
target: nxArgs.target,
projectGraph, projectGraph,
nxJson, defaultDependencyConfigs,
nxArgs, projectNames,
taskGraph, [nxArgs.target],
} nxArgs.configuration,
); overrides
);
let anyFailures; const cycle = findCycle(taskGraph);
try { if (cycle) {
if (nxArgs.nxIgnoreCycles) {
output.warn({
title: `The task graph has a circular dependency`,
bodyLines: [`${cycle.join(' --> ')}`],
});
makeAcyclic(taskGraph);
} else {
output.error({
title: `Could not execute command because the task graph has a circular dependency`,
bodyLines: [`${cycle.join(' --> ')}`],
});
process.exit(1);
}
}
const tasks = Object.values(taskGraph.tasks);
if (nxArgs.outputStyle == 'stream') {
process.env.NX_STREAM_OUTPUT = 'true';
process.env.NX_PREFIX_OUTPUT = 'true';
}
if (nxArgs.outputStyle == 'stream-without-prefixes') {
process.env.NX_STREAM_OUTPUT = 'true';
}
const { lifeCycle, renderIsDone } = await getTerminalOutputLifeCycle(
initiatingProject,
projectNames,
tasks,
nxArgs,
overrides,
runnerOptions
);
const lifeCycles = [lifeCycle] as LifeCycle[];
if (process.env.NX_PERF_LOGGING) {
lifeCycles.push(new TaskTimingsLifeCycle());
}
if (process.env.NX_PROFILE) {
lifeCycles.push(new TaskProfilingLifeCycle(process.env.NX_PROFILE));
}
const promiseOrObservable = tasksRunner(
tasks,
{ ...runnerOptions, lifeCycle: new CompositeLifeCycle(lifeCycles) },
{
initiatingProject:
nxArgs.outputStyle === 'compact' ? null : initiatingProject,
target: nxArgs.target,
projectGraph,
nxJson,
nxArgs,
taskGraph,
}
);
let anyFailures;
if ((promiseOrObservable as any).subscribe) { if ((promiseOrObservable as any).subscribe) {
anyFailures = await anyFailuresInObservable(promiseOrObservable); anyFailures = await anyFailuresInObservable(promiseOrObservable);
} else { } else {
@ -172,18 +172,11 @@ export async function runCommand(
anyFailures = await anyFailuresInPromise(promiseOrObservable as any); anyFailures = await anyFailuresInPromise(promiseOrObservable as any);
} }
await renderIsDone; await renderIsDone;
} catch (e) { return anyFailures ? 1 : 0;
output.error({ });
title: 'Unhandled error in task executor',
});
console.error(e);
process.exit(1);
}
// fix for https://github.com/nrwl/nx/issues/1666 // fix for https://github.com/nrwl/nx/issues/1666
if (process.stdin['unref']) (process.stdin as any).unref(); if (process.stdin['unref']) (process.stdin as any).unref();
process.exit(status);
process.exit(anyFailures ? 1 : 0);
} }
function mergeTargetDependencies( function mergeTargetDependencies(
@ -261,10 +254,7 @@ export function getRunner(
let runner = nxArgs.runner; let runner = nxArgs.runner;
runner = runner || 'default'; runner = runner || 'default';
if (!nxJson.tasksRunnerOptions) { if (!nxJson.tasksRunnerOptions) {
output.error({ throw new Error(`Could not find any runner configurations in nx.json`);
title: `Could not find any runner configurations in nx.json`,
});
process.exit(1);
} }
if (nxJson.tasksRunnerOptions[runner]) { if (nxJson.tasksRunnerOptions[runner]) {
let modulePath: string = nxJson.tasksRunnerOptions[runner].runner; let modulePath: string = nxJson.tasksRunnerOptions[runner].runner;
@ -292,9 +282,6 @@ export function getRunner(
}, },
}; };
} else { } else {
output.error({ throw new Error(`Could not find runner configuration for ${runner}`);
title: `Could not find runner configuration for ${runner}`,
});
process.exit(1);
} }
} }

View File

@ -68,7 +68,7 @@ describe('utils', () => {
getOutputsForTargetAndConfiguration( getOutputsForTargetAndConfiguration(
task, task,
getNode({ getNode({
outputs: ['{project.root}/sub', 'two'], outputs: ['{projectRoot}/sub', 'two'],
options: { options: {
myVar: 'one', myVar: 'one',
}, },

View File

@ -97,8 +97,11 @@ export function getOutputsForTargetAndConfiguration(
return targets.outputs return targets.outputs
.map((output: string) => { .map((output: string) => {
const interpolated = interpolate(output, { const interpolated = interpolate(output, {
workspaceRoot: '', // this is to make sure interpolation works
projectRoot: node.data.root,
projectName: node.data.name,
project: { ...node.data, name: node.data.name }, // this is legacy
options, options,
project: { ...node.data, name: node.name },
}); });
return isRelativePath(interpolated) return isRelativePath(interpolated)
? joinPathFragments(node.data.root, interpolated) ? joinPathFragments(node.data.root, interpolated)

View File

@ -76,7 +76,6 @@ const runOne: string[] = [
'exclude', 'exclude',
'onlyFailed', 'onlyFailed',
'help', 'help',
'withDeps',
'skipNxCache', 'skipNxCache',
'scan', 'scan',
'outputStyle', 'outputStyle',
@ -120,7 +119,6 @@ export interface NxArgs {
help?: boolean; help?: boolean;
version?: boolean; version?: boolean;
plain?: boolean; plain?: boolean;
withDeps?: boolean;
projects?: string[]; projects?: string[];
select?: string; select?: string;
skipNxCache?: boolean; skipNxCache?: boolean;

View File

@ -4,7 +4,7 @@ import {
TargetConfiguration, TargetConfiguration,
ProjectsConfigurations, ProjectsConfigurations,
} from '../config/workspace-json-project-json'; } from '../config/workspace-json-project-json';
import { execSync } from 'child_process'; import { output } from './output';
type PropertyDescription = { type PropertyDescription = {
type?: string | string[]; type?: string | string[];
@ -77,16 +77,21 @@ export async function handleErrors(isVerbose: boolean, fn: Function) {
return await fn(); return await fn();
} catch (err) { } catch (err) {
err ??= new Error('Unknown error caught'); err ??= new Error('Unknown error caught');
if (err.constructor.name === 'UnsuccessfulWorkflowExecution') { if (err.constructor.name === 'UnsuccessfulWorkflowExecution') {
logger.error('The generator workflow failed. See above.'); logger.error('The generator workflow failed. See above.');
} else if (err.message) {
logger.error(err.message);
} else { } else {
logger.error(err); const lines = (err.message ? err.message : err.toString()).split('\n');
} const bodyLines = lines.slice(1);
if (isVerbose && err.stack) { if (err.stack && !isVerbose) {
logger.info(err.stack); bodyLines.push('Pass --verbose to see the stacktrace.');
}
output.error({
title: lines[0],
bodyLines,
});
if (err.stack && isVerbose) {
logger.info(err.stack);
}
} }
return 1; return 1;
} }

View File

@ -47,9 +47,6 @@ function getHttpServerArgs(options: Schema) {
function getBuildTargetCommand(options: Schema) { function getBuildTargetCommand(options: Schema) {
const cmd = ['nx', 'run', options.buildTarget]; const cmd = ['nx', 'run', options.buildTarget];
if (options.withDeps) {
cmd.push(`--with-deps`);
}
if (options.parallel) { if (options.parallel) {
cmd.push(`--parallel`); cmd.push(`--parallel`);
} }

View File

@ -212,20 +212,13 @@ export function checkDependentProjectsHaveBeenBuilt(
targetName, targetName,
projectDependencies projectDependencies
); );
if (missing.length === projectDependencies.length && missing.length > 0) { if (missing.length > 0) {
console.error(stripIndents` console.error(stripIndents`
It looks like all of ${projectName}'s dependencies have not been built yet: It looks like all of ${projectName}'s dependencies have not been built yet:
${missing.map((x) => ` - ${x.node.name}`).join('\n')} ${missing.map((x) => ` - ${x.node.name}`).join('\n')}
You might be missing a "targetDefaults" configuration in your root nx.json (https://nx.dev/configuration/packagejson#target-defaults), You might be missing a "targetDefaults" configuration in your root nx.json (https://nx.dev/configuration/projectjson#target-defaults),
or "dependsOn" configured in ${projectName}'s angular.json/workspace.json record or project.json (https://nx.dev/configuration/packagejson#dependson) or "dependsOn" configured in ${projectName}'s project.json (https://nx.dev/configuration/projectjson#dependson)
`);
} else if (missing.length > 0) {
console.error(stripIndents`
Some of the project ${projectName}'s dependencies have not been built yet. Please build these libraries before:
${missing.map((x) => ` - ${x.node.name}`).join('\n')}
Try: nx run ${projectName}:${targetName} --with-deps
`); `);
return false; return false;
} else { } else {