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 The name of the project to move
### skipFormat
Alias(es): skip-format
Default: `false`
Type: `boolean`
Skip formatting files.
### updateImportPath ### updateImportPath
Default: `true` Default: `true`

View File

@ -56,6 +56,16 @@ Type: `string`
The name of the project to move The name of the project to move
### skipFormat
Alias(es): skip-format
Default: `false`
Type: `boolean`
Skip formatting files.
### updateImportPath ### updateImportPath
Default: `true` Default: `true`

View File

@ -56,6 +56,16 @@ Type: `string`
The name of the project to move The name of the project to move
### skipFormat
Alias(es): skip-format
Default: `false`
Type: `boolean`
Skip formatting files.
### updateImportPath ### updateImportPath
Default: `true` Default: `true`

View File

@ -750,9 +750,7 @@ describe('Move Project', () => {
const jestConfig = readFile(jestConfigPath); const jestConfig = readFile(jestConfigPath);
expect(jestConfig).toContain(`displayName: 'shared-${lib1}-data-access'`); expect(jestConfig).toContain(`displayName: 'shared-${lib1}-data-access'`);
expect(jestConfig).toContain(`preset: '../../../../jest.preset.js'`); expect(jestConfig).toContain(`preset: '../../../../jest.preset.js'`);
expect(jestConfig).toContain( expect(jestConfig).toContain(`'../../../../coverage/${newPath}'`);
`coverageDirectory: '../../../../coverage/${newPath}'`
);
const tsConfigPath = `${newPath}/tsconfig.json`; const tsConfigPath = `${newPath}/tsconfig.json`;
expect(moveOutput).toContain(`CREATE ${tsConfigPath}`); expect(moveOutput).toContain(`CREATE ${tsConfigPath}`);
@ -888,9 +886,7 @@ describe('Move Project', () => {
const jestConfig = readFile(jestConfigPath); const jestConfig = readFile(jestConfigPath);
expect(jestConfig).toContain(`displayName: 'shared-${lib1}-data-access'`); expect(jestConfig).toContain(`displayName: 'shared-${lib1}-data-access'`);
expect(jestConfig).toContain(`preset: '../../../../jest.preset.js'`); expect(jestConfig).toContain(`preset: '../../../../jest.preset.js'`);
expect(jestConfig).toContain( expect(jestConfig).toContain(`'../../../../coverage/${newPath}'`);
`coverageDirectory: '../../../../coverage/${newPath}'`
);
const tsConfigPath = `${newPath}/tsconfig.json`; const tsConfigPath = `${newPath}/tsconfig.json`;
expect(moveOutput).toContain(`CREATE ${tsConfigPath}`); expect(moveOutput).toContain(`CREATE ${tsConfigPath}`);
@ -1029,9 +1025,7 @@ describe('Move Project', () => {
const jestConfig = readFile(jestConfigPath); const jestConfig = readFile(jestConfigPath);
expect(jestConfig).toContain(`displayName: 'shared-${lib1}-data-access'`); expect(jestConfig).toContain(`displayName: 'shared-${lib1}-data-access'`);
expect(jestConfig).toContain(`preset: '../../../../jest.preset.js'`); expect(jestConfig).toContain(`preset: '../../../../jest.preset.js'`);
expect(jestConfig).toContain( expect(jestConfig).toContain(`'../../../../coverage/${newPath}'`);
`coverageDirectory: '../../../../coverage/${newPath}'`
);
const tsConfigPath = `${newPath}/tsconfig.json`; const tsConfigPath = `${newPath}/tsconfig.json`;
expect(moveOutput).toContain(`CREATE ${tsConfigPath}`); expect(moveOutput).toContain(`CREATE ${tsConfigPath}`);

View File

@ -21,9 +21,14 @@ export { generateFiles } from './src/generators/generate-files';
export { export {
addProjectConfiguration, addProjectConfiguration,
readProjectConfiguration, readProjectConfiguration,
removeProjectConfiguration,
updateProjectConfiguration, updateProjectConfiguration,
readWorkspaceConfiguration,
updateWorkspaceConfiguration,
getProjects,
} from './src/generators/project-configuration'; } from './src/generators/project-configuration';
export { toJS } from './src/generators/to-js'; 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 { readJson, writeJson, updateJson } from './src/utils/json';
export { addDependenciesToPackageJson } from './src/utils/package-json'; export { addDependenciesToPackageJson } from './src/utils/package-json';

View File

@ -28,6 +28,7 @@
"dependencies": { "dependencies": {
"@nrwl/tao": "*", "@nrwl/tao": "*",
"ejs": "^3.1.5", "ejs": "^3.1.5",
"ignore": "^5.0.4",
"semver": "6.3.0", "semver": "6.3.0",
"strip-json-comments": "2.0.1", "strip-json-comments": "2.0.1",
"tslib": "^2.0.0" "tslib": "^2.0.0"

View File

@ -4,7 +4,7 @@ import {
toNewFormat, toNewFormat,
WorkspaceConfiguration, WorkspaceConfiguration,
} from '@nrwl/tao/src/shared/workspace'; } from '@nrwl/tao/src/shared/workspace';
import { readJson } from '../utils/json'; import { readJson, updateJson } from '../utils/json';
import { import {
NxJsonConfiguration, NxJsonConfiguration,
NxJsonProjectConfiguration, NxJsonProjectConfiguration,
@ -47,6 +47,62 @@ export function updateProjectConfiguration(
setProjectConfiguration(host, projectName, projectConfiguration, 'update'); 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. * Reads a project configuration.
* *
@ -56,36 +112,45 @@ export function updateProjectConfiguration(
* @param host - the file system tree * @param host - the file system tree
* @param projectName - unique name. Often directories are part of the name (e.g., mydir-mylib) * @param projectName - unique name. Often directories are part of the name (e.g., mydir-mylib)
*/ */
export function readProjectConfiguration( export function readProjectConfiguration(host: Tree, projectName: string) {
host: Tree, const workspace = readWorkspace(host);
projectName: string if (!workspace.projects[projectName]) {
): 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]) {
throw new Error( 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'); const nxJson = readJson<NxJsonConfiguration>(host, 'nx.json');
if (!nxJson.projects[projectName]) { if (!nxJson.projects[projectName]) {
throw new Error( throw new Error(
`Cannot find configuration for '${projectName}' in nx.json` `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]; return nxJson.projects[projectName];
} }
@ -93,25 +158,42 @@ function setProjectConfiguration(
host: Tree, host: Tree,
projectName: string, projectName: string,
projectConfiguration: ProjectConfiguration & NxJsonProjectConfiguration, 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 { const {
tags, tags,
implicitDependencies, implicitDependencies,
...workspaceConfiguration ...workspaceConfiguration
} = projectConfiguration; } = projectConfiguration;
addProjectToWorkspaceJson(host, projectName, workspaceConfiguration, mode); addProjectToWorkspaceJson(host, projectName, workspaceConfiguration, mode);
addProjectToNxJson(host, projectName, { addProjectToNxJson(
host,
projectName,
{
tags, tags,
implicitDependencies, implicitDependencies,
}); },
mode
);
} }
function addProjectToWorkspaceJson( function addProjectToWorkspaceJson(
host: Tree, host: Tree,
projectName: string, projectName: string,
project: ProjectConfiguration, project: ProjectConfiguration,
mode: 'create' | 'update' mode: 'create' | 'update' | 'delete'
) { ) {
const path = getWorkspacePath(host); const path = getWorkspacePath(host);
const workspaceJson = readJson<WorkspaceConfiguration>(host, path); const workspaceJson = readJson<WorkspaceConfiguration>(host, path);
@ -125,6 +207,11 @@ function addProjectToWorkspaceJson(
`Cannot update Project '${projectName}'. It does not exist.` `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; workspaceJson.projects[projectName] = project;
host.write(path, JSON.stringify(workspaceJson)); host.write(path, JSON.stringify(workspaceJson));
} }
@ -132,14 +219,29 @@ function addProjectToWorkspaceJson(
function addProjectToNxJson( function addProjectToNxJson(
host: Tree, host: Tree,
projectName: string, projectName: string,
config: NxJsonProjectConfiguration config: NxJsonProjectConfiguration,
mode: 'create' | 'update' | 'delete'
) { ) {
const nxJson = readJson<NxJsonConfiguration>(host, 'nx.json'); const nxJson = readJson<NxJsonConfiguration>(host, 'nx.json');
if (mode === 'delete') {
delete nxJson.projects[projectName];
} else {
nxJson.projects[projectName] = { nxJson.projects[projectName] = {
...{ ...{
tags: [], tags: [],
}, },
...(config || {}), ...(config || {}),
}; };
}
host.write('nx.json', JSON.stringify(nxJson)); 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. * Creates a host for testing.
*/ */
export function createTree() { export function createTree() {
return new FsTree('/', false); return new FsTree('/virtual', false);
} }

View File

@ -77,10 +77,7 @@ class DevkitTreeFromAngularDevkitTree {
children(dirPath: string): string[] { children(dirPath: string): string[] {
const { subdirs, subfiles } = this.tree.getDir(dirPath); const { subdirs, subfiles } = this.tree.getDir(dirPath);
return [ return [...subdirs, ...subfiles];
...subdirs.map((fragment) => join(this.root, fragment)),
...subfiles.map((fragment) => join(this.root, fragment)),
];
} }
delete(filePath: string): void { delete(filePath: string): void {
@ -100,12 +97,12 @@ class DevkitTreeFromAngularDevkitTree {
for (const action of this.tree.actions) { for (const action of this.tree.actions) {
if (action.kind === 'r') { if (action.kind === 'r') {
fileChanges.push({ fileChanges.push({
path: this.normalize(action.path), path: this.normalize(action.to),
type: 'CREATE', type: 'CREATE',
content: this.read(action.path), content: this.read(action.to),
}); });
fileChanges.push({ fileChanges.push({
path: this.normalize(action.to), path: this.normalize(action.path),
type: 'DELETE', type: 'DELETE',
content: null, content: null,
}); });

View File

@ -134,6 +134,35 @@ describe('applyChangesToString', () => {
expect(result).toEqual('Updated Text'); 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', () => { it('should sort changes when replacing text', () => {
const original = 'Original Text'; const original = 'Original Text';

View File

@ -1,5 +1,5 @@
export enum ChangeType { export enum ChangeType {
Delete = 'DELETE', Delete = 'Delete',
Insert = 'Insert', Insert = 'Insert',
} }
@ -68,22 +68,29 @@ export function applyChangesToString(
changes: StringChange[] changes: StringChange[]
): string { ): string {
assertChangesValid(changes); assertChangesValid(changes);
const sortedChanges = changes.sort( const sortedChanges = changes.sort((a, b) => {
(a, b) => getChangeIndex(a) - getChangeIndex(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; let offset = 0;
for (const change of sortedChanges) { for (const change of sortedChanges) {
const index = getChangeIndex(change) + offset;
switch (change.type) { switch (change.type) {
case ChangeType.Insert: { case ChangeType.Insert: {
const index = change.index + Math.max(offset, 0);
text = text.substr(0, index) + change.text + text.substr(index); text = text.substr(0, index) + change.text + text.substr(index);
offset += change.text.length; offset += change.text.length;
break; break;
} }
case ChangeType.Delete: { case ChangeType.Delete: {
text = text = text.substr(0, index) + text.substr(index + change.length);
text.substr(0, change.start + offset) +
text.substr(change.start + change.length + offset);
offset -= change.length; offset -= change.length;
break; break;
} }

View File

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

View File

@ -216,27 +216,79 @@ describe('tree', () => {
} catch (e) {} } catch (e) {}
}); });
describe('children', () => {
it('should return the list of children of a dir', () => { 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']);
});
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.write('parent/new-child/new-child-file.txt', 'new child content');
expect(tree.children('parent/child')).toEqual(['child-file.txt']); expect(tree.children('parent')).toEqual([
expect(tree.children('parent/new-child')).toEqual(['new-child-file.txt']); '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',
]);
});
it('should return the list of children after renaming', () => {
tree.rename( tree.rename(
'parent/child/child-file.txt', 'parent/child/child-file.txt',
'parent/child/renamed-child-file.txt' 'parent/child/renamed-child-file.txt'
); );
expect(tree.children('parent/child')).toEqual([
'renamed-child-file.txt',
]);
tree.rename( tree.rename(
'parent/new-child/new-child-file.txt', 'parent/child/renamed-child-file.txt',
'parent/new-child/renamed-new-child-file.txt' 'parent/renamed-child/renamed-child-file.txt'
); );
expect(tree.children('parent/child')).toEqual(['renamed-child-file.txt']); expect(tree.children('parent')).toEqual([
expect(tree.children('parent/new-child')).toEqual([ 'parent-file.txt',
'renamed-new-child-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', () => { it('should be able to rename dirs', () => {
// not supported yet // not supported yet
}); });

View File

@ -122,6 +122,27 @@ export class FsTree implements Tree {
this.write(filePath, content); 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 { exists(filePath: string): boolean {
filePath = this.normalize(filePath); filePath = this.normalize(filePath);
try { 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 { rename(from: string, to: string): void {
from = this.normalize(from); from = this.normalize(from);
to = this.normalize(to); to = this.normalize(to);
const content = this.read(this.rp(from)); const content = this.read(this.rp(from));
this.recordedChanges[this.rp(from)] = { content: null, isDeleted: true }; this.delete(this.rp(from));
this.recordedChanges[this.rp(to)] = { content: content, isDeleted: false }; this.write(this.rp(to), content);
} }
isFile(filePath: string): boolean { isFile(filePath: string): boolean {
@ -176,11 +184,12 @@ export class FsTree implements Tree {
let res = this.fsReadDir(dirPath); let res = this.fsReadDir(dirPath);
res = [...res, ...this.directChildrenOfDir(this.rp(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)]; const r = this.recordedChanges[join(this.rp(dirPath), q)];
if (r && r.isDeleted) return false; return !r?.isDeleted;
return true;
}); });
// Dedupe
return Array.from(new Set(res));
} }
listChanges(): FileChange[] { listChanges(): FileChange[] {
@ -255,12 +264,18 @@ export class FsTree implements Tree {
private directChildrenOfDir(path: string): string[] { private directChildrenOfDir(path: string): string[] {
const res = {}; const res = {};
if (path === '') {
return Object.keys(this.recordedChanges).map(
(file) => file.split('/')[0]
);
}
Object.keys(this.recordedChanges).forEach((f) => { Object.keys(this.recordedChanges).forEach((f) => {
if (f.startsWith(path + '/')) { if (f.startsWith(path + '/')) {
const [_, file] = f.split(path + '/'); const [_, file] = f.split(path + '/');
res[file.split('/')[0]] = true; res[file.split('/')[0]] = true;
} }
}); });
return Object.keys(res); return Object.keys(res);
} }

View File

@ -7,6 +7,10 @@ import '../compat/compat';
* Workspace configuration * Workspace configuration
*/ */
export interface WorkspaceConfiguration { export interface WorkspaceConfiguration {
/**
* Version of the configuration format
*/
version: number;
/** /**
* Projects' configurations * Projects' configurations
*/ */
@ -386,7 +390,7 @@ export function reformattedWorkspaceJsonOrNull(w: any) {
return w.version === 2 ? toNewFormatOrNull(w) : toOldFormatOrNull(w); return w.version === 2 ? toNewFormatOrNull(w) : toOldFormatOrNull(w);
} }
export function toNewFormat(w: any) { export function toNewFormat(w: any): WorkspaceConfiguration {
const f = toNewFormatOrNull(w); const f = toNewFormatOrNull(w);
return f ? f : w; return f ? f : w;
} }

View File

@ -24,14 +24,14 @@
}, },
"move": { "move": {
"factory": "./src/schematics/move/move", "factory": "./src/schematics/move/move#moveSchematic",
"schema": "./src/schematics/move/schema.json", "schema": "./src/schematics/move/schema.json",
"aliases": ["mv"], "aliases": ["mv"],
"description": "Move an application or library to another folder" "description": "Move an application or library to another folder"
}, },
"remove": { "remove": {
"factory": "./src/schematics/remove/remove", "factory": "./src/schematics/remove/remove#moveSchematic",
"schema": "./src/schematics/remove/schema.json", "schema": "./src/schematics/remove/schema.json",
"aliases": ["rm"], "aliases": ["rm"],
"description": "Remove an application or library" "description": "Remove an application or library"
@ -88,14 +88,14 @@
}, },
"move": { "move": {
"factory": "./src/schematics/move/move", "factory": "./src/schematics/move/move#moveGenerator",
"schema": "./src/schematics/move/schema.json", "schema": "./src/schematics/move/schema.json",
"aliases": ["mv"], "aliases": ["mv"],
"description": "Move an application or library to another folder" "description": "Move an application or library to another folder"
}, },
"remove": { "remove": {
"factory": "./src/schematics/remove/remove", "factory": "./src/schematics/remove/remove#removeGenerator",
"schema": "./src/schematics/remove/schema.json", "schema": "./src/schematics/remove/schema.json",
"aliases": ["rm"], "aliases": ["rm"],
"description": "Remove an application or library" "description": "Remove an application or library"

View File

@ -9,12 +9,9 @@ import {
onlyWorkspaceProjects, onlyWorkspaceProjects,
ProjectGraph, ProjectGraph,
ProjectGraphNode, ProjectGraphNode,
ProjectGraphDependency,
} from '../core/project-graph'; } from '../core/project-graph';
import { appRootPath } from '../utils/app-root'; import { appRootPath } from '../utils/app-root';
import { output } from '../utils/output'; 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 // maps file extention to MIME types
const mimeType = { 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 {
import { createEmptyWorkspace } from '@nrwl/workspace/testing'; ProjectConfiguration,
import { callRule, runSchematic } from '../../../utils/testing'; readProjectConfiguration,
Tree,
} from '@nrwl/devkit';
import { createTreeWithEmptyWorkspace } from '@nrwl/devkit/testing';
import { Schema } from '../schema'; import { Schema } from '../schema';
import { checkDestination } from './check-destination'; import { checkDestination } from './check-destination';
import { libraryGenerator } from '../../library/library';
describe('checkDestination Rule', () => { describe('checkDestination', () => {
let tree: Tree; let tree: Tree;
let projectConfig: ProjectConfiguration;
beforeEach(async () => { beforeEach(async () => {
tree = createEmptyWorkspace(Tree.empty()); tree = createTreeWithEmptyWorkspace();
tree = await runSchematic('lib', { name: 'my-lib' }, tree); await libraryGenerator(tree, { name: 'my-lib' });
projectConfig = readProjectConfiguration(tree, 'my-lib');
}); });
it('should throw an error if the path is not explicit', async () => { it('should throw an error if the path is not explicit', async () => {
@ -20,13 +26,15 @@ describe('checkDestination Rule', () => {
updateImportPath: true, updateImportPath: true,
}; };
await expect(callRule(checkDestination(schema), tree)).rejects.toThrow( expect(() => {
checkDestination(tree, schema, projectConfig);
}).toThrow(
`Invalid destination: [${schema.destination}] - Please specify explicit path.` `Invalid destination: [${schema.destination}] - Please specify explicit path.`
); );
}); });
it('should throw an error if the path already exists', async () => { 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 = { const schema: Schema = {
projectName: 'my-lib', projectName: 'my-lib',
@ -35,7 +43,9 @@ describe('checkDestination Rule', () => {
updateImportPath: true, updateImportPath: true,
}; };
await expect(callRule(checkDestination(schema), tree)).rejects.toThrow( expect(() => {
checkDestination(tree, schema, projectConfig);
}).toThrow(
`Invalid destination: [${schema.destination}] - Path is not empty.` `Invalid destination: [${schema.destination}] - Path is not empty.`
); );
}); });
@ -48,9 +58,9 @@ describe('checkDestination Rule', () => {
updateImportPath: true, updateImportPath: true,
}; };
await expect( expect(() => {
callRule(checkDestination(schema), tree) checkDestination(tree, schema, projectConfig);
).resolves.not.toThrow(); }).not.toThrow();
}); });
it('should normalize the destination', async () => { it('should normalize the destination', async () => {
@ -61,7 +71,7 @@ describe('checkDestination Rule', () => {
updateImportPath: true, updateImportPath: true,
}; };
await callRule(checkDestination(schema), tree); checkDestination(tree, schema, projectConfig);
expect(schema.destination).toBe('my-other-lib/wibble'); expect(schema.destination).toBe('my-other-lib/wibble');
}); });

View File

@ -1,8 +1,4 @@
import { Rule, SchematicContext } from '@angular-devkit/schematics'; import { ProjectConfiguration, Tree } from '@nrwl/devkit';
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 { Schema } from '../schema'; import { Schema } from '../schema';
import { getDestination, normalizeSlashes } from './utils'; import { getDestination, normalizeSlashes } from './utils';
@ -14,30 +10,24 @@ import { getDestination, normalizeSlashes } from './utils';
* *
* @param schema The options provided to the schematic * @param schema The options provided to the schematic
*/ */
export function checkDestination(schema: Schema): Rule { export function checkDestination(
return (tree: Tree, _context: SchematicContext): Observable<Tree> => { tree: Tree,
return from(getWorkspace(tree)).pipe( schema: Schema,
map((workspace) => { projectConfig: ProjectConfiguration
) {
const INVALID_DESTINATION = `Invalid destination: [${schema.destination}]`; const INVALID_DESTINATION = `Invalid destination: [${schema.destination}]`;
if (schema.destination.includes('..')) { if (schema.destination.includes('..')) {
throw new Error( throw new Error(`${INVALID_DESTINATION} - Please specify explicit path.`);
`${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) { if (tree.children(destination).length > 0) {
throw new Error(`${INVALID_DESTINATION} - Path is not empty.`); throw new Error(`${INVALID_DESTINATION} - Path is not empty.`);
} }
if (schema.destination.startsWith('/')) { if (schema.destination.startsWith('/')) {
schema.destination = normalizeSlashes(schema.destination.substr(1)); schema.destination = normalizeSlashes(schema.destination.substr(1));
} }
return tree;
})
);
};
} }

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 {
import { createEmptyWorkspace } from '@nrwl/workspace/testing'; ProjectConfiguration,
import { runSchematic } from '../../../utils/testing'; readProjectConfiguration,
Tree,
} from '@nrwl/devkit';
import { createTreeWithEmptyWorkspace } from '@nrwl/devkit/testing';
import { Schema } from '../schema'; 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 tree: Tree;
let projectConfig: ProjectConfiguration;
beforeEach(async () => { beforeEach(async () => {
tree = createEmptyWorkspace(Tree.empty()); tree = createTreeWithEmptyWorkspace();
tree = await runSchematic('lib', { name: 'my-lib' }, tree); await libraryGenerator(tree, { name: 'my-lib' });
projectConfig = readProjectConfiguration(tree, 'my-lib');
}); });
it('should copy all files and delete the source folder', async () => { it('should copy all files and delete the source folder', async () => {
@ -19,22 +26,12 @@ describe('moveProject Rule', () => {
updateImportPath: true, updateImportPath: true,
}; };
// TODO - Currently this test will fail due to moveProject(tree, schema, projectConfig);
// https://github.com/angular/angular-cli/issues/16527
// host = await callRule(moveProject(schema), host);
// const destinationDir = host.getDir('libs/my-destination'); const destinationChildren = tree.children('libs/my-destination');
// let filesFound = false; expect(destinationChildren.length).toBeGreaterThan(0);
// destinationDir.visit(_file => {
// filesFound = true;
// });
// expect(filesFound).toBeTruthy();
// const sourceDir = host.getDir('libs/my-lib'); expect(tree.exists('libs/my-lib')).toBeFalsy();
// filesFound = false; expect(tree.children('libs')).not.toContain('my-lib');
// sourceDir.visit(_file => {
// filesFound = true;
// });
// expect(filesFound).toBeFalsy();
}); });
}); });

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 { getWorkspace } from '@nrwl/workspace';
import { from, Observable } from 'rxjs'; import { from, Observable } from 'rxjs';
import { map } from 'rxjs/operators'; import { map } from 'rxjs/operators';
@ -10,23 +11,18 @@ import { getDestination } from './utils';
* *
* @param schema The options provided to the schematic * @param schema The options provided to the schematic
*/ */
export function moveProject(schema: Schema) { export function moveProject(
return (tree: Tree, _context: SchematicContext): Observable<Tree> => { tree: Tree,
return from(getWorkspace(tree)).pipe( schema: Schema,
map((workspace) => { project: ProjectConfiguration
const project = workspace.projects.get(schema.projectName); ) {
const destination = getDestination(tree, schema, project);
const destination = getDestination(schema, workspace, tree); visitNotIgnoredFiles(tree, project.root, (file) => {
const dir = tree.getDir(project.root); // This is a rename but Angular Devkit isn't capable of writing to a file after it's renamed so this is a workaround
dir.visit((file) => { const content = tree.read(file);
const newPath = file.replace(project.root, destination); tree.write(file.replace(project.root, destination), content);
tree.create(newPath, tree.read(file)); tree.delete(file);
}); });
tree.delete(project.root); tree.delete(project.root);
return tree;
})
);
};
} }

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 {
import { UnitTestTree } from '@angular-devkit/schematics/testing'; ProjectConfiguration,
import { readJsonInTree } from '@nrwl/workspace'; readProjectConfiguration,
import { createEmptyWorkspace } from '@nrwl/workspace/testing'; Tree,
import { callRule, runSchematic } from '../../../utils/testing'; readJson,
writeJson,
} from '@nrwl/devkit';
import { createTreeWithEmptyWorkspace } from '@nrwl/devkit/testing';
import { Schema } from '../schema'; import { Schema } from '../schema';
import { updateCypressJson } from './update-cypress-json'; import { updateCypressJson } from './update-cypress-json';
import { libraryGenerator } from '../../library/library';
describe('updateCypressJson Rule', () => { describe('updateCypressJson', () => {
let tree: UnitTestTree; let tree: Tree;
let schema: Schema;
let projectConfig: ProjectConfiguration;
beforeEach(async () => { beforeEach(async () => {
tree = new UnitTestTree(Tree.empty()); schema = {
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 = {
projectName: 'my-lib', projectName: 'my-lib',
destination: 'my-destination', destination: 'my-destination',
importPath: undefined, importPath: undefined,
updateImportPath: true, updateImportPath: true,
}; };
await expect( tree = createTreeWithEmptyWorkspace();
callRule(updateCypressJson(schema), tree) await libraryGenerator(tree, { name: 'my-lib' });
).resolves.not.toThrow(); 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 () => { it('should update the videos and screenshots folders', async () => {
@ -44,24 +47,11 @@ describe('updateCypressJson Rule', () => {
chromeWebSecurity: false, chromeWebSecurity: false,
}; };
tree = await runSchematic('lib', { name: 'my-lib' }, tree); writeJson(tree, '/libs/my-destination/cypress.json', cypressJson);
tree.create(
'/libs/my-destination/cypress.json',
JSON.stringify(cypressJson)
);
expect(tree.files).toContain('/libs/my-destination/cypress.json'); updateCypressJson(tree, schema, projectConfig);
const schema: Schema = { expect(readJson(tree, '/libs/my-destination/cypress.json')).toEqual({
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({
...cypressJson, ...cypressJson,
videosFolder: '../../dist/cypress/libs/my-destination/videos', videosFolder: '../../dist/cypress/libs/my-destination/videos',
screenshotsFolder: '../../dist/cypress/libs/my-destination/screenshots', screenshotsFolder: '../../dist/cypress/libs/my-destination/screenshots',

View File

@ -1,11 +1,8 @@
import { Rule, SchematicContext } from '@angular-devkit/schematics'; import { Tree } from '@nrwl/devkit';
import { Tree } from '@angular-devkit/schematics/src/tree/interface';
import { getWorkspace } from '@nrwl/workspace';
import * as path from 'path'; import * as path from 'path';
import { from, Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import { Schema } from '../schema'; import { Schema } from '../schema';
import { getDestination } from './utils'; import { getDestination } from './utils';
import { ProjectConfiguration } from '@nrwl/tao/src/shared/workspace';
interface PartialCypressJson { interface PartialCypressJson {
videosFolder: string; videosFolder: string;
@ -19,12 +16,12 @@ interface PartialCypressJson {
* *
* @param schema The options provided to the schematic * @param schema The options provided to the schematic
*/ */
export function updateCypressJson(schema: Schema): Rule { export function updateCypressJson(
return (tree: Tree, _context: SchematicContext): Observable<Tree> => { tree: Tree,
return from(getWorkspace(tree)).pipe( schema: Schema,
map((workspace) => { project: ProjectConfiguration
const project = workspace.projects.get(schema.projectName); ) {
const destination = getDestination(schema, workspace, tree); const destination = getDestination(tree, schema, project);
const cypressJsonPath = path.join(destination, 'cypress.json'); const cypressJsonPath = path.join(destination, 'cypress.json');
@ -45,10 +42,5 @@ export function updateCypressJson(schema: Schema): Rule {
destination destination
); );
tree.overwrite(cypressJsonPath, JSON.stringify(cypressJson)); tree.write(cypressJsonPath, JSON.stringify(cypressJson));
return tree;
})
);
};
} }

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 { readJson, readProjectConfiguration, Tree } from '@nrwl/devkit';
import { Tree } from '@angular-devkit/schematics'; import { createTreeWithEmptyWorkspace } from '@nrwl/devkit/testing';
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';
describe('updateEslint Rule', () => { import { Linter } from '@nrwl/workspace';
let tree: UnitTestTree;
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 () => { beforeEach(async () => {
tree = new UnitTestTree(Tree.empty()); schema = {
tree = createEmptyWorkspace(tree) as UnitTestTree; projectName: 'my-lib',
destination: 'shared/my-destination',
importPath: undefined,
updateImportPath: true,
};
tree = createTreeWithEmptyWorkspace();
}); });
it('should handle .eslintrc.json not existing', async () => { it('should handle .eslintrc.json not existing', async () => {
tree = await runSchematic( await libraryGenerator(tree, {
'lib', name: 'my-lib',
{ name: 'my-lib', linter: Linter.TsLint }, linter: Linter.TsLint,
tree });
);
expect(tree.files).not.toContain('/libs/my-destination/.estlintrc.json'); const projectConfig = readProjectConfiguration(tree, 'my-lib');
const schema: Schema = { expect(() => {
projectName: 'my-lib', updateEslintrcJson(tree, schema, projectConfig);
destination: 'my-destination', }).not.toThrow();
importPath: undefined,
updateImportPath: true,
};
await expect(
callRule(updateEslintrcJson(schema), tree)
).resolves.not.toThrow();
}); });
it('should update .eslintrc.json extends path when project is moved to subdirectory', async () => { it('should update .eslintrc.json extends path when project is moved to subdirectory', async () => {
const eslintRc = { await libraryGenerator(tree, {
extends: '../../.eslintrc.json', name: 'my-lib',
rules: {}, linter: Linter.EsLint,
ignorePatterns: ['!**/*'], });
};
tree = await runSchematic( // This step is usually handled elsewhere
'lib', tree.rename(
{ name: 'my-lib', linter: Linter.EsLint }, 'libs/my-lib/.eslintrc.json',
tree '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 = { expect(
projectName: 'my-lib', readJson(tree, '/libs/shared/my-destination/.eslintrc.json')
destination: 'core/my-lib', ).toEqual(
importPath: undefined,
updateImportPath: true,
};
tree = (await callRule(updateEslintrcJson(schema), tree)) as UnitTestTree;
expect(readJsonInTree(tree, '/libs/core/my-lib/.eslintrc.json')).toEqual(
jasmine.objectContaining({ jasmine.objectContaining({
extends: '../../../.eslintrc.json', 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 { join } from 'path';
import { offsetFromRoot } from '@nrwl/devkit'; import {
import { getDestination } from '@nrwl/workspace/src/schematics/move/lib/utils'; offsetFromRoot,
ProjectConfiguration,
readJson,
Tree,
updateJson,
} from '@nrwl/devkit';
import { Schema } from '../schema';
import { getDestination } from './utils';
interface PartialEsLintRcJson { interface PartialEsLintRcJson {
extends: string; extends: string;
@ -16,30 +19,24 @@ interface PartialEsLintRcJson {
* *
* @param schema The options provided to the schematic * @param schema The options provided to the schematic
*/ */
export function updateEslintrcJson(schema: Schema): Rule { export function updateEslintrcJson(
return (tree: Tree, _context: SchematicContext): Observable<Tree> => { tree: Tree,
return from(getWorkspace(tree)).pipe( schema: Schema,
map((workspace) => { project: ProjectConfiguration
const destination = getDestination(schema, workspace, tree); ) {
const destination = getDestination(tree, schema, project);
const eslintRcPath = join(destination, '.eslintrc.json'); const eslintRcPath = join(destination, '.eslintrc.json');
if (!tree.exists(eslintRcPath)) { if (!tree.exists(eslintRcPath)) {
// no .eslintrc found. nothing to do // no .eslintrc found. nothing to do
return tree; 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 eslintRcJson;
});
return tree;
})
);
};
} }

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 { Schema } from '../schema';
import { getNewProjectName } from './utils'; import { getNewProjectName } from './utils';
@ -7,8 +9,8 @@ import { getNewProjectName } from './utils';
* *
* @param schema The options provided to the schematic * @param schema The options provided to the schematic
*/ */
export function updateNxJson(schema: Schema) { export function updateImplicitDependencies(tree: Tree, schema: Schema) {
return updateJsonInTree<NxJson>('nx.json', (json) => { updateJson<NxJson>(tree, 'nx.json', (json) => {
Object.values(json.projects).forEach((project) => { Object.values(json.projects).forEach((project) => {
if (project.implicitDependencies) { if (project.implicitDependencies) {
const index = project.implicitDependencies.indexOf(schema.projectName); 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; return json;
}); });
} }

View File

@ -1,18 +1,16 @@
import { Tree } from '@angular-devkit/schematics'; import { readProjectConfiguration, Tree } from '@nrwl/devkit';
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 { callRule, runSchematic } from '../../../utils/testing';
import { Schema } from '../schema'; import { Schema } from '../schema';
import { updateImports } from './update-imports'; import { updateImports } from './update-imports';
import { libraryGenerator } from '../../library/library';
import { createTreeWithEmptyWorkspace } from '@nrwl/devkit/testing';
describe('updateImports Rule', () => { describe('updateImports', () => {
let tree: UnitTestTree; let tree: Tree;
let schema: Schema; let schema: Schema;
beforeEach(async () => { beforeEach(async () => {
tree = new UnitTestTree(Tree.empty()); tree = createTreeWithEmptyWorkspace();
tree = createEmptyWorkspace(tree) as UnitTestTree;
schema = { schema = {
projectName: 'my-source', 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 // 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 // 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. // source and destination to make sure that the workspace has libraries with those names.
tree = await runSchematic('lib', { name: 'my-destination' }, tree); await libraryGenerator(tree, { name: 'my-destination' });
tree = await runSchematic('lib', { name: 'my-source' }, tree); 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'; const importerFilePath = 'libs/my-importer/src/importer.ts';
tree.create( tree.write(
importerFilePath, importerFilePath,
` `
import { MyClass } from '@proj/my-source'; 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( expect(tree.read(importerFilePath).toString()).toMatchSnapshot();
`import { MyClass } from '@proj/my-destination';`
);
}); });
/** /**
@ -53,12 +50,12 @@ describe('updateImports Rule', () => {
* be updated. * be updated.
*/ */
it('should not update import paths when they contain a partial match', async () => { it('should not update import paths when they contain a partial match', async () => {
tree = await runSchematic('lib', { name: 'table' }, tree); await libraryGenerator(tree, { name: 'table' });
tree = await runSchematic('lib', { name: 'tab' }, tree); 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'; const importerFilePath = 'libs/my-importer/src/importer.ts';
tree.create( tree.write(
importerFilePath, importerFilePath,
` `
import { Table } from '@proj/table'; import { Table } from '@proj/table';
@ -69,15 +66,17 @@ describe('updateImports Rule', () => {
` `
); );
tree = (await callRule( const projectConfig = readProjectConfiguration(tree, 'tab');
updateImports({ updateImports(
tree,
{
projectName: 'tab', projectName: 'tab',
destination: 'tabs', destination: 'tabs',
importPath: undefined, importPath: undefined,
updateImportPath: true, updateImportPath: true,
}), },
tree projectConfig
)) as UnitTestTree; );
expect(tree.read(importerFilePath).toString()).toContain( expect(tree.read(importerFilePath).toString()).toContain(
`import { Table } from '@proj/table';` `import { Table } from '@proj/table';`
@ -86,15 +85,17 @@ describe('updateImports Rule', () => {
expect(tree.read(importerFilePath).toString()).toContain( expect(tree.read(importerFilePath).toString()).toContain(
`import { Tab } from '@proj/tabs';` `import { Tab } from '@proj/tabs';`
); );
expect(tree.read(importerFilePath).toString()).toMatchSnapshot();
}); });
it('should correctly update deep imports', async () => { it('should correctly update deep imports', async () => {
tree = await runSchematic('lib', { name: 'table' }, tree); await libraryGenerator(tree, { name: 'table' });
tree = await runSchematic('lib', { name: 'tab' }, tree); 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'; const importerFilePath = 'libs/my-importer/src/importer.ts';
tree.create( tree.write(
importerFilePath, importerFilePath,
` `
import { Table } from '@proj/table/components'; import { Table } from '@proj/table/components';
@ -105,15 +106,17 @@ describe('updateImports Rule', () => {
` `
); );
tree = (await callRule( const projectConfig = readProjectConfiguration(tree, 'tab');
updateImports({ updateImports(
tree,
{
projectName: 'tab', projectName: 'tab',
destination: 'tabs', destination: 'tabs',
importPath: undefined, importPath: undefined,
updateImportPath: true, updateImportPath: true,
}), },
tree projectConfig
)) as UnitTestTree; );
expect(tree.read(importerFilePath).toString()).toContain( expect(tree.read(importerFilePath).toString()).toContain(
`import { Table } from '@proj/table/components';` `import { Table } from '@proj/table/components';`
@ -122,15 +125,17 @@ describe('updateImports Rule', () => {
expect(tree.read(importerFilePath).toString()).toContain( expect(tree.read(importerFilePath).toString()).toContain(
`import { Tab } from '@proj/tabs/components';` `import { Tab } from '@proj/tabs/components';`
); );
expect(tree.read(importerFilePath).toString()).toMatchSnapshot();
}); });
it('should update dynamic imports', async () => { it('should update dynamic imports', async () => {
tree = await runSchematic('lib', { name: 'table' }, tree); await libraryGenerator(tree, { name: 'table' });
tree = await runSchematic('lib', { name: 'tab' }, tree); 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'; const importerFilePath = 'libs/my-importer/src/importer.ts';
tree.create( tree.write(
importerFilePath, importerFilePath,
` `
import('@proj/table').then(m => m.Table); import('@proj/table').then(m => m.Table);
@ -140,15 +145,17 @@ describe('updateImports Rule', () => {
` `
); );
tree = (await callRule( const projectConfig = readProjectConfiguration(tree, 'tab');
updateImports({ updateImports(
tree,
{
projectName: 'tab', projectName: 'tab',
destination: 'tabs', destination: 'tabs',
importPath: undefined, importPath: undefined,
updateImportPath: true, updateImportPath: true,
}), },
tree projectConfig
)) as UnitTestTree; );
expect(tree.read(importerFilePath).toString()).toContain( expect(tree.read(importerFilePath).toString()).toContain(
`import('@proj/table').then(m => m.Table);` `import('@proj/table').then(m => m.Table);`
@ -165,133 +172,135 @@ describe('updateImports Rule', () => {
expect(tree.read(importerFilePath).toString()).toContain( expect(tree.read(importerFilePath).toString()).toContain(
`import('@proj/tabs/components').then(m => m.Tab);` `import('@proj/tabs/components').then(m => m.Tab);`
); );
expect(tree.read(importerFilePath).toString()).toMatchSnapshot();
}); });
//
it('should update require imports', async () => { // it('should update require imports', async () => {
tree = await runSchematic('lib', { name: 'table' }, tree); // tree = await runSchematic('lib', { name: 'table' }, tree);
tree = await runSchematic('lib', { name: 'tab' }, tree); // tree = await runSchematic('lib', { name: 'tab' }, tree);
//
tree = await runSchematic('lib', { name: 'my-importer' }, tree); // tree = await runSchematic('lib', { name: 'my-importer' }, tree);
const importerFilePath = 'libs/my-importer/src/importer.ts'; // const importerFilePath = 'libs/my-importer/src/importer.ts';
tree.create( // tree.create(
importerFilePath, // importerFilePath,
` // `
require('@proj/table'); // require('@proj/table');
require('@proj/table/components'); // require('@proj/table/components');
require('@proj/tab'); // require('@proj/tab');
require('@proj/tab/components'); // require('@proj/tab/components');
` // `
); // );
//
tree = (await callRule( // tree = (await callRule(
updateImports({ // updateImports({
projectName: 'tab', // projectName: 'tab',
destination: 'tabs', // destination: 'tabs',
importPath: undefined, // importPath: undefined,
updateImportPath: true, // updateImportPath: true,
}), // }),
tree // tree
)) as UnitTestTree; // )) as UnitTestTree;
//
expect(tree.read(importerFilePath).toString()).toContain( // expect(tree.read(importerFilePath).toString()).toContain(
`require('@proj/table');` // `require('@proj/table');`
); // );
//
expect(tree.read(importerFilePath).toString()).toContain( // expect(tree.read(importerFilePath).toString()).toContain(
`require('@proj/table/components');` // `require('@proj/table/components');`
); // );
//
expect(tree.read(importerFilePath).toString()).toContain( // expect(tree.read(importerFilePath).toString()).toContain(
`require('@proj/tabs');` // `require('@proj/tabs');`
); // );
//
expect(tree.read(importerFilePath).toString()).toContain( // expect(tree.read(importerFilePath).toString()).toContain(
`require('@proj/tabs/components');` // `require('@proj/tabs/components');`
); // );
}); // });
//
it('should not update project refs when --updateImportPath=false', async () => { // 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 // // 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 // // 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. // // 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-destination' }, tree);
tree = await runSchematic('lib', { name: 'my-source' }, tree); // tree = await runSchematic('lib', { name: 'my-source' }, tree);
//
tree = await runSchematic('lib', { name: 'my-importer' }, tree); // tree = await runSchematic('lib', { name: 'my-importer' }, tree);
const importerFilePath = 'libs/my-importer/src/importer.ts'; // const importerFilePath = 'libs/my-importer/src/importer.ts';
tree.create( // tree.create(
importerFilePath, // importerFilePath,
` // `
import { MyClass } from '@proj/my-source'; // import { MyClass } from '@proj/my-source';
//
export MyExtendedClass extends MyClass {}; // export MyExtendedClass extends MyClass {};
` // `
); // );
//
schema.updateImportPath = false; // schema.updateImportPath = false;
tree = (await callRule(updateImports(schema), tree)) as UnitTestTree; // tree = (await callRule(updateImports(schema), tree)) as UnitTestTree;
//
expect(tree.read(importerFilePath).toString()).toContain( // expect(tree.read(importerFilePath).toString()).toContain(
`import { MyClass } from '@proj/my-source';` // `import { MyClass } from '@proj/my-source';`
); // );
}); // });
//
it('should update project refs to --importPath when provided', async () => { // 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 // // 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 // // 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. // // 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-destination' }, tree);
tree = await runSchematic('lib', { name: 'my-source' }, tree); // tree = await runSchematic('lib', { name: 'my-source' }, tree);
//
tree = await runSchematic('lib', { name: 'my-importer' }, tree); // tree = await runSchematic('lib', { name: 'my-importer' }, tree);
const importerFilePath = 'libs/my-importer/src/importer.ts'; // const importerFilePath = 'libs/my-importer/src/importer.ts';
tree.create( // tree.create(
importerFilePath, // importerFilePath,
` // `
import { MyClass } from '@proj/my-source'; // import { MyClass } from '@proj/my-source';
//
export class MyExtendedClass extends MyClass {}; // export class MyExtendedClass extends MyClass {};
` // `
); // );
//
schema.importPath = '@proj/wibble'; // schema.importPath = '@proj/wibble';
tree = (await callRule(updateImports(schema), tree)) as UnitTestTree; // tree = (await callRule(updateImports(schema), tree)) as UnitTestTree;
//
expect(tree.read(importerFilePath).toString()).toContain( // expect(tree.read(importerFilePath).toString()).toContain(
`import { MyClass } from '${schema.importPath}';` // `import { MyClass } from '${schema.importPath}';`
); // );
}); // });
//
it('should update project ref in the tsconfig file', async () => { // it('should update project ref in the tsconfig file', async () => {
tree = await runSchematic('lib', { name: 'my-source' }, tree); // tree = await runSchematic('lib', { name: 'my-source' }, tree);
//
let tsConfig = readJsonInTree(tree, '/tsconfig.base.json'); // let tsConfig = readJsonInTree(tree, '/tsconfig.base.json');
expect(tsConfig.compilerOptions.paths).toEqual({ // expect(tsConfig.compilerOptions.paths).toEqual({
'@proj/my-source': ['libs/my-source/src/index.ts'], // '@proj/my-source': ['libs/my-source/src/index.ts'],
}); // });
//
tree = (await callRule(updateImports(schema), tree)) as UnitTestTree; // tree = (await callRule(updateImports(schema), tree)) as UnitTestTree;
//
tsConfig = readJsonInTree(tree, '/tsconfig.base.json'); // tsConfig = readJsonInTree(tree, '/tsconfig.base.json');
expect(tsConfig.compilerOptions.paths).toEqual({ // expect(tsConfig.compilerOptions.paths).toEqual({
'@proj/my-destination': ['libs/my-destination/src/index.ts'], // '@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 () => { // it('should only update the project ref paths in the tsconfig file when --updateImportPath=false', async () => {
tree = await runSchematic('lib', { name: 'my-source' }, tree); // tree = await runSchematic('lib', { name: 'my-source' }, tree);
//
let tsConfig = readJsonInTree(tree, '/tsconfig.base.json'); // let tsConfig = readJsonInTree(tree, '/tsconfig.base.json');
expect(tsConfig.compilerOptions.paths).toEqual({ // expect(tsConfig.compilerOptions.paths).toEqual({
'@proj/my-source': ['libs/my-source/src/index.ts'], // '@proj/my-source': ['libs/my-source/src/index.ts'],
}); // });
//
schema.updateImportPath = false; // schema.updateImportPath = false;
tree = (await callRule(updateImports(schema), tree)) as UnitTestTree; // tree = (await callRule(updateImports(schema), tree)) as UnitTestTree;
//
tsConfig = readJsonInTree(tree, '/tsconfig.base.json'); // tsConfig = readJsonInTree(tree, '/tsconfig.base.json');
expect(tsConfig.compilerOptions.paths).toEqual({ // expect(tsConfig.compilerOptions.paths).toEqual({
'@proj/my-source': ['libs/my-destination/src/index.ts'], // '@proj/my-source': ['libs/my-destination/src/index.ts'],
}); // });
}); // });
}); });

View File

@ -1,17 +1,14 @@
import { findNodes, serializeJson } from '@nrwl/workspace';
import { import {
SchematicContext, applyChangesToString,
ChangeType,
getProjects,
getWorkspaceLayout,
ProjectConfiguration,
StringChange,
Tree, Tree,
UpdateRecorder, visitNotIgnoredFiles,
} from '@angular-devkit/schematics'; } from '@nrwl/devkit';
import {
findNodes,
getWorkspace,
NxJson,
readJsonInTree,
serializeJson,
} from '@nrwl/workspace';
import { from, Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import * as ts from 'typescript'; import * as ts from 'typescript';
import { Schema } from '../schema'; import { Schema } from '../schema';
import { normalizeSlashes } from './utils'; import { normalizeSlashes } from './utils';
@ -21,21 +18,19 @@ import { normalizeSlashes } from './utils';
* *
* @param schema The options provided to the schematic * @param schema The options provided to the schematic
*/ */
export function updateImports(schema: Schema) { export function updateImports(
return (tree: Tree, _context: SchematicContext): Observable<Tree> => { tree: Tree,
return from(getWorkspace(tree)).pipe( schema: Schema,
map((workspace) => { project: ProjectConfiguration
const nxJson = readJsonInTree<NxJson>(tree, 'nx.json'); ) {
const libsDir = nxJson.workspaceLayout?.libsDir if (project.projectType === 'application') {
? nxJson.workspaceLayout.libsDir
: 'libs';
const project = workspace.projects.get(schema.projectName);
if (project.extensions['projectType'] === 'application') {
// These shouldn't be imported anywhere? // These shouldn't be imported anywhere?
return tree; return;
} }
const { npmScope, libsDir } = getWorkspaceLayout(tree);
const projects = getProjects(tree);
// use the source root to find the from location // use the source root to find the from location
// this attempts to account for libs that have been created with --importPath // this attempts to account for libs that have been created with --importPath
const tsConfigPath = 'tsconfig.base.json'; const tsConfigPath = 'tsconfig.base.json';
@ -54,35 +49,28 @@ export function updateImports(schema: Schema) {
from: from:
fromPath || fromPath ||
normalizeSlashes( normalizeSlashes(
`@${nxJson.npmScope}/${project.root.substr(libsDir.length + 1)}` `@${npmScope}/${project.root.substr(libsDir.length + 1)}`
), ),
to: to:
schema.importPath || schema.importPath ||
normalizeSlashes(`@${nxJson.npmScope}/${schema.destination}`), normalizeSlashes(`@${npmScope}/${schema.destination}`),
}; };
if (schema.updateImportPath) { if (schema.updateImportPath) {
const replaceProjectRef = new RegExp(projectRef.from, 'g'); const replaceProjectRef = new RegExp(projectRef.from, 'g');
for (const [name, definition] of workspace.projects.entries()) { for (const [name, definition] of projects.entries()) {
if (name === schema.projectName) { if (name === schema.projectName) {
continue; continue;
} }
const projectDir = tree.getDir(definition.root); visitNotIgnoredFiles(tree, definition.root, (file) => {
projectDir.visit((file) => {
const contents = tree.read(file).toString('utf-8'); const contents = tree.read(file).toString('utf-8');
if (!replaceProjectRef.test(contents)) { if (!replaceProjectRef.test(contents)) {
return; return;
} }
updateImportPaths( updateImportPaths(tree, file, projectRef.from, projectRef.to);
tree,
file,
contents,
projectRef.from,
projectRef.to
);
}); });
} }
} }
@ -93,9 +81,7 @@ export function updateImports(schema: Schema) {
}; };
if (tsConfig) { if (tsConfig) {
const path = tsConfig.compilerOptions.paths[ const path = tsConfig.compilerOptions.paths[projectRef.from] as string[];
projectRef.from
] as string[];
if (!path) { if (!path) {
throw new Error( throw new Error(
[ [
@ -115,22 +101,15 @@ export function updateImports(schema: Schema) {
tsConfig.compilerOptions.paths[projectRef.from] = updatedPath; tsConfig.compilerOptions.paths[projectRef.from] = updatedPath;
} }
tree.overwrite(tsConfigPath, serializeJson(tsConfig)); tree.write(tsConfigPath, serializeJson(tsConfig));
} }
return tree;
})
);
};
} }
function updateImportPaths( /**
tree: Tree, * Changes imports in a file from one import to another
path: string, */
contents: string, function updateImportPaths(tree: Tree, path: string, from: string, to: string) {
from: string, const contents = tree.read(path).toString('utf-8');
to: string
) {
const sourceFile = ts.createSourceFile( const sourceFile = ts.createSourceFile(
path, path,
contents, contents,
@ -138,50 +117,54 @@ function updateImportPaths(
true 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 tree.write(path, newContents);
updateImportDeclarations(recorder, sourceFile, from, to);
updateDynamicImports(recorder, sourceFile, from, to);
tree.commitUpdate(recorder);
} }
/** /**
* Update the module specifiers on static imports * Update the module specifiers on static imports
*/ */
function updateImportDeclarations( function updateImportDeclarations(
recorder: UpdateRecorder,
sourceFile: ts.SourceFile, sourceFile: ts.SourceFile,
from: string, from: string,
to: string to: string
) { ): StringChange[] {
const importDecls = findNodes( const importDecls = findNodes(
sourceFile, sourceFile,
ts.SyntaxKind.ImportDeclaration ts.SyntaxKind.ImportDeclaration
) as ts.ImportDeclaration[]; ) as ts.ImportDeclaration[];
const changes: StringChange[] = [];
for (const { moduleSpecifier } of importDecls) { for (const { moduleSpecifier } of importDecls) {
if (ts.isStringLiteral(moduleSpecifier)) { 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 * Update the module specifiers on dynamic imports and require statements
*/ */
function updateDynamicImports( function updateDynamicImports(
recorder: UpdateRecorder,
sourceFile: ts.SourceFile, sourceFile: ts.SourceFile,
from: string, from: string,
to: string to: string
) { ): StringChange[] {
const expressions = findNodes( const expressions = findNodes(
sourceFile, sourceFile,
ts.SyntaxKind.CallExpression ts.SyntaxKind.CallExpression
) as ts.CallExpression[]; ) as ts.CallExpression[];
const changes: StringChange[] = [];
for (const { expression, arguments: args } of expressions) { for (const { expression, arguments: args } of expressions) {
const moduleSpecifier = args[0]; const moduleSpecifier = args[0];
@ -191,7 +174,7 @@ function updateDynamicImports(
moduleSpecifier && moduleSpecifier &&
ts.isStringLiteral(moduleSpecifier) ts.isStringLiteral(moduleSpecifier)
) { ) {
updateModuleSpecifier(recorder, moduleSpecifier, from, to); changes.push(...updateModuleSpecifier(moduleSpecifier, from, to));
} }
// handle require statements // handle require statements
@ -201,33 +184,38 @@ function updateDynamicImports(
moduleSpecifier && moduleSpecifier &&
ts.isStringLiteral(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 * Replace the old module specifier with a the new path
*/ */
function updateModuleSpecifier( function updateModuleSpecifier(
recorder: UpdateRecorder,
moduleSpecifier: ts.StringLiteral, moduleSpecifier: ts.StringLiteral,
from: string, from: string,
to: string to: string
) { ): StringChange[] {
if ( if (
moduleSpecifier.text === from || moduleSpecifier.text === from ||
moduleSpecifier.text.startsWith(from + '/') moduleSpecifier.text.startsWith(from + '/')
) { ) {
recorder.remove( return [
moduleSpecifier.getStart() + 1, {
moduleSpecifier.text.length type: ChangeType.Delete,
); start: moduleSpecifier.getStart() + 1,
length: moduleSpecifier.text.length,
// insert the new module specifier },
recorder.insertLeft( {
moduleSpecifier.getStart() + 1, type: ChangeType.Insert,
moduleSpecifier.text.replace(new RegExp(from, 'g'), to) 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 { readProjectConfiguration, Tree } from '@nrwl/devkit';
import { UnitTestTree } from '@angular-devkit/schematics/testing'; import { createTreeWithEmptyWorkspace } from '@nrwl/devkit/testing';
import { createEmptyWorkspace } from '@nrwl/workspace/testing';
import { callRule, runSchematic } from '../../../utils/testing';
import { Schema } from '../schema'; import { Schema } from '../schema';
import { updateJestConfig } from './update-jest-config'; import { updateJestConfig } from './update-jest-config';
import { libraryGenerator } from '../../library/library';
describe('updateJestConfig Rule', () => { describe('updateJestConfig', () => {
let tree: UnitTestTree; let tree: Tree;
beforeEach(async () => { beforeEach(async () => {
tree = new UnitTestTree(Tree.empty()); tree = createTreeWithEmptyWorkspace();
tree = createEmptyWorkspace(tree) as UnitTestTree;
}); });
it('should handle jest config not existing', async () => { 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 = { const schema: Schema = {
projectName: 'my-source', projectName: 'my-source',
@ -23,9 +25,7 @@ describe('updateJestConfig Rule', () => {
updateImportPath: true, updateImportPath: true,
}; };
await expect( updateJestConfig(tree, schema, projectConfig);
callRule(updateJestConfig(schema), tree)
).resolves.not.toThrow();
}); });
it('should update the name and coverage directory', async () => { it('should update the name and coverage directory', async () => {
@ -42,8 +42,11 @@ describe('updateJestConfig Rule', () => {
const rootJestConfigPath = '/jest.config.js'; const rootJestConfigPath = '/jest.config.js';
tree = await runSchematic('lib', { name: 'my-source' }, tree); await libraryGenerator(tree, {
tree.create(jestConfigPath, jestConfig); name: 'my-source',
});
const projectConfig = readProjectConfiguration(tree, 'my-source');
tree.write(jestConfigPath, jestConfig);
const schema: Schema = { const schema: Schema = {
projectName: 'my-source', projectName: 'my-source',
@ -52,7 +55,7 @@ describe('updateJestConfig Rule', () => {
updateImportPath: true, updateImportPath: true,
}; };
tree = (await callRule(updateJestConfig(schema), tree)) as UnitTestTree; updateJestConfig(tree, schema, projectConfig);
const jestConfigAfter = tree.read(jestConfigPath).toString(); const jestConfigAfter = tree.read(jestConfigPath).toString();
const rootJestConfigAfter = tree.read(rootJestConfigPath).toString(); const rootJestConfigAfter = tree.read(rootJestConfigPath).toString();

View File

@ -1,11 +1,9 @@
import { Rule, SchematicContext } from '@angular-devkit/schematics'; import { Tree, ProjectConfiguration, getWorkspaceLayout } from '@nrwl/devkit';
import { Tree } from '@angular-devkit/schematics/src/tree/interface';
import { getWorkspace } from '@nrwl/workspace';
import * as path from 'path'; import * as path from 'path';
import { from, Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import { Schema } from '../schema'; 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 * Updates the project name and coverage folder in the jest.config.js if it exists
@ -14,19 +12,19 @@ import { getDestination, getNewProjectName, getWorkspaceLayout } from './utils';
* *
* @param schema The options provided to the schematic * @param schema The options provided to the schematic
*/ */
export function updateJestConfig(schema: Schema): Rule { export function updateJestConfig(
return (tree: Tree, _context: SchematicContext): Observable<Tree> => { tree: Tree,
return from(getWorkspace(tree)).pipe( schema: Schema,
map((workspace) => { project: ProjectConfiguration
const project = workspace.projects.get(schema.projectName); ) {
const destination = getDestination(schema, workspace, tree); const destination = getDestination(tree, schema, project);
const newProjectName = getNewProjectName(schema.destination); 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)) { if (!tree.exists(jestConfigPath)) {
// nothing to do // nothing to do
return tree; return;
} }
const oldContent = tree.read(jestConfigPath).toString('utf-8'); const oldContent = tree.read(jestConfigPath).toString('utf-8');
@ -37,34 +35,29 @@ export function updateJestConfig(schema: Schema): Rule {
const newContent = oldContent const newContent = oldContent
.replace(findName, `'${newProjectName}'`) .replace(findName, `'${newProjectName}'`)
.replace(findDir, destination); .replace(findDir, destination);
tree.overwrite(jestConfigPath, newContent); tree.write(jestConfigPath, newContent);
// update root jest.config.js // update root jest.config.js
const rootJestConfigPath = '/jest.config.js'; const rootJestConfigPath = '/jest.config.js';
if (!tree.exists(rootJestConfigPath)) { if (!tree.exists(rootJestConfigPath)) {
return tree; return;
} }
const oldRootJestConfigContent = tree
.read(rootJestConfigPath)
.toString('utf-8');
const { libsDir, appsDir } = getWorkspaceLayout(tree); const { libsDir, appsDir } = getWorkspaceLayout(tree);
const findProject = new RegExp( const findProject = new RegExp(
`<rootDir>\/(${libsDir}|${appsDir})\/${schema.projectName}`, `<rootDir>\/(${libsDir}|${appsDir})\/${schema.projectName}`,
'g' 'g'
); );
const oldRootJestConfigContent = tree
.read(rootJestConfigPath)
.toString('utf-8');
const newRootJestConfigContent = oldRootJestConfigContent.replace( const newRootJestConfigContent = oldRootJestConfigContent.replace(
findProject, findProject,
`<rootDir>/${destination}` `<rootDir>/${destination}`
); );
tree.overwrite(rootJestConfigPath, newRootJestConfigContent); tree.write(rootJestConfigPath, newRootJestConfigContent);
return tree;
})
);
};
} }

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 { readProjectConfiguration, Tree } from '@nrwl/devkit';
import { UnitTestTree } from '@angular-devkit/schematics/testing'; import { createTreeWithEmptyWorkspace } from '@nrwl/devkit/testing';
import { createEmptyWorkspace } from '@nrwl/workspace/testing';
import { callRule, runSchematic } from '../../../utils/testing';
import { Schema } from '../schema'; import { Schema } from '../schema';
import { updateProjectRootFiles } from './update-project-root-files'; import { updateProjectRootFiles } from './update-project-root-files';
import { libraryGenerator } from '../../library/library';
describe('updateProjectRootFiles Rule', () => { describe('updateProjectRootFiles', () => {
let tree: UnitTestTree; let tree: Tree;
beforeEach(async () => { beforeEach(async () => {
tree = new UnitTestTree(Tree.empty()); tree = createTreeWithEmptyWorkspace();
tree = createEmptyWorkspace(tree) as UnitTestTree;
}); });
it('should update the relative root in files at the root of the project', async () => { 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'; const testFilePath = '/libs/subfolder/my-destination/jest.config.js';
tree = await runSchematic('lib', { name: 'my-source' }, tree); await libraryGenerator(tree, {
tree.create(testFilePath, testFile); name: 'my-source',
});
const projectConfig = readProjectConfiguration(tree, 'my-source');
tree.write(testFilePath, testFile);
const schema: Schema = { const schema: Schema = {
projectName: 'my-source', projectName: 'my-source',
@ -35,10 +36,7 @@ describe('updateProjectRootFiles Rule', () => {
updateImportPath: true, updateImportPath: true,
}; };
tree = (await callRule( updateProjectRootFiles(tree, schema, projectConfig);
updateProjectRootFiles(schema),
tree
)) as UnitTestTree;
const testFileAfter = tree.read(testFilePath).toString(); const testFileAfter = tree.read(testFilePath).toString();
expect(testFileAfter).toContain(`preset: '../../../jest.config.js'`); 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 * 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 { Schema } from '../schema';
import { getDestination } from './utils'; import { getDestination } from './utils';
import { extname, join } from 'path';
/** /**
* Updates the files in the root of the project * Updates the files in the root of the project
@ -15,12 +14,12 @@ import { getDestination } from './utils';
* *
* @param schema The options provided to the schematic * @param schema The options provided to the schematic
*/ */
export function updateProjectRootFiles(schema: Schema): Rule { export function updateProjectRootFiles(
return (tree: Tree, _context: SchematicContext): Observable<Tree> => { tree: Tree,
return from(getWorkspace(tree)).pipe( schema: Schema,
map((workspace) => { project: ProjectConfiguration
const project = workspace.projects.get(schema.projectName); ) {
const destination = getDestination(schema, workspace, tree); const destination = getDestination(tree, schema, project);
const newRelativeRoot = path const newRelativeRoot = path
.relative(path.join(appRootPath, destination), appRootPath) .relative(path.join(appRootPath, destination), appRootPath)
@ -33,26 +32,19 @@ export function updateProjectRootFiles(schema: Schema): Rule {
if (newRelativeRoot === oldRelativeRoot) { if (newRelativeRoot === oldRelativeRoot) {
// nothing to do // nothing to do
return tree; return;
} }
const dots = /\./g; const dots = /\./g;
const regex = new RegExp(oldRelativeRoot.replace(dots, '\\.'), 'g'); const regex = new RegExp(oldRelativeRoot.replace(dots, '\\.'), 'g');
const isRootFile = new RegExp(`${schema.destination}/[^/]+.js*`); for (const file of tree.children(destination)) {
const projectDir = tree.getDir(destination); if (!extname(file).startsWith('.js')) {
projectDir.visit((file) => { continue;
if (!isRootFile.test(file)) {
return;
} }
const oldContent = tree.read(file).toString(); const oldContent = tree.read(join(destination, file)).toString();
const newContent = oldContent.replace(regex, newRelativeRoot); const newContent = oldContent.replace(regex, newRelativeRoot);
tree.overwrite(file, newContent); tree.write(join(destination, file), newContent);
}); }
return tree;
})
);
};
} }

View File

@ -1,20 +1,21 @@
import { Tree } from '@angular-devkit/schematics'; import { readProjectConfiguration, Tree } from '@nrwl/devkit';
import { UnitTestTree } from '@angular-devkit/schematics/testing';
import { createEmptyWorkspace } from '@nrwl/workspace/testing';
import { callRule, runSchematic } from '../../../utils/testing';
import { Schema } from '../schema'; import { Schema } from '../schema';
import { updateStorybookConfig } from './update-storybook-config'; import { updateStorybookConfig } from './update-storybook-config';
import { createTreeWithEmptyWorkspace } from '@nrwl/devkit/testing';
import { libraryGenerator } from '../../library/library';
describe('updateStorybookConfig Rule', () => { describe('updateStorybookConfig', () => {
let tree: UnitTestTree; let tree: Tree;
beforeEach(async () => { beforeEach(async () => {
tree = new UnitTestTree(Tree.empty()); tree = createTreeWithEmptyWorkspace();
tree = createEmptyWorkspace(tree) as UnitTestTree;
}); });
it('should handle storybook config not existing', async () => { 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 = { const schema: Schema = {
projectName: 'my-source', projectName: 'my-source',
@ -23,9 +24,7 @@ describe('updateStorybookConfig Rule', () => {
updateImportPath: true, updateImportPath: true,
}; };
await expect( updateStorybookConfig(tree, schema, projectConfig);
callRule(updateStorybookConfig(schema), tree)
).resolves.not.toThrow();
}); });
it('should update the import path for main.js', async () => { it('should update the import path for main.js', async () => {
@ -37,8 +36,11 @@ describe('updateStorybookConfig Rule', () => {
const storybookMainPath = const storybookMainPath =
'/libs/namespace/my-destination/.storybook/main.js'; '/libs/namespace/my-destination/.storybook/main.js';
tree = await runSchematic('lib', { name: 'my-source' }, tree); await libraryGenerator(tree, {
tree.create(storybookMainPath, storybookMain); name: 'my-source',
});
const projectConfig = readProjectConfiguration(tree, 'my-source');
tree.write(storybookMainPath, storybookMain);
const schema: Schema = { const schema: Schema = {
projectName: 'my-source', projectName: 'my-source',
@ -47,10 +49,7 @@ describe('updateStorybookConfig Rule', () => {
updateImportPath: true, updateImportPath: true,
}; };
tree = (await callRule( updateStorybookConfig(tree, schema, projectConfig);
updateStorybookConfig(schema),
tree
)) as UnitTestTree;
const storybookMainAfter = tree.read(storybookMainPath).toString(); const storybookMainAfter = tree.read(storybookMainPath).toString();
expect(storybookMainAfter).toContain( expect(storybookMainAfter).toContain(
@ -66,8 +65,11 @@ describe('updateStorybookConfig Rule', () => {
const storybookWebpackConfigPath = const storybookWebpackConfigPath =
'/libs/namespace/my-destination/.storybook/webpack.config.js'; '/libs/namespace/my-destination/.storybook/webpack.config.js';
tree = await runSchematic('lib', { name: 'my-source' }, tree); await libraryGenerator(tree, {
tree.create(storybookWebpackConfigPath, storybookWebpackConfig); name: 'my-source',
});
const projectConfig = readProjectConfiguration(tree, 'my-source');
tree.write(storybookWebpackConfigPath, storybookWebpackConfig);
const schema: Schema = { const schema: Schema = {
projectName: 'my-source', projectName: 'my-source',
@ -76,10 +78,7 @@ describe('updateStorybookConfig Rule', () => {
updateImportPath: true, updateImportPath: true,
}; };
tree = (await callRule( updateStorybookConfig(tree, schema, projectConfig);
updateStorybookConfig(schema),
tree
)) as UnitTestTree;
const storybookWebpackConfigAfter = tree const storybookWebpackConfigAfter = tree
.read(storybookWebpackConfigPath) .read(storybookWebpackConfigPath)

View File

@ -1,61 +1,44 @@
import { Rule, SchematicContext } from '@angular-devkit/schematics'; import { ProjectConfiguration, Tree } from '@nrwl/devkit';
import { Tree } from '@angular-devkit/schematics/src/tree/interface';
import { getWorkspace } from '@nrwl/workspace';
import * as path from 'path'; 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 { Schema } from '../schema';
import { getDestination } from './utils'; import { getDestination } from './utils';
import { appRootPath } from '@nrwl/workspace/src/utils/app-root'; import { join } from 'path';
import { allFilesInDirInHost } from '@nrwl/workspace/src/utils/ast-utils';
import { Path, normalize } from '@angular-devkit/core';
/** /**
* Updates relative path to root storybook config for `main.js` & `webpack.config.js` * Updates relative path to root storybook config for `main.js` & `webpack.config.js`
* *
* @param schema The options provided to the schematic * @param schema The options provided to the schematic
*/ */
export function updateStorybookConfig(schema: Schema): Rule { export function updateStorybookConfig(
return (tree: Tree, _context: SchematicContext): Observable<Tree> => { tree: Tree,
return from(getWorkspace(tree)).pipe( schema: Schema,
map((workspace) => { project: ProjectConfiguration
const project = workspace.projects.get(schema.projectName); ) {
const destination = getDestination(schema, workspace, tree); const destination = getDestination(tree, schema, project);
const oldRelativeRoot = path const oldRelativeRoot = path
.relative( .relative(path.join(appRootPath, `${project.root}/.storybook`), appRootPath)
path.join(appRootPath, `${project.root}/.storybook`),
appRootPath
)
.split(path.sep) .split(path.sep)
.join('/'); .join('/');
const newRelativeRoot = path const newRelativeRoot = path
.relative( .relative(path.join(appRootPath, `${destination}/.storybook`), appRootPath)
path.join(appRootPath, `${destination}/.storybook`),
appRootPath
)
.split(path.sep) .split(path.sep)
.join('/'); .join('/');
const storybookDir = path.join(destination, '.storybook'); const storybookDir = path.join(destination, '.storybook');
if (!storybookDir) { if (!storybookDir) {
return tree; return;
} }
// Replace relative import path to root storybook folder for each file under project storybook // Replace relative import path to root storybook folder for each file under project storybook
tree.getDir(storybookDir).visit((file) => { for (const file of tree.children(storybookDir)) {
const oldContent = tree.read(file).toString('utf-8'); const oldContent = tree.read(join(storybookDir, file)).toString('utf-8');
const newContent = oldContent.replace( const newContent = oldContent.replace(oldRelativeRoot, newRelativeRoot);
oldRelativeRoot,
newRelativeRoot
);
tree.overwrite(file, newContent); tree.write(join(storybookDir, file), newContent);
}); }
return tree;
})
);
};
} }

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 * as path from 'path';
import { ProjectConfiguration, Tree, getWorkspaceLayout } from '@nrwl/devkit';
import { Schema } from '../schema'; 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 * This helper function ensures that we don't move libs or apps
* outside of the folders they should be in. * outside of the folders they should be in.
@ -34,16 +15,11 @@ export function getWorkspaceLayout(
* @param workspace * @param workspace
*/ */
export function getDestination( export function getDestination(
host: Tree,
schema: Schema, schema: Schema,
workspace: WorkspaceDefinition | any, project: ProjectConfiguration
host: Tree
): string { ): string {
const project = workspace.projects.get const projectType = project.projectType;
? workspace.projects.get(schema.projectName)
: workspace.projects[schema.projectName];
const projectType = project.extensions
? project.extensions['projectType']
: project.projectType;
const workspaceLayout = getWorkspaceLayout(host); const workspaceLayout = getWorkspaceLayout(host);
@ -51,7 +27,7 @@ export function getDestination(
if (projectType === 'application') { if (projectType === 'application') {
rootFolder = workspaceLayout.appsDir; 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 {
import { checkProjectExists } from '../../utils/rules/check-project-exists'; convertNxGenerator,
formatFiles,
readProjectConfiguration,
Tree,
} from '@nrwl/devkit';
import { checkDestination } from './lib/check-destination'; import { checkDestination } from './lib/check-destination';
import { moveProject } from './lib/move-project'; import { moveProject } from './lib/move-project';
import { updateCypressJson } from './lib/update-cypress-json'; import { updateCypressJson } from './lib/update-cypress-json';
import { updateImports } from './lib/update-imports'; import { updateImports } from './lib/update-imports';
import { updateJestConfig } from './lib/update-jest-config'; import { updateJestConfig } from './lib/update-jest-config';
import { updateStorybookConfig } from './lib/update-storybook-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 { 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 { Schema } from './schema';
import { wrapAngularDevkitSchematic } from '@nrwl/devkit/ngcli-adapter';
import { updateEslintrcJson } from './lib/update-eslintrc-json'; 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 { export async function moveGenerator(tree: Tree, schema: Schema) {
return chain([ const projectConfig = readProjectConfiguration(tree, schema.projectName);
checkProjectExists(schema), checkDestination(tree, schema, projectConfig);
checkDestination(schema), moveProject(tree, schema, projectConfig); // we MUST move the project first, if we don't we get a "This should never happen" error 🤦‍♀️
moveProject(schema), // we MUST move the project first, if we don't we get a "This should never happen" error 🤦‍♀️ updateImports(tree, schema, projectConfig);
updateProjectRootFiles(schema), updateProjectRootFiles(tree, schema, projectConfig);
updateCypressJson(schema), updateCypressJson(tree, schema, projectConfig);
updateJestConfig(schema), updateJestConfig(tree, schema, projectConfig);
updateStorybookConfig(schema), updateStorybookConfig(tree, schema, projectConfig);
updateNxJson(schema), updateEslintrcJson(tree, schema, projectConfig);
updateImports(schema), moveProjectConfiguration(tree, schema, projectConfig);
updateEslintrcJson(schema), updateBuildTargets(tree, schema);
updateWorkspace(schema), // Have to do this last because all previous rules need the information in here updateDefaultProject(tree, schema);
]); updateImplicitDependencies(tree, schema);
if (!schema.skipFormat) {
await formatFiles(tree);
}
} }
export const moveGenerator = wrapAngularDevkitSchematic( export default moveGenerator;
'@nrwl/workspace',
'move' export const moveSchematic = convertNxGenerator(moveGenerator);
);

View File

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

View File

@ -1,6 +1,7 @@
{ {
"$schema": "http://json-schema.org/schema", "$schema": "http://json-schema.org/schema",
"id": "NxWorkspaceMove", "id": "NxWorkspaceMove",
"cli": "nx",
"title": "Nx Move", "title": "Nx Move",
"description": "Move a project to another folder in the workspace", "description": "Move a project to another folder in the workspace",
"type": "object", "type": "object",
@ -32,6 +33,12 @@
"type": "boolean", "type": "boolean",
"description": "Should the generator update the import path to reflect the new location?", "description": "Should the generator update the import path to reflect the new location?",
"default": true "default": true
},
"skipFormat": {
"type": "boolean",
"aliases": ["skip-format"],
"description": "Skip formatting files.",
"default": false
} }
}, },
"required": ["projectName", "destination"] "required": ["projectName", "destination"]

View File

@ -1,6 +1,6 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP // 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 = { "module.exports = {
projects: [ projects: [
'<rootDir>/apps/my-app/', '<rootDir>/apps/my-app/',
@ -12,7 +12,7 @@ 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 = { "module.exports = {
projects: [ projects: [
'<rootDir>/apps/my-app/', '<rootDir>/apps/my-app/',

View File

@ -1,18 +1,19 @@
import { Tree } from '@angular-devkit/schematics'; import {
import { UnitTestTree } from '@angular-devkit/schematics/testing'; readProjectConfiguration,
import { updateJsonInTree } from '@nrwl/workspace'; Tree,
import { createEmptyWorkspace } from '@nrwl/workspace/testing'; updateProjectConfiguration,
import { callRule, runSchematic } from '../../../utils/testing'; } from '@nrwl/devkit';
import { Schema } from '../schema'; import { Schema } from '../schema';
import { checkDependencies } from './check-dependencies'; import { checkDependencies } from './check-dependencies';
import { createTreeWithEmptyWorkspace } from '@nrwl/devkit/testing';
import { libraryGenerator } from '../../library/library';
describe('updateImports Rule', () => { describe('checkDependencies', () => {
let tree: UnitTestTree; let tree: Tree;
let schema: Schema; let schema: Schema;
beforeEach(async () => { beforeEach(async () => {
tree = new UnitTestTree(Tree.empty()); tree = createTreeWithEmptyWorkspace();
tree = createEmptyWorkspace(tree) as UnitTestTree;
schema = { schema = {
projectName: 'my-source', projectName: 'my-source',
@ -20,21 +21,25 @@ describe('updateImports Rule', () => {
forceRemove: false, forceRemove: false,
}; };
tree = await runSchematic('lib', { name: 'my-dependent' }, tree); await libraryGenerator(tree, {
tree = await runSchematic('lib', { name: 'my-source' }, tree); name: 'my-dependent',
});
await libraryGenerator(tree, {
name: 'my-source',
});
}); });
describe('static dependencies', () => { describe('static dependencies', () => {
beforeEach(() => { beforeEach(() => {
const sourceFilePath = 'libs/my-source/src/lib/my-source.ts'; const sourceFilePath = 'libs/my-source/src/lib/my-source.ts';
tree.overwrite( tree.write(
sourceFilePath, sourceFilePath,
`export class MyClass {} `export class MyClass {}
` `
); );
const dependentFilePath = 'libs/my-dependent/src/lib/my-dependent.ts'; const dependentFilePath = 'libs/my-dependent/src/lib/my-dependent.ts';
tree.overwrite( tree.write(
dependentFilePath, dependentFilePath,
`import { MyClass } from '@proj/my-source'; `import { MyClass } from '@proj/my-source';
@ -44,33 +49,33 @@ describe('updateImports Rule', () => {
}); });
it('should fatally error if any dependent exists', async () => { it('should fatally error if any dependent exists', async () => {
await expect(callRule(checkDependencies(schema), tree)).rejects.toThrow( expect(() => {
`${schema.projectName} is still depended on by the following projects:\nmy-dependent` checkDependencies(tree, schema);
); }).toThrow();
}); });
it('should not error if forceRemove is true', async () => { it('should not error if forceRemove is true', async () => {
schema.forceRemove = true; schema.forceRemove = true;
await expect( expect(() => {
callRule(checkDependencies(schema), tree) checkDependencies(tree, schema);
).resolves.not.toThrow(); }).not.toThrow();
}); });
}); });
describe('implicit dependencies', () => { describe('implicit dependencies', () => {
beforeEach(async () => { beforeEach(async () => {
tree = (await callRule( const config = readProjectConfiguration(tree, 'my-dependent');
updateJsonInTree('nx.json', (json) => { updateProjectConfiguration(tree, 'my-dependent', {
json.projects['my-dependent'].implicitDependencies = ['my-source']; ...config,
return json; implicitDependencies: ['my-source'],
}), });
tree
)) as UnitTestTree;
}); });
it('should fatally error if any dependent exists', async () => { 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` `${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 () => { it('should not error if forceRemove is true', async () => {
schema.forceRemove = true; schema.forceRemove = true;
await expect( expect(() => {
callRule(checkDependencies(schema), tree) checkDependencies(tree, schema);
).resolves.not.toThrow(); }).not.toThrow();
}); });
}); });
it('should not error if there are no dependents', async () => { it('should not error if there are no dependents', async () => {
await expect( expect(() => {
callRule(checkDependencies(schema), tree) checkDependencies(tree, schema);
).resolves.not.toThrow(); }).not.toThrow();
}); });
}); });

View File

@ -1,75 +1,30 @@
import { Rule, Tree } from '@angular-devkit/schematics'; import { Tree } from '@nrwl/devkit';
import { FileData } from '@nrwl/workspace/src/core/file-utils';
import { 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, onlyWorkspaceProjects,
ProjectGraph, ProjectGraph,
reverse, reverse,
} from '../../../core/project-graph'; } from '../../../core/project-graph';
import { Schema } from '../schema'; 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 * 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. * 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) { 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));
for (const dir of tree.getDir('/').subdirs) {
if (ig.ignores(dir)) {
continue;
} }
tree.getDir(dir).visit((file: string) => { const graph: ProjectGraph = createProjectGraphFromTree(tree);
files.push({
file: path.relative(workspaceDir, file),
ext: path.extname(file),
hash: '',
});
});
}
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
);
const reverseGraph = onlyWorkspaceProjects(reverse(graph)); const reverseGraph = onlyWorkspaceProjects(reverse(graph));
const deps = reverseGraph.dependencies[schema.projectName] || []; const deps = reverseGraph.dependencies[schema.projectName] || [];
if (deps.length === 0) { if (deps.length === 0) {
return tree; return;
} }
throw new Error( throw new Error(
@ -79,5 +34,4 @@ export function checkDependencies(schema: Schema): Rule {
.map((x) => x.target) .map((x) => x.target)
.join('\n')}` .join('\n')}`
); );
};
} }

View File

@ -1,18 +1,14 @@
import { Tree } from '@angular-devkit/schematics'; import { addProjectConfiguration, Tree } from '@nrwl/devkit';
import { UnitTestTree } from '@angular-devkit/schematics/testing'; import { createTreeWithEmptyWorkspace } from '@nrwl/devkit/testing';
import { updateWorkspaceInTree } from '@nrwl/workspace/src/utils/ast-utils';
import { createEmptyWorkspace } from '@nrwl/workspace/testing';
import { callRule } from '../../../utils/testing';
import { Schema } from '../schema'; import { Schema } from '../schema';
import { checkTargets } from './check-targets'; import { checkTargets } from './check-targets';
describe('checkTargets Rule', () => { describe('checkTargets', () => {
let tree: UnitTestTree; let tree: Tree;
let schema: Schema; let schema: Schema;
beforeEach(async () => { beforeEach(async () => {
tree = new UnitTestTree(Tree.empty()); tree = createTreeWithEmptyWorkspace();
tree = createEmptyWorkspace(tree) as UnitTestTree;
schema = { schema = {
projectName: 'ng-app', projectName: 'ng-app',
@ -20,31 +16,25 @@ describe('checkTargets Rule', () => {
forceRemove: false, forceRemove: false,
}; };
tree = (await callRule( addProjectConfiguration(tree, 'ng-app', {
updateWorkspaceInTree((workspace) => {
return {
version: 1,
projects: {
'ng-app': {
projectType: 'application', projectType: 'application',
schematics: {},
root: 'apps/ng-app', root: 'apps/ng-app',
sourceRoot: 'apps/ng-app/src', sourceRoot: 'apps/ng-app/src',
prefix: 'happyorg', targets: {
architect: {
build: { build: {
builder: '@angular-devkit/build-angular:browser', executor: '@angular-devkit/build-angular:browser',
options: {}, options: {},
}, },
}, },
}, });
'ng-app-e2e': {
addProjectConfiguration(tree, 'ng-app-e2e', {
root: 'apps/ng-app-e2e', root: 'apps/ng-app-e2e',
sourceRoot: 'apps/ng-app-e2e/src', sourceRoot: 'apps/ng-app-e2e/src',
projectType: 'application', projectType: 'application',
architect: { targets: {
e2e: { e2e: {
builder: '@nrwl/cypress:cypress', executor: '@nrwl/cypress:cypress',
options: { options: {
cypressConfig: 'apps/ng-app-e2e/cypress.json', cypressConfig: 'apps/ng-app-e2e/cypress.json',
tsConfig: 'apps/ng-app-e2e/tsconfig.e2e.json', tsConfig: 'apps/ng-app-e2e/tsconfig.e2e.json',
@ -52,27 +42,28 @@ describe('checkTargets Rule', () => {
}, },
}, },
}, },
}, });
},
};
}),
tree
)) as UnitTestTree;
}); });
it('should throw an error if another project targets', async () => { 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 () => { it('should NOT throw an error if no other project targets', async () => {
schema.projectName = 'ng-app-e2e'; 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 () => { it('should not error if forceRemove is true', async () => {
schema.forceRemove = true; 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 { getProjects, Tree } from '@nrwl/devkit';
import { updateWorkspaceInTree } from '@nrwl/workspace';
import { Schema } from '../schema'; import { Schema } from '../schema';
/** /**
@ -9,36 +8,30 @@ import { Schema } from '../schema';
* *
* @param schema The options provided to the schematic * @param schema The options provided to the schematic
*/ */
export function checkTargets(schema: Schema) { export function checkTargets(tree: Tree, schema: Schema) {
if (schema.forceRemove) { if (schema.forceRemove) {
return (tree: Tree) => tree; return;
} }
return updateWorkspaceInTree((workspace) => {
const findTarget = new RegExp(`${schema.projectName}:`);
const usedIn = []; const usedIn = [];
getProjects(tree).forEach((project, projectName) => {
const findTarget = new RegExp(`${schema.projectName}:`);
for (const name of Object.keys(workspace.projects)) { if (projectName === schema.projectName) {
if (name === schema.projectName) { return;
continue;
} }
const projectStr = JSON.stringify(workspace.projects[name]); if (findTarget.test(JSON.stringify(project))) {
usedIn.push(projectName);
if (findTarget.test(projectStr)) {
usedIn.push(name);
}
} }
});
if (usedIn.length > 0) { if (usedIn.length > 0) {
let message = `${schema.projectName} is still targeted by the following projects:\n\n`; let message = `${schema.projectName} is still targeted by the following projects:\n\n`;
for (let project of usedIn) { for (let project of usedIn) {
message += `${project}\n`; message += `${project}\n`;
} }
throw new Error(message); throw new Error(message);
} }
return workspace;
});
} }

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 { readProjectConfiguration, Tree } from '@nrwl/devkit';
import { UnitTestTree } from '@angular-devkit/schematics/testing'; import { createTreeWithEmptyWorkspace } from '@nrwl/devkit/testing';
import { createEmptyWorkspace } from '@nrwl/workspace/testing';
import { runSchematic } from '../../../utils/testing';
import { Schema } from '../schema'; import { Schema } from '../schema';
import { libraryGenerator } from '../../library/library';
import { removeProject } from '@nrwl/workspace/src/schematics/remove/lib/remove-project';
describe('moveProject Rule', () => { describe('moveProject', () => {
let tree: UnitTestTree;
let schema: Schema; let schema: Schema;
let tree: Tree;
beforeEach(async () => { beforeEach(async () => {
tree = createEmptyWorkspace(Tree.empty()) as UnitTestTree; tree = createTreeWithEmptyWorkspace();
tree = await runSchematic('lib', { name: 'my-lib' }, tree); await libraryGenerator(tree, {
name: 'my-lib',
});
schema = { schema = {
projectName: 'my-lib', projectName: 'my-lib',
@ -20,15 +22,7 @@ describe('moveProject Rule', () => {
}); });
it('should delete the project folder', async () => { it('should delete the project folder', async () => {
// TODO - Currently this test will fail due to removeProject(tree, readProjectConfiguration(tree, 'my-lib'));
// https://github.com/angular/angular-cli/issues/16527 expect(tree.children('libs')).not.toContain('my-lib');
// 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();
}); });
}); });

View File

@ -1,22 +1,11 @@
import { SchematicContext, Tree } 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';
import { Schema } from '../schema';
/** /**
* Removes (deletes) a project from the folder tree * Removes (deletes) a project's files from the folder tree
*
* @param schema The options provided to the schematic
*/ */
export function removeProject(schema: Schema) { export function removeProject(tree: Tree, project: ProjectConfiguration) {
return (tree: Tree, _context: SchematicContext): Observable<Tree> => { visitNotIgnoredFiles(tree, project.root, (file) => {
return from(getWorkspace(tree)).pipe( tree.delete(file);
map((workspace) => { });
const project = workspace.projects.get(schema.projectName);
tree.delete(project.root); tree.delete(project.root);
return tree;
})
);
};
} }

View File

@ -1,22 +1,19 @@
import { Tree } from '@angular-devkit/schematics'; import { Tree } from '@nrwl/devkit';
import { UnitTestTree } from '@angular-devkit/schematics/testing'; import { createTreeWithEmptyWorkspace } from '@nrwl/devkit/testing';
import {
callRule,
createEmptyWorkspace,
runSchematic,
} from '@nrwl/workspace/testing';
import { Schema } from '../schema';
import { readFileSync } from 'fs'; import { readFileSync } from 'fs';
import { join } from 'path'; import { join } from 'path';
import { updateJestConfig } from './update-jest-config';
describe('updateRootJestConfig Rule', () => { import { Schema } from '../schema';
let tree: UnitTestTree; import { updateJestConfig } from './update-jest-config';
import { libraryGenerator } from '../../library/library';
describe('updateRootJestConfig', () => {
let tree: Tree;
let schema: Schema; let schema: Schema;
beforeEach(async () => { beforeEach(async () => {
tree = new UnitTestTree(Tree.empty()); tree = createTreeWithEmptyWorkspace();
tree = createEmptyWorkspace(tree) as UnitTestTree;
schema = { schema = {
projectName: 'my-lib', projectName: 'my-lib',
@ -24,22 +21,24 @@ describe('updateRootJestConfig Rule', () => {
forceRemove: false, forceRemove: false,
}; };
tree = await runSchematic('lib', { name: 'my-lib' }, tree); await libraryGenerator(tree, {
name: 'my-lib',
});
tree.overwrite( tree.write(
'jest.config.js', 'jest.config.js',
readFileSync(join(__dirname, './test-files/jest.config.js')).toString() readFileSync(join(__dirname, './test-files/jest.config.js')).toString()
); );
}); });
it('should delete lib project ref from root jest config', async () => { 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(); 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(); expect(updatedJestConfig).toMatchSnapshot();
}); });

View File

@ -1,23 +1,22 @@
import { SchematicContext, Tree } from '@angular-devkit/schematics'; import {
import { getWorkspace, insert, RemoveChange } from '@nrwl/workspace'; applyChangesToString,
ChangeType,
StringChange,
Tree,
} from '@nrwl/devkit';
import * as ts from 'typescript'; import * as ts from 'typescript';
import { getSourceNodes } from '@nrwl/workspace/src/utils/ast-utils'; 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. * Updates the root jest config projects array and removes the project.
*
* @param schema The options provided to the schematic
*/ */
export function updateJestConfig(schema) { export function updateJestConfig(tree: Tree, schema: Schema) {
return (tree: Tree, _context: SchematicContext): Observable<Tree> => {
return from(getWorkspace(tree)).pipe(
map((_) => {
const projectToRemove = schema.projectName; const projectToRemove = schema.projectName;
if (!tree.exists('jest.config.js')) { if (!tree.exists('jest.config.js')) {
return tree; return;
} }
const contents = tree.read('jest.config.js').toString(); const contents = tree.read('jest.config.js').toString();
@ -27,28 +26,21 @@ export function updateJestConfig(schema) {
ts.ScriptTarget.Latest ts.ScriptTarget.Latest
); );
const changes: RemoveChange[] = []; const changes: StringChange[] = [];
const sourceNodes = getSourceNodes(sourceFile); const sourceNodes = getSourceNodes(sourceFile);
sourceNodes.forEach((node, index) => { sourceNodes.forEach((node) => {
if ( if (
ts.isToken(node) && ts.isToken(node) &&
ts.isStringLiteral(node) && ts.isStringLiteral(node) &&
node.text.includes(projectToRemove) node.text.includes(projectToRemove)
) { ) {
changes.push( changes.push({
new RemoveChange( type: ChangeType.Delete,
'jest.config.js', start: node.getStart(sourceFile),
node.getStart(sourceFile), length: node.getFullText(sourceFile).length,
node.getFullText(sourceFile) });
)
);
} }
}); });
insert(tree, 'jest.config.js', changes); tree.write('jest.config.js', applyChangesToString(contents, changes));
return tree;
})
);
};
} }

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 { readJson, readProjectConfiguration, Tree } from '@nrwl/devkit';
import { UnitTestTree } from '@angular-devkit/schematics/testing'; import { createTreeWithEmptyWorkspace } from '@nrwl/devkit/testing';
import { readJsonInTree } from '@nrwl/workspace';
import { createEmptyWorkspace } from '@nrwl/workspace/testing';
import { callRule, runSchematic } from '../../../utils/testing';
import { Schema } from '../schema'; import { Schema } from '../schema';
import { updateTsconfig } from './update-tsconfig'; import { updateTsconfig } from './update-tsconfig';
import { libraryGenerator } from '../../library/library';
describe('updateTsconfig Rule', () => { describe('updateTsconfig', () => {
let tree: UnitTestTree; let tree: Tree;
let schema: Schema; let schema: Schema;
beforeEach(async () => { beforeEach(async () => {
tree = new UnitTestTree(Tree.empty()); tree = createTreeWithEmptyWorkspace();
tree = createEmptyWorkspace(tree) as UnitTestTree;
schema = { schema = {
projectName: 'my-lib', projectName: 'my-lib',
@ -22,16 +19,14 @@ describe('updateTsconfig Rule', () => {
}); });
it('should delete project ref from the tsconfig', async () => { it('should delete project ref from the tsconfig', async () => {
tree = await runSchematic('lib', { name: 'my-lib' }, tree); await libraryGenerator(tree, {
name: 'my-lib',
let tsConfig = readJsonInTree(tree, '/tsconfig.base.json');
expect(tsConfig.compilerOptions.paths).toEqual({
'@proj/my-lib': ['libs/my-lib/src/index.ts'],
}); });
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({}); expect(tsConfig.compilerOptions.paths).toEqual({});
}); });
}); });

View File

@ -1,12 +1,10 @@
import { SchematicContext, Tree } from '@angular-devkit/schematics';
import { import {
getWorkspace, ProjectConfiguration,
NxJson, Tree,
readJsonInTree, updateJson,
serializeJson, getWorkspaceLayout,
} from '@nrwl/workspace'; } from '@nrwl/devkit';
import { from, Observable } from 'rxjs'; import { NxJson, readJsonInTree } from '@nrwl/workspace';
import { map } from 'rxjs/operators';
import { Schema } from '../schema'; import { Schema } from '../schema';
/** /**
@ -14,24 +12,21 @@ import { Schema } from '../schema';
* *
* @param schema The options provided to the schematic * @param schema The options provided to the schematic
*/ */
export function updateTsconfig(schema: Schema) { export function updateTsconfig(
return (tree: Tree, _context: SchematicContext): Observable<Tree> => { tree: Tree,
return from(getWorkspace(tree)).pipe( schema: Schema,
map((workspace) => { project: ProjectConfiguration
const nxJson = readJsonInTree<NxJson>(tree, 'nx.json'); ) {
const project = workspace.projects.get(schema.projectName); const { npmScope } = getWorkspaceLayout(tree);
const tsConfigPath = 'tsconfig.base.json'; const tsConfigPath = 'tsconfig.base.json';
if (tree.exists(tsConfigPath)) { if (tree.exists(tsConfigPath)) {
const tsConfigJson = readJsonInTree(tree, tsConfigPath); updateJson(tree, tsConfigPath, (json) => {
delete tsConfigJson.compilerOptions.paths[ delete json.compilerOptions.paths[
`@${nxJson.npmScope}/${project.root.substr(5)}` `@${npmScope}/${project.root.substr(5)}`
]; ];
tree.overwrite(tsConfigPath, serializeJson(tsConfigJson));
}
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 {
import { checkProjectExists } from '../../utils/rules/check-project-exists'; convertNxGenerator,
import { formatFiles } from '../../utils/rules/format-files'; formatFiles,
readProjectConfiguration,
Tree,
} from '@nrwl/devkit';
import { checkDependencies } from './lib/check-dependencies'; import { checkDependencies } from './lib/check-dependencies';
import { checkTargets } from './lib/check-targets'; import { checkTargets } from './lib/check-targets';
import { removeProject } from './lib/remove-project'; import { removeProject } from './lib/remove-project';
import { updateNxJson } from './lib/update-nx-json';
import { updateTsconfig } from './lib/update-tsconfig'; import { updateTsconfig } from './lib/update-tsconfig';
import { updateWorkspace } from './lib/update-workspace'; import { removeProjectConfig } from './lib/remove-project-config';
import { Schema } from './schema'; import { Schema } from './schema';
import { wrapAngularDevkitSchematic } from '@nrwl/devkit/ngcli-adapter';
import { updateJestConfig } from './lib/update-jest-config'; import { updateJestConfig } from './lib/update-jest-config';
export default function (schema: Schema): Rule { export async function removeGenerator(tree: Tree, schema: Schema) {
return chain([ const project = readProjectConfiguration(tree, schema.projectName);
checkProjectExists(schema), checkDependencies(tree, schema);
checkDependencies(schema), checkTargets(tree, schema);
checkTargets(schema), removeProject(tree, project);
removeProject(schema), removeProjectConfig(tree, schema);
updateNxJson(schema), updateTsconfig(tree, schema, project);
updateTsconfig(schema), updateJestConfig(tree, schema);
updateWorkspace(schema), if (!schema.skipFormat) {
updateJestConfig(schema), await formatFiles(tree);
formatFiles(schema), }
]);
} }
export const removeGenerator = wrapAngularDevkitSchematic( export default removeGenerator;
'@nrwl/workspace',
'remove' export const removeSchematic = convertNxGenerator(removeGenerator);
);

View File

@ -1,6 +1,7 @@
{ {
"$schema": "http://json-schema.org/schema", "$schema": "http://json-schema.org/schema",
"id": "NxWorkspaceRemove", "id": "NxWorkspaceRemove",
"cli": "nx",
"title": "Nx Remove", "title": "Nx Remove",
"description": "Remove a project from the workspace", "description": "Remove a project from the workspace",
"type": "object", "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
);
}