diff --git a/docs/angular/api-workspace/schematics/remove.md b/docs/angular/api-workspace/schematics/remove.md new file mode 100644 index 0000000000..fd2588c14e --- /dev/null +++ b/docs/angular/api-workspace/schematics/remove.md @@ -0,0 +1,61 @@ +# remove + +Remove an application or library + +## Usage + +```bash +ng generate remove ... +``` + +```bash +ng g rm ... # same +``` + +By default, Nx will search for `remove` in the default collection provisioned in `angular.json`. + +You can specify the collection explicitly as follows: + +```bash +ng g @nrwl/workspace:remove ... +``` + +Show what will be generated without writing to disk: + +```bash +ng g remove ... --dry-run +``` + +### Examples + +Remove my-feature-lib from the workspace: + +```bash +ng g @nrwl/workspace:remove my-feature-lib +``` + +Force removal of my-feature-lib from the workspace: + +```bash +ng g @nrwl/workspace:remove my-feature-lib --forceRemove +``` + +## Options + +### forceRemove + +Alias(es): force-remove + +Default: `false` + +Type: `boolean` + +When true, forces removal even if the project is still in use. + +### projectName + +Alias(es): project + +Type: `string` + +The name of the project to remove diff --git a/docs/react/api-workspace/schematics/remove.md b/docs/react/api-workspace/schematics/remove.md new file mode 100644 index 0000000000..c623aec895 --- /dev/null +++ b/docs/react/api-workspace/schematics/remove.md @@ -0,0 +1,61 @@ +# remove + +Remove an application or library + +## Usage + +```bash +nx generate remove ... +``` + +```bash +nx g rm ... # same +``` + +By default, Nx will search for `remove` in the default collection provisioned in `workspace.json`. + +You can specify the collection explicitly as follows: + +```bash +nx g @nrwl/workspace:remove ... +``` + +Show what will be generated without writing to disk: + +```bash +nx g remove ... --dry-run +``` + +### Examples + +Remove my-feature-lib from the workspace: + +```bash +nx g @nrwl/workspace:remove my-feature-lib +``` + +Force removal of my-feature-lib from the workspace: + +```bash +nx g @nrwl/workspace:remove my-feature-lib --forceRemove +``` + +## Options + +### forceRemove + +Alias(es): force-remove + +Default: `false` + +Type: `boolean` + +When true, forces removal even if the project is still in use. + +### projectName + +Alias(es): project + +Type: `string` + +The name of the project to remove diff --git a/docs/web/api-workspace/schematics/remove.md b/docs/web/api-workspace/schematics/remove.md new file mode 100644 index 0000000000..c623aec895 --- /dev/null +++ b/docs/web/api-workspace/schematics/remove.md @@ -0,0 +1,61 @@ +# remove + +Remove an application or library + +## Usage + +```bash +nx generate remove ... +``` + +```bash +nx g rm ... # same +``` + +By default, Nx will search for `remove` in the default collection provisioned in `workspace.json`. + +You can specify the collection explicitly as follows: + +```bash +nx g @nrwl/workspace:remove ... +``` + +Show what will be generated without writing to disk: + +```bash +nx g remove ... --dry-run +``` + +### Examples + +Remove my-feature-lib from the workspace: + +```bash +nx g @nrwl/workspace:remove my-feature-lib +``` + +Force removal of my-feature-lib from the workspace: + +```bash +nx g @nrwl/workspace:remove my-feature-lib --forceRemove +``` + +## Options + +### forceRemove + +Alias(es): force-remove + +Default: `false` + +Type: `boolean` + +When true, forces removal even if the project is still in use. + +### projectName + +Alias(es): project + +Type: `string` + +The name of the project to remove diff --git a/e2e/remove.test.ts b/e2e/remove.test.ts new file mode 100644 index 0000000000..9aa36fec8e --- /dev/null +++ b/e2e/remove.test.ts @@ -0,0 +1,44 @@ +import { NxJson } from '@nrwl/workspace'; +import { + exists, + forEachCli, + newProject, + readFile, + readJson, + runCLI, + tmpProjPath, + uniq +} from './utils'; + +forEachCli(cli => { + describe('Remove Project', () => { + const workspace: string = cli === 'angular' ? 'angular' : 'workspace'; + + /** + * Tries creating then deleting a lib + */ + it('should work', () => { + const lib = uniq('mylib'); + + newProject(); + + runCLI(`generate @nrwl/workspace:lib ${lib}`); + expect(exists(tmpProjPath(`libs/${lib}`))).toBeTruthy(); + + const removeOutput = runCLI( + `generate @nrwl/workspace:remove --project ${lib}` + ); + + expect(removeOutput).toContain(`DELETE libs/${lib}`); + expect(exists(tmpProjPath(`libs/${lib}`))).toBeFalsy(); + + expect(removeOutput).toContain(`UPDATE nx.json`); + const nxJson = JSON.parse(readFile('nx.json')) as NxJson; + expect(nxJson.projects[`${lib}`]).toBeUndefined(); + + expect(removeOutput).toContain(`UPDATE ${workspace}.json`); + const workspaceJson = readJson(`${workspace}.json`); + expect(workspaceJson.projects[`${lib}`]).toBeUndefined(); + }); + }); +}); diff --git a/e2e/utils.ts b/e2e/utils.ts index cce3f518ef..b525a68690 100644 --- a/e2e/utils.ts +++ b/e2e/utils.ts @@ -1,8 +1,13 @@ import { exec, execSync } from 'child_process'; -import { readFileSync, renameSync, statSync, writeFileSync } from 'fs'; +import { + readdirSync, + readFileSync, + renameSync, + statSync, + writeFileSync +} from 'fs'; import { ensureDirSync } from 'fs-extra'; import * as path from 'path'; -import * as fs from 'fs'; export let cli; @@ -486,7 +491,7 @@ export function checkFilesDoNotExist(...expectedFiles: string[]) { } export function listFiles(dirName: string) { - return fs.readdirSync(tmpProjPath(dirName)); + return readdirSync(tmpProjPath(dirName)); } export function readJson(f: string): any { diff --git a/packages/workspace/collection.json b/packages/workspace/collection.json index 5fdcbb1879..8f368a53bc 100644 --- a/packages/workspace/collection.json +++ b/packages/workspace/collection.json @@ -30,6 +30,13 @@ "description": "Move an application or library to another folder" }, + "remove": { + "factory": "./src/schematics/remove/remove", + "schema": "./src/schematics/remove/schema.json", + "aliases": ["rm"], + "description": "Remove an application or library" + }, + "ng-new": { "factory": "./src/schematics/ng-new/ng-new", "schema": "./src/schematics/ng-new/schema.json", diff --git a/packages/workspace/src/core/file-utils.ts b/packages/workspace/src/core/file-utils.ts index bee4a423f9..c5597739f6 100644 --- a/packages/workspace/src/core/file-utils.ts +++ b/packages/workspace/src/core/file-utils.ts @@ -1,15 +1,15 @@ -import * as path from 'path'; -import * as fs from 'fs'; -import { appRootPath } from '../utils/app-root'; -import { extname } from 'path'; -import { jsonDiff } from '../utils/json-diff'; -import { readFileSync } from 'fs'; import { execSync } from 'child_process'; -import { readJsonFile } from '../utils/fileutils'; -import { Environment, NxJson } from './shared-interfaces'; -import { ProjectGraphNode } from './project-graph'; -import { WorkspaceResults } from '../command-line/workspace-results'; +import * as fs from 'fs'; +import { readFileSync } from 'fs'; +import * as path from 'path'; +import { extname } from 'path'; import { NxArgs } from '../command-line/utils'; +import { WorkspaceResults } from '../command-line/workspace-results'; +import { appRootPath } from '../utils/app-root'; +import { readJsonFile } from '../utils/fileutils'; +import { jsonDiff } from '../utils/json-diff'; +import { ProjectGraphNode } from './project-graph'; +import { Environment, NxJson } from './shared-interfaces'; const ignore = require('ignore'); @@ -204,10 +204,6 @@ export function rootWorkspaceFileNames(): string[] { return [`package.json`, workspaceFileName(), `nx.json`, `tsconfig.json`]; } -export function rootWorkspaceFileData(): FileData[] { - return rootWorkspaceFileNames().map(f => getFileData(`${appRootPath}/${f}`)); -} - export function readWorkspaceFiles(): FileData[] { const workspaceJson = readWorkspaceJson(); const files = []; diff --git a/packages/workspace/src/core/project-graph/project-graph.ts b/packages/workspace/src/core/project-graph/project-graph.ts index 8c388de5fa..121b176c62 100644 --- a/packages/workspace/src/core/project-graph/project-graph.ts +++ b/packages/workspace/src/core/project-graph/project-graph.ts @@ -1,6 +1,4 @@ import { mkdirSync } from 'fs'; -import { ProjectGraph } from './project-graph-models'; -import { ProjectGraphBuilder } from './project-graph-builder'; import { appRootPath } from '../../utils/app-root'; import { directoryExists, @@ -8,42 +6,43 @@ import { readJsonFile, writeJsonFile } from '../../utils/fileutils'; +import { assertWorkspaceValidity } from '../assert-workspace-validity'; +import { createFileMap, FileMap } from '../file-graph'; import { defaultFileRead, FileData, mtime, readNxJson, readWorkspaceFiles, - readWorkspaceJson, - rootWorkspaceFileData, - rootWorkspaceFileNames + readWorkspaceJson } from '../file-utils'; -import { createFileMap, FileMap } from '../file-graph'; -import { - BuildNodes, - buildNpmPackageNodes, - buildWorkspaceProjectNodes -} from './build-nodes'; +import { normalizeNxJson } from '../normalize-nx-json'; import { BuildDependencies, buildExplicitNpmDependencies, buildExplicitTypeScriptDependencies, buildImplicitProjectDependencies } from './build-dependencies'; -import { assertWorkspaceValidity } from '../assert-workspace-validity'; -import { normalizeNxJson } from '../normalize-nx-json'; +import { + BuildNodes, + buildNpmPackageNodes, + buildWorkspaceProjectNodes +} from './build-nodes'; +import { ProjectGraphBuilder } from './project-graph-builder'; +import { ProjectGraph } from './project-graph-models'; export function createProjectGraph( workspaceJson = readWorkspaceJson(), nxJson = readNxJson(), workspaceFiles = readWorkspaceFiles(), fileRead: (s: string) => string = defaultFileRead, - cache: false | { data: ProjectGraphCache; mtime: number } = readCache() + cache: false | { data: ProjectGraphCache; mtime: number } = readCache(), + shouldCache: boolean = true ): ProjectGraph { assertWorkspaceValidity(workspaceJson, nxJson); const normalizedNxJson = normalizeNxJson(nxJson); - if (cache && maxMTime(rootWorkspaceFileData()) > cache.mtime) { + if (cache && maxMTime(rootWorkspaceFileData(workspaceFiles)) > cache.mtime) { cache = false; } @@ -73,10 +72,12 @@ export function createProjectGraph( ); const projectGraph = builder.build(); - writeCache({ - projectGraph, - fileMap - }); + if (shouldCache) { + writeCache({ + projectGraph, + fileMap + }); + } return projectGraph; } else { // Cache file was modified _after_ all workspace files. @@ -136,6 +137,22 @@ function maxMTime(files: FileData[]) { return Math.max(...files.map(f => f.mtime)); } +function rootWorkspaceFileData(workspaceFiles: FileData[]): FileData[] { + return [ + `/package.json`, + '/workspace.json', + '/angular.json', + `/nx.json`, + `/tsconfig.json` + ].reduce((acc: FileData[], curr: string) => { + const fileData = workspaceFiles.find(x => x.file === curr); + if (fileData) { + acc.push(fileData); + } + return acc; + }, []); +} + function modifiedSinceCache( fileMap: FileMap, c: false | { data: ProjectGraphCache; mtime: number } diff --git a/packages/workspace/src/schematics/move/move.ts b/packages/workspace/src/schematics/move/move.ts index 66d65cbb2a..0fbb59649e 100644 --- a/packages/workspace/src/schematics/move/move.ts +++ b/packages/workspace/src/schematics/move/move.ts @@ -1,4 +1,4 @@ -import { chain } from '@angular-devkit/schematics'; +import { chain, Rule } from '@angular-devkit/schematics'; import { checkDestination } from './lib/check-destination'; import { checkProjectExists } from './lib/check-project-exists'; import { moveProject } from './lib/move-project'; @@ -10,7 +10,7 @@ import { updateProjectRootFiles } from './lib/update-project-root-files'; import { updateWorkspace } from './lib/update-workspace'; import { Schema } from './schema'; -export default function(schema: Schema) { +export default function(schema: Schema): Rule { return chain([ checkProjectExists(schema), checkDestination(schema), diff --git a/packages/workspace/src/schematics/remove/lib/check-dependencies.spec.ts b/packages/workspace/src/schematics/remove/lib/check-dependencies.spec.ts new file mode 100644 index 0000000000..ec926e80fe --- /dev/null +++ b/packages/workspace/src/schematics/remove/lib/check-dependencies.spec.ts @@ -0,0 +1,90 @@ +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 { Schema } from '../schema'; +import { checkDependencies } from './check-dependencies'; + +describe('updateImports Rule', () => { + let tree: UnitTestTree; + let schema: Schema; + + beforeEach(async () => { + tree = new UnitTestTree(Tree.empty()); + tree = createEmptyWorkspace(tree) as UnitTestTree; + + schema = { + projectName: 'my-source' + }; + + tree = await runSchematic('lib', { name: 'my-dependent' }, tree); + tree = await runSchematic('lib', { name: 'my-source' }, tree); + }); + + describe('static dependencies', () => { + beforeEach(() => { + const sourceFilePath = 'libs/my-source/src/lib/my-source.ts'; + tree.overwrite( + sourceFilePath, + `export class MyClass {} + ` + ); + + const dependentFilePath = 'libs/my-dependent/src/lib/my-dependent.ts'; + tree.overwrite( + dependentFilePath, + `import { MyClass } from '@proj/my-source'; + + export MyExtendedClass extends MyClass {}; + ` + ); + }); + + 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` + ); + }); + + it('should not error if forceRemove is true', async () => { + schema.forceRemove = true; + + await expect( + callRule(checkDependencies(schema), tree) + ).resolves.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; + }); + + 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` + ); + }); + + it('should not error if forceRemove is true', async () => { + schema.forceRemove = true; + + await expect( + callRule(checkDependencies(schema), tree) + ).resolves.not.toThrow(); + }); + }); + + it('should not error if there are no dependents', async () => { + await expect( + callRule(checkDependencies(schema), tree) + ).resolves.not.toThrow(); + }); +}); diff --git a/packages/workspace/src/schematics/remove/lib/check-dependencies.ts b/packages/workspace/src/schematics/remove/lib/check-dependencies.ts new file mode 100644 index 0000000000..2edf0a924c --- /dev/null +++ b/packages/workspace/src/schematics/remove/lib/check-dependencies.ts @@ -0,0 +1,66 @@ +import { Rule, Tree } from '@angular-devkit/schematics'; +import { FileData } from '@nrwl/workspace/src/core/file-utils'; +import { + readNxJsonInTree, + readWorkspace +} from '@nrwl/workspace/src/utils/ast-utils'; +import { getWorkspacePath } from '@nrwl/workspace/src/utils/cli-config-utils'; +import * as path from 'path'; +import { + createProjectGraph, + onlyWorkspaceProjects, + ProjectGraph, + reverse +} from '../../../core/project-graph'; +import { Schema } from '../schema'; + +/** + * 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 { + if (schema.forceRemove) { + return (tree: Tree) => tree; + } + + return (tree: Tree): Tree => { + const files: FileData[] = []; + const mtime = Date.now(); //can't get mtime data from the tree :( + const workspaceDir = path.dirname(getWorkspacePath(tree)); + tree.visit(file => { + files.push({ + file: path.relative(workspaceDir, file), + ext: path.extname(file), + mtime + }); + }); + + const graph: ProjectGraph = createProjectGraph( + readWorkspace(tree), + readNxJsonInTree(tree), + files, + file => tree.read(file).toString('utf-8'), + false, + false + ); + + 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')}` + ); + }; +} diff --git a/packages/workspace/src/schematics/remove/lib/check-targets.spec.ts b/packages/workspace/src/schematics/remove/lib/check-targets.spec.ts new file mode 100644 index 0000000000..b04cecaee7 --- /dev/null +++ b/packages/workspace/src/schematics/remove/lib/check-targets.spec.ts @@ -0,0 +1,76 @@ +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 { Schema } from '../schema'; +import { checkTargets } from './check-targets'; + +describe('checkTargets Rule', () => { + let tree: UnitTestTree; + let schema: Schema; + + beforeEach(async () => { + tree = new UnitTestTree(Tree.empty()); + tree = createEmptyWorkspace(tree) as UnitTestTree; + + schema = { + projectName: 'ng-app' + }; + + tree = (await callRule( + updateWorkspaceInTree(workspace => { + return { + version: 1, + projects: { + 'ng-app': { + projectType: 'application', + schematics: {}, + root: 'apps/ng-app', + sourceRoot: 'apps/ng-app/src', + prefix: 'happyorg', + architect: { + build: { + builder: '@angular-devkit/build-angular:browser', + options: {} + } + } + }, + 'ng-app-e2e': { + root: 'apps/ng-app-e2e', + sourceRoot: 'apps/ng-app-e2e/src', + projectType: 'application', + architect: { + e2e: { + builder: '@nrwl/cypress:cypress', + options: { + cypressConfig: 'apps/ng-app-e2e/cypress.json', + tsConfig: 'apps/ng-app-e2e/tsconfig.e2e.json', + devServerTarget: 'ng-app:serve' + } + } + } + } + } + }; + }), + tree + )) as UnitTestTree; + }); + + it('should throw an error if another project targets', async () => { + await expect(callRule(checkTargets(schema), tree)).rejects.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(); + }); + + it('should not error if forceRemove is true', async () => { + schema.forceRemove = true; + + await expect(callRule(checkTargets(schema), tree)).resolves.not.toThrow(); + }); +}); diff --git a/packages/workspace/src/schematics/remove/lib/check-targets.ts b/packages/workspace/src/schematics/remove/lib/check-targets.ts new file mode 100644 index 0000000000..9d79f475f7 --- /dev/null +++ b/packages/workspace/src/schematics/remove/lib/check-targets.ts @@ -0,0 +1,44 @@ +import { Tree } from '@angular-devkit/schematics'; +import { updateWorkspaceInTree } from '@nrwl/workspace'; +import { Schema } from '../schema'; + +/** + * Check whether the project to be removed has builders targetted 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 checkTargets(schema: Schema) { + if (schema.forceRemove) { + return (tree: Tree) => tree; + } + + return updateWorkspaceInTree(workspace => { + 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 (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); + } + + return workspace; + }); +} diff --git a/packages/workspace/src/schematics/remove/lib/remove-project.spec.ts b/packages/workspace/src/schematics/remove/lib/remove-project.spec.ts new file mode 100644 index 0000000000..6ba0f1f3f2 --- /dev/null +++ b/packages/workspace/src/schematics/remove/lib/remove-project.spec.ts @@ -0,0 +1,32 @@ +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 { Schema } from '../schema'; + +describe('moveProject Rule', () => { + let tree: UnitTestTree; + let schema: Schema; + + beforeEach(async () => { + tree = createEmptyWorkspace(Tree.empty()) as UnitTestTree; + tree = await runSchematic('lib', { name: 'my-lib' }, tree); + + schema = { + projectName: 'my-lib' + }; + }); + + 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(); + }); +}); diff --git a/packages/workspace/src/schematics/remove/lib/remove-project.ts b/packages/workspace/src/schematics/remove/lib/remove-project.ts new file mode 100644 index 0000000000..47735a5767 --- /dev/null +++ b/packages/workspace/src/schematics/remove/lib/remove-project.ts @@ -0,0 +1,22 @@ +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'; + +/** + * Removes (deletes) a project from the folder tree + * + * @param schema The options provided to the schematic + */ +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; + }) + ); + }; +} 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 new file mode 100644 index 0000000000..e6ded7cf41 --- /dev/null +++ b/packages/workspace/src/schematics/remove/lib/update-nx-json.spec.ts @@ -0,0 +1,32 @@ +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-lib' }, tree); + + let nxJson = readJsonInTree(tree, '/nx.json'); + expect(nxJson.projects['my-lib']).toBeDefined(); + + const schema: Schema = { + projectName: 'my-lib' + }; + + tree = (await callRule(updateNxJson(schema), tree)) as UnitTestTree; + + nxJson = readJsonInTree(tree, '/nx.json'); + expect(nxJson.projects['my-lib']).toBeUndefined(); + }); +}); diff --git a/packages/workspace/src/schematics/remove/lib/update-nx-json.ts b/packages/workspace/src/schematics/remove/lib/update-nx-json.ts new file mode 100644 index 0000000000..7ad7f5f061 --- /dev/null +++ b/packages/workspace/src/schematics/remove/lib/update-nx-json.ts @@ -0,0 +1,14 @@ +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]; + 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 new file mode 100644 index 0000000000..48620d1d28 --- /dev/null +++ b/packages/workspace/src/schematics/remove/lib/update-tsconfig.spec.ts @@ -0,0 +1,35 @@ +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 { updateTsconfig } from './update-tsconfig'; + +describe('updateTsconfig Rule', () => { + let tree: UnitTestTree; + let schema: Schema; + + beforeEach(async () => { + tree = new UnitTestTree(Tree.empty()); + tree = createEmptyWorkspace(tree) as UnitTestTree; + + schema = { + projectName: 'my-lib' + }; + }); + + it('should delete project ref from the tsconfig', async () => { + tree = await runSchematic('lib', { name: 'my-lib' }, tree); + + let tsConfig = readJsonInTree(tree, '/tsconfig.json'); + expect(tsConfig.compilerOptions.paths).toEqual({ + '@proj/my-lib': ['libs/my-lib/src/index.ts'] + }); + + tree = (await callRule(updateTsconfig(schema), tree)) as UnitTestTree; + + tsConfig = readJsonInTree(tree, '/tsconfig.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 new file mode 100644 index 0000000000..facebde5a0 --- /dev/null +++ b/packages/workspace/src/schematics/remove/lib/update-tsconfig.ts @@ -0,0 +1,37 @@ +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'; +import { Schema } from '../schema'; + +/** + * Updates the tsconfig paths to remove the project. + * + * @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); + + const tsConfigPath = 'tsconfig.json'; + if (tree.exists(tsConfigPath)) { + let contents = JSON.parse(tree.read(tsConfigPath).toString('utf-8')); + delete contents.compilerOptions.paths[ + `@${nxJson.npmScope}/${project.root.substr(5)}` + ]; + tree.overwrite(tsConfigPath, serializeJson(contents)); + } + + return tree; + }) + ); + }; +} diff --git a/packages/workspace/src/schematics/remove/lib/update-workspace.spec.ts b/packages/workspace/src/schematics/remove/lib/update-workspace.spec.ts new file mode 100644 index 0000000000..3eb6780a98 --- /dev/null +++ b/packages/workspace/src/schematics/remove/lib/update-workspace.spec.ts @@ -0,0 +1,70 @@ +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; + + schema = { + projectName: 'ng-app' + }; + + tree = (await callRule( + updateWorkspaceInTree(workspace => { + return { + version: 1, + projects: { + 'ng-app': { + projectType: 'application', + schematics: {}, + root: 'apps/ng-app', + sourceRoot: 'apps/ng-app/src', + prefix: 'happyorg', + architect: { + build: { + builder: '@angular-devkit/build-angular:browser', + options: {} + } + } + }, + 'ng-app-e2e': { + root: 'apps/ng-app-e2e', + sourceRoot: 'apps/ng-app-e2e/src', + projectType: 'application', + architect: { + e2e: { + builder: '@nrwl/cypress:cypress', + options: { + cypressConfig: 'apps/ng-app-e2e/cypress.json', + tsConfig: 'apps/ng-app-e2e/tsconfig.e2e.json', + devServerTarget: 'ng-app:serve' + } + } + } + } + } + }; + }), + tree + )) as UnitTestTree; + }); + + it('should delete the project', async () => { + let workspace = JSON.parse(tree.read('workspace.json').toString()); + expect(workspace.projects['ng-app']).toBeDefined(); + + tree = (await callRule(updateWorkspace(schema), tree)) as UnitTestTree; + + workspace = JSON.parse(tree.read('workspace.json').toString()); + expect(workspace.projects['ng-app']).toBeUndefined(); + }); +}); diff --git a/packages/workspace/src/schematics/remove/lib/update-workspace.ts b/packages/workspace/src/schematics/remove/lib/update-workspace.ts new file mode 100644 index 0000000000..78a8ca2596 --- /dev/null +++ b/packages/workspace/src/schematics/remove/lib/update-workspace.ts @@ -0,0 +1,14 @@ +import { updateWorkspaceInTree } from '@nrwl/workspace'; +import { Schema } from '../schema'; + +/** + * Deletes the project from the workspace file + * + * @param schema The options provided to the schematic + */ +export function updateWorkspace(schema: Schema) { + return updateWorkspaceInTree(workspace => { + delete workspace.projects[schema.projectName]; + return workspace; + }); +} diff --git a/packages/workspace/src/schematics/remove/remove.ts b/packages/workspace/src/schematics/remove/remove.ts new file mode 100644 index 0000000000..e52df8a67c --- /dev/null +++ b/packages/workspace/src/schematics/remove/remove.ts @@ -0,0 +1,19 @@ +import { chain, Rule } from '@angular-devkit/schematics'; +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 { Schema } from './schema'; + +export default function(schema: Schema): Rule { + return chain([ + checkDependencies(schema), + checkTargets(schema), + removeProject(schema), + updateNxJson(schema), + updateTsconfig(schema), + updateWorkspace(schema) + ]); +} diff --git a/packages/workspace/src/schematics/remove/schema.d.ts b/packages/workspace/src/schematics/remove/schema.d.ts new file mode 100644 index 0000000000..93e6688517 --- /dev/null +++ b/packages/workspace/src/schematics/remove/schema.d.ts @@ -0,0 +1,4 @@ +export interface Schema extends json.JsonObject { + projectName: string; + forceRemove?: boolean; +} diff --git a/packages/workspace/src/schematics/remove/schema.json b/packages/workspace/src/schematics/remove/schema.json new file mode 100644 index 0000000000..99f69f7f1b --- /dev/null +++ b/packages/workspace/src/schematics/remove/schema.json @@ -0,0 +1,35 @@ +{ + "$schema": "http://json-schema.org/schema", + "id": "NxWorkspaceRemove", + "title": "Nx Remove", + "description": "Remove a project from the workspace", + "type": "object", + "examples": [ + { + "command": "g @nrwl/workspace:remove my-feature-lib", + "description": "Remove my-feature-lib from the workspace" + }, + { + "command": "g @nrwl/workspace:remove my-feature-lib --forceRemove", + "description": "Force removal of my-feature-lib from the workspace" + } + ], + "properties": { + "projectName": { + "type": "string", + "alias": "project", + "description": "The name of the project to remove", + "$default": { + "$source": "argv", + "index": 0 + } + }, + "forceRemove": { + "type": "boolean", + "aliases": ["force-remove"], + "description": "When true, forces removal even if the project is still in use.", + "default": false + } + }, + "required": ["projectName"] +} diff --git a/packages/workspace/src/utils/fileutils.ts b/packages/workspace/src/utils/fileutils.ts index 52d437a59c..a7a293444e 100644 --- a/packages/workspace/src/utils/fileutils.ts +++ b/packages/workspace/src/utils/fileutils.ts @@ -1,8 +1,7 @@ import * as fs from 'fs'; -import * as path from 'path'; import { ensureDirSync } from 'fs-extra'; +import * as path from 'path'; import * as stripJsonComments from 'strip-json-comments'; -import { appRootPath } from './app-root'; const ignore = require('ignore'); export function writeToFile(filePath: string, str: string) {