From 098c0da1ca3251d38ab82dfd2948774ee0ea187f Mon Sep 17 00:00:00 2001 From: neverland Date: Mon, 21 Sep 2026 16:50:03 +0800 Subject: [PATCH 1/3] refactor(config): merge app config layers with Rsbuild --- packages/rstack/src/rsbuildConfig.ts | 25 ++++----- .../rstack/tests/config/app-merge.test.ts | 51 +++++++++++++++++++ 2 files changed, 64 insertions(+), 12 deletions(-) create mode 100644 packages/rstack/tests/config/app-merge.test.ts diff --git a/packages/rstack/src/rsbuildConfig.ts b/packages/rstack/src/rsbuildConfig.ts index 466d4705..c250bf72 100644 --- a/packages/rstack/src/rsbuildConfig.ts +++ b/packages/rstack/src/rsbuildConfig.ts @@ -1,21 +1,22 @@ -import type { ConfigParams, RsbuildConfigDefinition } from '@rsbuild/core'; +import { + type ConfigParams, + type RsbuildConfig, + type RsbuildConfigDefinition, + mergeRsbuildConfig, +} from '@rsbuild/core'; import { withConfigMeta } from '@rstackjs/load-config'; import { loadRstackConfig, type Configs } from './config.ts'; +import { resolveConfigLayers } from './configLayers.ts'; -const resolveRsbuildConfig = async (configs: Configs, params: ConfigParams) => { - const appConfig = configs.app; - if (!appConfig) { - return {}; - } - if (typeof appConfig === 'function') { - return appConfig(params); - } - return appConfig; -}; +export const resolveRsbuildConfig = async ( + layers: readonly Configs[], + params: ConfigParams, +): Promise => + mergeRsbuildConfig(...(await resolveConfigLayers(layers, 'app', params))); const loadRsbuildConfig: RsbuildConfigDefinition = async (params) => { const { configs, filePath, dependencies } = await loadRstackConfig(); - const config = await resolveRsbuildConfig(configs, params); + const config = await resolveRsbuildConfig([configs], params); return withConfigMeta(config, { filePath, dependencies }); }; diff --git a/packages/rstack/tests/config/app-merge.test.ts b/packages/rstack/tests/config/app-merge.test.ts new file mode 100644 index 00000000..5e5483d1 --- /dev/null +++ b/packages/rstack/tests/config/app-merge.test.ts @@ -0,0 +1,51 @@ +import type { ConfigParams, RsbuildConfig } from 'rstack/app'; +import { expect, rs, test } from 'rstack/test'; +import { resolveRsbuildConfig } from '../../src/rsbuildConfig.ts'; + +const params: ConfigParams = { command: 'build', env: 'production' }; + +test('merges app layers using native Rsbuild rules', async () => { + const basePlugin = { name: 'base', setup: rs.fn() }; + const projectPlugin = { name: 'project', setup: rs.fn() }; + const baseRspack = rs.fn(); + const projectRspack = rs.fn(); + const base: RsbuildConfig = { + source: { define: { SHARED: true, ENV: 'base' } }, + output: { distPath: 'build' }, + plugins: [basePlugin], + tools: { rspack: baseRspack }, + }; + const project = rs.fn(({ env }: ConfigParams) => + Promise.resolve({ + source: { define: { ENV: env } }, + output: { distPath: 'dist' }, + plugins: [projectPlugin], + tools: { rspack: projectRspack }, + }), + ); + + const config = await resolveRsbuildConfig( + [{ app: base }, { app: project }], + params, + ); + + expect(project).toHaveBeenCalledExactlyOnceWith(params); + expect(config).toEqual({ + source: { define: { SHARED: true, ENV: 'production' } }, + output: { distPath: { root: 'dist' } }, + plugins: [basePlugin, projectPlugin], + tools: { rspack: [baseRspack, projectRspack] }, + }); + expect(base.source?.define).toEqual({ SHARED: true, ENV: 'base' }); + expect(base.output?.distPath).toBe('build'); + expect(base.plugins).toEqual([basePlugin]); + expect(baseRspack).not.toHaveBeenCalled(); + expect(projectRspack).not.toHaveBeenCalled(); +}); + +test('keeps the empty app default without resolving other tools', async () => { + const fmt = rs.fn(() => ({})); + + expect(await resolveRsbuildConfig([{ fmt }], params)).toEqual({}); + expect(fmt).not.toHaveBeenCalled(); +}); From c786e5c121f31e3562f142b0141fb72a7f37c871 Mon Sep 17 00:00:00 2001 From: neverland Date: Thu, 24 Sep 2026 14:25:19 +0800 Subject: [PATCH 2/3] perf(config): skip merging a single app config --- packages/rstack/src/rsbuildConfig.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/rstack/src/rsbuildConfig.ts b/packages/rstack/src/rsbuildConfig.ts index c250bf72..a6ef3cc1 100644 --- a/packages/rstack/src/rsbuildConfig.ts +++ b/packages/rstack/src/rsbuildConfig.ts @@ -11,8 +11,12 @@ import { resolveConfigLayers } from './configLayers.ts'; export const resolveRsbuildConfig = async ( layers: readonly Configs[], params: ConfigParams, -): Promise => - mergeRsbuildConfig(...(await resolveConfigLayers(layers, 'app', params))); +): Promise => { + const configs = await resolveConfigLayers(layers, 'app', params); + return configs.length > 1 + ? mergeRsbuildConfig(...configs) + : (configs[0] ?? {}); +}; const loadRsbuildConfig: RsbuildConfigDefinition = async (params) => { const { configs, filePath, dependencies } = await loadRstackConfig(); From c5b47735e0c3bdb55c005c1fa3520296afcd5a8e Mon Sep 17 00:00:00 2001 From: neverland Date: Thu, 24 Sep 2026 14:30:19 +0800 Subject: [PATCH 3/3] test(config): simplify app merge coverage --- .../rstack/tests/config/app-merge.test.ts | 66 +++++++------------ 1 file changed, 23 insertions(+), 43 deletions(-) diff --git a/packages/rstack/tests/config/app-merge.test.ts b/packages/rstack/tests/config/app-merge.test.ts index 5e5483d1..2f1292c5 100644 --- a/packages/rstack/tests/config/app-merge.test.ts +++ b/packages/rstack/tests/config/app-merge.test.ts @@ -1,51 +1,31 @@ -import type { ConfigParams, RsbuildConfig } from 'rstack/app'; -import { expect, rs, test } from 'rstack/test'; +import { expect, test } from 'rstack/test'; import { resolveRsbuildConfig } from '../../src/rsbuildConfig.ts'; -const params: ConfigParams = { command: 'build', env: 'production' }; - test('merges app layers using native Rsbuild rules', async () => { - const basePlugin = { name: 'base', setup: rs.fn() }; - const projectPlugin = { name: 'project', setup: rs.fn() }; - const baseRspack = rs.fn(); - const projectRspack = rs.fn(); - const base: RsbuildConfig = { - source: { define: { SHARED: true, ENV: 'base' } }, - output: { distPath: 'build' }, - plugins: [basePlugin], - tools: { rspack: baseRspack }, - }; - const project = rs.fn(({ env }: ConfigParams) => - Promise.resolve({ - source: { define: { ENV: env } }, - output: { distPath: 'dist' }, - plugins: [projectPlugin], - tools: { rspack: projectRspack }, - }), - ); - const config = await resolveRsbuildConfig( - [{ app: base }, { app: project }], - params, + [ + { + app: { + source: { + define: { SHARED: true, ENV: 'base' }, + preEntry: ['./base.ts'], + }, + }, + }, + { + app: { + source: { + define: { ENV: 'production' }, + preEntry: ['./project.ts'], + }, + }, + }, + ], + { command: 'build', env: 'production' }, ); - expect(project).toHaveBeenCalledExactlyOnceWith(params); - expect(config).toEqual({ - source: { define: { SHARED: true, ENV: 'production' } }, - output: { distPath: { root: 'dist' } }, - plugins: [basePlugin, projectPlugin], - tools: { rspack: [baseRspack, projectRspack] }, + expect(config.source).toEqual({ + define: { SHARED: true, ENV: 'production' }, + preEntry: ['./base.ts', './project.ts'], }); - expect(base.source?.define).toEqual({ SHARED: true, ENV: 'base' }); - expect(base.output?.distPath).toBe('build'); - expect(base.plugins).toEqual([basePlugin]); - expect(baseRspack).not.toHaveBeenCalled(); - expect(projectRspack).not.toHaveBeenCalled(); -}); - -test('keeps the empty app default without resolving other tools', async () => { - const fmt = rs.fn(() => ({})); - - expect(await resolveRsbuildConfig([{ fmt }], params)).toEqual({}); - expect(fmt).not.toHaveBeenCalled(); });