diff --git a/packages/rstack/rstack.config.ts b/packages/rstack/rstack.config.ts index cbcfa566..7967b7a9 100644 --- a/packages/rstack/rstack.config.ts +++ b/packages/rstack/rstack.config.ts @@ -4,8 +4,13 @@ import { define } from 'rstack'; define.test(() => { return { - // Temporary projects may contain files that match Rstest's test glob. - exclude: ['**/test-temp-*/**'], + // Temporary projects and fixture projects may contain files that match + // Rstest's test glob. + exclude: [ + '**/test-temp-*/**', + // Project-loader fixtures run the CLI with their own test globs. + '**/tests/config/project-loader/packages/**', + ], extends: withRslibConfig(), testTimeout: 30_000, source: { diff --git a/packages/rstack/src/config.ts b/packages/rstack/src/config.ts index 904f5aad..74b54c25 100644 --- a/packages/rstack/src/config.ts +++ b/packages/rstack/src/config.ts @@ -84,10 +84,11 @@ 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 not provided, the config path set by the CLI is used. + * 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. */ - configFilePath?: string; + configFilePath?: string | null; /** * The directory the config file is searched in and relative config paths are resolved from. * Defaults to the current working directory. @@ -269,7 +270,8 @@ export const loadRstackConfig = async ({ cwd, }: LoadRstackConfigOptions = {}): Promise => { const state = getConfigState(); - const configPath = configFilePath ?? state.configPath; + const configPath = + configFilePath === null ? undefined : (configFilePath ?? state.configPath); const session: ConfigSession = { configs: {}, active: true, diff --git a/packages/rstack/src/rstestConfig.ts b/packages/rstack/src/rstestConfig.ts index 34700532..ae86ff87 100644 --- a/packages/rstack/src/rstestConfig.ts +++ b/packages/rstack/src/rstestConfig.ts @@ -1,15 +1,18 @@ +import { normalize } from 'node:path'; import type { ConfigParams } from '@rsbuild/core'; import { type RstestConfig, type RstestConfigExport, mergeRstestConfig, } from '@rstest/core'; +import { type LoadProjectConfig, PROJECT_LOADER } from '@rstest/core/api'; import { loadRstackConfig, type RstackConfigDefinitions } from './config.ts'; import { resolveConfigLayers } from './configLayers.ts'; const resolveAutomaticExtends = async ( layers: readonly RstackConfigDefinitions[], params: ConfigParams, + cwd?: string, ): Promise => { // Prefer the app when both app and lib are defined. Merging both adapters can // introduce conflicting runtime, resolve, and source transform settings. @@ -34,6 +37,10 @@ const resolveAutomaticExtends = async ( return withRslibConfig({ config: await resolveRslibConfig(layers, params), + // The adapter reads the project's tsconfig for decorator detection. It + // falls back to `process.cwd()`, so external projects need their own + // directory here or they would inherit the root tsconfig. + cwd, }); } @@ -58,13 +65,14 @@ const extendsConfig = async ( layers: readonly RstackConfigDefinitions[], testConfig: RstestConfig, params: ConfigParams, + cwd?: string, ) => { if ('extends' in testConfig) { return testConfig; } if (testConfig.projects === undefined) { - const automaticExtends = await resolveAutomaticExtends(layers, params); + const automaticExtends = await resolveAutomaticExtends(layers, params, cwd); return injectExtends(testConfig, automaticExtends); } @@ -75,7 +83,7 @@ const extendsConfig = async ( return testConfig; } - const automaticExtends = await resolveAutomaticExtends(layers, params); + const automaticExtends = await resolveAutomaticExtends(layers, params, cwd); return { ...testConfig, @@ -99,14 +107,45 @@ export const mergeRstestConfigLayers = async ( export const resolveRstestConfig = async ( layers: readonly RstackConfigDefinitions[], params: ConfigParams, + cwd?: string, ): Promise => { const testConfig = await mergeRstestConfigLayers(layers); - return extendsConfig(layers, testConfig, params); + return extendsConfig(layers, testConfig, params, cwd); }; +// Rstest reads the host loader only from the root config and calls it for every +// directory or file a string `projects` entry expands to. Rstack does not load +// native tool configs anywhere else, so a project without an Rstack config +// becomes an empty project instead of falling back to `rstest.config.*`. +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, + }); + if (loaded.filePath === null) { + return null; + } + + return { + content: await resolveRstestConfig([loaded.configs], params, dirPath), + // Rstest dedupes projects by this string. Directory and file entries can + // reach the same file with different path separators, so normalize. + filePath: normalize(loaded.filePath), + dependencies: loaded.dependencies, + }; + }; + const loadRstestConfig = (async (params: ConfigParams) => { const { configs } = await loadRstackConfig(); - return resolveRstestConfig([configs], params); + const config = await resolveRstestConfig([configs], params); + return { + ...config, + [PROJECT_LOADER]: createProjectLoader(params), + }; }) as RstestConfigExport; export default loadRstestConfig; diff --git a/packages/rstack/tests/config/project-loader/index.test.ts b/packages/rstack/tests/config/project-loader/index.test.ts new file mode 100644 index 00000000..96d4eab0 --- /dev/null +++ b/packages/rstack/tests/config/project-loader/index.test.ts @@ -0,0 +1,11 @@ +import { test } from '#test-helpers'; + +test('should load each project from its own Rstack config', ({ execCli }) => { + execCli('test'); +}); + +test('should load file project entries from their Rstack configs', ({ + execCli, +}) => { + execCli('test --config rstack.file-entries.config.ts'); +}); diff --git a/packages/rstack/tests/config/project-loader/packages/pkg-a/entry.ts b/packages/rstack/tests/config/project-loader/packages/pkg-a/entry.ts new file mode 100644 index 00000000..b759ec4f --- /dev/null +++ b/packages/rstack/tests/config/project-loader/packages/pkg-a/entry.ts @@ -0,0 +1,10 @@ +import { expect, test } from 'rstack/test'; + +declare const PROJECT_LABEL: string; + +test('should inherit the package app and test config', () => { + expect(PROJECT_LABEL).toBe('pkg-a'); + expect(typeof (globalThis as Record).describe).toBe( + 'function', + ); +}); diff --git a/packages/rstack/tests/config/project-loader/packages/pkg-a/ignored.test.ts b/packages/rstack/tests/config/project-loader/packages/pkg-a/ignored.test.ts new file mode 100644 index 00000000..8b07a3d3 --- /dev/null +++ b/packages/rstack/tests/config/project-loader/packages/pkg-a/ignored.test.ts @@ -0,0 +1,3 @@ +// This file must not be collected: the package config's `include` only matches +// `entry.ts`, so importing it means the project config was not applied. +throw new Error('pkg-a include was not applied'); diff --git a/packages/rstack/tests/config/project-loader/packages/pkg-a/rstack.config.ts b/packages/rstack/tests/config/project-loader/packages/pkg-a/rstack.config.ts new file mode 100644 index 00000000..c38b031e --- /dev/null +++ b/packages/rstack/tests/config/project-loader/packages/pkg-a/rstack.config.ts @@ -0,0 +1,14 @@ +import { define } from 'rstack'; + +define.app({ + source: { + define: { + PROJECT_LABEL: JSON.stringify('pkg-a'), + }, + }, +}); + +define.test({ + globals: true, + include: ['./entry.ts'], +}); diff --git a/packages/rstack/tests/config/project-loader/packages/pkg-native/native.test.ts b/packages/rstack/tests/config/project-loader/packages/pkg-native/native.test.ts new file mode 100644 index 00000000..a1d50193 --- /dev/null +++ b/packages/rstack/tests/config/project-loader/packages/pkg-native/native.test.ts @@ -0,0 +1,7 @@ +import { expect, test } from 'rstack/test'; + +test('should use the default config of an empty project', () => { + expect(typeof (globalThis as Record).describe).toBe( + 'undefined', + ); +}); diff --git a/packages/rstack/tests/config/project-loader/packages/pkg-native/rstest.config.ts b/packages/rstack/tests/config/project-loader/packages/pkg-native/rstest.config.ts new file mode 100644 index 00000000..0afeb319 --- /dev/null +++ b/packages/rstack/tests/config/project-loader/packages/pkg-native/rstest.config.ts @@ -0,0 +1,3 @@ +// Rstack does not fall back to native tool config files. This file must never +// be loaded for a project. +throw new Error('rstest.config.ts must not be loaded by Rstack'); diff --git a/packages/rstack/tests/config/project-loader/rstack.config.ts b/packages/rstack/tests/config/project-loader/rstack.config.ts new file mode 100644 index 00000000..ad2fbfad --- /dev/null +++ b/packages/rstack/tests/config/project-loader/rstack.config.ts @@ -0,0 +1,5 @@ +import { define } from 'rstack'; + +define.test({ + projects: ['packages/*'], +}); diff --git a/packages/rstack/tests/config/project-loader/rstack.file-entries.config.ts b/packages/rstack/tests/config/project-loader/rstack.file-entries.config.ts new file mode 100644 index 00000000..20942946 --- /dev/null +++ b/packages/rstack/tests/config/project-loader/rstack.file-entries.config.ts @@ -0,0 +1,5 @@ +import { define } from 'rstack'; + +define.test({ + projects: ['packages/*/rstack.config.ts'], +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 00cc2d4a..c2ae9166 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -208,8 +208,8 @@ catalogs: specifier: ~0.12.5 version: 0.12.5 '@rstest/core': - specifier: ~0.12.4 - version: 0.12.4 + specifier: ~0.12.5 + version: 0.12.5 '@shikijs/transformers': specifier: ^4.5.0 version: 4.5.0 @@ -530,7 +530,7 @@ importers: version: 0.9.5(jiti@2.7.0) '@rstest/core': specifier: 'catalog:' - version: 0.12.4(happy-dom@20.14.5) + version: 0.12.5(happy-dom@20.14.5) '@swc-next/parser': specifier: 'catalog:' version: 0.4.0 @@ -555,10 +555,10 @@ importers: version: 1.0.0 '@rstest/adapter-rsbuild': specifier: 'catalog:' - version: 0.12.5(@rsbuild/core@2.2.14)(@rstest/core@0.12.4) + version: 0.12.5(@rsbuild/core@2.2.14)(@rstest/core@0.12.5) '@rstest/adapter-rslib': specifier: 'catalog:' - version: 0.12.5(@rslib/core@1.0.3)(@rstest/core@0.12.4)(typescript@7.0.2) + version: 0.12.5(@rslib/core@1.0.3)(@rstest/core@0.12.5)(typescript@7.0.2) '@types/micromatch': specifier: 'catalog:' version: 4.0.10 @@ -1679,8 +1679,8 @@ packages: typescript: optional: true - '@rstest/core@0.12.4': - resolution: {integrity: sha512-NL+lxZ6GiJkL2BYgUR8WU2awQr71GiIzG0rDNKljvhEhmIX61YUoG6qjvLlEFJsvmB/AZmKIYwP/PyFTHZZy1A==} + '@rstest/core@0.12.5': + resolution: {integrity: sha512-qJ+3QOVcuB8DTGXi+OH2TSxruXaiz5xvDDrakyFNpjz580FFDWFR+1Wpg1upa22R6Q0yp/vkxjEveKDIzdBj5w==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true peerDependencies: @@ -4465,19 +4465,19 @@ snapshots: '@rstackjs/test-utils@1.0.0': {} - '@rstest/adapter-rsbuild@0.12.5(@rsbuild/core@2.2.14)(@rstest/core@0.12.4)': + '@rstest/adapter-rsbuild@0.12.5(@rsbuild/core@2.2.14)(@rstest/core@0.12.5)': dependencies: '@rsbuild/core': 2.2.14 - '@rstest/core': 0.12.4(happy-dom@20.14.5) + '@rstest/core': 0.12.5(happy-dom@20.14.5) - '@rstest/adapter-rslib@0.12.5(@rslib/core@1.0.3)(@rstest/core@0.12.4)(typescript@7.0.2)': + '@rstest/adapter-rslib@0.12.5(@rslib/core@1.0.3)(@rstest/core@0.12.5)(typescript@7.0.2)': dependencies: '@rslib/core': 1.0.3(typescript@7.0.2) - '@rstest/core': 0.12.4(happy-dom@20.14.5) + '@rstest/core': 0.12.5(happy-dom@20.14.5) optionalDependencies: typescript: 7.0.2 - '@rstest/core@0.12.4(happy-dom@20.14.5)': + '@rstest/core@0.12.5(happy-dom@20.14.5)': dependencies: '@rsbuild/core': 2.2.14 '@types/chai': 5.2.3 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 78f17fc0..817ea186 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -28,7 +28,7 @@ catalog: '@rstackjs/test-utils': ^1.0.0 '@rstest/adapter-rsbuild': '~0.12.5' '@rstest/adapter-rslib': '~0.12.5' - '@rstest/core': '~0.12.4' + '@rstest/core': '~0.12.5' '@swc-next/parser': '0.4.0' '@testing-library/dom': '^10.4.2' '@testing-library/jest-dom': '^7.0.1' diff --git a/website/docs/en/guide/configuration.mdx b/website/docs/en/guide/configuration.mdx index 68844127..bd2cc931 100644 --- a/website/docs/en/guide/configuration.mdx +++ b/website/docs/en/guide/configuration.mdx @@ -157,7 +157,7 @@ define.test({ When `extends` is omitted, Rstack CLI automatically connects the test configuration to `define.app()` through the Rsbuild adapter. If no application configuration is defined, it falls back to `define.lib()` through the Rslib adapter. The application configuration takes precedence when both are defined. Set `extends` explicitly to opt out of this automatic inheritance. -If the root test configuration does not define `extends` and contains `projects`, Rstack CLI applies automatic inheritance to each inline project that omits its own `extends`. A function-based application or library configuration is resolved once and shared by those projects. String project entries are passed to Rstest unchanged; they load their external configurations independently and do not inherit the current application or library configuration. +If the root test configuration does not define `extends` and contains `projects`, Rstack CLI applies automatic inheritance to each inline project that omits its own `extends`. A function-based application or library configuration is resolved once and shared by those projects. String project entries use each project's own Rstack config instead of the root configuration. These rules also apply to [shared configurations](#shared-configurations): Rstack first merges the test settings, then determines whether to inherit the merged App or Lib configuration. diff --git a/website/docs/en/guide/testing.mdx b/website/docs/en/guide/testing.mdx index c5a11fa9..d0568acd 100644 --- a/website/docs/en/guide/testing.mdx +++ b/website/docs/en/guide/testing.mdx @@ -107,11 +107,11 @@ define.test({ }); ``` -For multiple projects, setting `extends` on the root `define.test()` configuration disables automatic inheritance for every project. Setting it on an inline project disables inheritance only for that project. +For multiple projects, setting `extends` on the root `define.test()` configuration disables automatic inheritance for its inline projects. Setting it on an inline project disables inheritance only for that project. External projects resolve inheritance from their own Rstack config. ## Multiple projects -Set Rstest's [`projects`](https://rstest.rs/config/test/projects) option to run multiple test configurations together. Entries can be inline projects or strings that Rstest resolves as external projects. +Set Rstest's [`projects`](https://rstest.rs/config/test/projects) option to run multiple test configurations together. Entries can be inline projects or strings that point to a project directory or its Rstack config file. ### Inline projects @@ -153,14 +153,24 @@ See [`examples/test-inline-projects`](https://github.com/rstackjs/rstack-cli/tre ### External projects -Use a string entry for an externally configured project: +Use a string entry when a project keeps its test configuration in its own Rstack config file: ```ts title="rstack.config.ts" import { define } from 'rstack'; define.test({ - projects: ['./legacy/rstest.config.ts'], + projects: ['./packages/*'], }); ``` -Rstack CLI passes string entries to Rstest unchanged. External projects load their own configuration and do not inherit the current `define.app()` or `define.lib()` configuration. Use external projects when each project manages its configuration independently. +A directory entry loads `rstack.config.*` from that directory. To list a config file explicitly, use a file entry: + +```ts title="rstack.config.ts" +import { define } from 'rstack'; + +define.test({ + projects: ['./packages/*/rstack.config.ts'], +}); +``` + +Rstack CLI uses each project's own Rstack config to resolve string entries. A directory without a Rstack config file uses Rstest's default configuration. `rstest.config.ts` is not loaded for projects. In watch mode, changing a project's Rstack config file restarts the run. diff --git a/website/docs/zh/guide/configuration.mdx b/website/docs/zh/guide/configuration.mdx index ddcbfb7c..94cd5c54 100644 --- a/website/docs/zh/guide/configuration.mdx +++ b/website/docs/zh/guide/configuration.mdx @@ -157,7 +157,7 @@ define.test({ 未设置 `extends` 时,Rstack CLI 会通过 Rsbuild 适配器让测试配置自动继承 `define.app()`;如果未定义应用配置,则通过 Rslib 适配器回退到 `define.lib()`。二者同时存在时,应用配置的优先级更高。显式设置 `extends` 可关闭自动继承。 -如果测试根配置未定义 `extends` 且包含 `projects`,Rstack CLI 会为每个未自行设置 `extends` 的内联项目应用自动继承。函数形式的应用或库配置只会解析一次,并由这些项目共享。字符串形式的项目会原样传给 Rstest;它们会独立加载外部配置,不继承当前应用或库的配置。 +如果测试根配置未定义 `extends` 且包含 `projects`,Rstack CLI 会为每个未自行设置 `extends` 的内联项目应用自动继承。函数形式的应用或库配置只会解析一次,并由这些项目共享。字符串形式的项目使用各自的 Rstack 配置,不使用根配置。 这些规则同样适用于[公共配置](#shared-configurations):Rstack 先合并测试配置,再判断是否需要继承合并后的 App 或 Lib 配置。 diff --git a/website/docs/zh/guide/testing.mdx b/website/docs/zh/guide/testing.mdx index 9aa005f6..c1cfd835 100644 --- a/website/docs/zh/guide/testing.mdx +++ b/website/docs/zh/guide/testing.mdx @@ -107,11 +107,11 @@ define.test({ }); ``` -使用多项目配置时,在 `define.test()` 根配置中设置 `extends` 会关闭所有项目的自动继承;在某个内联项目中设置 `extends` 则只会关闭该项目的自动继承。 +使用多项目配置时,在 `define.test()` 根配置中设置 `extends` 会关闭其内联项目的自动继承;在某个内联项目中设置 `extends` 则只会关闭该项目的自动继承。外部项目的继承由各自的 Rstack 配置决定。 ## 多项目 \{#multiple-projects} -通过 Rstest 的 [`projects`](https://rstest.rs/zh/config/test/projects) 选项可以同时运行多套测试配置。每一项既可以是内联项目,也可以是由 Rstest 解析为外部项目的字符串。 +通过 Rstest 的 [`projects`](https://rstest.rs/zh/config/test/projects) 选项可以同时运行多套测试配置。每一项既可以是内联项目,也可以是指向项目目录或其 Rstack 配置文件的字符串。 ### 内联项目 \{#inline-projects} @@ -153,14 +153,24 @@ rs test --project dom ### 外部项目 \{#external-projects} -使用字符串形式的配置项加载外部项目: +项目的测试配置放在自己的 Rstack 配置文件中时,请使用字符串形式的配置项加载: ```ts title="rstack.config.ts" import { define } from 'rstack'; define.test({ - projects: ['./legacy/rstest.config.ts'], + projects: ['./packages/*'], }); ``` -Rstack CLI 会将字符串形式的项目原样传给 Rstest。外部项目会加载自己的配置,不会继承当前的 `define.app()` 或 `define.lib()` 配置。每个项目需要独立管理配置时,请使用外部项目。 +目录形式的配置项会加载该目录下的 `rstack.config.*`。需要显式指定配置文件时,改用文件形式的配置项: + +```ts title="rstack.config.ts" +import { define } from 'rstack'; + +define.test({ + projects: ['./packages/*/rstack.config.ts'], +}); +``` + +Rstack CLI 用每个项目自己的 Rstack 配置解析字符串条目。目录下没有 Rstack 配置文件时,使用 Rstest 的默认配置。项目的 `rstest.config.ts` 不会被加载。在 watch 模式下,修改项目的 Rstack 配置文件会重新运行测试。