Compare commits
44 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| cfb9718a12 | |||
| 17d78c0516 | |||
| 4f22832fff | |||
| 464d13e5dc | |||
| a0d501b0db | |||
| 0351ade509 | |||
| 00de5d8235 | |||
| fc1ba0adb5 | |||
| 1804b55da1 | |||
| aa6ea72b83 | |||
| 5abd0d11a1 | |||
| c16c1614b5 | |||
| 8f62e8d37e | |||
| 237d508e52 | |||
| 61fc7218ff | |||
| 34847aac0e | |||
| be42a100c1 | |||
| 5f637f8b50 | |||
| b170e8184c | |||
| 291a918a26 | |||
| 1d8f901c71 | |||
| 74a61e7b5c | |||
| fdd90990a3 | |||
| 34e6c1498f | |||
| b0ada64d19 | |||
| c8eb86d9ce | |||
| 3f18eaba90 | |||
| fa9c790b9d | |||
| 93fab2644f | |||
| d074f45ed3 | |||
| 5927a5b7f8 | |||
| 9699214ef7 | |||
| 5109068f9f | |||
| 020d2e1088 | |||
| 12ac1bc696 | |||
| 7e9587f384 | |||
| 496534d7e5 | |||
| 1777f5bc79 | |||
| d021876f06 | |||
| 6ee1dda6cd | |||
| d7f1a03f88 | |||
| 353a9d9f14 | |||
| 2871bd10a2 | |||
| 9a2341a8c1 |
@@ -64,6 +64,12 @@ Type: `boolean`
|
||||
|
||||
Whether or not to open the Cypress application to run the tests. If set to 'true', will run in headless mode
|
||||
|
||||
### ignoreTestFiles
|
||||
|
||||
Type: `string`
|
||||
|
||||
A String or Array of glob patterns used to ignore test files that would otherwise be shown in your list of tests. Cypress uses minimatch with the options: {dot: true, matchBase: true}. We suggest using https://globster.xyz to test what files would match.
|
||||
|
||||
### key
|
||||
|
||||
Type: `string`
|
||||
@@ -86,6 +92,18 @@ Type: `boolean`
|
||||
|
||||
Whether or not Cypress should record the results of the tests
|
||||
|
||||
### reporter
|
||||
|
||||
Type: `string`
|
||||
|
||||
The reporter used during cypress run
|
||||
|
||||
### reporterOptions
|
||||
|
||||
Type: `string`
|
||||
|
||||
The reporter options used. Supported options depend on the reporter.
|
||||
|
||||
### spec
|
||||
|
||||
Type: `string`
|
||||
|
||||
@@ -6,6 +6,14 @@ Builder properties can be configured in angular.json when defining the builder,
|
||||
|
||||
## Properties
|
||||
|
||||
### docsMode
|
||||
|
||||
Default: `false`
|
||||
|
||||
Type: `boolean`
|
||||
|
||||
Build a documentation-only site using addon-docs.
|
||||
|
||||
### host
|
||||
|
||||
Default: `localhost`
|
||||
|
||||
@@ -65,6 +65,12 @@ Type: `boolean`
|
||||
|
||||
Whether or not to open the Cypress application to run the tests. If set to 'true', will run in headless mode
|
||||
|
||||
### ignoreTestFiles
|
||||
|
||||
Type: `string`
|
||||
|
||||
A String or Array of glob patterns used to ignore test files that would otherwise be shown in your list of tests. Cypress uses minimatch with the options: {dot: true, matchBase: true}. We suggest using https://globster.xyz to test what files would match.
|
||||
|
||||
### key
|
||||
|
||||
Type: `string`
|
||||
@@ -87,6 +93,18 @@ Type: `boolean`
|
||||
|
||||
Whether or not Cypress should record the results of the tests
|
||||
|
||||
### reporter
|
||||
|
||||
Type: `string`
|
||||
|
||||
The reporter used during cypress run
|
||||
|
||||
### reporterOptions
|
||||
|
||||
Type: `string`
|
||||
|
||||
The reporter options used. Supported options depend on the reporter.
|
||||
|
||||
### spec
|
||||
|
||||
Type: `string`
|
||||
|
||||
@@ -7,6 +7,14 @@ Read more about how to use builders and the CLI here: https://nx.dev/react/guide
|
||||
|
||||
## Properties
|
||||
|
||||
### docsMode
|
||||
|
||||
Default: `false`
|
||||
|
||||
Type: `boolean`
|
||||
|
||||
Build a documentation-only site using addon-docs.
|
||||
|
||||
### host
|
||||
|
||||
Default: `localhost`
|
||||
|
||||
@@ -15,6 +15,8 @@ import {
|
||||
uniq,
|
||||
updateFile,
|
||||
workspaceConfigName,
|
||||
setCurrentProjName,
|
||||
runCreateWorkspace,
|
||||
} from '@nrwl/e2e/utils';
|
||||
|
||||
forEachCli((cli) => {
|
||||
@@ -666,6 +668,147 @@ forEachCli((cli) => {
|
||||
`import { fromLibOne } from '@proj/shared/${lib1}/data-access';`
|
||||
);
|
||||
});
|
||||
|
||||
it('should work for custom workspace layouts', () => {
|
||||
const lib1 = uniq('mylib');
|
||||
const lib2 = uniq('mylib');
|
||||
const lib3 = uniq('mylib');
|
||||
newProject();
|
||||
|
||||
let nxJson = readJson('nx.json');
|
||||
nxJson.workspaceLayout = { libsDir: 'packages' };
|
||||
updateFile('nx.json', JSON.stringify(nxJson));
|
||||
|
||||
runCLI(`generate @nrwl/workspace:lib ${lib1}/data-access`);
|
||||
|
||||
updateFile(
|
||||
`packages/${lib1}/data-access/src/lib/${lib1}-data-access.ts`,
|
||||
`export function fromLibOne() { console.log('This is completely pointless'); }`
|
||||
);
|
||||
|
||||
updateFile(
|
||||
`packages/${lib1}/data-access/src/index.ts`,
|
||||
`export * from './lib/${lib1}-data-access.ts'`
|
||||
);
|
||||
|
||||
/**
|
||||
* Create a library which imports a class from lib1
|
||||
*/
|
||||
|
||||
runCLI(`generate @nrwl/workspace:lib ${lib2}/ui`);
|
||||
|
||||
updateFile(
|
||||
`packages/${lib2}/ui/src/lib/${lib2}-ui.ts`,
|
||||
`import { fromLibOne } from '@proj/${lib1}/data-access';
|
||||
|
||||
export const fromLibTwo = () => fromLibOne(); }`
|
||||
);
|
||||
|
||||
/**
|
||||
* Create a library which has an implicit dependency on lib1
|
||||
*/
|
||||
|
||||
runCLI(`generate @nrwl/workspace:lib ${lib3}`);
|
||||
nxJson = JSON.parse(readFile('nx.json')) as NxJson;
|
||||
nxJson.projects[lib3].implicitDependencies = [`${lib1}-data-access`];
|
||||
updateFile(`nx.json`, JSON.stringify(nxJson));
|
||||
|
||||
/**
|
||||
* Now try to move lib1
|
||||
*/
|
||||
|
||||
const moveOutput = runCLI(
|
||||
`generate @nrwl/workspace:move --project ${lib1}-data-access shared/${lib1}/data-access`
|
||||
);
|
||||
|
||||
expect(moveOutput).toContain(`DELETE packages/${lib1}/data-access`);
|
||||
expect(exists(`packages/${lib1}/data-access`)).toBeFalsy();
|
||||
|
||||
const newPath = `packages/shared/${lib1}/data-access`;
|
||||
const newName = `shared-${lib1}-data-access`;
|
||||
|
||||
const readmePath = `${newPath}/README.md`;
|
||||
expect(moveOutput).toContain(`CREATE ${readmePath}`);
|
||||
checkFilesExist(readmePath);
|
||||
|
||||
const jestConfigPath = `${newPath}/jest.config.js`;
|
||||
expect(moveOutput).toContain(`CREATE ${jestConfigPath}`);
|
||||
checkFilesExist(jestConfigPath);
|
||||
const jestConfig = readFile(jestConfigPath);
|
||||
expect(jestConfig).toContain(`name: 'shared-${lib1}-data-access'`);
|
||||
expect(jestConfig).toContain(`preset: '../../../../jest.config.js'`);
|
||||
expect(jestConfig).toContain(
|
||||
`coverageDirectory: '../../../../coverage/${newPath}'`
|
||||
);
|
||||
|
||||
const tsConfigPath = `${newPath}/tsconfig.json`;
|
||||
expect(moveOutput).toContain(`CREATE ${tsConfigPath}`);
|
||||
checkFilesExist(tsConfigPath);
|
||||
|
||||
const tsConfigLibPath = `${newPath}/tsconfig.lib.json`;
|
||||
expect(moveOutput).toContain(`CREATE ${tsConfigLibPath}`);
|
||||
checkFilesExist(tsConfigLibPath);
|
||||
const tsConfigLib = readJson(tsConfigLibPath);
|
||||
expect(tsConfigLib.compilerOptions.outDir).toEqual(
|
||||
'../../../../dist/out-tsc'
|
||||
);
|
||||
|
||||
const tsConfigSpecPath = `${newPath}/tsconfig.spec.json`;
|
||||
expect(moveOutput).toContain(`CREATE ${tsConfigSpecPath}`);
|
||||
checkFilesExist(tsConfigSpecPath);
|
||||
const tsConfigSpec = readJson(tsConfigSpecPath);
|
||||
expect(tsConfigSpec.compilerOptions.outDir).toEqual(
|
||||
'../../../../dist/out-tsc'
|
||||
);
|
||||
|
||||
const indexPath = `${newPath}/src/index.ts`;
|
||||
expect(moveOutput).toContain(`CREATE ${indexPath}`);
|
||||
checkFilesExist(indexPath);
|
||||
|
||||
const rootClassPath = `${newPath}/src/lib/${lib1}-data-access.ts`;
|
||||
expect(moveOutput).toContain(`CREATE ${rootClassPath}`);
|
||||
checkFilesExist(rootClassPath);
|
||||
|
||||
expect(moveOutput).toContain('UPDATE nx.json');
|
||||
nxJson = JSON.parse(readFile('nx.json')) as NxJson;
|
||||
expect(nxJson.projects[`${lib1}-data-access`]).toBeUndefined();
|
||||
expect(nxJson.projects[newName]).toEqual({
|
||||
tags: [],
|
||||
});
|
||||
expect(nxJson.projects[lib3].implicitDependencies).toEqual([
|
||||
`shared-${lib1}-data-access`,
|
||||
]);
|
||||
|
||||
expect(moveOutput).toContain('UPDATE tsconfig.json');
|
||||
const rootTsConfig = readJson('tsconfig.json');
|
||||
expect(
|
||||
rootTsConfig.compilerOptions.paths[`@proj/${lib1}/data-access`]
|
||||
).toBeUndefined();
|
||||
expect(
|
||||
rootTsConfig.compilerOptions.paths[`@proj/shared/${lib1}/data-access`]
|
||||
).toEqual([`packages/shared/${lib1}/data-access/src/index.ts`]);
|
||||
|
||||
expect(moveOutput).toContain(`UPDATE ${workspace}.json`);
|
||||
const workspaceJson = readJson(`${workspace}.json`);
|
||||
expect(workspaceJson.projects[`${lib1}-data-access`]).toBeUndefined();
|
||||
const project = workspaceJson.projects[newName];
|
||||
expect(project).toBeTruthy();
|
||||
expect(project.root).toBe(newPath);
|
||||
expect(project.sourceRoot).toBe(`${newPath}/src`);
|
||||
expect(project.architect.lint.options.tsConfig).toEqual([
|
||||
`packages/shared/${lib1}/data-access/tsconfig.lib.json`,
|
||||
`packages/shared/${lib1}/data-access/tsconfig.spec.json`,
|
||||
]);
|
||||
|
||||
/**
|
||||
* Check that the import in lib2 has been updated
|
||||
*/
|
||||
const lib2FilePath = `packages/${lib2}/ui/src/lib/${lib2}-ui.ts`;
|
||||
const lib2File = readFile(lib2FilePath);
|
||||
expect(lib2File).toContain(
|
||||
`import { fromLibOne } from '@proj/shared/${lib1}/data-access';`
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Remove Project', () => {
|
||||
|
||||
@@ -165,7 +165,7 @@ forEachCli((cliName) => {
|
||||
expect(failedTests).toContain(`- ${myapp}`);
|
||||
expect(failedTests).toContain(`- ${myapp2}`);
|
||||
expect(failedTests).toContain(`Failed projects:`);
|
||||
expect(readJson('dist/.nx-results')).toEqual({
|
||||
expect(readJson('node_modules/.cache/nx/results.json')).toEqual({
|
||||
command: 'test',
|
||||
results: {
|
||||
[myapp]: false,
|
||||
@@ -318,7 +318,7 @@ forEachCli((cliName) => {
|
||||
expect(failedTests).toContain(
|
||||
'You can isolate the above projects by passing: --only-failed'
|
||||
);
|
||||
expect(readJson('dist/.nx-results')).toEqual({
|
||||
expect(readJson('node_modules/.cache/nx/results.json')).toEqual({
|
||||
command: 'test',
|
||||
results: {
|
||||
[myapp]: false,
|
||||
@@ -497,16 +497,6 @@ forEachCli((cliName) => {
|
||||
)
|
||||
);
|
||||
expect(resWithDeps.tasks).toEqual([
|
||||
{
|
||||
id: `${mypublishablelib}:build`,
|
||||
overrides: {},
|
||||
target: {
|
||||
project: mypublishablelib,
|
||||
target: 'build',
|
||||
},
|
||||
command: `npm run ${cliCommand} -- build ${mypublishablelib}`,
|
||||
outputs: [`dist/libs/${mypublishablelib}`],
|
||||
},
|
||||
{
|
||||
id: `${myapp}:build`,
|
||||
overrides: {},
|
||||
@@ -517,6 +507,16 @@ forEachCli((cliName) => {
|
||||
command: `npm run ${cliCommand} -- build ${myapp}`,
|
||||
outputs: [`dist/apps/${myapp}`],
|
||||
},
|
||||
{
|
||||
id: `${mypublishablelib}:build`,
|
||||
overrides: {},
|
||||
target: {
|
||||
project: mypublishablelib,
|
||||
target: 'build',
|
||||
},
|
||||
command: `npm run ${cliCommand} -- build ${mypublishablelib}`,
|
||||
outputs: [`dist/libs/${mypublishablelib}`],
|
||||
},
|
||||
]);
|
||||
compareTwoArrays(resWithDeps.projects, [
|
||||
mylib,
|
||||
|
||||
+3
-3
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@nrwl/nx-source",
|
||||
"version": "9.5.1",
|
||||
"version": "9.6.0",
|
||||
"description": "Extensible Dev Tools for Monorepos",
|
||||
"homepage": "https://nx.dev",
|
||||
"main": "index.js",
|
||||
@@ -78,6 +78,7 @@
|
||||
"@storybook/react": "5.3.9",
|
||||
"@svgr/webpack": "^5.2.0",
|
||||
"@testing-library/react": "9.4.0",
|
||||
"@types/copy-webpack-plugin": "6.0.0",
|
||||
"@types/eslint": "^6.1.8",
|
||||
"@types/express": "4.17.0",
|
||||
"@types/fast-levenshtein": "^0.0.1",
|
||||
@@ -120,7 +121,7 @@
|
||||
"commitizen": "^4.0.3",
|
||||
"confusing-browser-globals": "^1.0.9",
|
||||
"conventional-changelog-cli": "^2.0.23",
|
||||
"copy-webpack-plugin": "5.1.1",
|
||||
"copy-webpack-plugin": "6.0.3",
|
||||
"core-js": "^3.6.5",
|
||||
"cosmiconfig": "^4.0.0",
|
||||
"css-loader": "3.4.2",
|
||||
@@ -142,7 +143,6 @@
|
||||
"fork-ts-checker-webpack-plugin": "^3.1.1",
|
||||
"fs-extra": "7.0.1",
|
||||
"glob": "7.1.4",
|
||||
"hasha": "5.1.0",
|
||||
"html-webpack-plugin": "^3.2.0",
|
||||
"husky": "^3.0.3",
|
||||
"identity-obj-proxy": "3.0.0",
|
||||
|
||||
@@ -11,7 +11,8 @@
|
||||
"$source": "argv",
|
||||
"index": 0
|
||||
},
|
||||
"x-prompt": "What name would you like to use for the application?"
|
||||
"x-prompt": "What name would you like to use for the application?",
|
||||
"pattern": "^[a-zA-Z]{1}.*$"
|
||||
},
|
||||
"directory": {
|
||||
"description": "The directory of the new application.",
|
||||
|
||||
@@ -11,7 +11,8 @@
|
||||
"$source": "argv",
|
||||
"index": 0
|
||||
},
|
||||
"x-prompt": "What name would you like to use for the library?"
|
||||
"x-prompt": "What name would you like to use for the library?",
|
||||
"pattern": "^[a-zA-Z]{1}.*$"
|
||||
},
|
||||
"directory": {
|
||||
"type": "string",
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import chalk from 'chalk';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import { findWorkspaceRoot } from './find-workspace-root';
|
||||
import { output } from './output';
|
||||
@@ -11,14 +12,34 @@ export function initGlobal() {
|
||||
|
||||
if (workspace) {
|
||||
// Found a workspace root - hand off to the local copy of Nx
|
||||
require(path.join(
|
||||
const localNx = path.join(
|
||||
workspace.dir,
|
||||
'node_modules',
|
||||
'@nrwl',
|
||||
'cli',
|
||||
'bin',
|
||||
'nx.js'
|
||||
));
|
||||
);
|
||||
if (fs.existsSync(localNx)) {
|
||||
require(localNx);
|
||||
} else {
|
||||
if (fs.existsSync(path.join(workspace.dir, 'node_modules'))) {
|
||||
output.error({
|
||||
title: `Could not find Nx in this workspace.`,
|
||||
bodyLines: [
|
||||
`To convert an Angular workspace to Nx run: ${chalk.bold.white(
|
||||
`ng add @nrwl/workspace`
|
||||
)}`,
|
||||
],
|
||||
});
|
||||
} else {
|
||||
output.error({
|
||||
title: `Could not find a node_modules folder in this workspace.`,
|
||||
bodyLines: [`Have you run ${chalk.bold.white(`npm/yarn install`)}?`],
|
||||
});
|
||||
}
|
||||
process.exit(1);
|
||||
}
|
||||
} else {
|
||||
output.log({
|
||||
title: `The current directory isn't part of an Nx workspace.`,
|
||||
@@ -31,6 +52,6 @@ export function initGlobal() {
|
||||
output.note({
|
||||
title: `For more information please visit https://nx.dev/`,
|
||||
});
|
||||
process.exit(0);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import { parseRunOneOptions } from './parse-run-one-options';
|
||||
*/
|
||||
process.env.NX_CLI_SET = 'true';
|
||||
export function initLocal(workspace: Workspace) {
|
||||
require('@nrwl/workspace/' + 'src/utils/perf-logging');
|
||||
const supportedNxCommands = require('@nrwl/workspace/' +
|
||||
'src/command-line/supported-nx-commands').supportedNxCommands;
|
||||
const runOpts = runOneOptions(workspace);
|
||||
|
||||
@@ -14,6 +14,40 @@ describe('parseRunOneOptions', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('should work with --prod', () => {
|
||||
expect(
|
||||
parseRunOneOptions(nxJson, workspaceJson, [
|
||||
'build',
|
||||
'myproj',
|
||||
'--prod',
|
||||
'--flag=true',
|
||||
])
|
||||
).toEqual({
|
||||
project: 'myproj',
|
||||
target: 'build',
|
||||
configuration: 'production',
|
||||
parsedArgs: { _: [], flag: 'true' },
|
||||
});
|
||||
});
|
||||
|
||||
it('should override --prod with --configuration', () => {
|
||||
expect(
|
||||
parseRunOneOptions(nxJson, workspaceJson, [
|
||||
'build',
|
||||
'myproj',
|
||||
'--prod',
|
||||
'--configuration',
|
||||
'dev',
|
||||
'--flag=true',
|
||||
])
|
||||
).toEqual({
|
||||
project: 'myproj',
|
||||
target: 'build',
|
||||
configuration: 'dev',
|
||||
parsedArgs: { _: [], flag: 'true' },
|
||||
});
|
||||
});
|
||||
|
||||
it('should work with run syntax', () => {
|
||||
expect(
|
||||
parseRunOneOptions(nxJson, workspaceJson, [
|
||||
|
||||
@@ -43,8 +43,7 @@ export function parseRunOneOptions(
|
||||
|
||||
if (parsedArgs.configuration) {
|
||||
configuration = parsedArgs.configuration;
|
||||
}
|
||||
if (parsedArgs.prod) {
|
||||
} else if (parsedArgs.prod) {
|
||||
configuration = 'production';
|
||||
}
|
||||
if (parsedArgs.project) {
|
||||
|
||||
@@ -95,11 +95,7 @@ function setUpOutputWatching(captureStderr: boolean, forwardOutput: boolean) {
|
||||
}
|
||||
});
|
||||
|
||||
process.on('SIGINT', () => {
|
||||
writeToDisk(forwardOutput, outWithErr);
|
||||
});
|
||||
|
||||
process.on('SIGTERM', () => {
|
||||
writeToDisk(forwardOutput, outWithErr);
|
||||
process.exit(15);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -404,26 +404,25 @@ function createApp(
|
||||
interactive: boolean,
|
||||
defaultBase: string
|
||||
) {
|
||||
// creating the app itself
|
||||
const args = [
|
||||
name,
|
||||
...process.argv
|
||||
.slice(parsedArgs._[2] ? 3 : 2)
|
||||
.filter(
|
||||
(a) =>
|
||||
!a.startsWith('--cli') &&
|
||||
!a.startsWith('--preset') &&
|
||||
!a.startsWith('--appName') &&
|
||||
!a.startsWith('--app-name') &&
|
||||
!a.startsWith('--style') &&
|
||||
!a.startsWith('--nxCloud') &&
|
||||
!a.startsWith('--nx-cloud') &&
|
||||
!a.startsWith('--interactive') &&
|
||||
!a.startsWith('--defaultBase') &&
|
||||
!a.startsWith('--default-base')
|
||||
) // not used by the new command
|
||||
.map((a) => `"${a}"`),
|
||||
].join(' ');
|
||||
const filterArgs = [
|
||||
'_',
|
||||
'app-name',
|
||||
'appName',
|
||||
'cli',
|
||||
'default-base',
|
||||
'defaultBase',
|
||||
'interactive',
|
||||
'nx-cloud',
|
||||
'nxCloud',
|
||||
'preset',
|
||||
'style',
|
||||
];
|
||||
|
||||
// These are the arguments that are passed to the schematic
|
||||
const args = Object.keys(parsedArgs)
|
||||
.filter((key) => !filterArgs.includes(key))
|
||||
.map((key) => `--${key}=${parsedArgs[key]}`)
|
||||
.join(' ');
|
||||
|
||||
const appNameArg = appName ? ` --appName="${appName}"` : ``;
|
||||
const styleArg = style ? ` --style="${style}"` : ``;
|
||||
@@ -434,7 +433,7 @@ function createApp(
|
||||
const defaultBaseArg = defaultBase ? ` --defaultBase="${defaultBase}"` : ``;
|
||||
|
||||
console.log(
|
||||
`new ${args} --preset="${preset}"${appNameArg}${styleArg}${nxCloudArg}${interactiveArg}${defaultBaseArg} --collection=@nrwl/workspace`
|
||||
`new ${name} ${args} --preset="${preset}"${appNameArg}${styleArg}${nxCloudArg}${interactiveArg}${defaultBaseArg} --collection=@nrwl/workspace`
|
||||
);
|
||||
const executablePath = path.join(tmpDir, 'node_modules', '.bin', cli.command);
|
||||
const collectionJsonPath = path.join(
|
||||
@@ -445,7 +444,7 @@ function createApp(
|
||||
'collection.json'
|
||||
);
|
||||
execSync(
|
||||
`"${executablePath}" new ${args} --preset="${preset}"${appNameArg}${styleArg}${nxCloudArg}${interactiveArg}${defaultBaseArg} --collection=${collectionJsonPath}`,
|
||||
`"${executablePath}" new ${name} ${args} --preset="${preset}"${appNameArg}${styleArg}${nxCloudArg}${interactiveArg}${defaultBaseArg} --collection=${collectionJsonPath}`,
|
||||
{
|
||||
stdio: [0, 1, 2],
|
||||
}
|
||||
@@ -453,7 +452,7 @@ function createApp(
|
||||
|
||||
if (nxCloud) {
|
||||
output.addVerticalSeparator();
|
||||
execSync(`./node_modules/.bin/nx g @nrwl/nx-cloud:init --no-analytics`, {
|
||||
execSync(`npx nx g @nrwl/nx-cloud:init --no-analytics`, {
|
||||
stdio: [0, 1, 2],
|
||||
cwd: path.join(process.cwd(), name),
|
||||
});
|
||||
|
||||
@@ -188,6 +188,50 @@ describe('Cypress builder', () => {
|
||||
fakeEventEmitter.emit('exit', 0); // Passing tsc command
|
||||
});
|
||||
|
||||
it('should call `Cypress.run` with a string of files to ignore', async (done) => {
|
||||
const cfg = {
|
||||
...cypressBuilderOptions,
|
||||
ignoreTestFiles: '/some/path/to/a/file.js',
|
||||
};
|
||||
|
||||
cypressBuilderRunner(cfg, mockedBuilderContext)
|
||||
.toPromise()
|
||||
.then(() => {
|
||||
expect(cypressRun).toHaveBeenCalledWith(
|
||||
jasmine.objectContaining({
|
||||
ignoreTestFiles: cfg.ignoreTestFiles,
|
||||
})
|
||||
);
|
||||
expect(cypressOpen).not.toHaveBeenCalled();
|
||||
done();
|
||||
});
|
||||
|
||||
fakeEventEmitter.emit('exit', 0); // Passing tsc command
|
||||
});
|
||||
|
||||
it('should call `Cypress.run` with a reporter and reporterOptions', async (done) => {
|
||||
const cfg = {
|
||||
...cypressBuilderOptions,
|
||||
reporter: 'junit',
|
||||
reporterOptions: 'mochaFile=reports/results-[hash].xml,toConsole=true',
|
||||
};
|
||||
|
||||
cypressBuilderRunner(cfg, mockedBuilderContext)
|
||||
.toPromise()
|
||||
.then(() => {
|
||||
expect(cypressRun).toHaveBeenCalledWith(
|
||||
jasmine.objectContaining({
|
||||
reporter: cfg.reporter,
|
||||
reporterOptions: cfg.reporterOptions,
|
||||
})
|
||||
);
|
||||
expect(cypressOpen).not.toHaveBeenCalled();
|
||||
done();
|
||||
});
|
||||
|
||||
fakeEventEmitter.emit('exit', 0); // Passing tsc command
|
||||
});
|
||||
|
||||
it('should fail early if application build fails', async (done) => {
|
||||
(devkitArchitect as any).scheduleTargetAndForget = jest
|
||||
.fn()
|
||||
@@ -260,6 +304,32 @@ describe('Cypress builder', () => {
|
||||
done();
|
||||
});
|
||||
|
||||
test('when devServerTarget AND baseUrl options are both present, baseUrl should take precidence', async (done) => {
|
||||
const options: CypressBuilderOptions = {
|
||||
...cypressBuilderOptions,
|
||||
baseUrl: 'test-url-from-options',
|
||||
};
|
||||
const result = await cypressBuilderRunner(
|
||||
options,
|
||||
mockedBuilderContext
|
||||
).toPromise();
|
||||
expect(cypressRun.calls.mostRecent().args[0].config.baseUrl).toBe(
|
||||
'test-url-from-options'
|
||||
);
|
||||
done();
|
||||
});
|
||||
|
||||
test('when devServerTarget option present and baseUrl option is absent, baseUrl should come from devServerTarget', async (done) => {
|
||||
await cypressBuilderRunner(
|
||||
cypressBuilderOptions,
|
||||
mockedBuilderContext
|
||||
).toPromise();
|
||||
expect(cypressRun.calls.mostRecent().args[0].config.baseUrl).toBe(
|
||||
'http://localhost:4200'
|
||||
);
|
||||
done();
|
||||
});
|
||||
|
||||
describe('legacy', () => {
|
||||
beforeEach(() => {
|
||||
cypressConfig = {
|
||||
|
||||
@@ -34,6 +34,9 @@ export interface CypressBuilderOptions extends JsonObject {
|
||||
copyFiles?: string;
|
||||
ciBuildId?: string;
|
||||
group?: string;
|
||||
ignoreTestFiles?: string;
|
||||
reporter?: string;
|
||||
reporterOptions?: string;
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -64,7 +67,9 @@ export function cypressBuilderRunner(
|
||||
|
||||
return (!legacy
|
||||
? options.devServerTarget
|
||||
? startDevServer(options.devServerTarget, options.watch, context)
|
||||
? startDevServer(options.devServerTarget, options.watch, context).pipe(
|
||||
map((devServerBaseUrl) => options.baseUrl || devServerBaseUrl)
|
||||
)
|
||||
: of(options.baseUrl)
|
||||
: legacyCompile(options, context)
|
||||
).pipe(
|
||||
@@ -82,7 +87,10 @@ export function cypressBuilderRunner(
|
||||
options.env,
|
||||
options.spec,
|
||||
options.ciBuildId,
|
||||
options.group
|
||||
options.group,
|
||||
options.ignoreTestFiles,
|
||||
options.reporter,
|
||||
options.reporterOptions
|
||||
)
|
||||
),
|
||||
options.watch ? tap(noop) : take(1),
|
||||
@@ -115,6 +123,7 @@ export function cypressBuilderRunner(
|
||||
* @param spec
|
||||
* @param ciBuildId
|
||||
* @param group
|
||||
* @param ignoreTestFiles
|
||||
*/
|
||||
function initCypress(
|
||||
cypressConfig: string,
|
||||
@@ -129,7 +138,10 @@ function initCypress(
|
||||
env?: Record<string, string>,
|
||||
spec?: string,
|
||||
ciBuildId?: string,
|
||||
group?: string
|
||||
group?: string,
|
||||
ignoreTestFiles?: string,
|
||||
reporter?: string,
|
||||
reporterOptions?: string
|
||||
): Observable<BuilderOutput> {
|
||||
// Cypress expects the folder where a `cypress.json` is present
|
||||
const projectFolderPath = dirname(cypressConfig);
|
||||
@@ -162,6 +174,9 @@ function initCypress(
|
||||
options.parallel = parallel;
|
||||
options.ciBuildId = ciBuildId;
|
||||
options.group = group;
|
||||
options.ignoreTestFiles = ignoreTestFiles;
|
||||
options.reporter = reporter;
|
||||
options.reporterOptions = reporterOptions;
|
||||
|
||||
return fromPromise<any>(
|
||||
!isWatching || headless ? Cypress.run(options) : Cypress.open(options)
|
||||
|
||||
@@ -72,6 +72,18 @@
|
||||
"group": {
|
||||
"type": "string",
|
||||
"description": "A named group for recorded runs in the Cypress dashboard."
|
||||
},
|
||||
"ignoreTestFiles": {
|
||||
"type": "string",
|
||||
"description": "A String or Array of glob patterns used to ignore test files that would otherwise be shown in your list of tests. Cypress uses minimatch with the options: {dot: true, matchBase: true}. We suggest using https://globster.xyz to test what files would match."
|
||||
},
|
||||
"reporter": {
|
||||
"type": "string",
|
||||
"description": "The reporter used during cypress run"
|
||||
},
|
||||
"reporterOptions": {
|
||||
"type": "string",
|
||||
"description": "The reporter options used. Supported options depend on the reporter."
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
|
||||
@@ -993,7 +993,7 @@ linter.defineParser('@typescript-eslint/parser', parser);
|
||||
linter.defineRule(enforceModuleBoundariesRuleName, enforceModuleBoundaries);
|
||||
|
||||
function createFile(f) {
|
||||
return { file: f, ext: extname(f), mtime: 1 };
|
||||
return { file: f, ext: extname(f), hash: '' };
|
||||
}
|
||||
|
||||
function runRule(
|
||||
|
||||
@@ -11,7 +11,8 @@
|
||||
"$source": "argv",
|
||||
"index": 0
|
||||
},
|
||||
"x-prompt": "What name would you like to use for the node application?"
|
||||
"x-prompt": "What name would you like to use for the node application?",
|
||||
"pattern": "^[a-zA-Z]{1}.*$"
|
||||
},
|
||||
"directory": {
|
||||
"description": "The directory of the new application.",
|
||||
|
||||
@@ -17,7 +17,8 @@
|
||||
"$source": "argv",
|
||||
"index": 0
|
||||
},
|
||||
"x-prompt": "What name would you like to use for the application?"
|
||||
"x-prompt": "What name would you like to use for the application?",
|
||||
"pattern": "^[a-zA-Z]{1}.*$"
|
||||
},
|
||||
"directory": {
|
||||
"description": "The directory of the new application.",
|
||||
|
||||
@@ -39,7 +39,7 @@
|
||||
"@angular-devkit/schematics": "~9.1.0",
|
||||
"@angular-devkit/build-webpack": "~0.901.0",
|
||||
"circular-dependency-plugin": "5.2.0",
|
||||
"copy-webpack-plugin": "5.1.1",
|
||||
"copy-webpack-plugin": "6.0.3",
|
||||
"fork-ts-checker-webpack-plugin": "^3.1.1",
|
||||
"license-webpack-plugin": "2.1.2",
|
||||
"source-map-support": "0.5.12",
|
||||
|
||||
@@ -3,7 +3,7 @@ import { JsonObject, workspaces } from '@angular-devkit/core';
|
||||
import { runWebpack, BuildResult } from '@angular-devkit/build-webpack';
|
||||
|
||||
import { Observable, from } from 'rxjs';
|
||||
import { resolve } from 'path';
|
||||
import { join, resolve } from 'path';
|
||||
import { map, concatMap } from 'rxjs/operators';
|
||||
import { getNodeWebpackConfig } from '../../utils/node.config';
|
||||
import { OUT_FILENAME } from '../../utils/config';
|
||||
@@ -44,7 +44,7 @@ function run(
|
||||
context
|
||||
);
|
||||
options.tsConfig = createTmpTsConfig(
|
||||
options.tsConfig,
|
||||
join(context.workspaceRoot, options.tsConfig),
|
||||
context.workspaceRoot,
|
||||
target.data.root,
|
||||
dependencies
|
||||
|
||||
@@ -99,27 +99,25 @@ export function getBaseWebpackPartial(
|
||||
|
||||
// process asset entries
|
||||
if (options.assets) {
|
||||
const copyWebpackPluginPatterns = options.assets.map((asset: any) => {
|
||||
return {
|
||||
context: asset.input,
|
||||
// Now we remove starting slash to make Webpack place it from the output root.
|
||||
to: asset.output,
|
||||
ignore: asset.ignore,
|
||||
from: {
|
||||
glob: asset.glob,
|
||||
dot: true,
|
||||
},
|
||||
};
|
||||
const copyWebpackPluginInstance = new CopyWebpackPlugin({
|
||||
patterns: options.assets.map((asset: any) => {
|
||||
return {
|
||||
context: asset.input,
|
||||
// Now we remove starting slash to make Webpack place it from the output root.
|
||||
to: asset.output,
|
||||
from: asset.glob,
|
||||
globOptions: {
|
||||
ignore: [
|
||||
'.gitkeep',
|
||||
'**/.DS_Store',
|
||||
'**/Thumbs.db',
|
||||
...(asset.ignore ? asset.ignore : []),
|
||||
],
|
||||
dot: true,
|
||||
},
|
||||
};
|
||||
}),
|
||||
});
|
||||
|
||||
const copyWebpackPluginOptions = {
|
||||
ignore: ['.gitkeep', '**/.DS_Store', '**/Thumbs.db'],
|
||||
};
|
||||
|
||||
const copyWebpackPluginInstance = new CopyWebpackPlugin(
|
||||
copyWebpackPluginPatterns,
|
||||
copyWebpackPluginOptions
|
||||
);
|
||||
extraPlugins.push(copyWebpackPluginInstance);
|
||||
}
|
||||
|
||||
|
||||
@@ -42,6 +42,7 @@
|
||||
"eslint-plugin-import": "^2.20.1",
|
||||
"eslint-plugin-jsx-a11y": "^6.2.3",
|
||||
"eslint-plugin-react": "^7.18.3",
|
||||
"eslint-plugin-react-hooks": "^2.4.0"
|
||||
"eslint-plugin-react-hooks": "^2.4.0",
|
||||
"url-loader": "^3.0.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,6 +26,7 @@ function getWebpackConfig(config: Configuration) {
|
||||
options: {
|
||||
limit: 10000, // 10kB
|
||||
name: '[name].[hash:7].[ext]',
|
||||
esModule: false,
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
@@ -25,7 +25,8 @@
|
||||
"$source": "argv",
|
||||
"index": 0
|
||||
},
|
||||
"x-prompt": "What name would you like to use for the application?"
|
||||
"x-prompt": "What name would you like to use for the application?",
|
||||
"pattern": "^[a-zA-Z]{1}.*$"
|
||||
},
|
||||
"directory": {
|
||||
"description": "The directory of the new application.",
|
||||
|
||||
@@ -3,7 +3,7 @@ import { render } from '@testing-library/react';
|
||||
|
||||
import <%= className %> from './<%= fileName %>';
|
||||
|
||||
describe(' <%= className %>', () => {
|
||||
describe('<%= className %>', () => {
|
||||
it('should render successfully', () => {
|
||||
const { baseElement } = render(< <%= className %> />);
|
||||
expect(baseElement).toBeTruthy();
|
||||
|
||||
@@ -5,5 +5,6 @@
|
||||
],
|
||||
"plugins": [
|
||||
<% if (style === 'styled-components') { %>["styled-components", { "pure": true, "ssr": true }]<% } %>
|
||||
<% if (style === 'styled-jsx') { %>"styled-jsx/babel"<% } %>
|
||||
]
|
||||
}
|
||||
|
||||
@@ -21,7 +21,8 @@
|
||||
"$source": "argv",
|
||||
"index": 0
|
||||
},
|
||||
"x-prompt": "What name would you like to use for the library?"
|
||||
"x-prompt": "What name would you like to use for the library?",
|
||||
"pattern": "^[a-zA-Z]{1}.*$"
|
||||
},
|
||||
"directory": {
|
||||
"type": "string",
|
||||
|
||||
@@ -88,7 +88,6 @@ async function storybookOptionMapper(
|
||||
...frameworkOptions,
|
||||
frameworkPresets: [...(frameworkOptions.frameworkPresets || [])],
|
||||
watch: false,
|
||||
docsMode: builderOptions.docsMode,
|
||||
};
|
||||
optionsWithFramework.config;
|
||||
return optionsWithFramework;
|
||||
|
||||
@@ -78,6 +78,11 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"docsMode": {
|
||||
"type": "boolean",
|
||||
"description": "Build a documentation-only site using addon-docs.",
|
||||
"default": false
|
||||
},
|
||||
"quiet": {
|
||||
"type": "boolean",
|
||||
"description": "Suppress verbose build output.",
|
||||
|
||||
@@ -33,6 +33,7 @@ export interface StorybookBuilderOptions extends JsonObject {
|
||||
sslKey?: string;
|
||||
staticDir?: number[];
|
||||
watch?: boolean;
|
||||
docsMode?: boolean;
|
||||
}
|
||||
|
||||
try {
|
||||
|
||||
@@ -60,7 +60,7 @@
|
||||
"caniuse-lite": "^1.0.30001030",
|
||||
"circular-dependency-plugin": "5.2.0",
|
||||
"clean-css": "4.2.1",
|
||||
"copy-webpack-plugin": "5.1.1",
|
||||
"copy-webpack-plugin": "6.0.3",
|
||||
"core-js": "^3.6.5",
|
||||
"css-loader": "3.4.2",
|
||||
"file-loader": "4.2.0",
|
||||
@@ -106,7 +106,6 @@
|
||||
"terser-webpack-plugin": "2.3.1",
|
||||
"ts-loader": "5.4.5",
|
||||
"tsconfig-paths-webpack-plugin": "3.2.0",
|
||||
"url-loader": "^3.0.0",
|
||||
"webpack": "4.42.0",
|
||||
"webpack-dev-middleware": "3.7.0",
|
||||
"webpack-merge": "4.2.1",
|
||||
|
||||
@@ -15,7 +15,7 @@ import { writeIndexHtml } from '../../utils/third-party/cli-files/utilities/inde
|
||||
import { NodeJsSyncHost } from '@angular-devkit/core/node';
|
||||
import { execSync } from 'child_process';
|
||||
import { Range, satisfies } from 'semver';
|
||||
import { basename } from 'path';
|
||||
import { basename, join } from 'path';
|
||||
import { createProjectGraph } from '@nrwl/workspace/src/core/project-graph';
|
||||
import {
|
||||
calculateProjectDependencies,
|
||||
@@ -76,7 +76,7 @@ export function run(options: WebBuildBuilderOptions, context: BuilderContext) {
|
||||
context
|
||||
);
|
||||
options.tsConfig = createTmpTsConfig(
|
||||
options.tsConfig,
|
||||
join(context.workspaceRoot, options.tsConfig),
|
||||
context.workspaceRoot,
|
||||
target.data.root,
|
||||
dependencies
|
||||
|
||||
@@ -11,7 +11,8 @@
|
||||
"$source": "argv",
|
||||
"index": 0
|
||||
},
|
||||
"x-prompt": "What name would you like to use for the application?"
|
||||
"x-prompt": "What name would you like to use for the application?",
|
||||
"pattern": "^[a-zA-Z]{1}.*$"
|
||||
},
|
||||
"directory": {
|
||||
"description": "The directory of the new application.",
|
||||
|
||||
@@ -223,21 +223,23 @@ function getClientEnvironment(mode) {
|
||||
}
|
||||
|
||||
export function createCopyPlugin(assets: AssetGlobPattern[]) {
|
||||
return new CopyWebpackPlugin(
|
||||
assets.map((asset) => {
|
||||
return new CopyWebpackPlugin({
|
||||
patterns: assets.map((asset) => {
|
||||
return {
|
||||
context: asset.input,
|
||||
// Now we remove starting slash to make Webpack place it from the output root.
|
||||
to: asset.output,
|
||||
ignore: asset.ignore,
|
||||
from: {
|
||||
glob: asset.glob,
|
||||
from: asset.glob,
|
||||
globOptions: {
|
||||
ignore: [
|
||||
'.gitkeep',
|
||||
'**/.DS_Store',
|
||||
'**/Thumbs.db',
|
||||
...(asset.ignore ? asset.ignore : []),
|
||||
],
|
||||
dot: true,
|
||||
},
|
||||
};
|
||||
}),
|
||||
{
|
||||
ignore: ['.gitkeep', '**/.DS_Store', '**/Thumbs.db'],
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -170,6 +170,5 @@ export function convertBuildOptions(buildOptions: WebBuildBuilderOptions): any {
|
||||
aot: false,
|
||||
forkTypeChecker: false,
|
||||
lazyModules: [] as string[],
|
||||
assets: [] as string[],
|
||||
};
|
||||
}
|
||||
|
||||
-44
@@ -10,7 +10,6 @@ import {
|
||||
buildOptimizerLoaderPath,
|
||||
} from '@angular-devkit/build-optimizer';
|
||||
import { tags } from '@angular-devkit/core';
|
||||
import * as CopyWebpackPlugin from 'copy-webpack-plugin';
|
||||
import * as path from 'path';
|
||||
import { ScriptTarget } from 'typescript';
|
||||
import {
|
||||
@@ -235,49 +234,6 @@ export function getCommonConfig(wco: WebpackConfigOptions): Configuration {
|
||||
});
|
||||
}
|
||||
|
||||
// process asset entries
|
||||
if (buildOptions.assets) {
|
||||
const copyWebpackPluginPatterns = buildOptions.assets.map(
|
||||
(asset: AssetPatternClass) => {
|
||||
// Resolve input paths relative to workspace root and add slash at the end.
|
||||
asset.input = path.resolve(root, asset.input).replace(/\\/g, '/');
|
||||
asset.input = asset.input.endsWith('/')
|
||||
? asset.input
|
||||
: asset.input + '/';
|
||||
asset.output = asset.output.endsWith('/')
|
||||
? asset.output
|
||||
: asset.output + '/';
|
||||
|
||||
if (asset.output.startsWith('..')) {
|
||||
const message =
|
||||
'An asset cannot be written to a location outside of the output path.';
|
||||
throw new Error(message);
|
||||
}
|
||||
|
||||
return {
|
||||
context: asset.input,
|
||||
// Now we remove starting slash to make Webpack place it from the output root.
|
||||
to: asset.output.replace(/^\//, ''),
|
||||
ignore: asset.ignore,
|
||||
from: {
|
||||
glob: asset.glob,
|
||||
dot: true,
|
||||
},
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
const copyWebpackPluginOptions = {
|
||||
ignore: ['.gitkeep', '**/.DS_Store', '**/Thumbs.db'],
|
||||
};
|
||||
|
||||
const copyWebpackPluginInstance = new CopyWebpackPlugin(
|
||||
copyWebpackPluginPatterns,
|
||||
copyWebpackPluginOptions
|
||||
);
|
||||
extraPlugins.push(copyWebpackPluginInstance);
|
||||
}
|
||||
|
||||
if (buildOptions.progress) {
|
||||
extraPlugins.push(new ProgressPlugin({ profile: buildOptions.verbose }));
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
module.exports = {
|
||||
name: 'tao',
|
||||
name: 'workspace',
|
||||
preset: '../../jest.config.js',
|
||||
transform: {
|
||||
'^.+\\.[tj]sx?$': 'ts-jest',
|
||||
|
||||
@@ -57,7 +57,6 @@
|
||||
"dotenv": "8.2.0",
|
||||
"ignore": "5.0.4",
|
||||
"npm-run-all": "4.1.5",
|
||||
"hasha": "5.1.0",
|
||||
"opn": "^5.3.0",
|
||||
"rxjs": "^6.5.4",
|
||||
"semver": "5.4.1",
|
||||
|
||||
@@ -20,7 +20,10 @@ import { DefaultReporter } from '../tasks-runner/default-reporter';
|
||||
export function affected(command: string, parsedArgs: yargs.Arguments): void {
|
||||
const { nxArgs, overrides } = splitArgsIntoNxArgsAndOverrides(
|
||||
parsedArgs,
|
||||
'affected'
|
||||
'affected',
|
||||
{
|
||||
printWarnings: command !== 'print-affected' && !parsedArgs.plain,
|
||||
}
|
||||
);
|
||||
|
||||
const projectGraph = createProjectGraph();
|
||||
|
||||
@@ -3,10 +3,9 @@ import {
|
||||
onlyWorkspaceProjects,
|
||||
} from '../core/project-graph';
|
||||
import { WorkspaceIntegrityChecks } from './workspace-integrity-checks';
|
||||
import * as path from 'path';
|
||||
import { appRootPath } from '../utils/app-root';
|
||||
import { allFilesInDir } from '../core/file-utils';
|
||||
import { readWorkspaceFiles, workspaceLayout } from '../core/file-utils';
|
||||
import { output } from '../utils/output';
|
||||
import * as path from 'path';
|
||||
|
||||
export function workspaceLint() {
|
||||
const graph = onlyWorkspaceProjects(createProjectGraph());
|
||||
@@ -25,8 +24,11 @@ export function workspaceLint() {
|
||||
}
|
||||
|
||||
function readAllFilesFromAppsAndLibs() {
|
||||
return [
|
||||
...allFilesInDir(`${appRootPath}/apps`).map((f) => f.file),
|
||||
...allFilesInDir(`${appRootPath}/libs`).map((f) => f.file),
|
||||
].filter((f) => !path.basename(f).startsWith('.'));
|
||||
const wl = workspaceLayout();
|
||||
return readWorkspaceFiles()
|
||||
.map((f) => f.file)
|
||||
.filter(
|
||||
(f) => f.startsWith(`${wl.appsDir}/`) || f.startsWith(`${wl.libsDir}/`)
|
||||
)
|
||||
.filter((f) => !path.basename(f).startsWith('.'));
|
||||
}
|
||||
|
||||
@@ -72,7 +72,8 @@ const ignoreArgs = ['$0', '_'];
|
||||
|
||||
export function splitArgsIntoNxArgsAndOverrides(
|
||||
args: yargs.Arguments,
|
||||
mode: 'run-one' | 'run-many' | 'affected'
|
||||
mode: 'run-one' | 'run-many' | 'affected' | 'print-affected',
|
||||
options = { printWarnings: true }
|
||||
): { nxArgs: NxArgs; overrides: yargs.Arguments } {
|
||||
const nxSpecific =
|
||||
mode === 'run-one' ? runOne : mode === 'run-many' ? runMany : runAffected;
|
||||
@@ -103,7 +104,9 @@ export function splitArgsIntoNxArgsAndOverrides(
|
||||
}
|
||||
|
||||
if (mode === 'affected') {
|
||||
printArgsWarning(nxArgs);
|
||||
if (options.printWarnings) {
|
||||
printArgsWarning(nxArgs);
|
||||
}
|
||||
if (
|
||||
!nxArgs.files &&
|
||||
!nxArgs.uncommitted &&
|
||||
|
||||
@@ -106,5 +106,5 @@ describe('WorkspaceIntegrityChecks', () => {
|
||||
});
|
||||
|
||||
function createFile(f) {
|
||||
return { file: f, ext: extname(f), mtime: 1 };
|
||||
return { file: f, ext: extname(f), hash: '' };
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import * as fs from 'fs';
|
||||
|
||||
import { WorkspaceResults } from './workspace-results';
|
||||
import { serializeJson } from '../utils/fileutils';
|
||||
import { ProjectType } from '..//core/project-graph';
|
||||
import { ProjectType } from '../core/project-graph';
|
||||
|
||||
describe('WorkspacesResults', () => {
|
||||
let results: WorkspaceResults;
|
||||
@@ -43,7 +43,7 @@ describe('WorkspacesResults', () => {
|
||||
results.saveResults();
|
||||
|
||||
expect(fs.writeSync).not.toHaveBeenCalled();
|
||||
expect(fs.unlinkSync).toHaveBeenCalledWith('dist/.nx-results');
|
||||
expect(fs.unlinkSync).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -53,23 +53,6 @@ describe('WorkspacesResults', () => {
|
||||
|
||||
expect(results.getResult('proj')).toBe(false);
|
||||
});
|
||||
|
||||
it('should save results to file system', () => {
|
||||
spyOn(fs, 'writeFileSync');
|
||||
|
||||
results.setResult('proj', false);
|
||||
results.saveResults();
|
||||
|
||||
expect(fs.writeFileSync).toHaveBeenCalledWith(
|
||||
'dist/.nx-results',
|
||||
serializeJson({
|
||||
command: 'test',
|
||||
results: {
|
||||
proj: false,
|
||||
},
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when results already exist', () => {
|
||||
@@ -97,7 +80,6 @@ describe('WorkspacesResults', () => {
|
||||
},
|
||||
});
|
||||
|
||||
expect(fs.readFileSync).toHaveBeenCalledWith('dist/.nx-results', 'utf-8');
|
||||
expect(results.getResult('proj')).toBe(false);
|
||||
});
|
||||
|
||||
|
||||
@@ -1,9 +1,17 @@
|
||||
import * as fs from 'fs';
|
||||
import { readJsonFile, writeJsonFile } from '../utils/fileutils';
|
||||
import { unlinkSync } from 'fs';
|
||||
import {
|
||||
directoryExists,
|
||||
readJsonFile,
|
||||
writeJsonFile,
|
||||
} from '../utils/fileutils';
|
||||
import { existsSync, unlinkSync } from 'fs';
|
||||
import { ProjectGraphNode } from '../core/project-graph';
|
||||
import { join } from 'path';
|
||||
import { appRootPath } from '@nrwl/workspace/src/utils/app-root';
|
||||
import * as fsExtra from 'fs-extra';
|
||||
|
||||
const RESULTS_FILE = 'dist/.nx-results';
|
||||
const resultsDir = join(appRootPath, 'node_modules', '.cache', 'nx');
|
||||
const resultsFile = join(resultsDir, 'results.json');
|
||||
|
||||
interface NxResults {
|
||||
command: string;
|
||||
@@ -31,11 +39,11 @@ export class WorkspaceResults {
|
||||
private command: string,
|
||||
private projects: Record<string, ProjectGraphNode>
|
||||
) {
|
||||
const resultsExists = fs.existsSync(RESULTS_FILE);
|
||||
const resultsExists = fs.existsSync(resultsFile);
|
||||
this.startedWithFailedProjects = false;
|
||||
if (resultsExists) {
|
||||
try {
|
||||
const commandResults = readJsonFile(RESULTS_FILE);
|
||||
const commandResults = readJsonFile(resultsFile);
|
||||
this.startedWithFailedProjects = commandResults.command === command;
|
||||
if (this.startedWithFailedProjects) {
|
||||
this.commandResults = commandResults;
|
||||
@@ -56,10 +64,19 @@ export class WorkspaceResults {
|
||||
}
|
||||
|
||||
saveResults() {
|
||||
try {
|
||||
if (!existsSync(resultsDir)) {
|
||||
fsExtra.ensureDirSync(resultsDir);
|
||||
}
|
||||
} catch (e) {
|
||||
if (!directoryExists(resultsDir)) {
|
||||
throw new Error(`Failed to create directory: ${resultsDir}`);
|
||||
}
|
||||
}
|
||||
if (Object.values<boolean>(this.commandResults.results).includes(false)) {
|
||||
writeJsonFile(RESULTS_FILE, this.commandResults);
|
||||
} else if (fs.existsSync(RESULTS_FILE)) {
|
||||
unlinkSync(RESULTS_FILE);
|
||||
writeJsonFile(resultsFile, this.commandResults);
|
||||
} else if (fs.existsSync(resultsFile)) {
|
||||
unlinkSync(resultsFile);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -116,7 +116,7 @@ describe('project graph', () => {
|
||||
files = Object.keys(filesJson).map((f) => ({
|
||||
file: f,
|
||||
ext: extname(f),
|
||||
mtime: 1,
|
||||
hash: 'some-hash',
|
||||
}));
|
||||
readFileAtRevision = (p, r) => {
|
||||
const fromFs = filesJson[`./${p}`];
|
||||
@@ -139,13 +139,13 @@ describe('project graph', () => {
|
||||
{
|
||||
file: 'something-for-api.txt',
|
||||
ext: '.txt',
|
||||
mtime: 1,
|
||||
hash: 'some-hash',
|
||||
getChanges: () => [new WholeFileChange()],
|
||||
},
|
||||
{
|
||||
file: 'libs/ui/src/index.ts',
|
||||
ext: '.ts',
|
||||
mtime: 1,
|
||||
hash: 'some-hash',
|
||||
getChanges: () => [new WholeFileChange()],
|
||||
},
|
||||
]);
|
||||
@@ -211,7 +211,7 @@ describe('project graph', () => {
|
||||
{
|
||||
file: 'package.json',
|
||||
ext: '.json',
|
||||
mtime: 1,
|
||||
hash: 'some-hash',
|
||||
getChanges: () => jsonDiff(packageJson, updatedPackageJson),
|
||||
},
|
||||
]);
|
||||
@@ -279,7 +279,7 @@ describe('project graph', () => {
|
||||
{
|
||||
file: 'package.json',
|
||||
ext: '.json',
|
||||
mtime: 1,
|
||||
hash: 'some-hash',
|
||||
getChanges: () => jsonDiff(packageJson, updatedPackageJson),
|
||||
},
|
||||
]);
|
||||
@@ -300,7 +300,7 @@ describe('project graph', () => {
|
||||
{
|
||||
file: 'package.json',
|
||||
ext: '.json',
|
||||
mtime: 1,
|
||||
hash: 'some-hash',
|
||||
getChanges: () => jsonDiff(packageJson, updatedPackageJson),
|
||||
},
|
||||
]);
|
||||
|
||||
+2
-2
@@ -35,7 +35,7 @@ describe('getImplicitlyTouchedProjectsByJsonChanges', () => {
|
||||
[
|
||||
{
|
||||
file: 'package.json',
|
||||
mtime: 0,
|
||||
hash: 'some-hash',
|
||||
ext: '.json',
|
||||
getChanges: () => [
|
||||
{
|
||||
@@ -60,7 +60,7 @@ describe('getImplicitlyTouchedProjectsByJsonChanges', () => {
|
||||
[
|
||||
{
|
||||
file: 'package.json',
|
||||
mtime: 0,
|
||||
hash: 'some-hash',
|
||||
ext: '.json',
|
||||
getChanges: () => [new WholeFileChange()],
|
||||
},
|
||||
|
||||
@@ -67,7 +67,7 @@ describe('getTouchedNpmPackages', () => {
|
||||
[
|
||||
{
|
||||
file: 'package.json',
|
||||
mtime: 0,
|
||||
hash: 'some-hash',
|
||||
ext: '.json',
|
||||
getChanges: () => [
|
||||
{
|
||||
@@ -98,7 +98,7 @@ describe('getTouchedNpmPackages', () => {
|
||||
[
|
||||
{
|
||||
file: 'package.json',
|
||||
mtime: 0,
|
||||
hash: 'some-hash',
|
||||
ext: '.json',
|
||||
getChanges: () => [
|
||||
{
|
||||
@@ -137,7 +137,7 @@ describe('getTouchedNpmPackages', () => {
|
||||
[
|
||||
{
|
||||
file: 'package.json',
|
||||
mtime: 0,
|
||||
hash: 'some-hash',
|
||||
ext: '.json',
|
||||
getChanges: () => [
|
||||
{
|
||||
@@ -177,7 +177,7 @@ describe('getTouchedNpmPackages', () => {
|
||||
[
|
||||
{
|
||||
file: 'package.json',
|
||||
mtime: 0,
|
||||
hash: 'some-hash',
|
||||
ext: '.json',
|
||||
getChanges: () => [new WholeFileChange()],
|
||||
},
|
||||
|
||||
@@ -9,7 +9,7 @@ describe('getTouchedProjectsInNxJson', () => {
|
||||
{
|
||||
file: 'source.ts',
|
||||
ext: '.ts',
|
||||
mtime: 0,
|
||||
hash: 'some-hash',
|
||||
getChanges: () => [new WholeFileChange()],
|
||||
},
|
||||
],
|
||||
@@ -32,7 +32,7 @@ describe('getTouchedProjectsInNxJson', () => {
|
||||
{
|
||||
file: 'nx.json',
|
||||
ext: '.json',
|
||||
mtime: 0,
|
||||
hash: 'some-hash',
|
||||
getChanges: () => [new WholeFileChange()],
|
||||
},
|
||||
],
|
||||
@@ -58,7 +58,7 @@ describe('getTouchedProjectsInNxJson', () => {
|
||||
{
|
||||
file: 'nx.json',
|
||||
ext: '.json',
|
||||
mtime: 0,
|
||||
hash: 'some-hash',
|
||||
getChanges: () => [
|
||||
{
|
||||
type: DiffType.Modified,
|
||||
@@ -93,7 +93,7 @@ describe('getTouchedProjectsInNxJson', () => {
|
||||
{
|
||||
file: 'nx.json',
|
||||
ext: '.json',
|
||||
mtime: 0,
|
||||
hash: 'some-hash',
|
||||
getChanges: () => [
|
||||
{
|
||||
type: DiffType.Added,
|
||||
@@ -138,7 +138,7 @@ describe('getTouchedProjectsInNxJson', () => {
|
||||
{
|
||||
file: 'nx.json',
|
||||
ext: '.json',
|
||||
mtime: 0,
|
||||
hash: 'some-hash',
|
||||
getChanges: () => [
|
||||
{
|
||||
type: DiffType.Deleted,
|
||||
@@ -175,7 +175,7 @@ describe('getTouchedProjectsInNxJson', () => {
|
||||
{
|
||||
file: 'nx.json',
|
||||
ext: '.json',
|
||||
mtime: 0,
|
||||
hash: 'some-hash',
|
||||
getChanges: () => [
|
||||
{
|
||||
type: DiffType.Modified,
|
||||
|
||||
+9
-9
@@ -43,7 +43,7 @@ describe('getTouchedProjectsFromTsConfig', () => {
|
||||
{
|
||||
file: 'source.ts',
|
||||
ext: '.ts',
|
||||
mtime: 0,
|
||||
hash: 'some-hash',
|
||||
getChanges: () => [new WholeFileChange()],
|
||||
},
|
||||
],
|
||||
@@ -67,7 +67,7 @@ describe('getTouchedProjectsFromTsConfig', () => {
|
||||
{
|
||||
file: 'tsconfig.json',
|
||||
ext: '.json',
|
||||
mtime: 0,
|
||||
hash: 'some-hash',
|
||||
getChanges: () => [new WholeFileChange()],
|
||||
},
|
||||
],
|
||||
@@ -87,7 +87,7 @@ describe('getTouchedProjectsFromTsConfig', () => {
|
||||
{
|
||||
file: 'tsconfig.json',
|
||||
ext: '.json',
|
||||
mtime: 0,
|
||||
hash: 'some-hash',
|
||||
getChanges: () =>
|
||||
jsonDiff(
|
||||
{
|
||||
@@ -119,7 +119,7 @@ describe('getTouchedProjectsFromTsConfig', () => {
|
||||
{
|
||||
file: 'tsconfig.json',
|
||||
ext: '.json',
|
||||
mtime: 0,
|
||||
hash: 'some-hash',
|
||||
getChanges: () =>
|
||||
jsonDiff(
|
||||
{
|
||||
@@ -151,7 +151,7 @@ describe('getTouchedProjectsFromTsConfig', () => {
|
||||
{
|
||||
file: 'tsconfig.json',
|
||||
ext: '.json',
|
||||
mtime: 0,
|
||||
hash: 'some-hash',
|
||||
getChanges: () =>
|
||||
jsonDiff(
|
||||
{
|
||||
@@ -185,7 +185,7 @@ describe('getTouchedProjectsFromTsConfig', () => {
|
||||
{
|
||||
file: 'tsconfig.json',
|
||||
ext: '.json',
|
||||
mtime: 0,
|
||||
hash: 'some-hash',
|
||||
getChanges: () =>
|
||||
jsonDiff(
|
||||
{
|
||||
@@ -217,7 +217,7 @@ describe('getTouchedProjectsFromTsConfig', () => {
|
||||
{
|
||||
file: 'tsconfig.json',
|
||||
ext: '.json',
|
||||
mtime: 0,
|
||||
hash: 'some-hash',
|
||||
getChanges: () =>
|
||||
jsonDiff(
|
||||
{
|
||||
@@ -254,7 +254,7 @@ describe('getTouchedProjectsFromTsConfig', () => {
|
||||
{
|
||||
file: 'tsconfig.json',
|
||||
ext: '.json',
|
||||
mtime: 0,
|
||||
hash: 'some-hash',
|
||||
getChanges: () =>
|
||||
jsonDiff(
|
||||
{
|
||||
@@ -289,7 +289,7 @@ describe('getTouchedProjectsFromTsConfig', () => {
|
||||
{
|
||||
file: 'tsconfig.json',
|
||||
ext: '.json',
|
||||
mtime: 0,
|
||||
hash: 'some-hash',
|
||||
getChanges: () =>
|
||||
jsonDiff(
|
||||
{
|
||||
|
||||
+6
-6
@@ -9,7 +9,7 @@ describe('getTouchedProjectsInWorkspaceJson', () => {
|
||||
{
|
||||
file: 'source.ts',
|
||||
ext: '.ts',
|
||||
mtime: 0,
|
||||
hash: 'some-hash',
|
||||
getChanges: () => [new WholeFileChange()],
|
||||
},
|
||||
],
|
||||
@@ -32,7 +32,7 @@ describe('getTouchedProjectsInWorkspaceJson', () => {
|
||||
{
|
||||
file: 'workspace.json',
|
||||
ext: '.json',
|
||||
mtime: 0,
|
||||
hash: 'some-hash',
|
||||
getChanges: () => [new WholeFileChange()],
|
||||
},
|
||||
],
|
||||
@@ -57,7 +57,7 @@ describe('getTouchedProjectsInWorkspaceJson', () => {
|
||||
{
|
||||
file: 'workspace.json',
|
||||
ext: '.json',
|
||||
mtime: 0,
|
||||
hash: 'some-hash',
|
||||
getChanges: () => [
|
||||
{
|
||||
type: DiffType.Modified,
|
||||
@@ -91,7 +91,7 @@ describe('getTouchedProjectsInWorkspaceJson', () => {
|
||||
{
|
||||
file: 'workspace.json',
|
||||
ext: '.json',
|
||||
mtime: 0,
|
||||
hash: 'some-hash',
|
||||
getChanges: () => [
|
||||
{
|
||||
type: DiffType.Added,
|
||||
@@ -132,7 +132,7 @@ describe('getTouchedProjectsInWorkspaceJson', () => {
|
||||
{
|
||||
file: 'workspace.json',
|
||||
ext: '.json',
|
||||
mtime: 0,
|
||||
hash: 'some-hash',
|
||||
getChanges: () => [
|
||||
{
|
||||
type: DiffType.Deleted,
|
||||
@@ -167,7 +167,7 @@ describe('getTouchedProjectsInWorkspaceJson', () => {
|
||||
{
|
||||
file: 'workspace.json',
|
||||
ext: '.json',
|
||||
mtime: 0,
|
||||
hash: 'some-hash',
|
||||
getChanges: () => [
|
||||
{
|
||||
type: DiffType.Modified,
|
||||
|
||||
+5
-5
@@ -7,13 +7,13 @@ describe('getTouchedProjects', () => {
|
||||
{
|
||||
file: 'libs/a/index.ts',
|
||||
ext: '.ts',
|
||||
mtime: 0,
|
||||
hash: 'some-hash',
|
||||
getChanges: () => [new WholeFileChange()],
|
||||
},
|
||||
{
|
||||
file: 'libs/b/index.ts',
|
||||
ext: '.ts',
|
||||
mtime: 0,
|
||||
hash: 'some-hash',
|
||||
getChanges: () => [new WholeFileChange()],
|
||||
},
|
||||
];
|
||||
@@ -30,7 +30,7 @@ describe('getTouchedProjects', () => {
|
||||
{
|
||||
file: 'libs/a-b/index.ts',
|
||||
ext: '.ts',
|
||||
mtime: 0,
|
||||
hash: 'some-hash',
|
||||
getChanges: () => [new WholeFileChange()],
|
||||
},
|
||||
];
|
||||
@@ -47,7 +47,7 @@ describe('getTouchedProjects', () => {
|
||||
{
|
||||
file: 'libs/a-b/index.ts',
|
||||
ext: '.ts',
|
||||
mtime: 0,
|
||||
hash: 'some-hash',
|
||||
getChanges: () => [new WholeFileChange()],
|
||||
},
|
||||
];
|
||||
@@ -64,7 +64,7 @@ describe('getTouchedProjects', () => {
|
||||
{
|
||||
file: 'libs/a/b/index.ts',
|
||||
ext: '.ts',
|
||||
mtime: 0,
|
||||
hash: 'some-hash',
|
||||
getChanges: () => [new WholeFileChange()],
|
||||
},
|
||||
];
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { assertWorkspaceValidity } from './assert-workspace-validity';
|
||||
import { output } from '../utils/output';
|
||||
|
||||
describe('assertWorkspaceValidity', () => {
|
||||
let mockNxJson: any;
|
||||
@@ -44,53 +45,85 @@ describe('assertWorkspaceValidity', () => {
|
||||
});
|
||||
|
||||
it('should throw for a missing project in workspace.json', () => {
|
||||
spyOn(output, 'error');
|
||||
delete mockWorkspaceJson.projects.app1;
|
||||
try {
|
||||
assertWorkspaceValidity(mockWorkspaceJson, mockNxJson);
|
||||
fail('Did not throw');
|
||||
} catch (e) {
|
||||
expect(e.message).toContain('projects are missing in');
|
||||
}
|
||||
|
||||
const mockExit = jest
|
||||
.spyOn(process, 'exit')
|
||||
.mockImplementation(((code?: number) => {}) as any);
|
||||
assertWorkspaceValidity(mockWorkspaceJson, mockNxJson);
|
||||
|
||||
expect(output.error).toHaveBeenCalledWith({
|
||||
title: 'Configuration Error',
|
||||
bodyLines: [
|
||||
`workspace.json and nx.json are out of sync. The following projects are missing in workspace.json: app1`,
|
||||
],
|
||||
});
|
||||
expect(mockExit).toHaveBeenCalledWith(1);
|
||||
mockExit.mockRestore();
|
||||
});
|
||||
|
||||
it('should throw for a missing project in nx.json', () => {
|
||||
spyOn(output, 'error');
|
||||
|
||||
delete mockNxJson.projects.app1;
|
||||
try {
|
||||
assertWorkspaceValidity(mockWorkspaceJson, mockNxJson);
|
||||
fail('Did not throw');
|
||||
} catch (e) {
|
||||
expect(e.message).toContain('projects are missing in nx.json');
|
||||
}
|
||||
|
||||
const mockExit = jest
|
||||
.spyOn(process, 'exit')
|
||||
.mockImplementation(((code?: number) => {}) as any);
|
||||
assertWorkspaceValidity(mockWorkspaceJson, mockNxJson);
|
||||
|
||||
expect(mockExit).toHaveBeenCalledWith(1);
|
||||
expect(output.error).toHaveBeenCalledWith({
|
||||
title: 'Configuration Error',
|
||||
bodyLines: [
|
||||
`workspace.json and nx.json are out of sync. The following projects are missing in nx.json: app1`,
|
||||
],
|
||||
});
|
||||
mockExit.mockRestore();
|
||||
});
|
||||
|
||||
it('should throw for an invalid top-level implicit dependency', () => {
|
||||
spyOn(output, 'error');
|
||||
mockNxJson.implicitDependencies = {
|
||||
'README.md': ['invalidproj'],
|
||||
};
|
||||
try {
|
||||
assertWorkspaceValidity(mockWorkspaceJson, mockNxJson);
|
||||
fail('Did not throw');
|
||||
} catch (e) {
|
||||
expect(e.message).toContain(
|
||||
'implicitDependencies specified in nx.json are invalid'
|
||||
);
|
||||
expect(e.message).toContain(' README.md');
|
||||
expect(e.message).toContain(' invalidproj');
|
||||
}
|
||||
|
||||
const mockExit = jest
|
||||
.spyOn(process, 'exit')
|
||||
.mockImplementation(((code?: number) => {}) as any);
|
||||
assertWorkspaceValidity(mockWorkspaceJson, mockNxJson);
|
||||
|
||||
expect(mockExit).toHaveBeenCalledWith(1);
|
||||
expect(output.error).toHaveBeenCalledWith({
|
||||
title: 'Configuration Error',
|
||||
bodyLines: [
|
||||
`The following implicitDependencies specified in nx.json are invalid:
|
||||
README.md
|
||||
invalidproj`,
|
||||
],
|
||||
});
|
||||
mockExit.mockRestore();
|
||||
});
|
||||
|
||||
it('should throw for an invalid project-level implicit dependency', () => {
|
||||
spyOn(output, 'error');
|
||||
mockNxJson.projects.app2.implicitDependencies = ['invalidproj'];
|
||||
|
||||
try {
|
||||
assertWorkspaceValidity(mockWorkspaceJson, mockNxJson);
|
||||
fail('Did not throw');
|
||||
} catch (e) {
|
||||
expect(e.message).toContain(
|
||||
'implicitDependencies specified in nx.json are invalid'
|
||||
);
|
||||
expect(e.message).toContain(' app2');
|
||||
expect(e.message).toContain(' invalidproj');
|
||||
}
|
||||
const mockExit = jest
|
||||
.spyOn(process, 'exit')
|
||||
.mockImplementation(((code?: number) => {}) as any);
|
||||
assertWorkspaceValidity(mockWorkspaceJson, mockNxJson);
|
||||
|
||||
expect(mockExit).toHaveBeenCalledWith(1);
|
||||
expect(output.error).toHaveBeenCalledWith({
|
||||
title: 'Configuration Error',
|
||||
bodyLines: [
|
||||
`The following implicitDependencies specified in nx.json are invalid:
|
||||
app2
|
||||
invalidproj`,
|
||||
],
|
||||
});
|
||||
mockExit.mockRestore();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,26 +1,40 @@
|
||||
import { workspaceFileName } from './file-utils';
|
||||
import { ImplicitJsonSubsetDependency } from '@nrwl/workspace/src/core/shared-interfaces';
|
||||
import {
|
||||
ImplicitJsonSubsetDependency,
|
||||
NxJson,
|
||||
} from '@nrwl/workspace/src/core/shared-interfaces';
|
||||
import { output } from '../utils/output';
|
||||
|
||||
export function assertWorkspaceValidity(workspaceJson, nxJson) {
|
||||
export function assertWorkspaceValidity(workspaceJson, nxJson: NxJson) {
|
||||
const workspaceJsonProjects = Object.keys(workspaceJson.projects);
|
||||
const nxJsonProjects = Object.keys(nxJson.projects);
|
||||
|
||||
if (minus(workspaceJsonProjects, nxJsonProjects).length > 0) {
|
||||
throw new Error(
|
||||
`${workspaceFileName()} and nx.json are out of sync. The following projects are missing in nx.json: ${minus(
|
||||
workspaceJsonProjects,
|
||||
nxJsonProjects
|
||||
).join(', ')}`
|
||||
);
|
||||
output.error({
|
||||
title: 'Configuration Error',
|
||||
bodyLines: [
|
||||
`${workspaceFileName()} and nx.json are out of sync. The following projects are missing in nx.json: ${minus(
|
||||
workspaceJsonProjects,
|
||||
nxJsonProjects
|
||||
).join(', ')}`,
|
||||
],
|
||||
});
|
||||
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (minus(nxJsonProjects, workspaceJsonProjects).length > 0) {
|
||||
throw new Error(
|
||||
`${workspaceFileName()} and nx.json are out of sync. The following projects are missing in ${workspaceFileName()}: ${minus(
|
||||
nxJsonProjects,
|
||||
workspaceJsonProjects
|
||||
).join(', ')}`
|
||||
);
|
||||
output.error({
|
||||
title: 'Configuration Error',
|
||||
bodyLines: [
|
||||
`${workspaceFileName()} and nx.json are out of sync. The following projects are missing in ${workspaceFileName()}: ${minus(
|
||||
nxJsonProjects,
|
||||
workspaceJsonProjects
|
||||
).join(', ')}`,
|
||||
],
|
||||
});
|
||||
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const projects = {
|
||||
@@ -81,7 +95,12 @@ export function assertWorkspaceValidity(workspaceJson, nxJson) {
|
||||
message += str;
|
||||
});
|
||||
|
||||
throw new Error(message);
|
||||
output.error({
|
||||
title: 'Configuration Error',
|
||||
bodyLines: [message],
|
||||
});
|
||||
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
function detectAndSetInvalidProjectValues(
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="#ffffff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-crosshair"><circle cx="12" cy="12" r="10"/><line x1="22" y1="12" x2="18" y2="12"/><line x1="6" y1="12" x2="2" y2="12"/><line x1="12" y1="6" x2="12" y2="2"/><line x1="12" y1="22" x2="12" y2="18"/></svg>
|
||||
|
Before Width: | Height: | Size: 400 B |
@@ -53,7 +53,7 @@ button.icon {
|
||||
line-height: 50%;
|
||||
}
|
||||
|
||||
button.icon img {
|
||||
button.icon svg {
|
||||
width: 1.25em;
|
||||
height: 1.25em;
|
||||
}
|
||||
@@ -168,11 +168,11 @@ text {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
svg {
|
||||
#svg-canvas {
|
||||
cursor: grab;
|
||||
}
|
||||
|
||||
svg:active {
|
||||
#svg-canvas:active {
|
||||
cursor: grabbing;
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,24 @@
|
||||
<link rel="stylesheet" href="dep-graph.css" />
|
||||
</head>
|
||||
<body>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" style="display: none;">
|
||||
<symbol id="crosshair" viewBox="0 0 24 24">
|
||||
<g
|
||||
fill="none"
|
||||
stroke="#ffffff"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<circle cx="12" cy="12" r="10" />
|
||||
<line x1="22" y1="12" x2="18" y2="12" />
|
||||
<line x1="6" y1="12" x2="2" y2="12" />
|
||||
<line x1="12" y1="6" x2="12" y2="2" />
|
||||
<line x1="12" y1="22" x2="12" y2="18" />
|
||||
</g>
|
||||
</symbol>
|
||||
</svg>
|
||||
|
||||
<div id="app">
|
||||
<div class="sidebar">
|
||||
<div class="sidebar-content">
|
||||
@@ -28,7 +46,10 @@
|
||||
<button id="select-all-button" onclick="window.selectAllProjects()">
|
||||
Select All
|
||||
</button>
|
||||
<button id="deselect-all-button" onclick="window.deselectAllProjects()">
|
||||
<button
|
||||
id="deselect-all-button"
|
||||
onclick="window.deselectAllProjects()"
|
||||
>
|
||||
Deselect All
|
||||
</button>
|
||||
</div>
|
||||
@@ -56,35 +77,14 @@
|
||||
x="-75%"
|
||||
y="-75%"
|
||||
>
|
||||
<!-- Thicken out the original shape -->
|
||||
<feMorphology
|
||||
operator="dilate"
|
||||
radius="4"
|
||||
in="SourceAlpha"
|
||||
result="thicken"
|
||||
<feDropShadow
|
||||
dx="0"
|
||||
dy="0"
|
||||
stdDeviation="10"
|
||||
flood-color="rgb(8, 108, 159)"
|
||||
flood-opacity="1"
|
||||
/>
|
||||
|
||||
<!-- Use a gaussian blur to create the soft blurriness of the glow -->
|
||||
<feGaussianBlur in="thicken" stdDeviation="10" result="blurred" />
|
||||
|
||||
<!-- Change the colour -->
|
||||
<feFlood flood-color="rgb(8, 108, 159)" result="glowColor" />
|
||||
|
||||
<!-- Color in the glows -->
|
||||
<feComposite
|
||||
in="glowColor"
|
||||
in2="blurred"
|
||||
operator="in"
|
||||
result="softGlow_colored"
|
||||
/>
|
||||
|
||||
<!-- Layer the effects together -->
|
||||
<feMerge>
|
||||
<feMergeNode in="softGlow_colored" />
|
||||
<feMergeNode in="SourceGraphic" />
|
||||
</feMerge>
|
||||
</filter>
|
||||
|
||||
<filter
|
||||
id="sofGlowFocusAffected"
|
||||
height="300%"
|
||||
@@ -92,33 +92,13 @@
|
||||
x="-75%"
|
||||
y="-75%"
|
||||
>
|
||||
<!-- Thicken out the original shape -->
|
||||
<feMorphology
|
||||
operator="dilate"
|
||||
radius="4"
|
||||
in="SourceAlpha"
|
||||
result="thicken"
|
||||
<feDropShadow
|
||||
dx="0"
|
||||
dy="0"
|
||||
stdDeviation="10"
|
||||
flood-color="rgb(248,84,119)"
|
||||
flood-opacity="1"
|
||||
/>
|
||||
|
||||
<!-- Use a gaussian blur to create the soft blurriness of the glow -->
|
||||
<feGaussianBlur in="thicken" stdDeviation="10" result="blurred" />
|
||||
|
||||
<!-- Change the colour -->
|
||||
<feFlood flood-color="rgb(248,84,119)" result="glowColor" />
|
||||
|
||||
<!-- Color in the glows -->
|
||||
<feComposite
|
||||
in="glowColor"
|
||||
in2="blurred"
|
||||
operator="in"
|
||||
result="softGlow_colored"
|
||||
/>
|
||||
|
||||
<!-- Layer the effects together -->
|
||||
<feMerge>
|
||||
<feMergeNode in="softGlow_colored" />
|
||||
<feMergeNode in="SourceGraphic" />
|
||||
</feMerge>
|
||||
</filter>
|
||||
</defs>
|
||||
</svg>
|
||||
|
||||
@@ -40,9 +40,24 @@ function createProjectList(headerText, projects) {
|
||||
let focusButton = document.createElement('button');
|
||||
focusButton.className = 'icon';
|
||||
|
||||
let buttonIcon = document.createElement('img');
|
||||
buttonIcon.src = 'crosshair.svg';
|
||||
focusButton.append(buttonIcon);
|
||||
let buttonIconContainer = document.createElementNS(
|
||||
'http://www.w3.org/2000/svg',
|
||||
'svg'
|
||||
);
|
||||
let buttonIcon = document.createElementNS(
|
||||
'http://www.w3.org/2000/svg',
|
||||
'use'
|
||||
);
|
||||
|
||||
buttonIcon.setAttributeNS(
|
||||
'http://www.w3.org/1999/xlink',
|
||||
'xlink:href',
|
||||
'#crosshair'
|
||||
);
|
||||
|
||||
buttonIconContainer.appendChild(buttonIcon);
|
||||
|
||||
focusButton.append(buttonIconContainer);
|
||||
|
||||
focusButton.onclick = () => {
|
||||
window.focusProject(project.name);
|
||||
@@ -353,7 +368,7 @@ function render() {
|
||||
const render = createRenderer();
|
||||
|
||||
// Set up an SVG group so that we can translate the final graph.
|
||||
var svg = d3.select('svg');
|
||||
var svg = d3.select('#svg-canvas');
|
||||
svg.select('g').remove();
|
||||
let inner = svg.append('g');
|
||||
|
||||
|
||||
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 387 KiB |
@@ -22,17 +22,19 @@ describe('createFileMap', () => {
|
||||
},
|
||||
};
|
||||
const files = [
|
||||
{ file: 'apps/demo/src/main.ts', mtime: 1, ext: '.ts' },
|
||||
{ file: 'apps/demo-e2e/src/main.ts', mtime: 1, ext: '.ts' },
|
||||
{ file: 'libs/ui/src/index.ts', mtime: 1, ext: '.ts' },
|
||||
{ file: 'apps/demo/src/main.ts', hash: 'some-hash', ext: '.ts' },
|
||||
{ file: 'apps/demo-e2e/src/main.ts', hash: 'some-hash', ext: '.ts' },
|
||||
{ file: 'libs/ui/src/index.ts', hash: 'some-hash', ext: '.ts' },
|
||||
];
|
||||
|
||||
const result = createFileMap(workspaceJson, files);
|
||||
|
||||
expect(result).toEqual({
|
||||
demo: [{ file: 'apps/demo/src/main.ts', mtime: 1, ext: '.ts' }],
|
||||
'demo-e2e': [{ file: 'apps/demo-e2e/src/main.ts', mtime: 1, ext: '.ts' }],
|
||||
ui: [{ file: 'libs/ui/src/index.ts', mtime: 1, ext: '.ts' }],
|
||||
demo: [{ file: 'apps/demo/src/main.ts', hash: 'some-hash', ext: '.ts' }],
|
||||
'demo-e2e': [
|
||||
{ file: 'apps/demo-e2e/src/main.ts', hash: 'some-hash', ext: '.ts' },
|
||||
],
|
||||
ui: [{ file: 'libs/ui/src/index.ts', hash: 'some-hash', ext: '.ts' }],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -6,16 +6,18 @@ import { extname } from 'path';
|
||||
import { NxArgs } from '../command-line/utils';
|
||||
import { WorkspaceResults } from '../command-line/workspace-results';
|
||||
import { appRootPath } from '../utils/app-root';
|
||||
import { readJsonFile, fileExists } from '../utils/fileutils';
|
||||
import { fileExists, readJsonFile } from '../utils/fileutils';
|
||||
import { jsonDiff } from '../utils/json-diff';
|
||||
import { ProjectGraphNode } from './project-graph';
|
||||
import { Environment, NxJson } from './shared-interfaces';
|
||||
import { defaultFileHasher } from './hasher/file-hasher';
|
||||
import { performance } from 'perf_hooks';
|
||||
|
||||
const ignore = require('ignore');
|
||||
|
||||
export interface FileData {
|
||||
file: string;
|
||||
mtime: number;
|
||||
hash: string;
|
||||
ext: string;
|
||||
}
|
||||
|
||||
@@ -47,15 +49,15 @@ export function calculateFileChanges(
|
||||
if (ignore) {
|
||||
files = files.filter((f) => !ignore.ignores(f));
|
||||
}
|
||||
|
||||
return files.map((f) => {
|
||||
const ext = extname(f);
|
||||
const _mtime = mtime(`${appRootPath}/${f}`);
|
||||
// Memoize results so we don't recalculate on successive invocation.
|
||||
const hash = defaultFileHasher.hashFile(f);
|
||||
|
||||
return {
|
||||
file: f,
|
||||
ext,
|
||||
mtime: _mtime,
|
||||
hash,
|
||||
getChanges: (): Change[] => {
|
||||
if (!nxArgs) {
|
||||
return [new WholeFileChange()];
|
||||
@@ -110,11 +112,11 @@ function defaultReadFileAtRevision(
|
||||
}
|
||||
|
||||
function getFileData(filePath: string): FileData {
|
||||
const stat = fs.statSync(filePath);
|
||||
const file = path.relative(appRootPath, filePath).split(path.sep).join('/');
|
||||
return {
|
||||
file: path.relative(appRootPath, filePath).split(path.sep).join('/'),
|
||||
file: file,
|
||||
hash: defaultFileHasher.hashFile(filePath),
|
||||
ext: path.extname(filePath),
|
||||
mtime: stat.mtimeMs,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -192,30 +194,63 @@ export function readNxJson(): NxJson {
|
||||
return config;
|
||||
}
|
||||
|
||||
export function workspaceLayout(): { appsDir: string; libsDir: string } {
|
||||
const nxJson = readNxJson();
|
||||
const appsDir =
|
||||
(nxJson.workspaceLayout && nxJson.workspaceLayout.appsDir) || 'apps';
|
||||
const libsDir =
|
||||
(nxJson.workspaceLayout && nxJson.workspaceLayout.libsDir) || 'libs';
|
||||
return { appsDir, libsDir };
|
||||
}
|
||||
|
||||
// TODO: Make this list extensible
|
||||
export function rootWorkspaceFileNames(): string[] {
|
||||
return [`package.json`, workspaceFileName(), `nx.json`, `tsconfig.json`];
|
||||
}
|
||||
|
||||
export function readWorkspaceFiles(): FileData[] {
|
||||
const workspaceJson = readWorkspaceJson();
|
||||
const files = [];
|
||||
|
||||
files.push(
|
||||
...rootWorkspaceFileNames().map((f) => getFileData(`${appRootPath}/${f}`))
|
||||
export function rootWorkspaceFileData(): FileData[] {
|
||||
return rootWorkspaceFileNames().map((f) =>
|
||||
getFileData(`${appRootPath}/${f}`)
|
||||
);
|
||||
}
|
||||
|
||||
// Add known workspace files and directories
|
||||
files.push(...allFilesInDir(appRootPath, false));
|
||||
files.push(...allFilesInDir(`${appRootPath}/tools`));
|
||||
export function readWorkspaceFiles(): FileData[] {
|
||||
performance.mark('read workspace files:start');
|
||||
|
||||
// Add files for workspace projects
|
||||
Object.keys(workspaceJson.projects).forEach((projectName) => {
|
||||
const project = workspaceJson.projects[projectName];
|
||||
files.push(...allFilesInDir(`${appRootPath}/${project.root}`));
|
||||
});
|
||||
if (defaultFileHasher.usesGitForHashing) {
|
||||
const ignoredGlobs = getIgnoredGlobs();
|
||||
const r = defaultFileHasher.workspaceFiles
|
||||
.filter((f) => !ignoredGlobs.ignores(f))
|
||||
.map((f) => getFileData(`${appRootPath}/${f}`));
|
||||
performance.mark('read workspace files:end');
|
||||
performance.measure(
|
||||
'read workspace files',
|
||||
'read workspace files:start',
|
||||
'read workspace files:end'
|
||||
);
|
||||
r.sort((x, y) => x.file.localeCompare(y.file));
|
||||
return r;
|
||||
} else {
|
||||
const r = [];
|
||||
r.push(...rootWorkspaceFileData());
|
||||
|
||||
return files;
|
||||
// Add known workspace files and directories
|
||||
r.push(...allFilesInDir(appRootPath, false));
|
||||
r.push(...allFilesInDir(`${appRootPath}/tools`));
|
||||
const wl = workspaceLayout();
|
||||
r.push(...allFilesInDir(`${appRootPath}/${wl.appsDir}`));
|
||||
if (wl.appsDir !== wl.libsDir) {
|
||||
r.push(...allFilesInDir(`${appRootPath}/${wl.libsDir}`));
|
||||
}
|
||||
performance.mark('read workspace files:end');
|
||||
performance.measure(
|
||||
'read workspace files',
|
||||
'read workspace files:start',
|
||||
'read workspace files:end'
|
||||
);
|
||||
r.sort((x, y) => x.file.localeCompare(y.file));
|
||||
return r;
|
||||
}
|
||||
}
|
||||
|
||||
export function readEnvironment(
|
||||
@@ -229,17 +264,6 @@ export function readEnvironment(
|
||||
return { nxJson, workspaceJson, workspaceResults };
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the time when file was last modified
|
||||
* Returns -Infinity for a non-existent file
|
||||
*/
|
||||
export function mtime(filePath: string): number {
|
||||
if (!fs.existsSync(filePath)) {
|
||||
return -Infinity;
|
||||
}
|
||||
return fs.statSync(filePath).mtimeMs;
|
||||
}
|
||||
|
||||
export function normalizedProjectRoot(p: ProjectGraphNode): string {
|
||||
if (p.data && p.data.root) {
|
||||
const path = p.data.root.split('/').filter((v) => !!v);
|
||||
@@ -252,3 +276,13 @@ export function normalizedProjectRoot(p: ProjectGraphNode): string {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
export function filesChanged(a: FileData[], b: FileData[]) {
|
||||
if (a.length !== b.length) return true;
|
||||
|
||||
for (let i = 0; i < a.length; ++i) {
|
||||
if (a[i].file !== b[i].file) return true;
|
||||
if (a[i].hash !== b[i].hash) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
import { getFileHashes } from './git-hasher';
|
||||
import { readFileSync } from 'fs';
|
||||
import { defaultHashing, HashingImp } from './hashing-impl';
|
||||
import { appRootPath } from '../../utils/app-root';
|
||||
import { performance } from 'perf_hooks';
|
||||
|
||||
type PathAndTransformer = {
|
||||
path: string;
|
||||
transformer: (x: string) => string | null;
|
||||
};
|
||||
|
||||
export function extractNameAndVersion(content: string): string {
|
||||
try {
|
||||
const c = JSON.parse(content);
|
||||
return `${c.name}${c.version}`;
|
||||
} catch (e) {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
export class FileHasher {
|
||||
fileHashes: { [path: string]: string } = {};
|
||||
workspaceFiles = [];
|
||||
usesGitForHashing = false;
|
||||
|
||||
constructor(private readonly hashing: HashingImp) {
|
||||
this.init();
|
||||
}
|
||||
|
||||
init() {
|
||||
performance.mark('init hashing:start');
|
||||
this.fileHashes = {};
|
||||
this.workspaceFiles = [];
|
||||
this.getHashesFromGit();
|
||||
this.usesGitForHashing = Object.keys(this.fileHashes).length > 0;
|
||||
performance.mark('init hashing:end');
|
||||
performance.measure(
|
||||
'init hashing',
|
||||
'init hashing:start',
|
||||
'init hashing:end'
|
||||
);
|
||||
}
|
||||
|
||||
hashFile(path: string, transformer: (x: string) => string | null = null) {
|
||||
const relativePath = path.startsWith(appRootPath)
|
||||
? path.substr(appRootPath.length + 1)
|
||||
: path;
|
||||
if (!this.fileHashes[relativePath]) {
|
||||
this.fileHashes[relativePath] = this.processPath({ path, transformer });
|
||||
}
|
||||
return this.fileHashes[relativePath];
|
||||
}
|
||||
|
||||
private getHashesFromGit() {
|
||||
const sliceIndex = appRootPath.length + 1;
|
||||
getFileHashes(appRootPath).forEach((hash, filename) => {
|
||||
this.fileHashes[filename.substr(sliceIndex)] = hash;
|
||||
/**
|
||||
* we have to store it separately because fileHashes can be modified
|
||||
* later on and can contain files that do not exist in the workspace
|
||||
*/
|
||||
this.workspaceFiles.push(filename.substr(sliceIndex));
|
||||
});
|
||||
}
|
||||
|
||||
private processPath(pathAndTransformer: PathAndTransformer): string {
|
||||
try {
|
||||
if (pathAndTransformer.transformer) {
|
||||
const transformedFile = pathAndTransformer.transformer(
|
||||
readFileSync(pathAndTransformer.path).toString()
|
||||
);
|
||||
return this.hashing.hashArray([transformedFile]);
|
||||
} else {
|
||||
return this.hashing.hashFile(pathAndTransformer.path);
|
||||
}
|
||||
} catch (e) {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const defaultFileHasher = new FileHasher(defaultHashing);
|
||||
@@ -0,0 +1,97 @@
|
||||
import { dirSync } from 'tmp';
|
||||
import { rmdirSync } from 'fs-extra';
|
||||
import { execSync } from 'child_process';
|
||||
import { getFileHashes } from './git-hasher';
|
||||
|
||||
describe('git-hasher', () => {
|
||||
let dir;
|
||||
|
||||
beforeEach(() => {
|
||||
dir = dirSync().name;
|
||||
run(`git init`);
|
||||
run(`git config user.email "test@test.com"`);
|
||||
run(`git config user.name "test"`);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
rmdirSync(dir, { recursive: true });
|
||||
});
|
||||
|
||||
it('should work', () => {
|
||||
run(`echo AAA > a.txt`);
|
||||
run(`git add .`);
|
||||
run(`git commit -am init`);
|
||||
const hashes1 = getFileHashes(dir);
|
||||
expect([...hashes1.keys()]).toEqual([`${dir}/a.txt`]);
|
||||
expect(hashes1.get(`${dir}/a.txt`)).toBeDefined();
|
||||
|
||||
// should handle additions
|
||||
run(`echo BBB > b.txt`);
|
||||
expect([...getFileHashes(dir).keys()]).toEqual([
|
||||
`${dir}/a.txt`,
|
||||
`${dir}/b.txt`,
|
||||
]);
|
||||
|
||||
run(`git add .`);
|
||||
expect([...getFileHashes(dir).keys()]).toEqual([
|
||||
`${dir}/a.txt`,
|
||||
`${dir}/b.txt`,
|
||||
]);
|
||||
|
||||
run(`git commit -am second`);
|
||||
expect([...getFileHashes(dir).keys()]).toEqual([
|
||||
`${dir}/a.txt`,
|
||||
`${dir}/b.txt`,
|
||||
]);
|
||||
|
||||
// should handle removals
|
||||
run(`rm b.txt`);
|
||||
expect([...getFileHashes(dir).keys()]).toEqual([`${dir}/a.txt`]);
|
||||
|
||||
run(`git add .`);
|
||||
expect([...getFileHashes(dir).keys()]).toEqual([`${dir}/a.txt`]);
|
||||
|
||||
run(`git commit -am third`);
|
||||
expect([...getFileHashes(dir).keys()]).toEqual([`${dir}/a.txt`]);
|
||||
|
||||
// should handle moves
|
||||
run(`mv a.txt newa.txt`);
|
||||
expect([...getFileHashes(dir).keys()]).toEqual([`${dir}/newa.txt`]);
|
||||
|
||||
run(`git add .`);
|
||||
expect([...getFileHashes(dir).keys()]).toEqual([`${dir}/newa.txt`]);
|
||||
|
||||
run(`echo AAAA > a.txt`);
|
||||
expect([...getFileHashes(dir).keys()]).toEqual([
|
||||
`${dir}/a.txt`,
|
||||
`${dir}/newa.txt`,
|
||||
]);
|
||||
|
||||
run(`git add .`);
|
||||
expect([...getFileHashes(dir).keys()]).toEqual([
|
||||
`${dir}/a.txt`,
|
||||
`${dir}/newa.txt`,
|
||||
]);
|
||||
});
|
||||
|
||||
it('should handle spaces in filenames', () => {
|
||||
run(`echo AAA > "a b".txt`);
|
||||
run(`git add .`);
|
||||
run(`git commit -am init`);
|
||||
expect([...getFileHashes(dir).keys()]).toEqual([`${dir}/a b.txt`]);
|
||||
});
|
||||
|
||||
it('should handle renames and modifications', () => {
|
||||
run(`echo AAA > "a".txt`);
|
||||
run(`git add .`);
|
||||
run(`git commit -am init`);
|
||||
run(`mv a.txt moda.txt`);
|
||||
run(`git add .`);
|
||||
run(`echo modified >> moda.txt`);
|
||||
expect([...getFileHashes(dir).keys()]).toEqual([`${dir}/moda.txt`]);
|
||||
});
|
||||
|
||||
function run(command: string) {
|
||||
return execSync(command, { cwd: dir, stdio: ['pipe', 'pipe', 'pipe'] });
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,166 @@
|
||||
import { spawnSync } from 'child_process';
|
||||
import { join } from 'path';
|
||||
import { statSync } from 'fs';
|
||||
|
||||
function parseGitLsTree(output: string): Map<string, string> {
|
||||
const changes: Map<string, string> = new Map<string, string>();
|
||||
if (output) {
|
||||
const gitRegex: RegExp = /([0-9]{6})\s(blob|commit)\s([a-f0-9]{40})\s*(.*)/;
|
||||
output.split('\n').forEach((line) => {
|
||||
if (line) {
|
||||
const matches: RegExpMatchArray | null = line.match(gitRegex);
|
||||
if (matches && matches[3] && matches[4]) {
|
||||
const hash: string = matches[3];
|
||||
const filename: string = matches[4];
|
||||
changes.set(filename, hash);
|
||||
} else {
|
||||
throw new Error(`Cannot parse git ls-tree input: "${line}"`);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
return changes;
|
||||
}
|
||||
|
||||
function parseGitStatus(output: string): Map<string, string> {
|
||||
const changes: Map<string, string> = new Map<string, string>();
|
||||
if (!output) {
|
||||
return changes;
|
||||
}
|
||||
output
|
||||
.trim()
|
||||
.split('\n')
|
||||
.forEach((line) => {
|
||||
const [changeType, ...filenames] = line
|
||||
.trim()
|
||||
.match(/(?:[^\s"]+|"[^"]*")+/g)
|
||||
.map((r) => (r.startsWith('"') ? r.substring(1, r.length - 1) : r))
|
||||
.filter((r) => !!r);
|
||||
if (changeType && filenames && filenames.length > 0) {
|
||||
// the before filename we mark as deleted, so we remove it from the map
|
||||
// changeType can be A/D/R/RM etc
|
||||
// if it R and RM, we need to split the output into before and after
|
||||
// the before part gets marked as deleted
|
||||
if (changeType[0] === 'R') {
|
||||
changes.set(filenames[0], 'D');
|
||||
}
|
||||
changes.set(filenames[filenames.length - 1], changeType);
|
||||
}
|
||||
});
|
||||
return changes;
|
||||
}
|
||||
|
||||
function spawnProcess(command: string, args: string[], cwd: string): string {
|
||||
const r = spawnSync(command, args, { cwd, maxBuffer: 50 * 1024 * 1024 });
|
||||
if (r.status !== 0) {
|
||||
throw new Error(
|
||||
`Failed to run ${command} ${args.join(' ')}.\n${r.stdout}\n${r.stderr}`
|
||||
);
|
||||
}
|
||||
return r.stdout.toString().trim();
|
||||
}
|
||||
|
||||
function getGitHashForFiles(
|
||||
filesToHash: string[],
|
||||
path: string
|
||||
): Map<string, string> {
|
||||
const changes: Map<string, string> = new Map<string, string>();
|
||||
if (filesToHash.length) {
|
||||
const hashStdout = spawnProcess(
|
||||
'git',
|
||||
['hash-object', ...filesToHash],
|
||||
path
|
||||
);
|
||||
const hashes: string[] = hashStdout.split('\n');
|
||||
if (hashes.length !== filesToHash.length) {
|
||||
throw new Error(
|
||||
`Passed ${filesToHash.length} file paths to Git to hash, but received ${hashes.length} hashes.`
|
||||
);
|
||||
}
|
||||
for (let i: number = 0; i < hashes.length; i++) {
|
||||
const hash: string = hashes[i];
|
||||
const filePath: string = filesToHash[i];
|
||||
changes.set(filePath, hash);
|
||||
}
|
||||
}
|
||||
return changes;
|
||||
}
|
||||
|
||||
function gitLsTree(path: string): Map<string, string> {
|
||||
return parseGitLsTree(spawnProcess('git', ['ls-tree', 'HEAD', '-r'], path));
|
||||
}
|
||||
|
||||
function gitStatus(
|
||||
path: string
|
||||
): { status: Map<string, string>; deletedFiles: string[] } {
|
||||
const deletedFiles: string[] = [];
|
||||
const filesToHash: string[] = [];
|
||||
parseGitStatus(
|
||||
spawnProcess('git', ['status', '-s', '-u', '.'], path)
|
||||
).forEach((changeType: string, filename: string) => {
|
||||
if (changeType !== 'D') {
|
||||
filesToHash.push(filename);
|
||||
} else {
|
||||
deletedFiles.push(filename);
|
||||
}
|
||||
});
|
||||
|
||||
const updated = checkForDeletedFiles(path, filesToHash, deletedFiles);
|
||||
const status = getGitHashForFiles(updated.filesToHash, path);
|
||||
return { deletedFiles: updated.deletedFiles, status };
|
||||
}
|
||||
|
||||
/**
|
||||
* This is only needed because of potential issues with interpreting "git status".
|
||||
* We had a few issues where we didn't interpret renames correctly. Even though
|
||||
* doing this somewhat slow, we will keep it for now.
|
||||
*
|
||||
* @vsavkin remove it in nx 10.2
|
||||
*/
|
||||
function checkForDeletedFiles(
|
||||
path: string,
|
||||
files: string[],
|
||||
deletedFiles: string[]
|
||||
) {
|
||||
let filesToHash = [];
|
||||
|
||||
files.forEach((f) => {
|
||||
try {
|
||||
statSync(join(path, f)).isFile();
|
||||
filesToHash.push(f);
|
||||
} catch (err) {
|
||||
console.warn(
|
||||
`Warning: Fell back to using 'fs' to identify ${f} as deleted. Please open an issue at https://github.com/nrwl/nx so we can investigate.`
|
||||
);
|
||||
deletedFiles.push(f);
|
||||
}
|
||||
});
|
||||
|
||||
return { filesToHash, deletedFiles };
|
||||
}
|
||||
|
||||
export function getFileHashes(path: string): Map<string, string> {
|
||||
const res = new Map<string, string>();
|
||||
|
||||
try {
|
||||
const { deletedFiles, status } = gitStatus(path);
|
||||
const m1 = gitLsTree(path);
|
||||
m1.forEach((hash: string, filename: string) => {
|
||||
if (deletedFiles.indexOf(filename) === -1) {
|
||||
res.set(`${path}/${filename}`, hash);
|
||||
}
|
||||
});
|
||||
status.forEach((hash: string, filename: string) => {
|
||||
res.set(`${path}/${filename}`, hash);
|
||||
});
|
||||
return res;
|
||||
} catch (e) {
|
||||
// this strategy is only used for speeding things up.
|
||||
// ignoring all the errors
|
||||
if (process.env.NX_GIT_HASHER_LOGGING) {
|
||||
console.error(`Internal error:`);
|
||||
console.error(e);
|
||||
}
|
||||
return new Map<string, string>();
|
||||
}
|
||||
}
|
||||
+84
-88
@@ -1,8 +1,7 @@
|
||||
import { Hasher, extractNameAndVersion } from './hasher';
|
||||
import { Hasher } from './hasher';
|
||||
import { extractNameAndVersion } from '@nrwl/workspace/src/core/hasher/file-hasher';
|
||||
|
||||
const hasha = require('hasha');
|
||||
const fs = require('fs');
|
||||
jest.mock('hasha');
|
||||
jest.mock('fs');
|
||||
|
||||
describe('Hasher', () => {
|
||||
@@ -14,14 +13,13 @@ describe('Hasher', () => {
|
||||
'tsconfig.json': 'tsconfig.json.hash',
|
||||
'workspace.json': 'workspace.json.hash',
|
||||
};
|
||||
beforeEach(() => {
|
||||
hasha.mockImplementation((values) => values.join('|'));
|
||||
hasha.fromFile.mockImplementation((path) => Promise.resolve(hashes[path]));
|
||||
fs.statSync.mockReturnValue({ size: 100 });
|
||||
fs.readFileSync.mockImplementation(() =>
|
||||
JSON.stringify({ dependencies: {}, devDependencies: {} })
|
||||
);
|
||||
});
|
||||
|
||||
function createHashing(): any {
|
||||
return {
|
||||
hashArray: (values: string[]) => values.join('|'),
|
||||
hashFile: (path: string) => hashes[path],
|
||||
};
|
||||
}
|
||||
|
||||
it('should create project hash', async (done) => {
|
||||
hashes['/file'] = 'file.hash';
|
||||
@@ -31,7 +29,7 @@ describe('Hasher', () => {
|
||||
proj: {
|
||||
name: 'proj',
|
||||
type: 'lib',
|
||||
data: { files: [{ file: '/file', ext: '.ts', mtime: 1 }] },
|
||||
data: { files: [{ file: '/file', ext: '.ts', hash: 'some-hash' }] },
|
||||
},
|
||||
},
|
||||
dependencies: {
|
||||
@@ -41,14 +39,19 @@ describe('Hasher', () => {
|
||||
{} as any,
|
||||
{
|
||||
runtimeCacheInputs: ['echo runtime123', 'echo runtime456'],
|
||||
}
|
||||
},
|
||||
createHashing()
|
||||
);
|
||||
|
||||
const hash = await hasher.hash({
|
||||
target: { project: 'proj', target: 'build' },
|
||||
id: 'proj-build',
|
||||
overrides: { prop: 'prop-value' },
|
||||
});
|
||||
const hash = (
|
||||
await hasher.hashTasks([
|
||||
{
|
||||
target: { project: 'proj', target: 'build' },
|
||||
id: 'proj-build',
|
||||
overrides: { prop: 'prop-value' },
|
||||
},
|
||||
])
|
||||
)[0];
|
||||
|
||||
expect(hash.value).toContain('yarn.lock.hash'); //implicits
|
||||
expect(hash.value).toContain('file.hash'); //project files
|
||||
@@ -60,7 +63,7 @@ describe('Hasher', () => {
|
||||
|
||||
expect(hash.details.command).toEqual('proj|build||{"prop":"prop-value"}');
|
||||
expect(hash.details.sources).toEqual({
|
||||
proj: 'file.hash',
|
||||
proj: '/file|file.hash',
|
||||
});
|
||||
expect(hash.details.implicitDeps).toEqual({
|
||||
'yarn.lock': 'yarn.lock.hash',
|
||||
@@ -87,15 +90,18 @@ describe('Hasher', () => {
|
||||
{} as any,
|
||||
{
|
||||
runtimeCacheInputs: ['boom'],
|
||||
}
|
||||
},
|
||||
createHashing()
|
||||
);
|
||||
|
||||
try {
|
||||
await hasher.hash({
|
||||
target: { project: 'proj', target: 'build' },
|
||||
id: 'proj-build',
|
||||
overrides: {},
|
||||
});
|
||||
await hasher.hashTasks([
|
||||
{
|
||||
target: { project: 'proj', target: 'build' },
|
||||
id: 'proj-build',
|
||||
overrides: {},
|
||||
},
|
||||
]);
|
||||
fail('Should not be here');
|
||||
} catch (e) {
|
||||
expect(e.message).toContain(
|
||||
@@ -115,12 +121,16 @@ describe('Hasher', () => {
|
||||
parent: {
|
||||
name: 'parent',
|
||||
type: 'lib',
|
||||
data: { files: [{ file: '/filea', ext: '.ts', mtime: 1 }] },
|
||||
data: {
|
||||
files: [{ file: '/filea', ext: '.ts', hash: 'some-hash' }],
|
||||
},
|
||||
},
|
||||
child: {
|
||||
name: 'child',
|
||||
type: 'lib',
|
||||
data: { files: [{ file: '/fileb', ext: '.ts', mtime: 1 }] },
|
||||
data: {
|
||||
files: [{ file: '/fileb', ext: '.ts', hash: 'some-hash' }],
|
||||
},
|
||||
},
|
||||
},
|
||||
dependencies: {
|
||||
@@ -128,19 +138,24 @@ describe('Hasher', () => {
|
||||
},
|
||||
},
|
||||
{} as any,
|
||||
{}
|
||||
{},
|
||||
createHashing()
|
||||
);
|
||||
|
||||
const hasha = await hasher.hash({
|
||||
target: { project: 'parent', target: 'build' },
|
||||
id: 'parent-build',
|
||||
overrides: { prop: 'prop-value' },
|
||||
});
|
||||
const hash = (
|
||||
await hasher.hashTasks([
|
||||
{
|
||||
target: { project: 'parent', target: 'build' },
|
||||
id: 'parent-build',
|
||||
overrides: { prop: 'prop-value' },
|
||||
},
|
||||
])
|
||||
)[0];
|
||||
|
||||
// note that the parent hash is based on parent source files only!
|
||||
expect(hasha.details.sources).toEqual({
|
||||
parent: 'a.hash',
|
||||
child: 'b.hash',
|
||||
expect(hash.details.sources).toEqual({
|
||||
parent: '/filea|a.hash',
|
||||
child: '/fileb|b.hash',
|
||||
});
|
||||
|
||||
done();
|
||||
@@ -155,12 +170,16 @@ describe('Hasher', () => {
|
||||
proja: {
|
||||
name: 'proja',
|
||||
type: 'lib',
|
||||
data: { files: [{ file: '/filea', ext: '.ts', mtime: 1 }] },
|
||||
data: {
|
||||
files: [{ file: '/filea', ext: '.ts', hash: 'some-hash' }],
|
||||
},
|
||||
},
|
||||
projb: {
|
||||
name: 'projb',
|
||||
type: 'lib',
|
||||
data: { files: [{ file: '/fileb', ext: '.ts', mtime: 1 }] },
|
||||
data: {
|
||||
files: [{ file: '/fileb', ext: '.ts', hash: 'some-hash' }],
|
||||
},
|
||||
},
|
||||
},
|
||||
dependencies: {
|
||||
@@ -169,14 +188,19 @@ describe('Hasher', () => {
|
||||
},
|
||||
},
|
||||
{} as any,
|
||||
{}
|
||||
{},
|
||||
createHashing()
|
||||
);
|
||||
|
||||
const hasha = await hasher.hash({
|
||||
target: { project: 'proja', target: 'build' },
|
||||
id: 'proja-build',
|
||||
overrides: { prop: 'prop-value' },
|
||||
});
|
||||
const hasha = (
|
||||
await hasher.hashTasks([
|
||||
{
|
||||
target: { project: 'proja', target: 'build' },
|
||||
id: 'proja-build',
|
||||
overrides: { prop: 'prop-value' },
|
||||
},
|
||||
])
|
||||
)[0];
|
||||
|
||||
expect(hasha.value).toContain('yarn.lock.hash'); //implicits
|
||||
expect(hasha.value).toContain('a.hash'); //project files
|
||||
@@ -184,59 +208,31 @@ describe('Hasher', () => {
|
||||
expect(hasha.value).toContain('prop-value'); //overrides
|
||||
expect(hasha.value).toContain('proj'); //project
|
||||
expect(hasha.value).toContain('build'); //target
|
||||
expect(hasha.details.sources).toEqual({ proja: 'a.hash', projb: 'b.hash' });
|
||||
|
||||
const hashb = await hasher.hash({
|
||||
target: { project: 'projb', target: 'build' },
|
||||
id: 'projb-build',
|
||||
overrides: { prop: 'prop-value' },
|
||||
expect(hasha.details.sources).toEqual({
|
||||
proja: '/filea|a.hash',
|
||||
projb: '/fileb|b.hash',
|
||||
});
|
||||
|
||||
const hashb = (
|
||||
await hasher.hashTasks([
|
||||
{
|
||||
target: { project: 'projb', target: 'build' },
|
||||
id: 'projb-build',
|
||||
overrides: { prop: 'prop-value' },
|
||||
},
|
||||
])
|
||||
)[0];
|
||||
|
||||
expect(hashb.value).toContain('yarn.lock.hash'); //implicits
|
||||
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('build'); //target
|
||||
expect(hashb.details.sources).toEqual({ proja: 'a.hash', projb: 'b.hash' });
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
it('should handle large binary files in a special way', async (done) => {
|
||||
fs.statSync.mockImplementation((f) => {
|
||||
if (f === '/file') return { size: 1000000 * 5 + 1 };
|
||||
return { size: 100 };
|
||||
expect(hashb.details.sources).toEqual({
|
||||
proja: '/filea|a.hash',
|
||||
projb: '/fileb|b.hash',
|
||||
});
|
||||
hashes['/file'] = 'file.hash';
|
||||
const hasher = new Hasher(
|
||||
{
|
||||
nodes: {
|
||||
proja: {
|
||||
name: 'proj',
|
||||
type: 'lib',
|
||||
data: { files: [{ file: '/file', ext: '.ts', mtime: 1 }] },
|
||||
},
|
||||
},
|
||||
dependencies: {},
|
||||
},
|
||||
{} as any,
|
||||
{}
|
||||
);
|
||||
|
||||
const hash = (
|
||||
await hasher.hash({
|
||||
target: { project: 'proja', target: 'build' },
|
||||
id: 'proja-build',
|
||||
overrides: { prop: 'prop-value' },
|
||||
})
|
||||
).value;
|
||||
|
||||
expect(hash).toContain('yarn.lock.hash'); //implicits
|
||||
expect(hash).toContain('5000001'); //project files
|
||||
expect(hash).toContain('prop-value'); //overrides
|
||||
expect(hash).toContain('proj'); //project
|
||||
expect(hash).toContain('build'); //target
|
||||
|
||||
done();
|
||||
});
|
||||
+75
-144
@@ -1,11 +1,17 @@
|
||||
import { ProjectGraph } from '../core/project-graph';
|
||||
import { NxJson } from '../core/shared-interfaces';
|
||||
import { Task } from './tasks-runner';
|
||||
import { statSync, readFileSync } from 'fs';
|
||||
import { rootWorkspaceFileNames } from '../core/file-utils';
|
||||
import { ProjectGraph } from '../project-graph';
|
||||
import { NxJson } from '../shared-interfaces';
|
||||
import { Task } from '../../tasks-runner/tasks-runner';
|
||||
import { readFileSync } from 'fs';
|
||||
import { rootWorkspaceFileNames } from '../file-utils';
|
||||
import { execSync } from 'child_process';
|
||||
import {
|
||||
defaultFileHasher,
|
||||
extractNameAndVersion,
|
||||
FileHasher,
|
||||
} from './file-hasher';
|
||||
import { defaultHashing, HashingImp } from './hashing-impl';
|
||||
|
||||
const resolve = require('resolve');
|
||||
const hasha = require('hasha');
|
||||
|
||||
export interface Hash {
|
||||
value: string;
|
||||
@@ -38,28 +44,44 @@ interface NodeModulesResult {
|
||||
|
||||
export class Hasher {
|
||||
static version = '1.0';
|
||||
implicitDependencies: Promise<ImplicitHashResult>;
|
||||
nodeModules: Promise<NodeModulesResult>;
|
||||
runtimeInputs: Promise<RuntimeHashResult>;
|
||||
fileHashes = new FileHashes();
|
||||
projectHashes = new ProjectHashes(this.projectGraph, this.fileHashes);
|
||||
private implicitDependencies: Promise<ImplicitHashResult>;
|
||||
private nodeModules: Promise<NodeModulesResult>;
|
||||
private runtimeInputs: Promise<RuntimeHashResult>;
|
||||
private fileHasher: FileHasher;
|
||||
private projectHashes: ProjectHasher;
|
||||
private hashing: HashingImp;
|
||||
|
||||
constructor(
|
||||
private readonly projectGraph: ProjectGraph,
|
||||
private readonly nxJson: NxJson,
|
||||
private readonly options: any
|
||||
) {}
|
||||
|
||||
async hash(task: Task): Promise<Hash> {
|
||||
const command = hasha(
|
||||
[
|
||||
task.target.project || '',
|
||||
task.target.target || '',
|
||||
task.target.configuration || '',
|
||||
JSON.stringify(task.overrides),
|
||||
],
|
||||
{ algorithm: 'sha256' }
|
||||
private readonly options: any,
|
||||
hashing: HashingImp = undefined
|
||||
) {
|
||||
if (!hashing) {
|
||||
this.hashing = defaultHashing;
|
||||
this.fileHasher = defaultFileHasher;
|
||||
} else {
|
||||
this.hashing = hashing;
|
||||
this.fileHasher = new FileHasher(hashing);
|
||||
}
|
||||
this.projectHashes = new ProjectHasher(
|
||||
this.projectGraph,
|
||||
this.fileHasher,
|
||||
this.hashing
|
||||
);
|
||||
}
|
||||
|
||||
async hashTasks(tasks: Task[]): Promise<Hash[]> {
|
||||
return Promise.all(tasks.map((t) => this.hash(t)));
|
||||
}
|
||||
|
||||
private async hash(task: Task): Promise<Hash> {
|
||||
const command = this.hashing.hashArray([
|
||||
task.target.project || '',
|
||||
task.target.target || '',
|
||||
task.target.configuration || '',
|
||||
JSON.stringify(task.overrides),
|
||||
]);
|
||||
|
||||
const values = (await Promise.all([
|
||||
this.projectHashes.hashProject(task.target.project, [
|
||||
@@ -75,12 +97,11 @@ export class Hasher {
|
||||
NodeModulesResult
|
||||
];
|
||||
|
||||
const value = hasha(
|
||||
[Hasher.version, command, ...values.map((v) => v.value)],
|
||||
{
|
||||
algorithm: 'sha256',
|
||||
}
|
||||
);
|
||||
const value = this.hashing.hashArray([
|
||||
Hasher.version,
|
||||
command,
|
||||
...values.map((v) => v.value),
|
||||
]);
|
||||
|
||||
return {
|
||||
value,
|
||||
@@ -109,12 +130,7 @@ export class Hasher {
|
||||
})
|
||||
)) as any;
|
||||
|
||||
const value = await hasha(
|
||||
values.map((v) => v.value),
|
||||
{
|
||||
algorithm: 'sha256',
|
||||
}
|
||||
);
|
||||
const value = this.hashing.hashArray(values.map((v) => v.value));
|
||||
const runtime = values.reduce(
|
||||
(m, c) => ((m[c.input] = c.value), m),
|
||||
{}
|
||||
@@ -143,18 +159,12 @@ export class Hasher {
|
||||
];
|
||||
|
||||
this.implicitDependencies = Promise.resolve().then(async () => {
|
||||
const fileHashes = await Promise.all(
|
||||
fileNames.map(async (file) => {
|
||||
const hash = await this.fileHashes.hashFile(file);
|
||||
return { file, hash };
|
||||
})
|
||||
);
|
||||
|
||||
const combinedHash = await hasha(
|
||||
fileHashes.map((v) => v.hash),
|
||||
{
|
||||
algorithm: 'sha256',
|
||||
}
|
||||
const fileHashes = fileNames.map((file) => {
|
||||
const hash = this.fileHasher.hashFile(file);
|
||||
return { file, hash };
|
||||
});
|
||||
const combinedHash = this.hashing.hashArray(
|
||||
fileHashes.map((v) => v.hash)
|
||||
);
|
||||
return {
|
||||
value: combinedHash,
|
||||
@@ -174,21 +184,17 @@ export class Hasher {
|
||||
...Object.keys(j.dependencies),
|
||||
...Object.keys(j.devDependencies),
|
||||
];
|
||||
const packageJsonHashes = await Promise.all(
|
||||
allPackages.map((d) => {
|
||||
try {
|
||||
const path = resolve.sync(`${d}/package.json`, {
|
||||
basedir: process.cwd(),
|
||||
});
|
||||
return this.fileHashes
|
||||
.hashFile(path, extractNameAndVersion)
|
||||
.catch(() => '');
|
||||
} catch (e) {
|
||||
return '';
|
||||
}
|
||||
})
|
||||
);
|
||||
return { value: await hasha(packageJsonHashes) };
|
||||
const packageJsonHashes = allPackages.map((d) => {
|
||||
try {
|
||||
const path = resolve.sync(`${d}/package.json`, {
|
||||
basedir: process.cwd(),
|
||||
});
|
||||
return this.fileHasher.hashFile(path, extractNameAndVersion);
|
||||
} catch (e) {
|
||||
return '';
|
||||
}
|
||||
});
|
||||
return { value: this.hashing.hashArray(packageJsonHashes) };
|
||||
} catch (e) {
|
||||
return { value: '' };
|
||||
}
|
||||
@@ -198,12 +204,13 @@ export class Hasher {
|
||||
}
|
||||
}
|
||||
|
||||
export class ProjectHashes {
|
||||
class ProjectHasher {
|
||||
private sourceHashes: { [projectName: string]: Promise<string> } = {};
|
||||
|
||||
constructor(
|
||||
private readonly projectGraph: ProjectGraph,
|
||||
private readonly fileHashes: FileHashes
|
||||
private readonly fileHasher: FileHasher,
|
||||
private readonly hashing: HashingImp
|
||||
) {}
|
||||
|
||||
async hashProject(
|
||||
@@ -231,7 +238,7 @@ export class ProjectHashes {
|
||||
},
|
||||
{ [projectName]: projectHash }
|
||||
);
|
||||
const value = await hasha([
|
||||
const value = this.hashing.hashArray([
|
||||
...depHashes.map((d) => d.value),
|
||||
projectHash,
|
||||
]);
|
||||
@@ -243,89 +250,13 @@ export class ProjectHashes {
|
||||
if (!this.sourceHashes[projectName]) {
|
||||
this.sourceHashes[projectName] = new Promise(async (res) => {
|
||||
const p = this.projectGraph.nodes[projectName];
|
||||
const fileNames = p.data.files.map((f) => f.file);
|
||||
const values = await Promise.all(
|
||||
p.data.files.map((f) => this.fileHashes.hashFile(f.file))
|
||||
fileNames.map((f) => this.fileHasher.hashFile(f))
|
||||
);
|
||||
res(hasha(values, { algorithm: 'sha256' }));
|
||||
res(this.hashing.hashArray([...fileNames, ...values]));
|
||||
});
|
||||
}
|
||||
return this.sourceHashes[projectName];
|
||||
}
|
||||
}
|
||||
|
||||
export function extractNameAndVersion(content: string): string {
|
||||
try {
|
||||
const c = JSON.parse(content);
|
||||
return `${c.name}${c.version}`;
|
||||
} catch (e) {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
type PathAndTransformer = {
|
||||
path: string;
|
||||
transformer: (x: string) => string | null;
|
||||
};
|
||||
|
||||
export class FileHashes {
|
||||
private queue = [] as PathAndTransformer[];
|
||||
private numberOfConcurrentReads = 0;
|
||||
private fileHashes: { [path: string]: Promise<string> } = {};
|
||||
private resolvers: { [path: string]: Function } = {};
|
||||
|
||||
async hashFile(
|
||||
path: string,
|
||||
transformer: (x: string) => string | null = null
|
||||
) {
|
||||
if (!this.fileHashes[path]) {
|
||||
this.fileHashes[path] = new Promise((res) => {
|
||||
this.resolvers[path] = res;
|
||||
this.pushFileIntoQueue({ path, transformer });
|
||||
});
|
||||
}
|
||||
return this.fileHashes[path];
|
||||
}
|
||||
|
||||
private pushFileIntoQueue(pathAndTransformer: PathAndTransformer) {
|
||||
this.queue.push(pathAndTransformer);
|
||||
if (this.numberOfConcurrentReads < 2000) {
|
||||
this.numberOfConcurrentReads++;
|
||||
this.takeFromQueue();
|
||||
}
|
||||
}
|
||||
|
||||
private takeFromQueue() {
|
||||
if (this.queue.length > 0) {
|
||||
const pathAndTransformer = this.queue.pop();
|
||||
this.processPath(pathAndTransformer)
|
||||
.then((value) => {
|
||||
this.resolvers[pathAndTransformer.path](value);
|
||||
})
|
||||
.then(() => this.takeFromQueue());
|
||||
} else {
|
||||
this.numberOfConcurrentReads--;
|
||||
}
|
||||
}
|
||||
|
||||
private processPath(pathAndTransformer: PathAndTransformer) {
|
||||
try {
|
||||
const stats = statSync(pathAndTransformer.path);
|
||||
const fileSizeInMegabytes = stats.size / 1000000;
|
||||
// large binary file, skip it
|
||||
if (fileSizeInMegabytes > 5) {
|
||||
return Promise.resolve(stats.size.toString());
|
||||
} else if (pathAndTransformer.transformer) {
|
||||
const transformedFile = pathAndTransformer.transformer(
|
||||
readFileSync(pathAndTransformer.path).toString()
|
||||
);
|
||||
return Promise.resolve('').then(() =>
|
||||
hasha([transformedFile], { algorithm: 'sha256' })
|
||||
);
|
||||
} else {
|
||||
return hasha.fromFile(pathAndTransformer.path, { algorithm: 'sha256' });
|
||||
}
|
||||
} catch (e) {
|
||||
return Promise.resolve('');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import * as crypto from 'crypto';
|
||||
import { readFileSync } from 'fs';
|
||||
|
||||
export class HashingImp {
|
||||
hashArray(input: string[]): string {
|
||||
const hasher = crypto.createHash('sha256');
|
||||
for (const part of input) {
|
||||
hasher.update(part);
|
||||
}
|
||||
const hash = hasher.digest().buffer;
|
||||
return Buffer.from(hash).toString('hex');
|
||||
}
|
||||
|
||||
hashFile(path: string): string {
|
||||
const hasher = crypto.createHash('sha256');
|
||||
const file = readFileSync(path);
|
||||
hasher.update(file);
|
||||
const hash = hasher.digest().buffer;
|
||||
return Buffer.from(hash).toString('hex');
|
||||
}
|
||||
}
|
||||
|
||||
export const defaultHashing = new HashingImp();
|
||||
@@ -0,0 +1,121 @@
|
||||
import { FileData, filesChanged } from '../file-utils';
|
||||
import {
|
||||
ProjectGraph,
|
||||
ProjectGraphDependency,
|
||||
ProjectGraphNode,
|
||||
} from '../project-graph';
|
||||
import { join } from 'path';
|
||||
import { appRootPath } from '../../utils/app-root';
|
||||
import { existsSync } from 'fs';
|
||||
import * as fsExtra from 'fs-extra';
|
||||
import {
|
||||
directoryExists,
|
||||
fileExists,
|
||||
readJsonFile,
|
||||
writeJsonFile,
|
||||
} from '../../utils/fileutils';
|
||||
import { FileMap } from '@nrwl/workspace/src/core/file-graph';
|
||||
import { performance } from 'perf_hooks';
|
||||
|
||||
export interface ProjectGraphCache {
|
||||
version: string;
|
||||
rootFiles: FileData[];
|
||||
nodes: Record<string, ProjectGraphNode>;
|
||||
dependencies: Record<string, ProjectGraphDependency[]>;
|
||||
}
|
||||
|
||||
const nxDepsDir = join(appRootPath, 'node_modules', '.cache', 'nx');
|
||||
const nxDepsPath = join(nxDepsDir, 'nxdeps.json');
|
||||
export function readCache(): false | ProjectGraphCache {
|
||||
performance.mark('read cache:start');
|
||||
try {
|
||||
if (!existsSync(nxDepsDir)) {
|
||||
fsExtra.ensureDirSync(nxDepsDir);
|
||||
}
|
||||
} catch (e) {
|
||||
/*
|
||||
* @jeffbcross: Node JS docs recommend against checking for existence of directory immediately before creating it.
|
||||
* Instead, just try to create the directory and handle the error.
|
||||
*
|
||||
* We ran into race conditions when running scripts concurrently, where multiple scripts were
|
||||
* arriving here simultaneously, checking for directory existence, then trying to create the directory simultaneously.
|
||||
*
|
||||
* In this case, we're creating the directory. If the operation failed, we ensure that the directory
|
||||
* exists before continuing (or raise an exception).
|
||||
*/
|
||||
if (!directoryExists(nxDepsDir)) {
|
||||
throw new Error(`Failed to create directory: ${nxDepsDir}`);
|
||||
}
|
||||
}
|
||||
|
||||
const data = fileExists(nxDepsPath) ? readJsonFile(nxDepsPath) : null;
|
||||
|
||||
performance.mark('read cache:end');
|
||||
performance.measure('read cache', 'read cache:start', 'read cache:end');
|
||||
return data ? data : false;
|
||||
}
|
||||
|
||||
export function writeCache(
|
||||
rootFiles: FileData[],
|
||||
projectGraph: ProjectGraph
|
||||
): void {
|
||||
performance.mark('write cache:start');
|
||||
writeJsonFile(nxDepsPath, {
|
||||
version: '2.0',
|
||||
rootFiles,
|
||||
nodes: projectGraph.nodes,
|
||||
dependencies: projectGraph.dependencies,
|
||||
});
|
||||
performance.mark('write cache:end');
|
||||
performance.measure('write cache', 'write cache:start', 'write cache:end');
|
||||
}
|
||||
|
||||
export function differentFromCache(
|
||||
fileMap: FileMap,
|
||||
c: ProjectGraphCache
|
||||
): {
|
||||
noDifference: boolean;
|
||||
filesDifferentFromCache: FileMap;
|
||||
partiallyConstructedProjectGraph?: ProjectGraph;
|
||||
} {
|
||||
const currentProjects = Object.keys(fileMap).sort();
|
||||
const previousProjects = Object.keys(c.nodes)
|
||||
.sort()
|
||||
.filter((name) => c.nodes[name].data.files.length > 0);
|
||||
|
||||
// Projects changed -> compute entire graph
|
||||
if (
|
||||
currentProjects.length !== previousProjects.length ||
|
||||
currentProjects.some((val, idx) => val !== previousProjects[idx])
|
||||
) {
|
||||
return {
|
||||
filesDifferentFromCache: fileMap,
|
||||
partiallyConstructedProjectGraph: null,
|
||||
noDifference: false,
|
||||
};
|
||||
}
|
||||
|
||||
// Projects are same -> compute projects with file changes
|
||||
const filesDifferentFromCache: FileMap = {};
|
||||
currentProjects.forEach((p) => {
|
||||
if (filesChanged(c.nodes[p].data.files, fileMap[p])) {
|
||||
filesDifferentFromCache[p] = fileMap[p];
|
||||
}
|
||||
});
|
||||
|
||||
// Re-compute nodes and dependencies for each project in file map.
|
||||
Object.keys(filesDifferentFromCache).forEach((key) => {
|
||||
delete c.dependencies[key];
|
||||
});
|
||||
|
||||
const partiallyConstructedProjectGraph = {
|
||||
nodes: c.nodes,
|
||||
dependencies: c.dependencies,
|
||||
};
|
||||
|
||||
return {
|
||||
filesDifferentFromCache: filesDifferentFromCache,
|
||||
partiallyConstructedProjectGraph,
|
||||
noDifference: Object.keys(filesDifferentFromCache).length === 0,
|
||||
};
|
||||
}
|
||||
@@ -10,6 +10,7 @@ export function buildWorkspaceProjectNodes(
|
||||
Object.keys(ctx.fileMap).forEach((key) => {
|
||||
const p = ctx.workspaceJson.projects[key];
|
||||
|
||||
// TODO, types and projectType should allign
|
||||
const projectType =
|
||||
p.projectType === 'application'
|
||||
? key.endsWith('-e2e')
|
||||
|
||||
@@ -173,6 +173,65 @@ describe('withDeps', () => {
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
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 = [{ 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', () => {
|
||||
|
||||
@@ -80,22 +80,35 @@ export function withDeps(
|
||||
subsetNodes: ProjectGraphNode[]
|
||||
): ProjectGraph {
|
||||
const builder = new ProjectGraphBuilder();
|
||||
Object.values(subsetNodes).forEach(recur);
|
||||
const visitedNodes = [];
|
||||
const visitedEdges = [];
|
||||
Object.values(subsetNodes).forEach(recurNodes);
|
||||
Object.values(subsetNodes).forEach(recurEdges);
|
||||
return builder.build();
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function recur(node) {
|
||||
const ds = original.dependencies[node.name];
|
||||
// 1. Recursively add all source nodes
|
||||
ds.forEach((n) => {
|
||||
recur(original.nodes[n.target]);
|
||||
});
|
||||
// 2. Add current node
|
||||
function recurNodes(node) {
|
||||
if (visitedNodes.indexOf(node.name) > -1) return;
|
||||
builder.addNode(node);
|
||||
// 3. Add all source dependencies
|
||||
visitedNodes.push(node.name);
|
||||
|
||||
original.dependencies[node.name].forEach((n) => {
|
||||
recurNodes(original.nodes[n.target]);
|
||||
});
|
||||
}
|
||||
|
||||
function recurEdges(node) {
|
||||
if (visitedEdges.indexOf(node.name) > -1) return;
|
||||
visitedEdges.push(node.name);
|
||||
|
||||
const ds = original.dependencies[node.name];
|
||||
ds.forEach((n) => {
|
||||
builder.addDependency(n.type, n.source, n.target);
|
||||
});
|
||||
|
||||
ds.forEach((n) => {
|
||||
recurEdges(original.nodes[n.target]);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import { vol, fs } from 'memfs';
|
||||
jest.mock('fs', () => require('memfs').fs);
|
||||
jest.mock('../../utils/app-root', () => ({ appRootPath: '/root' }));
|
||||
|
||||
import { stripIndents } from '@angular-devkit/core/src/utils/literals';
|
||||
import { createProjectGraph } from './project-graph';
|
||||
import { DependencyType } from './project-graph-models';
|
||||
import { NxJson } from '../shared-interfaces';
|
||||
|
||||
jest.mock('fs', () => require('memfs').fs);
|
||||
jest.mock('../../utils/app-root', () => ({ appRootPath: '/root' }));
|
||||
import { defaultFileHasher } from '@nrwl/workspace/src/core/hasher/file-hasher';
|
||||
|
||||
describe('project graph', () => {
|
||||
let packageJson: any;
|
||||
@@ -198,6 +199,9 @@ describe('project graph', () => {
|
||||
//wait a tick to ensure the modified time of workspace.json will be after the creation of the project graph file
|
||||
await new Promise((resolve) => setTimeout(resolve, 1));
|
||||
fs.writeFileSync('/root/workspace.json', JSON.stringify(workspaceJson));
|
||||
|
||||
defaultFileHasher.init();
|
||||
|
||||
graph = createProjectGraph();
|
||||
expect(graph.nodes).toMatchObject({
|
||||
demo: { name: 'demo', type: 'lib' },
|
||||
|
||||
@@ -1,20 +1,12 @@
|
||||
import { mkdirSync } from 'fs';
|
||||
import { appRootPath } from '../../utils/app-root';
|
||||
import {
|
||||
directoryExists,
|
||||
fileExists,
|
||||
readJsonFile,
|
||||
writeJsonFile,
|
||||
} from '../../utils/fileutils';
|
||||
import { assertWorkspaceValidity } from '../assert-workspace-validity';
|
||||
import { createFileMap, FileMap } from '../file-graph';
|
||||
import {
|
||||
defaultFileRead,
|
||||
FileData,
|
||||
mtime,
|
||||
filesChanged,
|
||||
readNxJson,
|
||||
readWorkspaceFiles,
|
||||
readWorkspaceJson,
|
||||
rootWorkspaceFileData,
|
||||
} from '../file-utils';
|
||||
import { normalizeNxJson } from '../normalize-nx-json';
|
||||
import {
|
||||
@@ -30,188 +22,89 @@ import {
|
||||
} from './build-nodes';
|
||||
import { ProjectGraphBuilder } from './project-graph-builder';
|
||||
import { ProjectGraph } from './project-graph-models';
|
||||
|
||||
/**
|
||||
* This version is stored in the project graph cache to determine if it can be reused.
|
||||
*/
|
||||
const projectGraphCacheVersion = '1';
|
||||
import {
|
||||
differentFromCache,
|
||||
ProjectGraphCache,
|
||||
readCache,
|
||||
writeCache,
|
||||
} from '../nx-deps/nx-deps-cache';
|
||||
import { NxJson } from '../shared-interfaces';
|
||||
import { performance } from 'perf_hooks';
|
||||
|
||||
export function createProjectGraph(
|
||||
workspaceJson = readWorkspaceJson(),
|
||||
nxJson = readNxJson(),
|
||||
workspaceFiles = readWorkspaceFiles(),
|
||||
fileRead: (s: string) => string = defaultFileRead,
|
||||
cache: false | { data: ProjectGraphCache; mtime: number } = readCache(),
|
||||
cache: false | ProjectGraphCache = readCache(),
|
||||
shouldCache: boolean = true
|
||||
): ProjectGraph {
|
||||
assertWorkspaceValidity(workspaceJson, nxJson);
|
||||
|
||||
const normalizedNxJson = normalizeNxJson(nxJson);
|
||||
if (cache && maxMTime(rootWorkspaceFileData(workspaceFiles)) > cache.mtime) {
|
||||
cache = false;
|
||||
}
|
||||
|
||||
if (!cache || maxMTime(workspaceFiles) > cache.mtime) {
|
||||
const fileMap = createFileMap(workspaceJson, workspaceFiles);
|
||||
const incremental = modifiedSinceCache(fileMap, cache);
|
||||
const rootFiles = rootWorkspaceFileData();
|
||||
const fileMap = createFileMap(workspaceJson, workspaceFiles);
|
||||
|
||||
if (cache && !filesChanged(rootFiles, cache.rootFiles)) {
|
||||
const diff = differentFromCache(fileMap, cache);
|
||||
if (diff.noDifference) {
|
||||
return diff.partiallyConstructedProjectGraph;
|
||||
}
|
||||
|
||||
const ctx = {
|
||||
workspaceJson,
|
||||
nxJson: normalizedNxJson,
|
||||
fileMap: incremental.fileMap,
|
||||
fileMap: diff.filesDifferentFromCache,
|
||||
};
|
||||
const builder = new ProjectGraphBuilder(incremental.projectGraph);
|
||||
const buildNodesFns: BuildNodes[] = [
|
||||
buildWorkspaceProjectNodes,
|
||||
buildNpmPackageNodes,
|
||||
];
|
||||
const buildDependenciesFns: BuildDependencies[] = [
|
||||
buildExplicitTypeScriptDependencies,
|
||||
buildImplicitProjectDependencies,
|
||||
buildExplicitNpmDependencies,
|
||||
];
|
||||
|
||||
buildNodesFns.forEach((f) =>
|
||||
f(ctx, builder.addNode.bind(builder), fileRead)
|
||||
const projectGraph = buildProjectGraph(
|
||||
ctx,
|
||||
fileRead,
|
||||
diff.partiallyConstructedProjectGraph
|
||||
);
|
||||
|
||||
buildDependenciesFns.forEach((f) =>
|
||||
f(ctx, builder.nodes, builder.addDependency.bind(builder), fileRead)
|
||||
);
|
||||
|
||||
const projectGraph = builder.build();
|
||||
if (shouldCache) {
|
||||
writeCache({
|
||||
version: projectGraphCacheVersion,
|
||||
projectGraph,
|
||||
fileMap,
|
||||
});
|
||||
writeCache(rootFiles, projectGraph);
|
||||
}
|
||||
return projectGraph;
|
||||
} else {
|
||||
// Cache file was modified _after_ all workspace files.
|
||||
// Safe to return the cached graph.
|
||||
return cache.data.projectGraph;
|
||||
}
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
interface ProjectGraphCache {
|
||||
version: string;
|
||||
projectGraph: ProjectGraph;
|
||||
fileMap: FileMap;
|
||||
}
|
||||
|
||||
const distPath = `${appRootPath}/dist`;
|
||||
const nxDepsPath = `${distPath}/nxdeps.json`;
|
||||
|
||||
function readCache(): false | { data: ProjectGraphCache; mtime: number } {
|
||||
try {
|
||||
mkdirSync(distPath);
|
||||
} catch (e) {
|
||||
/*
|
||||
* @jeffbcross: Node JS docs recommend against checking for existence of directory immediately before creating it.
|
||||
* Instead, just try to create the directory and handle the error.
|
||||
*
|
||||
* We ran into race conditions when running scripts concurrently, where multiple scripts were
|
||||
* arriving here simultaneously, checking for directory existence, then trying to create the directory simultaneously.
|
||||
*
|
||||
* In this case, we're creating the directory. If the operation failed, we ensure that the directory
|
||||
* exists before continuing (or raise an exception).
|
||||
*/
|
||||
if (!directoryExists(distPath)) {
|
||||
throw new Error(`Failed to create directory: ${distPath}`);
|
||||
const ctx = {
|
||||
workspaceJson,
|
||||
nxJson: normalizedNxJson,
|
||||
fileMap: fileMap,
|
||||
};
|
||||
const projectGraph = buildProjectGraph(ctx, fileRead, null);
|
||||
if (shouldCache) {
|
||||
writeCache(rootFiles, projectGraph);
|
||||
}
|
||||
return projectGraph;
|
||||
}
|
||||
}
|
||||
|
||||
const data = getValidCache(
|
||||
fileExists(nxDepsPath) ? readJsonFile(nxDepsPath) : null
|
||||
function buildProjectGraph(
|
||||
ctx: { nxJson: NxJson<string[]>; workspaceJson: any; fileMap: FileMap },
|
||||
fileRead: (s: string) => string,
|
||||
projectGraph: ProjectGraph
|
||||
) {
|
||||
performance.mark('build project graph:start');
|
||||
const builder = new ProjectGraphBuilder(projectGraph);
|
||||
const buildNodesFns: BuildNodes[] = [
|
||||
buildWorkspaceProjectNodes,
|
||||
buildNpmPackageNodes,
|
||||
];
|
||||
const buildDependenciesFns: BuildDependencies[] = [
|
||||
buildExplicitTypeScriptDependencies,
|
||||
buildImplicitProjectDependencies,
|
||||
buildExplicitNpmDependencies,
|
||||
];
|
||||
buildNodesFns.forEach((f) => f(ctx, builder.addNode.bind(builder), fileRead));
|
||||
buildDependenciesFns.forEach((f) =>
|
||||
f(ctx, builder.nodes, builder.addDependency.bind(builder), fileRead)
|
||||
);
|
||||
|
||||
return data ? { data, mtime: mtime(nxDepsPath) } : false;
|
||||
}
|
||||
|
||||
function getValidCache(cache: ProjectGraphCache | null) {
|
||||
if (!cache) {
|
||||
return null;
|
||||
}
|
||||
if (
|
||||
cache.projectGraph &&
|
||||
cache.fileMap &&
|
||||
cache.version &&
|
||||
cache.version === projectGraphCacheVersion
|
||||
) {
|
||||
return cache;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function writeCache(cache: ProjectGraphCache): void {
|
||||
writeJsonFile(nxDepsPath, cache);
|
||||
}
|
||||
|
||||
function maxMTime(files: FileData[]) {
|
||||
return Math.max(...files.map((f) => f.mtime));
|
||||
}
|
||||
|
||||
function rootWorkspaceFileData(workspaceFiles: FileData[]): FileData[] {
|
||||
return [
|
||||
`package.json`,
|
||||
'workspace.json',
|
||||
'angular.json',
|
||||
`nx.json`,
|
||||
`tsconfig.json`,
|
||||
].reduce((acc: FileData[], curr: string) => {
|
||||
const fileData = workspaceFiles.find((x) => x.file === curr);
|
||||
if (fileData) {
|
||||
acc.push(fileData);
|
||||
}
|
||||
return acc;
|
||||
}, []);
|
||||
}
|
||||
|
||||
function modifiedSinceCache(
|
||||
fileMap: FileMap,
|
||||
c: false | { data: ProjectGraphCache; mtime: number }
|
||||
): { fileMap: FileMap; projectGraph?: ProjectGraph } {
|
||||
// No cache -> compute entire graph
|
||||
if (!c) {
|
||||
return { fileMap };
|
||||
}
|
||||
|
||||
const cachedFileMap = c.data.fileMap;
|
||||
const currentProjects = Object.keys(fileMap).sort();
|
||||
const previousProjects = Object.keys(cachedFileMap).sort();
|
||||
|
||||
// Projects changed -> compute entire graph
|
||||
if (
|
||||
currentProjects.length !== previousProjects.length ||
|
||||
currentProjects.some((val, idx) => val !== previousProjects[idx])
|
||||
) {
|
||||
return { fileMap };
|
||||
}
|
||||
|
||||
// Projects are same -> compute projects with file changes
|
||||
const modifiedSince: FileMap = {};
|
||||
currentProjects.forEach((p) => {
|
||||
let projectFilesChanged = false;
|
||||
for (const f of fileMap[p]) {
|
||||
const fromCache = cachedFileMap[p].find((x) => x.file === f.file);
|
||||
if (!fromCache || f.mtime > fromCache.mtime) {
|
||||
projectFilesChanged = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (projectFilesChanged) {
|
||||
modifiedSince[p] = fileMap[p];
|
||||
}
|
||||
});
|
||||
|
||||
// Re-compute nodes and dependencies for each project in file map.
|
||||
Object.keys(modifiedSince).forEach((key) => {
|
||||
delete c.data.projectGraph.dependencies[key];
|
||||
});
|
||||
|
||||
return { fileMap: modifiedSince, projectGraph: c.data.projectGraph };
|
||||
const r = builder.build();
|
||||
performance.mark('build project graph:end');
|
||||
performance.measure(
|
||||
'build project graph',
|
||||
'build project graph:start',
|
||||
'build project graph:end'
|
||||
);
|
||||
return r;
|
||||
}
|
||||
|
||||
@@ -65,49 +65,49 @@ describe('findTargetProjectWithImport', () => {
|
||||
proj: [
|
||||
{
|
||||
file: 'libs/proj/index.ts',
|
||||
mtime: 0,
|
||||
hash: 'some-hash',
|
||||
ext: '.ts',
|
||||
},
|
||||
],
|
||||
proj2: [
|
||||
{
|
||||
file: 'libs/proj2/index.ts',
|
||||
mtime: 0,
|
||||
hash: 'some-hash',
|
||||
ext: '.ts',
|
||||
},
|
||||
],
|
||||
proj3a: [
|
||||
{
|
||||
file: 'libs/proj3a/index.ts',
|
||||
mtime: 0,
|
||||
hash: 'some-hash',
|
||||
ext: '.ts',
|
||||
},
|
||||
],
|
||||
proj4ab: [
|
||||
{
|
||||
file: 'libs/proj4ab/index.ts',
|
||||
mtime: 0,
|
||||
hash: 'some-hash',
|
||||
ext: '.ts',
|
||||
},
|
||||
],
|
||||
proj123: [
|
||||
{
|
||||
file: 'libs/proj123/index.ts',
|
||||
mtime: 0,
|
||||
hash: 'some-hash',
|
||||
ext: '.ts',
|
||||
},
|
||||
],
|
||||
proj1234: [
|
||||
{
|
||||
file: 'libs/proj1234/index.ts',
|
||||
mtime: 0,
|
||||
hash: 'some-hash',
|
||||
ext: '.ts',
|
||||
},
|
||||
],
|
||||
'proj1234-child': [
|
||||
{
|
||||
file: 'libs/proj1234-child/index.ts',
|
||||
mtime: 0,
|
||||
hash: 'some-hash',
|
||||
ext: '.ts',
|
||||
},
|
||||
],
|
||||
|
||||
@@ -5,6 +5,6 @@ This library was generated with [Nx](https://nx.dev).
|
||||
|
||||
## Running unit tests
|
||||
|
||||
Run `ng test <%= name %>` to execute the unit tests via [Jest](https://jestjs.io).
|
||||
Run `<%= cliCommand %> test <%= name %>` to execute the unit tests via [Jest](https://jestjs.io).
|
||||
|
||||
<% } %>
|
||||
|
||||
@@ -2,6 +2,7 @@ import { Tree } from '@angular-devkit/schematics';
|
||||
import { createEmptyWorkspace } from '@nrwl/workspace/testing';
|
||||
import { readJsonInTree, updateJsonInTree } from '@nrwl/workspace';
|
||||
import { NxJson } from '@nrwl/workspace';
|
||||
|
||||
import { runSchematic } from '../../utils/testing';
|
||||
|
||||
describe('lib', () => {
|
||||
@@ -98,9 +99,14 @@ describe('lib', () => {
|
||||
|
||||
it('should generate files', async () => {
|
||||
const tree = await runSchematic('lib', { name: 'myLib' }, appTree);
|
||||
|
||||
expect(tree.exists(`libs/my-lib/jest.config.js`)).toBeTruthy();
|
||||
expect(tree.exists('libs/my-lib/src/index.ts')).toBeTruthy();
|
||||
expect(tree.exists('libs/my-lib/src/lib/my-lib.ts')).toBeTruthy();
|
||||
expect(tree.exists('libs/my-lib/README.md')).toBeTruthy();
|
||||
|
||||
const ReadmeContent = tree.readContent('libs/my-lib/README.md');
|
||||
expect(ReadmeContent).toContain('nx test my-lib');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -21,6 +21,7 @@ import { formatFiles } from '@nrwl/workspace';
|
||||
import { offsetFromRoot } from '@nrwl/workspace';
|
||||
import { generateProjectLint, addLintFiles } from '../../utils/lint';
|
||||
import { addProjectToNxJsonInTree, libsDir } from '../../utils/ast-utils';
|
||||
import { cliCommand } from '../../core/file-utils';
|
||||
|
||||
export interface NormalizedSchema extends Schema {
|
||||
name: string;
|
||||
@@ -74,6 +75,7 @@ function createFiles(options: NormalizedSchema): Rule {
|
||||
template({
|
||||
...options,
|
||||
...names(options.name),
|
||||
cliCommand: cliCommand(),
|
||||
tmpl: '',
|
||||
offsetFromRoot: offsetFromRoot(options.projectRoot),
|
||||
hasUnitTestRunner: options.unitTestRunner !== 'none',
|
||||
|
||||
@@ -17,7 +17,8 @@
|
||||
"$source": "argv",
|
||||
"index": 0
|
||||
},
|
||||
"x-prompt": "What name would you like to use for the library?"
|
||||
"x-prompt": "What name would you like to use for the library?",
|
||||
"pattern": "^[a-zA-Z]{1}.*$"
|
||||
},
|
||||
"directory": {
|
||||
"type": "string",
|
||||
|
||||
@@ -26,7 +26,7 @@ export function checkDestination(schema: Schema): Rule {
|
||||
);
|
||||
}
|
||||
|
||||
const destination = getDestination(schema, workspace);
|
||||
const destination = getDestination(schema, workspace, tree);
|
||||
|
||||
if (tree.getDir(destination).subfiles.length > 0) {
|
||||
throw new Error(`${INVALID_DESTINATION} - Path is not empty.`);
|
||||
|
||||
@@ -16,7 +16,7 @@ export function moveProject(schema: Schema) {
|
||||
map((workspace) => {
|
||||
const project = workspace.projects.get(schema.projectName);
|
||||
|
||||
const destination = getDestination(schema, workspace);
|
||||
const destination = getDestination(schema, workspace, tree);
|
||||
const dir = tree.getDir(project.root);
|
||||
dir.visit((file) => {
|
||||
const newPath = file.replace(project.root, destination);
|
||||
|
||||
@@ -24,7 +24,7 @@ export function updateCypressJson(schema: Schema): Rule {
|
||||
return from(getWorkspace(tree)).pipe(
|
||||
map((workspace) => {
|
||||
const project = workspace.projects.get(schema.projectName);
|
||||
const destination = getDestination(schema, workspace);
|
||||
const destination = getDestination(schema, workspace, tree);
|
||||
|
||||
const cypressJsonPath = path.join(destination, 'cypress.json');
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ import { from, Observable } from 'rxjs';
|
||||
import { map } from 'rxjs/operators';
|
||||
import { Schema } from '../schema';
|
||||
import { normalizeSlashes } from './utils';
|
||||
import { ProjectType } from '@nrwl/workspace/src/utils/project-type';
|
||||
|
||||
/**
|
||||
* Updates all the imports in the workspace and modifies the tsconfig appropriately.
|
||||
@@ -20,6 +21,9 @@ export function updateImports(schema: Schema) {
|
||||
return from(getWorkspace(tree)).pipe(
|
||||
map((workspace) => {
|
||||
const nxJson = readJsonInTree<NxJson>(tree, 'nx.json');
|
||||
const libsDir = nxJson.workspaceLayout?.libsDir
|
||||
? nxJson.workspaceLayout.libsDir
|
||||
: 'libs';
|
||||
const project = workspace.projects.get(schema.projectName);
|
||||
|
||||
if (project.extensions['projectType'] === 'application') {
|
||||
@@ -29,7 +33,7 @@ export function updateImports(schema: Schema) {
|
||||
|
||||
const projectRef = {
|
||||
from: normalizeSlashes(
|
||||
`@${nxJson.npmScope}/${project.root.substr(5)}`
|
||||
`@${nxJson.npmScope}/${project.root.substr(libsDir.length + 1)}`
|
||||
),
|
||||
to: normalizeSlashes(`@${nxJson.npmScope}/${schema.destination}`),
|
||||
};
|
||||
@@ -57,7 +61,7 @@ export function updateImports(schema: Schema) {
|
||||
}
|
||||
|
||||
const projectRoot = {
|
||||
from: project.root.substr(5),
|
||||
from: project.root.substr(libsDir.length + 1),
|
||||
to: schema.destination,
|
||||
};
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ export function updateJestConfig(schema: Schema): Rule {
|
||||
return from(getWorkspace(tree)).pipe(
|
||||
map((workspace) => {
|
||||
const project = workspace.projects.get(schema.projectName);
|
||||
const destination = getDestination(schema, workspace);
|
||||
const destination = getDestination(schema, workspace, tree);
|
||||
const newProjectName = getNewProjectName(schema.destination);
|
||||
|
||||
const jestConfigPath = path.join(destination, 'jest.config.js');
|
||||
|
||||
@@ -20,7 +20,7 @@ export function updateProjectRootFiles(schema: Schema): Rule {
|
||||
return from(getWorkspace(tree)).pipe(
|
||||
map((workspace) => {
|
||||
const project = workspace.projects.get(schema.projectName);
|
||||
const destination = getDestination(schema, workspace);
|
||||
const destination = getDestination(schema, workspace, tree);
|
||||
|
||||
const newRelativeRoot = path
|
||||
.relative(path.join(appRootPath, destination), appRootPath)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Tree } from '@angular-devkit/schematics';
|
||||
import { UnitTestTree } from '@angular-devkit/schematics/testing';
|
||||
import { NxJson, updateJsonInTree } from '@nrwl/workspace';
|
||||
import { createEmptyWorkspace } from '@nrwl/workspace/testing';
|
||||
import { callRule } from '../../../utils/testing';
|
||||
import { Schema } from '../schema';
|
||||
@@ -207,4 +208,28 @@ describe('updateWorkspace Rule', () => {
|
||||
e2eProject.architect.e2e.configurations.production.devServerTarget
|
||||
).toBe('subfolder-my-destination:serve:production');
|
||||
});
|
||||
|
||||
it('honor custom workspace layouts', async () => {
|
||||
const schema: Schema = {
|
||||
projectName: 'my-source',
|
||||
destination: 'subfolder/my-destination',
|
||||
};
|
||||
|
||||
tree = (await callRule(
|
||||
updateJsonInTree<NxJson>('nx.json', (json) => {
|
||||
json.workspaceLayout = { appsDir: 'e2e', libsDir: 'packages' };
|
||||
return json;
|
||||
}),
|
||||
tree
|
||||
)) as UnitTestTree;
|
||||
|
||||
tree = (await callRule(updateWorkspace(schema), tree)) as UnitTestTree;
|
||||
|
||||
const workspace = JSON.parse(tree.read('workspace.json').toString());
|
||||
|
||||
const project = workspace.projects['subfolder-my-destination'];
|
||||
expect(project).toBeDefined();
|
||||
expect(project.root).toBe('e2e/subfolder/my-destination');
|
||||
expect(project.sourceRoot).toBe('e2e/subfolder/my-destination/src');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { SchematicContext, Tree } from '@angular-devkit/schematics';
|
||||
import { updateWorkspaceInTree } from '@nrwl/workspace';
|
||||
import { Schema } from '../schema';
|
||||
import { getDestination, getNewProjectName } from './utils';
|
||||
@@ -12,38 +13,40 @@ import { getDestination, getNewProjectName } from './utils';
|
||||
* @param schema The options provided to the schematic
|
||||
*/
|
||||
export function updateWorkspace(schema: Schema) {
|
||||
return updateWorkspaceInTree((workspace) => {
|
||||
const project = workspace.projects[schema.projectName];
|
||||
const newProjectName = getNewProjectName(schema.destination);
|
||||
return (tree: Tree, _context: SchematicContext) => {
|
||||
return updateWorkspaceInTree((workspace) => {
|
||||
const project = workspace.projects[schema.projectName];
|
||||
const newProjectName = getNewProjectName(schema.destination);
|
||||
|
||||
// update root path refs in that project only
|
||||
const oldProject = JSON.stringify(project);
|
||||
const newProject = oldProject.replace(
|
||||
new RegExp(project.root, 'g'),
|
||||
getDestination(schema, workspace)
|
||||
);
|
||||
// update root path refs in that project only
|
||||
const oldProject = JSON.stringify(project);
|
||||
const newProject = oldProject.replace(
|
||||
new RegExp(project.root, 'g'),
|
||||
getDestination(schema, workspace, tree)
|
||||
);
|
||||
|
||||
// rename
|
||||
delete workspace.projects[schema.projectName];
|
||||
workspace.projects[newProjectName] = JSON.parse(newProject);
|
||||
// rename
|
||||
delete workspace.projects[schema.projectName];
|
||||
workspace.projects[newProjectName] = JSON.parse(newProject);
|
||||
|
||||
// update target refs
|
||||
const strWorkspace = JSON.stringify(workspace);
|
||||
workspace = JSON.parse(
|
||||
strWorkspace.replace(
|
||||
new RegExp(`${schema.projectName}:`, 'g'),
|
||||
`${newProjectName}:`
|
||||
)
|
||||
);
|
||||
// update target refs
|
||||
const strWorkspace = JSON.stringify(workspace);
|
||||
workspace = JSON.parse(
|
||||
strWorkspace.replace(
|
||||
new RegExp(`${schema.projectName}:`, 'g'),
|
||||
`${newProjectName}:`
|
||||
)
|
||||
);
|
||||
|
||||
// update default project (if necessary)
|
||||
if (
|
||||
workspace.defaultProject &&
|
||||
workspace.defaultProject === schema.projectName
|
||||
) {
|
||||
workspace.defaultProject = newProjectName;
|
||||
}
|
||||
// update default project (if necessary)
|
||||
if (
|
||||
workspace.defaultProject &&
|
||||
workspace.defaultProject === schema.projectName
|
||||
) {
|
||||
workspace.defaultProject = newProjectName;
|
||||
}
|
||||
|
||||
return workspace;
|
||||
});
|
||||
return workspace;
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,7 +1,28 @@
|
||||
import { WorkspaceDefinition } from '@angular-devkit/core/src/workspace';
|
||||
import { Tree } from '@angular-devkit/schematics';
|
||||
import { NxJson } from '@nrwl/workspace/src/core/shared-interfaces';
|
||||
import { readJsonInTree } from '@nrwl/workspace/src/utils/ast-utils';
|
||||
import * as path from 'path';
|
||||
import { Schema } from '../schema';
|
||||
|
||||
/**
|
||||
* This helper function retrieves the users workspace layout from
|
||||
* `nx.json`. If the user does not have this property defined then
|
||||
* we assume the default `apps/` and `libs/` layout.
|
||||
*
|
||||
* @param host The host tree
|
||||
*/
|
||||
export function getWorkspaceLayout(
|
||||
host: Tree
|
||||
): { appsDir?: string; libsDir?: string } {
|
||||
const nxJson = readJsonInTree<NxJson>(host, 'nx.json');
|
||||
const workspaceLayout = nxJson.workspaceLayout
|
||||
? nxJson.workspaceLayout
|
||||
: { appsDir: 'apps', libsDir: 'libs' };
|
||||
|
||||
return workspaceLayout;
|
||||
}
|
||||
|
||||
/**
|
||||
* This helper function ensures that we don't move libs or apps
|
||||
* outside of the folders they should be in.
|
||||
@@ -14,7 +35,8 @@ import { Schema } from '../schema';
|
||||
*/
|
||||
export function getDestination(
|
||||
schema: Schema,
|
||||
workspace: WorkspaceDefinition | any
|
||||
workspace: WorkspaceDefinition | any,
|
||||
host: Tree
|
||||
): string {
|
||||
const project = workspace.projects.get
|
||||
? workspace.projects.get(schema.projectName)
|
||||
@@ -23,9 +45,11 @@ export function getDestination(
|
||||
? project.extensions['projectType']
|
||||
: project.projectType;
|
||||
|
||||
let rootFolder = 'libs';
|
||||
const workspaceLayout = getWorkspaceLayout(host);
|
||||
|
||||
let rootFolder = workspaceLayout.libsDir;
|
||||
if (projectType === 'application') {
|
||||
rootFolder = 'apps';
|
||||
rootFolder = workspaceLayout.appsDir;
|
||||
}
|
||||
return path.join(rootFolder, schema.destination).split(path.sep).join('/');
|
||||
}
|
||||
|
||||
@@ -33,7 +33,6 @@ export function checkDependencies(schema: Schema): Rule {
|
||||
ig = ig.add(tree.read('.gitignore').toString());
|
||||
}
|
||||
const files: FileData[] = [];
|
||||
const mtime = Date.now(); //can't get mtime data from the tree :(
|
||||
const workspaceDir = path.dirname(getWorkspacePath(tree));
|
||||
|
||||
for (const dir of tree.getDir('/').subdirs) {
|
||||
@@ -45,7 +44,7 @@ export function checkDependencies(schema: Schema): Rule {
|
||||
files.push({
|
||||
file: path.relative(workspaceDir, file),
|
||||
ext: path.extname(file),
|
||||
mtime,
|
||||
hash: '',
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -13,19 +13,104 @@ describe('updateWorkspace Rule', () => {
|
||||
beforeEach(async () => {
|
||||
tree = new UnitTestTree(Tree.empty());
|
||||
tree = createEmptyWorkspace(tree) as UnitTestTree;
|
||||
});
|
||||
|
||||
schema = {
|
||||
projectName: 'ng-app',
|
||||
skipFormat: false,
|
||||
forceRemove: false,
|
||||
};
|
||||
describe('delete project', async () => {
|
||||
beforeEach(async () => {
|
||||
schema = {
|
||||
projectName: 'ng-app',
|
||||
skipFormat: false,
|
||||
forceRemove: false,
|
||||
};
|
||||
|
||||
tree = (await callRule(
|
||||
updateWorkspaceInTree((workspace) => {
|
||||
return {
|
||||
version: 1,
|
||||
projects: {
|
||||
'ng-app': {
|
||||
tree = (await callRule(
|
||||
updateWorkspaceInTree((workspace) => {
|
||||
return {
|
||||
version: 1,
|
||||
projects: {
|
||||
'ng-app': {
|
||||
projectType: 'application',
|
||||
schematics: {},
|
||||
root: 'apps/ng-app',
|
||||
sourceRoot: 'apps/ng-app/src',
|
||||
prefix: 'happyorg',
|
||||
architect: {
|
||||
build: {
|
||||
builder: '@angular-devkit/build-angular:browser',
|
||||
options: {},
|
||||
},
|
||||
},
|
||||
},
|
||||
'ng-app-e2e': {
|
||||
root: 'apps/ng-app-e2e',
|
||||
sourceRoot: 'apps/ng-app-e2e/src',
|
||||
projectType: 'application',
|
||||
architect: {
|
||||
e2e: {
|
||||
builder: '@nrwl/cypress:cypress',
|
||||
options: {
|
||||
cypressConfig: 'apps/ng-app-e2e/cypress.json',
|
||||
tsConfig: 'apps/ng-app-e2e/tsconfig.e2e.json',
|
||||
devServerTarget: 'ng-app:serve',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
}),
|
||||
tree
|
||||
)) as UnitTestTree;
|
||||
});
|
||||
|
||||
it('should delete the project', async () => {
|
||||
let workspace = JSON.parse(tree.read('workspace.json').toString());
|
||||
expect(workspace.projects['ng-app']).toBeDefined();
|
||||
|
||||
tree = (await callRule(updateWorkspace(schema), tree)) as UnitTestTree;
|
||||
|
||||
workspace = JSON.parse(tree.read('workspace.json').toString());
|
||||
expect(workspace.projects['ng-app']).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('defaultProject', () => {
|
||||
beforeEach(async () => {
|
||||
tree = (await callRule(
|
||||
updateWorkspaceInTree((workspace) => {
|
||||
return {
|
||||
version: 1,
|
||||
projects: {
|
||||
'ng-app': {
|
||||
projectType: 'application',
|
||||
schematics: {},
|
||||
root: 'apps/ng-app',
|
||||
sourceRoot: 'apps/ng-app/src',
|
||||
prefix: 'happyorg',
|
||||
architect: {
|
||||
build: {
|
||||
builder: '@angular-devkit/build-angular:browser',
|
||||
options: {},
|
||||
},
|
||||
},
|
||||
},
|
||||
'ng-app-e2e': {
|
||||
root: 'apps/ng-app-e2e',
|
||||
sourceRoot: 'apps/ng-app-e2e/src',
|
||||
projectType: 'application',
|
||||
architect: {
|
||||
e2e: {
|
||||
builder: '@nrwl/cypress:cypress',
|
||||
options: {
|
||||
cypressConfig: 'apps/ng-app-e2e/cypress.json',
|
||||
tsConfig: 'apps/ng-app-e2e/tsconfig.e2e.json',
|
||||
devServerTarget: 'ng-app:serve',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
'ng-other-app': {
|
||||
projectType: 'application',
|
||||
schematics: {},
|
||||
root: 'apps/ng-app',
|
||||
@@ -38,35 +123,43 @@ describe('updateWorkspace Rule', () => {
|
||||
},
|
||||
},
|
||||
},
|
||||
'ng-app-e2e': {
|
||||
root: 'apps/ng-app-e2e',
|
||||
sourceRoot: 'apps/ng-app-e2e/src',
|
||||
projectType: 'application',
|
||||
architect: {
|
||||
e2e: {
|
||||
builder: '@nrwl/cypress:cypress',
|
||||
options: {
|
||||
cypressConfig: 'apps/ng-app-e2e/cypress.json',
|
||||
tsConfig: 'apps/ng-app-e2e/tsconfig.e2e.json',
|
||||
devServerTarget: 'ng-app:serve',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
}),
|
||||
tree
|
||||
)) as UnitTestTree;
|
||||
});
|
||||
defaultProject: 'ng-app',
|
||||
};
|
||||
}),
|
||||
tree
|
||||
)) as UnitTestTree;
|
||||
});
|
||||
|
||||
it('should delete the project', async () => {
|
||||
let workspace = JSON.parse(tree.read('workspace.json').toString());
|
||||
expect(workspace.projects['ng-app']).toBeDefined();
|
||||
it('should remove defaultProject if it matches the project being deleted', async () => {
|
||||
schema = {
|
||||
projectName: 'ng-app',
|
||||
skipFormat: false,
|
||||
forceRemove: false,
|
||||
};
|
||||
|
||||
tree = (await callRule(updateWorkspace(schema), tree)) as UnitTestTree;
|
||||
let workspace = JSON.parse(tree.read('workspace.json').toString());
|
||||
expect(workspace.defaultProject).toBeDefined();
|
||||
|
||||
workspace = JSON.parse(tree.read('workspace.json').toString());
|
||||
expect(workspace.projects['ng-app']).toBeUndefined();
|
||||
tree = (await callRule(updateWorkspace(schema), tree)) as UnitTestTree;
|
||||
|
||||
workspace = JSON.parse(tree.read('workspace.json').toString());
|
||||
expect(workspace.defaultProject).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should not remove defaultProject if it does not match the project being deleted', async () => {
|
||||
schema = {
|
||||
projectName: 'ng-other-app',
|
||||
skipFormat: false,
|
||||
forceRemove: false,
|
||||
};
|
||||
|
||||
let workspace = JSON.parse(tree.read('workspace.json').toString());
|
||||
expect(workspace.defaultProject).toBeDefined();
|
||||
|
||||
tree = (await callRule(updateWorkspace(schema), tree)) as UnitTestTree;
|
||||
|
||||
workspace = JSON.parse(tree.read('workspace.json').toString());
|
||||
expect(workspace.defaultProject).toBeDefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { updateWorkspaceInTree } from '@nrwl/workspace';
|
||||
import { Schema } from '../schema';
|
||||
import { SchematicContext, Tree } from '@angular-devkit/schematics';
|
||||
import { updateWorkspaceInTree, getWorkspacePath } from '@nrwl/workspace';
|
||||
|
||||
/**
|
||||
* Deletes the project from the workspace file
|
||||
@@ -7,8 +8,20 @@ import { Schema } from '../schema';
|
||||
* @param schema The options provided to the schematic
|
||||
*/
|
||||
export function updateWorkspace(schema: Schema) {
|
||||
return updateWorkspaceInTree((workspace) => {
|
||||
delete workspace.projects[schema.projectName];
|
||||
return workspace;
|
||||
});
|
||||
return updateWorkspaceInTree(
|
||||
(workspace, context: SchematicContext, host: Tree) => {
|
||||
delete workspace.projects[schema.projectName];
|
||||
if (
|
||||
workspace.defaultProject &&
|
||||
workspace.defaultProject === schema.projectName
|
||||
) {
|
||||
delete workspace.defaultProject;
|
||||
const workspacePath = getWorkspacePath(host);
|
||||
context.logger.warn(
|
||||
`Default project was removed in ${workspacePath} because it was "${schema.projectName}". If you want a default project you should define a new one.`
|
||||
);
|
||||
}
|
||||
return workspace;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
@@ -145,7 +145,7 @@ export function sharedNew(cli: string, options: Schema): Rule {
|
||||
|
||||
function addCloudDependencies(options: Schema) {
|
||||
return options.nxCloud
|
||||
? addDepsToPackageJson({}, { '@nrwl/nx-cloud': 'latest' })
|
||||
? addDepsToPackageJson({}, { '@nrwl/nx-cloud': 'latest' }, false)
|
||||
: noop();
|
||||
}
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ import { ProjectGraph, ProjectGraphNode } from '../core/project-graph';
|
||||
import { Environment, NxJson } from '../core/shared-interfaces';
|
||||
import { NxArgs } from '@nrwl/workspace/src/command-line/utils';
|
||||
import { isRelativePath } from '../utils/fileutils';
|
||||
import { Hasher } from './hasher';
|
||||
import { Hasher } from '../core/hasher/hasher';
|
||||
import { projectHasTargetAndConfiguration } from '../utils/project-graph-utils';
|
||||
|
||||
type RunArgs = yargs.Arguments & ReporterArgs;
|
||||
@@ -42,14 +42,11 @@ export async function runCommand<T extends RunArgs>(
|
||||
});
|
||||
|
||||
const hasher = new Hasher(projectGraph, nxJson, tasksOptions);
|
||||
await Promise.all(
|
||||
tasks.map(async (t) => {
|
||||
const hash = await hasher.hash(t);
|
||||
t.hash = hash.value;
|
||||
t.hashDetails = hash.details;
|
||||
})
|
||||
);
|
||||
|
||||
const res = await hasher.hashTasks(tasks);
|
||||
for (let i = 0; i < res.length; ++i) {
|
||||
tasks[i].hash = res[i].value;
|
||||
tasks[i].hashDetails = res[i].details;
|
||||
}
|
||||
const cached = [];
|
||||
tasksRunner(tasks, tasksOptions, {
|
||||
initiatingProject: initiatingProject,
|
||||
|
||||
@@ -976,7 +976,7 @@ describe('Enforce Module Boundaries', () => {
|
||||
});
|
||||
|
||||
function createFile(f) {
|
||||
return { file: f, ext: extname(f), mtime: 1 };
|
||||
return { file: f, ext: extname(f), hash: '' };
|
||||
}
|
||||
|
||||
function runRule(
|
||||
|
||||
@@ -406,16 +406,14 @@ export function getFullProjectGraphFromHost(host: Tree): ProjectGraph {
|
||||
|
||||
const workspaceFiles: FileData[] = [];
|
||||
|
||||
const mtime = +Date.now();
|
||||
|
||||
workspaceFiles.push(
|
||||
...allFilesInDirInHost(host, normalize(''), { recursive: false }).map((f) =>
|
||||
getFileDataInHost(host, f, mtime)
|
||||
getFileDataInHost(host, f)
|
||||
)
|
||||
);
|
||||
workspaceFiles.push(
|
||||
...allFilesInDirInHost(host, normalize('tools')).map((f) =>
|
||||
getFileDataInHost(host, f, mtime)
|
||||
getFileDataInHost(host, f)
|
||||
)
|
||||
);
|
||||
|
||||
@@ -424,7 +422,7 @@ export function getFullProjectGraphFromHost(host: Tree): ProjectGraph {
|
||||
const project = workspaceJson.projects[projectName];
|
||||
workspaceFiles.push(
|
||||
...allFilesInDirInHost(host, normalize(project.root)).map((f) =>
|
||||
getFileDataInHost(host, f, mtime)
|
||||
getFileDataInHost(host, f)
|
||||
)
|
||||
);
|
||||
});
|
||||
@@ -438,15 +436,11 @@ export function getFullProjectGraphFromHost(host: Tree): ProjectGraph {
|
||||
);
|
||||
}
|
||||
|
||||
export function getFileDataInHost(
|
||||
host: Tree,
|
||||
path: Path,
|
||||
mtime: number
|
||||
): FileData {
|
||||
export function getFileDataInHost(host: Tree, path: Path): FileData {
|
||||
return {
|
||||
file: path,
|
||||
ext: extname(normalize(path)),
|
||||
mtime,
|
||||
hash: '',
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
import { PerformanceObserver } from 'perf_hooks';
|
||||
|
||||
if (process.env.NX_PERF_LOGGING) {
|
||||
const obs = new PerformanceObserver((list) => {
|
||||
const entry = list.getEntries()[0];
|
||||
console.log(`Time for '${entry.name}'`, entry.duration);
|
||||
});
|
||||
obs.observe({ entryTypes: ['measure'], buffered: false });
|
||||
}
|
||||
@@ -25,9 +25,10 @@ function getCompilerSetup(rootDir) {
|
||||
}
|
||||
let compilerSetup;
|
||||
|
||||
const command = process.argv[3].split(':')[1];
|
||||
if (command === 'test') {
|
||||
// this is needed so tests don't create nxdeps.josn in the dist folder
|
||||
if (
|
||||
process.argv[1].indexOf('jest-worker') > -1 ||
|
||||
(process.argv.length >= 4 && process.argv[3].split(':')[1] === 'test')
|
||||
) {
|
||||
process.env.NX_WORKSPACE_ROOT_PATH = path_1.join(
|
||||
__dirname,
|
||||
'..',
|
||||
|
||||
+3
-3
@@ -42,12 +42,12 @@ do
|
||||
|
||||
PACKAGE_NAME=`node -e "console.log(require('./package.json').name)"`
|
||||
|
||||
echo "Publishing ${PACKAGE_NAME}@${VERSION} --tag ${TAG}"
|
||||
echo "Publishing ${PACKAGE_NAME}@${VERSION}"
|
||||
|
||||
if [ "$LOCALBUILD" = "--local" ]; then
|
||||
npm publish --tag $TAG --access public --registry=NPM_REGISTRY
|
||||
npm publish --access public --registry=NPM_REGISTRY
|
||||
else
|
||||
npm publish --tag $TAG --access public
|
||||
npm publish --access public
|
||||
fi
|
||||
|
||||
cd $ORIG_DIRECTORY
|
||||
|
||||
+1
-1
@@ -113,7 +113,7 @@
|
||||
},
|
||||
{
|
||||
"input": "packages/workspace",
|
||||
"glob": "**/*.{js,css,html}",
|
||||
"glob": "**/*.{js,css,html,svg}",
|
||||
"output": "/"
|
||||
},
|
||||
"LICENSE"
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user