-
Notifications
You must be signed in to change notification settings - Fork 2.4k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(core): add monorepo generator to convert from standalone projects
- Loading branch information
Showing
11 changed files
with
566 additions
and
26 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
149 changes: 149 additions & 0 deletions
149
packages/workspace/src/generators/monorepo/monorepo.spec.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,149 @@ | ||
import { readJson, Tree } from '@nx/devkit'; | ||
import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing'; | ||
import { monorepoGenerator } from './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, { | ||
appsDir: 'apps', | ||
libsDir: 'libs', | ||
}); | ||
|
||
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, { | ||
appsDir: 'apps', | ||
libsDir: 'libs', | ||
}); | ||
|
||
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', | ||
}, | ||
}, | ||
}, | ||
}); | ||
|
||
// Extracted base config files | ||
expect(tree.exists('tsconfig.base.json')).toBeTruthy(); | ||
expect(tree.exists('.eslintrc.base.json')).toBeTruthy(); | ||
expect(tree.exists('jest.config.app.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, { | ||
appsDir: 'apps', | ||
libsDir: 'libs', | ||
}); | ||
|
||
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.app.ts')).toBeTruthy(); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,76 @@ | ||
import { | ||
convertNxGenerator, | ||
getProjects, | ||
joinPathFragments, | ||
ProjectConfiguration, | ||
readNxJson, | ||
Tree, | ||
updateNxJson, | ||
} from '@nx/devkit'; | ||
import { moveGenerator } from '../move/move'; | ||
import { Schema } from './schema'; | ||
|
||
export async function monorepoGenerator(tree: Tree, options: Schema) { | ||
const projects = getProjects(tree); | ||
const { extractTsConfigBase } = require('@nx/' + 'js'); | ||
extractTsConfigBase(tree); | ||
|
||
maybeExtractJestConfigBase(tree); | ||
|
||
const nxJson = readNxJson(tree); | ||
nxJson.workspaceLayout = { | ||
// normalize paths without trailing slash | ||
appsDir: joinPathFragments(options.appsDir), | ||
libsDir: joinPathFragments(options.libsDir), | ||
}; | ||
|
||
updateNxJson(tree, nxJson); | ||
|
||
for (const [_name, project] of projects) { | ||
maybeExtractEslintConfigIfRootProject(tree, project); | ||
await moveGenerator(tree, { | ||
projectName: project.name, | ||
destination: project.name, | ||
updateImportPath: project.projectType === 'library', | ||
}); | ||
} | ||
} | ||
|
||
function maybeExtractJestConfigBase(tree: Tree): void { | ||
let jestInitGenerator: any; | ||
try { | ||
jestInitGenerator = require('@nx/' + 'jest').jestInitGenerator; | ||
} catch { | ||
// not installed | ||
} | ||
jestInitGenerator?.(tree, {}); | ||
} | ||
|
||
function maybeExtractEslintConfigIfRootProject( | ||
tree: Tree, | ||
rootProject: ProjectConfiguration | ||
): void { | ||
if (rootProject.root !== '.') return; | ||
if (tree.exists('.eslintrc.base.json')) return; | ||
let migrateConfigToMonorepoStyle: any; | ||
try { | ||
migrateConfigToMonorepoStyle = require('@nx/' + | ||
'linter/src/generators/init/init-migration').migrateConfigToMonorepoStyle; | ||
} catch { | ||
// linter not install | ||
} | ||
// Only need to handle migrating the root rootProject. | ||
// If other libs/apps exist, then this migration is already done by `@nx/linter:lint-rootProject` generator. | ||
migrateConfigToMonorepoStyle?.( | ||
[rootProject.name], | ||
tree, | ||
tree.exists(joinPathFragments(rootProject.root, 'jest.config.ts')) || | ||
tree.exists(joinPathFragments(rootProject.root, 'jest.config.js')) | ||
? 'jest' | ||
: 'none' | ||
); | ||
} | ||
|
||
export default monorepoGenerator; | ||
|
||
export const monorepoSchematic = convertNxGenerator(monorepoGenerator); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
export interface Schema { | ||
appsDir: string; | ||
libsDir: string; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
{ | ||
"$schema": "http://json-schema.org/schema", | ||
"$id": "NxWorkspaceMonorepo", | ||
"cli": "nx", | ||
"title": "Nx Monorepo", | ||
"description": "Convert a Nx project to a monorepo.", | ||
"type": "object", | ||
"examples": [ | ||
{ | ||
"command": "nx g @nx/workspace:monorepo --appsDir=apps --libsDir=packages", | ||
"description": "Convert to a monorepo with apps and libs directories." | ||
} | ||
], | ||
"properties": { | ||
"appsDir": { | ||
"type": "string", | ||
"description": "The directory where apps are placed.", | ||
"default": "apps" | ||
}, | ||
"libsDir": { | ||
"type": "string", | ||
"alias": "packagesDir", | ||
"description": "The directory where libs are placed.", | ||
"default": "libs" | ||
} | ||
}, | ||
"required": ["appsDir", "libsDir"] | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.