Skip to content

Commit

Permalink
feat(core): add convert-to-monorepo generator to convert from standal…
Browse files Browse the repository at this point in the history
…one projects (#18245)
  • Loading branch information
jaysoo committed Jul 31, 2023
1 parent 30c3e99 commit dcefa4a
Show file tree
Hide file tree
Showing 20 changed files with 637 additions and 16 deletions.
8 changes: 8 additions & 0 deletions docs/generated/manifests/menus.json
Original file line number Diff line number Diff line change
Expand Up @@ -7897,6 +7897,14 @@
"isExternal": false,
"disableCollapsible": false
},
{
"id": "convert-to-monorepo",
"path": "/packages/workspace/generators/convert-to-monorepo",
"name": "convert-to-monorepo",
"children": [],
"isExternal": false,
"disableCollapsible": false
},
{
"id": "new",
"path": "/packages/workspace/generators/new",
Expand Down
9 changes: 9 additions & 0 deletions docs/generated/manifests/packages.json
Original file line number Diff line number Diff line change
Expand Up @@ -2884,6 +2884,15 @@
"path": "/packages/workspace/generators/remove",
"type": "generator"
},
"/packages/workspace/generators/convert-to-monorepo": {
"description": "Convert a Nx project to a monorepo.",
"file": "generated/packages/workspace/generators/convert-to-monorepo.json",
"hidden": false,
"name": "convert-to-monorepo",
"originalFilePath": "/packages/workspace/src/generators/convert-to-monorepo/schema.json",
"path": "/packages/workspace/generators/convert-to-monorepo",
"type": "generator"
},
"/packages/workspace/generators/new": {
"description": "Create a workspace.",
"file": "generated/packages/workspace/generators/new.json",
Expand Down
9 changes: 9 additions & 0 deletions docs/generated/packages-metadata.json
Original file line number Diff line number Diff line change
Expand Up @@ -2853,6 +2853,15 @@
"path": "workspace/generators/remove",
"type": "generator"
},
{
"description": "Convert a Nx project to a monorepo.",
"file": "generated/packages/workspace/generators/convert-to-monorepo.json",
"hidden": false,
"name": "convert-to-monorepo",
"originalFilePath": "/packages/workspace/src/generators/convert-to-monorepo/schema.json",
"path": "workspace/generators/convert-to-monorepo",
"type": "generator"
},
{
"description": "Create a workspace.",
"file": "generated/packages/workspace/generators/new.json",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
{
"name": "convert-to-monorepo",
"factory": "./src/generators/convert-to-monorepo/convert-to-monorepo",
"schema": {
"$schema": "http://json-schema.org/schema",
"$id": "NxWorkspaceConvertToMonorepo",
"cli": "nx",
"title": "Nx Convert to Monorepo",
"description": "Convert an Nx project to a monorepo.",
"type": "object",
"examples": [
{
"command": "nx g @nx/workspace:monorepo",
"description": "Convert an Nx standalone project to a monorepo."
}
],
"properties": {},
"required": [],
"presets": []
},
"description": "Convert a Nx project to a monorepo.",
"implementation": "/packages/workspace/src/generators/convert-to-monorepo/convert-to-monorepo.ts",
"aliases": [],
"hidden": false,
"path": "/packages/workspace/src/generators/convert-to-monorepo/schema.json",
"type": "generator"
}
1 change: 1 addition & 0 deletions docs/shared/reference/sitemap.md
Original file line number Diff line number Diff line change
Expand Up @@ -613,6 +613,7 @@
- [preset](/packages/workspace/generators/preset)
- [move](/packages/workspace/generators/move)
- [remove](/packages/workspace/generators/remove)
- [convert-to-monorepo](/packages/workspace/generators/convert-to-monorepo)
- [new](/packages/workspace/generators/new)
- [workspace-generator](/packages/workspace/generators/workspace-generator)
- [run-commands](/packages/workspace/generators/run-commands)
Expand Down
26 changes: 26 additions & 0 deletions e2e/nx-misc/src/workspace.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,32 @@ import {

let proj: string;

describe('@nx/workspace:convert-to-monorepo', () => {
beforeEach(() => {
proj = newProject();
});

afterEach(() => cleanupProject());

it('should convert a standalone project to a monorepo', async () => {
const reactApp = uniq('reactapp');
runCLI(
`generate @nx/react:app ${reactApp} --rootProject=true --bundler=webpack --unitTestRunner=jest --e2eTestRunner=cypress --no-interactive`
);

runCLI('generate @nx/workspace:convert-to-monorepo --no-interactive');

checkFilesExist(
`apps/${reactApp}/src/main.tsx`,
`apps/e2e/cypress.config.ts`
);

expect(() => runCLI(`build ${reactApp}`)).not.toThrow();
expect(() => runCLI(`test ${reactApp}`)).not.toThrow();
expect(() => runCLI(`e2e e2e`)).not.toThrow();
});
});

describe('Workspace Tests', () => {
beforeAll(() => {
proj = newProject();
Expand Down
2 changes: 2 additions & 0 deletions packages/jest/src/generators/init/init.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,7 @@ export default {
addProjectConfiguration(tree, 'my-project', {
root: '.',
name: 'my-project',
projectType: 'application',
sourceRoot: 'src',
targets: {
test: {
Expand Down Expand Up @@ -261,6 +262,7 @@ projects: getJestProjects()
root: '.',
name: 'my-project',
sourceRoot: 'src',
projectType: 'application',
targets: {
test: {
executor: '@nx/jest:jest',
Expand Down
9 changes: 6 additions & 3 deletions packages/jest/src/generators/init/init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
GeneratorCallback,
getProjects,
readNxJson,
readProjectConfiguration,
removeDependenciesFromPackageJson,
runTasksInSerial,
stripIndents,
Expand Down Expand Up @@ -100,10 +101,12 @@ function createJestConfig(tree: Tree, options: NormalizedSchema) {
const isProjectConfig = jestTarget?.options?.jestConfig === rootJestPath;
// if root project doesn't have jest target, there's nothing to migrate
if (isProjectConfig) {
const jestAppConfig = `jest.config.app.${options.js ? 'js' : 'ts'}`;
const jestProjectConfig = `jest.config.${
rootProjectConfig.projectType === 'application' ? 'app' : 'lib'
}.${options.js ? 'js' : 'ts'}`;

tree.rename(rootJestPath, jestAppConfig);
jestTarget.options.jestConfig = jestAppConfig;
tree.rename(rootJestPath, jestProjectConfig);
jestTarget.options.jestConfig = jestProjectConfig;
updateProjectConfiguration(tree, rootProject, rootProjectConfig);
}
// generate new global config as it was move to project config or is missing
Expand Down
10 changes: 10 additions & 0 deletions packages/workspace/generators.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,11 @@
"aliases": ["rm"],
"description": "Remove an application or library."
},
"convert-to-monorepo": {
"factory": "./src/generators/convert-to-monorepo/convert-to-monorepo#monorepoSchematic",
"schema": "./src/generators/convert-to-monorepo/schema.json",
"description": "Convert a Nx project to a monorepo."
},
"workspace-generator": {
"factory": "./src/generators/workspace-generator/workspace-generator",
"schema": "./src/generators/workspace-generator/schema.json",
Expand Down Expand Up @@ -53,6 +58,11 @@
"aliases": ["rm"],
"description": "Remove an application or library."
},
"convert-to-monorepo": {
"factory": "./src/generators/convert-to-monorepo/convert-to-monorepo",
"schema": "./src/generators/convert-to-monorepo/schema.json",
"description": "Convert a Nx project to a monorepo."
},
"new": {
"factory": "./src/generators/new/new#newGenerator",
"schema": "./src/generators/new/schema.json",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
import { readJson, readProjectConfiguration, Tree } from '@nx/devkit';
import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing';
import { monorepoGenerator } from './convert-to-monorepo';

// nx-ignore-next-line
const { libraryGenerator } = require('@nx/js');
// nx-ignore-next-line
const { applicationGenerator: reactAppGenerator } = require('@nx/react');
// nx-ignore-next-line
const { applicationGenerator: nextAppGenerator } = require('@nx/next');

describe('monorepo generator', () => {
let tree: Tree;
beforeEach(() => {
tree = createTreeWithEmptyWorkspace();
});

it('should convert root JS lib', async () => {
// Files that should not move
tree.write('.gitignore', '');
tree.write('README.md', '');
tree.write('tools/scripts/custom_script.sh', '');

await libraryGenerator(tree, { name: 'my-lib', rootProject: true });
await libraryGenerator(tree, { name: 'other-lib' });

await monorepoGenerator(tree, {
appsDir: 'apps',
libsDir: 'packages',
});

expect(readJson(tree, 'packages/my-lib/project.json')).toMatchObject({
sourceRoot: 'packages/my-lib/src',
targets: {
build: {
executor: '@nx/js:tsc',
options: {
main: 'packages/my-lib/src/index.ts',
tsConfig: 'packages/my-lib/tsconfig.lib.json',
},
},
},
});
expect(readJson(tree, 'packages/other-lib/project.json')).toMatchObject({
sourceRoot: 'packages/other-lib/src',
});

// Did not move files that don't belong to root project
expect(tree.exists('.gitignore')).toBeTruthy();
expect(tree.exists('README.md')).toBeTruthy();
expect(tree.exists('tools/scripts/custom_script.sh')).toBeTruthy();

// Extracted base config files
expect(tree.exists('tsconfig.base.json')).toBeTruthy();
});

it('should convert root React app (Vite, Vitest)', async () => {
await reactAppGenerator(tree, {
name: 'demo',
style: 'css',
bundler: 'vite',
unitTestRunner: 'vitest',
e2eTestRunner: 'none',
linter: 'eslint',
rootProject: true,
});

await monorepoGenerator(tree, {});

expect(readJson(tree, 'apps/demo/project.json')).toMatchObject({
sourceRoot: 'apps/demo/src',
});

// Extracted base config files
expect(tree.exists('tsconfig.base.json')).toBeTruthy();
expect(tree.exists('.eslintrc.base.json')).toBeTruthy();
});

it('should convert root React app (Webpack, Jest)', async () => {
await reactAppGenerator(tree, {
name: 'demo',
style: 'css',
bundler: 'webpack',
unitTestRunner: 'jest',
e2eTestRunner: 'none',
linter: 'eslint',
rootProject: true,
});

await monorepoGenerator(tree, {});

expect(readJson(tree, 'apps/demo/project.json')).toMatchObject({
sourceRoot: 'apps/demo/src',
targets: {
build: {
executor: '@nx/webpack:webpack',
options: {
main: 'apps/demo/src/main.tsx',
tsConfig: 'apps/demo/tsconfig.app.json',
webpackConfig: 'apps/demo/webpack.config.js',
},
},
test: {
executor: '@nx/jest:jest',
options: {
jestConfig: 'apps/demo/jest.config.app.ts',
},
},
},
});

// Extracted base config files
expect(tree.exists('tsconfig.base.json')).toBeTruthy();
expect(tree.exists('.eslintrc.base.json')).toBeTruthy();
expect(tree.exists('jest.config.ts')).toBeTruthy();
});

it('should convert root Next.js app with existing libraries', async () => {
await nextAppGenerator(tree, {
name: 'demo',
style: 'css',
unitTestRunner: 'jest',
e2eTestRunner: 'none',
appDir: true,
linter: 'eslint',
rootProject: true,
});
await libraryGenerator(tree, { name: 'util' });

await monorepoGenerator(tree, {});

expect(readJson(tree, 'apps/demo/project.json')).toMatchObject({
sourceRoot: 'apps/demo',
});
expect(tree.read('apps/demo/app/page.tsx', 'utf-8')).toContain('demo');
expect(readJson(tree, 'libs/util/project.json')).toMatchObject({
sourceRoot: 'libs/util/src',
});
expect(tree.read('libs/util/src/lib/util.ts', 'utf-8')).toContain('util');

// Extracted base config files
expect(tree.exists('tsconfig.base.json')).toBeTruthy();
expect(tree.exists('.eslintrc.base.json')).toBeTruthy();
expect(tree.exists('jest.config.ts')).toBeTruthy();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import {
convertNxGenerator,
getProjects,
joinPathFragments,
ProjectConfiguration,
readNxJson,
Tree,
updateNxJson,
} from '@nx/devkit';
import { moveGenerator } from '../move/move';

export async function monorepoGenerator(tree: Tree, options: {}) {
const projects = getProjects(tree);

const nxJson = readNxJson(tree);
updateNxJson(tree, nxJson);

let rootProject: ProjectConfiguration;
const projectsToMove: ProjectConfiguration[] = [];

// Need to determine libs vs packages directory base on the type of root project.
for (const [, project] of projects) {
if (project.root === '.') rootProject = project;
projectsToMove.push(project);
}

// Currently, Nx only handles apps+libs or packages. You cannot mix and match them.
// If the standalone project is an app (React, Angular, etc), then use apps+libs.
// Otherwise, for TS standalone (lib), use packages.
const isRootProjectApp = rootProject.projectType === 'application';
const appsDir = isRootProjectApp ? 'apps' : 'packages';
const libsDir = isRootProjectApp ? 'libs' : 'packages';

for (const project of projectsToMove) {
await moveGenerator(tree, {
projectName: project.name,
newProjectName: project.name,
destination:
project.projectType === 'application'
? joinPathFragments(appsDir, project.name)
: joinPathFragments(libsDir, project.name),
destinationRelativeToRoot: true,
updateImportPath: project.projectType === 'library',
});
}
}

export default monorepoGenerator;

export const monorepoSchematic = convertNxGenerator(monorepoGenerator);
16 changes: 16 additions & 0 deletions packages/workspace/src/generators/convert-to-monorepo/schema.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"$schema": "http://json-schema.org/schema",
"$id": "NxWorkspaceConvertToMonorepo",
"cli": "nx",
"title": "Nx Convert to Monorepo",
"description": "Convert an Nx project to a monorepo.",
"type": "object",
"examples": [
{
"command": "nx g @nx/workspace:monorepo",
"description": "Convert an Nx standalone project to a monorepo."
}
],
"properties": {},
"required": []
}

1 comment on commit dcefa4a

@vercel
Copy link

@vercel vercel bot commented on dcefa4a Jul 31, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

nx-dev – ./

nx-dev-nrwl.vercel.app
nx.dev
nx-dev-git-master-nrwl.vercel.app
nx-five.vercel.app

Please sign in to comment.