From aeec4bd4d9717103e37bbdea868ec65d9ca27677 Mon Sep 17 00:00:00 2001 From: Jason Jean Date: Tue, 19 Jan 2021 16:45:44 -0500 Subject: [PATCH] feat(core): migrate move to devkit (#4558) --- docs/angular/api-workspace/generators/move.md | 10 + docs/node/api-workspace/generators/move.md | 10 + docs/react/api-workspace/generators/move.md | 10 + .../src/workspace-aux-commands.test.ts | 12 +- packages/devkit/index.ts | 5 + packages/devkit/package.json | 1 + .../src/generators/project-configuration.ts | 170 +++++++-- .../visit-not-ignored-files.spec.ts | 52 +++ .../src/generators/visit-not-ignored-files.ts | 34 ++ packages/devkit/src/tests/create-tree.ts | 2 +- .../devkit/src/utils/invoke-nx-generator.ts | 11 +- .../devkit/src/utils/string-change.spec.ts | 29 ++ packages/devkit/src/utils/string-change.ts | 23 +- .../jest/src/executors/jest/jest.impl.spec.ts | 1 + packages/tao/src/shared/tree.spec.ts | 84 ++++- packages/tao/src/shared/tree.ts | 51 ++- packages/tao/src/shared/workspace.ts | 6 +- packages/workspace/collection.json | 8 +- .../workspace/src/command-line/dep-graph.ts | 3 - .../__snapshots__/update-imports.spec.ts.snap | 38 ++ .../move/lib/check-destination.spec.ts | 36 +- .../schematics/move/lib/check-destination.ts | 44 +-- .../lib/move-project-configuration.spec.ts | 205 ++++++++++ .../move/lib/move-project-configuration.ts | 36 ++ .../schematics/move/lib/move-project.spec.ts | 39 +- .../src/schematics/move/lib/move-project.ts | 34 +- .../move/lib/update-build-targets.spec.ts | 57 +++ .../move/lib/update-build-targets.ts | 21 ++ .../move/lib/update-cypress-json.spec.ts | 62 ++- .../move/lib/update-cypress-json.ts | 58 ++- .../move/lib/update-default-project.spec.ts | 43 +++ .../move/lib/update-default-project.ts | 29 ++ .../move/lib/update-eslintrc-json.spec.ts | 127 ++----- .../move/lib/update-eslintrc-json.ts | 55 ++- .../lib/update-implicit-dependencies.spec.ts | 45 +++ ...son.ts => update-implicit-dependencies.ts} | 12 +- .../move/lib/update-imports.spec.ts | 357 +++++++++--------- .../src/schematics/move/lib/update-imports.ts | 270 +++++++------ .../move/lib/update-jest-config.spec.ts | 33 +- .../schematics/move/lib/update-jest-config.ts | 89 ++--- .../move/lib/update-nx-json.spec.ts | 36 -- .../lib/update-project-root-files.spec.ts | 26 +- .../move/lib/update-project-root-files.ts | 74 ++-- .../move/lib/update-storybook-config.spec.ts | 47 ++- .../move/lib/update-storybook-config.ts | 75 ++-- .../move/lib/update-workspace.spec.ts | 245 ------------ .../schematics/move/lib/update-workspace.ts | 52 --- .../src/schematics/move/lib/utils.ts | 38 +- .../workspace/src/schematics/move/move.ts | 55 +-- .../workspace/src/schematics/move/schema.d.ts | 1 + .../workspace/src/schematics/move/schema.json | 7 + .../update-jest-config.spec.ts.snap | 6 +- .../remove/lib/check-dependencies.spec.ts | 71 ++-- .../remove/lib/check-dependencies.ts | 80 +--- .../remove/lib/check-targets.spec.ts | 89 ++--- .../schematics/remove/lib/check-targets.ts | 43 +-- .../remove/lib/remove-project-config.spec.ts | 120 ++++++ .../remove/lib/remove-project-config.ts | 46 +++ .../remove/lib/remove-project.spec.ts | 30 +- .../schematics/remove/lib/remove-project.ts | 25 +- .../remove/lib/update-jest-config.spec.ts | 35 +- .../remove/lib/update-jest-config.ts | 78 ++-- .../remove/lib/update-nx-json.spec.ts | 50 --- .../schematics/remove/lib/update-nx-json.ts | 23 -- .../remove/lib/update-tsconfig.spec.ts | 27 +- .../schematics/remove/lib/update-tsconfig.ts | 47 ++- .../remove/lib/update-workspace.spec.ts | 165 -------- .../schematics/remove/lib/update-workspace.ts | 27 -- .../workspace/src/schematics/remove/remove.ts | 44 +-- .../src/schematics/remove/schema.json | 1 + .../utils/create-project-graph-from-tree.ts | 37 ++ 71 files changed, 2050 insertions(+), 1862 deletions(-) create mode 100644 packages/devkit/src/generators/visit-not-ignored-files.spec.ts create mode 100644 packages/devkit/src/generators/visit-not-ignored-files.ts create mode 100644 packages/workspace/src/schematics/move/lib/__snapshots__/update-imports.spec.ts.snap create mode 100644 packages/workspace/src/schematics/move/lib/move-project-configuration.spec.ts create mode 100644 packages/workspace/src/schematics/move/lib/move-project-configuration.ts create mode 100644 packages/workspace/src/schematics/move/lib/update-build-targets.spec.ts create mode 100644 packages/workspace/src/schematics/move/lib/update-build-targets.ts create mode 100644 packages/workspace/src/schematics/move/lib/update-default-project.spec.ts create mode 100644 packages/workspace/src/schematics/move/lib/update-default-project.ts create mode 100644 packages/workspace/src/schematics/move/lib/update-implicit-dependencies.spec.ts rename packages/workspace/src/schematics/move/lib/{update-nx-json.ts => update-implicit-dependencies.ts} (63%) delete mode 100644 packages/workspace/src/schematics/move/lib/update-nx-json.spec.ts delete mode 100644 packages/workspace/src/schematics/move/lib/update-workspace.spec.ts delete mode 100644 packages/workspace/src/schematics/move/lib/update-workspace.ts create mode 100644 packages/workspace/src/schematics/remove/lib/remove-project-config.spec.ts create mode 100644 packages/workspace/src/schematics/remove/lib/remove-project-config.ts delete mode 100644 packages/workspace/src/schematics/remove/lib/update-nx-json.spec.ts delete mode 100644 packages/workspace/src/schematics/remove/lib/update-nx-json.ts delete mode 100644 packages/workspace/src/schematics/remove/lib/update-workspace.spec.ts delete mode 100644 packages/workspace/src/schematics/remove/lib/update-workspace.ts create mode 100644 packages/workspace/src/utils/create-project-graph-from-tree.ts diff --git a/docs/angular/api-workspace/generators/move.md b/docs/angular/api-workspace/generators/move.md index 5a7c36e581..8b76fb8b6a 100644 --- a/docs/angular/api-workspace/generators/move.md +++ b/docs/angular/api-workspace/generators/move.md @@ -56,6 +56,16 @@ Type: `string` The name of the project to move +### skipFormat + +Alias(es): skip-format + +Default: `false` + +Type: `boolean` + +Skip formatting files. + ### updateImportPath Default: `true` diff --git a/docs/node/api-workspace/generators/move.md b/docs/node/api-workspace/generators/move.md index 7748305ff3..723a022898 100644 --- a/docs/node/api-workspace/generators/move.md +++ b/docs/node/api-workspace/generators/move.md @@ -56,6 +56,16 @@ Type: `string` The name of the project to move +### skipFormat + +Alias(es): skip-format + +Default: `false` + +Type: `boolean` + +Skip formatting files. + ### updateImportPath Default: `true` diff --git a/docs/react/api-workspace/generators/move.md b/docs/react/api-workspace/generators/move.md index 7748305ff3..723a022898 100644 --- a/docs/react/api-workspace/generators/move.md +++ b/docs/react/api-workspace/generators/move.md @@ -56,6 +56,16 @@ Type: `string` The name of the project to move +### skipFormat + +Alias(es): skip-format + +Default: `false` + +Type: `boolean` + +Skip formatting files. + ### updateImportPath Default: `true` diff --git a/e2e/workspace/src/workspace-aux-commands.test.ts b/e2e/workspace/src/workspace-aux-commands.test.ts index 932d2b8762..3dec663ebc 100644 --- a/e2e/workspace/src/workspace-aux-commands.test.ts +++ b/e2e/workspace/src/workspace-aux-commands.test.ts @@ -750,9 +750,7 @@ describe('Move Project', () => { const jestConfig = readFile(jestConfigPath); expect(jestConfig).toContain(`displayName: 'shared-${lib1}-data-access'`); expect(jestConfig).toContain(`preset: '../../../../jest.preset.js'`); - expect(jestConfig).toContain( - `coverageDirectory: '../../../../coverage/${newPath}'` - ); + expect(jestConfig).toContain(`'../../../../coverage/${newPath}'`); const tsConfigPath = `${newPath}/tsconfig.json`; expect(moveOutput).toContain(`CREATE ${tsConfigPath}`); @@ -888,9 +886,7 @@ describe('Move Project', () => { const jestConfig = readFile(jestConfigPath); expect(jestConfig).toContain(`displayName: 'shared-${lib1}-data-access'`); expect(jestConfig).toContain(`preset: '../../../../jest.preset.js'`); - expect(jestConfig).toContain( - `coverageDirectory: '../../../../coverage/${newPath}'` - ); + expect(jestConfig).toContain(`'../../../../coverage/${newPath}'`); const tsConfigPath = `${newPath}/tsconfig.json`; expect(moveOutput).toContain(`CREATE ${tsConfigPath}`); @@ -1029,9 +1025,7 @@ describe('Move Project', () => { const jestConfig = readFile(jestConfigPath); expect(jestConfig).toContain(`displayName: 'shared-${lib1}-data-access'`); expect(jestConfig).toContain(`preset: '../../../../jest.preset.js'`); - expect(jestConfig).toContain( - `coverageDirectory: '../../../../coverage/${newPath}'` - ); + expect(jestConfig).toContain(`'../../../../coverage/${newPath}'`); const tsConfigPath = `${newPath}/tsconfig.json`; expect(moveOutput).toContain(`CREATE ${tsConfigPath}`); diff --git a/packages/devkit/index.ts b/packages/devkit/index.ts index af4a7302a5..3a2365ecac 100644 --- a/packages/devkit/index.ts +++ b/packages/devkit/index.ts @@ -21,9 +21,14 @@ export { generateFiles } from './src/generators/generate-files'; export { addProjectConfiguration, readProjectConfiguration, + removeProjectConfiguration, updateProjectConfiguration, + readWorkspaceConfiguration, + updateWorkspaceConfiguration, + getProjects, } from './src/generators/project-configuration'; export { toJS } from './src/generators/to-js'; +export { visitNotIgnoredFiles } from './src/generators/visit-not-ignored-files'; export { readJson, writeJson, updateJson } from './src/utils/json'; export { addDependenciesToPackageJson } from './src/utils/package-json'; diff --git a/packages/devkit/package.json b/packages/devkit/package.json index 2c54800c5d..6309791af8 100644 --- a/packages/devkit/package.json +++ b/packages/devkit/package.json @@ -28,6 +28,7 @@ "dependencies": { "@nrwl/tao": "*", "ejs": "^3.1.5", + "ignore": "^5.0.4", "semver": "6.3.0", "strip-json-comments": "2.0.1", "tslib": "^2.0.0" diff --git a/packages/devkit/src/generators/project-configuration.ts b/packages/devkit/src/generators/project-configuration.ts index b820947a46..554ce204c9 100644 --- a/packages/devkit/src/generators/project-configuration.ts +++ b/packages/devkit/src/generators/project-configuration.ts @@ -4,7 +4,7 @@ import { toNewFormat, WorkspaceConfiguration, } from '@nrwl/tao/src/shared/workspace'; -import { readJson } from '../utils/json'; +import { readJson, updateJson } from '../utils/json'; import { NxJsonConfiguration, NxJsonProjectConfiguration, @@ -47,6 +47,62 @@ export function updateProjectConfiguration( setProjectConfiguration(host, projectName, projectConfiguration, 'update'); } +/** + * Removes the configuration of an existing project. + * + * The project configuration is stored in workspace.json and nx.json. + * The utility will update both files. + */ +export function removeProjectConfiguration(host: Tree, projectName: string) { + setProjectConfiguration(host, projectName, undefined, 'delete'); +} + +/** + * Get a map of all projects in a workspace. + * + * Use {@link readProjectConfiguration} if only one project is needed. + */ +export function getProjects(host: Tree) { + const workspace = readWorkspace(host); + const nxJson = readJson(host, 'nx.json'); + + return new Map( + Object.keys(workspace.projects).map((projectName) => { + return [ + projectName, + getProjectConfiguration(projectName, workspace, nxJson), + ]; + }) + ); +} + +/** + * Read general workspace configuration such as the default project or cli settings + * + * This does _not_ provide projects configuration, use {@link readProjectConfiguration} instead. + */ +export function readWorkspaceConfiguration( + host: Tree +): Omit { + const workspace = readWorkspace(host); + delete workspace.projects; + return workspace; +} + +/** + * Update general workspace configuration such as the default project or cli settings. + * + * This does _not_ update projects configuration, use {@link updateProjectConfiguration} or {@link addProjectConfiguration} instead. + */ +export function updateWorkspaceConfiguration( + host: Tree, + workspace: Omit +) { + updateJson(host, getWorkspacePath(host), (json) => { + return { ...workspace, projects: json.projects }; + }); +} + /** * Reads a project configuration. * @@ -56,36 +112,45 @@ export function updateProjectConfiguration( * @param host - the file system tree * @param projectName - unique name. Often directories are part of the name (e.g., mydir-mylib) */ -export function readProjectConfiguration( - host: Tree, - projectName: string -): ProjectConfiguration & NxJsonProjectConfiguration { - return { - ...readWorkspaceSection(host, projectName), - ...readNxJsonSection(host, projectName), - }; -} - -function readWorkspaceSection(host: Tree, projectName: string) { - const path = getWorkspacePath(host); - const workspaceJson = readJson(host, path); - const newFormat = toNewFormat(workspaceJson); - - if (!newFormat.projects[projectName]) { +export function readProjectConfiguration(host: Tree, projectName: string) { + const workspace = readWorkspace(host); + if (!workspace.projects[projectName]) { throw new Error( - `Cannot find configuration for '${projectName}' in ${path}.` + `Cannot find configuration for '${projectName}' in ${getWorkspacePath( + host + )}.` ); } - return newFormat.projects[projectName] as ProjectConfiguration; -} -function readNxJsonSection(host: Tree, projectName: string) { const nxJson = readJson(host, 'nx.json'); if (!nxJson.projects[projectName]) { throw new Error( `Cannot find configuration for '${projectName}' in nx.json` ); } + + return getProjectConfiguration(projectName, workspace, nxJson); +} + +function getProjectConfiguration( + projectName: string, + workspace: WorkspaceConfiguration, + nxJson: NxJsonConfiguration +): ProjectConfiguration & NxJsonProjectConfiguration { + return { + ...readWorkspaceSection(workspace, projectName), + ...readNxJsonSection(nxJson, projectName), + }; +} + +function readWorkspaceSection( + workspace: WorkspaceConfiguration, + projectName: string +) { + return workspace.projects[projectName] as ProjectConfiguration; +} + +function readNxJsonSection(nxJson: NxJsonConfiguration, projectName: string) { return nxJson.projects[projectName]; } @@ -93,25 +158,42 @@ function setProjectConfiguration( host: Tree, projectName: string, projectConfiguration: ProjectConfiguration & NxJsonProjectConfiguration, - mode: 'create' | 'update' + mode: 'create' | 'update' | 'delete' ) { + if (mode === 'delete') { + addProjectToNxJson(host, projectName, undefined, mode); + addProjectToWorkspaceJson(host, projectName, undefined, mode); + return; + } + + if (!projectConfiguration) { + throw new Error( + `Cannot ${mode} "${projectName}" with value ${projectConfiguration}` + ); + } + const { tags, implicitDependencies, ...workspaceConfiguration } = projectConfiguration; addProjectToWorkspaceJson(host, projectName, workspaceConfiguration, mode); - addProjectToNxJson(host, projectName, { - tags, - implicitDependencies, - }); + addProjectToNxJson( + host, + projectName, + { + tags, + implicitDependencies, + }, + mode + ); } function addProjectToWorkspaceJson( host: Tree, projectName: string, project: ProjectConfiguration, - mode: 'create' | 'update' + mode: 'create' | 'update' | 'delete' ) { const path = getWorkspacePath(host); const workspaceJson = readJson(host, path); @@ -125,6 +207,11 @@ function addProjectToWorkspaceJson( `Cannot update Project '${projectName}'. It does not exist.` ); } + if (mode == 'delete' && !workspaceJson.projects[projectName]) { + throw new Error( + `Cannot update Project '${projectName}'. It does not exist.` + ); + } workspaceJson.projects[projectName] = project; host.write(path, JSON.stringify(workspaceJson)); } @@ -132,14 +219,29 @@ function addProjectToWorkspaceJson( function addProjectToNxJson( host: Tree, projectName: string, - config: NxJsonProjectConfiguration + config: NxJsonProjectConfiguration, + mode: 'create' | 'update' | 'delete' ) { const nxJson = readJson(host, 'nx.json'); - nxJson.projects[projectName] = { - ...{ - tags: [], - }, - ...(config || {}), - }; + if (mode === 'delete') { + delete nxJson.projects[projectName]; + } else { + nxJson.projects[projectName] = { + ...{ + tags: [], + }, + ...(config || {}), + }; + } host.write('nx.json', JSON.stringify(nxJson)); } + +function readWorkspace(host: Tree): WorkspaceConfiguration { + const path = getWorkspacePath(host); + const workspaceJson = readJson(host, path); + const originalVersion = workspaceJson.version; + return { + ...toNewFormat(workspaceJson), + version: originalVersion, + }; +} diff --git a/packages/devkit/src/generators/visit-not-ignored-files.spec.ts b/packages/devkit/src/generators/visit-not-ignored-files.spec.ts new file mode 100644 index 0000000000..211b2490be --- /dev/null +++ b/packages/devkit/src/generators/visit-not-ignored-files.spec.ts @@ -0,0 +1,52 @@ +import { createTree } from '@nrwl/devkit/testing'; +import { Tree, visitNotIgnoredFiles } from '@nrwl/devkit'; + +describe('visitNotIgnoredFiles', () => { + let tree: Tree; + + beforeEach(() => { + tree = createTree(); + }); + + it('should visit files recursively in a directory', () => { + tree.write('dir/file1.ts', ''); + tree.write('dir/dir2/file2.ts', ''); + + const visitor = jest.fn(); + visitNotIgnoredFiles(tree, 'dir', visitor); + + expect(visitor).toHaveBeenCalledWith('dir/file1.ts'); + expect(visitor).toHaveBeenCalledWith('dir/dir2/file2.ts'); + }); + + it('should not visit ignored files in a directory', () => { + tree.write('.gitignore', 'node_modules'); + + tree.write('dir/file1.ts', ''); + tree.write('dir/node_modules/file1.ts', ''); + tree.write('dir/dir2/file2.ts', ''); + + const visitor = jest.fn(); + visitNotIgnoredFiles(tree, 'dir', visitor); + + expect(visitor).toHaveBeenCalledWith('dir/file1.ts'); + expect(visitor).toHaveBeenCalledWith('dir/dir2/file2.ts'); + expect(visitor).not.toHaveBeenCalledWith('dir/node_modules/file1.ts'); + }); + + it('should be able to visit the root', () => { + tree.write('.gitignore', 'node_modules'); + + tree.write('dir/file1.ts', ''); + tree.write('dir/node_modules/file1.ts', ''); + tree.write('dir/dir2/file2.ts', ''); + + const visitor = jest.fn(); + visitNotIgnoredFiles(tree, '', visitor); + + expect(visitor).toHaveBeenCalledWith('.gitignore'); + expect(visitor).toHaveBeenCalledWith('dir/file1.ts'); + expect(visitor).toHaveBeenCalledWith('dir/dir2/file2.ts'); + expect(visitor).not.toHaveBeenCalledWith('dir/node_modules/file1.ts'); + }); +}); diff --git a/packages/devkit/src/generators/visit-not-ignored-files.ts b/packages/devkit/src/generators/visit-not-ignored-files.ts new file mode 100644 index 0000000000..11297b538e --- /dev/null +++ b/packages/devkit/src/generators/visit-not-ignored-files.ts @@ -0,0 +1,34 @@ +import { Tree } from '@nrwl/tao/src/shared/tree'; + +import { join } from 'path'; + +import ignore from 'ignore'; + +/** + * Utility to act on all files in a tree that are not ignored by git. + */ +export function visitNotIgnoredFiles( + tree: Tree, + dirPath: string = tree.root, + visitor: (path: string) => void +) { + let ig; + if (tree.exists('.gitignore')) { + ig = ignore(); + ig.add(tree.read('.gitignore').toString()); + } + if (dirPath !== '' && ig?.ignores(dirPath)) { + return; + } + for (const child of tree.children(dirPath)) { + const fullPath = join(dirPath, child); + if (ig?.ignores(fullPath)) { + continue; + } + if (tree.isFile(fullPath)) { + visitor(fullPath); + } else { + visitNotIgnoredFiles(tree, fullPath, visitor); + } + } +} diff --git a/packages/devkit/src/tests/create-tree.ts b/packages/devkit/src/tests/create-tree.ts index 216de2b7f5..b365325765 100644 --- a/packages/devkit/src/tests/create-tree.ts +++ b/packages/devkit/src/tests/create-tree.ts @@ -4,5 +4,5 @@ import { FsTree } from '@nrwl/tao/src/shared/tree'; * Creates a host for testing. */ export function createTree() { - return new FsTree('/', false); + return new FsTree('/virtual', false); } diff --git a/packages/devkit/src/utils/invoke-nx-generator.ts b/packages/devkit/src/utils/invoke-nx-generator.ts index 181343b7ac..d640b680c2 100644 --- a/packages/devkit/src/utils/invoke-nx-generator.ts +++ b/packages/devkit/src/utils/invoke-nx-generator.ts @@ -77,10 +77,7 @@ class DevkitTreeFromAngularDevkitTree { children(dirPath: string): string[] { const { subdirs, subfiles } = this.tree.getDir(dirPath); - return [ - ...subdirs.map((fragment) => join(this.root, fragment)), - ...subfiles.map((fragment) => join(this.root, fragment)), - ]; + return [...subdirs, ...subfiles]; } delete(filePath: string): void { @@ -100,12 +97,12 @@ class DevkitTreeFromAngularDevkitTree { for (const action of this.tree.actions) { if (action.kind === 'r') { fileChanges.push({ - path: this.normalize(action.path), + path: this.normalize(action.to), type: 'CREATE', - content: this.read(action.path), + content: this.read(action.to), }); fileChanges.push({ - path: this.normalize(action.to), + path: this.normalize(action.path), type: 'DELETE', content: null, }); diff --git a/packages/devkit/src/utils/string-change.spec.ts b/packages/devkit/src/utils/string-change.spec.ts index 600f808678..d27ae95d8f 100644 --- a/packages/devkit/src/utils/string-change.spec.ts +++ b/packages/devkit/src/utils/string-change.spec.ts @@ -134,6 +134,35 @@ describe('applyChangesToString', () => { expect(result).toEqual('Updated Text'); }); + it('should be able to replace text twice', () => { + const original = 'Original Text'; + + const result = applyChangesToString(original, [ + { + type: ChangeType.Delete, + start: 0, + length: 8, + }, + { + type: ChangeType.Insert, + index: 0, + text: 'Updated', + }, + { + type: ChangeType.Delete, + start: 9, + length: 4, + }, + { + type: ChangeType.Insert, + index: 9, + text: 'Updated', + }, + ]); + + expect(result).toEqual('Updated Updated'); + }); + it('should sort changes when replacing text', () => { const original = 'Original Text'; diff --git a/packages/devkit/src/utils/string-change.ts b/packages/devkit/src/utils/string-change.ts index a4aa9f1998..9bf67076c0 100644 --- a/packages/devkit/src/utils/string-change.ts +++ b/packages/devkit/src/utils/string-change.ts @@ -1,5 +1,5 @@ export enum ChangeType { - Delete = 'DELETE', + Delete = 'Delete', Insert = 'Insert', } @@ -68,22 +68,29 @@ export function applyChangesToString( changes: StringChange[] ): string { assertChangesValid(changes); - const sortedChanges = changes.sort( - (a, b) => getChangeIndex(a) - getChangeIndex(b) - ); + const sortedChanges = changes.sort((a, b) => { + const diff = getChangeIndex(a) - getChangeIndex(b); + if (diff === 0) { + if (a.type === b.type) { + return 0; + } else { + // When at the same place, Insert before Delete + return a.type === ChangeType.Insert ? -1 : 1; + } + } + return diff; + }); let offset = 0; for (const change of sortedChanges) { + const index = getChangeIndex(change) + offset; switch (change.type) { case ChangeType.Insert: { - const index = change.index + Math.max(offset, 0); text = text.substr(0, index) + change.text + text.substr(index); offset += change.text.length; break; } case ChangeType.Delete: { - text = - text.substr(0, change.start + offset) + - text.substr(change.start + change.length + offset); + text = text.substr(0, index) + text.substr(index + change.length); offset -= change.length; break; } diff --git a/packages/jest/src/executors/jest/jest.impl.spec.ts b/packages/jest/src/executors/jest/jest.impl.spec.ts index 34b574ef53..318cae53c9 100644 --- a/packages/jest/src/executors/jest/jest.impl.spec.ts +++ b/packages/jest/src/executors/jest/jest.impl.spec.ts @@ -27,6 +27,7 @@ describe('Jest Executor', () => { root: '/root', projectName: 'proj', workspace: { + version: 2, projects: { proj: { root: 'proj', diff --git a/packages/tao/src/shared/tree.spec.ts b/packages/tao/src/shared/tree.spec.ts index 571f722fe8..7cabdb72f0 100644 --- a/packages/tao/src/shared/tree.spec.ts +++ b/packages/tao/src/shared/tree.spec.ts @@ -216,25 +216,77 @@ describe('tree', () => { } catch (e) {} }); - it('should return the list of children of a dir', () => { - tree.write('parent/new-child/new-child-file.txt', 'new child content'); + describe('children', () => { + it('should return the list of children of a dir', () => { + expect(tree.children('parent')).toEqual(['child', 'parent-file.txt']); + expect(tree.children('parent/child')).toEqual(['child-file.txt']); + }); - expect(tree.children('parent/child')).toEqual(['child-file.txt']); - expect(tree.children('parent/new-child')).toEqual(['new-child-file.txt']); + it('should add new children after writing new files', () => { + tree.write('parent/child/child-file2.txt', 'new child content'); + tree.write('parent/new-child/new-child-file.txt', 'new child content'); - tree.rename( - 'parent/child/child-file.txt', - 'parent/child/renamed-child-file.txt' - ); - tree.rename( - 'parent/new-child/new-child-file.txt', - 'parent/new-child/renamed-new-child-file.txt' - ); + expect(tree.children('parent')).toEqual([ + 'child', + 'parent-file.txt', + 'new-child', + ]); + expect(tree.children('parent/child')).toEqual([ + 'child-file.txt', + 'child-file2.txt', + ]); + expect(tree.children('parent/new-child')).toEqual([ + 'new-child-file.txt', + ]); + }); - expect(tree.children('parent/child')).toEqual(['renamed-child-file.txt']); - expect(tree.children('parent/new-child')).toEqual([ - 'renamed-new-child-file.txt', - ]); + it('should return the list of children after renaming', () => { + tree.rename( + 'parent/child/child-file.txt', + 'parent/child/renamed-child-file.txt' + ); + + expect(tree.children('parent/child')).toEqual([ + 'renamed-child-file.txt', + ]); + + tree.rename( + 'parent/child/renamed-child-file.txt', + 'parent/renamed-child/renamed-child-file.txt' + ); + + expect(tree.children('parent')).toEqual([ + 'parent-file.txt', + 'renamed-child', + ]); + }); + + describe('at the root', () => { + it('should return a list of children', () => { + expect(tree.children('')).toEqual(['parent', 'root-file.txt']); + }); + + it('should add a child after writing a file', () => { + tree.write('root-file2.txt', ''); + + expect(tree.children('')).toEqual([ + 'parent', + 'root-file.txt', + 'root-file2.txt', + ]); + }); + + it('should remove a child after deleting a file', () => { + tree.delete('parent/child/child-file.txt'); + tree.delete('parent/parent-file.txt'); + + expect(tree.children('')).not.toContain('parent'); + + tree.delete('root-file.txt'); + + expect(tree.children('')).not.toContain('root-file.txt'); + }); + }); }); it('should be able to rename dirs', () => { diff --git a/packages/tao/src/shared/tree.ts b/packages/tao/src/shared/tree.ts index 2bcce0554f..adacaa1616 100644 --- a/packages/tao/src/shared/tree.ts +++ b/packages/tao/src/shared/tree.ts @@ -122,6 +122,27 @@ export class FsTree implements Tree { this.write(filePath, content); } + delete(filePath: string): void { + filePath = this.normalize(filePath); + if (this.filesForDir(this.rp(filePath)).length > 0) { + this.filesForDir(this.rp(filePath)).forEach( + (f) => (this.recordedChanges[f] = { content: null, isDeleted: true }) + ); + } + this.recordedChanges[this.rp(filePath)] = { + content: null, + isDeleted: true, + }; + + // Delete directories when + if ( + this.exists(dirname(this.rp(filePath))) && + this.children(dirname(this.rp(filePath))).length < 1 + ) { + this.delete(dirname(this.rp(filePath))); + } + } + exists(filePath: string): boolean { filePath = this.normalize(filePath); try { @@ -137,25 +158,12 @@ export class FsTree implements Tree { } } - delete(filePath: string): void { - filePath = this.normalize(filePath); - if (this.filesForDir(this.rp(filePath)).length > 0) { - this.filesForDir(this.rp(filePath)).forEach( - (f) => (this.recordedChanges[f] = { content: null, isDeleted: true }) - ); - } - this.recordedChanges[this.rp(filePath)] = { - content: null, - isDeleted: true, - }; - } - rename(from: string, to: string): void { from = this.normalize(from); to = this.normalize(to); const content = this.read(this.rp(from)); - this.recordedChanges[this.rp(from)] = { content: null, isDeleted: true }; - this.recordedChanges[this.rp(to)] = { content: content, isDeleted: false }; + this.delete(this.rp(from)); + this.write(this.rp(to), content); } isFile(filePath: string): boolean { @@ -176,11 +184,12 @@ export class FsTree implements Tree { let res = this.fsReadDir(dirPath); res = [...res, ...this.directChildrenOfDir(this.rp(dirPath))]; - return res.filter((q) => { + res = res.filter((q) => { const r = this.recordedChanges[join(this.rp(dirPath), q)]; - if (r && r.isDeleted) return false; - return true; + return !r?.isDeleted; }); + // Dedupe + return Array.from(new Set(res)); } listChanges(): FileChange[] { @@ -255,12 +264,18 @@ export class FsTree implements Tree { private directChildrenOfDir(path: string): string[] { const res = {}; + if (path === '') { + return Object.keys(this.recordedChanges).map( + (file) => file.split('/')[0] + ); + } Object.keys(this.recordedChanges).forEach((f) => { if (f.startsWith(path + '/')) { const [_, file] = f.split(path + '/'); res[file.split('/')[0]] = true; } }); + return Object.keys(res); } diff --git a/packages/tao/src/shared/workspace.ts b/packages/tao/src/shared/workspace.ts index e05d42d879..667d47fb21 100644 --- a/packages/tao/src/shared/workspace.ts +++ b/packages/tao/src/shared/workspace.ts @@ -7,6 +7,10 @@ import '../compat/compat'; * Workspace configuration */ export interface WorkspaceConfiguration { + /** + * Version of the configuration format + */ + version: number; /** * Projects' configurations */ @@ -386,7 +390,7 @@ export function reformattedWorkspaceJsonOrNull(w: any) { return w.version === 2 ? toNewFormatOrNull(w) : toOldFormatOrNull(w); } -export function toNewFormat(w: any) { +export function toNewFormat(w: any): WorkspaceConfiguration { const f = toNewFormatOrNull(w); return f ? f : w; } diff --git a/packages/workspace/collection.json b/packages/workspace/collection.json index d183d31a6e..de302b643c 100644 --- a/packages/workspace/collection.json +++ b/packages/workspace/collection.json @@ -24,14 +24,14 @@ }, "move": { - "factory": "./src/schematics/move/move", + "factory": "./src/schematics/move/move#moveSchematic", "schema": "./src/schematics/move/schema.json", "aliases": ["mv"], "description": "Move an application or library to another folder" }, "remove": { - "factory": "./src/schematics/remove/remove", + "factory": "./src/schematics/remove/remove#moveSchematic", "schema": "./src/schematics/remove/schema.json", "aliases": ["rm"], "description": "Remove an application or library" @@ -88,14 +88,14 @@ }, "move": { - "factory": "./src/schematics/move/move", + "factory": "./src/schematics/move/move#moveGenerator", "schema": "./src/schematics/move/schema.json", "aliases": ["mv"], "description": "Move an application or library to another folder" }, "remove": { - "factory": "./src/schematics/remove/remove", + "factory": "./src/schematics/remove/remove#removeGenerator", "schema": "./src/schematics/remove/schema.json", "aliases": ["rm"], "description": "Remove an application or library" diff --git a/packages/workspace/src/command-line/dep-graph.ts b/packages/workspace/src/command-line/dep-graph.ts index 2124e64c2a..4dc3a19a8e 100644 --- a/packages/workspace/src/command-line/dep-graph.ts +++ b/packages/workspace/src/command-line/dep-graph.ts @@ -9,12 +9,9 @@ import { onlyWorkspaceProjects, ProjectGraph, ProjectGraphNode, - ProjectGraphDependency, } from '../core/project-graph'; import { appRootPath } from '../utils/app-root'; import { output } from '../utils/output'; -import { checkProjectExists } from '../utils/rules/check-project-exists'; -import { filter } from '@angular-devkit/schematics'; // maps file extention to MIME types const mimeType = { diff --git a/packages/workspace/src/schematics/move/lib/__snapshots__/update-imports.spec.ts.snap b/packages/workspace/src/schematics/move/lib/__snapshots__/update-imports.spec.ts.snap new file mode 100644 index 0000000000..8a0103dc3c --- /dev/null +++ b/packages/workspace/src/schematics/move/lib/__snapshots__/update-imports.spec.ts.snap @@ -0,0 +1,38 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`updateImports should correctly update deep imports 1`] = ` +" + import { Table } from '@proj/table/components'; + import { Tab } from '@proj/tabs/components'; + + export class MyTable extends Table {}; + export class MyTab extends Tab {}; + " +`; + +exports[`updateImports should not update import paths when they contain a partial match 1`] = ` +" + import { Table } from '@proj/table'; + import { Tab } from '@proj/tabs'; + + export class MyTable extends Table {}; + export class MyTab extends Tab {}; + " +`; + +exports[`updateImports should update dynamic imports 1`] = ` +" + import('@proj/table').then(m => m.Table); + import('@proj/table/components').then(m => m.Table); + import('@proj/tabs').then(m => m.Tab); + import('@proj/tabs/components').then(m => m.Tab); + " +`; + +exports[`updateImports should update project refs 1`] = ` +" + import { MyClass } from '@proj/my-destination'; + + export class MyExtendedClass extends MyClass {}; + " +`; diff --git a/packages/workspace/src/schematics/move/lib/check-destination.spec.ts b/packages/workspace/src/schematics/move/lib/check-destination.spec.ts index 7b5bc518a2..5128c9c3f9 100644 --- a/packages/workspace/src/schematics/move/lib/check-destination.spec.ts +++ b/packages/workspace/src/schematics/move/lib/check-destination.spec.ts @@ -1,15 +1,21 @@ -import { Tree } from '@angular-devkit/schematics'; -import { createEmptyWorkspace } from '@nrwl/workspace/testing'; -import { callRule, runSchematic } from '../../../utils/testing'; +import { + ProjectConfiguration, + readProjectConfiguration, + Tree, +} from '@nrwl/devkit'; +import { createTreeWithEmptyWorkspace } from '@nrwl/devkit/testing'; import { Schema } from '../schema'; import { checkDestination } from './check-destination'; +import { libraryGenerator } from '../../library/library'; -describe('checkDestination Rule', () => { +describe('checkDestination', () => { let tree: Tree; + let projectConfig: ProjectConfiguration; beforeEach(async () => { - tree = createEmptyWorkspace(Tree.empty()); - tree = await runSchematic('lib', { name: 'my-lib' }, tree); + tree = createTreeWithEmptyWorkspace(); + await libraryGenerator(tree, { name: 'my-lib' }); + projectConfig = readProjectConfiguration(tree, 'my-lib'); }); it('should throw an error if the path is not explicit', async () => { @@ -20,13 +26,15 @@ describe('checkDestination Rule', () => { updateImportPath: true, }; - await expect(callRule(checkDestination(schema), tree)).rejects.toThrow( + expect(() => { + checkDestination(tree, schema, projectConfig); + }).toThrow( `Invalid destination: [${schema.destination}] - Please specify explicit path.` ); }); it('should throw an error if the path already exists', async () => { - tree = await runSchematic('lib', { name: 'my-other-lib' }, tree); + await libraryGenerator(tree, { name: 'my-other-lib' }); const schema: Schema = { projectName: 'my-lib', @@ -35,7 +43,9 @@ describe('checkDestination Rule', () => { updateImportPath: true, }; - await expect(callRule(checkDestination(schema), tree)).rejects.toThrow( + expect(() => { + checkDestination(tree, schema, projectConfig); + }).toThrow( `Invalid destination: [${schema.destination}] - Path is not empty.` ); }); @@ -48,9 +58,9 @@ describe('checkDestination Rule', () => { updateImportPath: true, }; - await expect( - callRule(checkDestination(schema), tree) - ).resolves.not.toThrow(); + expect(() => { + checkDestination(tree, schema, projectConfig); + }).not.toThrow(); }); it('should normalize the destination', async () => { @@ -61,7 +71,7 @@ describe('checkDestination Rule', () => { updateImportPath: true, }; - await callRule(checkDestination(schema), tree); + checkDestination(tree, schema, projectConfig); expect(schema.destination).toBe('my-other-lib/wibble'); }); diff --git a/packages/workspace/src/schematics/move/lib/check-destination.ts b/packages/workspace/src/schematics/move/lib/check-destination.ts index f639d5f27e..60c6f86d06 100644 --- a/packages/workspace/src/schematics/move/lib/check-destination.ts +++ b/packages/workspace/src/schematics/move/lib/check-destination.ts @@ -1,8 +1,4 @@ -import { Rule, SchematicContext } from '@angular-devkit/schematics'; -import { Tree } from '@angular-devkit/schematics/src/tree/interface'; -import { getWorkspace } from '@nrwl/workspace'; -import { from, Observable } from 'rxjs'; -import { map } from 'rxjs/operators'; +import { ProjectConfiguration, Tree } from '@nrwl/devkit'; import { Schema } from '../schema'; import { getDestination, normalizeSlashes } from './utils'; @@ -14,30 +10,24 @@ import { getDestination, normalizeSlashes } from './utils'; * * @param schema The options provided to the schematic */ -export function checkDestination(schema: Schema): Rule { - return (tree: Tree, _context: SchematicContext): Observable => { - return from(getWorkspace(tree)).pipe( - map((workspace) => { - const INVALID_DESTINATION = `Invalid destination: [${schema.destination}]`; +export function checkDestination( + tree: Tree, + schema: Schema, + projectConfig: ProjectConfiguration +) { + const INVALID_DESTINATION = `Invalid destination: [${schema.destination}]`; - if (schema.destination.includes('..')) { - throw new Error( - `${INVALID_DESTINATION} - Please specify explicit path.` - ); - } + if (schema.destination.includes('..')) { + throw new Error(`${INVALID_DESTINATION} - Please specify explicit path.`); + } - const destination = getDestination(schema, workspace, tree); + const destination = getDestination(tree, schema, projectConfig); - if (tree.getDir(destination).subfiles.length > 0) { - throw new Error(`${INVALID_DESTINATION} - Path is not empty.`); - } + if (tree.children(destination).length > 0) { + throw new Error(`${INVALID_DESTINATION} - Path is not empty.`); + } - if (schema.destination.startsWith('/')) { - schema.destination = normalizeSlashes(schema.destination.substr(1)); - } - - return tree; - }) - ); - }; + if (schema.destination.startsWith('/')) { + schema.destination = normalizeSlashes(schema.destination.substr(1)); + } } diff --git a/packages/workspace/src/schematics/move/lib/move-project-configuration.spec.ts b/packages/workspace/src/schematics/move/lib/move-project-configuration.spec.ts new file mode 100644 index 0000000000..afd862011f --- /dev/null +++ b/packages/workspace/src/schematics/move/lib/move-project-configuration.spec.ts @@ -0,0 +1,205 @@ +import { + addProjectConfiguration, + readJson, + readProjectConfiguration, + Tree, + updateJson, +} from '@nrwl/devkit'; +import { NxJson } from '@nrwl/workspace'; +import { Schema } from '../schema'; +import { createTreeWithEmptyWorkspace } from '@nrwl/devkit/testing'; +import { moveProjectConfiguration } from '@nrwl/workspace/src/schematics/move/lib/move-project-configuration'; + +describe('moveProjectConfiguration', () => { + let tree: Tree; + let projectConfig; + let schema: Schema; + beforeEach(async () => { + schema = { + projectName: 'my-source', + destination: 'subfolder/my-destination', + importPath: undefined, + updateImportPath: true, + }; + + tree = createTreeWithEmptyWorkspace(); + + addProjectConfiguration(tree, 'my-source', { + projectType: 'application', + root: 'apps/my-source', + sourceRoot: 'apps/my-source/src', + targets: { + build: { + executor: '@angular-devkit/build-angular:browser', + options: { + outputPath: 'dist/apps/my-source', + index: 'apps/my-source/src/index.html', + main: 'apps/my-source/src/main.ts', + polyfills: 'apps/my-source/src/polyfills.ts', + tsConfig: 'apps/my-source/tsconfig.app.json', + aot: false, + assets: [ + 'apps/my-source/src/favicon.ico', + 'apps/my-source/src/assets', + ], + styles: ['apps/my-source/src/styles.scss'], + scripts: [], + }, + configurations: { + production: { + fileReplacements: [ + { + replace: 'apps/my-source/src/environments/environment.ts', + with: 'apps/my-source/src/environments/environment.prod.ts', + }, + ], + optimization: true, + outputHashing: 'all', + sourceMap: false, + extractCss: true, + namedChunks: false, + aot: true, + extractLicenses: true, + vendorChunk: false, + buildOptimizer: true, + budgets: [ + { + type: 'initial', + maximumWarning: '2mb', + maximumError: '5mb', + }, + { + type: 'anyComponentStyle', + maximumWarning: '6kb', + maximumError: '10kb', + }, + ], + }, + }, + }, + serve: { + executor: '@angular-devkit/build-angular:dev-server', + options: { + browserTarget: 'my-source:build', + }, + configurations: { + production: { + browserTarget: 'my-source:build:production', + }, + }, + }, + 'extract-i18n': { + executor: '@angular-devkit/build-angular:extract-i18n', + options: { + browserTarget: 'my-source:build', + }, + }, + lint: { + executor: '@angular-devkit/build-angular:tslint', + options: { + tsConfig: [ + 'apps/my-source/tsconfig.app.json', + 'apps/my-source/tsconfig.spec.json', + ], + exclude: ['**/node_modules/**', '!apps/my-source/**/*'], + }, + }, + test: { + executor: '@nrwl/jest:jest', + options: { + jestConfig: 'apps/my-source/jest.config.js', + tsConfig: 'apps/my-source/tsconfig.spec.json', + setupFile: 'apps/my-source/src/test-setup.ts', + }, + }, + }, + tags: ['type:ui'], + implicitDependencies: ['my-other-lib'], + }); + + addProjectConfiguration(tree, 'my-source-e2e', { + root: 'apps/my-source-e2e', + sourceRoot: 'apps/my-source-e2e/src', + projectType: 'application', + targets: { + e2e: { + executor: '@nrwl/cypress:cypress', + options: { + cypressConfig: 'apps/my-source-e2e/cypress.json', + tsConfig: 'apps/my-source-e2e/tsconfig.e2e.json', + devServerTarget: 'my-source:serve', + }, + configurations: { + production: { + devServerTarget: 'my-source:serve:production', + }, + }, + }, + lint: { + executor: '@angular-devkit/build-angular:tslint', + options: { + tsConfig: ['apps/my-source-e2e/tsconfig.e2e.json'], + exclude: ['**/node_modules/**', '!apps/my-source-e2e/**/*'], + }, + }, + }, + }); + + projectConfig = readProjectConfiguration(tree, 'my-source'); + }); + it('should rename the project', async () => { + moveProjectConfiguration(tree, schema, projectConfig); + + expect(() => { + readProjectConfiguration(tree, 'my-source'); + }).toThrow(); + + expect( + readProjectConfiguration(tree, 'subfolder-my-destination') + ).toBeDefined(); + }); + + it('should update paths in only the intended project', async () => { + moveProjectConfiguration(tree, schema, projectConfig); + + const actualProject = readProjectConfiguration( + tree, + 'subfolder-my-destination' + ); + expect(actualProject).toBeDefined(); + expect(actualProject.root).toBe('apps/subfolder/my-destination'); + expect(actualProject.root).toBe('apps/subfolder/my-destination'); + + const similarProject = readProjectConfiguration(tree, 'my-source-e2e'); + expect(similarProject).toBeDefined(); + expect(similarProject.root).toBe('apps/my-source-e2e'); + }); + + it('honor custom workspace layouts', async () => { + updateJson(tree, 'nx.json', (json) => { + json.workspaceLayout = { appsDir: 'e2e', libsDir: 'packages' }; + return json; + }); + + moveProjectConfiguration(tree, schema, projectConfig); + + const project = readProjectConfiguration(tree, 'subfolder-my-destination'); + expect(project).toBeDefined(); + expect(project.root).toBe('e2e/subfolder/my-destination'); + expect(project.sourceRoot).toBe('e2e/subfolder/my-destination/src'); + }); + + it('should update nx.json', () => { + moveProjectConfiguration(tree, schema, projectConfig); + + const actualProject = readProjectConfiguration( + tree, + 'subfolder-my-destination' + ); + + expect(actualProject.tags).toEqual(['type:ui']); + expect(actualProject.implicitDependencies).toEqual(['my-other-lib']); + + expect(readJson(tree, 'nx.json').projects['my-source']).not.toBeDefined(); + }); +}); diff --git a/packages/workspace/src/schematics/move/lib/move-project-configuration.ts b/packages/workspace/src/schematics/move/lib/move-project-configuration.ts new file mode 100644 index 0000000000..6a63a741cf --- /dev/null +++ b/packages/workspace/src/schematics/move/lib/move-project-configuration.ts @@ -0,0 +1,36 @@ +import { + addProjectConfiguration, + removeProjectConfiguration, + NxJsonProjectConfiguration, + ProjectConfiguration, + Tree, +} from '@nrwl/devkit'; + +import { Schema } from '../schema'; +import { getDestination, getNewProjectName } from './utils'; + +export function moveProjectConfiguration( + tree: Tree, + schema: Schema, + projectConfig: ProjectConfiguration & NxJsonProjectConfiguration +) { + let destination = getDestination(tree, schema, projectConfig); + const projectString = JSON.stringify(projectConfig); + const newProjectString = projectString.replace( + new RegExp(projectConfig.root, 'g'), + destination + ); + + // rename + const newProject: ProjectConfiguration = JSON.parse(newProjectString); + + // Delete the original project + removeProjectConfiguration(tree, schema.projectName); + + // Create a new project with the root replaced + addProjectConfiguration( + tree, + getNewProjectName(schema.destination), + newProject + ); +} diff --git a/packages/workspace/src/schematics/move/lib/move-project.spec.ts b/packages/workspace/src/schematics/move/lib/move-project.spec.ts index 29f95f9de1..39079d8122 100644 --- a/packages/workspace/src/schematics/move/lib/move-project.spec.ts +++ b/packages/workspace/src/schematics/move/lib/move-project.spec.ts @@ -1,14 +1,21 @@ -import { Tree } from '@angular-devkit/schematics'; -import { createEmptyWorkspace } from '@nrwl/workspace/testing'; -import { runSchematic } from '../../../utils/testing'; +import { + ProjectConfiguration, + readProjectConfiguration, + Tree, +} from '@nrwl/devkit'; +import { createTreeWithEmptyWorkspace } from '@nrwl/devkit/testing'; import { Schema } from '../schema'; +import { libraryGenerator } from '../../library/library'; +import { moveProject } from '@nrwl/workspace/src/schematics/move/lib/move-project'; -describe('moveProject Rule', () => { +describe('moveProject', () => { let tree: Tree; + let projectConfig: ProjectConfiguration; beforeEach(async () => { - tree = createEmptyWorkspace(Tree.empty()); - tree = await runSchematic('lib', { name: 'my-lib' }, tree); + tree = createTreeWithEmptyWorkspace(); + await libraryGenerator(tree, { name: 'my-lib' }); + projectConfig = readProjectConfiguration(tree, 'my-lib'); }); it('should copy all files and delete the source folder', async () => { @@ -19,22 +26,12 @@ describe('moveProject Rule', () => { updateImportPath: true, }; - // TODO - Currently this test will fail due to - // https://github.com/angular/angular-cli/issues/16527 - // host = await callRule(moveProject(schema), host); + moveProject(tree, schema, projectConfig); - // const destinationDir = host.getDir('libs/my-destination'); - // let filesFound = false; - // destinationDir.visit(_file => { - // filesFound = true; - // }); - // expect(filesFound).toBeTruthy(); + const destinationChildren = tree.children('libs/my-destination'); + expect(destinationChildren.length).toBeGreaterThan(0); - // const sourceDir = host.getDir('libs/my-lib'); - // filesFound = false; - // sourceDir.visit(_file => { - // filesFound = true; - // }); - // expect(filesFound).toBeFalsy(); + expect(tree.exists('libs/my-lib')).toBeFalsy(); + expect(tree.children('libs')).not.toContain('my-lib'); }); }); diff --git a/packages/workspace/src/schematics/move/lib/move-project.ts b/packages/workspace/src/schematics/move/lib/move-project.ts index 4babf5e887..1fbaaa862b 100644 --- a/packages/workspace/src/schematics/move/lib/move-project.ts +++ b/packages/workspace/src/schematics/move/lib/move-project.ts @@ -1,4 +1,5 @@ -import { SchematicContext, Tree } from '@angular-devkit/schematics'; +import { SchematicContext } from '@angular-devkit/schematics'; +import { ProjectConfiguration, Tree, visitNotIgnoredFiles } from '@nrwl/devkit'; import { getWorkspace } from '@nrwl/workspace'; import { from, Observable } from 'rxjs'; import { map } from 'rxjs/operators'; @@ -10,23 +11,18 @@ import { getDestination } from './utils'; * * @param schema The options provided to the schematic */ -export function moveProject(schema: Schema) { - return (tree: Tree, _context: SchematicContext): Observable => { - return from(getWorkspace(tree)).pipe( - map((workspace) => { - const project = workspace.projects.get(schema.projectName); +export function moveProject( + tree: Tree, + schema: Schema, + project: ProjectConfiguration +) { + const destination = getDestination(tree, schema, project); + visitNotIgnoredFiles(tree, project.root, (file) => { + // This is a rename but Angular Devkit isn't capable of writing to a file after it's renamed so this is a workaround + const content = tree.read(file); + tree.write(file.replace(project.root, destination), content); + tree.delete(file); + }); - const destination = getDestination(schema, workspace, tree); - const dir = tree.getDir(project.root); - dir.visit((file) => { - const newPath = file.replace(project.root, destination); - tree.create(newPath, tree.read(file)); - }); - - tree.delete(project.root); - - return tree; - }) - ); - }; + tree.delete(project.root); } diff --git a/packages/workspace/src/schematics/move/lib/update-build-targets.spec.ts b/packages/workspace/src/schematics/move/lib/update-build-targets.spec.ts new file mode 100644 index 0000000000..6ab0a72e1a --- /dev/null +++ b/packages/workspace/src/schematics/move/lib/update-build-targets.spec.ts @@ -0,0 +1,57 @@ +import { Schema } from '../schema'; +import { + addProjectConfiguration, + readProjectConfiguration, + Tree, +} from '@nrwl/devkit'; +import { createTreeWithEmptyWorkspace } from '@nrwl/devkit/testing'; + +import { updateBuildTargets } from './update-build-targets'; + +describe('updateBuildTargets', () => { + let tree: Tree; + let schema: Schema; + + beforeEach(async () => { + schema = { + projectName: 'my-source', + destination: 'subfolder/my-destination', + importPath: undefined, + updateImportPath: true, + }; + tree = createTreeWithEmptyWorkspace(); + addProjectConfiguration(tree, 'my-source', { + root: 'libs/my-source', + targets: {}, + }); + addProjectConfiguration(tree, 'my-source-e2e', { + root: 'libs/my-source', + targets: { + e2e: { + executor: 'test-executor:hi', + options: { + devServerTarget: 'my-source:serve', + }, + configurations: { + production: { + devServerTarget: 'my-source:serve:production', + }, + }, + }, + }, + }); + }); + + it('should update build targets', async () => { + updateBuildTargets(tree, schema); + + const e2eProject = readProjectConfiguration(tree, 'my-source-e2e'); + expect(e2eProject).toBeDefined(); + expect(e2eProject.targets.e2e.options.devServerTarget).toBe( + 'subfolder-my-destination:serve' + ); + expect( + e2eProject.targets.e2e.configurations.production.devServerTarget + ).toBe('subfolder-my-destination:serve:production'); + }); +}); diff --git a/packages/workspace/src/schematics/move/lib/update-build-targets.ts b/packages/workspace/src/schematics/move/lib/update-build-targets.ts new file mode 100644 index 0000000000..3163487b6d --- /dev/null +++ b/packages/workspace/src/schematics/move/lib/update-build-targets.ts @@ -0,0 +1,21 @@ +import { getWorkspacePath, Tree, updateJson } from '@nrwl/devkit'; +import { Schema } from '../schema'; +import { getNewProjectName } from './utils'; + +/** + * Update other references to the source project's targets + */ +export function updateBuildTargets(tree: Tree, schema: Schema) { + const newProjectName = getNewProjectName(schema.destination); + + updateJson(tree, getWorkspacePath(tree), (json) => { + const strWorkspace = JSON.stringify(json); + json = JSON.parse( + strWorkspace.replace( + new RegExp(`${schema.projectName}:`, 'g'), + `${newProjectName}:` + ) + ); + return json; + }); +} diff --git a/packages/workspace/src/schematics/move/lib/update-cypress-json.spec.ts b/packages/workspace/src/schematics/move/lib/update-cypress-json.spec.ts index 2253603f29..546ea42e15 100644 --- a/packages/workspace/src/schematics/move/lib/update-cypress-json.spec.ts +++ b/packages/workspace/src/schematics/move/lib/update-cypress-json.spec.ts @@ -1,34 +1,37 @@ -import { Tree } from '@angular-devkit/schematics'; -import { UnitTestTree } from '@angular-devkit/schematics/testing'; -import { readJsonInTree } from '@nrwl/workspace'; -import { createEmptyWorkspace } from '@nrwl/workspace/testing'; -import { callRule, runSchematic } from '../../../utils/testing'; +import { + ProjectConfiguration, + readProjectConfiguration, + Tree, + readJson, + writeJson, +} from '@nrwl/devkit'; +import { createTreeWithEmptyWorkspace } from '@nrwl/devkit/testing'; import { Schema } from '../schema'; import { updateCypressJson } from './update-cypress-json'; +import { libraryGenerator } from '../../library/library'; -describe('updateCypressJson Rule', () => { - let tree: UnitTestTree; +describe('updateCypressJson', () => { + let tree: Tree; + let schema: Schema; + let projectConfig: ProjectConfiguration; beforeEach(async () => { - tree = new UnitTestTree(Tree.empty()); - tree = createEmptyWorkspace(tree) as UnitTestTree; - }); - - it('should handle cypress.json not existing', async () => { - tree = await runSchematic('lib', { name: 'my-lib' }, tree); - - expect(tree.files).not.toContain('/libs/my-destination/cypress.json'); - - const schema: Schema = { + schema = { projectName: 'my-lib', destination: 'my-destination', importPath: undefined, updateImportPath: true, }; - await expect( - callRule(updateCypressJson(schema), tree) - ).resolves.not.toThrow(); + tree = createTreeWithEmptyWorkspace(); + await libraryGenerator(tree, { name: 'my-lib' }); + projectConfig = readProjectConfiguration(tree, 'my-lib'); + }); + + it('should handle cypress.json not existing', async () => { + expect(() => { + updateCypressJson(tree, schema, projectConfig); + }).not.toThrow(); }); it('should update the videos and screenshots folders', async () => { @@ -44,24 +47,11 @@ describe('updateCypressJson Rule', () => { chromeWebSecurity: false, }; - tree = await runSchematic('lib', { name: 'my-lib' }, tree); - tree.create( - '/libs/my-destination/cypress.json', - JSON.stringify(cypressJson) - ); + writeJson(tree, '/libs/my-destination/cypress.json', cypressJson); - expect(tree.files).toContain('/libs/my-destination/cypress.json'); + updateCypressJson(tree, schema, projectConfig); - const schema: Schema = { - projectName: 'my-lib', - destination: 'my-destination', - importPath: undefined, - updateImportPath: true, - }; - - tree = (await callRule(updateCypressJson(schema), tree)) as UnitTestTree; - - expect(readJsonInTree(tree, '/libs/my-destination/cypress.json')).toEqual({ + expect(readJson(tree, '/libs/my-destination/cypress.json')).toEqual({ ...cypressJson, videosFolder: '../../dist/cypress/libs/my-destination/videos', screenshotsFolder: '../../dist/cypress/libs/my-destination/screenshots', diff --git a/packages/workspace/src/schematics/move/lib/update-cypress-json.ts b/packages/workspace/src/schematics/move/lib/update-cypress-json.ts index bc0ee760e2..46e09aabd4 100644 --- a/packages/workspace/src/schematics/move/lib/update-cypress-json.ts +++ b/packages/workspace/src/schematics/move/lib/update-cypress-json.ts @@ -1,11 +1,8 @@ -import { Rule, SchematicContext } from '@angular-devkit/schematics'; -import { Tree } from '@angular-devkit/schematics/src/tree/interface'; -import { getWorkspace } from '@nrwl/workspace'; +import { Tree } from '@nrwl/devkit'; import * as path from 'path'; -import { from, Observable } from 'rxjs'; -import { map } from 'rxjs/operators'; import { Schema } from '../schema'; import { getDestination } from './utils'; +import { ProjectConfiguration } from '@nrwl/tao/src/shared/workspace'; interface PartialCypressJson { videosFolder: string; @@ -19,36 +16,31 @@ interface PartialCypressJson { * * @param schema The options provided to the schematic */ -export function updateCypressJson(schema: Schema): Rule { - return (tree: Tree, _context: SchematicContext): Observable => { - return from(getWorkspace(tree)).pipe( - map((workspace) => { - const project = workspace.projects.get(schema.projectName); - const destination = getDestination(schema, workspace, tree); +export function updateCypressJson( + tree: Tree, + schema: Schema, + project: ProjectConfiguration +) { + const destination = getDestination(tree, schema, project); - const cypressJsonPath = path.join(destination, 'cypress.json'); + const cypressJsonPath = path.join(destination, 'cypress.json'); - if (!tree.exists(cypressJsonPath)) { - // nothing to do - return tree; - } + if (!tree.exists(cypressJsonPath)) { + // nothing to do + return tree; + } - const cypressJson = JSON.parse( - tree.read(cypressJsonPath).toString('utf-8') - ) as PartialCypressJson; - cypressJson.videosFolder = cypressJson.videosFolder.replace( - project.root, - destination - ); - cypressJson.screenshotsFolder = cypressJson.screenshotsFolder.replace( - project.root, - destination - ); + const cypressJson = JSON.parse( + tree.read(cypressJsonPath).toString('utf-8') + ) as PartialCypressJson; + cypressJson.videosFolder = cypressJson.videosFolder.replace( + project.root, + destination + ); + cypressJson.screenshotsFolder = cypressJson.screenshotsFolder.replace( + project.root, + destination + ); - tree.overwrite(cypressJsonPath, JSON.stringify(cypressJson)); - - return tree; - }) - ); - }; + tree.write(cypressJsonPath, JSON.stringify(cypressJson)); } diff --git a/packages/workspace/src/schematics/move/lib/update-default-project.spec.ts b/packages/workspace/src/schematics/move/lib/update-default-project.spec.ts new file mode 100644 index 0000000000..d37eb6fbb7 --- /dev/null +++ b/packages/workspace/src/schematics/move/lib/update-default-project.spec.ts @@ -0,0 +1,43 @@ +import { Schema } from '@nrwl/workspace/src/schematics/move/schema'; +import { + addProjectConfiguration, + readWorkspaceConfiguration, + Tree, + updateWorkspaceConfiguration, +} from '@nrwl/devkit'; +import { createTreeWithEmptyWorkspace } from '@nrwl/devkit/testing'; +import { updateDefaultProject } from '@nrwl/workspace/src/schematics/move/lib/update-default-project'; + +describe('updateDefaultProject', () => { + let tree: Tree; + + beforeEach(() => { + tree = createTreeWithEmptyWorkspace(); + addProjectConfiguration(tree, 'my-source', { + root: 'libs/my-source', + targets: {}, + }); + + const workspace = readWorkspaceConfiguration(tree); + + updateWorkspaceConfiguration(tree, { + ...workspace, + defaultProject: 'my-source', + }); + }); + + it('should update the default project', async () => { + const schema: Schema = { + projectName: 'my-source', + destination: 'subfolder/my-destination', + importPath: undefined, + updateImportPath: true, + }; + + updateDefaultProject(tree, schema); + + const { defaultProject } = readWorkspaceConfiguration(tree); + + expect(defaultProject).toBe('subfolder-my-destination'); + }); +}); diff --git a/packages/workspace/src/schematics/move/lib/update-default-project.ts b/packages/workspace/src/schematics/move/lib/update-default-project.ts new file mode 100644 index 0000000000..56d17e3488 --- /dev/null +++ b/packages/workspace/src/schematics/move/lib/update-default-project.ts @@ -0,0 +1,29 @@ +import { + readWorkspaceConfiguration, + Tree, + updateWorkspaceConfiguration, +} from '@nrwl/devkit'; +import { Schema } from '../schema'; +import { getNewProjectName } from './utils'; + +/** + * Updates the project in the workspace file + * + * - update all references to the old root path + * - change the project name + * - change target references + */ +export function updateDefaultProject(tree: Tree, schema: Schema) { + const workspaceConfiguration = readWorkspaceConfiguration(tree); + + // update default project (if necessary) + if ( + workspaceConfiguration.defaultProject && + workspaceConfiguration.defaultProject === schema.projectName + ) { + workspaceConfiguration.defaultProject = getNewProjectName( + schema.destination + ); + updateWorkspaceConfiguration(tree, workspaceConfiguration); + } +} diff --git a/packages/workspace/src/schematics/move/lib/update-eslintrc-json.spec.ts b/packages/workspace/src/schematics/move/lib/update-eslintrc-json.spec.ts index 3691233f1b..7804d773f7 100644 --- a/packages/workspace/src/schematics/move/lib/update-eslintrc-json.spec.ts +++ b/packages/workspace/src/schematics/move/lib/update-eslintrc-json.spec.ts @@ -1,109 +1,62 @@ -import { UnitTestTree } from '@angular-devkit/schematics/testing'; -import { Tree } from '@angular-devkit/schematics'; -import { - callRule, - createEmptyWorkspace, - runSchematic, -} from '@nrwl/workspace/testing'; -import { Schema } from '@nrwl/workspace/src/schematics/move/schema'; -import { Linter, readJsonInTree } from '@nrwl/workspace'; -import { updateEslintrcJson } from '@nrwl/workspace/src/schematics/move/lib/update-eslintrc-json'; +import { readJson, readProjectConfiguration, Tree } from '@nrwl/devkit'; +import { createTreeWithEmptyWorkspace } from '@nrwl/devkit/testing'; -describe('updateEslint Rule', () => { - let tree: UnitTestTree; +import { Linter } from '@nrwl/workspace'; + +import { Schema } from '../schema'; +import { updateEslintrcJson } from './update-eslintrc-json'; +import { libraryGenerator } from '../../library/library'; + +describe('updateEslint', () => { + let tree: Tree; + let schema: Schema; beforeEach(async () => { - tree = new UnitTestTree(Tree.empty()); - tree = createEmptyWorkspace(tree) as UnitTestTree; + schema = { + projectName: 'my-lib', + destination: 'shared/my-destination', + importPath: undefined, + updateImportPath: true, + }; + + tree = createTreeWithEmptyWorkspace(); }); it('should handle .eslintrc.json not existing', async () => { - tree = await runSchematic( - 'lib', - { name: 'my-lib', linter: Linter.TsLint }, - tree - ); + await libraryGenerator(tree, { + name: 'my-lib', + linter: Linter.TsLint, + }); - expect(tree.files).not.toContain('/libs/my-destination/.estlintrc.json'); + const projectConfig = readProjectConfiguration(tree, 'my-lib'); - const schema: Schema = { - projectName: 'my-lib', - destination: 'my-destination', - importPath: undefined, - updateImportPath: true, - }; - - await expect( - callRule(updateEslintrcJson(schema), tree) - ).resolves.not.toThrow(); + expect(() => { + updateEslintrcJson(tree, schema, projectConfig); + }).not.toThrow(); }); it('should update .eslintrc.json extends path when project is moved to subdirectory', async () => { - const eslintRc = { - extends: '../../.eslintrc.json', - rules: {}, - ignorePatterns: ['!**/*'], - }; + await libraryGenerator(tree, { + name: 'my-lib', + linter: Linter.EsLint, + }); - tree = await runSchematic( - 'lib', - { name: 'my-lib', linter: Linter.EsLint }, - tree + // This step is usually handled elsewhere + tree.rename( + 'libs/my-lib/.eslintrc.json', + 'libs/shared/my-destination/.eslintrc.json' ); - tree.create('/libs/core/my-lib/.eslintrc.json', JSON.stringify(eslintRc)); + const projectConfig = readProjectConfiguration(tree, 'my-lib'); - expect(tree.files).toContain('/libs/core/my-lib/.eslintrc.json'); + updateEslintrcJson(tree, schema, projectConfig); - const schema: Schema = { - projectName: 'my-lib', - destination: 'core/my-lib', - importPath: undefined, - updateImportPath: true, - }; - - tree = (await callRule(updateEslintrcJson(schema), tree)) as UnitTestTree; - - expect(readJsonInTree(tree, '/libs/core/my-lib/.eslintrc.json')).toEqual( + expect( + readJson(tree, '/libs/shared/my-destination/.eslintrc.json') + ).toEqual( jasmine.objectContaining({ extends: '../../../.eslintrc.json', }) ); }); - - it('should update .eslintrc.json extends path when is renamed', async () => { - const eslintRc = { - extends: '../../.eslintrc.json', - rules: {}, - ignorePatterns: ['!**/*'], - }; - - tree = await runSchematic( - 'lib', - { name: 'my-lib', linter: Linter.EsLint }, - tree - ); - - tree.create( - '/libs/my-destination/.eslintrc.json', - JSON.stringify(eslintRc) - ); - - expect(tree.files).toContain('/libs/my-destination/.eslintrc.json'); - - const schema: Schema = { - projectName: 'my-lib', - destination: 'my-destination', - importPath: undefined, - updateImportPath: true, - }; - - tree = (await callRule(updateEslintrcJson(schema), tree)) as UnitTestTree; - - expect(readJsonInTree(tree, '/libs/my-destination/.eslintrc.json')).toEqual( - jasmine.objectContaining({ - extends: '../../.eslintrc.json', - }) - ); - }); }); diff --git a/packages/workspace/src/schematics/move/lib/update-eslintrc-json.ts b/packages/workspace/src/schematics/move/lib/update-eslintrc-json.ts index 72fc815718..d899cd028a 100644 --- a/packages/workspace/src/schematics/move/lib/update-eslintrc-json.ts +++ b/packages/workspace/src/schematics/move/lib/update-eslintrc-json.ts @@ -1,11 +1,14 @@ -import { Rule, SchematicContext, Tree } from '@angular-devkit/schematics'; -import { Schema } from '../schema'; -import { from, Observable } from 'rxjs'; -import { getWorkspace } from '@nrwl/workspace'; -import { map } from 'rxjs/operators'; import { join } from 'path'; -import { offsetFromRoot } from '@nrwl/devkit'; -import { getDestination } from '@nrwl/workspace/src/schematics/move/lib/utils'; +import { + offsetFromRoot, + ProjectConfiguration, + readJson, + Tree, + updateJson, +} from '@nrwl/devkit'; + +import { Schema } from '../schema'; +import { getDestination } from './utils'; interface PartialEsLintRcJson { extends: string; @@ -16,30 +19,24 @@ interface PartialEsLintRcJson { * * @param schema The options provided to the schematic */ -export function updateEslintrcJson(schema: Schema): Rule { - return (tree: Tree, _context: SchematicContext): Observable => { - return from(getWorkspace(tree)).pipe( - map((workspace) => { - const destination = getDestination(schema, workspace, tree); - const eslintRcPath = join(destination, '.eslintrc.json'); +export function updateEslintrcJson( + tree: Tree, + schema: Schema, + project: ProjectConfiguration +) { + const destination = getDestination(tree, schema, project); + const eslintRcPath = join(destination, '.eslintrc.json'); - if (!tree.exists(eslintRcPath)) { - // no .eslintrc found. nothing to do - return tree; - } + if (!tree.exists(eslintRcPath)) { + // no .eslintrc found. nothing to do + return; + } - const eslintRcJson = JSON.parse( - tree.read(eslintRcPath).toString('utf-8') - ) as PartialEsLintRcJson; + const offset = offsetFromRoot(destination); - const offset = offsetFromRoot(destination); + updateJson(tree, eslintRcPath, (eslintRcJson) => { + eslintRcJson.extends = offset + '.eslintrc.json'; - eslintRcJson.extends = offset + '.eslintrc.json'; - - tree.overwrite(eslintRcPath, JSON.stringify(eslintRcJson)); - - return tree; - }) - ); - }; + return eslintRcJson; + }); } diff --git a/packages/workspace/src/schematics/move/lib/update-implicit-dependencies.spec.ts b/packages/workspace/src/schematics/move/lib/update-implicit-dependencies.spec.ts new file mode 100644 index 0000000000..0d25a2daed --- /dev/null +++ b/packages/workspace/src/schematics/move/lib/update-implicit-dependencies.spec.ts @@ -0,0 +1,45 @@ +import { + addProjectConfiguration, + readProjectConfiguration, + Tree, +} from '@nrwl/devkit'; +import { createTreeWithEmptyWorkspace } from '@nrwl/devkit/testing'; + +import { Schema } from '../schema'; +import { updateImplicitDependencies } from './update-implicit-dependencies'; + +describe('updateImplicitDepenencies', () => { + let tree: Tree; + let schema: Schema; + + beforeEach(async () => { + schema = { + projectName: 'my-lib', + destination: 'my-destination', + importPath: undefined, + updateImportPath: true, + }; + + tree = createTreeWithEmptyWorkspace(); + + addProjectConfiguration(tree, 'my-lib', { + root: 'libs/my-lib', + targets: {}, + }); + addProjectConfiguration(tree, 'my-other-lib', { + root: 'libs/my-other-lib', + targets: {}, + implicitDependencies: ['my-lib'], + }); + }); + + it('should update implicit dependencies onto the moved project', () => { + updateImplicitDependencies(tree, schema); + + const { implicitDependencies } = readProjectConfiguration( + tree, + 'my-other-lib' + ); + expect(implicitDependencies).toEqual(['my-destination']); + }); +}); diff --git a/packages/workspace/src/schematics/move/lib/update-nx-json.ts b/packages/workspace/src/schematics/move/lib/update-implicit-dependencies.ts similarity index 63% rename from packages/workspace/src/schematics/move/lib/update-nx-json.ts rename to packages/workspace/src/schematics/move/lib/update-implicit-dependencies.ts index 92f21dd245..816f185fac 100644 --- a/packages/workspace/src/schematics/move/lib/update-nx-json.ts +++ b/packages/workspace/src/schematics/move/lib/update-implicit-dependencies.ts @@ -1,4 +1,6 @@ -import { NxJson, updateJsonInTree } from '@nrwl/workspace'; +import { Tree, updateJson } from '@nrwl/devkit'; + +import { NxJson } from '../../../core/shared-interfaces'; import { Schema } from '../schema'; import { getNewProjectName } from './utils'; @@ -7,8 +9,8 @@ import { getNewProjectName } from './utils'; * * @param schema The options provided to the schematic */ -export function updateNxJson(schema: Schema) { - return updateJsonInTree('nx.json', (json) => { +export function updateImplicitDependencies(tree: Tree, schema: Schema) { + updateJson(tree, 'nx.json', (json) => { Object.values(json.projects).forEach((project) => { if (project.implicitDependencies) { const index = project.implicitDependencies.indexOf(schema.projectName); @@ -19,10 +21,6 @@ export function updateNxJson(schema: Schema) { } } }); - json.projects[getNewProjectName(schema.destination)] = { - ...json.projects[schema.projectName], - }; - delete json.projects[schema.projectName]; return json; }); } diff --git a/packages/workspace/src/schematics/move/lib/update-imports.spec.ts b/packages/workspace/src/schematics/move/lib/update-imports.spec.ts index 8b448003f2..5c20ae7df5 100644 --- a/packages/workspace/src/schematics/move/lib/update-imports.spec.ts +++ b/packages/workspace/src/schematics/move/lib/update-imports.spec.ts @@ -1,18 +1,16 @@ -import { Tree } from '@angular-devkit/schematics'; -import { UnitTestTree } from '@angular-devkit/schematics/testing'; -import { readJsonInTree } from '@nrwl/workspace'; -import { createEmptyWorkspace } from '@nrwl/workspace/testing'; +import { readProjectConfiguration, Tree } from '@nrwl/devkit'; import { callRule, runSchematic } from '../../../utils/testing'; import { Schema } from '../schema'; import { updateImports } from './update-imports'; +import { libraryGenerator } from '../../library/library'; +import { createTreeWithEmptyWorkspace } from '@nrwl/devkit/testing'; -describe('updateImports Rule', () => { - let tree: UnitTestTree; +describe('updateImports', () => { + let tree: Tree; let schema: Schema; beforeEach(async () => { - tree = new UnitTestTree(Tree.empty()); - tree = createEmptyWorkspace(tree) as UnitTestTree; + tree = createTreeWithEmptyWorkspace(); schema = { projectName: 'my-source', @@ -26,25 +24,24 @@ describe('updateImports Rule', () => { // this is a bit of a cheat - we expect to run this rule on an intermediate state // tree where the workspace hasn't been updated yet, so just create libs representing // source and destination to make sure that the workspace has libraries with those names. - tree = await runSchematic('lib', { name: 'my-destination' }, tree); - tree = await runSchematic('lib', { name: 'my-source' }, tree); + await libraryGenerator(tree, { name: 'my-destination' }); + await libraryGenerator(tree, { name: 'my-source' }); - tree = await runSchematic('lib', { name: 'my-importer' }, tree); + await libraryGenerator(tree, { name: 'my-importer' }); const importerFilePath = 'libs/my-importer/src/importer.ts'; - tree.create( + tree.write( importerFilePath, ` import { MyClass } from '@proj/my-source'; - + export class MyExtendedClass extends MyClass {}; ` ); - tree = (await callRule(updateImports(schema), tree)) as UnitTestTree; + const projectConfig = readProjectConfiguration(tree, 'my-source'); + updateImports(tree, schema, projectConfig); - expect(tree.read(importerFilePath).toString()).toContain( - `import { MyClass } from '@proj/my-destination';` - ); + expect(tree.read(importerFilePath).toString()).toMatchSnapshot(); }); /** @@ -53,31 +50,33 @@ describe('updateImports Rule', () => { * be updated. */ it('should not update import paths when they contain a partial match', async () => { - tree = await runSchematic('lib', { name: 'table' }, tree); - tree = await runSchematic('lib', { name: 'tab' }, tree); + await libraryGenerator(tree, { name: 'table' }); + await libraryGenerator(tree, { name: 'tab' }); - tree = await runSchematic('lib', { name: 'my-importer' }, tree); + await libraryGenerator(tree, { name: 'my-importer' }); const importerFilePath = 'libs/my-importer/src/importer.ts'; - tree.create( + tree.write( importerFilePath, ` import { Table } from '@proj/table'; import { Tab } from '@proj/tab'; - + export class MyTable extends Table {}; export class MyTab extends Tab {}; ` ); - tree = (await callRule( - updateImports({ + const projectConfig = readProjectConfiguration(tree, 'tab'); + updateImports( + tree, + { projectName: 'tab', destination: 'tabs', importPath: undefined, updateImportPath: true, - }), - tree - )) as UnitTestTree; + }, + projectConfig + ); expect(tree.read(importerFilePath).toString()).toContain( `import { Table } from '@proj/table';` @@ -86,34 +85,38 @@ describe('updateImports Rule', () => { expect(tree.read(importerFilePath).toString()).toContain( `import { Tab } from '@proj/tabs';` ); + + expect(tree.read(importerFilePath).toString()).toMatchSnapshot(); }); it('should correctly update deep imports', async () => { - tree = await runSchematic('lib', { name: 'table' }, tree); - tree = await runSchematic('lib', { name: 'tab' }, tree); + await libraryGenerator(tree, { name: 'table' }); + await libraryGenerator(tree, { name: 'tab' }); - tree = await runSchematic('lib', { name: 'my-importer' }, tree); + await libraryGenerator(tree, { name: 'my-importer' }); const importerFilePath = 'libs/my-importer/src/importer.ts'; - tree.create( + tree.write( importerFilePath, ` import { Table } from '@proj/table/components'; import { Tab } from '@proj/tab/components'; - + export class MyTable extends Table {}; export class MyTab extends Tab {}; ` ); - tree = (await callRule( - updateImports({ + const projectConfig = readProjectConfiguration(tree, 'tab'); + updateImports( + tree, + { projectName: 'tab', destination: 'tabs', importPath: undefined, updateImportPath: true, - }), - tree - )) as UnitTestTree; + }, + projectConfig + ); expect(tree.read(importerFilePath).toString()).toContain( `import { Table } from '@proj/table/components';` @@ -122,15 +125,17 @@ describe('updateImports Rule', () => { expect(tree.read(importerFilePath).toString()).toContain( `import { Tab } from '@proj/tabs/components';` ); + + expect(tree.read(importerFilePath).toString()).toMatchSnapshot(); }); it('should update dynamic imports', async () => { - tree = await runSchematic('lib', { name: 'table' }, tree); - tree = await runSchematic('lib', { name: 'tab' }, tree); + await libraryGenerator(tree, { name: 'table' }); + await libraryGenerator(tree, { name: 'tab' }); - tree = await runSchematic('lib', { name: 'my-importer' }, tree); + await libraryGenerator(tree, { name: 'my-importer' }); const importerFilePath = 'libs/my-importer/src/importer.ts'; - tree.create( + tree.write( importerFilePath, ` import('@proj/table').then(m => m.Table); @@ -140,15 +145,17 @@ describe('updateImports Rule', () => { ` ); - tree = (await callRule( - updateImports({ + const projectConfig = readProjectConfiguration(tree, 'tab'); + updateImports( + tree, + { projectName: 'tab', destination: 'tabs', importPath: undefined, updateImportPath: true, - }), - tree - )) as UnitTestTree; + }, + projectConfig + ); expect(tree.read(importerFilePath).toString()).toContain( `import('@proj/table').then(m => m.Table);` @@ -165,133 +172,135 @@ describe('updateImports Rule', () => { expect(tree.read(importerFilePath).toString()).toContain( `import('@proj/tabs/components').then(m => m.Tab);` ); + + expect(tree.read(importerFilePath).toString()).toMatchSnapshot(); }); - - it('should update require imports', async () => { - tree = await runSchematic('lib', { name: 'table' }, tree); - tree = await runSchematic('lib', { name: 'tab' }, tree); - - tree = await runSchematic('lib', { name: 'my-importer' }, tree); - const importerFilePath = 'libs/my-importer/src/importer.ts'; - tree.create( - importerFilePath, - ` - require('@proj/table'); - require('@proj/table/components'); - require('@proj/tab'); - require('@proj/tab/components'); - ` - ); - - tree = (await callRule( - updateImports({ - projectName: 'tab', - destination: 'tabs', - importPath: undefined, - updateImportPath: true, - }), - tree - )) as UnitTestTree; - - expect(tree.read(importerFilePath).toString()).toContain( - `require('@proj/table');` - ); - - expect(tree.read(importerFilePath).toString()).toContain( - `require('@proj/table/components');` - ); - - expect(tree.read(importerFilePath).toString()).toContain( - `require('@proj/tabs');` - ); - - expect(tree.read(importerFilePath).toString()).toContain( - `require('@proj/tabs/components');` - ); - }); - - it('should not update project refs when --updateImportPath=false', async () => { - // this is a bit of a cheat - we expect to run this rule on an intermediate state - // tree where the workspace hasn't been updated yet, so just create libs representing - // source and destination to make sure that the workspace has libraries with those names. - tree = await runSchematic('lib', { name: 'my-destination' }, tree); - tree = await runSchematic('lib', { name: 'my-source' }, tree); - - tree = await runSchematic('lib', { name: 'my-importer' }, tree); - const importerFilePath = 'libs/my-importer/src/importer.ts'; - tree.create( - importerFilePath, - ` - import { MyClass } from '@proj/my-source'; - - export MyExtendedClass extends MyClass {}; - ` - ); - - schema.updateImportPath = false; - tree = (await callRule(updateImports(schema), tree)) as UnitTestTree; - - expect(tree.read(importerFilePath).toString()).toContain( - `import { MyClass } from '@proj/my-source';` - ); - }); - - it('should update project refs to --importPath when provided', async () => { - // this is a bit of a cheat - we expect to run this rule on an intermediate state - // tree where the workspace hasn't been updated yet, so just create libs representing - // source and destination to make sure that the workspace has libraries with those names. - tree = await runSchematic('lib', { name: 'my-destination' }, tree); - tree = await runSchematic('lib', { name: 'my-source' }, tree); - - tree = await runSchematic('lib', { name: 'my-importer' }, tree); - const importerFilePath = 'libs/my-importer/src/importer.ts'; - tree.create( - importerFilePath, - ` - import { MyClass } from '@proj/my-source'; - - export class MyExtendedClass extends MyClass {}; - ` - ); - - schema.importPath = '@proj/wibble'; - tree = (await callRule(updateImports(schema), tree)) as UnitTestTree; - - expect(tree.read(importerFilePath).toString()).toContain( - `import { MyClass } from '${schema.importPath}';` - ); - }); - - it('should update project ref in the tsconfig file', async () => { - tree = await runSchematic('lib', { name: 'my-source' }, tree); - - let tsConfig = readJsonInTree(tree, '/tsconfig.base.json'); - expect(tsConfig.compilerOptions.paths).toEqual({ - '@proj/my-source': ['libs/my-source/src/index.ts'], - }); - - tree = (await callRule(updateImports(schema), tree)) as UnitTestTree; - - tsConfig = readJsonInTree(tree, '/tsconfig.base.json'); - expect(tsConfig.compilerOptions.paths).toEqual({ - '@proj/my-destination': ['libs/my-destination/src/index.ts'], - }); - }); - - it('should only update the project ref paths in the tsconfig file when --updateImportPath=false', async () => { - tree = await runSchematic('lib', { name: 'my-source' }, tree); - - let tsConfig = readJsonInTree(tree, '/tsconfig.base.json'); - expect(tsConfig.compilerOptions.paths).toEqual({ - '@proj/my-source': ['libs/my-source/src/index.ts'], - }); - - schema.updateImportPath = false; - tree = (await callRule(updateImports(schema), tree)) as UnitTestTree; - - tsConfig = readJsonInTree(tree, '/tsconfig.base.json'); - expect(tsConfig.compilerOptions.paths).toEqual({ - '@proj/my-source': ['libs/my-destination/src/index.ts'], - }); - }); + // + // it('should update require imports', async () => { + // tree = await runSchematic('lib', { name: 'table' }, tree); + // tree = await runSchematic('lib', { name: 'tab' }, tree); + // + // tree = await runSchematic('lib', { name: 'my-importer' }, tree); + // const importerFilePath = 'libs/my-importer/src/importer.ts'; + // tree.create( + // importerFilePath, + // ` + // require('@proj/table'); + // require('@proj/table/components'); + // require('@proj/tab'); + // require('@proj/tab/components'); + // ` + // ); + // + // tree = (await callRule( + // updateImports({ + // projectName: 'tab', + // destination: 'tabs', + // importPath: undefined, + // updateImportPath: true, + // }), + // tree + // )) as UnitTestTree; + // + // expect(tree.read(importerFilePath).toString()).toContain( + // `require('@proj/table');` + // ); + // + // expect(tree.read(importerFilePath).toString()).toContain( + // `require('@proj/table/components');` + // ); + // + // expect(tree.read(importerFilePath).toString()).toContain( + // `require('@proj/tabs');` + // ); + // + // expect(tree.read(importerFilePath).toString()).toContain( + // `require('@proj/tabs/components');` + // ); + // }); + // + // it('should not update project refs when --updateImportPath=false', async () => { + // // this is a bit of a cheat - we expect to run this rule on an intermediate state + // // tree where the workspace hasn't been updated yet, so just create libs representing + // // source and destination to make sure that the workspace has libraries with those names. + // tree = await runSchematic('lib', { name: 'my-destination' }, tree); + // tree = await runSchematic('lib', { name: 'my-source' }, tree); + // + // tree = await runSchematic('lib', { name: 'my-importer' }, tree); + // const importerFilePath = 'libs/my-importer/src/importer.ts'; + // tree.create( + // importerFilePath, + // ` + // import { MyClass } from '@proj/my-source'; + // + // export MyExtendedClass extends MyClass {}; + // ` + // ); + // + // schema.updateImportPath = false; + // tree = (await callRule(updateImports(schema), tree)) as UnitTestTree; + // + // expect(tree.read(importerFilePath).toString()).toContain( + // `import { MyClass } from '@proj/my-source';` + // ); + // }); + // + // it('should update project refs to --importPath when provided', async () => { + // // this is a bit of a cheat - we expect to run this rule on an intermediate state + // // tree where the workspace hasn't been updated yet, so just create libs representing + // // source and destination to make sure that the workspace has libraries with those names. + // tree = await runSchematic('lib', { name: 'my-destination' }, tree); + // tree = await runSchematic('lib', { name: 'my-source' }, tree); + // + // tree = await runSchematic('lib', { name: 'my-importer' }, tree); + // const importerFilePath = 'libs/my-importer/src/importer.ts'; + // tree.create( + // importerFilePath, + // ` + // import { MyClass } from '@proj/my-source'; + // + // export class MyExtendedClass extends MyClass {}; + // ` + // ); + // + // schema.importPath = '@proj/wibble'; + // tree = (await callRule(updateImports(schema), tree)) as UnitTestTree; + // + // expect(tree.read(importerFilePath).toString()).toContain( + // `import { MyClass } from '${schema.importPath}';` + // ); + // }); + // + // it('should update project ref in the tsconfig file', async () => { + // tree = await runSchematic('lib', { name: 'my-source' }, tree); + // + // let tsConfig = readJsonInTree(tree, '/tsconfig.base.json'); + // expect(tsConfig.compilerOptions.paths).toEqual({ + // '@proj/my-source': ['libs/my-source/src/index.ts'], + // }); + // + // tree = (await callRule(updateImports(schema), tree)) as UnitTestTree; + // + // tsConfig = readJsonInTree(tree, '/tsconfig.base.json'); + // expect(tsConfig.compilerOptions.paths).toEqual({ + // '@proj/my-destination': ['libs/my-destination/src/index.ts'], + // }); + // }); + // + // it('should only update the project ref paths in the tsconfig file when --updateImportPath=false', async () => { + // tree = await runSchematic('lib', { name: 'my-source' }, tree); + // + // let tsConfig = readJsonInTree(tree, '/tsconfig.base.json'); + // expect(tsConfig.compilerOptions.paths).toEqual({ + // '@proj/my-source': ['libs/my-source/src/index.ts'], + // }); + // + // schema.updateImportPath = false; + // tree = (await callRule(updateImports(schema), tree)) as UnitTestTree; + // + // tsConfig = readJsonInTree(tree, '/tsconfig.base.json'); + // expect(tsConfig.compilerOptions.paths).toEqual({ + // '@proj/my-source': ['libs/my-destination/src/index.ts'], + // }); + // }); }); diff --git a/packages/workspace/src/schematics/move/lib/update-imports.ts b/packages/workspace/src/schematics/move/lib/update-imports.ts index 27938a3af0..e87d50cdf9 100644 --- a/packages/workspace/src/schematics/move/lib/update-imports.ts +++ b/packages/workspace/src/schematics/move/lib/update-imports.ts @@ -1,17 +1,14 @@ +import { findNodes, serializeJson } from '@nrwl/workspace'; import { - SchematicContext, + applyChangesToString, + ChangeType, + getProjects, + getWorkspaceLayout, + ProjectConfiguration, + StringChange, Tree, - UpdateRecorder, -} from '@angular-devkit/schematics'; -import { - findNodes, - getWorkspace, - NxJson, - readJsonInTree, - serializeJson, -} from '@nrwl/workspace'; -import { from, Observable } from 'rxjs'; -import { map } from 'rxjs/operators'; + visitNotIgnoredFiles, +} from '@nrwl/devkit'; import * as ts from 'typescript'; import { Schema } from '../schema'; import { normalizeSlashes } from './utils'; @@ -21,116 +18,98 @@ import { normalizeSlashes } from './utils'; * * @param schema The options provided to the schematic */ -export function updateImports(schema: Schema) { - return (tree: Tree, _context: SchematicContext): Observable => { - return from(getWorkspace(tree)).pipe( - map((workspace) => { - const nxJson = readJsonInTree(tree, 'nx.json'); - const libsDir = nxJson.workspaceLayout?.libsDir - ? nxJson.workspaceLayout.libsDir - : 'libs'; - const project = workspace.projects.get(schema.projectName); +export function updateImports( + tree: Tree, + schema: Schema, + project: ProjectConfiguration +) { + if (project.projectType === 'application') { + // These shouldn't be imported anywhere? + return; + } - if (project.extensions['projectType'] === 'application') { - // These shouldn't be imported anywhere? - return tree; - } + const { npmScope, libsDir } = getWorkspaceLayout(tree); + const projects = getProjects(tree); - // use the source root to find the from location - // this attempts to account for libs that have been created with --importPath - const tsConfigPath = 'tsconfig.base.json'; - let tsConfig: any; - let fromPath: string; - if (tree.exists(tsConfigPath)) { - tsConfig = JSON.parse(tree.read(tsConfigPath).toString('utf-8')); - fromPath = Object.keys(tsConfig.compilerOptions.paths).find((path) => - tsConfig.compilerOptions.paths[path].some((x) => - x.startsWith(project.sourceRoot) - ) - ); - } - - const projectRef = { - from: - fromPath || - normalizeSlashes( - `@${nxJson.npmScope}/${project.root.substr(libsDir.length + 1)}` - ), - to: - schema.importPath || - normalizeSlashes(`@${nxJson.npmScope}/${schema.destination}`), - }; - - if (schema.updateImportPath) { - const replaceProjectRef = new RegExp(projectRef.from, 'g'); - - for (const [name, definition] of workspace.projects.entries()) { - if (name === schema.projectName) { - continue; - } - - const projectDir = tree.getDir(definition.root); - projectDir.visit((file) => { - const contents = tree.read(file).toString('utf-8'); - if (!replaceProjectRef.test(contents)) { - return; - } - - updateImportPaths( - tree, - file, - contents, - projectRef.from, - projectRef.to - ); - }); - } - } - - const projectRoot = { - from: project.root.substr(libsDir.length + 1), - to: schema.destination, - }; - - if (tsConfig) { - const path = tsConfig.compilerOptions.paths[ - projectRef.from - ] as string[]; - if (!path) { - throw new Error( - [ - `unable to find "${projectRef.from}" in`, - `${tsConfigPath} compilerOptions.paths`, - ].join(' ') - ); - } - const updatedPath = path.map((x) => - x.replace(new RegExp(projectRoot.from, 'g'), projectRoot.to) - ); - - if (schema.updateImportPath) { - tsConfig.compilerOptions.paths[projectRef.to] = updatedPath; - delete tsConfig.compilerOptions.paths[projectRef.from]; - } else { - tsConfig.compilerOptions.paths[projectRef.from] = updatedPath; - } - - tree.overwrite(tsConfigPath, serializeJson(tsConfig)); - } - - return tree; - }) + // use the source root to find the from location + // this attempts to account for libs that have been created with --importPath + const tsConfigPath = 'tsconfig.base.json'; + let tsConfig: any; + let fromPath: string; + if (tree.exists(tsConfigPath)) { + tsConfig = JSON.parse(tree.read(tsConfigPath).toString('utf-8')); + fromPath = Object.keys(tsConfig.compilerOptions.paths).find((path) => + tsConfig.compilerOptions.paths[path].some((x) => + x.startsWith(project.sourceRoot) + ) ); + } + + const projectRef = { + from: + fromPath || + normalizeSlashes( + `@${npmScope}/${project.root.substr(libsDir.length + 1)}` + ), + to: + schema.importPath || + normalizeSlashes(`@${npmScope}/${schema.destination}`), }; + + if (schema.updateImportPath) { + const replaceProjectRef = new RegExp(projectRef.from, 'g'); + + for (const [name, definition] of projects.entries()) { + if (name === schema.projectName) { + continue; + } + + visitNotIgnoredFiles(tree, definition.root, (file) => { + const contents = tree.read(file).toString('utf-8'); + if (!replaceProjectRef.test(contents)) { + return; + } + + updateImportPaths(tree, file, projectRef.from, projectRef.to); + }); + } + } + + const projectRoot = { + from: project.root.substr(libsDir.length + 1), + to: schema.destination, + }; + + if (tsConfig) { + const path = tsConfig.compilerOptions.paths[projectRef.from] as string[]; + if (!path) { + throw new Error( + [ + `unable to find "${projectRef.from}" in`, + `${tsConfigPath} compilerOptions.paths`, + ].join(' ') + ); + } + const updatedPath = path.map((x) => + x.replace(new RegExp(projectRoot.from, 'g'), projectRoot.to) + ); + + if (schema.updateImportPath) { + tsConfig.compilerOptions.paths[projectRef.to] = updatedPath; + delete tsConfig.compilerOptions.paths[projectRef.from]; + } else { + tsConfig.compilerOptions.paths[projectRef.from] = updatedPath; + } + + tree.write(tsConfigPath, serializeJson(tsConfig)); + } } -function updateImportPaths( - tree: Tree, - path: string, - contents: string, - from: string, - to: string -) { +/** + * Changes imports in a file from one import to another + */ +function updateImportPaths(tree: Tree, path: string, from: string, to: string) { + const contents = tree.read(path).toString('utf-8'); const sourceFile = ts.createSourceFile( path, contents, @@ -138,50 +117,54 @@ function updateImportPaths( true ); - const recorder = tree.beginUpdate(path); + // Apply changes on the various types of imports + const newContents = applyChangesToString(contents, [ + ...updateImportDeclarations(sourceFile, from, to), + ...updateDynamicImports(sourceFile, from, to), + ]); - // perform transformations on the various types of imports - updateImportDeclarations(recorder, sourceFile, from, to); - updateDynamicImports(recorder, sourceFile, from, to); - - tree.commitUpdate(recorder); + tree.write(path, newContents); } /** * Update the module specifiers on static imports */ function updateImportDeclarations( - recorder: UpdateRecorder, sourceFile: ts.SourceFile, from: string, to: string -) { +): StringChange[] { const importDecls = findNodes( sourceFile, ts.SyntaxKind.ImportDeclaration ) as ts.ImportDeclaration[]; + const changes: StringChange[] = []; + for (const { moduleSpecifier } of importDecls) { if (ts.isStringLiteral(moduleSpecifier)) { - updateModuleSpecifier(recorder, moduleSpecifier, from, to); + changes.push(...updateModuleSpecifier(moduleSpecifier, from, to)); } } + + return changes; } /** * Update the module specifiers on dynamic imports and require statements */ function updateDynamicImports( - recorder: UpdateRecorder, sourceFile: ts.SourceFile, from: string, to: string -) { +): StringChange[] { const expressions = findNodes( sourceFile, ts.SyntaxKind.CallExpression ) as ts.CallExpression[]; + const changes: StringChange[] = []; + for (const { expression, arguments: args } of expressions) { const moduleSpecifier = args[0]; @@ -191,7 +174,7 @@ function updateDynamicImports( moduleSpecifier && ts.isStringLiteral(moduleSpecifier) ) { - updateModuleSpecifier(recorder, moduleSpecifier, from, to); + changes.push(...updateModuleSpecifier(moduleSpecifier, from, to)); } // handle require statements @@ -201,33 +184,38 @@ function updateDynamicImports( moduleSpecifier && ts.isStringLiteral(moduleSpecifier) ) { - updateModuleSpecifier(recorder, moduleSpecifier, from, to); + changes.push(...updateModuleSpecifier(moduleSpecifier, from, to)); } } + + return changes; } /** * Replace the old module specifier with a the new path */ function updateModuleSpecifier( - recorder: UpdateRecorder, moduleSpecifier: ts.StringLiteral, from: string, to: string -) { +): StringChange[] { if ( moduleSpecifier.text === from || moduleSpecifier.text.startsWith(from + '/') ) { - recorder.remove( - moduleSpecifier.getStart() + 1, - moduleSpecifier.text.length - ); - - // insert the new module specifier - recorder.insertLeft( - moduleSpecifier.getStart() + 1, - moduleSpecifier.text.replace(new RegExp(from, 'g'), to) - ); + return [ + { + type: ChangeType.Delete, + start: moduleSpecifier.getStart() + 1, + length: moduleSpecifier.text.length, + }, + { + type: ChangeType.Insert, + index: moduleSpecifier.getStart() + 1, + text: moduleSpecifier.text.replace(new RegExp(from, 'g'), to), + }, + ]; + } else { + return []; } } diff --git a/packages/workspace/src/schematics/move/lib/update-jest-config.spec.ts b/packages/workspace/src/schematics/move/lib/update-jest-config.spec.ts index 21b2f7bf2b..65bb4be6c4 100644 --- a/packages/workspace/src/schematics/move/lib/update-jest-config.spec.ts +++ b/packages/workspace/src/schematics/move/lib/update-jest-config.spec.ts @@ -1,20 +1,22 @@ -import { Tree } from '@angular-devkit/schematics'; -import { UnitTestTree } from '@angular-devkit/schematics/testing'; -import { createEmptyWorkspace } from '@nrwl/workspace/testing'; -import { callRule, runSchematic } from '../../../utils/testing'; +import { readProjectConfiguration, Tree } from '@nrwl/devkit'; +import { createTreeWithEmptyWorkspace } from '@nrwl/devkit/testing'; + import { Schema } from '../schema'; import { updateJestConfig } from './update-jest-config'; +import { libraryGenerator } from '../../library/library'; -describe('updateJestConfig Rule', () => { - let tree: UnitTestTree; +describe('updateJestConfig', () => { + let tree: Tree; beforeEach(async () => { - tree = new UnitTestTree(Tree.empty()); - tree = createEmptyWorkspace(tree) as UnitTestTree; + tree = createTreeWithEmptyWorkspace(); }); it('should handle jest config not existing', async () => { - tree = await runSchematic('lib', { name: 'my-source' }, tree); + await libraryGenerator(tree, { + name: 'my-source', + }); + const projectConfig = readProjectConfiguration(tree, 'my-source'); const schema: Schema = { projectName: 'my-source', @@ -23,9 +25,7 @@ describe('updateJestConfig Rule', () => { updateImportPath: true, }; - await expect( - callRule(updateJestConfig(schema), tree) - ).resolves.not.toThrow(); + updateJestConfig(tree, schema, projectConfig); }); it('should update the name and coverage directory', async () => { @@ -42,8 +42,11 @@ describe('updateJestConfig Rule', () => { const rootJestConfigPath = '/jest.config.js'; - tree = await runSchematic('lib', { name: 'my-source' }, tree); - tree.create(jestConfigPath, jestConfig); + await libraryGenerator(tree, { + name: 'my-source', + }); + const projectConfig = readProjectConfiguration(tree, 'my-source'); + tree.write(jestConfigPath, jestConfig); const schema: Schema = { projectName: 'my-source', @@ -52,7 +55,7 @@ describe('updateJestConfig Rule', () => { updateImportPath: true, }; - tree = (await callRule(updateJestConfig(schema), tree)) as UnitTestTree; + updateJestConfig(tree, schema, projectConfig); const jestConfigAfter = tree.read(jestConfigPath).toString(); const rootJestConfigAfter = tree.read(rootJestConfigPath).toString(); diff --git a/packages/workspace/src/schematics/move/lib/update-jest-config.ts b/packages/workspace/src/schematics/move/lib/update-jest-config.ts index e29ad6b833..8246c4eb65 100644 --- a/packages/workspace/src/schematics/move/lib/update-jest-config.ts +++ b/packages/workspace/src/schematics/move/lib/update-jest-config.ts @@ -1,11 +1,9 @@ -import { Rule, SchematicContext } from '@angular-devkit/schematics'; -import { Tree } from '@angular-devkit/schematics/src/tree/interface'; -import { getWorkspace } from '@nrwl/workspace'; +import { Tree, ProjectConfiguration, getWorkspaceLayout } from '@nrwl/devkit'; + import * as path from 'path'; -import { from, Observable } from 'rxjs'; -import { map } from 'rxjs/operators'; + import { Schema } from '../schema'; -import { getDestination, getNewProjectName, getWorkspaceLayout } from './utils'; +import { getDestination, getNewProjectName } from './utils'; /** * Updates the project name and coverage folder in the jest.config.js if it exists @@ -14,57 +12,52 @@ import { getDestination, getNewProjectName, getWorkspaceLayout } from './utils'; * * @param schema The options provided to the schematic */ -export function updateJestConfig(schema: Schema): Rule { - return (tree: Tree, _context: SchematicContext): Observable => { - return from(getWorkspace(tree)).pipe( - map((workspace) => { - const project = workspace.projects.get(schema.projectName); - const destination = getDestination(schema, workspace, tree); - const newProjectName = getNewProjectName(schema.destination); +export function updateJestConfig( + tree: Tree, + schema: Schema, + project: ProjectConfiguration +) { + const destination = getDestination(tree, schema, project); + const newProjectName = getNewProjectName(schema.destination); - const jestConfigPath = path.join(destination, 'jest.config.js'); + const jestConfigPath = path.join(destination, 'jest.config.js'); - if (!tree.exists(jestConfigPath)) { - // nothing to do - return tree; - } + if (!tree.exists(jestConfigPath)) { + // nothing to do + return; + } - const oldContent = tree.read(jestConfigPath).toString('utf-8'); + const oldContent = tree.read(jestConfigPath).toString('utf-8'); - const findName = new RegExp(`'${schema.projectName}'`, 'g'); - const findDir = new RegExp(project.root, 'g'); + const findName = new RegExp(`'${schema.projectName}'`, 'g'); + const findDir = new RegExp(project.root, 'g'); - const newContent = oldContent - .replace(findName, `'${newProjectName}'`) - .replace(findDir, destination); - tree.overwrite(jestConfigPath, newContent); + const newContent = oldContent + .replace(findName, `'${newProjectName}'`) + .replace(findDir, destination); + tree.write(jestConfigPath, newContent); - // update root jest.config.js - const rootJestConfigPath = '/jest.config.js'; + // update root jest.config.js + const rootJestConfigPath = '/jest.config.js'; - if (!tree.exists(rootJestConfigPath)) { - return tree; - } + if (!tree.exists(rootJestConfigPath)) { + return; + } - const oldRootJestConfigContent = tree - .read(rootJestConfigPath) - .toString('utf-8'); + const { libsDir, appsDir } = getWorkspaceLayout(tree); + const findProject = new RegExp( + `\/(${libsDir}|${appsDir})\/${schema.projectName}`, + 'g' + ); - const { libsDir, appsDir } = getWorkspaceLayout(tree); - const findProject = new RegExp( - `\/(${libsDir}|${appsDir})\/${schema.projectName}`, - 'g' - ); + const oldRootJestConfigContent = tree + .read(rootJestConfigPath) + .toString('utf-8'); - const newRootJestConfigContent = oldRootJestConfigContent.replace( - findProject, - `/${destination}` - ); + const newRootJestConfigContent = oldRootJestConfigContent.replace( + findProject, + `/${destination}` + ); - tree.overwrite(rootJestConfigPath, newRootJestConfigContent); - - return tree; - }) - ); - }; + tree.write(rootJestConfigPath, newRootJestConfigContent); } diff --git a/packages/workspace/src/schematics/move/lib/update-nx-json.spec.ts b/packages/workspace/src/schematics/move/lib/update-nx-json.spec.ts deleted file mode 100644 index 4d9bf9559a..0000000000 --- a/packages/workspace/src/schematics/move/lib/update-nx-json.spec.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { Tree } from '@angular-devkit/schematics'; -import { UnitTestTree } from '@angular-devkit/schematics/testing'; -import { readJsonInTree } from '@nrwl/workspace'; -import { createEmptyWorkspace } from '@nrwl/workspace/testing'; -import { callRule, runSchematic } from '../../../utils/testing'; -import { Schema } from '../schema'; -import { updateNxJson } from './update-nx-json'; - -describe('updateNxJson Rule', () => { - let tree: UnitTestTree; - - beforeEach(async () => { - tree = new UnitTestTree(Tree.empty()); - tree = createEmptyWorkspace(tree) as UnitTestTree; - }); - - it('should update nx.json', async () => { - tree = await runSchematic('lib', { name: 'my-source' }, tree); - - const schema: Schema = { - projectName: 'my-source', - destination: 'my-destination', - importPath: undefined, - updateImportPath: true, - }; - - tree = (await callRule(updateNxJson(schema), tree)) as UnitTestTree; - - const nxJson = readJsonInTree(tree, '/nx.json'); - - expect(nxJson.projects['my-source']).toBeUndefined(); - expect(nxJson.projects['my-destination']).toEqual({ - tags: [], - }); - }); -}); diff --git a/packages/workspace/src/schematics/move/lib/update-project-root-files.spec.ts b/packages/workspace/src/schematics/move/lib/update-project-root-files.spec.ts index 7c5b1655dc..92d160e66e 100644 --- a/packages/workspace/src/schematics/move/lib/update-project-root-files.spec.ts +++ b/packages/workspace/src/schematics/move/lib/update-project-root-files.spec.ts @@ -1,16 +1,14 @@ -import { Tree } from '@angular-devkit/schematics'; -import { UnitTestTree } from '@angular-devkit/schematics/testing'; -import { createEmptyWorkspace } from '@nrwl/workspace/testing'; -import { callRule, runSchematic } from '../../../utils/testing'; +import { readProjectConfiguration, Tree } from '@nrwl/devkit'; +import { createTreeWithEmptyWorkspace } from '@nrwl/devkit/testing'; import { Schema } from '../schema'; import { updateProjectRootFiles } from './update-project-root-files'; +import { libraryGenerator } from '../../library/library'; -describe('updateProjectRootFiles Rule', () => { - let tree: UnitTestTree; +describe('updateProjectRootFiles', () => { + let tree: Tree; beforeEach(async () => { - tree = new UnitTestTree(Tree.empty()); - tree = createEmptyWorkspace(tree) as UnitTestTree; + tree = createTreeWithEmptyWorkspace(); }); it('should update the relative root in files at the root of the project', async () => { @@ -25,8 +23,11 @@ describe('updateProjectRootFiles Rule', () => { };`; const testFilePath = '/libs/subfolder/my-destination/jest.config.js'; - tree = await runSchematic('lib', { name: 'my-source' }, tree); - tree.create(testFilePath, testFile); + await libraryGenerator(tree, { + name: 'my-source', + }); + const projectConfig = readProjectConfiguration(tree, 'my-source'); + tree.write(testFilePath, testFile); const schema: Schema = { projectName: 'my-source', @@ -35,10 +36,7 @@ describe('updateProjectRootFiles Rule', () => { updateImportPath: true, }; - tree = (await callRule( - updateProjectRootFiles(schema), - tree - )) as UnitTestTree; + updateProjectRootFiles(tree, schema, projectConfig); const testFileAfter = tree.read(testFilePath).toString(); expect(testFileAfter).toContain(`preset: '../../../jest.config.js'`); diff --git a/packages/workspace/src/schematics/move/lib/update-project-root-files.ts b/packages/workspace/src/schematics/move/lib/update-project-root-files.ts index a372e99a75..0ed72565e8 100644 --- a/packages/workspace/src/schematics/move/lib/update-project-root-files.ts +++ b/packages/workspace/src/schematics/move/lib/update-project-root-files.ts @@ -1,12 +1,11 @@ -import { Rule, SchematicContext } from '@angular-devkit/schematics'; -import { Tree } from '@angular-devkit/schematics/src/tree/interface'; -import { getWorkspace } from '@nrwl/workspace'; -import { appRootPath } from '@nrwl/workspace/src/utils/app-root'; import * as path from 'path'; -import { from, Observable } from 'rxjs'; -import { map } from 'rxjs/operators'; + +import { ProjectConfiguration, Tree } from '@nrwl/devkit'; + +import { appRootPath } from '../../../utils/app-root'; import { Schema } from '../schema'; import { getDestination } from './utils'; +import { extname, join } from 'path'; /** * Updates the files in the root of the project @@ -15,44 +14,37 @@ import { getDestination } from './utils'; * * @param schema The options provided to the schematic */ -export function updateProjectRootFiles(schema: Schema): Rule { - return (tree: Tree, _context: SchematicContext): Observable => { - return from(getWorkspace(tree)).pipe( - map((workspace) => { - const project = workspace.projects.get(schema.projectName); - const destination = getDestination(schema, workspace, tree); +export function updateProjectRootFiles( + tree: Tree, + schema: Schema, + project: ProjectConfiguration +) { + const destination = getDestination(tree, schema, project); - const newRelativeRoot = path - .relative(path.join(appRootPath, destination), appRootPath) - .split(path.sep) - .join('/'); - const oldRelativeRoot = path - .relative(path.join(appRootPath, project.root), appRootPath) - .split(path.sep) - .join('/'); + const newRelativeRoot = path + .relative(path.join(appRootPath, destination), appRootPath) + .split(path.sep) + .join('/'); + const oldRelativeRoot = path + .relative(path.join(appRootPath, project.root), appRootPath) + .split(path.sep) + .join('/'); - if (newRelativeRoot === oldRelativeRoot) { - // nothing to do - return tree; - } + if (newRelativeRoot === oldRelativeRoot) { + // nothing to do + return; + } - const dots = /\./g; - const regex = new RegExp(oldRelativeRoot.replace(dots, '\\.'), 'g'); + const dots = /\./g; + const regex = new RegExp(oldRelativeRoot.replace(dots, '\\.'), 'g'); - const isRootFile = new RegExp(`${schema.destination}/[^/]+.js*`); - const projectDir = tree.getDir(destination); - projectDir.visit((file) => { - if (!isRootFile.test(file)) { - return; - } + for (const file of tree.children(destination)) { + if (!extname(file).startsWith('.js')) { + continue; + } - const oldContent = tree.read(file).toString(); - const newContent = oldContent.replace(regex, newRelativeRoot); - tree.overwrite(file, newContent); - }); - - return tree; - }) - ); - }; + const oldContent = tree.read(join(destination, file)).toString(); + const newContent = oldContent.replace(regex, newRelativeRoot); + tree.write(join(destination, file), newContent); + } } diff --git a/packages/workspace/src/schematics/move/lib/update-storybook-config.spec.ts b/packages/workspace/src/schematics/move/lib/update-storybook-config.spec.ts index 2d4f172719..eebe9af7b1 100644 --- a/packages/workspace/src/schematics/move/lib/update-storybook-config.spec.ts +++ b/packages/workspace/src/schematics/move/lib/update-storybook-config.spec.ts @@ -1,20 +1,21 @@ -import { Tree } from '@angular-devkit/schematics'; -import { UnitTestTree } from '@angular-devkit/schematics/testing'; -import { createEmptyWorkspace } from '@nrwl/workspace/testing'; -import { callRule, runSchematic } from '../../../utils/testing'; +import { readProjectConfiguration, Tree } from '@nrwl/devkit'; import { Schema } from '../schema'; import { updateStorybookConfig } from './update-storybook-config'; +import { createTreeWithEmptyWorkspace } from '@nrwl/devkit/testing'; +import { libraryGenerator } from '../../library/library'; -describe('updateStorybookConfig Rule', () => { - let tree: UnitTestTree; +describe('updateStorybookConfig', () => { + let tree: Tree; beforeEach(async () => { - tree = new UnitTestTree(Tree.empty()); - tree = createEmptyWorkspace(tree) as UnitTestTree; + tree = createTreeWithEmptyWorkspace(); }); it('should handle storybook config not existing', async () => { - tree = await runSchematic('lib', { name: 'my-source' }, tree); + await libraryGenerator(tree, { + name: 'my-source', + }); + const projectConfig = readProjectConfiguration(tree, 'my-source'); const schema: Schema = { projectName: 'my-source', @@ -23,9 +24,7 @@ describe('updateStorybookConfig Rule', () => { updateImportPath: true, }; - await expect( - callRule(updateStorybookConfig(schema), tree) - ).resolves.not.toThrow(); + updateStorybookConfig(tree, schema, projectConfig); }); it('should update the import path for main.js', async () => { @@ -37,8 +36,11 @@ describe('updateStorybookConfig Rule', () => { const storybookMainPath = '/libs/namespace/my-destination/.storybook/main.js'; - tree = await runSchematic('lib', { name: 'my-source' }, tree); - tree.create(storybookMainPath, storybookMain); + await libraryGenerator(tree, { + name: 'my-source', + }); + const projectConfig = readProjectConfiguration(tree, 'my-source'); + tree.write(storybookMainPath, storybookMain); const schema: Schema = { projectName: 'my-source', @@ -47,10 +49,7 @@ describe('updateStorybookConfig Rule', () => { updateImportPath: true, }; - tree = (await callRule( - updateStorybookConfig(schema), - tree - )) as UnitTestTree; + updateStorybookConfig(tree, schema, projectConfig); const storybookMainAfter = tree.read(storybookMainPath).toString(); expect(storybookMainAfter).toContain( @@ -66,8 +65,11 @@ describe('updateStorybookConfig Rule', () => { const storybookWebpackConfigPath = '/libs/namespace/my-destination/.storybook/webpack.config.js'; - tree = await runSchematic('lib', { name: 'my-source' }, tree); - tree.create(storybookWebpackConfigPath, storybookWebpackConfig); + await libraryGenerator(tree, { + name: 'my-source', + }); + const projectConfig = readProjectConfiguration(tree, 'my-source'); + tree.write(storybookWebpackConfigPath, storybookWebpackConfig); const schema: Schema = { projectName: 'my-source', @@ -76,10 +78,7 @@ describe('updateStorybookConfig Rule', () => { updateImportPath: true, }; - tree = (await callRule( - updateStorybookConfig(schema), - tree - )) as UnitTestTree; + updateStorybookConfig(tree, schema, projectConfig); const storybookWebpackConfigAfter = tree .read(storybookWebpackConfigPath) diff --git a/packages/workspace/src/schematics/move/lib/update-storybook-config.ts b/packages/workspace/src/schematics/move/lib/update-storybook-config.ts index 1f633cc0b3..72003dc7f0 100644 --- a/packages/workspace/src/schematics/move/lib/update-storybook-config.ts +++ b/packages/workspace/src/schematics/move/lib/update-storybook-config.ts @@ -1,61 +1,44 @@ -import { Rule, SchematicContext } from '@angular-devkit/schematics'; -import { Tree } from '@angular-devkit/schematics/src/tree/interface'; -import { getWorkspace } from '@nrwl/workspace'; +import { ProjectConfiguration, Tree } from '@nrwl/devkit'; + import * as path from 'path'; -import { from, Observable } from 'rxjs'; -import { map } from 'rxjs/operators'; + +import { appRootPath } from '../../../utils/app-root'; import { Schema } from '../schema'; import { getDestination } from './utils'; -import { appRootPath } from '@nrwl/workspace/src/utils/app-root'; -import { allFilesInDirInHost } from '@nrwl/workspace/src/utils/ast-utils'; -import { Path, normalize } from '@angular-devkit/core'; +import { join } from 'path'; /** * Updates relative path to root storybook config for `main.js` & `webpack.config.js` * * @param schema The options provided to the schematic */ -export function updateStorybookConfig(schema: Schema): Rule { - return (tree: Tree, _context: SchematicContext): Observable => { - return from(getWorkspace(tree)).pipe( - map((workspace) => { - const project = workspace.projects.get(schema.projectName); - const destination = getDestination(schema, workspace, tree); +export function updateStorybookConfig( + tree: Tree, + schema: Schema, + project: ProjectConfiguration +) { + const destination = getDestination(tree, schema, project); - const oldRelativeRoot = path - .relative( - path.join(appRootPath, `${project.root}/.storybook`), - appRootPath - ) - .split(path.sep) - .join('/'); - const newRelativeRoot = path - .relative( - path.join(appRootPath, `${destination}/.storybook`), - appRootPath - ) - .split(path.sep) - .join('/'); + const oldRelativeRoot = path + .relative(path.join(appRootPath, `${project.root}/.storybook`), appRootPath) + .split(path.sep) + .join('/'); + const newRelativeRoot = path + .relative(path.join(appRootPath, `${destination}/.storybook`), appRootPath) + .split(path.sep) + .join('/'); - const storybookDir = path.join(destination, '.storybook'); + const storybookDir = path.join(destination, '.storybook'); - if (!storybookDir) { - return tree; - } + if (!storybookDir) { + return; + } - // Replace relative import path to root storybook folder for each file under project storybook - tree.getDir(storybookDir).visit((file) => { - const oldContent = tree.read(file).toString('utf-8'); - const newContent = oldContent.replace( - oldRelativeRoot, - newRelativeRoot - ); + // Replace relative import path to root storybook folder for each file under project storybook + for (const file of tree.children(storybookDir)) { + const oldContent = tree.read(join(storybookDir, file)).toString('utf-8'); + const newContent = oldContent.replace(oldRelativeRoot, newRelativeRoot); - tree.overwrite(file, newContent); - }); - - return tree; - }) - ); - }; + tree.write(join(storybookDir, file), newContent); + } } diff --git a/packages/workspace/src/schematics/move/lib/update-workspace.spec.ts b/packages/workspace/src/schematics/move/lib/update-workspace.spec.ts deleted file mode 100644 index 9b184ecc3f..0000000000 --- a/packages/workspace/src/schematics/move/lib/update-workspace.spec.ts +++ /dev/null @@ -1,245 +0,0 @@ -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'; -import { updateWorkspace } from './update-workspace'; - -describe('updateWorkspace Rule', () => { - let tree: UnitTestTree; - - beforeEach(async () => { - tree = new UnitTestTree(Tree.empty()); - tree = createEmptyWorkspace(tree) as UnitTestTree; - - const workspace = { - version: 1, - projects: { - 'my-source': { - projectType: 'application', - root: 'apps/my-source', - sourceRoot: 'apps/my-source/src', - prefix: 'app', - architect: { - build: { - builder: '@angular-devkit/build-angular:browser', - options: { - outputPath: 'dist/apps/my-source', - index: 'apps/my-source/src/index.html', - main: 'apps/my-source/src/main.ts', - polyfills: 'apps/my-source/src/polyfills.ts', - tsConfig: 'apps/my-source/tsconfig.app.json', - aot: false, - assets: [ - 'apps/my-source/src/favicon.ico', - 'apps/my-source/src/assets', - ], - styles: ['apps/my-source/src/styles.scss'], - scripts: [], - }, - configurations: { - production: { - fileReplacements: [ - { - replace: 'apps/my-source/src/environments/environment.ts', - with: - 'apps/my-source/src/environments/environment.prod.ts', - }, - ], - optimization: true, - outputHashing: 'all', - sourceMap: false, - extractCss: true, - namedChunks: false, - aot: true, - extractLicenses: true, - vendorChunk: false, - buildOptimizer: true, - budgets: [ - { - type: 'initial', - maximumWarning: '2mb', - maximumError: '5mb', - }, - { - type: 'anyComponentStyle', - maximumWarning: '6kb', - maximumError: '10kb', - }, - ], - }, - }, - }, - serve: { - builder: '@angular-devkit/build-angular:dev-server', - options: { - browserTarget: 'my-source:build', - }, - configurations: { - production: { - browserTarget: 'my-source:build:production', - }, - }, - }, - 'extract-i18n': { - builder: '@angular-devkit/build-angular:extract-i18n', - options: { - browserTarget: 'my-source:build', - }, - }, - lint: { - builder: '@angular-devkit/build-angular:tslint', - options: { - tsConfig: [ - 'apps/my-source/tsconfig.app.json', - 'apps/my-source/tsconfig.spec.json', - ], - exclude: ['**/node_modules/**', '!apps/my-source/**/*'], - }, - }, - test: { - builder: '@nrwl/jest:jest', - options: { - jestConfig: 'apps/my-source/jest.config.js', - tsConfig: 'apps/my-source/tsconfig.spec.json', - setupFile: 'apps/my-source/src/test-setup.ts', - }, - }, - }, - }, - 'my-source-e2e': { - root: 'apps/my-source-e2e', - sourceRoot: 'apps/my-source-e2e/src', - projectType: 'application', - architect: { - e2e: { - builder: '@nrwl/cypress:cypress', - options: { - cypressConfig: 'apps/my-source-e2e/cypress.json', - tsConfig: 'apps/my-source-e2e/tsconfig.e2e.json', - devServerTarget: 'my-source:serve', - }, - configurations: { - production: { - devServerTarget: 'my-source:serve:production', - }, - }, - }, - lint: { - builder: '@angular-devkit/build-angular:tslint', - options: { - tsConfig: ['apps/my-source-e2e/tsconfig.e2e.json'], - exclude: ['**/node_modules/**', '!apps/my-source-e2e/**/*'], - }, - }, - }, - }, - }, - defaultProject: 'my-source', - }; - - tree.overwrite('workspace.json', JSON.stringify(workspace)); - }); - - it('should rename the project', async () => { - const schema: Schema = { - projectName: 'my-source', - destination: 'subfolder/my-destination', - importPath: undefined, - updateImportPath: true, - }; - - tree = (await callRule(updateWorkspace(schema), tree)) as UnitTestTree; - - const workspace = JSON.parse(tree.read('workspace.json').toString()); - - expect(workspace.projects['my-source']).toBeUndefined(); - expect(workspace.projects['subfolder-my-destination']).toBeDefined(); - }); - - it('should update the default project', async () => { - const schema: Schema = { - projectName: 'my-source', - destination: 'subfolder/my-destination', - importPath: undefined, - updateImportPath: true, - }; - - tree = (await callRule(updateWorkspace(schema), tree)) as UnitTestTree; - - const workspace = JSON.parse(tree.read('workspace.json').toString()); - - expect(workspace.defaultProject).toBe('subfolder-my-destination'); - }); - - it('should update paths in only the intended project', async () => { - const schema: Schema = { - projectName: 'my-source', - destination: 'subfolder/my-destination', - importPath: undefined, - updateImportPath: true, - }; - - tree = (await callRule(updateWorkspace(schema), tree)) as UnitTestTree; - - const workspace = JSON.parse(tree.read('workspace.json').toString()); - - const actualProject = workspace.projects['subfolder-my-destination']; - expect(actualProject).toBeDefined(); - expect(actualProject.root).toBe('apps/subfolder/my-destination'); - expect(actualProject.root).toBe('apps/subfolder/my-destination'); - - const similarProject = workspace.projects['my-source-e2e']; - expect(similarProject).toBeDefined(); - expect(similarProject.root).toBe('apps/my-source-e2e'); - }); - - it('should update build targets', async () => { - const schema: Schema = { - projectName: 'my-source', - destination: 'subfolder/my-destination', - importPath: undefined, - updateImportPath: true, - }; - - tree = (await callRule(updateWorkspace(schema), tree)) as UnitTestTree; - - const workspace = JSON.parse(tree.read('workspace.json').toString()); - - const e2eProject = workspace.projects['my-source-e2e']; - expect(e2eProject).toBeDefined(); - expect(e2eProject.architect.e2e.options.devServerTarget).toBe( - 'subfolder-my-destination:serve' - ); - expect( - 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', - importPath: undefined, - updateImportPath: true, - }; - - tree = (await callRule( - updateJsonInTree('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'); - }); -}); diff --git a/packages/workspace/src/schematics/move/lib/update-workspace.ts b/packages/workspace/src/schematics/move/lib/update-workspace.ts deleted file mode 100644 index af2559fadb..0000000000 --- a/packages/workspace/src/schematics/move/lib/update-workspace.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { SchematicContext, Tree } from '@angular-devkit/schematics'; -import { updateWorkspaceInTree } from '@nrwl/workspace'; -import { Schema } from '../schema'; -import { getDestination, getNewProjectName } from './utils'; - -/** - * Updates the project in the workspace file - * - * - update all references to the old root path - * - change the project name - * - change target references - * - * @param schema The options provided to the schematic - */ -export function updateWorkspace(schema: Schema) { - 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, tree) - ); - - // 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 default project (if necessary) - if ( - workspace.defaultProject && - workspace.defaultProject === schema.projectName - ) { - workspace.defaultProject = newProjectName; - } - - return workspace; - }); - }; -} diff --git a/packages/workspace/src/schematics/move/lib/utils.ts b/packages/workspace/src/schematics/move/lib/utils.ts index 32d261b397..2943880700 100644 --- a/packages/workspace/src/schematics/move/lib/utils.ts +++ b/packages/workspace/src/schematics/move/lib/utils.ts @@ -1,28 +1,9 @@ -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 { ProjectConfiguration, Tree, getWorkspaceLayout } from '@nrwl/devkit'; + 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(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. @@ -34,16 +15,11 @@ export function getWorkspaceLayout( * @param workspace */ export function getDestination( + host: Tree, schema: Schema, - workspace: WorkspaceDefinition | any, - host: Tree + project: ProjectConfiguration ): string { - const project = workspace.projects.get - ? workspace.projects.get(schema.projectName) - : workspace.projects[schema.projectName]; - const projectType = project.extensions - ? project.extensions['projectType'] - : project.projectType; + const projectType = project.projectType; const workspaceLayout = getWorkspaceLayout(host); @@ -51,7 +27,7 @@ export function getDestination( if (projectType === 'application') { rootFolder = workspaceLayout.appsDir; } - return path.join(rootFolder, schema.destination).split(path.sep).join('/'); + return path.join(rootFolder, schema.destination); } /** diff --git a/packages/workspace/src/schematics/move/move.ts b/packages/workspace/src/schematics/move/move.ts index d5966ce00f..5e8ddb8cb4 100644 --- a/packages/workspace/src/schematics/move/move.ts +++ b/packages/workspace/src/schematics/move/move.ts @@ -1,35 +1,44 @@ -import { chain, Rule } from '@angular-devkit/schematics'; -import { checkProjectExists } from '../../utils/rules/check-project-exists'; +import { + convertNxGenerator, + formatFiles, + readProjectConfiguration, + Tree, +} from '@nrwl/devkit'; + import { checkDestination } from './lib/check-destination'; import { moveProject } from './lib/move-project'; import { updateCypressJson } from './lib/update-cypress-json'; import { updateImports } from './lib/update-imports'; import { updateJestConfig } from './lib/update-jest-config'; import { updateStorybookConfig } from './lib/update-storybook-config'; -import { updateNxJson } from './lib/update-nx-json'; +import { updateImplicitDependencies } from './lib/update-implicit-dependencies'; import { updateProjectRootFiles } from './lib/update-project-root-files'; -import { updateWorkspace } from './lib/update-workspace'; +import { updateDefaultProject } from './lib/update-default-project'; import { Schema } from './schema'; -import { wrapAngularDevkitSchematic } from '@nrwl/devkit/ngcli-adapter'; import { updateEslintrcJson } from './lib/update-eslintrc-json'; +import { moveProjectConfiguration } from './lib/move-project-configuration'; +import { updateBuildTargets } from './lib/update-build-targets'; -export default function (schema: Schema): Rule { - return chain([ - checkProjectExists(schema), - checkDestination(schema), - moveProject(schema), // we MUST move the project first, if we don't we get a "This should never happen" error 🤦‍♀️ - updateProjectRootFiles(schema), - updateCypressJson(schema), - updateJestConfig(schema), - updateStorybookConfig(schema), - updateNxJson(schema), - updateImports(schema), - updateEslintrcJson(schema), - updateWorkspace(schema), // Have to do this last because all previous rules need the information in here - ]); +export async function moveGenerator(tree: Tree, schema: Schema) { + const projectConfig = readProjectConfiguration(tree, schema.projectName); + checkDestination(tree, schema, projectConfig); + moveProject(tree, schema, projectConfig); // we MUST move the project first, if we don't we get a "This should never happen" error 🤦‍♀️ + updateImports(tree, schema, projectConfig); + updateProjectRootFiles(tree, schema, projectConfig); + updateCypressJson(tree, schema, projectConfig); + updateJestConfig(tree, schema, projectConfig); + updateStorybookConfig(tree, schema, projectConfig); + updateEslintrcJson(tree, schema, projectConfig); + moveProjectConfiguration(tree, schema, projectConfig); + updateBuildTargets(tree, schema); + updateDefaultProject(tree, schema); + updateImplicitDependencies(tree, schema); + + if (!schema.skipFormat) { + await formatFiles(tree); + } } -export const moveGenerator = wrapAngularDevkitSchematic( - '@nrwl/workspace', - 'move' -); +export default moveGenerator; + +export const moveSchematic = convertNxGenerator(moveGenerator); diff --git a/packages/workspace/src/schematics/move/schema.d.ts b/packages/workspace/src/schematics/move/schema.d.ts index 14f247d3b7..d5410c04ff 100644 --- a/packages/workspace/src/schematics/move/schema.d.ts +++ b/packages/workspace/src/schematics/move/schema.d.ts @@ -3,4 +3,5 @@ export interface Schema { destination: string; importPath?: string; updateImportPath: boolean; + skipFormat?: boolean; } diff --git a/packages/workspace/src/schematics/move/schema.json b/packages/workspace/src/schematics/move/schema.json index 7ecc4eb469..fb73b43098 100644 --- a/packages/workspace/src/schematics/move/schema.json +++ b/packages/workspace/src/schematics/move/schema.json @@ -1,6 +1,7 @@ { "$schema": "http://json-schema.org/schema", "id": "NxWorkspaceMove", + "cli": "nx", "title": "Nx Move", "description": "Move a project to another folder in the workspace", "type": "object", @@ -32,6 +33,12 @@ "type": "boolean", "description": "Should the generator update the import path to reflect the new location?", "default": true + }, + "skipFormat": { + "type": "boolean", + "aliases": ["skip-format"], + "description": "Skip formatting files.", + "default": false } }, "required": ["projectName", "destination"] diff --git a/packages/workspace/src/schematics/remove/lib/__snapshots__/update-jest-config.spec.ts.snap b/packages/workspace/src/schematics/remove/lib/__snapshots__/update-jest-config.spec.ts.snap index e4920f279c..af52db4633 100644 --- a/packages/workspace/src/schematics/remove/lib/__snapshots__/update-jest-config.spec.ts.snap +++ b/packages/workspace/src/schematics/remove/lib/__snapshots__/update-jest-config.spec.ts.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`updateRootJestConfig Rule should delete lib project ref from root jest config 1`] = ` +exports[`updateRootJestConfig should delete lib project ref from root jest config 1`] = ` "module.exports = { projects: [ '/apps/my-app/', @@ -12,12 +12,12 @@ exports[`updateRootJestConfig Rule should delete lib project ref from root jest " `; -exports[`updateRootJestConfig Rule should delete lib project ref from root jest config 2`] = ` +exports[`updateRootJestConfig should delete lib project ref from root jest config 2`] = ` "module.exports = { projects: [ '/apps/my-app/', '/apps/my-other-app', - '/libs/my-other-lib/', + '/libs/my-other-lib/', ], }; " diff --git a/packages/workspace/src/schematics/remove/lib/check-dependencies.spec.ts b/packages/workspace/src/schematics/remove/lib/check-dependencies.spec.ts index 87bde5f483..2926bccc86 100644 --- a/packages/workspace/src/schematics/remove/lib/check-dependencies.spec.ts +++ b/packages/workspace/src/schematics/remove/lib/check-dependencies.spec.ts @@ -1,18 +1,19 @@ -import { Tree } from '@angular-devkit/schematics'; -import { UnitTestTree } from '@angular-devkit/schematics/testing'; -import { updateJsonInTree } from '@nrwl/workspace'; -import { createEmptyWorkspace } from '@nrwl/workspace/testing'; -import { callRule, runSchematic } from '../../../utils/testing'; +import { + readProjectConfiguration, + Tree, + updateProjectConfiguration, +} from '@nrwl/devkit'; import { Schema } from '../schema'; import { checkDependencies } from './check-dependencies'; +import { createTreeWithEmptyWorkspace } from '@nrwl/devkit/testing'; +import { libraryGenerator } from '../../library/library'; -describe('updateImports Rule', () => { - let tree: UnitTestTree; +describe('checkDependencies', () => { + let tree: Tree; let schema: Schema; beforeEach(async () => { - tree = new UnitTestTree(Tree.empty()); - tree = createEmptyWorkspace(tree) as UnitTestTree; + tree = createTreeWithEmptyWorkspace(); schema = { projectName: 'my-source', @@ -20,21 +21,25 @@ describe('updateImports Rule', () => { forceRemove: false, }; - tree = await runSchematic('lib', { name: 'my-dependent' }, tree); - tree = await runSchematic('lib', { name: 'my-source' }, tree); + await libraryGenerator(tree, { + name: 'my-dependent', + }); + await libraryGenerator(tree, { + name: 'my-source', + }); }); describe('static dependencies', () => { beforeEach(() => { const sourceFilePath = 'libs/my-source/src/lib/my-source.ts'; - tree.overwrite( + tree.write( sourceFilePath, `export class MyClass {} ` ); const dependentFilePath = 'libs/my-dependent/src/lib/my-dependent.ts'; - tree.overwrite( + tree.write( dependentFilePath, `import { MyClass } from '@proj/my-source'; @@ -44,33 +49,33 @@ describe('updateImports Rule', () => { }); it('should fatally error if any dependent exists', async () => { - await expect(callRule(checkDependencies(schema), tree)).rejects.toThrow( - `${schema.projectName} is still depended on by the following projects:\nmy-dependent` - ); + expect(() => { + checkDependencies(tree, schema); + }).toThrow(); }); it('should not error if forceRemove is true', async () => { schema.forceRemove = true; - await expect( - callRule(checkDependencies(schema), tree) - ).resolves.not.toThrow(); + expect(() => { + checkDependencies(tree, schema); + }).not.toThrow(); }); }); describe('implicit dependencies', () => { beforeEach(async () => { - tree = (await callRule( - updateJsonInTree('nx.json', (json) => { - json.projects['my-dependent'].implicitDependencies = ['my-source']; - return json; - }), - tree - )) as UnitTestTree; + const config = readProjectConfiguration(tree, 'my-dependent'); + updateProjectConfiguration(tree, 'my-dependent', { + ...config, + implicitDependencies: ['my-source'], + }); }); it('should fatally error if any dependent exists', async () => { - await expect(callRule(checkDependencies(schema), tree)).rejects.toThrow( + expect(() => { + checkDependencies(tree, schema); + }).toThrow( `${schema.projectName} is still depended on by the following projects:\nmy-dependent` ); }); @@ -78,15 +83,15 @@ describe('updateImports Rule', () => { it('should not error if forceRemove is true', async () => { schema.forceRemove = true; - await expect( - callRule(checkDependencies(schema), tree) - ).resolves.not.toThrow(); + expect(() => { + checkDependencies(tree, schema); + }).not.toThrow(); }); }); it('should not error if there are no dependents', async () => { - await expect( - callRule(checkDependencies(schema), tree) - ).resolves.not.toThrow(); + expect(() => { + checkDependencies(tree, schema); + }).not.toThrow(); }); }); diff --git a/packages/workspace/src/schematics/remove/lib/check-dependencies.ts b/packages/workspace/src/schematics/remove/lib/check-dependencies.ts index 62d09a4ea8..6a97d6b639 100644 --- a/packages/workspace/src/schematics/remove/lib/check-dependencies.ts +++ b/packages/workspace/src/schematics/remove/lib/check-dependencies.ts @@ -1,83 +1,37 @@ -import { Rule, Tree } from '@angular-devkit/schematics'; -import { FileData } from '@nrwl/workspace/src/core/file-utils'; +import { Tree } from '@nrwl/devkit'; import { - readNxJsonInTree, - readWorkspace, -} from '@nrwl/workspace/src/utils/ast-utils'; -import { getWorkspacePath } from '@nrwl/workspace/src/utils/cli-config-utils'; -import ignore from 'ignore'; -import * as path from 'path'; -import { - createProjectGraph, onlyWorkspaceProjects, ProjectGraph, reverse, } from '../../../core/project-graph'; import { Schema } from '../schema'; +import { createProjectGraphFromTree } from '../../../utils/create-project-graph-from-tree'; /** * Check whether the project to be removed is depended on by another project * * Throws an error if the project is in use, unless the `--forceRemove` option is used. - * - * @param schema The options provided to the schematic */ -export function checkDependencies(schema: Schema): Rule { +export function checkDependencies(tree: Tree, schema: Schema) { if (schema.forceRemove) { - return (tree: Tree) => tree; + return; } - let ig = ignore(); - return (tree: Tree): Tree => { - if (tree.exists('.gitignore')) { - ig = ig.add(tree.read('.gitignore').toString()); - } - const files: FileData[] = []; - const workspaceDir = path.dirname(getWorkspacePath(tree)); + const graph: ProjectGraph = createProjectGraphFromTree(tree); - for (const dir of tree.getDir('/').subdirs) { - if (ig.ignores(dir)) { - continue; - } + const reverseGraph = onlyWorkspaceProjects(reverse(graph)); - tree.getDir(dir).visit((file: string) => { - files.push({ - file: path.relative(workspaceDir, file), - ext: path.extname(file), - hash: '', - }); - }); - } + const deps = reverseGraph.dependencies[schema.projectName] || []; - const graph: ProjectGraph = createProjectGraph( - readWorkspace(tree), - readNxJsonInTree(tree), - files, - (file) => { - try { - return tree.read(file).toString('utf-8'); - } catch (e) { - throw new Error(`Could not read ${file}`); - } - }, - false, - false - ); + if (deps.length === 0) { + return; + } - const reverseGraph = onlyWorkspaceProjects(reverse(graph)); - - const deps = reverseGraph.dependencies[schema.projectName] || []; - - if (deps.length === 0) { - return tree; - } - - throw new Error( - `${ - schema.projectName - } is still depended on by the following projects:\n${deps - .map((x) => x.target) - .join('\n')}` - ); - }; + throw new Error( + `${ + schema.projectName + } is still depended on by the following projects:\n${deps + .map((x) => x.target) + .join('\n')}` + ); } diff --git a/packages/workspace/src/schematics/remove/lib/check-targets.spec.ts b/packages/workspace/src/schematics/remove/lib/check-targets.spec.ts index 8f51a3c510..604898c0ac 100644 --- a/packages/workspace/src/schematics/remove/lib/check-targets.spec.ts +++ b/packages/workspace/src/schematics/remove/lib/check-targets.spec.ts @@ -1,18 +1,14 @@ -import { Tree } from '@angular-devkit/schematics'; -import { UnitTestTree } from '@angular-devkit/schematics/testing'; -import { updateWorkspaceInTree } from '@nrwl/workspace/src/utils/ast-utils'; -import { createEmptyWorkspace } from '@nrwl/workspace/testing'; -import { callRule } from '../../../utils/testing'; +import { addProjectConfiguration, Tree } from '@nrwl/devkit'; +import { createTreeWithEmptyWorkspace } from '@nrwl/devkit/testing'; import { Schema } from '../schema'; import { checkTargets } from './check-targets'; -describe('checkTargets Rule', () => { - let tree: UnitTestTree; +describe('checkTargets', () => { + let tree: Tree; let schema: Schema; beforeEach(async () => { - tree = new UnitTestTree(Tree.empty()); - tree = createEmptyWorkspace(tree) as UnitTestTree; + tree = createTreeWithEmptyWorkspace(); schema = { projectName: 'ng-app', @@ -20,59 +16,54 @@ describe('checkTargets Rule', () => { forceRemove: false, }; - 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', - }, - }, - }, - }, + addProjectConfiguration(tree, 'ng-app', { + projectType: 'application', + root: 'apps/ng-app', + sourceRoot: 'apps/ng-app/src', + targets: { + build: { + executor: '@angular-devkit/build-angular:browser', + options: {}, + }, + }, + }); + + addProjectConfiguration(tree, 'ng-app-e2e', { + root: 'apps/ng-app-e2e', + sourceRoot: 'apps/ng-app-e2e/src', + projectType: 'application', + targets: { + e2e: { + executor: '@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 throw an error if another project targets', async () => { - await expect(callRule(checkTargets(schema), tree)).rejects.toThrow(); + expect(() => { + checkTargets(tree, schema); + }).toThrow(); }); it('should NOT throw an error if no other project targets', async () => { schema.projectName = 'ng-app-e2e'; - await expect(callRule(checkTargets(schema), tree)).resolves.not.toThrow(); + expect(() => { + checkTargets(tree, schema); + }).not.toThrow(); }); it('should not error if forceRemove is true', async () => { schema.forceRemove = true; - await expect(callRule(checkTargets(schema), tree)).resolves.not.toThrow(); + expect(() => { + checkTargets(tree, schema); + }).not.toThrow(); }); }); diff --git a/packages/workspace/src/schematics/remove/lib/check-targets.ts b/packages/workspace/src/schematics/remove/lib/check-targets.ts index 9c98ea93be..af91cd1b2b 100644 --- a/packages/workspace/src/schematics/remove/lib/check-targets.ts +++ b/packages/workspace/src/schematics/remove/lib/check-targets.ts @@ -1,5 +1,4 @@ -import { Tree } from '@angular-devkit/schematics'; -import { updateWorkspaceInTree } from '@nrwl/workspace'; +import { getProjects, Tree } from '@nrwl/devkit'; import { Schema } from '../schema'; /** @@ -9,36 +8,30 @@ import { Schema } from '../schema'; * * @param schema The options provided to the schematic */ -export function checkTargets(schema: Schema) { +export function checkTargets(tree: Tree, schema: Schema) { if (schema.forceRemove) { - return (tree: Tree) => tree; + return; } - return updateWorkspaceInTree((workspace) => { + const usedIn = []; + getProjects(tree).forEach((project, projectName) => { const findTarget = new RegExp(`${schema.projectName}:`); - const usedIn = []; - - for (const name of Object.keys(workspace.projects)) { - if (name === schema.projectName) { - continue; - } - - const projectStr = JSON.stringify(workspace.projects[name]); - - if (findTarget.test(projectStr)) { - usedIn.push(name); - } + if (projectName === schema.projectName) { + return; } - if (usedIn.length > 0) { - let message = `${schema.projectName} is still targeted by the following projects:\n\n`; - for (let project of usedIn) { - message += `${project}\n`; - } - throw new Error(message); + if (findTarget.test(JSON.stringify(project))) { + usedIn.push(projectName); } - - return workspace; }); + + if (usedIn.length > 0) { + let message = `${schema.projectName} is still targeted by the following projects:\n\n`; + for (let project of usedIn) { + message += `${project}\n`; + } + + throw new Error(message); + } } diff --git a/packages/workspace/src/schematics/remove/lib/remove-project-config.spec.ts b/packages/workspace/src/schematics/remove/lib/remove-project-config.spec.ts new file mode 100644 index 0000000000..b6c13d606c --- /dev/null +++ b/packages/workspace/src/schematics/remove/lib/remove-project-config.spec.ts @@ -0,0 +1,120 @@ +import { + addProjectConfiguration, + readProjectConfiguration, + readWorkspaceConfiguration, + Tree, + updateWorkspaceConfiguration, +} from '@nrwl/devkit'; +import { createTreeWithEmptyWorkspace } from '@nrwl/devkit/testing'; + +import { Schema } from '../schema'; +import { removeProjectConfig } from './remove-project-config'; + +describe('removeProjectConfig', () => { + let tree: Tree; + let schema: Schema; + + beforeEach(async () => { + tree = createTreeWithEmptyWorkspace(); + + addProjectConfiguration(tree, 'ng-app', { + projectType: 'application', + root: 'apps/ng-app', + sourceRoot: 'apps/ng-app/src', + targets: { + build: { + executor: '@angular-devkit/build-angular:browser', + options: {}, + }, + }, + }); + + addProjectConfiguration(tree, 'ng-app-e2e', { + root: 'apps/ng-app-e2e', + sourceRoot: 'apps/ng-app-e2e/src', + projectType: 'application', + targets: { + e2e: { + executor: '@nrwl/cypress:cypress', + options: { + cypressConfig: 'apps/ng-app-e2e/cypress.json', + tsConfig: 'apps/ng-app-e2e/tsconfig.e2e.json', + devServerTarget: 'ng-app:serve', + }, + }, + }, + implicitDependencies: ['ng-app'], + }); + }); + + describe('delete project', async () => { + beforeEach(async () => { + schema = { + projectName: 'ng-app', + skipFormat: false, + forceRemove: false, + }; + }); + + it('should delete the project', async () => { + removeProjectConfig(tree, schema); + + expect(() => { + readProjectConfiguration(tree, schema.projectName); + }).toThrow(); + }); + }); + + describe('defaultProject', () => { + beforeEach(async () => { + const workspaceConfig = readWorkspaceConfiguration(tree); + updateWorkspaceConfiguration(tree, { + ...workspaceConfig, + defaultProject: 'ng-app', + }); + }); + + it('should remove defaultProject if it matches the project being deleted', async () => { + schema = { + projectName: 'ng-app', + skipFormat: false, + forceRemove: false, + }; + + removeProjectConfig(tree, schema); + + const { defaultProject } = readWorkspaceConfiguration(tree); + expect(defaultProject).toBeUndefined(); + }); + + it('should not remove defaultProject if it does not match the project being deleted', async () => { + schema = { + projectName: 'ng-app-e2e', + skipFormat: false, + forceRemove: false, + }; + + removeProjectConfig(tree, schema); + + const { defaultProject } = readWorkspaceConfiguration(tree); + expect(defaultProject).toEqual('ng-app'); + }); + + it('should remove implicit dependencies onto the removed project', () => { + schema = { + projectName: 'ng-app', + skipFormat: false, + forceRemove: false, + }; + + removeProjectConfig(tree, schema); + + const { implicitDependencies } = readProjectConfiguration( + tree, + 'ng-app-e2e' + ); + + expect(implicitDependencies).not.toContain('ng-app'); + }); + }); +}); diff --git a/packages/workspace/src/schematics/remove/lib/remove-project-config.ts b/packages/workspace/src/schematics/remove/lib/remove-project-config.ts new file mode 100644 index 0000000000..8ac38f77dd --- /dev/null +++ b/packages/workspace/src/schematics/remove/lib/remove-project-config.ts @@ -0,0 +1,46 @@ +import { Schema } from '../schema'; +import { + getProjects, + Tree, + updateProjectConfiguration, + updateWorkspaceConfiguration, +} from '@nrwl/devkit'; +import { + readWorkspaceConfiguration, + removeProjectConfiguration, + getWorkspacePath, +} from '@nrwl/devkit'; + +/** + * Deletes the project from the workspace file + * + * @param schema The options provided to the schematic + */ +export function removeProjectConfig(tree: Tree, schema: Schema) { + removeProjectConfiguration(tree, schema.projectName); + + // Unset default project if deleting the default project + const workspaceConfiguration = readWorkspaceConfiguration(tree); + if ( + workspaceConfiguration.defaultProject && + workspaceConfiguration.defaultProject === schema.projectName + ) { + const workspacePath = getWorkspacePath(tree); + delete workspaceConfiguration.defaultProject; + console.warn( + `Default project was removed in ${workspacePath} because it was "${schema.projectName}". If you want a default project you should define a new one.` + ); + + updateWorkspaceConfiguration(tree, workspaceConfiguration); + } + + // Remove implicit dependencies onto removed project + getProjects(tree).forEach((project, projectName) => { + if (project.implicitDependencies) { + project.implicitDependencies = project.implicitDependencies.filter( + (projectName) => projectName !== schema.projectName + ); + } + updateProjectConfiguration(tree, projectName, project); + }); +} diff --git a/packages/workspace/src/schematics/remove/lib/remove-project.spec.ts b/packages/workspace/src/schematics/remove/lib/remove-project.spec.ts index d856534f3d..7928ed9224 100644 --- a/packages/workspace/src/schematics/remove/lib/remove-project.spec.ts +++ b/packages/workspace/src/schematics/remove/lib/remove-project.spec.ts @@ -1,16 +1,18 @@ -import { Tree } from '@angular-devkit/schematics'; -import { UnitTestTree } from '@angular-devkit/schematics/testing'; -import { createEmptyWorkspace } from '@nrwl/workspace/testing'; -import { runSchematic } from '../../../utils/testing'; +import { readProjectConfiguration, Tree } from '@nrwl/devkit'; +import { createTreeWithEmptyWorkspace } from '@nrwl/devkit/testing'; import { Schema } from '../schema'; +import { libraryGenerator } from '../../library/library'; +import { removeProject } from '@nrwl/workspace/src/schematics/remove/lib/remove-project'; -describe('moveProject Rule', () => { - let tree: UnitTestTree; +describe('moveProject', () => { let schema: Schema; + let tree: Tree; beforeEach(async () => { - tree = createEmptyWorkspace(Tree.empty()) as UnitTestTree; - tree = await runSchematic('lib', { name: 'my-lib' }, tree); + tree = createTreeWithEmptyWorkspace(); + await libraryGenerator(tree, { + name: 'my-lib', + }); schema = { projectName: 'my-lib', @@ -20,15 +22,7 @@ describe('moveProject Rule', () => { }); it('should delete the project folder', async () => { - // TODO - Currently this test will fail due to - // https://github.com/angular/angular-cli/issues/16527 - // tree = (await callRule(removeProject(schema), tree)) as UnitTestTree; - // - // const libDir = tree.getDir('libs/my-lib'); - // let filesFound = false; - // libDir.visit(_file => { - // filesFound = true; - // }); - // expect(filesFound).toBeFalsy(); + removeProject(tree, readProjectConfiguration(tree, 'my-lib')); + expect(tree.children('libs')).not.toContain('my-lib'); }); }); diff --git a/packages/workspace/src/schematics/remove/lib/remove-project.ts b/packages/workspace/src/schematics/remove/lib/remove-project.ts index 965789b9bc..8e3a09309e 100644 --- a/packages/workspace/src/schematics/remove/lib/remove-project.ts +++ b/packages/workspace/src/schematics/remove/lib/remove-project.ts @@ -1,22 +1,11 @@ -import { SchematicContext, Tree } from '@angular-devkit/schematics'; -import { getWorkspace } from '@nrwl/workspace'; -import { from, Observable } from 'rxjs'; -import { map } from 'rxjs/operators'; -import { Schema } from '../schema'; +import { ProjectConfiguration, Tree, visitNotIgnoredFiles } from '@nrwl/devkit'; /** - * Removes (deletes) a project from the folder tree - * - * @param schema The options provided to the schematic + * Removes (deletes) a project's files from the folder tree */ -export function removeProject(schema: Schema) { - return (tree: Tree, _context: SchematicContext): Observable => { - return from(getWorkspace(tree)).pipe( - map((workspace) => { - const project = workspace.projects.get(schema.projectName); - tree.delete(project.root); - return tree; - }) - ); - }; +export function removeProject(tree: Tree, project: ProjectConfiguration) { + visitNotIgnoredFiles(tree, project.root, (file) => { + tree.delete(file); + }); + tree.delete(project.root); } diff --git a/packages/workspace/src/schematics/remove/lib/update-jest-config.spec.ts b/packages/workspace/src/schematics/remove/lib/update-jest-config.spec.ts index fb4da78d37..839c941f5e 100644 --- a/packages/workspace/src/schematics/remove/lib/update-jest-config.spec.ts +++ b/packages/workspace/src/schematics/remove/lib/update-jest-config.spec.ts @@ -1,22 +1,19 @@ -import { Tree } from '@angular-devkit/schematics'; -import { UnitTestTree } from '@angular-devkit/schematics/testing'; -import { - callRule, - createEmptyWorkspace, - runSchematic, -} from '@nrwl/workspace/testing'; -import { Schema } from '../schema'; +import { Tree } from '@nrwl/devkit'; +import { createTreeWithEmptyWorkspace } from '@nrwl/devkit/testing'; + import { readFileSync } from 'fs'; import { join } from 'path'; -import { updateJestConfig } from './update-jest-config'; -describe('updateRootJestConfig Rule', () => { - let tree: UnitTestTree; +import { Schema } from '../schema'; +import { updateJestConfig } from './update-jest-config'; +import { libraryGenerator } from '../../library/library'; + +describe('updateRootJestConfig', () => { + let tree: Tree; let schema: Schema; beforeEach(async () => { - tree = new UnitTestTree(Tree.empty()); - tree = createEmptyWorkspace(tree) as UnitTestTree; + tree = createTreeWithEmptyWorkspace(); schema = { projectName: 'my-lib', @@ -24,22 +21,24 @@ describe('updateRootJestConfig Rule', () => { forceRemove: false, }; - tree = await runSchematic('lib', { name: 'my-lib' }, tree); + await libraryGenerator(tree, { + name: 'my-lib', + }); - tree.overwrite( + tree.write( 'jest.config.js', readFileSync(join(__dirname, './test-files/jest.config.js')).toString() ); }); it('should delete lib project ref from root jest config', async () => { - const jestConfig = tree.readContent('jest.config.js'); + const jestConfig = tree.read('jest.config.js').toString(); expect(jestConfig).toMatchSnapshot(); - tree = (await callRule(updateJestConfig(schema), tree)) as UnitTestTree; + updateJestConfig(tree, schema); - const updatedJestConfig = tree.readContent('jest.config.js'); + const updatedJestConfig = tree.read('jest.config.js').toString(); expect(updatedJestConfig).toMatchSnapshot(); }); diff --git a/packages/workspace/src/schematics/remove/lib/update-jest-config.ts b/packages/workspace/src/schematics/remove/lib/update-jest-config.ts index 3d5296eeb4..f8cc91ece9 100644 --- a/packages/workspace/src/schematics/remove/lib/update-jest-config.ts +++ b/packages/workspace/src/schematics/remove/lib/update-jest-config.ts @@ -1,54 +1,46 @@ -import { SchematicContext, Tree } from '@angular-devkit/schematics'; -import { getWorkspace, insert, RemoveChange } from '@nrwl/workspace'; +import { + applyChangesToString, + ChangeType, + StringChange, + Tree, +} from '@nrwl/devkit'; import * as ts from 'typescript'; import { getSourceNodes } from '@nrwl/workspace/src/utils/ast-utils'; -import { from, Observable } from 'rxjs'; -import { map } from 'rxjs/operators'; + +import { Schema } from '../schema'; /** * Updates the root jest config projects array and removes the project. - * - * @param schema The options provided to the schematic */ -export function updateJestConfig(schema) { - return (tree: Tree, _context: SchematicContext): Observable => { - return from(getWorkspace(tree)).pipe( - map((_) => { - const projectToRemove = schema.projectName; +export function updateJestConfig(tree: Tree, schema: Schema) { + const projectToRemove = schema.projectName; - if (!tree.exists('jest.config.js')) { - return tree; - } + if (!tree.exists('jest.config.js')) { + return; + } - const contents = tree.read('jest.config.js').toString(); - const sourceFile = ts.createSourceFile( - 'jest.config.js', - contents, - ts.ScriptTarget.Latest - ); + const contents = tree.read('jest.config.js').toString(); + const sourceFile = ts.createSourceFile( + 'jest.config.js', + contents, + ts.ScriptTarget.Latest + ); - const changes: RemoveChange[] = []; - const sourceNodes = getSourceNodes(sourceFile); + const changes: StringChange[] = []; + const sourceNodes = getSourceNodes(sourceFile); - sourceNodes.forEach((node, index) => { - if ( - ts.isToken(node) && - ts.isStringLiteral(node) && - node.text.includes(projectToRemove) - ) { - changes.push( - new RemoveChange( - 'jest.config.js', - node.getStart(sourceFile), - node.getFullText(sourceFile) - ) - ); - } - }); - insert(tree, 'jest.config.js', changes); - - return tree; - }) - ); - }; + sourceNodes.forEach((node) => { + if ( + ts.isToken(node) && + ts.isStringLiteral(node) && + node.text.includes(projectToRemove) + ) { + changes.push({ + type: ChangeType.Delete, + start: node.getStart(sourceFile), + length: node.getFullText(sourceFile).length, + }); + } + }); + tree.write('jest.config.js', applyChangesToString(contents, changes)); } diff --git a/packages/workspace/src/schematics/remove/lib/update-nx-json.spec.ts b/packages/workspace/src/schematics/remove/lib/update-nx-json.spec.ts deleted file mode 100644 index c06083ad53..0000000000 --- a/packages/workspace/src/schematics/remove/lib/update-nx-json.spec.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { Tree } from '@angular-devkit/schematics'; -import { UnitTestTree } from '@angular-devkit/schematics/testing'; -import { readJsonInTree } from '@nrwl/workspace'; -import { createEmptyWorkspace } from '@nrwl/workspace/testing'; -import { callRule, runSchematic } from '../../../utils/testing'; -import { Schema } from '../schema'; -import { updateNxJson } from './update-nx-json'; -import { updateJsonInTree } from '@nrwl/workspace/src/utils/ast-utils'; - -describe('updateNxJson Rule', () => { - let tree: UnitTestTree; - - beforeEach(async () => { - tree = new UnitTestTree(Tree.empty()); - tree = createEmptyWorkspace(tree) as UnitTestTree; - }); - - it('should update nx.json', async () => { - tree = await runSchematic('lib', { name: 'my-lib1' }, tree); - tree = await runSchematic('lib', { name: 'my-lib2' }, tree); - - let nxJson = readJsonInTree(tree, '/nx.json'); - expect(nxJson.projects['my-lib1']).toBeDefined(); - - tree = (await callRule( - updateJsonInTree('nx.json', (json) => { - json.projects['my-lib2'].implicitDependencies = [ - 'my-lib1', - 'my-other-lib', - ]; - return json; - }), - tree - )) as UnitTestTree; - - const schema: Schema = { - projectName: 'my-lib1', - skipFormat: false, - forceRemove: false, - }; - - tree = (await callRule(updateNxJson(schema), tree)) as UnitTestTree; - - nxJson = readJsonInTree(tree, '/nx.json'); - expect(nxJson.projects['my-lib1']).toBeUndefined(); - expect(nxJson.projects['my-lib2'].implicitDependencies).toEqual([ - 'my-other-lib', - ]); - }); -}); diff --git a/packages/workspace/src/schematics/remove/lib/update-nx-json.ts b/packages/workspace/src/schematics/remove/lib/update-nx-json.ts deleted file mode 100644 index c165ea6a7f..0000000000 --- a/packages/workspace/src/schematics/remove/lib/update-nx-json.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { NxJson, updateJsonInTree } from '@nrwl/workspace'; -import { Schema } from '../schema'; - -/** - * Updates the nx.json file to remove the project - * - * @param schema The options provided to the schematic - */ -export function updateNxJson(schema: Schema) { - return updateJsonInTree('nx.json', (json) => { - delete json.projects[schema.projectName]; - - Object.values(json.projects).forEach((project) => { - if (project.implicitDependencies) { - project.implicitDependencies = project.implicitDependencies.filter( - (dep) => dep !== schema.projectName - ); - } - }); - - return json; - }); -} diff --git a/packages/workspace/src/schematics/remove/lib/update-tsconfig.spec.ts b/packages/workspace/src/schematics/remove/lib/update-tsconfig.spec.ts index 1c8b85fae1..3c4baa3ed9 100644 --- a/packages/workspace/src/schematics/remove/lib/update-tsconfig.spec.ts +++ b/packages/workspace/src/schematics/remove/lib/update-tsconfig.spec.ts @@ -1,18 +1,15 @@ -import { Tree } from '@angular-devkit/schematics'; -import { UnitTestTree } from '@angular-devkit/schematics/testing'; -import { readJsonInTree } from '@nrwl/workspace'; -import { createEmptyWorkspace } from '@nrwl/workspace/testing'; -import { callRule, runSchematic } from '../../../utils/testing'; +import { readJson, readProjectConfiguration, Tree } from '@nrwl/devkit'; +import { createTreeWithEmptyWorkspace } from '@nrwl/devkit/testing'; import { Schema } from '../schema'; import { updateTsconfig } from './update-tsconfig'; +import { libraryGenerator } from '../../library/library'; -describe('updateTsconfig Rule', () => { - let tree: UnitTestTree; +describe('updateTsconfig', () => { + let tree: Tree; let schema: Schema; beforeEach(async () => { - tree = new UnitTestTree(Tree.empty()); - tree = createEmptyWorkspace(tree) as UnitTestTree; + tree = createTreeWithEmptyWorkspace(); schema = { projectName: 'my-lib', @@ -22,16 +19,14 @@ describe('updateTsconfig Rule', () => { }); it('should delete project ref from the tsconfig', async () => { - tree = await runSchematic('lib', { name: 'my-lib' }, tree); - - let tsConfig = readJsonInTree(tree, '/tsconfig.base.json'); - expect(tsConfig.compilerOptions.paths).toEqual({ - '@proj/my-lib': ['libs/my-lib/src/index.ts'], + await libraryGenerator(tree, { + name: 'my-lib', }); + const project = readProjectConfiguration(tree, 'my-lib'); - tree = (await callRule(updateTsconfig(schema), tree)) as UnitTestTree; + updateTsconfig(tree, schema, project); - tsConfig = readJsonInTree(tree, '/tsconfig.base.json'); + const tsConfig = readJson(tree, '/tsconfig.base.json'); expect(tsConfig.compilerOptions.paths).toEqual({}); }); }); diff --git a/packages/workspace/src/schematics/remove/lib/update-tsconfig.ts b/packages/workspace/src/schematics/remove/lib/update-tsconfig.ts index 58a96fc9c1..5128288b69 100644 --- a/packages/workspace/src/schematics/remove/lib/update-tsconfig.ts +++ b/packages/workspace/src/schematics/remove/lib/update-tsconfig.ts @@ -1,12 +1,10 @@ -import { SchematicContext, Tree } from '@angular-devkit/schematics'; import { - getWorkspace, - NxJson, - readJsonInTree, - serializeJson, -} from '@nrwl/workspace'; -import { from, Observable } from 'rxjs'; -import { map } from 'rxjs/operators'; + ProjectConfiguration, + Tree, + updateJson, + getWorkspaceLayout, +} from '@nrwl/devkit'; +import { NxJson, readJsonInTree } from '@nrwl/workspace'; import { Schema } from '../schema'; /** @@ -14,24 +12,21 @@ import { Schema } from '../schema'; * * @param schema The options provided to the schematic */ -export function updateTsconfig(schema: Schema) { - return (tree: Tree, _context: SchematicContext): Observable => { - return from(getWorkspace(tree)).pipe( - map((workspace) => { - const nxJson = readJsonInTree(tree, 'nx.json'); - const project = workspace.projects.get(schema.projectName); +export function updateTsconfig( + tree: Tree, + schema: Schema, + project: ProjectConfiguration +) { + const { npmScope } = getWorkspaceLayout(tree); - const tsConfigPath = 'tsconfig.base.json'; - if (tree.exists(tsConfigPath)) { - const tsConfigJson = readJsonInTree(tree, tsConfigPath); - delete tsConfigJson.compilerOptions.paths[ - `@${nxJson.npmScope}/${project.root.substr(5)}` - ]; - tree.overwrite(tsConfigPath, serializeJson(tsConfigJson)); - } + const tsConfigPath = 'tsconfig.base.json'; + if (tree.exists(tsConfigPath)) { + updateJson(tree, tsConfigPath, (json) => { + delete json.compilerOptions.paths[ + `@${npmScope}/${project.root.substr(5)}` + ]; - return tree; - }) - ); - }; + return json; + }); + } } diff --git a/packages/workspace/src/schematics/remove/lib/update-workspace.spec.ts b/packages/workspace/src/schematics/remove/lib/update-workspace.spec.ts deleted file mode 100644 index 196addadef..0000000000 --- a/packages/workspace/src/schematics/remove/lib/update-workspace.spec.ts +++ /dev/null @@ -1,165 +0,0 @@ -import { Tree } from '@angular-devkit/schematics'; -import { UnitTestTree } from '@angular-devkit/schematics/testing'; -import { updateWorkspaceInTree } from '@nrwl/workspace'; -import { createEmptyWorkspace } from '@nrwl/workspace/testing'; -import { callRule } from '../../../utils/testing'; -import { Schema } from '../schema'; -import { updateWorkspace } from './update-workspace'; - -describe('updateWorkspace Rule', () => { - let tree: UnitTestTree; - let schema: Schema; - - beforeEach(async () => { - tree = new UnitTestTree(Tree.empty()); - tree = createEmptyWorkspace(tree) as UnitTestTree; - }); - - 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': { - 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', - sourceRoot: 'apps/ng-app/src', - prefix: 'happyorg', - architect: { - build: { - builder: '@angular-devkit/build-angular:browser', - options: {}, - }, - }, - }, - defaultProject: 'ng-app', - }; - }), - tree - )) as UnitTestTree; - }); - - it('should remove defaultProject if it matches the project being deleted', async () => { - schema = { - projectName: 'ng-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).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(); - }); - }); -}); diff --git a/packages/workspace/src/schematics/remove/lib/update-workspace.ts b/packages/workspace/src/schematics/remove/lib/update-workspace.ts deleted file mode 100644 index eabdacbbd2..0000000000 --- a/packages/workspace/src/schematics/remove/lib/update-workspace.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { Schema } from '../schema'; -import { SchematicContext, Tree } from '@angular-devkit/schematics'; -import { updateWorkspaceInTree, getWorkspacePath } from '@nrwl/workspace'; - -/** - * Deletes the project from the workspace file - * - * @param schema The options provided to the schematic - */ -export function updateWorkspace(schema: Schema) { - 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; - } - ); -} diff --git a/packages/workspace/src/schematics/remove/remove.ts b/packages/workspace/src/schematics/remove/remove.ts index f45606eab4..8c87bb823e 100644 --- a/packages/workspace/src/schematics/remove/remove.ts +++ b/packages/workspace/src/schematics/remove/remove.ts @@ -1,31 +1,31 @@ -import { chain, Rule } from '@angular-devkit/schematics'; -import { checkProjectExists } from '../../utils/rules/check-project-exists'; -import { formatFiles } from '../../utils/rules/format-files'; +import { + convertNxGenerator, + formatFiles, + readProjectConfiguration, + Tree, +} from '@nrwl/devkit'; + import { checkDependencies } from './lib/check-dependencies'; import { checkTargets } from './lib/check-targets'; import { removeProject } from './lib/remove-project'; -import { updateNxJson } from './lib/update-nx-json'; import { updateTsconfig } from './lib/update-tsconfig'; -import { updateWorkspace } from './lib/update-workspace'; +import { removeProjectConfig } from './lib/remove-project-config'; import { Schema } from './schema'; -import { wrapAngularDevkitSchematic } from '@nrwl/devkit/ngcli-adapter'; import { updateJestConfig } from './lib/update-jest-config'; -export default function (schema: Schema): Rule { - return chain([ - checkProjectExists(schema), - checkDependencies(schema), - checkTargets(schema), - removeProject(schema), - updateNxJson(schema), - updateTsconfig(schema), - updateWorkspace(schema), - updateJestConfig(schema), - formatFiles(schema), - ]); +export async function removeGenerator(tree: Tree, schema: Schema) { + const project = readProjectConfiguration(tree, schema.projectName); + checkDependencies(tree, schema); + checkTargets(tree, schema); + removeProject(tree, project); + removeProjectConfig(tree, schema); + updateTsconfig(tree, schema, project); + updateJestConfig(tree, schema); + if (!schema.skipFormat) { + await formatFiles(tree); + } } -export const removeGenerator = wrapAngularDevkitSchematic( - '@nrwl/workspace', - 'remove' -); +export default removeGenerator; + +export const removeSchematic = convertNxGenerator(removeGenerator); diff --git a/packages/workspace/src/schematics/remove/schema.json b/packages/workspace/src/schematics/remove/schema.json index f839c53ea4..2531b6314a 100644 --- a/packages/workspace/src/schematics/remove/schema.json +++ b/packages/workspace/src/schematics/remove/schema.json @@ -1,6 +1,7 @@ { "$schema": "http://json-schema.org/schema", "id": "NxWorkspaceRemove", + "cli": "nx", "title": "Nx Remove", "description": "Remove a project from the workspace", "type": "object", diff --git a/packages/workspace/src/utils/create-project-graph-from-tree.ts b/packages/workspace/src/utils/create-project-graph-from-tree.ts new file mode 100644 index 0000000000..6107c8ad12 --- /dev/null +++ b/packages/workspace/src/utils/create-project-graph-from-tree.ts @@ -0,0 +1,37 @@ +import { + getWorkspacePath, + readJson, + Tree, + visitNotIgnoredFiles, +} from '@nrwl/devkit'; +import { createProjectGraph } from '../core/project-graph/project-graph'; +import { FileData } from '../core/file-utils'; +import { extname } from 'path'; + +export function createProjectGraphFromTree(tree: Tree) { + const workspaceJson = readJson(tree, getWorkspacePath(tree)); + const nxJson = readJson(tree, 'nx.json'); + + const files: FileData[] = []; + + visitNotIgnoredFiles(tree, '', (file) => { + files.push({ + file: file, + ext: extname(file), + hash: '', + }); + }); + + const readFile = (path) => { + return tree.read(path).toString('utf-8'); + }; + + return createProjectGraph( + workspaceJson, + nxJson, + files, + readFile, + false, + false + ); +}