feat(core): migrate move to devkit (#4558)

This commit is contained in:
Jason Jean 2021-01-19 16:45:44 -05:00 committed by GitHub
parent 958c302c17
commit aeec4bd4d9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
71 changed files with 2050 additions and 1862 deletions

View File

@ -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`

View File

@ -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`

View File

@ -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`

View File

@ -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}`);

View File

@ -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';

View File

@ -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"

View File

@ -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<NxJsonConfiguration>(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<WorkspaceConfiguration, 'projects'> {
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<WorkspaceConfiguration, 'projects'>
) {
updateJson<WorkspaceConfiguration>(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<WorkspaceConfiguration>(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<NxJsonConfiguration>(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<WorkspaceConfiguration>(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<NxJsonConfiguration>(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<WorkspaceConfiguration>(host, path);
const originalVersion = workspaceJson.version;
return {
...toNewFormat(workspaceJson),
version: originalVersion,
};
}

View File

@ -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');
});
});

View File

@ -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);
}
}
}

View File

@ -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);
}

View File

@ -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,
});

View File

@ -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';

View File

@ -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;
}

View File

@ -27,6 +27,7 @@ describe('Jest Executor', () => {
root: '/root',
projectName: 'proj',
workspace: {
version: 2,
projects: {
proj: {
root: 'proj',

View File

@ -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', () => {

View File

@ -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);
}

View File

@ -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;
}

View File

@ -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"

View File

@ -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 = {

View File

@ -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 {};
"
`;

View File

@ -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');
});

View File

@ -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<Tree> => {
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));
}
}

View File

@ -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<NxJson>(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();
});
});

View File

@ -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
);
}

View File

@ -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');
});
});

View File

@ -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<Tree> => {
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);
}

View File

@ -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');
});
});

View File

@ -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;
});
}

View File

@ -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',

View File

@ -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<Tree> => {
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));
}

View File

@ -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');
});
});

View File

@ -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);
}
}

View File

@ -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',
})
);
});
});

View File

@ -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<Tree> => {
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<PartialEsLintRcJson>(tree, eslintRcPath, (eslintRcJson) => {
eslintRcJson.extends = offset + '.eslintrc.json';
eslintRcJson.extends = offset + '.eslintrc.json';
tree.overwrite(eslintRcPath, JSON.stringify(eslintRcJson));
return tree;
})
);
};
return eslintRcJson;
});
}

View File

@ -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']);
});
});

View File

@ -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<NxJson>('nx.json', (json) => {
export function updateImplicitDependencies(tree: Tree, schema: Schema) {
updateJson<NxJson>(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;
});
}

View File

@ -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,12 +24,12 @@ 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';
@ -40,11 +38,10 @@ describe('updateImports Rule', () => {
`
);
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,12 +50,12 @@ 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';
@ -69,15 +66,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 { Table } from '@proj/table';`
@ -86,15 +85,17 @@ 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';
@ -105,15 +106,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 { 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'],
// });
// });
});

View File

@ -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<Tree> => {
return from(getWorkspace(tree)).pipe(
map((workspace) => {
const nxJson = readJsonInTree<NxJson>(tree, 'nx.json');
const libsDir = nxJson.workspaceLayout?.libsDir
? nxJson.workspaceLayout.libsDir
: 'libs';
const project = workspace.projects.get(schema.projectName);
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 [];
}
}

View File

@ -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();

View File

@ -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<Tree> => {
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(
`<rootDir>\/(${libsDir}|${appsDir})\/${schema.projectName}`,
'g'
);
const { libsDir, appsDir } = getWorkspaceLayout(tree);
const findProject = new RegExp(
`<rootDir>\/(${libsDir}|${appsDir})\/${schema.projectName}`,
'g'
);
const oldRootJestConfigContent = tree
.read(rootJestConfigPath)
.toString('utf-8');
const newRootJestConfigContent = oldRootJestConfigContent.replace(
findProject,
`<rootDir>/${destination}`
);
const newRootJestConfigContent = oldRootJestConfigContent.replace(
findProject,
`<rootDir>/${destination}`
);
tree.overwrite(rootJestConfigPath, newRootJestConfigContent);
return tree;
})
);
};
tree.write(rootJestConfigPath, newRootJestConfigContent);
}

View File

@ -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: [],
});
});
});

View File

@ -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'`);

View File

@ -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<Tree> => {
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);
}
}

View File

@ -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)

View File

@ -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<Tree> => {
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);
}
}

View File

@ -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<NxJson>('nx.json', (json) => {
json.workspaceLayout = { appsDir: 'e2e', libsDir: 'packages' };
return json;
}),
tree
)) as UnitTestTree;
tree = (await callRule(updateWorkspace(schema), tree)) as UnitTestTree;
const workspace = JSON.parse(tree.read('workspace.json').toString());
const project = workspace.projects['subfolder-my-destination'];
expect(project).toBeDefined();
expect(project.root).toBe('e2e/subfolder/my-destination');
expect(project.sourceRoot).toBe('e2e/subfolder/my-destination/src');
});
});

View File

@ -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;
});
};
}

View File

@ -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<NxJson>(host, 'nx.json');
const workspaceLayout = nxJson.workspaceLayout
? nxJson.workspaceLayout
: { appsDir: 'apps', libsDir: 'libs' };
return workspaceLayout;
}
/**
* This helper function ensures that we don't move libs or apps
* outside of the folders they should be in.
@ -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);
}
/**

View File

@ -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);

View File

@ -3,4 +3,5 @@ export interface Schema {
destination: string;
importPath?: string;
updateImportPath: boolean;
skipFormat?: boolean;
}

View File

@ -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"]

View File

@ -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: [
'<rootDir>/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: [
'<rootDir>/apps/my-app/',
'<rootDir>/apps/my-other-app',
'<rootDir>/libs/my-other-lib/',
'<rootDir>/libs/my-other-lib/',
],
};
"

View File

@ -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();
});
});

View File

@ -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')}`
);
}

View File

@ -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();
});
});

View File

@ -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);
}
}

View File

@ -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');
});
});
});

View File

@ -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);
});
}

View File

@ -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');
});
});

View File

@ -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<Tree> => {
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);
}

View File

@ -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();
});

View File

@ -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<Tree> => {
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));
}

View File

@ -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',
]);
});
});

View File

@ -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<NxJson>('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;
});
}

View File

@ -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({});
});
});

View File

@ -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<Tree> => {
return from(getWorkspace(tree)).pipe(
map((workspace) => {
const nxJson = readJsonInTree<NxJson>(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;
});
}
}

View File

@ -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();
});
});
});

View File

@ -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;
}
);
}

View File

@ -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);

View File

@ -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",

View File

@ -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
);
}