From f83b47e3da640ea28d5ae83efbf80e3192f95d49 Mon Sep 17 00:00:00 2001 From: neverland Date: Sun, 26 Jul 2026 09:13:20 +0800 Subject: [PATCH 1/6] feat: support loading config from explicit path --- packages/rstack/src/config.ts | 26 ++++++- .../config/load-state/explicit-dependency.ts | 1 + .../config/load-state/explicit.config.ts | 8 ++ .../tests/config/load-state/fresh.config.ts | 16 ++++ .../tests/config/load-state/index.test.ts | 78 ++++++++++++++++--- 5 files changed, 116 insertions(+), 13 deletions(-) create mode 100644 packages/rstack/tests/config/load-state/explicit-dependency.ts create mode 100644 packages/rstack/tests/config/load-state/explicit.config.ts create mode 100644 packages/rstack/tests/config/load-state/fresh.config.ts diff --git a/packages/rstack/src/config.ts b/packages/rstack/src/config.ts index 81fa02f..f805ca3 100644 --- a/packages/rstack/src/config.ts +++ b/packages/rstack/src/config.ts @@ -1,3 +1,4 @@ +import { resolve } from 'node:path'; import { loadConfig } from '@rstackjs/load-config'; import type { RsbuildConfigDefinition } from '@rsbuild/core'; import type { RslibConfigDefinition } from '@rslib/core'; @@ -24,6 +25,19 @@ type LoadedRstackConfig = { dependencies: string[]; }; +type LoadRstackConfigOptions = { + /** + * The path to the Rstack config file, can be a relative or absolute path. + * If `configFilePath` is not provided, the function will search for the config file in the current working directory. + */ + configFilePath?: string; + /** + * Whether to bypass module cache when loading the config. + * @default true + */ + fresh?: boolean; +}; + type ConfigState = { configs: Configs; configPath?: string; @@ -108,17 +122,21 @@ export const define: Define = { staged: (config) => setConfig('staged', config), }; -export const loadRstackConfig = async (): Promise => { +export const loadRstackConfig = async ({ + configFilePath, + fresh = true, +}: LoadRstackConfigOptions = {}): Promise => { const state = getConfigState(); + const configPath = configFilePath ?? state.configPath; state.configs = {}; try { const { filePath, dependencies } = await loadConfig({ loader: 'native', exportName: false, - fresh: true, - ...(state.configPath !== undefined - ? { path: state.configPath } + fresh, + ...(configPath !== undefined + ? { path: resolve(configPath) } : { configFileNames: [ 'rstack.config.ts', diff --git a/packages/rstack/tests/config/load-state/explicit-dependency.ts b/packages/rstack/tests/config/load-state/explicit-dependency.ts new file mode 100644 index 0000000..1cc9cc9 --- /dev/null +++ b/packages/rstack/tests/config/load-state/explicit-dependency.ts @@ -0,0 +1 @@ +export const title = 'explicit config works'; diff --git a/packages/rstack/tests/config/load-state/explicit.config.ts b/packages/rstack/tests/config/load-state/explicit.config.ts new file mode 100644 index 0000000..c77b982 --- /dev/null +++ b/packages/rstack/tests/config/load-state/explicit.config.ts @@ -0,0 +1,8 @@ +import { define } from 'rstack'; +import { title } from './explicit-dependency.ts'; + +define.app({ + html: { + title, + }, +}); diff --git a/packages/rstack/tests/config/load-state/fresh.config.ts b/packages/rstack/tests/config/load-state/fresh.config.ts new file mode 100644 index 0000000..7696ebf --- /dev/null +++ b/packages/rstack/tests/config/load-state/fresh.config.ts @@ -0,0 +1,16 @@ +import { define } from 'rstack'; + +declare global { + // rslint-disable-next-line no-var + var __rstackLoadConfigFreshCount: number | undefined; +} + +globalThis.__rstackLoadConfigFreshCount = (globalThis.__rstackLoadConfigFreshCount ?? 0) + 1; + +define.app({ + source: { + define: { + RSTACK_LOAD_COUNT: JSON.stringify(globalThis.__rstackLoadConfigFreshCount), + }, + }, +}); diff --git a/packages/rstack/tests/config/load-state/index.test.ts b/packages/rstack/tests/config/load-state/index.test.ts index 1f4e34f..ab77414 100644 --- a/packages/rstack/tests/config/load-state/index.test.ts +++ b/packages/rstack/tests/config/load-state/index.test.ts @@ -1,17 +1,77 @@ import path from 'node:path'; -import { expect, test } from 'rstack/test'; +import { afterEach, expect, test } from 'rstack/test'; import { getConfigState, loadRstackConfig } from '../../../src/config.ts'; +const state = getConfigState(); + +afterEach(() => { + state.configs = {}; + delete state.configPath; + delete globalThis.__rstackLoadConfigFreshCount; +}); + test('should reset config state before and after loading', async () => { - const state = getConfigState(); state.configs = { app: {} }; state.configPath = path.join(import.meta.dirname, 'rstack.config.ts'); - try { - await expect(loadRstackConfig()).rejects.toThrow('test config error'); - expect(state.configs).toEqual({}); - } finally { - state.configs = {}; - delete state.configPath; - } + await expect(loadRstackConfig()).rejects.toThrow('test config error'); + expect(state.configs).toEqual({}); +}); + +test('should load an explicit config path before the legacy state path', async () => { + const configFilePath = path.join(import.meta.dirname, 'explicit.config.ts'); + const dependencyPath = path.join(import.meta.dirname, 'explicit-dependency.ts'); + const relativeConfigPath = path.relative(process.cwd(), configFilePath); + state.configPath = path.join(import.meta.dirname, 'rstack.config.ts'); + + const loaded = await loadRstackConfig({ + configFilePath: relativeConfigPath, + }); + + expect(loaded.configs.app).toEqual({ + html: { + title: 'explicit config works', + }, + }); + expect(loaded.filePath).toBe(configFilePath); + expect(loaded.dependencies).toEqual([dependencyPath]); + expect(state.configs).toEqual({}); +}); + +test('should report the resolved path when an explicit config is missing', async () => { + const configFilePath = path.join(import.meta.dirname, 'missing.config.ts'); + state.configs = { app: {} }; + + await expect(loadRstackConfig({ configFilePath })).rejects.toThrow( + `Cannot find config file: ${configFilePath}`, + ); + expect(state.configs).toEqual({}); +}); + +test('should forward the fresh option to the config loader', async () => { + const configFilePath = path.join(import.meta.dirname, 'fresh.config.ts'); + + const cached = await loadRstackConfig({ + configFilePath, + fresh: false, + }); + const fresh = await loadRstackConfig({ + configFilePath, + fresh: true, + }); + + expect(cached.configs.app).toEqual({ + source: { + define: { + RSTACK_LOAD_COUNT: '1', + }, + }, + }); + expect(fresh.configs.app).toEqual({ + source: { + define: { + RSTACK_LOAD_COUNT: '2', + }, + }, + }); }); From dad79428e96b6d73e822060c74f85dd80a166c1c Mon Sep 17 00:00:00 2001 From: neverland Date: Sun, 26 Jul 2026 09:21:34 +0800 Subject: [PATCH 2/6] fix(config): keep Rstack config loads fresh --- packages/rstack/src/config.ts | 11 ++----- .../tests/config/load-state/fresh.config.ts | 16 ---------- .../tests/config/load-state/index.test.ts | 29 ------------------- 3 files changed, 3 insertions(+), 53 deletions(-) delete mode 100644 packages/rstack/tests/config/load-state/fresh.config.ts diff --git a/packages/rstack/src/config.ts b/packages/rstack/src/config.ts index f805ca3..c086e64 100644 --- a/packages/rstack/src/config.ts +++ b/packages/rstack/src/config.ts @@ -28,14 +28,10 @@ type LoadedRstackConfig = { type LoadRstackConfigOptions = { /** * The path to the Rstack config file, can be a relative or absolute path. - * If `configFilePath` is not provided, the function will search for the config file in the current working directory. + * If `configFilePath` is not provided, the config path set by the CLI is used. + * If neither path is provided, the function will search for the config file in the current working directory. */ configFilePath?: string; - /** - * Whether to bypass module cache when loading the config. - * @default true - */ - fresh?: boolean; }; type ConfigState = { @@ -124,7 +120,6 @@ export const define: Define = { export const loadRstackConfig = async ({ configFilePath, - fresh = true, }: LoadRstackConfigOptions = {}): Promise => { const state = getConfigState(); const configPath = configFilePath ?? state.configPath; @@ -134,7 +129,7 @@ export const loadRstackConfig = async ({ const { filePath, dependencies } = await loadConfig({ loader: 'native', exportName: false, - fresh, + fresh: true, ...(configPath !== undefined ? { path: resolve(configPath) } : { diff --git a/packages/rstack/tests/config/load-state/fresh.config.ts b/packages/rstack/tests/config/load-state/fresh.config.ts deleted file mode 100644 index 7696ebf..0000000 --- a/packages/rstack/tests/config/load-state/fresh.config.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { define } from 'rstack'; - -declare global { - // rslint-disable-next-line no-var - var __rstackLoadConfigFreshCount: number | undefined; -} - -globalThis.__rstackLoadConfigFreshCount = (globalThis.__rstackLoadConfigFreshCount ?? 0) + 1; - -define.app({ - source: { - define: { - RSTACK_LOAD_COUNT: JSON.stringify(globalThis.__rstackLoadConfigFreshCount), - }, - }, -}); diff --git a/packages/rstack/tests/config/load-state/index.test.ts b/packages/rstack/tests/config/load-state/index.test.ts index ab77414..059fad9 100644 --- a/packages/rstack/tests/config/load-state/index.test.ts +++ b/packages/rstack/tests/config/load-state/index.test.ts @@ -7,7 +7,6 @@ const state = getConfigState(); afterEach(() => { state.configs = {}; delete state.configPath; - delete globalThis.__rstackLoadConfigFreshCount; }); test('should reset config state before and after loading', async () => { @@ -47,31 +46,3 @@ test('should report the resolved path when an explicit config is missing', async ); expect(state.configs).toEqual({}); }); - -test('should forward the fresh option to the config loader', async () => { - const configFilePath = path.join(import.meta.dirname, 'fresh.config.ts'); - - const cached = await loadRstackConfig({ - configFilePath, - fresh: false, - }); - const fresh = await loadRstackConfig({ - configFilePath, - fresh: true, - }); - - expect(cached.configs.app).toEqual({ - source: { - define: { - RSTACK_LOAD_COUNT: '1', - }, - }, - }); - expect(fresh.configs.app).toEqual({ - source: { - define: { - RSTACK_LOAD_COUNT: '2', - }, - }, - }); -}); From 61f31bd70eb5a95537ac9e483c25460f17ab7c80 Mon Sep 17 00:00:00 2001 From: neverland Date: Sun, 26 Jul 2026 09:30:23 +0800 Subject: [PATCH 3/6] test: rename config loader test directory --- .../config/{load-state => load-config}/explicit-dependency.ts | 0 .../tests/config/{load-state => load-config}/explicit.config.ts | 0 .../rstack/tests/config/{load-state => load-config}/index.test.ts | 0 .../tests/config/{load-state => load-config}/rstack.config.ts | 0 4 files changed, 0 insertions(+), 0 deletions(-) rename packages/rstack/tests/config/{load-state => load-config}/explicit-dependency.ts (100%) rename packages/rstack/tests/config/{load-state => load-config}/explicit.config.ts (100%) rename packages/rstack/tests/config/{load-state => load-config}/index.test.ts (100%) rename packages/rstack/tests/config/{load-state => load-config}/rstack.config.ts (100%) diff --git a/packages/rstack/tests/config/load-state/explicit-dependency.ts b/packages/rstack/tests/config/load-config/explicit-dependency.ts similarity index 100% rename from packages/rstack/tests/config/load-state/explicit-dependency.ts rename to packages/rstack/tests/config/load-config/explicit-dependency.ts diff --git a/packages/rstack/tests/config/load-state/explicit.config.ts b/packages/rstack/tests/config/load-config/explicit.config.ts similarity index 100% rename from packages/rstack/tests/config/load-state/explicit.config.ts rename to packages/rstack/tests/config/load-config/explicit.config.ts diff --git a/packages/rstack/tests/config/load-state/index.test.ts b/packages/rstack/tests/config/load-config/index.test.ts similarity index 100% rename from packages/rstack/tests/config/load-state/index.test.ts rename to packages/rstack/tests/config/load-config/index.test.ts diff --git a/packages/rstack/tests/config/load-state/rstack.config.ts b/packages/rstack/tests/config/load-config/rstack.config.ts similarity index 100% rename from packages/rstack/tests/config/load-state/rstack.config.ts rename to packages/rstack/tests/config/load-config/rstack.config.ts From bf6ebf694af3b0dacb403c7256995a318a33f84d Mon Sep 17 00:00:00 2001 From: neverland Date: Sun, 26 Jul 2026 17:06:15 +0800 Subject: [PATCH 4/6] refactor(config): rely on load-config path resolution --- packages/rstack/src/config.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/rstack/src/config.ts b/packages/rstack/src/config.ts index c086e64..ce73030 100644 --- a/packages/rstack/src/config.ts +++ b/packages/rstack/src/config.ts @@ -1,4 +1,3 @@ -import { resolve } from 'node:path'; import { loadConfig } from '@rstackjs/load-config'; import type { RsbuildConfigDefinition } from '@rsbuild/core'; import type { RslibConfigDefinition } from '@rslib/core'; @@ -131,7 +130,7 @@ export const loadRstackConfig = async ({ exportName: false, fresh: true, ...(configPath !== undefined - ? { path: resolve(configPath) } + ? { path: configPath } : { configFileNames: [ 'rstack.config.ts', From f89affdd675f8026df8d3d5c8bdf71ee634f96c5 Mon Sep 17 00:00:00 2001 From: neverland Date: Sun, 26 Jul 2026 17:10:54 +0800 Subject: [PATCH 5/6] test: refine explicit config path coverage --- .../rstack/tests/config/load-config/index.test.ts | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/packages/rstack/tests/config/load-config/index.test.ts b/packages/rstack/tests/config/load-config/index.test.ts index 059fad9..2220e87 100644 --- a/packages/rstack/tests/config/load-config/index.test.ts +++ b/packages/rstack/tests/config/load-config/index.test.ts @@ -17,7 +17,7 @@ test('should reset config state before and after loading', async () => { expect(state.configs).toEqual({}); }); -test('should load an explicit config path before the legacy state path', async () => { +test('should prefer an explicit config path over the state config path', async () => { const configFilePath = path.join(import.meta.dirname, 'explicit.config.ts'); const dependencyPath = path.join(import.meta.dirname, 'explicit-dependency.ts'); const relativeConfigPath = path.relative(process.cwd(), configFilePath); @@ -36,13 +36,3 @@ test('should load an explicit config path before the legacy state path', async ( expect(loaded.dependencies).toEqual([dependencyPath]); expect(state.configs).toEqual({}); }); - -test('should report the resolved path when an explicit config is missing', async () => { - const configFilePath = path.join(import.meta.dirname, 'missing.config.ts'); - state.configs = { app: {} }; - - await expect(loadRstackConfig({ configFilePath })).rejects.toThrow( - `Cannot find config file: ${configFilePath}`, - ); - expect(state.configs).toEqual({}); -}); From f14ea2c7e622b6231cd007c8fd7d956c8e68c20e Mon Sep 17 00:00:00 2001 From: neverland Date: Sun, 26 Jul 2026 17:21:05 +0800 Subject: [PATCH 6/6] test: simplify explicit config path coverage --- .../config/load-config/explicit-dependency.ts | 1 - .../tests/config/load-config/explicit.config.ts | 7 +------ .../tests/config/load-config/index.test.ts | 16 +++------------- 3 files changed, 4 insertions(+), 20 deletions(-) delete mode 100644 packages/rstack/tests/config/load-config/explicit-dependency.ts diff --git a/packages/rstack/tests/config/load-config/explicit-dependency.ts b/packages/rstack/tests/config/load-config/explicit-dependency.ts deleted file mode 100644 index 1cc9cc9..0000000 --- a/packages/rstack/tests/config/load-config/explicit-dependency.ts +++ /dev/null @@ -1 +0,0 @@ -export const title = 'explicit config works'; diff --git a/packages/rstack/tests/config/load-config/explicit.config.ts b/packages/rstack/tests/config/load-config/explicit.config.ts index c77b982..89e13f6 100644 --- a/packages/rstack/tests/config/load-config/explicit.config.ts +++ b/packages/rstack/tests/config/load-config/explicit.config.ts @@ -1,8 +1,3 @@ import { define } from 'rstack'; -import { title } from './explicit-dependency.ts'; -define.app({ - html: { - title, - }, -}); +define.app({}); diff --git a/packages/rstack/tests/config/load-config/index.test.ts b/packages/rstack/tests/config/load-config/index.test.ts index 2220e87..aa2d834 100644 --- a/packages/rstack/tests/config/load-config/index.test.ts +++ b/packages/rstack/tests/config/load-config/index.test.ts @@ -19,20 +19,10 @@ test('should reset config state before and after loading', async () => { test('should prefer an explicit config path over the state config path', async () => { const configFilePath = path.join(import.meta.dirname, 'explicit.config.ts'); - const dependencyPath = path.join(import.meta.dirname, 'explicit-dependency.ts'); - const relativeConfigPath = path.relative(process.cwd(), configFilePath); state.configPath = path.join(import.meta.dirname, 'rstack.config.ts'); - const loaded = await loadRstackConfig({ - configFilePath: relativeConfigPath, - }); + const { configs, filePath } = await loadRstackConfig({ configFilePath }); - expect(loaded.configs.app).toEqual({ - html: { - title: 'explicit config works', - }, - }); - expect(loaded.filePath).toBe(configFilePath); - expect(loaded.dependencies).toEqual([dependencyPath]); - expect(state.configs).toEqual({}); + expect(configs.app).toEqual({}); + expect(filePath).toBe(configFilePath); });