Skip to content

API stabilization of 1.0.0 #1294

Description

@fi3ework

Tracking list of open API design decisions to settle before the 1.0 release. Items here would be breaking (or potentially breaking) to change after 1.0; deferred / will-add-later / unimplemented items go into separate roadmap issues.

Each item is numbered ([N]) so it can be referenced in discussion (e.g. "let's talk about [5]").

Convention: when you check a box, fill the 3-field template under it so the rationale is captured inline.

- [x] [N] item
  - Picked: <chosen option>
  - Why: <one-line reason>
  - Refs: <PR / ADR / discussion link>

🟠 Options drafted — pending decision (0)

⚪ Not yet reviewed (4)

  • [21] File-level hook naming: onTestFileStart / onTestFileReady / onTestFileResult or onTestModuleStart / onTestModuleEnd

    • Picked:
    • Why:
    • Refs:
  • [22] Reporter data shape: plain object or wrapper classes (TestModule / TestSuite / TestCase), or mark the whole Reporter interface as Best-effort tier and defer

    • Picked:
    • Why:
    • Refs:
  • [23] TestResultStatus literal lock-in: commit current values 'pass' | 'fail' | 'skip' | 'todo' as the Stable contract for custom reporters, or keep room to rename

    • Picked:
    • Why:
    • Refs:
  • [26] browser.browser field shape: single-browser field or browser.instances[] array

    • Picked:
    • Why:
    • Refs:

✅ Decided (32)

  • [13] shard config object form { count, index } tier: keep as a Stable config-side form alongside the CLI flag, or limit to CLI flag only

    • Picked: Option 2 — CLI-only. Drop the shard config field; expose sharding only via the --shard <index>/<count> flag.
    • Why: shard is per-invocation — every CI runner must pass a different index, which a value committed to the shared config file cannot express. Mirrors jest, which carries shard on Argv/GlobalConfig and never adds it to user config types. Internally the resolved shard now rides on an internal ResolvedRstestConfig / NormalizedConfig that the CLI injects; the obsolete per-project shard validation is removed; docs move out of config/test/ into the CLI guide (rstest run -> Run a shard).
    • Refs: refactor(core)!: drop shard config field, use --shard CLI only #1449
  • [7] pool shape and whether to remove minWorkers before 1.0.0

    • Picked: Option 1 — keep flat pool; remove the public minWorkers field and the --pool.minWorkers CLI flag. Shape settles on { type?, maxWorkers?, execArgv? }.
    • Why: minWorkers only kept idle workers alive for isolate: false — niche. Jest exposes only maxWorkers/--runInBand; Vitest v4 removed minWorkers. The internal idle-runner floor still defaults to min(maxWorkers, recommended), so default warm-worker behavior is unchanged — only the user-facing override is dropped. The now-unreachable maxWorkers < minWorkers validation is removed.
    • Refs: refactor(core)!: remove pool.minWorkers config field and CLI flag #1451
  • [16] chaiConfig tier classification, and whether to keep the field name bound to a specific assertion-library implementation

    • Picked: Option 1 — keep the top-level chaiConfig?: ChaiConfig field (do not nest under expect), with the type narrowed to a curated whitelist. Tier: Stable.
    • Why: it is a single self-contained escape hatch for the underlying assertion engine, so a top-level field keeps it discoverable alongside the other test-behavior knobs; nesting under expect would only group without decoupling from chai (the name and type stay chai-derived either way), trading a breaking restructure for no real abstraction gain. The implementation-named key is acceptable because the type is no longer a wide passthrough — it is a curated Pick, so it does not leak chai's full config surface.
    • Note: Option 1's text above is stale. The type is already narrowed from the old wide Partial<Omit<typeof config, 'useProxy' | 'proxyExcludedKeys' | 'deepEqual'>> to a whitelist — ChaiConfig = Partial<Pick<typeof config, 'showDiff' | 'truncateThreshold'>>. So "narrow the type" is already satisfied by the current code; no further change is required.
    • Refs: packages/core/src/types/config.ts:13-15,530
  • [4] rs.getConfig() inclusion: keep alongside setConfig / resetConfig, or limit the runtime-config trio to setConfig / resetConfig

    • Picked: Option 1 — keep getConfig, tiered Best-effort while setConfig / resetConfig stay Stable.
    • Why: it is the only runtime-readable source of the effective config value, letting a config-agnostic helper adjust relative to the current one (e.g. double testTimeout); public since 0.6.6, so removing it would be breaking. Best-effort keeps room to evolve the returned shape.
    • Refs: packages/core/src/runtime/api/utilities.ts:338
  • [35] @rstest/coverage-istanbul's pluginCoverage / CoverageProvider tier classification: Stable / Best-effort

    • Picked: Best-effort. Keep both on the public . entry as-is; defer any /internal move or rename.
    • Why: their only consumer is core's loadCoverageProvider, which resolves the package by name and dynamic-imports the named exports { pluginCoverage, CoverageProvider } — an internal core↔provider protocol, not an end-user extension API. The shapes still move (collect() carries an uncleaned outputModule param; pluginCoverage depends on Rsbuild internals like CHAIN_ID / SWC loader options), so Best-effort keeps 1.x adjustment room. Promote to Stable only if a real third-party provider extension point lands.
    • Refs: packages/coverage-istanbul/src/index.ts:1-2, packages/core/src/types/coverage.ts:161, packages/core/src/coverage/index.ts:16-46, packages/core/src/coverage/install.ts:9-12
  • [29] createRstest / initCli / runCLI exposure: @internal JSDoc / move to /internal subpath / promote to documented Stable

    • Picked: split — promote a curated programmatic surface to a dedicated @rstest/core/api subpath (documented Stable), and remove all programmatic/orchestration symbols from the main . entry (which now exposes only in-test runtime globals, config helpers, and types). Public on /api: async createRstest() factory → RstestInstance (run / listTests / mergeReports / addReporter / close) and a jest-compatible runCli({ argv?, cwd? }). Internal-only (not exported): initCli, the old sync host factory (renamed createRstestcreateRstestContext), and the old runCLI.
    • Why: the previous main entry leaked host/CLI orchestration symbols next to in-test globals, mixing two audiences; a dedicated /api subpath gives the programmatic runner a stable, predictable shape modeled on rsbuild's createRsbuild. Per-build config/projects are re-resolved inside the async factory, and per-invocation selection (filters / related / changed / shard / bail) moves to RunOptions, so the construction-vs-run split is explicit.
    • Refs: feat(core): stabilize programmatic JS API with createRstest + /api surface #1372
  • [9] extends field semantics: adapter function (ExtendConfigFn) / config-file path inheritance / both

    • Picked: keep current semantics — extends?: ExtendConfigFn | ExtendConfig | (ExtendConfigFn | ExtendConfig)[]; do NOT add a vitest-style string config-file path branch in 1.0. Tier: Stable.
    • Why: the function form is rstest's adapter bridge — withRslibConfig() / withRsbuildConfig() return ExtendConfigFn, so narrowing to a string path would break every adapter user. Crucially the scope models don't align: vitest's extends lives only on project-inline entries (TestProjectInlineConfiguration, default-isolated, opt-in inheritance) and has no root-level form, so copying it would force the common single-project + adapter user to wrap themselves in a projects array just to extend. rstest's extends is root-level adapter injection AND already works inside projects inline entries (each runs resolveExtends). The union is open, so adding a string branch later stays non-breaking; string-base sharing is already covered by importing an ExtendConfig object or by projects layering, so it's not a 1.0 must-have.
    • Refs: packages/core/src/types/config.ts:233-238 (union), packages/core/src/config.ts:120-148 (resolveExtends), packages/core/src/cli/init.ts:438 (per-project resolve), packages/adapter-rslib/src/index.ts:99, packages/adapter-rsbuild/src/index.ts; vitest extends?: string | true is project-inline-only (packages/vitest/src/node/types/config.ts:1326-1333)
  • [3] rs.mockRequire / requireMock / requireActual / doMockRequire / unmockRequire / doUnmockRequire (CJS-flavored entries) tier classification: Stable / Best-effort / Internal

    • Picked: all six → Stable.
    • Why: they are the require() mirror of the ESM mock set (mock / doMock / unmock / doUnmock / importMock / importActual) — mockRequire is the direct migration target for jest.mock() on require()-based code, and requireActual / requireMock mirror jest.requireActual / jest.requireMock. Splitting a mirror pair across tiers is a DX wart, and the user contract ("require() returns the mock / actual module") stays stable regardless of how the underlying Rspack→native-ESM runtime evolves.
    • Refs: packages/core/src/types/mock.ts:428-498, website/docs/en/guide/migration/jest.mdx:287
  • [1] onConsoleLog callback signature: (content: string) => boolean | void or (log, type, entity) => boolean | void

  • [2] rs.clearAllMocks / resetAllMocks / restoreAllMocks and other reset methods: chainable this return or void

    • Picked: keep () => RstestUtilities chainable return
    • Why: the documented type signature is already () => Rstest, which is a public contract; the chainable shape is consistent across ~25 namespace methods. Downgrading to void would break declared return-type expectations.
    • Refs: packages/core/src/types/mock.ts:390,394,398, website/docs/en/api/runtime-api/rstest/mock-functions.mdx:223,230,237
  • [5] testEnvironment field name: testEnvironment or environment

    • Picked: keep testEnvironment
    • Why: coupled with [18] — the bare environment name would collide with rsbuild's own environments config concept, so the test-prefixed field stays self-explanatory and keeps the config field and --testEnvironment CLI flag mirrored.
    • Refs: packages/core/src/types/config.ts:346
  • [6] testEnvironment field shape: nested { name, options } object, or split into *Environment + *EnvironmentOptions two fields

    • Picked: Keep the current nested object form — testEnvironment?: EnvironmentName | { name: EnvironmentName; options? }, normalized internally to { name, options }. Do not split into separate testEnvironment + testEnvironmentOptions top-level fields. The string shorthand (testEnvironment: 'jsdom') stays the primary form. Tier: Stable.
    • Why: name and options belong to one environment unit, so the nested shape states "configure one environment" directly and keeps a single field to evolve (e.g. a future discriminated { name, options } per environment). Avoids a breaking rename/restructure, and testEnvironment.options has near-zero real usage so the flat-field ergonomics gain is marginal.
    • Refs: packages/core/src/types/config.ts:238-243,346,604, packages/core/src/config.ts:221-223,331-336
  • [8] resetMocks vs mockReset field name

    • Picked: resetMocks
    • Why: aligns with the sibling fields clearMocks / restoreMocks (verb + plural noun), so the three reset-family options share one phrase structure.
    • Refs: packages/core/src/types/config.ts:447
  • [10] coverage.provider literal union: lock to 'istanbul' | 'v8' only or open up (e.g. 'istanbul' | 'v8' | (string & {}))

    • Picked: lock to 'istanbul' | 'v8'
    • Why: extending a closed union later is non-breaking; tightening an open one is breaking. No user has requested a custom-provider escape hatch.
    • Refs: packages/core/src/types/coverage.ts:102, packages/core/src/coverage/install.ts:9-12
  • [11] coverage.reporters field name: plural reporters (current, mirrors the CLI flag and the top-level reporters config field) or singular reporter

    • Picked: plural reporters
    • Why: it takes an array and stays symmetric with the top-level reporters field and the --coverage.reporters CLI flag, so the whole reporter surface shares one plural convention.
    • Refs: packages/core/src/types/coverage.ts:105-118
  • [12] exclude object form { patterns, override } tier: keep as a Stable extension alongside the string[] form, or limit to string[] only

    • Picked: Keep the object form as a Stable extension alongside string[].
    • Why: override: true is only expressible via the object form — the sole opt-out for replacing the built-in defaults (node_modules, dist) instead of extending them. (Contrast jest's testPathIgnorePatterns, a plain array that replaces defaults, forcing users to re-list /node_modules/.)
    • Refs: packages/core/src/types/config.ts:278-287, packages/core/src/config.ts:161-173
  • [14] hideSkippedTestFiles retention: keep as a separate field alongside hideSkippedTests, or fold into hideSkippedTests semantics

    • Picked: keep both fields
    • Why: orthogonal granularities — hideSkippedTestFiles drops an entirely-skipped file's whole title block; hideSkippedTests only hides individual skipped case lines in a still-printed file. Folding loses the ability to hide stray skipped cases while still surfacing that a whole file was skipped.
    • Refs: packages/core/src/types/config.ts:414,420, packages/core/src/reporter/index.ts:100-103,115-127
  • [15] output.bundleDependencies tier classification: Stable / Best-effort

    • Picked: keep, Stable
    • Why: it is a bundler-native core extension controlling test-time externalization with settled semantics; the boolean | BundleDependencyPattern[] shape already allows additive extension, so locking it Stable costs little.
    • Refs: packages/core/src/types/config.ts:86, packages/core/src/core/plugins/external.ts:268-286
  • [17] Whether to add a --reporters plural alias for --reporter

  • [18] --testEnvironment CLI flag name (coupled with [5])

    • Picked: keep --testEnvironment
    • Why: rsbuild already owns an environments config concept, so the test-prefixed name avoids colliding with it and keeps the flag self-explanatory; it also mirrors the testEnvironment config field.
    • Refs: packages/core/src/cli/commands.ts:111,256, packages/core/src/types/config.ts:346
  • [19] --config-loader tier classification, since the loader strategy enum (auto / jiti / native) may evolve

    • Picked: keep, full passthrough; Best-effort
    • Why: --config-loader forwards LoadConfigOptions['loader'] straight to @rsbuild/core's config loader; the auto/jiti/native set is owned by @rsbuild/core, so its evolution is outside rstest's semver control — nothing to implement, just pass through.
    • Refs: packages/core/src/config.ts:69-94, packages/core/src/cli/commands.ts:34,181
  • [20] --hideSkippedTestFiles and --trace (Perfetto tracing) tier classification, since both relate to surfaces whose semantics or format may still evolve

    • Picked: Both Stable.
    • Why: Their only foreseeable evolution is adding optional flag arguments, which is non-breaking — committing them as Stable now costs nothing.
    • Refs:
  • [24] md built-in reporter name tier classification: Stable / Best-effort

    • Picked: keep md, Stable
    • Why: the name is a stable, self-descriptive built-in; MdReporterOptions fields are all optional so it stays additive, and locking it avoids breaking configs that reference 'md'.
    • Refs: packages/core/src/types/reporter.ts:31,58,179
  • [25] browser.provider literal union: lock to 'playwright' only or open up

    • Picked: lock to 'playwright' only
    • Why: only playwright is implemented today; widening the union with additional literals later is a non-breaking change, while starting open ((string & {})) and tightening later would be breaking. Runtime already enforces this via SUPPORTED_PROVIDERS = ['playwright'].
    • Refs: packages/core/src/types/config.ts:171, packages/browser/src/configValidation.ts:4
  • [27] DevicePreset enum literal lock-in for browser.viewport: commit the current 17 preset ids as Stable, or keep room to rename

    • Picked: Lock the 17 DevicePreset ids as Stable, and widen BrowserViewport to a half-open form: { width: number; height: number } | DevicePreset | (string & {}).
    • Why: the 17 ids mirror Chrome DevTools device names — stable source, rarely renamed — so the long-term cost of a Stable guarantee is low, and users already write these literals into config/CI so 1.0 must not break them. The (string & {}) widening keeps autocomplete for the known ids while letting the runtime accept custom strings; adding new presets later stays a non-breaking addition (removing an id would require a major).
    • Refs: packages/core/src/types/config.ts:133-157, packages/browser/src/viewportPresets.ts:1-65
  • [28] BROWSER_VIEWPORT_PRESET_DIMENSIONS / BROWSER_VIEWPORT_PRESET_IDS / resolveBrowserViewportPreset exports (from @rstest/browser) tier classification: Stable / Best-effort

    • Picked: Internal — removed the src/index.ts re-export so they have zero published exposure.
    • Why: They were never public (no @public JSDoc, absent from the public . entry). The src/index.ts re-export (→ ./internal) had zero consumers: package internals import them by relative path, and the only cross-package user (@rstest/browser-ui, which is never published) resolves them through a tsconfig-paths source alias, not a package export. Dropping the re-export makes them fully Internal.
    • Refs: packages/browser/src/viewportPresets.ts, packages/browser/src/index.ts (re-export removed), packages/browser-ui/tsconfig.json (@rstest/browser/viewport-presets paths alias), refactor(browser): drop unused internal browser surface #1360
  • [30] Consolidation of ./browser and ./browser-runtime subpaths: merge / rename ./browser-runtime to /internal/browser-runtime / keep both

    • Picked: Rename ./browser./internal/browser and ./browser-runtime./internal/browser-runtime; keep the two paths separate (do not merge).
    • Why: Both subpaths are consumed only by first-party packages (@rstest/browser, @rstest/browser-ui), undocumented, and JSDoc-marked "not part of the public API" — the internal/ prefix makes the path name match that reality. They stay separate because their dependency profiles are opposite: ./internal/browser carries Node-only deps (@rsbuild/core, coverage, logger, getTestEntries), while ./internal/browser-runtime is browser-safe with zero Node deps; merging would drag Node deps into the browser bundle.
    • Refs: packages/core/package.json (exports["./internal/browser"], ["./internal/browser-runtime"]), packages/core/src/browser.ts:1-5 (JSDoc), packages/core/src/browserRuntime.ts:1-9, refactor(core): move internal browser host subpaths under ./internal #1358
  • [31] Whether globals: true should inject both rs and rstest as global names

    • Picked: inject both; prefer rs
    • Why: rs is the preferred name (docs and examples lead with it), but rstest stays injected as a supported alias — dropping it would break existing tests, and keeping both costs nothing.
    • Refs: packages/core/src/utils/constants.ts:232-246, packages/core/src/runtime/worker/runInPool.ts:48-56
  • [33] defineInlineProject vs defineProject consolidation: keep both helpers, or merge inline semantics into defineProject

    • Picked: keep both helpers
    • Why: the two are orthogonal — defineInlineProject types entries inside the projects: [...] array, defineProject types a standalone project config file's export; different input types and use sites, so merging would lose per-site autocompletion.
    • Refs: packages/core/src/index.ts:67-94, packages/core/src/types/config.ts:225-226
  • [34] @rstest/browser's validateBrowserConfig exposure: demote to Internal / move to ./internal subpath

    • Picked: Keep on the ./internal subpath (Internal tier).
    • Why: It is a config-precondition checker that core invokes internally via loadBrowserModule; test authors never call it. It is already exported only from src/index.ts → the ./internal subpath, never from the Stable . entry (src/browser.ts, which only re-exports ./client/api). Realigned core's ambient type module from @rstest/browser to @rstest/browser/internal to match the subpath the runtime actually loads.
    • Refs: packages/browser/src/index.ts:12, packages/browser/package.json (exports["./internal"]), packages/core/src/core/browserLoader.ts:81, packages/core/src/env.d.ts:11, refactor(core): move internal browser host subpaths under ./internal #1358
  • [36] @rstest/adapter-rsbuild's toRstestConfig exposure: Best-effort / Internal

    • Picked: Best-effort, for toRstestConfig in @rstest/adapter-rsbuild and @rstest/adapter-rspack. @rstest/adapter-rslib has no such export and stays out of scope.
    • Why: already exported and documented, so not Internal; mapping is bound to upstream rsbuild/rspack config, so Best-effort over Stable. adapter-rslib's omission is structural (conversion inlined in withRslibConfig, not pure), and adding it later is non-breaking.
    • Refs: packages/adapter-rsbuild/src/index.ts:60, packages/adapter-rsbuild/src/toRstestConfig.ts:79, packages/adapter-rspack/src/index.ts:327, packages/adapter-rslib/src/index.ts:99-153
  • [37] @rstest/browser-react's ./pure subpath tier classification: Stable / Best-effort

    • Picked: Stable.
    • Why: ./pure exposes the same render / renderHook / cleanup / configure / act API as the default . entry, only without the auto-cleanup beforeEach side effect — the established Testing Library /pure convention. It adds no surface beyond . to stabilize, so it shares the same tier.
    • Refs: packages/browser-react/src/index.ts, packages/browser-react/src/pure.tsx, packages/browser-react/package.json (exports["./pure"])

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions