feat(core): rewrite path mappings when buildings apps

This commit is contained in:
Victor Savkin 2020-03-19 16:32:21 -04:00 committed by Victor Savkin
parent 0b7535ae92
commit 2c42431130
41 changed files with 626 additions and 599 deletions

View File

@ -64,11 +64,13 @@ The prefix to apply to generated selectors.
### publishable
Alias(es): buildable
Default: `false`
Type: `boolean`
Generate a simple TS library when set to true.
Generate a buildable library.
### routing

View File

@ -78,9 +78,11 @@ Library name
### publishable
Alias(es): buildable
Type: `boolean`
Create a publishable library. A "build" architect will be added for this project the workspace configuration.
Create a buildable library.
### service

View File

@ -12,6 +12,14 @@ Type: `array`
List of static application assets.
### buildLibsFromSource
Default: `false`
Type: `boolean`
Read buildable libraries from source instead of building them separately.
### externalDependencies
Default: `all`

View File

@ -62,9 +62,11 @@ Library name
### publishable
Alias(es): buildable
Type: `boolean`
Create a publishable library. A "build" architect will be added for this project the workspace configuration.
Create a publishable library.
### skipFormat

View File

@ -102,9 +102,11 @@ Use pascal case component file name (e.g. App.tsx)
### publishable
Alias(es): buildable
Type: `boolean`
Create a publishable library. A "build" architect will be added for this project the workspace configuration.
Create a buildable library.
### routing

View File

@ -26,6 +26,14 @@ Type: `array`
Budget thresholds to ensure parts of your application stay within boundaries which you set.
### buildLibsFromSource
Default: `false`
Type: `boolean`
Read buildable libraries from source instead of building them separately.
### commonChunk
Default: `true`

View File

@ -64,11 +64,13 @@ The prefix to apply to generated selectors.
### publishable
Alias(es): buildable
Default: `false`
Type: `boolean`
Generate a simple TS library when set to true.
Generate a buildable library.
### routing

View File

@ -78,9 +78,11 @@ Library name
### publishable
Alias(es): buildable
Type: `boolean`
Create a publishable library. A "build" architect will be added for this project the workspace configuration.
Create a buildable library.
### service

View File

@ -13,6 +13,14 @@ Type: `array`
List of static application assets.
### buildLibsFromSource
Default: `false`
Type: `boolean`
Read buildable libraries from source instead of building them separately.
### externalDependencies
Default: `all`

View File

@ -62,9 +62,11 @@ Library name
### publishable
Alias(es): buildable
Type: `boolean`
Create a publishable library. A "build" architect will be added for this project the workspace configuration.
Create a publishable library.
### skipFormat

View File

@ -102,9 +102,11 @@ Use pascal case component file name (e.g. App.tsx)
### publishable
Alias(es): buildable
Type: `boolean`
Create a publishable library. A "build" architect will be added for this project the workspace configuration.
Create a buildable library.
### routing

View File

@ -27,6 +27,14 @@ Type: `array`
Budget thresholds to ensure parts of your application stay within boundaries which you set.
### buildLibsFromSource
Default: `false`
Type: `boolean`
Read buildable libraries from source instead of building them separately.
### commonChunk
Default: `true`

View File

@ -64,11 +64,13 @@ The prefix to apply to generated selectors.
### publishable
Alias(es): buildable
Default: `false`
Type: `boolean`
Generate a simple TS library when set to true.
Generate a buildable library.
### routing

View File

@ -78,9 +78,11 @@ Library name
### publishable
Alias(es): buildable
Type: `boolean`
Create a publishable library. A "build" architect will be added for this project the workspace configuration.
Create a buildable library.
### service

View File

@ -13,6 +13,14 @@ Type: `array`
List of static application assets.
### buildLibsFromSource
Default: `false`
Type: `boolean`
Read buildable libraries from source instead of building them separately.
### externalDependencies
Default: `all`

View File

@ -62,9 +62,11 @@ Library name
### publishable
Alias(es): buildable
Type: `boolean`
Create a publishable library. A "build" architect will be added for this project the workspace configuration.
Create a publishable library.
### skipFormat

View File

@ -102,9 +102,11 @@ Use pascal case component file name (e.g. App.tsx)
### publishable
Alias(es): buildable
Type: `boolean`
Create a publishable library. A "build" architect will be added for this project the workspace configuration.
Create a buildable library.
### routing

View File

@ -27,6 +27,14 @@ Type: `array`
Budget thresholds to ensure parts of your application stay within boundaries which you set.
### buildLibsFromSource
Default: `false`
Type: `boolean`
Read buildable libraries from source instead of building them separately.
### commonChunk
Default: `true`

View File

@ -19,7 +19,8 @@ import {
runNgAdd,
copyMissingPackages,
setMaxWorkers,
newProject
newProject,
checkFilesDoNotExist
} from './utils';
import { stripIndents } from '@angular-devkit/core/src/utils/literals';
import { readFile } from './utils';
@ -43,6 +44,27 @@ forEachCli(currentCLIName => {
const linter = currentCLIName === 'angular' ? 'tslint' : 'eslint';
describe('Node Applications', () => {
it('should be able to generate an empty application', async () => {
ensureProject();
const nodeapp = uniq('nodeapp');
runCLI(`generate @nrwl/node:app ${nodeapp} --linter=${linter}`);
setMaxWorkers(nodeapp);
const lintResults = runCLI(`lint ${nodeapp}`);
expect(lintResults).toContain('All files pass linting.');
updateFile(`apps/${nodeapp}/src/main.ts`, `console.log('Hello World!');`);
await runCLIAsync(`build ${nodeapp}`);
checkFilesExist(`dist/apps/${nodeapp}/main.js`);
const result = execSync(`node dist/apps/${nodeapp}/main.js`, {
cwd: tmpProjPath()
}).toString();
expect(result).toContain('Hello World!');
}, 60000);
it('should be able to generate an express application', async done => {
ensureProject();
const nodeapp = uniq('nodeapp');
@ -227,95 +249,8 @@ forEachCli(currentCLIName => {
});
});
}, 120000);
describe('nest libraries', function() {
it('should be able to generate a nest library', async () => {
ensureProject();
const nestlib = uniq('nestlib');
runCLI(`generate @nrwl/nest:lib ${nestlib}`);
const jestConfigContent = readFile(`libs/${nestlib}/jest.config.js`);
expect(stripIndents`${jestConfigContent}`).toEqual(
stripIndents`module.exports = {
name: '${nestlib}',
preset: '../../jest.config.js',
testEnvironment: 'node',
transform: {
'^.+\\.[tj]sx?$': 'ts-jest'
},
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'html'],
coverageDirectory: '../../coverage/libs/${nestlib}'
};
`
);
const lintResults = runCLI(`lint ${nestlib}`);
expect(lintResults).toContain('All files pass linting.');
}, 60000);
it('should be able to generate a nest library w/ service', async () => {
ensureProject();
const nestlib = uniq('nestlib');
runCLI(`generate @nrwl/nest:lib ${nestlib} --service`);
const lintResults = runCLI(`lint ${nestlib}`);
expect(lintResults).toContain('All files pass linting.');
const jestResult = await runCLIAsync(`test ${nestlib}`);
expect(jestResult.stderr).toContain('Test Suites: 1 passed, 1 total');
}, 60000);
it('should be able to generate a nest library w/ controller', async () => {
ensureProject();
const nestlib = uniq('nestlib');
runCLI(`generate @nrwl/nest:lib ${nestlib} --controller`);
const lintResults = runCLI(`lint ${nestlib}`);
expect(lintResults).toContain('All files pass linting.');
const jestResult = await runCLIAsync(`test ${nestlib}`);
expect(jestResult.stderr).toContain('Test Suites: 1 passed, 1 total');
}, 60000);
it('should be able to generate a nest library w/ controller and service', async () => {
ensureProject();
const nestlib = uniq('nestlib');
runCLI(`generate @nrwl/nest:lib ${nestlib} --controller --service`);
const lintResults = runCLI(`lint ${nestlib}`);
expect(lintResults).toContain('All files pass linting.');
const jestResult = await runCLIAsync(`test ${nestlib}`);
expect(jestResult.stderr).toContain('Test Suites: 2 passed, 2 total');
}, 60000);
});
it('should be able to generate an empty application', async () => {
ensureProject();
const nodeapp = uniq('nodeapp');
runCLI(`generate @nrwl/node:app ${nodeapp} --linter=${linter}`);
setMaxWorkers(nodeapp);
const lintResults = runCLI(`lint ${nodeapp}`);
expect(lintResults).toContain('All files pass linting.');
updateFile(`apps/${nodeapp}/src/main.ts`, `console.log('Hello World!');`);
await runCLIAsync(`build ${nodeapp}`);
checkFilesExist(`dist/apps/${nodeapp}/main.js`);
const result = execSync(`node dist/apps/${nodeapp}/main.js`, {
cwd: tmpProjPath()
}).toString();
expect(result).toContain('Hello World!');
}, 60000);
});
describe('Node Libraries', () => {
it('should be able to generate a node library', async () => {
ensureProject();
@ -390,148 +325,208 @@ forEachCli(currentCLIName => {
runCLI(`build ${nodelib}`);
checkFilesExist(`./dist/libs/${nodelib}/esm2015/index.js`);
}, 60000);
});
describe('with dependencies', () => {
beforeAll(() => {
// force a new project to avoid collissions with the npmScope that has been altered before
newProject();
});
describe('nest libraries', function() {
it('should be able to generate a nest library', async () => {
ensureProject();
const nestlib = uniq('nestlib');
/**
* Graph:
*
* childLib
* /
* parentLib =>
* \
* \
* childLib2
*
*/
let parentLib: string;
let childLib: string;
let childLib2: string;
runCLI(`generate @nrwl/nest:lib ${nestlib}`);
beforeEach(() => {
parentLib = uniq('parentlib');
childLib = uniq('childlib');
childLib2 = uniq('childlib2');
const jestConfigContent = readFile(`libs/${nestlib}/jest.config.js`);
ensureProject();
runCLI(`generate @nrwl/node:lib ${parentLib} --publishable=true`);
runCLI(`generate @nrwl/node:lib ${childLib} --publishable=true`);
runCLI(`generate @nrwl/node:lib ${childLib2} --publishable=true`);
// create dependencies by importing
const createDep = (parent, children: string[]) => {
updateFile(
`libs/${parent}/src/lib/${parent}.ts`,
expect(stripIndents`${jestConfigContent}`).toEqual(
stripIndents`module.exports = {
name: '${nestlib}',
preset: '../../jest.config.js',
testEnvironment: 'node',
transform: {
'^.+\\.[tj]sx?$': 'ts-jest'
},
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'html'],
coverageDirectory: '../../coverage/libs/${nestlib}'
};
`
);
const lintResults = runCLI(`lint ${nestlib}`);
expect(lintResults).toContain('All files pass linting.');
}, 60000);
it('should be able to generate a nest library w/ service', async () => {
ensureProject();
const nestlib = uniq('nestlib');
runCLI(`generate @nrwl/nest:lib ${nestlib} --service`);
const lintResults = runCLI(`lint ${nestlib}`);
expect(lintResults).toContain('All files pass linting.');
const jestResult = await runCLIAsync(`test ${nestlib}`);
expect(jestResult.stderr).toContain('Test Suites: 1 passed, 1 total');
}, 60000);
it('should be able to generate a nest library w/ controller', async () => {
ensureProject();
const nestlib = uniq('nestlib');
runCLI(`generate @nrwl/nest:lib ${nestlib} --controller`);
const lintResults = runCLI(`lint ${nestlib}`);
expect(lintResults).toContain('All files pass linting.');
const jestResult = await runCLIAsync(`test ${nestlib}`);
expect(jestResult.stderr).toContain('Test Suites: 1 passed, 1 total');
}, 60000);
it('should be able to generate a nest library w/ controller and service', async () => {
ensureProject();
const nestlib = uniq('nestlib');
runCLI(`generate @nrwl/nest:lib ${nestlib} --controller --service`);
const lintResults = runCLI(`lint ${nestlib}`);
expect(lintResults).toContain('All files pass linting.');
const jestResult = await runCLIAsync(`test ${nestlib}`);
expect(jestResult.stderr).toContain('Test Suites: 2 passed, 2 total');
}, 60000);
});
describe('with dependencies', () => {
/**
* Graph:
*
* childLib
* /
* app => parentLib =>
* \
* \
* childLib2
*
*/
let app: string;
let parentLib: string;
let childLib: string;
let childLib2: string;
beforeEach(() => {
app = uniq('app');
parentLib = uniq('parentlib');
childLib = uniq('childlib');
childLib2 = uniq('childlib2');
ensureProject();
runCLI(`generate @nrwl/express:app ${app}`);
setMaxWorkers(app);
runCLI(`generate @nrwl/node:lib ${parentLib} --publishable=true`);
runCLI(`generate @nrwl/node:lib ${childLib} --publishable=true`);
runCLI(`generate @nrwl/node:lib ${childLib2} --publishable=true`);
// create dependencies by importing
const createDep = (parent, children: string[]) => {
updateFile(
`libs/${parent}/src/lib/${parent}.ts`,
`
${children
.map(entry => `import { ${entry} } from '@proj/${entry}';`)
.join('\n')}
export function ${parent}(): string {
return '${parent}' + ' ' + ${children
.map(entry => `${entry}()`)
.join('+')}
.map(entry => `${entry}()`)
.join('+')}
}
`
);
};
createDep(parentLib, [childLib, childLib2]);
});
it('should throw an error if the dependent library has not been built before building the parent lib', () => {
expect.assertions(2);
try {
runCLI(`build ${parentLib}`);
} catch (e) {
expect(e.stderr.toString()).toContain(
`Some of the project ${parentLib}'s dependencies have not been built yet. Please build these libraries before:`
);
expect(e.stderr.toString()).toContain(`${childLib}`);
}
});
it('should build a library without dependencies', () => {
const childLibOutput = runCLI(`build ${childLib}`);
expect(childLibOutput).toContain(
`Done compiling TypeScript files for library ${childLib}`
);
};
createDep(parentLib, [childLib, childLib2]);
updateFile(
`apps/${app}/src/main.ts`,
`
import "@proj/${parentLib}";
`
);
// we are setting paths to {} to make sure built libs are read from dist
updateFile('tsconfig.json', c => {
const json = JSON.parse(c);
json.compilerOptions.paths = {};
return JSON.stringify(json, null, 2);
});
it('should build a parent library if the dependent libraries have been built before', () => {
const childLibOutput = runCLI(`build ${childLib}`);
expect(childLibOutput).toContain(
`Done compiling TypeScript files for library ${childLib}`
);
const childLib2Output = runCLI(`build ${childLib2}`);
expect(childLib2Output).toContain(
`Done compiling TypeScript files for library ${childLib2}`
);
const parentLibOutput = runCLI(`build ${parentLib}`);
expect(parentLibOutput).toContain(
`Done compiling TypeScript files for library ${parentLib}`
);
// assert package.json deps have been set
const assertPackageJson = (
parent: string,
lib: string,
version: string
) => {
const jsonFile = readJson(`dist/libs/${parent}/package.json`);
const childDependencyVersion = jsonFile.dependencies[`@proj/${lib}`];
expect(childDependencyVersion).toBe(version);
};
assertPackageJson(parentLib, childLib, '0.0.1');
assertPackageJson(parentLib, childLib2, '0.0.1');
});
// it('should automatically build all deps and update package.json when passing --withDeps flags', () => {
// const parentLibOutput = runCLI(`build ${parentLib} --withDeps`);
// expect(parentLibOutput).toContain(
// `Done compiling TypeScript files for library ${parentLib}`
// );
// expect(parentLibOutput).toContain(
// `Done compiling TypeScript files for library ${childLib}`
// );
// expect(parentLibOutput).toContain(
// `Done compiling TypeScript files for library ${childChildLib}`
// );
// expect(parentLibOutput).toContain(
// `Done compiling TypeScript files for library ${childLib2}`
// );
// expect(parentLibOutput).toContain(
// `Done compiling TypeScript files for library ${childLibShared}`
// );
// // // assert package.json deps have been set
// const assertPackageJson = (
// parent: string,
// lib: string,
// version: string
// ) => {
// const jsonFile = readJson(`dist/libs/${parent}/package.json`);
// const childDependencyVersion =
// jsonFile.dependencies[`@proj/${lib}`];
// expect(childDependencyVersion).toBe(version);
// };
// assertPackageJson(parentLib, childLib, '0.0.1');
// assertPackageJson(childLib, childChildLib, '0.0.1');
// assertPackageJson(childLib, childLibShared, '0.0.1');
// assertPackageJson(childLib2, childLibShared, '0.0.1');
// });
});
it('should throw an error if the dependent library has not been built before building the parent lib', () => {
expect.assertions(2);
try {
runCLI(`build ${parentLib}`);
} catch (e) {
expect(e.stderr.toString()).toContain(
`Some of the project ${parentLib}'s dependencies have not been built yet. Please build these libraries before:`
);
expect(e.stderr.toString()).toContain(`${childLib}`);
}
});
it('should build a library without dependencies', () => {
const childLibOutput = runCLI(`build ${childLib}`);
expect(childLibOutput).toContain(
`Done compiling TypeScript files for library ${childLib}`
);
});
it('should build a parent library if the dependent libraries have been built before', () => {
const childLibOutput = runCLI(`build ${childLib}`);
expect(childLibOutput).toContain(
`Done compiling TypeScript files for library ${childLib}`
);
const childLib2Output = runCLI(`build ${childLib2}`);
expect(childLib2Output).toContain(
`Done compiling TypeScript files for library ${childLib2}`
);
const parentLibOutput = runCLI(`build ${parentLib}`);
expect(parentLibOutput).toContain(
`Done compiling TypeScript files for library ${parentLib}`
);
// assert package.json deps have been set
const assertPackageJson = (
parent: string,
lib: string,
version: string
) => {
const jsonFile = readJson(`dist/libs/${parent}/package.json`);
const childDependencyVersion = jsonFile.dependencies[`@proj/${lib}`];
expect(childDependencyVersion).toBe(version);
};
assertPackageJson(parentLib, childLib, '0.0.1');
assertPackageJson(parentLib, childLib2, '0.0.1');
});
if (currentCLIName === 'nx') {
it('should build an app composed out of buildable libs', () => {
const buildWithDeps = runCLI(`build ${app} --with-deps`);
expect(buildWithDeps).toContain(`Running target "build" succeeded`);
checkFilesDoNotExist(`apps/${app}/tsconfig/tsconfig.nx-tmp`);
// we remove all path mappings from the root tsconfig, so when trying to build
// libs from source, the builder will throw
const failedBuild = runCLI(
`build ${app} --with-deps --buildLibsFromSource`,
{ silenceError: true }
);
expect(failedBuild).toContain(`Can't resolve`);
}, 1000000);
}
});
});

View File

@ -1,44 +1,50 @@
import {
checkFilesDoNotExist,
ensureProject,
forEachCli,
readJson,
runCLI,
setMaxWorkers,
uniq,
updateFile
} from './utils';
forEachCli('nx', cli => {
describe('Build React library', () => {
describe('Build React libraries and apps', () => {
/**
* Graph:
*
* childLib
* /
* parentLib =>
* \
* \
* childLib2
* childLib
* /
* app => parentLib =>
* \
* childLib2
*
*/
let app: string;
let parentLib: string;
let childLib: string;
let childLib2: string;
beforeEach(() => {
app = uniq('app');
parentLib = uniq('parentlib');
childLib = uniq('childlib');
childLib2 = uniq('childlib2');
ensureProject();
runCLI(`generate @nrwl/react:app ${app}`);
setMaxWorkers(app);
runCLI(
`generate @nrwl/react:library ${parentLib} --publishable=true --no-interactive`
`generate @nrwl/react:library ${parentLib} --buildable --no-interactive`
);
runCLI(
`generate @nrwl/react:library ${childLib} --publishable=true --no-interactive`
`generate @nrwl/react:library ${childLib} --buildable --no-interactive`
);
runCLI(
`generate @nrwl/react:library ${childLib2} --publishable=true --no-interactive`
`generate @nrwl/react:library ${childLib2} --buildable --no-interactive`
);
// create dependencies by importing
@ -53,6 +59,20 @@ forEachCli('nx', cli => {
};
createDep(parentLib, [childLib, childLib2]);
updateFile(
`apps/${app}/src/main.tsx`,
`
import "@proj/${parentLib}";
`
);
// we are setting paths to {} to make sure built libs are read from dist
updateFile('tsconfig.json', c => {
const json = JSON.parse(c);
json.compilerOptions.paths = {};
return JSON.stringify(json, null, 2);
});
});
it('should throw an error if the dependent library has not been built before building the parent lib', () => {
@ -70,11 +90,11 @@ forEachCli('nx', cli => {
it('should build the library when it does not have any deps', () => {
const output = runCLI(`build ${childLib}`);
expect(output).toContain(`${childLib}.esm5.js`);
expect(output).toContain(`${childLib}.esm.js`);
expect(output).toContain(`Bundle complete`);
});
fit('should properly add references to any dependency into the parent package.json', () => {
it('should properly add references to any dependency into the parent package.json', () => {
const childLibOutput = runCLI(`build ${childLib}`);
const childLib2Output = runCLI(`build ${childLib2}`);
const parentLibOutput = runCLI(`build ${parentLib}`);
@ -97,6 +117,20 @@ forEachCli('nx', cli => {
[`@proj/${childLib2}`]: '0.0.1'
});
});
it('should build an app composed out of buildable libs', () => {
const buildWithDeps = runCLI(`build ${app} --with-deps`);
expect(buildWithDeps).toContain(`Running target "build" succeeded`);
checkFilesDoNotExist(`apps/${app}/tsconfig/tsconfig.nx-tmp`);
// we remove all path mappings from the root tsconfig, so when trying to build
// libs from source, the builder will throw
const failedBuild = runCLI(
`build ${app} --with-deps --buildLibsFromSource`,
{ silenceError: true }
);
expect(failedBuild).toContain(`Can't resolve`);
}, 1000000);
});
});

View File

@ -14,7 +14,7 @@ import {
DependentBuildableProjectNode,
updateBuildableProjectPackageJsonDependencies,
updatePaths
} from '@nrwl/workspace/src/utils/buildale-libs-utils';
} from '@nrwl/workspace/src/utils/buildable-libs-utils';
import { createProjectGraph } from '@nrwl/workspace/src/core/project-graph';
export interface BuildAngularLibraryBuilderOptions {

View File

@ -20,7 +20,8 @@
"publishable": {
"type": "boolean",
"default": false,
"description": "Generate a simple TS library when set to true."
"description": "Generate a buildable library.",
"alias": "buildable"
},
"prefix": {
"type": "string",

View File

@ -53,7 +53,8 @@
},
"publishable": {
"type": "boolean",
"description": "Create a publishable library. A \"build\" architect will be added for this project the workspace configuration."
"description": "Create a buildable library.",
"alias": "buildable"
},
"global": {
"type": "boolean",

View File

@ -1,3 +1,9 @@
{
"schematics": {}
"schematics": {
"set-build-libs-from-source": {
"version": "9.2.0-beta.1",
"description": "Set buildLibsFromSource property to true to not break existing projects.",
"factory": "./src/migrations/update-9-2-0/set-build-libs-from-source"
}
}
}

View File

@ -1,122 +0,0 @@
import { normalize, JsonObject, workspaces } from '@angular-devkit/core';
import { join } from 'path';
jest.mock('tsconfig-paths-webpack-plugin');
import TsConfigPathsPlugin from 'tsconfig-paths-webpack-plugin';
import { BuildNodeBuilderOptions } from './build.impl';
import { of } from 'rxjs';
import * as fs from 'fs';
import * as buildWebpack from '@angular-devkit/build-webpack';
import { Architect } from '@angular-devkit/architect';
import { getTestArchitect } from '../../utils/testing';
describe('NodeBuildBuilder', () => {
let testOptions: BuildNodeBuilderOptions & JsonObject;
let architect: Architect;
let runWebpack: jest.Mock;
beforeEach(async () => {
[architect] = await getTestArchitect();
testOptions = {
main: 'apps/nodeapp/src/main.ts',
tsConfig: 'apps/nodeapp/tsconfig.app.json',
outputPath: 'dist/apps/nodeapp',
externalDependencies: 'all',
fileReplacements: [
{
replace: 'apps/environment/environment.ts',
with: 'apps/environment/environment.prod.ts'
},
{
replace: 'module1.ts',
with: 'module2.ts'
}
],
assets: [],
statsJson: false
};
runWebpack = jest.fn().mockImplementation((config, context, options) => {
options.logging({
toJson: () => ({
stats: 'stats'
})
});
return of({ success: true });
});
(buildWebpack as any).runWebpack = runWebpack;
spyOn(workspaces, 'readWorkspace').and.returnValue({
workspace: {
projects: {
get: () => ({
sourceRoot: '/root/apps/nodeapp/src'
})
}
}
});
(<any>TsConfigPathsPlugin).mockImplementation(
function MockPathsPlugin() {}
);
});
describe('run', () => {
it('should call runWebpack', async () => {
const run = await architect.scheduleBuilder(
'@nrwl/node:build',
testOptions
);
await run.output.toPromise();
await run.stop();
expect(runWebpack).toHaveBeenCalled();
});
it('should emit the outfile along with success', async () => {
const run = await architect.scheduleBuilder(
'@nrwl/node:build',
testOptions
);
const output = await run.output.toPromise();
await run.stop();
expect(output.success).toEqual(true);
expect(output.outfile).toEqual('/root/dist/apps/nodeapp/main.js');
});
describe('webpackConfig option', () => {
it('should require the specified function and use the return value', async () => {
const mockFunction = jest.fn(config => ({
config: 'config'
}));
jest.mock(
join(normalize('/root'), 'apps/nodeapp/webpack.config.js'),
() => mockFunction,
{
virtual: true
}
);
testOptions.webpackConfig = 'apps/nodeapp/webpack.config.js';
const run = await architect.scheduleBuilder(
'@nrwl/node:build',
testOptions
);
await run.output.toPromise();
await run.stop();
expect(mockFunction).toHaveBeenCalled();
expect(runWebpack).toHaveBeenCalledWith(
{
config: 'config'
},
jasmine.anything(),
jasmine.anything()
);
// expect(runWebpack.calls.first().args[0]).toEqual({
// config: 'config'
// });
});
});
});
});

View File

@ -10,6 +10,11 @@ import { OUT_FILENAME } from '../../utils/config';
import { BuildBuilderOptions } from '../../utils/types';
import { normalizeBuildOptions } from '../../utils/normalize';
import { NodeJsSyncHost } from '@angular-devkit/core/node';
import { createProjectGraph } from '@nrwl/workspace/src/core/project-graph';
import {
calculateProjectDependencies,
createTmpTsConfig
} from '@nrwl/workspace/src/utils/buildable-libs-utils';
try {
require('dotenv').config();
@ -19,6 +24,7 @@ export interface BuildNodeBuilderOptions extends BuildBuilderOptions {
optimization?: boolean;
sourceMap?: boolean;
externalDependencies: 'all' | 'none' | string[];
buildLibsFromSource?: boolean;
}
export type NodeBuildEvent = BuildResult & {
@ -31,6 +37,20 @@ function run(
options: JsonObject & BuildNodeBuilderOptions,
context: BuilderContext
): Observable<NodeBuildEvent> {
if (!options.buildLibsFromSource) {
const projGraph = createProjectGraph();
const { target, dependencies } = calculateProjectDependencies(
projGraph,
context
);
options.tsConfig = createTmpTsConfig(
options.tsConfig,
context.workspaceRoot,
target.data.root,
dependencies
);
}
return from(getSourceRoot(context)).pipe(
map(sourceRoot =>
normalizeBuildOptions(options, context.workspaceRoot, sourceRoot)

View File

@ -108,6 +108,11 @@
"webpackConfig": {
"type": "string",
"description": "Path to a function which takes a webpack config, context and returns the resulting webpack config"
},
"buildLibsFromSource": {
"type": "boolean",
"description": "Read buildable libraries from source instead of building them separately.",
"default": false
}
},
"required": ["tsConfig", "main"],

View File

@ -1,18 +1,19 @@
import { Architect } from '@angular-devkit/architect';
import { EventEmitter } from 'events';
import { join } from 'path';
import { getMockContext, getTestArchitect } from '../../utils/testing';
import { getMockContext } from '../../utils/testing';
import { MockBuilderContext } from '@nrwl/workspace/testing';
import * as projectGraphUtils from '@nrwl/workspace/src/core/project-graph';
import {
ProjectGraph,
ProjectType
} from '@nrwl/workspace/src/core/project-graph';
import * as projectGraphUtils from '@nrwl/workspace/src/core/project-graph';
import {
NodePackageBuilderOptions,
runNodePackageBuilder
} from './package.impl';
import * as fsMock from 'fs';
jest.mock('glob');
let glob = require('glob');
jest.mock('fs-extra');
@ -23,7 +24,6 @@ jest.mock('child_process');
let { fork } = require('child_process');
jest.mock('tree-kill');
let treeKill = require('tree-kill');
import * as fsMock from 'fs';
describe('NodeCompileBuilder', () => {
let testOptions: NodePackageBuilderOptions;
@ -284,11 +284,7 @@ describe('NodeCompileBuilder', () => {
});
it('should call the tsc compiler with the modified tsconfig.json', done => {
let tmpTsConfigPath = join(
context.workspaceRoot,
'libs/nodelib',
'tsconfig.lib.nx-tmp'
);
let tmpTsConfigPath = join('libs/nodelib', 'tsconfig.nx-tmp');
runNodePackageBuilder(testOptions, context).subscribe({
complete: () => {
@ -308,9 +304,6 @@ describe('NodeCompileBuilder', () => {
}
});
fakeEventEmitter.emit('exit', 0);
// assert temp tsconfig file gets deleted again
expect(fsMock.unlinkSync).toHaveBeenCalledWith(tmpTsConfigPath);
});
});
});

View File

@ -11,21 +11,19 @@ import { copy, removeSync } from 'fs-extra';
import * as glob from 'glob';
import { basename, dirname, join, normalize, relative } from 'path';
import { Observable, of, Subscriber } from 'rxjs';
import { finalize, map, switchMap, tap } from 'rxjs/operators';
import { map, switchMap, tap } from 'rxjs/operators';
import * as treeKill from 'tree-kill';
import {
createProjectGraph,
ProjectGraph
} from '@nrwl/workspace/src/core/project-graph';
import * as ts from 'typescript';
import { unlinkSync } from 'fs';
import {
calculateProjectDependencies,
checkDependentProjectsHaveBeenBuilt,
createTmpTsConfig,
DependentBuildableProjectNode,
updateBuildableProjectPackageJsonDependencies,
updatePaths
} from '@nrwl/workspace/src/utils/buildale-libs-utils';
updateBuildableProjectPackageJsonDependencies
} from '@nrwl/workspace/src/utils/buildable-libs-utils';
export interface NodePackageBuilderOptions extends JsonObject {
main: string;
@ -181,30 +179,13 @@ function compileTypeScriptFiles(
return Observable.create((subscriber: Subscriber<BuilderOutput>) => {
if (projectDependencies.length > 0) {
// const parsedTSConfig = readTsConfig(tsConfigPath);
const parsedTSConfig = ts.readConfigFile(tsConfigPath, ts.sys.readFile)
.config;
// update TSConfig paths to point to the dist folder
parsedTSConfig.compilerOptions = parsedTSConfig.compilerOptions || {};
parsedTSConfig.compilerOptions.paths =
parsedTSConfig.compilerOptions.paths || {};
updatePaths(projectDependencies, parsedTSConfig.compilerOptions.paths);
// find the library root folder
const libRoot = projGraph.nodes[context.target.project].data.root;
// write the tmp tsconfig needed for building
const tmpTsConfigPath = join(
tsConfigPath = createTmpTsConfig(
tsConfigPath,
context.workspaceRoot,
libRoot,
'tsconfig.lib.nx-tmp'
projectDependencies
);
writeJsonFile(tmpTsConfigPath, parsedTSConfig);
// adjust the tsConfig path s.t. it points to the temporary one
// with the adjusted paths
tsConfigPath = tmpTsConfigPath;
}
try {
@ -248,17 +229,7 @@ function compileTypeScriptFiles(
new Error(`Could not compile Typescript files: \n ${error}`)
);
}
}).pipe(
finalize(() => {
cleanupTmpTsConfigFile(tsConfigPath);
})
);
}
function cleanupTmpTsConfigFile(tsConfigPath) {
if (tsConfigPath.indexOf('.nx-tmp') > -1) {
unlinkSync(tsConfigPath);
}
});
}
function killProcess(context: BuilderContext): void {

View File

@ -0,0 +1,47 @@
import { Tree } from '@angular-devkit/schematics';
import { SchematicTestRunner } from '@angular-devkit/schematics/testing';
import { readWorkspace } from '@nrwl/workspace';
import * as path from 'path';
describe('set buildLibsFromSource to true', () => {
let tree: Tree;
let schematicRunner: SchematicTestRunner;
beforeEach(async () => {
tree = Tree.empty();
schematicRunner = new SchematicTestRunner(
'@nrwl/web',
path.join(__dirname, '../../../migrations.json')
);
});
it(`should set buildLibsFromSource to true`, async () => {
tree.create(
'workspace.json',
JSON.stringify({
projects: {
demo: {
root: 'apps/demo',
sourceRoot: 'apps/demo/src',
architect: {
build: {
builder: '@nrwl/node:build',
options: {}
}
}
}
}
})
);
tree = await schematicRunner
.runSchematicAsync('set-build-libs-from-source', {}, tree)
.toPromise();
const config = readWorkspace(tree);
expect(config.projects.demo.architect.build.options).toEqual({
buildLibsFromSource: true
});
});
});

View File

@ -0,0 +1,23 @@
import { Rule } from '@angular-devkit/schematics';
import { updateWorkspaceInTree } from '@nrwl/workspace';
export default function update(): Rule {
return updateWorkspaceInTree(workspaceJson => {
Object.entries<any>(workspaceJson.projects).forEach(
([projectName, project]) => {
Object.entries<any>(project.architect).forEach(
([targetName, targetConfig]) => {
if (targetConfig.builder === '@nrwl/node:build') {
const architect =
workspaceJson.projects[projectName].architect[targetName];
if (architect && architect.options) {
architect.options.buildLibsFromSource = true;
}
}
}
);
}
);
return workspaceJson;
});
}

View File

@ -53,7 +53,8 @@
},
"publishable": {
"type": "boolean",
"description": "Create a publishable library. A \"build\" architect will be added for this project the workspace configuration."
"description": "Create a publishable library.",
"alias": "buildable"
},
"testEnvironment": {
"type": "string",

View File

@ -109,7 +109,8 @@
},
"publishable": {
"type": "boolean",
"description": "Create a publishable library. A \"build\" architect will be added for this project the workspace configuration."
"description": "Create a buildable library.",
"alias": "buildable"
},
"component": {
"type": "boolean",

View File

@ -9,6 +9,11 @@
"version": "9.0.0-beta.1",
"description": "Rename @nrwl/web:bundle => @nrwl/web:package",
"factory": "./src/migrations/update-9-0-0/update-builder-9-0-0"
},
"set-build-libs-from-source": {
"version": "9.2.0-beta.1",
"description": "Set buildLibsFromSource property to true to not break existing projects.",
"factory": "./src/migrations/update-9-2-0/set-build-libs-from-source"
}
}
}

View File

@ -1,142 +0,0 @@
import { join } from 'path';
import { workspaces } from '@angular-devkit/core';
import { of } from 'rxjs';
import * as buildWebpack from '@angular-devkit/build-webpack';
jest.mock('tsconfig-paths-webpack-plugin');
import TsConfigPathsPlugin from 'tsconfig-paths-webpack-plugin';
import { MockBuilderContext } from '@nrwl/workspace/testing';
import { run, WebBuildBuilderOptions } from './build.impl';
import * as webConfigUtils from '../../utils/web.config';
import { getMockContext } from '../../utils/testing';
import * as indexHtmlUtils from '../../utils/third-party/cli-files/utilities/index-file/write-index-html';
describe('WebBuildBuilder', () => {
let context: MockBuilderContext;
let testOptions: WebBuildBuilderOptions;
let runWebpack: jasmine.Spy;
let writeIndexHtml: jasmine.Spy;
beforeEach(async () => {
context = await getMockContext();
testOptions = {
index: 'apps/webapp/src/index.html',
budgets: [],
baseHref: '/',
optimization: true,
deployUrl: '/',
scripts: ['apps/webapp/src/scripts.js'],
styles: ['apps/webapp/src/styles.css'],
main: 'apps/webapp/src/main.ts',
tsConfig: 'apps/webapp/tsconfig.app.json',
outputPath: 'dist/apps/webapp',
fileReplacements: [
{
replace: 'apps/webapp/environment/environment.ts',
with: 'apps/webapp/environment/environment.prod.ts'
},
{
replace: 'module1.ts',
with: 'module2.ts'
}
],
assets: [],
statsJson: false
};
const stats = {
stats: 'stats'
};
runWebpack = spyOn(buildWebpack, 'runWebpack').and.callFake(
(config, context, opts) => {
opts.logging({
toJson: () => stats,
toString: () => JSON.stringify(stats)
});
return of({
success: true,
emittedFiles: [
{
file: 'scripts.js',
extension: '.js'
},
{
file: 'styles.css',
extension: '.css'
}
]
});
}
);
spyOn(workspaces, 'readWorkspace').and.returnValue({
workspace: {
projects: {
get: () => ({
sourceRoot: join(__dirname, '../../..')
})
}
}
});
spyOn(webConfigUtils, 'getWebConfig').and.returnValue({
config: 'config'
});
writeIndexHtml = spyOn(indexHtmlUtils, 'writeIndexHtml').and.returnValue(
of(null)
);
(<any>TsConfigPathsPlugin).mockImplementation(
function MockPathsPlugin() {}
);
});
describe('run', () => {
it('should call runWebpack', async () => {
await run(testOptions, context).toPromise();
expect(runWebpack).toHaveBeenCalled();
});
it('should emit success', async () => {
const buildEvent = await run(testOptions, context).toPromise();
expect(buildEvent.success).toEqual(true);
});
it('should write the HTML', async () => {
await run(testOptions, context).toPromise();
expect(writeIndexHtml).toHaveBeenCalled();
});
describe('differential loading', () => {
it('should call runWebpack twice', async () => {
await run(testOptions, context).toPromise();
expect(runWebpack).toHaveBeenCalledTimes(2);
});
});
describe('webpackConfig option', () => {
it('should require the specified function and use the return value', async () => {
const mockFunction = jest.fn(config => ({
config: 'config'
}));
jest.mock('/root/apps/webapp/webpack.config.js', () => mockFunction, {
virtual: true
});
await run(
{
...testOptions,
webpackConfig: './apps/webapp/webpack.config.js'
},
context
).toPromise();
expect(mockFunction).toHaveBeenCalled();
expect(runWebpack.calls.first().args[0]).toEqual({
config: 'config'
});
});
});
});
});

View File

@ -5,7 +5,7 @@ import {
normalize
} from '@angular-devkit/core';
import { BuildResult, runWebpack } from '@angular-devkit/build-webpack';
import { from, Observable, of } from 'rxjs';
import { from, of } from 'rxjs';
import { normalizeWebBuildOptions } from '../../utils/normalize';
import { getWebConfig } from '../../utils/web.config';
import { BuildBuilderOptions } from '../../utils/types';
@ -16,6 +16,11 @@ import { NodeJsSyncHost } from '@angular-devkit/core/node';
import { execSync } from 'child_process';
import { Range, satisfies } from 'semver';
import { basename } from 'path';
import { createProjectGraph } from '@nrwl/workspace/src/core/project-graph';
import {
calculateProjectDependencies,
createTmpTsConfig
} from '@nrwl/workspace/src/utils/buildable-libs-utils';
const IGNORED_WEBPACK_OUTPUT = [/WARNING in The comment file/i];
@ -38,6 +43,7 @@ export interface WebBuildBuilderOptions extends BuildBuilderOptions {
subresourceIntegrity?: boolean;
verbose?: boolean;
buildLibsFromSource?: boolean;
}
export default createBuilder<WebBuildBuilderOptions & JsonObject>(run);
@ -62,6 +68,21 @@ export function run(options: WebBuildBuilderOptions, context: BuilderContext) {
`Node version ${nodeVersion} is not supported. Supported range is "${supportedRange.raw}".`
);
}
if (!options.buildLibsFromSource) {
const projGraph = createProjectGraph();
const { target, dependencies } = calculateProjectDependencies(
projGraph,
context
);
options.tsConfig = createTmpTsConfig(
options.tsConfig,
context.workspaceRoot,
target.data.root,
dependencies
);
}
return from(getSourceRoot(context, host))
.pipe(
map(sourceRoot => {

View File

@ -230,6 +230,11 @@
"webpackConfig": {
"type": "string",
"description": "Path to a function which takes a webpack config, some context and returns the resulting webpack config"
},
"buildLibsFromSource": {
"type": "boolean",
"description": "Read buildable libraries from source instead of building them separately.",
"default": false
}
},
"required": ["tsConfig", "main", "index"],

View File

@ -5,8 +5,8 @@ import {
createBuilder
} from '@angular-devkit/architect';
import { JsonObject } from '@angular-devkit/core';
import { from, Observable, of } from 'rxjs';
import { switchMap, tap, last, mergeMap, catchError } from 'rxjs/operators';
import { Observable, of } from 'rxjs';
import { catchError, last, switchMap, tap } from 'rxjs/operators';
import { runRollup } from './run-rollup';
import { createBabelConfig as _createBabelConfig } from '../../utils/babel-config';
import * as autoprefixer from 'autoprefixer';
@ -27,18 +27,14 @@ import {
readJsonFile,
writeJsonFile
} from '@nrwl/workspace/src/utils/fileutils';
import {
createProjectGraph,
ProjectGraphNode
} from '@nrwl/workspace/src/core/project-graph';
import { createProjectGraph } from '@nrwl/workspace/src/core/project-graph';
import {
calculateProjectDependencies,
checkDependentProjectsHaveBeenBuilt,
DependentBuildableProjectNode,
updateBuildableProjectPackageJsonDependencies,
updatePaths
} from '@nrwl/workspace/src/utils/buildale-libs-utils';
import * as ts from 'typescript';
readTsConfigWithRemappedPaths,
updateBuildableProjectPackageJsonDependencies
} from '@nrwl/workspace/src/utils/buildable-libs-utils';
// These use require because the ES import isn't correct.
const resolve = require('@rollup/plugin-node-resolve');
@ -151,12 +147,10 @@ function createRollupOptions(
context: BuilderContext,
packageJson: any
): rollup.InputOptions {
const parsedTSConfig = ts.readConfigFile(options.tsConfig, ts.sys.readFile)
.config;
parsedTSConfig.compilerOptions = parsedTSConfig.compilerOptions || {};
parsedTSConfig.compilerOptions.paths =
parsedTSConfig.compilerOptions.paths || {};
updatePaths(dependencies, parsedTSConfig.compilerOptions.paths);
const parsedTSConfig = readTsConfigWithRemappedPaths(
options.tsConfig,
dependencies
);
const plugins = [
image(),

View File

@ -1,17 +1,10 @@
import { Tree } from '@angular-devkit/schematics';
import { SchematicTestRunner } from '@angular-devkit/schematics/testing';
import {
updateJsonInTree,
readJsonInTree,
updateWorkspaceInTree,
readWorkspace,
getWorkspacePath
} from '@nrwl/workspace';
import { readWorkspace } from '@nrwl/workspace';
import * as path from 'path';
import { stripIndents } from '@angular-devkit/core/src/utils/literals';
describe('Update 8-5-0', () => {
describe('set buildLibsFromSource to true', () => {
let tree: Tree;
let schematicRunner: SchematicTestRunner;
@ -23,7 +16,7 @@ describe('Update 8-5-0', () => {
);
});
it(`should remove differentialLoading as an option for build builder`, async () => {
it(`should set buildLibsFromSource to true`, async () => {
tree.create(
'workspace.json',
JSON.stringify({
@ -34,9 +27,7 @@ describe('Update 8-5-0', () => {
architect: {
build: {
builder: '@nrwl/web:build',
options: {
differentialLoading: true
}
options: {}
}
}
}
@ -45,10 +36,12 @@ describe('Update 8-5-0', () => {
);
tree = await schematicRunner
.runSchematicAsync('update-builder-8.5.0', {}, tree)
.runSchematicAsync('set-build-libs-from-source', {}, tree)
.toPromise();
const config = readWorkspace(tree);
expect(config.projects.demo.architect.build.options).toEqual({});
expect(config.projects.demo.architect.build.options).toEqual({
buildLibsFromSource: true
});
});
});

View File

@ -0,0 +1,23 @@
import { Rule } from '@angular-devkit/schematics';
import { updateWorkspaceInTree } from '@nrwl/workspace';
export default function update(): Rule {
return updateWorkspaceInTree(workspaceJson => {
Object.entries<any>(workspaceJson.projects).forEach(
([projectName, project]) => {
Object.entries<any>(project.architect).forEach(
([targetName, targetConfig]) => {
if (targetConfig.builder === '@nrwl/web:build') {
const architect =
workspaceJson.projects[projectName].architect[targetName];
if (architect && architect.options) {
architect.options.buildLibsFromSource = true;
}
}
}
);
}
);
return workspaceJson;
});
}

View File

@ -4,7 +4,7 @@ import {
ProjectType
} from '../core/project-graph';
import { BuilderContext } from '@angular-devkit/architect';
import { join } from 'path';
import { join, resolve, dirname } from 'path';
import {
fileExists,
readJsonFile,
@ -12,6 +12,8 @@ import {
} from '@nrwl/workspace/src/utils/fileutils';
import { stripIndents } from '@angular-devkit/core/src/utils/literals';
import { getOutputsForTargetAndConfiguration } from '@nrwl/workspace/src/tasks-runner/utils';
import * as ts from 'typescript';
import { unlinkSync } from 'fs';
function isBuildable(target: string, node: ProjectGraphNode): boolean {
return (
@ -33,10 +35,13 @@ export function calculateProjectDependencies(
): { target: ProjectGraphNode; dependencies: DependentBuildableProjectNode[] } {
const target = projGraph.nodes[context.target.project];
// gather the library dependencies
const dependencies = (projGraph.dependencies[context.target.project] || [])
.map(dependency => {
const depNode = projGraph.nodes[dependency.target];
const dependencies = recursivelyCollectDependencies(
context.target.project,
projGraph,
[]
)
.map(dep => {
const depNode = projGraph.nodes[dep];
if (
depNode.type === ProjectType.lib &&
isBuildable(context.target.target, depNode)
@ -62,6 +67,83 @@ export function calculateProjectDependencies(
return { target, dependencies };
}
function recursivelyCollectDependencies(
project: string,
projGraph: ProjectGraph,
acc: string[]
) {
(projGraph.dependencies[project] || []).forEach(dependency => {
if (acc.indexOf(dependency.target) === -1) {
acc.push(dependency.target);
recursivelyCollectDependencies(dependency.target, projGraph, acc);
}
});
return acc;
}
export function readTsConfigWithRemappedPaths(
tsConfig: string,
dependencies: DependentBuildableProjectNode[]
) {
const parsedTSConfig = ts.readConfigFile(tsConfig, ts.sys.readFile).config;
parsedTSConfig.compilerOptions = parsedTSConfig.compilerOptions || {};
parsedTSConfig.compilerOptions.paths = readPaths(tsConfig) || {};
updatePaths(dependencies, parsedTSConfig.compilerOptions.paths);
return parsedTSConfig;
}
function readPaths(tsConfig: string) {
try {
const parsedTSConfig = ts.readConfigFile(tsConfig, ts.sys.readFile).config;
if (
parsedTSConfig.compilerOptions &&
parsedTSConfig.compilerOptions.paths
) {
return parsedTSConfig.compilerOptions.paths;
} else if (parsedTSConfig.extends) {
return readPaths(resolve(dirname(tsConfig), parsedTSConfig.extends));
} else {
return null;
}
} catch (e) {
return null;
}
}
export function createTmpTsConfig(
tsconfigPath: string,
workspaceRoot: string,
projectRoot: string,
dependencies: DependentBuildableProjectNode[]
) {
const parsedTSConfig = readTsConfigWithRemappedPaths(
tsconfigPath,
dependencies
);
const tmpTsConfigPath = join(workspaceRoot, projectRoot, 'tsconfig.nx-tmp');
process.on('exit', () => {
cleanupTmpTsConfigFile(tmpTsConfigPath);
});
process.on('SIGTERM', () => {
cleanupTmpTsConfigFile(tmpTsConfigPath);
process.exit(0);
});
process.on('SIGINT', () => {
cleanupTmpTsConfigFile(tmpTsConfigPath);
process.exit(0);
});
writeJsonFile(tmpTsConfigPath, parsedTSConfig);
return join(projectRoot, 'tsconfig.nx-tmp');
}
function cleanupTmpTsConfigFile(tmpTsConfigPath) {
try {
if (tmpTsConfigPath) {
unlinkSync(tmpTsConfigPath);
}
} catch (e) {}
}
export function checkDependentProjectsHaveBeenBuilt(
context: BuilderContext,
projectDependencies: DependentBuildableProjectNode[]
@ -86,9 +168,7 @@ export function checkDependentProjectsHaveBeenBuilt(
}'s dependencies have not been built yet. Please build these libraries before:
${depLibsToBuildFirst.map(x => ` - ${x.node.name}`).join('\n')}
Try: nx run-many --target ${context.target.target} --projects ${
context.target.project
},...
Try: nx run ${context.target.project}:${context.target.target} --with-deps
`);
return false;