diff --git a/docs/generated/manifests/menus.json b/docs/generated/manifests/menus.json index d4ef3e610051c..bf4afe3a9431a 100644 --- a/docs/generated/manifests/menus.json +++ b/docs/generated/manifests/menus.json @@ -9902,6 +9902,14 @@ "children": [], "isExternal": false, "disableCollapsible": false + }, + { + "id": "configuration", + "path": "/nx-api/rsbuild/generators/configuration", + "name": "configuration", + "children": [], + "isExternal": false, + "disableCollapsible": false } ], "isExternal": false, diff --git a/docs/generated/manifests/nx-api.json b/docs/generated/manifests/nx-api.json index c99ddf4f3b537..cf5480dce8714 100644 --- a/docs/generated/manifests/nx-api.json +++ b/docs/generated/manifests/nx-api.json @@ -2909,6 +2909,15 @@ "originalFilePath": "/packages/rsbuild/src/generators/init/schema.json", "path": "/nx-api/rsbuild/generators/init", "type": "generator" + }, + "/nx-api/rsbuild/generators/configuration": { + "description": "Add an Rsbuild configuration for the provided project.", + "file": "generated/packages/rsbuild/generators/configuration.json", + "hidden": false, + "name": "configuration", + "originalFilePath": "/packages/rsbuild/src/generators/configuration/schema.json", + "path": "/nx-api/rsbuild/generators/configuration", + "type": "generator" } }, "path": "/nx-api/rsbuild" diff --git a/docs/generated/packages-metadata.json b/docs/generated/packages-metadata.json index 4e990d688902d..a3925e6a18671 100644 --- a/docs/generated/packages-metadata.json +++ b/docs/generated/packages-metadata.json @@ -2878,6 +2878,15 @@ "originalFilePath": "/packages/rsbuild/src/generators/init/schema.json", "path": "rsbuild/generators/init", "type": "generator" + }, + { + "description": "Add an Rsbuild configuration for the provided project.", + "file": "generated/packages/rsbuild/generators/configuration.json", + "hidden": false, + "name": "configuration", + "originalFilePath": "/packages/rsbuild/src/generators/configuration/schema.json", + "path": "rsbuild/generators/configuration", + "type": "generator" } ], "githubRoot": "https://github.com/nrwl/nx/blob/master", diff --git a/docs/generated/packages/rsbuild/generators/configuration.json b/docs/generated/packages/rsbuild/generators/configuration.json new file mode 100644 index 0000000000000..8a787d4eb4781 --- /dev/null +++ b/docs/generated/packages/rsbuild/generators/configuration.json @@ -0,0 +1,50 @@ +{ + "name": "configuration", + "factory": "./src/generators/configuration/configuration", + "schema": { + "$schema": "http://json-schema.org/schema", + "$id": "Rsbuild", + "title": "Nx Rsbuild Configuration Generator", + "description": "Rsbuild configuration generator.", + "type": "object", + "properties": { + "project": { + "type": "string", + "description": "The name of the project.", + "$default": { "$source": "argv", "index": 0 }, + "x-dropdown": "project", + "x-prompt": "What is the name of the project to set up a Rsbuild for?", + "x-priority": "important" + }, + "entry": { + "type": "string", + "description": "Path relative to the workspace root for the entry file. Defaults to '/src/index.ts'.", + "x-priority": "important" + }, + "tsConfig": { + "type": "string", + "description": "Path relative to the workspace root for the tsconfig file to build with. Defaults to '/tsconfig.app.json'.", + "x-priority": "important" + }, + "target": { + "type": "string", + "description": "Target platform for the build, same as the Rsbuild output.target config option.", + "enum": ["node", "web", "web-worker"], + "default": "web" + }, + "skipFormat": { + "description": "Skip formatting files.", + "type": "boolean", + "default": false, + "x-priority": "internal" + } + }, + "presets": [] + }, + "description": "Add an Rsbuild configuration for the provided project.", + "implementation": "/packages/rsbuild/src/generators/configuration/configuration.ts", + "aliases": [], + "hidden": false, + "path": "/packages/rsbuild/src/generators/configuration/schema.json", + "type": "generator" +} diff --git a/docs/shared/reference/sitemap.md b/docs/shared/reference/sitemap.md index 690e748435dad..72420293bffd5 100644 --- a/docs/shared/reference/sitemap.md +++ b/docs/shared/reference/sitemap.md @@ -688,6 +688,7 @@ - [rsbuild](/nx-api/rsbuild) - [generators](/nx-api/rsbuild/generators) - [init](/nx-api/rsbuild/generators/init) + - [configuration](/nx-api/rsbuild/generators/configuration) - [rspack](/nx-api/rspack) - [documents](/nx-api/rspack/documents) - [Overview](/nx-api/rspack/documents/overview) diff --git a/packages/rsbuild/generators.json b/packages/rsbuild/generators.json index 39f4e88ba36cc..ae1efc4f8681a 100644 --- a/packages/rsbuild/generators.json +++ b/packages/rsbuild/generators.json @@ -8,6 +8,11 @@ "description": "Initialize the `@nx/rsbuild` plugin.", "aliases": ["ng-add"], "hidden": true + }, + "configuration": { + "factory": "./src/generators/configuration/configuration", + "schema": "./src/generators/configuration/schema.json", + "description": "Add an Rsbuild configuration for the provided project." } } } diff --git a/packages/rsbuild/src/generators/configuration/configuration.spec.ts b/packages/rsbuild/src/generators/configuration/configuration.spec.ts new file mode 100644 index 0000000000000..6ec421e2d7396 --- /dev/null +++ b/packages/rsbuild/src/generators/configuration/configuration.spec.ts @@ -0,0 +1,170 @@ +import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing'; +import { type Tree } from '@nx/devkit'; +import configurationGenerator from './configuration'; + +jest.mock('@nx/devkit', () => { + const original = jest.requireActual('@nx/devkit'); + return { + ...original, + createProjectGraphAsync: jest.fn().mockResolvedValue({ + dependencies: {}, + nodes: { + myapp: { + name: 'myapp', + type: 'app', + data: { + root: 'apps/myapp', + sourceRoot: 'apps/myapp/src', + targets: {}, + }, + }, + }, + }), + }; +}); + +describe('Rsbuild configuration generator', () => { + let tree: Tree; + + beforeEach(() => { + tree = createTreeWithEmptyWorkspace(); + tree.write( + 'apps/myapp/project.json', + JSON.stringify({ + name: 'myapp', + projectType: 'application', + root: 'apps/myapp', + sourceRoot: 'apps/myapp/src', + targets: {}, + }) + ); + tree.write( + 'apps/myapp/src/index.ts', + 'export function main() { console.log("Hello world"); }' + ); + }); + + it('should generate Rsbuild configuration files', async () => { + await configurationGenerator(tree, { + project: 'myapp', + skipFormat: true, + }); + + expect(tree.exists('apps/myapp/rsbuild.config.ts')).toBeTruthy(); + expect(tree.read('apps/myapp/rsbuild.config.ts', 'utf-8')) + .toMatchInlineSnapshot(` + "import { defineConfig } from '@rsbuild/core'; + + export default defineConfig({ + source: { + entry: { + index: './src/index.ts' + }, + }, + output: { + target: 'web', + distPath: { + root: 'dist', + }, + } + }); + " + `); + }); + + it('should generate Rsbuild configuration with custom entry file', async () => { + tree.write( + 'apps/myapp/src/main.ts', + 'export function main() { console.log("Hello world"); }' + ); + await configurationGenerator(tree, { + project: 'myapp', + entry: 'src/main.ts', + skipFormat: true, + }); + + expect(tree.exists('apps/myapp/rsbuild.config.ts')).toBeTruthy(); + expect(tree.read('apps/myapp/rsbuild.config.ts', 'utf-8')) + .toMatchInlineSnapshot(` + "import { defineConfig } from '@rsbuild/core'; + + export default defineConfig({ + source: { + entry: { + index: './src/main.ts' + }, + }, + output: { + target: 'web', + distPath: { + root: 'dist', + }, + } + }); + " + `); + }); + + it('should generate Rsbuild configuration with custom entry file with project root path', async () => { + tree.write( + 'apps/myapp/src/main.ts', + 'export function main() { console.log("Hello world"); }' + ); + await configurationGenerator(tree, { + project: 'myapp', + entry: 'apps/myapp/src/main.ts', + skipFormat: true, + }); + + expect(tree.exists('apps/myapp/rsbuild.config.ts')).toBeTruthy(); + expect(tree.read('apps/myapp/rsbuild.config.ts', 'utf-8')) + .toMatchInlineSnapshot(` + "import { defineConfig } from '@rsbuild/core'; + + export default defineConfig({ + source: { + entry: { + index: './src/main.ts' + }, + }, + output: { + target: 'web', + distPath: { + root: 'dist', + }, + } + }); + " + `); + }); + + it('should generate Rsbuild configuration with custom tsconfig file', async () => { + await configurationGenerator(tree, { + project: 'myapp', + tsConfig: 'apps/myapp/tsconfig.json', + skipFormat: true, + }); + + expect(tree.exists('apps/myapp/rsbuild.config.ts')).toBeTruthy(); + expect(tree.read('apps/myapp/rsbuild.config.ts', 'utf-8')) + .toMatchInlineSnapshot(` + "import { defineConfig } from '@rsbuild/core'; + + export default defineConfig({ + source: { + entry: { + index: './src/index.ts' + }, + tsconfigPath: './tsconfig.json', + }, + output: { + target: 'web', + distPath: { + root: 'dist', + }, + } + }); + " + `); + }); +}); diff --git a/packages/rsbuild/src/generators/configuration/configuration.ts b/packages/rsbuild/src/generators/configuration/configuration.ts new file mode 100644 index 0000000000000..85184db79202d --- /dev/null +++ b/packages/rsbuild/src/generators/configuration/configuration.ts @@ -0,0 +1,66 @@ +import { + addDependenciesToPackageJson, + createProjectGraphAsync, + generateFiles, + GeneratorCallback, + readProjectConfiguration, + readProjectsConfigurationFromProjectGraph, + runTasksInSerial, + type Tree, +} from '@nx/devkit'; +import { type Schema } from './schema'; +import { normalizeOptions } from './lib'; +import { initGenerator as jsInitGenerator } from '@nx/js'; +import { initGenerator } from '../init/init'; +import { rsbuildVersion } from '../../utils/versions'; +import { join } from 'path'; + +export async function configurationGenerator(tree: Tree, schema: Schema) { + const projectGraph = await createProjectGraphAsync(); + const projects = readProjectsConfigurationFromProjectGraph(projectGraph); + const project = projects.projects[schema.project]; + if (!project) { + throw new Error( + `Could not find project '${schema.project}'. Please choose a project that exists in the Nx Workspace.` + ); + } + + const options = await normalizeOptions(tree, schema, project); + const tasks: GeneratorCallback[] = []; + + const jsInitTask = await jsInitGenerator(tree, { + ...schema, + skipFormat: true, + tsConfigName: + options.projectRoot === '.' ? 'tsconfig.json' : 'tsconfig.base.json', + }); + tasks.push(jsInitTask); + const initTask = await initGenerator(tree, { skipFormat: true }); + tasks.push(initTask); + + if (options.skipValidation) { + const projectJson = readProjectConfiguration(tree, project.name); + if (projectJson.targets['build']) { + delete projectJson.targets['build']; + } + if (projectJson.targets['serve']) { + delete projectJson.targets['serve']; + } + if (projectJson.targets['dev']) { + delete projectJson.targets['dev']; + } + } + + tasks.push( + addDependenciesToPackageJson(tree, {}, { '@rsbuild/core': rsbuildVersion }) + ); + + generateFiles(tree, join(__dirname, 'files'), options.projectRoot, { + ...options, + tpl: '', + }); + + return runTasksInSerial(...tasks); +} + +export default configurationGenerator; diff --git a/packages/rsbuild/src/generators/configuration/files/rsbuild.config.ts__tpl__ b/packages/rsbuild/src/generators/configuration/files/rsbuild.config.ts__tpl__ new file mode 100644 index 0000000000000..88c2d5def6980 --- /dev/null +++ b/packages/rsbuild/src/generators/configuration/files/rsbuild.config.ts__tpl__ @@ -0,0 +1,16 @@ +import { defineConfig } from '@rsbuild/core'; + +export default defineConfig({ + source: { + entry: { + index: '<%= entry %>' + },<% if (tsConfig) { %> + tsconfigPath: '<%= tsConfig %>',<% } %> + }, + output: { + target: '<%= target %>', + distPath: { + root: 'dist', + }, + } +}); diff --git a/packages/rsbuild/src/generators/configuration/lib/index.ts b/packages/rsbuild/src/generators/configuration/lib/index.ts new file mode 100644 index 0000000000000..f9451963f55a2 --- /dev/null +++ b/packages/rsbuild/src/generators/configuration/lib/index.ts @@ -0,0 +1 @@ +export * from './normalize-options'; diff --git a/packages/rsbuild/src/generators/configuration/lib/normalize-options.ts b/packages/rsbuild/src/generators/configuration/lib/normalize-options.ts new file mode 100644 index 0000000000000..5fcc1150b4a72 --- /dev/null +++ b/packages/rsbuild/src/generators/configuration/lib/normalize-options.ts @@ -0,0 +1,61 @@ +import { + joinPathFragments, + type Tree, + type ProjectConfiguration, +} from '@nx/devkit'; +import { type Schema } from '../schema'; +import { relative } from 'path'; + +export interface NormalizedOptions extends Schema { + entry: string; + target: 'node' | 'web' | 'web-worker'; + tsConfig: string; + projectRoot: string; +} + +export async function normalizeOptions( + tree: Tree, + schema: Schema, + project: ProjectConfiguration +) { + // Paths should be relative to the project root because inferred task will run from project root + let options: NormalizedOptions = { + ...schema, + target: schema.target ?? 'web', + entry: normalizeRelativePath( + schema.entry ?? './src/index.ts', + project.root + ), + tsConfig: normalizeRelativePath( + schema.tsConfig ?? './tsconfig.json', + project.root + ), + projectRoot: project.root, + skipFormat: schema.skipFormat ?? false, + skipValidation: schema.skipValidation ?? false, + }; + + if (!schema.tsConfig) { + const possibleTsConfigPaths = [ + './tsconfig.app.json', + './tsconfig.lib.json', + './tsconfig.json', + ]; + const tsConfigPath = possibleTsConfigPaths.find((p) => + tree.exists(joinPathFragments(project.root, p)) + ); + options.tsConfig = tsConfigPath ?? undefined; + } + + return options; +} + +function normalizeRelativePath(filePath: string, projectRoot: string) { + if (filePath.startsWith('./')) { + return filePath; + } + filePath = filePath.startsWith(projectRoot) + ? relative(projectRoot, filePath) + : filePath; + return `./${filePath}`; +} diff --git a/packages/rsbuild/src/generators/configuration/schema.d.ts b/packages/rsbuild/src/generators/configuration/schema.d.ts new file mode 100644 index 0000000000000..6af6677d2b687 --- /dev/null +++ b/packages/rsbuild/src/generators/configuration/schema.d.ts @@ -0,0 +1,8 @@ +export interface Schema { + project: string; + entry?: string; + tsConfig?: string; + target?: 'node' | 'web' | 'web-worker'; + skipValidation?: boolean; + skipFormat?: boolean; +} diff --git a/packages/rsbuild/src/generators/configuration/schema.json b/packages/rsbuild/src/generators/configuration/schema.json new file mode 100644 index 0000000000000..d85164e6f1249 --- /dev/null +++ b/packages/rsbuild/src/generators/configuration/schema.json @@ -0,0 +1,42 @@ +{ + "$schema": "http://json-schema.org/schema", + "$id": "Rsbuild", + "title": "Nx Rsbuild Configuration Generator", + "description": "Rsbuild configuration generator.", + "type": "object", + "properties": { + "project": { + "type": "string", + "description": "The name of the project.", + "$default": { + "$source": "argv", + "index": 0 + }, + "x-dropdown": "project", + "x-prompt": "What is the name of the project to set up a Rsbuild for?", + "x-priority": "important" + }, + "entry": { + "type": "string", + "description": "Path relative to the workspace root for the entry file. Defaults to '/src/index.ts'.", + "x-priority": "important" + }, + "tsConfig": { + "type": "string", + "description": "Path relative to the workspace root for the tsconfig file to build with. Defaults to '/tsconfig.app.json'.", + "x-priority": "important" + }, + "target": { + "type": "string", + "description": "Target platform for the build, same as the Rsbuild output.target config option.", + "enum": ["node", "web", "web-worker"], + "default": "web" + }, + "skipFormat": { + "description": "Skip formatting files.", + "type": "boolean", + "default": false, + "x-priority": "internal" + } + } +}