From aa9b13fdac304abe532297859936d9e08c2b7d05 Mon Sep 17 00:00:00 2001 From: neverland Date: Fri, 30 Oct 2026 10:55:34 +0800 Subject: [PATCH 1/2] refactor(config): move CLI path fallback to callers --- packages/rstack/src/cli/commands.ts | 2 +- packages/rstack/src/cli/config.ts | 35 ++++++++++++++++ packages/rstack/src/config.ts | 41 ++----------------- packages/rstack/src/fmt/cli.ts | 5 ++- packages/rstack/src/rsbuildConfig.ts | 5 ++- packages/rstack/src/rslibConfig.ts | 5 ++- packages/rstack/src/rslintConfig.ts | 9 ++-- packages/rstack/src/rspressConfig.ts | 5 ++- packages/rstack/src/rstestConfig.ts | 7 ++-- packages/rstack/src/staged.ts | 4 +- .../rstack/tests/cli/staged/index.test.ts | 8 ++-- .../tests/config/load-config/index.test.ts | 14 ++++--- .../tests/config/project-loader/index.test.ts | 6 +++ website/docs/en/guide/api-reference.mdx | 2 +- website/docs/zh/guide/api-reference.mdx | 2 +- 15 files changed, 80 insertions(+), 70 deletions(-) create mode 100644 packages/rstack/src/cli/config.ts diff --git a/packages/rstack/src/cli/commands.ts b/packages/rstack/src/cli/commands.ts index 0f935fbf..eca7fc08 100644 --- a/packages/rstack/src/cli/commands.ts +++ b/packages/rstack/src/cli/commands.ts @@ -1,6 +1,6 @@ import { join, resolve } from 'node:path'; import { pathToFileURL } from 'node:url'; -import { CLI_CONFIG_PATH_ENV, getConfigState } from '../config.ts'; +import { CLI_CONFIG_PATH_ENV, getConfigState } from './config.ts'; import { insertConfigArg, parseArgs, parseCliArgs } from './args.ts'; import { hasHelpFlag, printCommandHelp } from './help.ts'; diff --git a/packages/rstack/src/cli/config.ts b/packages/rstack/src/cli/config.ts new file mode 100644 index 00000000..39ef0905 --- /dev/null +++ b/packages/rstack/src/cli/config.ts @@ -0,0 +1,35 @@ +import { + loadRstackConfig, + type LoadedRstackConfig, + type LoadRstackConfigOptions, +} from '../config.ts'; + +type ConfigState = { + /** + * Absolute config path from the global `--config` flag, resolved at parse + * time so a later cwd (such as an LSP workspace root) cannot change it. + */ + configPath?: string; +}; + +declare global { + // rslint-disable-next-line no-var + var __rstackCliState: ConfigState | undefined; +} + +export const getConfigState = (): ConfigState => { + // The CLI and internal tool configs may be loaded as separate module instances. + return (globalThis.__rstackCliState ??= {}); +}; + +/** Passes `--config` to Rslint's JS plugin workers, which run in a fresh realm. */ +export const CLI_CONFIG_PATH_ENV = 'RSTACK_CLI_CONFIG_PATH'; + +/** Loads a root tool config using the CLI path unless explicitly overridden. */ +export const loadCliRstackConfig = ( + options: LoadRstackConfigOptions = {}, +): Promise => + loadRstackConfig({ + ...options, + configFilePath: options.configFilePath ?? getConfigState().configPath, + }); diff --git a/packages/rstack/src/config.ts b/packages/rstack/src/config.ts index 74b54c25..6c694b8d 100644 --- a/packages/rstack/src/config.ts +++ b/packages/rstack/src/config.ts @@ -84,11 +84,9 @@ export type LoadRstackConfigOptions = { /** * The path to the Rstack config file, can be a relative or absolute path. * A relative path is resolved from `cwd`. - * If `configFilePath` is omitted, the config path set by the CLI is used. - * If neither path is provided, the function will search for the config file in `cwd`. - * Pass `null` to search `cwd` and ignore the CLI config path. + * If omitted, the function searches for the config file in `cwd`. */ - configFilePath?: string | null; + configFilePath?: string; /** * The directory the config file is searched in and relative config paths are resolved from. * Defaults to the current working directory. @@ -102,21 +100,10 @@ type ConfigSession = { active: boolean; }; -type ConfigState = { - /** - * Config file path from the global `--config` flag. Always absolute: the CLI - * resolves it at parse time so it stays independent of later cwd choices - * (`loadRstackConfig` may be called with an LSP workspace root as `cwd`). - */ - configPath?: string; -}; - declare global { // rslint-disable-next-line no-var var __rstackConfigSessionStorage: AsyncLocalStorage | undefined; - // rslint-disable-next-line no-var - var __rstackCliState: ConfigState | undefined; } const getConfigSessionStorage = (): AsyncLocalStorage => { @@ -131,23 +118,6 @@ const getConfigSessionStorage = (): AsyncLocalStorage => { return globalThis.__rstackConfigSessionStorage; }; -export const getConfigState = (): ConfigState => { - // The CLI and its internal tool config can also be loaded as separate module - // instances. Keep only the CLI config path in its own global state. - if (!globalThis.__rstackCliState) { - globalThis.__rstackCliState = {}; - } - - return globalThis.__rstackCliState; -}; - -/** - * Passes the `--config` path to Rslint's JS plugin workers, which import the - * internal lint config in a fresh realm without the CLI state above. Only that - * config reads it. - */ -export const CLI_CONFIG_PATH_ENV = 'RSTACK_CLI_CONFIG_PATH'; - type Define = { /** * Inherits shared configs from left to right, followed by the project's own @@ -269,9 +239,6 @@ export const loadRstackConfig = async ({ configFilePath, cwd, }: LoadRstackConfigOptions = {}): Promise => { - const state = getConfigState(); - const configPath = - configFilePath === null ? undefined : (configFilePath ?? state.configPath); const session: ConfigSession = { configs: {}, active: true, @@ -284,8 +251,8 @@ export const loadRstackConfig = async ({ exportName: false, fresh: true, cwd, - ...(configPath !== undefined - ? { path: configPath } + ...(configFilePath !== undefined + ? { path: configFilePath } : { configFileNames: [ 'rstack.config.ts', diff --git a/packages/rstack/src/fmt/cli.ts b/packages/rstack/src/fmt/cli.ts index ec47edf2..4e3f8dba 100644 --- a/packages/rstack/src/fmt/cli.ts +++ b/packages/rstack/src/fmt/cli.ts @@ -3,7 +3,8 @@ import { performance } from 'node:perf_hooks'; import { color, logger } from 'rslog'; import { parseArgs } from '../cli/args.ts'; import { printCommandHelp } from '../cli/help.ts'; -import { loadRstackConfig, type LoadedRstackConfig } from '../config.ts'; +import type { LoadedRstackConfig } from '../config.ts'; +import { loadCliRstackConfig } from '../cli/config.ts'; import { ensureProjectCacheDir } from '../projectCache.ts'; import { fmtCacheFileName } from './cacheStore.ts'; import { resolveFmtConfig } from './config.ts'; @@ -250,7 +251,7 @@ const loadFmtConfig = async ( loadedConfig?: LoadedRstackConfig, ): Promise => { const { configs, filePath } = - loadedConfig ?? (await loadRstackConfig({ cwd })); + loadedConfig ?? (await loadCliRstackConfig({ cwd })); return resolveFmtConfig({ layers: [configs], diff --git a/packages/rstack/src/rsbuildConfig.ts b/packages/rstack/src/rsbuildConfig.ts index 84207472..57a31326 100644 --- a/packages/rstack/src/rsbuildConfig.ts +++ b/packages/rstack/src/rsbuildConfig.ts @@ -5,7 +5,8 @@ import { mergeRsbuildConfig, } from '@rsbuild/core'; import { withConfigMeta } from '@rstackjs/load-config'; -import { loadRstackConfig, type RstackConfigDefinitions } from './config.ts'; +import type { RstackConfigDefinitions } from './config.ts'; +import { loadCliRstackConfig } from './cli/config.ts'; import { resolveConfigLayers } from './configLayers.ts'; export const resolveRsbuildConfig = async ( @@ -19,7 +20,7 @@ export const resolveRsbuildConfig = async ( }; const loadRsbuildConfig: RsbuildConfigDefinition = async (params) => { - const { configs, filePath, dependencies } = await loadRstackConfig(); + const { configs, filePath, dependencies } = await loadCliRstackConfig(); const config = await resolveRsbuildConfig([configs], params); return withConfigMeta(config, { filePath, dependencies }); diff --git a/packages/rstack/src/rslibConfig.ts b/packages/rstack/src/rslibConfig.ts index 749d30b3..f395804c 100644 --- a/packages/rstack/src/rslibConfig.ts +++ b/packages/rstack/src/rslibConfig.ts @@ -5,7 +5,8 @@ import { mergeRslibConfig, } from '@rslib/core'; import { withConfigMeta } from '@rstackjs/load-config'; -import { loadRstackConfig, type RstackConfigDefinitions } from './config.ts'; +import type { RstackConfigDefinitions } from './config.ts'; +import { loadCliRstackConfig } from './cli/config.ts'; import { resolveConfigLayers } from './configLayers.ts'; export const resolveRslibConfig = async ( @@ -17,7 +18,7 @@ export const resolveRslibConfig = async ( }; const loadRslibConfig = (async (params: ConfigParams) => { - const { configs, filePath, dependencies } = await loadRstackConfig(); + const { configs, filePath, dependencies } = await loadCliRstackConfig(); const config = await resolveRslibConfig([configs], params); return withConfigMeta(config, { filePath, dependencies }); diff --git a/packages/rstack/src/rslintConfig.ts b/packages/rstack/src/rslintConfig.ts index 38dce415..e9a38cfd 100644 --- a/packages/rstack/src/rslintConfig.ts +++ b/packages/rstack/src/rslintConfig.ts @@ -1,10 +1,7 @@ import { existsSync } from 'node:fs'; import { join } from 'node:path'; -import { - CLI_CONFIG_PATH_ENV, - loadRstackConfig, - type LoadedRstackConfig, -} from './config.ts'; +import { CLI_CONFIG_PATH_ENV, loadCliRstackConfig } from './cli/config.ts'; +import type { LoadedRstackConfig } from './config.ts'; import type { RslintConfig } from '@rslint/core'; import { color, logger } from 'rslog'; import { resolveRslintConfig } from './configLayers.ts'; @@ -12,7 +9,7 @@ import { resolveRslintConfig } from './configLayers.ts'; // Expose the loaded config so `rs check` can pass it to fmt instead of loading // and executing the Rstack config a second time. Rslint also imports this // module in its JS plugin workers, which get `--config` from the environment. -export const loadedConfig: LoadedRstackConfig = await loadRstackConfig({ +export const loadedConfig: LoadedRstackConfig = await loadCliRstackConfig({ configFilePath: process.env[CLI_CONFIG_PATH_ENV], }); const { configs } = loadedConfig; diff --git a/packages/rstack/src/rspressConfig.ts b/packages/rstack/src/rspressConfig.ts index afa5f0cd..4ff2e313 100644 --- a/packages/rstack/src/rspressConfig.ts +++ b/packages/rstack/src/rspressConfig.ts @@ -1,6 +1,7 @@ import type { WatchFiles } from '@rsbuild/core'; import type { UserConfig } from '@rspress/core'; -import { loadRstackConfig, type RstackConfigDefinitions } from './config.ts'; +import type { RstackConfigDefinitions } from './config.ts'; +import { loadCliRstackConfig } from './cli/config.ts'; import { resolveConfigLayers } from './configLayers.ts'; export const resolveRspressConfig = async ( @@ -16,7 +17,7 @@ export const resolveRspressConfig = async ( }; export default async (): Promise => { - const { configs, filePath, dependencies } = await loadRstackConfig(); + const { configs, filePath, dependencies } = await loadCliRstackConfig(); const config = await resolveRspressConfig([configs]); if (!filePath) { diff --git a/packages/rstack/src/rstestConfig.ts b/packages/rstack/src/rstestConfig.ts index ae86ff87..5a0442be 100644 --- a/packages/rstack/src/rstestConfig.ts +++ b/packages/rstack/src/rstestConfig.ts @@ -7,6 +7,7 @@ import { } from '@rstest/core'; import { type LoadProjectConfig, PROJECT_LOADER } from '@rstest/core/api'; import { loadRstackConfig, type RstackConfigDefinitions } from './config.ts'; +import { loadCliRstackConfig } from './cli/config.ts'; import { resolveConfigLayers } from './configLayers.ts'; const resolveAutomaticExtends = async ( @@ -120,11 +121,9 @@ export const resolveRstestConfig = async ( const createProjectLoader = (params: ConfigParams): LoadProjectConfig => async ({ dirPath, filePath }) => { - // `null` searches the project directory with the default config file names - // instead of reusing the `rs --config` path that loads the root config. const loaded = await loadRstackConfig({ cwd: dirPath, - configFilePath: filePath ?? null, + configFilePath: filePath, }); if (loaded.filePath === null) { return null; @@ -140,7 +139,7 @@ const createProjectLoader = }; const loadRstestConfig = (async (params: ConfigParams) => { - const { configs } = await loadRstackConfig(); + const { configs } = await loadCliRstackConfig(); const config = await resolveRstestConfig([configs], params); return { ...config, diff --git a/packages/rstack/src/staged.ts b/packages/rstack/src/staged.ts index ff45c2f7..1eae82fc 100644 --- a/packages/rstack/src/staged.ts +++ b/packages/rstack/src/staged.ts @@ -1,7 +1,7 @@ import lintStaged from 'lint-staged'; import { parseArgs } from './cli/args.ts'; import { printCommandHelp } from './cli/help.ts'; -import { loadRstackConfig } from './config.ts'; +import { loadCliRstackConfig } from './cli/config.ts'; import { resolveStagedConfig } from './configLayers.ts'; export type StagedSyncTaskGenerator = ( @@ -51,7 +51,7 @@ export async function runStagedCLI(args: string[]): Promise { return; } - const { configs } = await loadRstackConfig(); + const { configs } = await loadCliRstackConfig(); const stagedConfig = await resolveStagedConfig([configs]); if (stagedConfig === undefined) { throw new Error( diff --git a/packages/rstack/tests/cli/staged/index.test.ts b/packages/rstack/tests/cli/staged/index.test.ts index b43a928b..fd1800a2 100644 --- a/packages/rstack/tests/cli/staged/index.test.ts +++ b/packages/rstack/tests/cli/staged/index.test.ts @@ -1,15 +1,15 @@ import lintStaged from 'lint-staged'; import { afterEach, beforeEach, rs } from 'rstack/test'; import { normalizeHelpOutput, test } from '#test-helpers'; -import { loadRstackConfig } from '../../../src/config.ts'; +import { loadCliRstackConfig } from '../../../src/cli/config.ts'; import { runStagedCLI, type StagedConfig } from '../../../src/staged.ts'; rs.mock('lint-staged'); -rs.mock('../../../src/config.ts'); +rs.mock('../../../src/cli/config.ts'); const mocks = { lintStaged: rs.mocked(lintStaged), - loadRstackConfig: rs.mocked(loadRstackConfig), + loadCliRstackConfig: rs.mocked(loadCliRstackConfig), }; const stagedConfig: StagedConfig = { @@ -20,7 +20,7 @@ beforeEach(() => { delete process.env.RSTACK_STAGED; rs.resetAllMocks(); mocks.lintStaged.mockResolvedValue(true); - mocks.loadRstackConfig.mockResolvedValue({ + mocks.loadCliRstackConfig.mockResolvedValue({ configs: { staged: stagedConfig }, filePath: null, dependencies: [], diff --git a/packages/rstack/tests/config/load-config/index.test.ts b/packages/rstack/tests/config/load-config/index.test.ts index 2f7c5da7..8d3f861d 100644 --- a/packages/rstack/tests/config/load-config/index.test.ts +++ b/packages/rstack/tests/config/load-config/index.test.ts @@ -1,10 +1,10 @@ import path from 'node:path'; import { afterEach, expect, test } from 'rstack/test'; +import { define, loadRstackConfig } from '../../../src/config.ts'; import { - define, getConfigState, - loadRstackConfig, -} from '../../../src/config.ts'; + loadCliRstackConfig, +} from '../../../src/cli/config.ts'; type Deferred = ReturnType>; @@ -38,7 +38,7 @@ afterEach(() => { test('should discard a config session after loading fails', async () => { state.configPath = configPath('rstack.config.ts'); - await expect(loadRstackConfig()).rejects.toThrow('test config error'); + await expect(loadCliRstackConfig()).rejects.toThrow('test config error'); const { configs } = await loadConfigFile('explicit.config.ts'); expect(configs).toEqual({ @@ -50,7 +50,7 @@ test('should prefer an explicit config path over the state config path', async ( const explicitConfigPath = configPath('explicit.config.ts'); state.configPath = configPath('rstack.config.ts'); - const { configs, filePath } = await loadRstackConfig({ + const { configs, filePath } = await loadCliRstackConfig({ configFilePath: explicitConfigPath, }); @@ -86,7 +86,9 @@ test('should resolve a relative explicit config path from cwd', async () => { expect(filePath).toBe(configPath('explicit.config.ts')); }); -test('should search for the config file in cwd', async () => { +test('should search cwd without inheriting the CLI config path', async () => { + state.configPath = configPath('explicit.config.ts'); + await expect(loadRstackConfig({ cwd: import.meta.dirname })).rejects.toThrow( 'test config error', ); diff --git a/packages/rstack/tests/config/project-loader/index.test.ts b/packages/rstack/tests/config/project-loader/index.test.ts index 96d4eab0..150ddb7a 100644 --- a/packages/rstack/tests/config/project-loader/index.test.ts +++ b/packages/rstack/tests/config/project-loader/index.test.ts @@ -4,6 +4,12 @@ test('should load each project from its own Rstack config', ({ execCli }) => { execCli('test'); }); +test('should load directory projects independently of the CLI config path', ({ + execCli, +}) => { + execCli('test --config rstack.config.ts'); +}); + test('should load file project entries from their Rstack configs', ({ execCli, }) => { diff --git a/website/docs/en/guide/api-reference.mdx b/website/docs/en/guide/api-reference.mdx index 4e63f2bd..393fa918 100644 --- a/website/docs/en/guide/api-reference.mdx +++ b/website/docs/en/guide/api-reference.mdx @@ -120,7 +120,7 @@ const { configs, filePath, dependencies } = await loadRstackConfig({ The function accepts these optional parameters: - `cwd`: the directory to search for a configuration file. Relative `configFilePath` values are also resolved from this directory. Defaults to the current working directory. -- `configFilePath`: a relative or absolute path to the configuration file. If omitted, the loader uses the path specified by the CLI's `--config` option. If neither is set, it searches `cwd` for the [default file names](./configuration#configuration-file). +- `configFilePath`: a relative or absolute path to the configuration file. If omitted, it searches `cwd` for the [default file names](./configuration#configuration-file). This API does not read the CLI's `--config` option. The returned object has these fields: diff --git a/website/docs/zh/guide/api-reference.mdx b/website/docs/zh/guide/api-reference.mdx index 4d2c3fd8..ae30278f 100644 --- a/website/docs/zh/guide/api-reference.mdx +++ b/website/docs/zh/guide/api-reference.mdx @@ -120,7 +120,7 @@ const { configs, filePath, dependencies } = await loadRstackConfig({ 支持以下可选参数: - `cwd`:查找配置文件的目录。`configFilePath` 为相对路径时,也以此目录为基准。默认为当前工作目录。 -- `configFilePath`:配置文件路径,可以是相对路径或绝对路径。省略时,优先使用 CLI 通过 `--config` 指定的路径;若也未指定,则在 `cwd` 中按[默认文件名](./configuration#configuration-file)查找配置。 +- `configFilePath`:配置文件路径,可以是相对路径或绝对路径。省略时,在 `cwd` 中按[默认文件名](./configuration#configuration-file)查找配置。此 API 不读取 CLI 的 `--config` 选项。 返回对象包含以下字段: From 93c64b4010c69658cbe3167a11d0392b1e189728 Mon Sep 17 00:00:00 2001 From: neverland Date: Mon, 2 Nov 2026 14:05:09 +0800 Subject: [PATCH 2/2] docs: simplify config path description --- website/docs/en/guide/api-reference.mdx | 2 +- website/docs/zh/guide/api-reference.mdx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/website/docs/en/guide/api-reference.mdx b/website/docs/en/guide/api-reference.mdx index 393fa918..826ddf83 100644 --- a/website/docs/en/guide/api-reference.mdx +++ b/website/docs/en/guide/api-reference.mdx @@ -120,7 +120,7 @@ const { configs, filePath, dependencies } = await loadRstackConfig({ The function accepts these optional parameters: - `cwd`: the directory to search for a configuration file. Relative `configFilePath` values are also resolved from this directory. Defaults to the current working directory. -- `configFilePath`: a relative or absolute path to the configuration file. If omitted, it searches `cwd` for the [default file names](./configuration#configuration-file). This API does not read the CLI's `--config` option. +- `configFilePath`: a relative or absolute path to the configuration file. If omitted, it searches `cwd` for the [default file names](./configuration#configuration-file). The returned object has these fields: diff --git a/website/docs/zh/guide/api-reference.mdx b/website/docs/zh/guide/api-reference.mdx index ae30278f..a90d84d7 100644 --- a/website/docs/zh/guide/api-reference.mdx +++ b/website/docs/zh/guide/api-reference.mdx @@ -120,7 +120,7 @@ const { configs, filePath, dependencies } = await loadRstackConfig({ 支持以下可选参数: - `cwd`:查找配置文件的目录。`configFilePath` 为相对路径时,也以此目录为基准。默认为当前工作目录。 -- `configFilePath`:配置文件路径,可以是相对路径或绝对路径。省略时,在 `cwd` 中按[默认文件名](./configuration#configuration-file)查找配置。此 API 不读取 CLI 的 `--config` 选项。 +- `configFilePath`:配置文件路径,可以是相对路径或绝对路径。省略时,在 `cwd` 中按[默认文件名](./configuration#configuration-file)查找配置。 返回对象包含以下字段: