Skip to content

Commit

Permalink
feat(core): add move generator (#588)
Browse files Browse the repository at this point in the history
  • Loading branch information
AgentEnder authored Jan 14, 2023
1 parent fe639d7 commit d2a1d85
Show file tree
Hide file tree
Showing 9 changed files with 327 additions and 2 deletions.
19 changes: 19 additions & 0 deletions docs/core/generators/move.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# @nx-dotnet/core:move

## @nx-dotnet/core:move

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.

## Options

### <span className="required">projectName</span>

- (string): Name of the project to move

### <span className="required">destination</span>

- (string): Where should it be moved to?

### relativeToRoot

- (boolean): If true, the destination path is relative to the root rather than the workspace layout from nx.json
4 changes: 4 additions & 0 deletions docs/core/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,10 @@ Generate a target to extract the swagger.json file from a .NET webapi

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

### [move](./generators/move.md)

Moves a dotnet based project and updates project references which pointed to it.

## Executors

### [build](./executors/build.md)
Expand Down
2 changes: 1 addition & 1 deletion docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ slug: /
## [@nx-dotnet/core](./core)

- 7 Executors
- 11 Generators
- 12 Generators

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

Expand Down
6 changes: 6 additions & 0 deletions packages/core/generators.json
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,12 @@
"factory": "./src/generators/swagger-typescript/generator",
"schema": "./src/generators/swagger-typescript/schema.json",
"description": "Generates typescript code based on a specified openapi/swagger json file"
},
"move": {
"factory": "./src/generators/move/generator",
"schema": "./src/generators/move/schema.json",
"description": "Moves a dotnet based project and updates project references which pointed to it.",
"alias": "mv"
}
}
}
101 changes: 101 additions & 0 deletions packages/core/src/generators/move/generator.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import { createTreeWithEmptyWorkspace } from '@nrwl/devkit/testing';
import {
Tree,
readProjectConfiguration,
addProjectConfiguration,
joinPathFragments,
names,
} from '@nrwl/devkit';
import { uniq } from '@nrwl/nx-plugin/testing';

import generator from './generator';
import { basename } from 'path';

describe('move generator', () => {
let tree: Tree;

beforeEach(() => {
tree = createTreeWithEmptyWorkspace({
layout: 'apps-libs',
});
});

it('should move simple projects successfully', async () => {
const { project } = makeSimpleProject(tree, 'app');
const destination = uniq('app');
await generator(tree, { projectName: project, destination });
const config = readProjectConfiguration(tree, destination);
expect(config).toBeDefined();
expect(tree.exists(`apps/${destination}/readme.md`)).toBeTruthy();
});

it('should move simple projects down a directory', async () => {
const { project } = makeSimpleProject(tree, 'app', 'apps/libs/test');
const destination = uniq('app');
await generator(tree, { projectName: project, destination });
const config = readProjectConfiguration(tree, destination);
expect(config).toBeDefined();
expect(tree.exists(`apps/${destination}/readme.md`)).toBeTruthy();
expect(tree.exists(`apps/libs/test/readme.md`)).toBeFalsy();
});

it('should move simple projects up a directory', async () => {
const { project } = makeSimpleProject(tree, 'app', 'apps/test');
const destination = joinPathFragments('test', 'nested', uniq('app'));
await generator(tree, { projectName: project, destination });
const config = readProjectConfiguration(
tree,
destination.replace(/[\\|/]/g, '-'),
);
expect(config).toBeDefined();
expect(tree.exists(`apps/${destination}/readme.md`)).toBeTruthy();
expect(tree.exists(`apps/test/readme.md`)).toBeFalsy();
});

it('should update references in .csproj files', async () => {
const { project, root } = makeSimpleProject(tree, 'app', 'apps/test');
const csProjPath = 'apps/other/Other.csproj';
tree.write(
csProjPath,
`<Project Sdk="Microsoft.NET.Sdk">
<ItemGroup>
<ProjectReference Include="../test/${names(project).className}.csproj" />
</ItemGroup>
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<RootNamespace>test_lib2</RootNamespace>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>
`,
);
const destination = joinPathFragments(uniq('app'));
console.log(tree.read(csProjPath)?.toString());
await generator(tree, { projectName: project, destination });
const config = readProjectConfiguration(
tree,
destination.replace(/[\\|/]/g, '-'),
);
expect(config).toBeDefined();
expect(tree.exists(`apps/${destination}/readme.md`)).toBeTruthy();
expect(tree.exists(`apps/test/readme.md`)).toBeFalsy();
const updatedCsProj = tree.read(csProjPath)?.toString();
expect(updatedCsProj).not.toContain(root);
expect(updatedCsProj).not.toContain(project);
expect(updatedCsProj).toContain(basename(destination));
});
});

function makeSimpleProject(tree: Tree, type: 'app' | 'lib', path?: string) {
const project = uniq(type);
const root = path ? path.replaceAll('{n}', project) : `${type}s/${project}`;
addProjectConfiguration(tree, project, {
root: root,
projectType: type === 'app' ? 'application' : 'library',
targets: { 'my-target': { executor: 'nx:noop' } },
});
tree.write(joinPathFragments(root, 'readme.md'), 'contents');
return { project, root };
}
155 changes: 155 additions & 0 deletions packages/core/src/generators/move/generator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
import {
addProjectConfiguration,
formatFiles,
getWorkspaceLayout,
joinPathFragments,
names,
normalizePath,
ProjectConfiguration,
readProjectConfiguration,
removeProjectConfiguration,
Tree,
visitNotIgnoredFiles,
} from '@nrwl/devkit';
import { dirname, extname, relative } from 'path';
import { MoveGeneratorSchema } from './schema';

type NormalizedSchema = {
currentRoot: string;
destinationRoot: string;
currentProject: string;
destinationProject: string;
};

function normalizeOptions(
tree: Tree,
options: MoveGeneratorSchema,
): NormalizedSchema {
const { appsDir, libsDir } = getWorkspaceLayout(tree);
const currentRoot = readProjectConfiguration(tree, options.projectName).root;
let destinationRoot = options.destination;
if (!options.relativeToRoot) {
if (currentRoot.startsWith(appsDir)) {
destinationRoot = joinPathFragments(
appsDir,
options.destination.replace(new RegExp(`^${appsDir}`), ''),
);
} else if (currentRoot.startsWith(libsDir)) {
destinationRoot = joinPathFragments(
libsDir,
options.destination.replace(new RegExp(`^${libsDir}`), ''),
);
}
}

return {
currentRoot,
destinationRoot,
currentProject: options.projectName,
destinationProject: names(options.destination).fileName.replace(
/[\\|/]/g,
'-',
),
};
}

export default async function (tree: Tree, options: MoveGeneratorSchema) {
const normalizedOptions = normalizeOptions(tree, options);
const config = readProjectConfiguration(
tree,
normalizedOptions.currentProject,
);
config.root = normalizedOptions.destinationRoot;
config.name = normalizedOptions.destinationProject;
removeProjectConfiguration(tree, normalizedOptions.currentProject);
renameDirectory(
tree,
normalizedOptions.currentRoot,
normalizedOptions.destinationRoot,
);
addProjectConfiguration(
tree,
options.projectName,
transformConfiguration(config, normalizedOptions),
);
updateXmlReferences(tree, normalizedOptions);
await formatFiles(tree);
}

function transformConfiguration(
config: ProjectConfiguration,
options: NormalizedSchema,
) {
return updateReferencesInObject(config, options);
}

function updateReferencesInObject<
// eslint-disable-next-line @typescript-eslint/ban-types
T extends Object | Array<unknown>,
>(object: T, options: NormalizedSchema): T {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const newValue: any = Array.isArray(object)
? ([] as unknown as T)
: ({} as T);
for (const key in object) {
if (typeof object[key] === 'string') {
newValue[key] = (object[key] as string).replace(
options.currentProject,
options.destinationRoot,
);
} else if (typeof object[key] === 'object') {
newValue[key] = updateReferencesInObject(object[key] as T, options);
} else {
newValue[key] = object[key];
}
}
return newValue;
}

function updateXmlReferences(tree: Tree, options: NormalizedSchema) {
visitNotIgnoredFiles(tree, '.', (path) => {
const extension = extname(path);
const directory = dirname(path);
if (['.csproj', '.vbproj', '.fsproj', '.sln'].includes(extension)) {
const contents = tree.read(path);
if (!contents) {
return;
}
const pathToUpdate = normalizePath(
relative(directory, options.currentRoot),
);
const pathToUpdateWithWindowsSeparators = normalizePath(
relative(directory, options.currentRoot),
).replaceAll('/', '\\');
const newPath = normalizePath(
relative(directory, options.destinationRoot),
);

console.log({ pathToUpdate, newPath });

tree.write(
path,
contents
.toString()
.replaceAll(pathToUpdate, newPath)
.replaceAll(pathToUpdateWithWindowsSeparators, newPath),
);
}
});
}

function renameDirectory(tree: Tree, from: string, to: string) {
const children = tree.children(from);
for (const child of children) {
const childFrom = joinPathFragments(from, child);
const childTo = joinPathFragments(to, child);
if (tree.isFile(childFrom)) {
tree.rename(childFrom, childTo);
} else {
renameDirectory(tree, childFrom, childTo);
}
}
if (!to.startsWith(from)) {
tree.delete(from);
}
}
5 changes: 5 additions & 0 deletions packages/core/src/generators/move/schema.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export interface MoveGeneratorSchema {
projectName: string;
destination: string;
relativeToRoot?: string;
}
35 changes: 35 additions & 0 deletions packages/core/src/generators/move/schema.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
{
"$schema": "http://json-schema.org/schema",
"cli": "nx",
"$id": "Move",
"title": "@nx-dotnet/core:move",
"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.",
"type": "object",
"properties": {
"projectName": {
"type": "string",
"description": "Name of the project to move",
"$default": {
"$source": "argv",
"index": 0
},
"x-prompt": "What name would you like to use?",
"x-dropdown": "projects"
},
"destination": {
"type": "string",
"description": "Where should it be moved to?",
"$default": {
"$source": "argv",
"index": 0
},
"x-prompt": "What name would you like to use?"
},
"relativeToRoot": {
"type": "boolean",
"description": "If true, the destination path is relative to the root rather than the workspace layout from nx.json",
"default": false
}
},
"required": ["projectName", "destination"]
}
2 changes: 1 addition & 1 deletion tsconfig.base.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
"importHelpers": true,
"target": "es2015",
"module": "esnext",
"lib": ["es2019", "dom"],
"lib": ["es2021", "dom"],
"skipLibCheck": true,
"skipDefaultLibCheck": true,
"baseUrl": ".",
Expand Down

0 comments on commit d2a1d85

Please sign in to comment.