feat(rsbuild): add rsbuild configuration generator (#29321)

## Current Behavior
Nx currently does not offer a generator to help scaffold configuration
for an Rsbuild project

## Expected Behavior
Add a `configuration` generator to the `@nx/rsbuild` package to help
scaffold a configuration for a basic app
This commit is contained in:
Colum Ferry 2024-12-12 17:25:23 +00:00 committed by GitHub
parent 22cec78331
commit 36eaafdcfd
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 446 additions and 0 deletions

View File

@ -9902,6 +9902,14 @@
"children": [], "children": [],
"isExternal": false, "isExternal": false,
"disableCollapsible": false "disableCollapsible": false
},
{
"id": "configuration",
"path": "/nx-api/rsbuild/generators/configuration",
"name": "configuration",
"children": [],
"isExternal": false,
"disableCollapsible": false
} }
], ],
"isExternal": false, "isExternal": false,

View File

@ -2909,6 +2909,15 @@
"originalFilePath": "/packages/rsbuild/src/generators/init/schema.json", "originalFilePath": "/packages/rsbuild/src/generators/init/schema.json",
"path": "/nx-api/rsbuild/generators/init", "path": "/nx-api/rsbuild/generators/init",
"type": "generator" "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" "path": "/nx-api/rsbuild"

View File

@ -2878,6 +2878,15 @@
"originalFilePath": "/packages/rsbuild/src/generators/init/schema.json", "originalFilePath": "/packages/rsbuild/src/generators/init/schema.json",
"path": "rsbuild/generators/init", "path": "rsbuild/generators/init",
"type": "generator" "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", "githubRoot": "https://github.com/nrwl/nx/blob/master",

View File

@ -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 '<projectRoot>/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 '<projectRoot>/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"
}

View File

@ -688,6 +688,7 @@
- [rsbuild](/nx-api/rsbuild) - [rsbuild](/nx-api/rsbuild)
- [generators](/nx-api/rsbuild/generators) - [generators](/nx-api/rsbuild/generators)
- [init](/nx-api/rsbuild/generators/init) - [init](/nx-api/rsbuild/generators/init)
- [configuration](/nx-api/rsbuild/generators/configuration)
- [rspack](/nx-api/rspack) - [rspack](/nx-api/rspack)
- [documents](/nx-api/rspack/documents) - [documents](/nx-api/rspack/documents)
- [Overview](/nx-api/rspack/documents/overview) - [Overview](/nx-api/rspack/documents/overview)

View File

@ -8,6 +8,11 @@
"description": "Initialize the `@nx/rsbuild` plugin.", "description": "Initialize the `@nx/rsbuild` plugin.",
"aliases": ["ng-add"], "aliases": ["ng-add"],
"hidden": true "hidden": true
},
"configuration": {
"factory": "./src/generators/configuration/configuration",
"schema": "./src/generators/configuration/schema.json",
"description": "Add an Rsbuild configuration for the provided project."
} }
} }
} }

View File

@ -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',
},
}
});
"
`);
});
});

View File

@ -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;

View File

@ -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',
},
}
});

View File

@ -0,0 +1 @@
export * from './normalize-options';

View File

@ -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}`;
}

View File

@ -0,0 +1,8 @@
export interface Schema {
project: string;
entry?: string;
tsConfig?: string;
target?: 'node' | 'web' | 'web-worker';
skipValidation?: boolean;
skipFormat?: boolean;
}

View File

@ -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 '<projectRoot>/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 '<projectRoot>/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"
}
}
}