feat(core): add remove schematic (#2484)
* feat(core): add remove schematic * fix(core): update remove schematic to use project graph
This commit is contained in:
parent
e73a6a0046
commit
d834e79dc4
61
docs/angular/api-workspace/schematics/remove.md
Normal file
61
docs/angular/api-workspace/schematics/remove.md
Normal file
@ -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
|
||||
61
docs/react/api-workspace/schematics/remove.md
Normal file
61
docs/react/api-workspace/schematics/remove.md
Normal file
@ -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
|
||||
61
docs/web/api-workspace/schematics/remove.md
Normal file
61
docs/web/api-workspace/schematics/remove.md
Normal file
@ -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
|
||||
44
e2e/remove.test.ts
Normal file
44
e2e/remove.test.ts
Normal file
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
11
e2e/utils.ts
11
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 {
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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 = [];
|
||||
|
||||
@ -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 }
|
||||
|
||||
@ -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),
|
||||
|
||||
@ -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();
|
||||
});
|
||||
});
|
||||
@ -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')}`
|
||||
);
|
||||
};
|
||||
}
|
||||
@ -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();
|
||||
});
|
||||
});
|
||||
@ -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;
|
||||
});
|
||||
}
|
||||
@ -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();
|
||||
});
|
||||
});
|
||||
@ -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<Tree> => {
|
||||
return from(getWorkspace(tree)).pipe(
|
||||
map(workspace => {
|
||||
const project = workspace.projects.get(schema.projectName);
|
||||
tree.delete(project.root);
|
||||
return tree;
|
||||
})
|
||||
);
|
||||
};
|
||||
}
|
||||
@ -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();
|
||||
});
|
||||
});
|
||||
@ -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<NxJson>('nx.json', json => {
|
||||
delete json.projects[schema.projectName];
|
||||
return json;
|
||||
});
|
||||
}
|
||||
@ -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({});
|
||||
});
|
||||
});
|
||||
@ -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<Tree> => {
|
||||
return from(getWorkspace(tree)).pipe(
|
||||
map(workspace => {
|
||||
const nxJson = readJsonInTree<NxJson>(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;
|
||||
})
|
||||
);
|
||||
};
|
||||
}
|
||||
@ -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();
|
||||
});
|
||||
});
|
||||
@ -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;
|
||||
});
|
||||
}
|
||||
19
packages/workspace/src/schematics/remove/remove.ts
Normal file
19
packages/workspace/src/schematics/remove/remove.ts
Normal file
@ -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)
|
||||
]);
|
||||
}
|
||||
4
packages/workspace/src/schematics/remove/schema.d.ts
vendored
Normal file
4
packages/workspace/src/schematics/remove/schema.d.ts
vendored
Normal file
@ -0,0 +1,4 @@
|
||||
export interface Schema extends json.JsonObject {
|
||||
projectName: string;
|
||||
forceRemove?: boolean;
|
||||
}
|
||||
35
packages/workspace/src/schematics/remove/schema.json
Normal file
35
packages/workspace/src/schematics/remove/schema.json
Normal file
@ -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"]
|
||||
}
|
||||
@ -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) {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user