diff --git a/docs/generated/manifests/menus.json b/docs/generated/manifests/menus.json index 0536857145d0fd..d20ddea9638027 100644 --- a/docs/generated/manifests/menus.json +++ b/docs/generated/manifests/menus.json @@ -9889,7 +9889,25 @@ "id": "rsbuild", "path": "/nx-api/rsbuild", "name": "rsbuild", - "children": [], + "children": [ + { + "id": "generators", + "path": "/nx-api/rsbuild/generators", + "name": "generators", + "children": [ + { + "id": "init", + "path": "/nx-api/rsbuild/generators/init", + "name": "init", + "children": [], + "isExternal": false, + "disableCollapsible": false + } + ], + "isExternal": false, + "disableCollapsible": false + } + ], "isExternal": false, "disableCollapsible": false }, diff --git a/docs/generated/manifests/nx-api.json b/docs/generated/manifests/nx-api.json index 0b1b466538cc0a..c99ddf4f3b5372 100644 --- a/docs/generated/manifests/nx-api.json +++ b/docs/generated/manifests/nx-api.json @@ -2900,7 +2900,17 @@ "root": "/packages/rsbuild", "source": "/packages/rsbuild/src", "executors": {}, - "generators": {}, + "generators": { + "/nx-api/rsbuild/generators/init": { + "description": "Initialize the `@nx/rsbuild` plugin.", + "file": "generated/packages/rsbuild/generators/init.json", + "hidden": true, + "name": "init", + "originalFilePath": "/packages/rsbuild/src/generators/init/schema.json", + "path": "/nx-api/rsbuild/generators/init", + "type": "generator" + } + }, "path": "/nx-api/rsbuild" }, "rspack": { diff --git a/docs/generated/packages-metadata.json b/docs/generated/packages-metadata.json index bb64df1836f166..4e990d688902df 100644 --- a/docs/generated/packages-metadata.json +++ b/docs/generated/packages-metadata.json @@ -2869,7 +2869,17 @@ "description": "The Nx Plugin for Rsbuild contains an Nx plugin, executors and utilities that support building applications using Rsbuild.", "documents": [], "executors": [], - "generators": [], + "generators": [ + { + "description": "Initialize the `@nx/rsbuild` plugin.", + "file": "generated/packages/rsbuild/generators/init.json", + "hidden": true, + "name": "init", + "originalFilePath": "/packages/rsbuild/src/generators/init/schema.json", + "path": "rsbuild/generators/init", + "type": "generator" + } + ], "githubRoot": "https://github.com/nrwl/nx/blob/master", "name": "rsbuild", "packageName": "@nx/rsbuild", diff --git a/docs/generated/packages/rsbuild/generators/init.json b/docs/generated/packages/rsbuild/generators/init.json new file mode 100644 index 00000000000000..385d83e216bad2 --- /dev/null +++ b/docs/generated/packages/rsbuild/generators/init.json @@ -0,0 +1,44 @@ +{ + "name": "init", + "factory": "./src/generators/init/init#initGeneratorInternal", + "schema": { + "$schema": "http://json-schema.org/schema", + "$id": "Init", + "title": "Nx Rsbuild Init Generator", + "type": "object", + "description": "Rsbuild init generator.", + "properties": { + "rootProject": { "type": "boolean", "x-priority": "internal" }, + "keepExistingVersions": { + "type": "boolean", + "x-priority": "internal", + "description": "Keep existing dependencies versions", + "default": false + }, + "updatePackageScripts": { + "type": "boolean", + "x-priority": "internal", + "description": "Update package scripts", + "default": false + }, + "skipFormat": { + "description": "Skip formatting files.", + "type": "boolean", + "default": false + }, + "skipPackageJson": { + "description": "Do not add dependencies to `package.json`.", + "type": "boolean", + "default": false + } + }, + "required": [], + "presets": [] + }, + "description": "Initialize the `@nx/rsbuild` plugin.", + "aliases": ["ng-add"], + "hidden": true, + "implementation": "/packages/rsbuild/src/generators/init/init#initGeneratorInternal.ts", + "path": "/packages/rsbuild/src/generators/init/schema.json", + "type": "generator" +} diff --git a/docs/shared/reference/sitemap.md b/docs/shared/reference/sitemap.md index 9081a0ecc19610..a89165294abd18 100644 --- a/docs/shared/reference/sitemap.md +++ b/docs/shared/reference/sitemap.md @@ -686,6 +686,8 @@ - [configuration](/nx-api/rollup/generators/configuration) - [convert-to-inferred](/nx-api/rollup/generators/convert-to-inferred) - [rsbuild](/nx-api/rsbuild) + - [generators](/nx-api/rsbuild/generators) + - [init](/nx-api/rsbuild/generators/init) - [rspack](/nx-api/rspack) - [documents](/nx-api/rspack/documents) - [Overview](/nx-api/rspack/documents/overview) diff --git a/package.json b/package.json index 0e15fb9acb0aba..4ad596ba24b377 100644 --- a/package.json +++ b/package.json @@ -102,6 +102,7 @@ "@rollup/plugin-node-resolve": "^15.2.3", "@rollup/plugin-url": "^8.0.2", "@rspack/core": "^1.1.5", + "@rsbuild/core": "1.1.8", "@rspack/dev-server": "1.0.9", "@rspack/plugin-minify": "^0.7.5", "@rspack/plugin-react-refresh": "^1.0.0", diff --git a/packages/nx/src/command-line/init/init-v2.ts b/packages/nx/src/command-line/init/init-v2.ts index 711757a447afd0..33e29ced07540c 100644 --- a/packages/nx/src/command-line/init/init-v2.ts +++ b/packages/nx/src/command-line/init/init-v2.ts @@ -179,6 +179,7 @@ const npmPackageToPluginMap: Record = { nuxt: '@nx/nuxt', 'react-native': '@nx/react-native', '@remix-run/dev': '@nx/remix', + '@rsbuild/core': '@nx/rsbuild', }; export async function detectPlugins( diff --git a/packages/rsbuild/generators.json b/packages/rsbuild/generators.json new file mode 100644 index 00000000000000..39f4e88ba36cc6 --- /dev/null +++ b/packages/rsbuild/generators.json @@ -0,0 +1,13 @@ +{ + "name": "Nx Rsbuild", + "version": "0.1", + "generators": { + "init": { + "factory": "./src/generators/init/init#initGeneratorInternal", + "schema": "./src/generators/init/schema.json", + "description": "Initialize the `@nx/rsbuild` plugin.", + "aliases": ["ng-add"], + "hidden": true + } + } +} diff --git a/packages/rsbuild/package.json b/packages/rsbuild/package.json index bf26aab9b403ab..b4aa85c237ea07 100644 --- a/packages/rsbuild/package.json +++ b/packages/rsbuild/package.json @@ -26,9 +26,14 @@ "license": "MIT", "homepage": "https://nx.dev", "main": "index.js", + "generators": "./generators.json", "executors": "./executors.json", "dependencies": { - "tslib": "^2.3.0" + "tslib": "^2.3.0", + "@nx/devkit": "file:../devkit", + "@nx/js": "file:../js", + "@rsbuild/core": "1.1.8", + "minimatch": "9.0.3" }, "peerDependencies": {}, "nx-migrations": { diff --git a/packages/rsbuild/plugin.ts b/packages/rsbuild/plugin.ts new file mode 100644 index 00000000000000..9d72a71f155370 --- /dev/null +++ b/packages/rsbuild/plugin.ts @@ -0,0 +1 @@ +export { createNodesV2, RsbuildPluginOptions } from './src/plugins/plugin'; diff --git a/packages/rsbuild/src/generators/init/init.ts b/packages/rsbuild/src/generators/init/init.ts new file mode 100644 index 00000000000000..b070638d1d0b73 --- /dev/null +++ b/packages/rsbuild/src/generators/init/init.ts @@ -0,0 +1,75 @@ +import { + type Tree, + type GeneratorCallback, + readNxJson, + createProjectGraphAsync, + addDependenciesToPackageJson, + formatFiles, + runTasksInSerial, +} from '@nx/devkit'; +import { addPlugin } from '@nx/devkit/src/utils/add-plugin'; +import { InitGeneratorSchema } from './schema'; +import { createNodesV2 } from '../../plugins/plugin'; +import { nxVersion, rsbuildVersion } from '../../utils/versions'; + +export function updateDependencies(tree: Tree, schema: InitGeneratorSchema) { + return addDependenciesToPackageJson( + tree, + {}, + { + '@nx/rsbuild': nxVersion, + '@rsbuild/core': rsbuildVersion, + }, + undefined, + schema.keepExistingVersions + ); +} + +export function initGenerator(tree: Tree, schema: InitGeneratorSchema) { + return initGeneratorInternal(tree, { addPlugin: false, ...schema }); +} + +export async function initGeneratorInternal( + tree: Tree, + schema: InitGeneratorSchema +) { + const nxJson = readNxJson(tree); + const addPluginDefault = + process.env.NX_ADD_PLUGINS !== 'false' && + nxJson.useInferencePlugins !== false; + schema.addPlugin ??= addPluginDefault; + + if (schema.addPlugin) { + await addPlugin( + tree, + await createProjectGraphAsync(), + '@nx/rsbuild/plugin', + createNodesV2, + { + buildTargetName: ['build', 'rsbuild:build', 'rsbuild-build'], + devTargetName: ['dev', 'rsbuild:dev', 'rsbuild-dev'], + previewTargetName: ['preview', 'rsbuild:preview', 'rsbuild-preview'], + inspectTargetName: ['inspect', 'rsbuild:inspect', 'rsbuild-inspect'], + typecheckTargetName: [ + 'typecheck', + 'rsbuild:typecheck', + 'rsbuild-typecheck', + ], + }, + schema.updatePackageScripts + ); + } + + const tasks: GeneratorCallback[] = []; + if (!schema.skipPackageJson) { + tasks.push(updateDependencies(tree, schema)); + } + + if (!schema.skipFormat) { + await formatFiles(tree); + } + + return runTasksInSerial(...tasks); +} + +export default initGenerator; diff --git a/packages/rsbuild/src/generators/init/schema.d.ts b/packages/rsbuild/src/generators/init/schema.d.ts new file mode 100644 index 00000000000000..adf5cd7eb4191d --- /dev/null +++ b/packages/rsbuild/src/generators/init/schema.d.ts @@ -0,0 +1,7 @@ +export interface InitGeneratorSchema { + keepExistingVersions?: boolean; + updatePackageScripts?: boolean; + addPlugin?: boolean; + skipFormat?: boolean; + skipPackageJson?: boolean; +} diff --git a/packages/rsbuild/src/generators/init/schema.json b/packages/rsbuild/src/generators/init/schema.json new file mode 100644 index 00000000000000..8222d49965a1c2 --- /dev/null +++ b/packages/rsbuild/src/generators/init/schema.json @@ -0,0 +1,36 @@ +{ + "$schema": "http://json-schema.org/schema", + "$id": "Init", + "title": "Nx Rsbuild Init Generator", + "type": "object", + "description": "Rsbuild init generator.", + "properties": { + "rootProject": { + "type": "boolean", + "x-priority": "internal" + }, + "keepExistingVersions": { + "type": "boolean", + "x-priority": "internal", + "description": "Keep existing dependencies versions", + "default": false + }, + "updatePackageScripts": { + "type": "boolean", + "x-priority": "internal", + "description": "Update package scripts", + "default": false + }, + "skipFormat": { + "description": "Skip formatting files.", + "type": "boolean", + "default": false + }, + "skipPackageJson": { + "description": "Do not add dependencies to `package.json`.", + "type": "boolean", + "default": false + } + }, + "required": [] +} diff --git a/packages/rsbuild/src/plugins/plugin.ts b/packages/rsbuild/src/plugins/plugin.ts new file mode 100644 index 00000000000000..2d03d46a2f138c --- /dev/null +++ b/packages/rsbuild/src/plugins/plugin.ts @@ -0,0 +1,296 @@ +import { + type ProjectConfiguration, + type TargetConfiguration, + readJsonFile, + writeJsonFile, + CreateNodesV2, + CreateNodesContext, + createNodesFromFiles, + joinPathFragments, + getPackageManagerCommand, + detectPackageManager, +} from '@nx/devkit'; +import { getNamedInputs } from '@nx/devkit/src/utils/get-named-inputs'; +import { hashObject } from 'nx/src/hasher/file-hasher'; +import { workspaceDataDirectory } from 'nx/src/utils/cache-directory'; +import { isUsingTsSolutionSetup as _isUsingTsSolutionSetup } from '@nx/js/src/utils/typescript/ts-solution-setup'; +import { calculateHashForCreateNodes } from '@nx/devkit/src/utils/calculate-hash-for-create-nodes'; +import { getLockFileName } from '@nx/js'; +import { existsSync, readdirSync } from 'fs'; +import { join, dirname, isAbsolute, relative } from 'path'; +import { minimatch } from 'minimatch'; +import { loadConfig, type RsbuildConfig } from '@rsbuild/core'; + +const pmc = getPackageManagerCommand(); + +export interface RsbuildPluginOptions { + buildTargetName?: string; + devTargetName?: string; + previewTargetName?: string; + inspectTargetName?: string; + typecheckTargetName?: string; +} + +type RsbuildTargets = Pick; + +function readTargetsCache(cachePath: string): Record { + return existsSync(cachePath) ? readJsonFile(cachePath) : {}; +} + +function writeTargetsCache( + cachePath, + results?: Record +) { + writeJsonFile(cachePath, results); +} + +const rsbuildConfigGlob = '**/rsbuild.config.{js,ts,mjs,mts,cjs,cts}'; + +export const createNodesV2: CreateNodesV2 = [ + rsbuildConfigGlob, + async (configFilePaths, options, context) => { + const optionsHash = hashObject(options); + const cachePath = join( + workspaceDataDirectory, + `rsbuild-${optionsHash}.hash` + ); + const targetsCache = readTargetsCache(cachePath); + const isUsingTsSolutionSetup = _isUsingTsSolutionSetup(); + try { + return await createNodesFromFiles( + (configFile, options, context) => + createNodesInternal( + configFile, + options, + context, + targetsCache, + isUsingTsSolutionSetup + ), + configFilePaths, + options, + context + ); + } finally { + writeTargetsCache(cachePath, targetsCache); + } + }, +]; + +async function createNodesInternal( + configFilePath: string, + options: RsbuildPluginOptions, + context: CreateNodesContext, + targetsCache: Record, + isUsingTsSolutionSetup: boolean +) { + const projectRoot = dirname(configFilePath); + // Do not create a project if package.json and project.json isn't there. + const siblingFiles = readdirSync(join(context.workspaceRoot, projectRoot)); + if ( + !siblingFiles.includes('package.json') && + !siblingFiles.includes('project.json') + ) { + return {}; + } + + const tsConfigFiles = + siblingFiles.filter((p) => minimatch(p, 'tsconfig*{.json,.*.json}')) ?? []; + + const normalizedOptions = normalizeOptions(options); + const hash = await calculateHashForCreateNodes( + projectRoot, + normalizedOptions, + context, + [getLockFileName(detectPackageManager(context.workspaceRoot))] + ); + + targetsCache[hash] ??= await createRsbuildTargets( + configFilePath, + projectRoot, + normalizedOptions, + tsConfigFiles, + isUsingTsSolutionSetup, + context + ); + + const { targets, metadata } = targetsCache[hash]; + + return { + projects: { + [projectRoot]: { + root: projectRoot, + targets, + metadata, + }, + }, + }; +} + +async function createRsbuildTargets( + configFilePath: string, + projectRoot: string, + options: RsbuildPluginOptions, + tsConfigFiles: string[], + isUsingTsSolutionSetup: boolean, + context: CreateNodesContext +): Promise { + const absoluteConfigFilePath = joinPathFragments( + context.workspaceRoot, + configFilePath + ); + + const rsbuildConfig = await loadConfig({ + path: absoluteConfigFilePath, + }); + if (!rsbuildConfig.filePath) { + return { targets: {}, metadata: {} }; + } + + const namedInputs = getNamedInputs(projectRoot, context); + const { buildOutputs } = getOutputs( + rsbuildConfig.content, + projectRoot, + context.workspaceRoot + ); + + const targets: Record = {}; + + targets[options.buildTargetName] = { + command: `rsbuild build`, + options: { cwd: projectRoot, args: ['--mode=production'] }, + cache: true, + dependsOn: [`^${options.buildTargetName}`], + inputs: [ + ...('production' in namedInputs + ? ['production', '^production'] + : ['default', '^default']), + { + externalDependencies: ['@rsbuild/core'], + }, + ], + outputs: buildOutputs, + metadata: { + technologies: ['rsbuild'], + description: `Run Rsbuild build`, + help: { + command: `${pmc.exec} rsbuild build --help`, + example: { + options: { + watch: false, + }, + }, + }, + }, + }; + + targets[options.devTargetName] = { + command: `rsbuild dev`, + options: { + cwd: projectRoot, + args: ['--mode=development'], + }, + }; + + targets[options.previewTargetName] = { + command: `rsbuild preview`, + options: { + cwd: projectRoot, + args: ['--mode=production'], + }, + }; + + targets[options.inspectTargetName] = { + command: `rsbuild inspect`, + options: { + cwd: projectRoot, + }, + }; + + if (tsConfigFiles.length) { + const tsConfigToUse = + ['tsconfig.app.json', 'tsconfig.lib.json', 'tsconfig.json'].find((t) => + tsConfigFiles.includes(t) + ) ?? tsConfigFiles[0]; + targets[options.typecheckTargetName] = { + cache: true, + inputs: [ + ...('production' in namedInputs + ? ['production', '^production'] + : ['default', '^default']), + { externalDependencies: ['typescript'] }, + ], + command: isUsingTsSolutionSetup + ? `tsc --build --emitDeclarationOnly --pretty --verbose` + : `tsc --noEmit -p ${tsConfigToUse}`, + options: { cwd: joinPathFragments(projectRoot) }, + metadata: { + description: `Run Typechecking`, + help: { + command: `${pmc.exec} tsc --help -p ${tsConfigToUse}`, + example: { + options: { + noEmit: true, + }, + }, + }, + }, + }; + } + + return { targets, metadata: {} }; +} + +function getOutputs( + rsbuildConfig: RsbuildConfig, + projectRoot: string, + workspaceRoot: string +): { buildOutputs: string[] } { + const { output, dev } = rsbuildConfig; + const buildOutputPath = normalizeOutputPath( + output?.distPath?.root ? dirname(output.distPath.root) : undefined, + projectRoot, + workspaceRoot, + 'dist' + ); + + const hasServeConfig = Boolean(dev); + + return { + buildOutputs: [buildOutputPath], + }; +} + +function normalizeOutputPath( + outputPath: string | undefined, + projectRoot: string, + workspaceRoot: string, + path: 'dist' +): string | undefined { + if (!outputPath) { + if (projectRoot === '.') { + return `{projectRoot}/${path}`; + } else { + return `{workspaceRoot}/${path}/{projectRoot}`; + } + } else { + if (isAbsolute(outputPath)) { + return `{workspaceRoot}/${relative(workspaceRoot, outputPath)}`; + } else { + if (outputPath.startsWith('..')) { + return join('{workspaceRoot}', join(projectRoot, outputPath)); + } else { + return join('{projectRoot}', outputPath); + } + } + } +} + +function normalizeOptions(options: RsbuildPluginOptions): RsbuildPluginOptions { + options ??= {}; + options.buildTargetName ??= 'build'; + options.devTargetName ??= 'dev'; + options.previewTargetName ??= 'preview'; + options.inspectTargetName ??= 'inspect'; + options.typecheckTargetName ??= 'typecheck'; + return options; +} diff --git a/packages/rsbuild/src/utils/versions.ts b/packages/rsbuild/src/utils/versions.ts new file mode 100644 index 00000000000000..4abb29c6b72b54 --- /dev/null +++ b/packages/rsbuild/src/utils/versions.ts @@ -0,0 +1,2 @@ +export const nxVersion = require('../../package.json').version; +export const rsbuildVersion = '1.1.8'; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 12c91217f69315..54accd0c92450f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -391,6 +391,9 @@ importers: '@rollup/plugin-url': specifier: ^8.0.2 version: 8.0.2(rollup@4.22.0) + '@rsbuild/core': + specifier: 1.1.8 + version: 1.1.8 '@rspack/core': specifier: ^1.1.5 version: 1.1.5(@swc/helpers@0.5.11) @@ -6192,6 +6195,11 @@ packages: engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true + '@rsbuild/core@1.1.8': + resolution: {integrity: sha512-UhP260og3aJcqGWpnRcQXLVapdOZZ09JXaQKY+tE55A7nBw8DQy+qrtTsFZYvVKas1bq8GzhGfLxuglCst4Lnw==} + engines: {node: '>=16.7.0'} + hasBin: true + '@rspack/binding-darwin-arm64@1.1.3': resolution: {integrity: sha512-gpLUBMDAS/uEcnE+ODy1ILTeyp1oM4QCq8rRhKHuOfsIe1AZ9Mct59v2omIE/r+R4dnbJ0ikIpto9qJZ6P2u1A==} cpu: [arm64] @@ -6821,6 +6829,9 @@ packages: '@swc/helpers@0.5.11': resolution: {integrity: sha512-YNlnKRWF2sVojTpIyzwou9XoTNbzbzONwRhOoniEioF1AtaitTvVZblaQRrAzChWQ1bLYyYSWzM18y4WwgzJ+A==} + '@swc/helpers@0.5.15': + resolution: {integrity: sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==} + '@swc/helpers@0.5.5': resolution: {integrity: sha512-KGYxvIOXcceOAbEk4bi/dVLEK9z8sZ0uBB3Il5b1rhfClSpcX0yfRO0KmTkqR2cnQDymwLB+25ZyMzICg/cm/A==} @@ -9047,6 +9058,9 @@ packages: core-js@3.37.1: resolution: {integrity: sha512-Xn6qmxrQZyB0FFY8E3bgRXei3lWDJHhvI+u0q9TKIYM49G8pAr0FgnnrFRAmsbptZL1yxRADVXn+x5AGsbBfyw==} + core-js@3.39.0: + resolution: {integrity: sha512-raM0ew0/jJUqkJ0E6e8UDtl+y/7ktFivgWvqw8dNSQeNWoSDLvQ1H/RN3aPXB9tBd4/FhyR4RDPGhsNIMsAn7g==} + core-util-is@1.0.2: resolution: {integrity: sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==} @@ -23948,6 +23962,13 @@ snapshots: optionalDependencies: fsevents: 2.3.3 + '@rsbuild/core@1.1.8': + dependencies: + '@rspack/core': 1.1.5(@swc/helpers@0.5.15) + '@rspack/lite-tapable': 1.0.1 + '@swc/helpers': 0.5.15 + core-js: 3.39.0 + '@rspack/binding-darwin-arm64@1.1.3': optional: true @@ -24044,6 +24065,15 @@ snapshots: optionalDependencies: '@swc/helpers': 0.5.11 + '@rspack/core@1.1.5(@swc/helpers@0.5.15)': + dependencies: + '@module-federation/runtime-tools': 0.5.1 + '@rspack/binding': 1.1.5 + '@rspack/lite-tapable': 1.0.1 + caniuse-lite: 1.0.30001684 + optionalDependencies: + '@swc/helpers': 0.5.15 + '@rspack/dev-server@1.0.9(@rspack/core@1.1.5(@swc/helpers@0.5.11))(@types/express@4.17.14)(webpack-cli@5.1.4(webpack-dev-server@5.0.4)(webpack@5.88.0))(webpack@5.88.0(@swc/core@1.5.7(@swc/helpers@0.5.11))(esbuild@0.19.5)(webpack-cli@5.1.4))': dependencies: '@rspack/core': 1.1.5(@swc/helpers@0.5.11) @@ -24737,6 +24767,10 @@ snapshots: dependencies: tslib: 2.7.0 + '@swc/helpers@0.5.15': + dependencies: + tslib: 2.8.1 + '@swc/helpers@0.5.5': dependencies: '@swc/counter': 0.1.3 @@ -27559,6 +27593,8 @@ snapshots: core-js@3.37.1: {} + core-js@3.39.0: {} + core-util-is@1.0.2: {} core-util-is@1.0.3: {}