Compare commits

...

44 Commits

Author SHA1 Message Date
Jason Jean cfb9718a12 Release 9.6.0 2020-08-19 17:15:05 -04:00
Victor Savkin 17d78c0516 fix(core): explicitly store workspace files instead of deriving them from hashes 2020-08-19 15:20:06 -04:00
Victor Savkin 4f22832fff fix(core): dont override sigint 2020-08-19 14:46:30 -04:00
Victor Savkin 464d13e5dc fix(core): respect nxignore when using git hashing 2020-08-19 10:33:30 -04:00
Jason Jean a0d501b0db fix(web): remove duplicate copy webpack plugin (#3556) 2020-08-18 17:13:59 -04:00
Marvin Luchs 0351ade509 fix(core): update copy-webpack-plugin (#3514)
fixes security vulnerability caused by serialize-javascript < 3.1.0

closes #3506
2020-08-18 17:13:43 -04:00
Jason Jean 00de5d8235 fix(repo): fix e2e test 2020-08-18 17:11:59 -04:00
Victor Savkin fc1ba0adb5 fix(repo): fix unit test script 2020-08-18 17:11:58 -04:00
Jason Jean 1804b55da1 cleanup(repo): fix formatting 2020-08-18 17:11:58 -04:00
Mehrad Rafigh aa6ea72b83 feat(testing): pass reporter and reporterOptions to cypress builder (#3536) 2020-08-18 17:11:58 -04:00
Jeremy Forsythe 5abd0d11a1 fix(core): remove defaultProject in workspace when removing project
Fixes #3511
2020-08-18 17:11:58 -04:00
Devin Shoemaker c16c1614b5 fix(core): tests 2020-08-18 17:11:57 -04:00
Devin Shoemaker 8f62e8d37e fix(core): honor workspace layout with workspace move schematic 2020-08-18 17:11:57 -04:00
Victor Savkin 237d508e52 fix(core): add a workaround for potential bugs in git hasher (#3521) 2020-08-18 13:26:48 -04:00
Jason Jean 61fc7218ff cleanup(core): fix formatting 2020-08-18 13:26:48 -04:00
Mehrad Rafigh 34847aac0e feat(testing): pass ignoreTestFiles to cypress builder
ISSUES CLOSED: #3439
2020-08-18 13:26:48 -04:00
Jakub Koralewski be42a100c1 fix(core): git hasher should handle file that are both renamed and modified 2020-08-18 13:26:48 -04:00
Zachary DeRose 5f637f8b50 fix(testing): builder should let baseUrl option take precidence if present (#3487) 2020-08-18 13:26:48 -04:00
Fernando Montoya b170e8184c fix(react): remove empty space from describe name (#3504) 2020-08-18 13:26:47 -04:00
Victor Savkin 291a918a26 fix(core): remove an unnecessary npm install when connecting to cloud 2020-08-18 13:26:47 -04:00
Victor Savkin 1d8f901c71 fix(core): create-nx-workspace uses a unix-style path on windows 2020-08-18 13:26:47 -04:00
Bram Borggreve 74a61e7b5c fix(core): create-nx-workspace preset and cli params should work with spaces 2020-08-18 13:26:47 -04:00
Martin Hochel fdd90990a3 fix(core): generate proper cli command in lib readme 2020-08-18 13:26:47 -04:00
Jason Jean 34e6c1498f fix(core): do not print warnings for print-affected 2020-08-18 13:26:46 -04:00
Victor Savkin b0ada64d19 fix(core): sort files before creating project graph to make stable hashes 2020-08-18 13:26:46 -04:00
Philip Fulcher c8eb86d9ce fix(core): fix crosshair icon for dep-graph 2020-08-18 13:26:46 -04:00
Jason Jean 3f18eaba90 fix(misc): fix exit code for running nx outside of a workspace 2020-08-18 13:26:46 -04:00
Jo Hanna Pearce fa9c790b9d fix(misc): add regex pattern to schematics to prevent empty app/lib creation (#3396)
ISSUES CLOSED: #2924
2020-08-13 17:48:45 -04:00
Jason Jean 93fab2644f fix(core): properly resolve tsconfig when serving from project root (#3403) 2020-08-13 17:48:45 -04:00
Jo Hanna Pearce d074f45ed3 fix(angular): show a nicer error when running nx in an angular workspace 2020-08-13 17:48:45 -04:00
Jack Hsu 5927a5b7f8 fix(react): add babel plugin for styled-jsx for libs (#3441) 2020-08-13 17:48:45 -04:00
Jason Jean 9699214ef7 fix(core): override --prod with --configuration 2020-08-13 17:48:44 -04:00
Bucky Maler 5109068f9f feat(storybook): add docs mode option to dev server builder (#2882)
closes #3147
2020-08-13 17:48:44 -04:00
Victor Savkin 020d2e1088 fix(core): with-deps should handle circular dependencies 2020-08-13 17:48:44 -04:00
Victor Savkin 12ac1bc696 cleanup(core): add unit tests to verify edge cases of git hashing 2020-08-13 17:48:44 -04:00
Victor Savkin 7e9587f384 fix(core): handle git renames and spaces in filenames when doing git hashing 2020-08-13 17:48:44 -04:00
Victor Savkin 496534d7e5 fix(misc): workspace-lint should respect nested gitignores 2020-08-13 17:48:43 -04:00
Victor Savkin 1777f5bc79 fix(core): remove deleted files from git-hashers result 2020-08-13 17:48:43 -04:00
Jason Jean d021876f06 fix(core): fix dep-graph crosshair svg not available (#3362) 2020-08-13 17:48:43 -04:00
Brandon Roberts 6ee1dda6cd feat(core): use custom output logging for configuration out of sync errors 2020-08-13 17:48:43 -04:00
Jack Hsu d7f1a03f88 fix(react): move url-loader to the react package.json (#3356) 2020-08-13 17:48:42 -04:00
Victor Savkin 353a9d9f14 feat(core): add perf logging 2020-08-13 17:48:42 -04:00
Victor Savkin 2871bd10a2 fix(core): increase maxBuffer when invoking git to get file hashes 2020-08-13 17:48:42 -04:00
Victor Savkin 9a2341a8c1 feat(core): redesign workspace file hashing 2020-08-13 17:48:42 -04:00
101 changed files with 1962 additions and 936 deletions
@@ -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', () => {
+12 -12
View File
@@ -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
View File
@@ -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",
+24 -3
View File
@@ -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);
}
}
+1
View File
@@ -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, [
+1 -2
View File
@@ -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) {
+1 -5
View File
@@ -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.",
+1 -1
View File
@@ -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
+18 -20
View File
@@ -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);
}
+2 -1
View File
@@ -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"
}
}
+1
View File
@@ -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 {
+1 -2
View File
@@ -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.",
+11 -9
View File
@@ -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'],
}
);
});
}
-1
View File
@@ -170,6 +170,5 @@ export function convertBuildOptions(buildOptions: WebBuildBuilderOptions): any {
aot: false,
forkTypeChecker: false,
lazyModules: [] as string[],
assets: [] as string[],
};
}
@@ -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 -1
View File
@@ -1,5 +1,5 @@
module.exports = {
name: 'tao',
name: 'workspace',
preset: '../../jest.config.js',
transform: {
'^.+\\.[tj]sx?$': 'ts-jest',
-1
View File
@@ -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();
+9 -7
View File
@@ -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('.'));
}
+5 -2
View File
@@ -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),
},
]);
@@ -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,
@@ -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(
{
@@ -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,
@@ -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' }],
});
});
});
+68 -34
View File
@@ -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>();
}
}
@@ -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();
});
@@ -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(
+5 -11
View File
@@ -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 });
}
+4 -3
View File
@@ -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
View File
@@ -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
View File
@@ -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