Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions packages/rstack/rstack.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand Down
8 changes: 5 additions & 3 deletions packages/rstack/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -269,7 +270,8 @@ export const loadRstackConfig = async ({
cwd,
}: LoadRstackConfigOptions = {}): Promise<LoadedRstackConfig> => {
const state = getConfigState();
const configPath = configFilePath ?? state.configPath;
const configPath =
configFilePath === null ? undefined : (configFilePath ?? state.configPath);
const session: ConfigSession = {
configs: {},
active: true,
Expand Down
47 changes: 43 additions & 4 deletions packages/rstack/src/rstestConfig.ts
Original file line number Diff line number Diff line change
@@ -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<RstestConfig['extends'] | undefined> => {
// Prefer the app when both app and lib are defined. Merging both adapters can
// introduce conflicting runtime, resolve, and source transform settings.
Expand All @@ -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,
});
}

Expand All @@ -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);
}

Expand All @@ -75,7 +83,7 @@ const extendsConfig = async (
return testConfig;
}

const automaticExtends = await resolveAutomaticExtends(layers, params);
const automaticExtends = await resolveAutomaticExtends(layers, params, cwd);

return {
...testConfig,
Expand All @@ -99,14 +107,45 @@ export const mergeRstestConfigLayers = async (
export const resolveRstestConfig = async (
layers: readonly RstackConfigDefinitions[],
params: ConfigParams,
cwd?: string,
): Promise<RstestConfig> => {
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;
11 changes: 11 additions & 0 deletions packages/rstack/tests/config/project-loader/index.test.ts
Original file line number Diff line number Diff line change
@@ -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');
});
Original file line number Diff line number Diff line change
@@ -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<string, unknown>).describe).toBe(
'function',
);
});
Original file line number Diff line number Diff line change
@@ -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');
Original file line number Diff line number Diff line change
@@ -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'],
});
Original file line number Diff line number Diff line change
@@ -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<string, unknown>).describe).toBe(
'undefined',
);
});
Original file line number Diff line number Diff line change
@@ -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');
5 changes: 5 additions & 0 deletions packages/rstack/tests/config/project-loader/rstack.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { define } from 'rstack';

define.test({
projects: ['packages/*'],
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { define } from 'rstack';

define.test({
projects: ['packages/*/rstack.config.ts'],
});
24 changes: 12 additions & 12 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion pnpm-workspace.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
2 changes: 1 addition & 1 deletion website/docs/en/guide/configuration.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
20 changes: 15 additions & 5 deletions website/docs/en/guide/testing.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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.
2 changes: 1 addition & 1 deletion website/docs/zh/guide/configuration.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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 配置。

Expand Down
20 changes: 15 additions & 5 deletions website/docs/zh/guide/testing.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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}

Expand Down Expand Up @@ -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 配置文件会重新运行测试。
Loading