diff --git a/packages/rstack/src/rstestConfig.ts b/packages/rstack/src/rstestConfig.ts index 131985e..c69bdac 100644 --- a/packages/rstack/src/rstestConfig.ts +++ b/packages/rstack/src/rstestConfig.ts @@ -8,36 +8,32 @@ import { loadRstackConfig, type Configs } from './config.ts'; import { resolveConfigLayers } from './configLayers.ts'; const resolveAutomaticExtends = async ( - configs: Configs, + layers: readonly Configs[], params: ConfigParams, ): Promise => { // Prefer the app when both app and lib are defined. Merging both adapters can // introduce conflicting runtime, resolve, and source transform settings. - const appConfig = configs.app; - if (appConfig) { + if (layers.some((layer) => layer.app !== undefined)) { const { withRsbuildConfig } = await import( /* rspackChunkName: 'adapterRsbuild' */ '@rstest/adapter-rsbuild' ); - const config = - typeof appConfig === 'function' ? await appConfig(params) : appConfig; + const { resolveRsbuildConfig } = await import('./rsbuildConfig.ts'); return withRsbuildConfig({ - config, + config: await resolveRsbuildConfig(layers, params), }); } - const libConfig = configs.lib; - if (libConfig) { + if (layers.some((layer) => layer.lib !== undefined)) { const { withRslibConfig } = await import( /* rspackChunkName: 'adapterRslib' */ '@rstest/adapter-rslib' ); - const config = - typeof libConfig === 'function' ? await libConfig(params) : libConfig; + const { resolveRslibConfig } = await import('./rslibConfig.ts'); return withRslibConfig({ - config, + config: await resolveRslibConfig(layers, params), }); } @@ -59,7 +55,7 @@ const injectExtends = ( }; const extendsConfig = async ( - configs: Configs, + layers: readonly Configs[], testConfig: RstestConfig, params: ConfigParams, ) => { @@ -68,7 +64,7 @@ const extendsConfig = async ( } if (testConfig.projects === undefined) { - const automaticExtends = await resolveAutomaticExtends(configs, params); + const automaticExtends = await resolveAutomaticExtends(layers, params); return injectExtends(testConfig, automaticExtends); } @@ -79,7 +75,7 @@ const extendsConfig = async ( return testConfig; } - const automaticExtends = await resolveAutomaticExtends(configs, params); + const automaticExtends = await resolveAutomaticExtends(layers, params); return { ...testConfig, @@ -93,17 +89,17 @@ const extendsConfig = async ( export const resolveRstestConfig = async ( layers: readonly Configs[], + params: ConfigParams, ): Promise => { const configs = await resolveConfigLayers(layers, 'test'); - return configs.length > 1 - ? mergeRstestConfig(...configs) - : (configs[0] ?? {}); + const testConfig = + configs.length > 1 ? mergeRstestConfig(...configs) : (configs[0] ?? {}); + return extendsConfig(layers, testConfig, params); }; const loadRstestConfig = (async (params: ConfigParams) => { const { configs } = await loadRstackConfig(); - const testConfig = await resolveRstestConfig([configs]); - return extendsConfig(configs, testConfig, params); + return resolveRstestConfig([configs], params); }) as RstestConfigExport; export default loadRstestConfig; diff --git a/packages/rstack/tests/config/test-merge.test.ts b/packages/rstack/tests/config/test-merge.test.ts index e60f313..a9033b0 100644 --- a/packages/rstack/tests/config/test-merge.test.ts +++ b/packages/rstack/tests/config/test-merge.test.ts @@ -1,31 +1,40 @@ +import assert from 'node:assert/strict'; import { createRequire } from 'node:module'; -import { expect, test } from 'rstack/test'; +import type { ConfigParams } from 'rstack/app'; +import { expect, rs, test } from 'rstack/test'; // Bypass Rstest's replacement of @rstest/core with its test runtime APIs. +// TODO: Import the source directly after upgrading to a Rstest version that fixes +// https://github.com/web-infra-dev/rstest/issues/1891. const { resolveRstestConfig } = createRequire(import.meta.url)( '../../dist/rstestConfig.js', ) as typeof import('../../src/rstestConfig.ts'); +const params: ConfigParams = { command: 'build', env: 'production' }; + test('merges test layers using native Rstest rules', async () => { - const config = await resolveRstestConfig([ - { - test: { - extends: { testTimeout: 5000 }, - retry: 1, - setupFiles: ['./base.ts'], - reporters: ['default'], - projects: ['./base.config.ts'], + const config = await resolveRstestConfig( + [ + { + test: { + extends: { testTimeout: 5000 }, + retry: 1, + setupFiles: ['./base.ts'], + reporters: ['default'], + projects: ['./base.config.ts'], + }, }, - }, - { - test: { - retry: 2, - setupFiles: ['./project.ts'], - reporters: ['dot'], - projects: ['./project.config.ts'], + { + test: { + retry: 2, + setupFiles: ['./project.ts'], + reporters: ['dot'], + projects: ['./project.config.ts'], + }, }, - }, - ]); + ], + params, + ); expect(config).toMatchObject({ extends: { testTimeout: 5000 }, @@ -35,3 +44,87 @@ test('merges test layers using native Rstest rules', async () => { projects: ['./base.config.ts', './project.config.ts'], }); }); + +test('shares the merged app config across projects and prefers it over lib', async () => { + const app = rs.fn(({ env }: ConfigParams) => ({ + source: { define: { ENV: env, VALUE: 'base' } }, + })); + const lib = rs.fn(() => ({})); + const config = await resolveRstestConfig( + [ + { app, lib, test: { projects: [{ name: 'base' }] } }, + { + app: { source: { define: { VALUE: 'project' } } }, + test: { + projects: [ + { name: 'project' }, + { name: 'explicit', extends: undefined }, + './external.config.ts', + ], + }, + }, + ], + params, + ); + + const first = config.projects?.[0]; + assert( + first && typeof first !== 'string' && typeof first.extends === 'function', + ); + expect(await first.extends(first)).toMatchObject({ + source: { define: { ENV: 'production', VALUE: 'project' } }, + }); + expect(config.projects).toEqual([ + { name: 'base', extends: first.extends }, + { name: 'project', extends: first.extends }, + { name: 'explicit', extends: undefined }, + './external.config.ts', + ]); + expect(app).toHaveBeenCalledExactlyOnceWith(params); + expect(lib).not.toHaveBeenCalled(); +}); + +test('inherits merged lib config when no layer defines app', async () => { + const config = await resolveRstestConfig( + [ + { lib: { source: { define: { SHARED: true, VALUE: 'base' } } } }, + { lib: { source: { define: { VALUE: 'project' } } } }, + ], + params, + ); + + assert(typeof config.extends === 'function'); + expect(await config.extends(config)).toMatchObject({ + source: { define: { SHARED: true, VALUE: 'project' } }, + }); +}); + +test('skips build config factories when the root explicitly defines extends', async () => { + const build = rs.fn(() => ({})); + await resolveRstestConfig( + [ + { app: build, lib: build, test: { retry: 1 } }, + { test: { extends: undefined } }, + ], + params, + ); + + expect(build).not.toHaveBeenCalled(); +}); + +test('skips build config factories when no project needs automatic inheritance', async () => { + const build = rs.fn(() => ({})); + await resolveRstestConfig( + [ + { app: build, lib: build, test: { retry: 1 } }, + { + test: { + projects: ['./external.config.ts', { name: 'explicit', extends: {} }], + }, + }, + ], + params, + ); + + expect(build).not.toHaveBeenCalled(); +});