Skip to content

Commit d2a1d85

Browse files
authored
feat(core): add move generator (#588)
1 parent fe639d7 commit d2a1d85

File tree

9 files changed

+327
-2
lines changed

9 files changed

+327
-2
lines changed

docs/core/generators/move.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# @nx-dotnet/core:move
2+
3+
## @nx-dotnet/core:move
4+
5+
Moves {projectName} to {destination}. Renames the Nx project to match the new folder location. Additionally, updates any .csproj, .vbproj, .fsproj, or .sln files which pointed to the project.
6+
7+
## Options
8+
9+
### <span className="required">projectName</span>
10+
11+
- (string): Name of the project to move
12+
13+
### <span className="required">destination</span>
14+
15+
- (string): Where should it be moved to?
16+
17+
### relativeToRoot
18+
19+
- (boolean): If true, the destination path is relative to the root rather than the workspace layout from nx.json

docs/core/index.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,10 @@ Generate a target to extract the swagger.json file from a .NET webapi
125125

126126
Generates typescript code based on a specified openapi/swagger json file
127127

128+
### [move](./generators/move.md)
129+
130+
Moves a dotnet based project and updates project references which pointed to it.
131+
128132
## Executors
129133

130134
### [build](./executors/build.md)

docs/index.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ slug: /
88
## [@nx-dotnet/core](./core)
99

1010
- 7 Executors
11-
- 11 Generators
11+
- 12 Generators
1212

1313
## [@nx-dotnet/nx-ghpages](./nx-ghpages)
1414

packages/core/generators.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,12 @@
6464
"factory": "./src/generators/swagger-typescript/generator",
6565
"schema": "./src/generators/swagger-typescript/schema.json",
6666
"description": "Generates typescript code based on a specified openapi/swagger json file"
67+
},
68+
"move": {
69+
"factory": "./src/generators/move/generator",
70+
"schema": "./src/generators/move/schema.json",
71+
"description": "Moves a dotnet based project and updates project references which pointed to it.",
72+
"alias": "mv"
6773
}
6874
}
6975
}
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
import { createTreeWithEmptyWorkspace } from '@nrwl/devkit/testing';
2+
import {
3+
Tree,
4+
readProjectConfiguration,
5+
addProjectConfiguration,
6+
joinPathFragments,
7+
names,
8+
} from '@nrwl/devkit';
9+
import { uniq } from '@nrwl/nx-plugin/testing';
10+
11+
import generator from './generator';
12+
import { basename } from 'path';
13+
14+
describe('move generator', () => {
15+
let tree: Tree;
16+
17+
beforeEach(() => {
18+
tree = createTreeWithEmptyWorkspace({
19+
layout: 'apps-libs',
20+
});
21+
});
22+
23+
it('should move simple projects successfully', async () => {
24+
const { project } = makeSimpleProject(tree, 'app');
25+
const destination = uniq('app');
26+
await generator(tree, { projectName: project, destination });
27+
const config = readProjectConfiguration(tree, destination);
28+
expect(config).toBeDefined();
29+
expect(tree.exists(`apps/${destination}/readme.md`)).toBeTruthy();
30+
});
31+
32+
it('should move simple projects down a directory', async () => {
33+
const { project } = makeSimpleProject(tree, 'app', 'apps/libs/test');
34+
const destination = uniq('app');
35+
await generator(tree, { projectName: project, destination });
36+
const config = readProjectConfiguration(tree, destination);
37+
expect(config).toBeDefined();
38+
expect(tree.exists(`apps/${destination}/readme.md`)).toBeTruthy();
39+
expect(tree.exists(`apps/libs/test/readme.md`)).toBeFalsy();
40+
});
41+
42+
it('should move simple projects up a directory', async () => {
43+
const { project } = makeSimpleProject(tree, 'app', 'apps/test');
44+
const destination = joinPathFragments('test', 'nested', uniq('app'));
45+
await generator(tree, { projectName: project, destination });
46+
const config = readProjectConfiguration(
47+
tree,
48+
destination.replace(/[\\|/]/g, '-'),
49+
);
50+
expect(config).toBeDefined();
51+
expect(tree.exists(`apps/${destination}/readme.md`)).toBeTruthy();
52+
expect(tree.exists(`apps/test/readme.md`)).toBeFalsy();
53+
});
54+
55+
it('should update references in .csproj files', async () => {
56+
const { project, root } = makeSimpleProject(tree, 'app', 'apps/test');
57+
const csProjPath = 'apps/other/Other.csproj';
58+
tree.write(
59+
csProjPath,
60+
`<Project Sdk="Microsoft.NET.Sdk">
61+
<ItemGroup>
62+
<ProjectReference Include="../test/${names(project).className}.csproj" />
63+
</ItemGroup>
64+
65+
<PropertyGroup>
66+
<TargetFramework>net6.0</TargetFramework>
67+
<RootNamespace>test_lib2</RootNamespace>
68+
<ImplicitUsings>enable</ImplicitUsings>
69+
<Nullable>enable</Nullable>
70+
</PropertyGroup>
71+
</Project>
72+
`,
73+
);
74+
const destination = joinPathFragments(uniq('app'));
75+
console.log(tree.read(csProjPath)?.toString());
76+
await generator(tree, { projectName: project, destination });
77+
const config = readProjectConfiguration(
78+
tree,
79+
destination.replace(/[\\|/]/g, '-'),
80+
);
81+
expect(config).toBeDefined();
82+
expect(tree.exists(`apps/${destination}/readme.md`)).toBeTruthy();
83+
expect(tree.exists(`apps/test/readme.md`)).toBeFalsy();
84+
const updatedCsProj = tree.read(csProjPath)?.toString();
85+
expect(updatedCsProj).not.toContain(root);
86+
expect(updatedCsProj).not.toContain(project);
87+
expect(updatedCsProj).toContain(basename(destination));
88+
});
89+
});
90+
91+
function makeSimpleProject(tree: Tree, type: 'app' | 'lib', path?: string) {
92+
const project = uniq(type);
93+
const root = path ? path.replaceAll('{n}', project) : `${type}s/${project}`;
94+
addProjectConfiguration(tree, project, {
95+
root: root,
96+
projectType: type === 'app' ? 'application' : 'library',
97+
targets: { 'my-target': { executor: 'nx:noop' } },
98+
});
99+
tree.write(joinPathFragments(root, 'readme.md'), 'contents');
100+
return { project, root };
101+
}
Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
import {
2+
addProjectConfiguration,
3+
formatFiles,
4+
getWorkspaceLayout,
5+
joinPathFragments,
6+
names,
7+
normalizePath,
8+
ProjectConfiguration,
9+
readProjectConfiguration,
10+
removeProjectConfiguration,
11+
Tree,
12+
visitNotIgnoredFiles,
13+
} from '@nrwl/devkit';
14+
import { dirname, extname, relative } from 'path';
15+
import { MoveGeneratorSchema } from './schema';
16+
17+
type NormalizedSchema = {
18+
currentRoot: string;
19+
destinationRoot: string;
20+
currentProject: string;
21+
destinationProject: string;
22+
};
23+
24+
function normalizeOptions(
25+
tree: Tree,
26+
options: MoveGeneratorSchema,
27+
): NormalizedSchema {
28+
const { appsDir, libsDir } = getWorkspaceLayout(tree);
29+
const currentRoot = readProjectConfiguration(tree, options.projectName).root;
30+
let destinationRoot = options.destination;
31+
if (!options.relativeToRoot) {
32+
if (currentRoot.startsWith(appsDir)) {
33+
destinationRoot = joinPathFragments(
34+
appsDir,
35+
options.destination.replace(new RegExp(`^${appsDir}`), ''),
36+
);
37+
} else if (currentRoot.startsWith(libsDir)) {
38+
destinationRoot = joinPathFragments(
39+
libsDir,
40+
options.destination.replace(new RegExp(`^${libsDir}`), ''),
41+
);
42+
}
43+
}
44+
45+
return {
46+
currentRoot,
47+
destinationRoot,
48+
currentProject: options.projectName,
49+
destinationProject: names(options.destination).fileName.replace(
50+
/[\\|/]/g,
51+
'-',
52+
),
53+
};
54+
}
55+
56+
export default async function (tree: Tree, options: MoveGeneratorSchema) {
57+
const normalizedOptions = normalizeOptions(tree, options);
58+
const config = readProjectConfiguration(
59+
tree,
60+
normalizedOptions.currentProject,
61+
);
62+
config.root = normalizedOptions.destinationRoot;
63+
config.name = normalizedOptions.destinationProject;
64+
removeProjectConfiguration(tree, normalizedOptions.currentProject);
65+
renameDirectory(
66+
tree,
67+
normalizedOptions.currentRoot,
68+
normalizedOptions.destinationRoot,
69+
);
70+
addProjectConfiguration(
71+
tree,
72+
options.projectName,
73+
transformConfiguration(config, normalizedOptions),
74+
);
75+
updateXmlReferences(tree, normalizedOptions);
76+
await formatFiles(tree);
77+
}
78+
79+
function transformConfiguration(
80+
config: ProjectConfiguration,
81+
options: NormalizedSchema,
82+
) {
83+
return updateReferencesInObject(config, options);
84+
}
85+
86+
function updateReferencesInObject<
87+
// eslint-disable-next-line @typescript-eslint/ban-types
88+
T extends Object | Array<unknown>,
89+
>(object: T, options: NormalizedSchema): T {
90+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
91+
const newValue: any = Array.isArray(object)
92+
? ([] as unknown as T)
93+
: ({} as T);
94+
for (const key in object) {
95+
if (typeof object[key] === 'string') {
96+
newValue[key] = (object[key] as string).replace(
97+
options.currentProject,
98+
options.destinationRoot,
99+
);
100+
} else if (typeof object[key] === 'object') {
101+
newValue[key] = updateReferencesInObject(object[key] as T, options);
102+
} else {
103+
newValue[key] = object[key];
104+
}
105+
}
106+
return newValue;
107+
}
108+
109+
function updateXmlReferences(tree: Tree, options: NormalizedSchema) {
110+
visitNotIgnoredFiles(tree, '.', (path) => {
111+
const extension = extname(path);
112+
const directory = dirname(path);
113+
if (['.csproj', '.vbproj', '.fsproj', '.sln'].includes(extension)) {
114+
const contents = tree.read(path);
115+
if (!contents) {
116+
return;
117+
}
118+
const pathToUpdate = normalizePath(
119+
relative(directory, options.currentRoot),
120+
);
121+
const pathToUpdateWithWindowsSeparators = normalizePath(
122+
relative(directory, options.currentRoot),
123+
).replaceAll('/', '\\');
124+
const newPath = normalizePath(
125+
relative(directory, options.destinationRoot),
126+
);
127+
128+
console.log({ pathToUpdate, newPath });
129+
130+
tree.write(
131+
path,
132+
contents
133+
.toString()
134+
.replaceAll(pathToUpdate, newPath)
135+
.replaceAll(pathToUpdateWithWindowsSeparators, newPath),
136+
);
137+
}
138+
});
139+
}
140+
141+
function renameDirectory(tree: Tree, from: string, to: string) {
142+
const children = tree.children(from);
143+
for (const child of children) {
144+
const childFrom = joinPathFragments(from, child);
145+
const childTo = joinPathFragments(to, child);
146+
if (tree.isFile(childFrom)) {
147+
tree.rename(childFrom, childTo);
148+
} else {
149+
renameDirectory(tree, childFrom, childTo);
150+
}
151+
}
152+
if (!to.startsWith(from)) {
153+
tree.delete(from);
154+
}
155+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
export interface MoveGeneratorSchema {
2+
projectName: string;
3+
destination: string;
4+
relativeToRoot?: string;
5+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
{
2+
"$schema": "http://json-schema.org/schema",
3+
"cli": "nx",
4+
"$id": "Move",
5+
"title": "@nx-dotnet/core:move",
6+
"description": "Moves {projectName} to {destination}. Renames the Nx project to match the new folder location. Additionally, updates any .csproj, .vbproj, .fsproj, or .sln files which pointed to the project.",
7+
"type": "object",
8+
"properties": {
9+
"projectName": {
10+
"type": "string",
11+
"description": "Name of the project to move",
12+
"$default": {
13+
"$source": "argv",
14+
"index": 0
15+
},
16+
"x-prompt": "What name would you like to use?",
17+
"x-dropdown": "projects"
18+
},
19+
"destination": {
20+
"type": "string",
21+
"description": "Where should it be moved to?",
22+
"$default": {
23+
"$source": "argv",
24+
"index": 0
25+
},
26+
"x-prompt": "What name would you like to use?"
27+
},
28+
"relativeToRoot": {
29+
"type": "boolean",
30+
"description": "If true, the destination path is relative to the root rather than the workspace layout from nx.json",
31+
"default": false
32+
}
33+
},
34+
"required": ["projectName", "destination"]
35+
}

tsconfig.base.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
"importHelpers": true,
1111
"target": "es2015",
1212
"module": "esnext",
13-
"lib": ["es2019", "dom"],
13+
"lib": ["es2021", "dom"],
1414
"skipLibCheck": true,
1515
"skipDefaultLibCheck": true,
1616
"baseUrl": ".",

0 commit comments

Comments
 (0)