feat(core): support prebundle test environments - #1663
Conversation
Deploying rstest with
|
| Latest commit: |
220659e
|
| Status: | ✅ Deploy successful! |
| Preview URL: | https://4a0e3939.rstest.pages.dev |
| Branch Preview URL: | https://9aoy-feat-test-environment-p.rstest.pages.dev |
Rsdoctor Bundle Diff AnalysisFound 13 projects in monorepo, 2 projects with changes. 📊 Quick Summary
📋 Detailed Reports (Click to expand)📁 core/mainPath:
📦 Download Diff Report: core/main Bundle Diff 📁 core/browserPath:
📦 Download Diff Report: core/browser Bundle Diff Generated by Rsdoctor GitHub Action |
There was a problem hiding this comment.
Pull request overview
Adds a new testEnvironment.prebundle option to @rstest/core to speed up DOM-environment startup by optionally prebundling jsdom / happy-dom once (per resolved dependency) and reusing the ESM bundle across workers, with validated fallback to native loading.
Changes:
- Introduces
testEnvironment.prebundle('auto' | true | false) and threads a resolved/bundled environment module reference into worker environment setup. - Implements host-side environment dependency resolution + optional Rsbuild-based prebundle generation with compatibility gating (auto matrix) and robust fallback behavior.
- Updates docs and tests to cover config merging, environment comments, list-mode lifecycle, and module loading fallback paths.
Reviewed changes
Copilot reviewed 22 out of 22 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| website/docs/zh/config/test/test-environment.mdx | Documents testEnvironment.prebundle (ZH) and its modes/fallback behavior. |
| website/docs/en/config/test/test-environment.mdx | Documents testEnvironment.prebundle (EN) and its modes/fallback behavior. |
| packages/core/tests/utils/environmentComments.test.ts | Ensures environment comments preserve prebundle in merged environment config. |
| packages/core/tests/runtime/worker/env/testEnvironmentModule.test.ts | Adds unit tests for bundled-vs-native module loading and fallback in workers. |
| packages/core/tests/core/testEnvironmentModule.test.ts | Adds unit tests for host-side prebundle preparation, version gating, and cleanup. |
| packages/core/tests/core/rsbuild.test.ts | Ensures list-mode resolves environment deps after modifyRstestConfig and closes pool on failure. |
| packages/core/tests/core/envDependencies.test.ts | Updates env dependency resolution expectations (no longer resolves from core package root). |
| packages/core/tests/config.test.ts | Verifies config merging preserves testEnvironment.prebundle. |
| packages/core/src/utils/environmentComments.ts | Extends environment comment application to carry through prebundle. |
| packages/core/src/types/worker.ts | Introduces TestEnvironmentModuleReference and threads it through WorkerContext. |
| packages/core/src/types/config.ts | Adds TestEnvironmentPrebundle and prebundle to EnvironmentWithOptions. |
| packages/core/src/runtime/worker/runInPool.ts | Loads and passes the (bundled/native) environment dependency module into env setup. |
| packages/core/src/runtime/worker/env/testEnvironmentModule.ts | Implements validated, cached loading of bundled/native environment dependency modules with fallback. |
| packages/core/src/runtime/worker/env/registry.ts | Adapts env loader registry to pass the resolved environment module into env setup. |
| packages/core/src/runtime/worker/env/jsdom.ts | Refactors jsdom env setup to accept an injected jsdom module (prebundle/native). |
| packages/core/src/runtime/worker/env/happyDom.ts | Refactors happy-dom env setup to accept an injected happy-dom module (prebundle/native). |
| packages/core/src/pool/index.ts | Threads testEnvironmentModules map into workers so they can load prebundles. |
| packages/core/src/core/testEnvironmentModule.ts | New host-side prebundle builder (Rsbuild), compatibility gating, and cleanup handling. |
| packages/core/src/core/listTests.ts | Ensures list-mode resolves env deps and prepares prebundles, with reliable cleanup on errors. |
| packages/core/src/core/executors/nodeExecutor.ts | Prepares and cleans up environment prebundles as part of node executor lifecycle. |
| packages/core/src/core/envDependencies.ts | Exposes env dependency package mapping and adjusts resolution roots. |
| e2e/types/projectConfig.ts | Adds type-level assertion for invalid prebundle values. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 21 out of 21 changed files in this pull request and generated no new comments.
Suppressed comments (2)
packages/core/src/types/worker.ts:102
- The
resolvedPathdocstring says it is resolved from the project's installation tree, but the resolution logic falls back to additional roots (e.g. the Rstest install/workspace roots). Updating this comment would prevent future confusion about where the module may be resolved from.
/** Native module entry resolved from the project's installation tree. */
packages/core/src/utils/environmentComments.ts:268
applyEnvironmentCommentnow always copiesbaseEnvironment.prebundle, even when the file-level environment comment switches to a different environment name. This makes aprebundlesetting for one environment leak into a different environment chosen by the comment (e.g. jsdom -> happy-dom), which is likely unintended.
Consider only preserving prebundle when the effective environment name stays the same as baseEnvironment.name.
...(options && Object.keys(options).length > 0 ? { options } : {}),
...(baseEnvironment.prebundle === undefined
? {}
: { prebundle: baseEnvironment.prebundle }),
};
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 21 out of 21 changed files in this pull request and generated no new comments.
Suppressed comments (2)
packages/core/src/runtime/worker/env/testEnvironmentModule.ts:35
validateBuiltinDependencyonly checks thatJSDOMexists, butsetupEnvironmentalso requiresCookieJar,ResourceLoader, andVirtualConsole. If a prebundle ends up missing one of these named exports, validation will pass and the worker will crash later (with no fallback). Tighten validation to match the exports actually consumed by the environment setup.
if (reference.name === 'jsdom' && typeof module.JSDOM === 'function') {
packages/core/src/core/testEnvironmentModule.ts:170
- When externalizing optional deps (e.g. canvas) for an ESM build (
module-import), returning an absolute filesystem path can produce an invalid import specifier on Windows (drive letters/backslashes). Convert resolved absolute paths tofile://URLs whendependencyType !== 'commonjs'so the generated ESMimportremains portable.
callback(
undefined,
resolvedPath ?? request,
dependencyType === 'commonjs' ? 'commonjs' : 'module-import',
);
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 21 out of 21 changed files in this pull request and generated no new comments.
Suppressed comments (1)
packages/core/src/core/testEnvironmentModule.ts:155
testEnvironmentExternalonly treats Node built-ins viaisBuiltin(request). In Yarn PnP setups,pnpapiis also treated as a built-in (see existingADDITIONAL_NODE_BUILTINSusage elsewhere), so Rspack may try to bundle it into the environment prebundle and fail. Consider treatingpnpapi(andnode:-prefixed specifiers) as externals here as well to match the rest of the codebase’s builtin-external behavior.
if (isBuiltin(request)) {
callback(
undefined,
request,
dependencyType === 'commonjs' ? 'commonjs' : 'module-import',
);
return;
}
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 21 out of 21 changed files in this pull request and generated no new comments.
Suppressed comments (1)
packages/core/src/runtime/worker/env/testEnvironmentModule.ts:58
- The thrown error for invalid environment-module exports is likely user-facing but doesn’t explain what was expected or where the module was loaded from, which makes diagnosing resolution/prebundle issues harder. Consider including the environment name and expected export(s), plus the resolved/bundle paths.
throw new Error(
`Invalid ${reference.packageName} test environment dependency exports.`,
);
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 33c3b9f762
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 21 out of 21 changed files in this pull request and generated no new comments.
Suppressed comments (1)
packages/core/src/core/executors/nodeExecutor.ts:349
prepareTestEnvironmentModules({ projects })runs even when a project has no discovered test entries (e.g.includematches nothing). That can do unnecessary environment resolution / prebundle work (and may cascade into dependency validation elsewhere) for runs that will return no tests. Consider filtering to only projects with non-emptyentriesCachebefore preparing the modules map.
testEnvironmentModules = await prepareTestEnvironmentModules({
projects,
rootPath,
});
…nment-prebundle # Conflicts: # packages/core/src/core/executors/nodeExecutor.ts # packages/core/src/pool/index.ts
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 048f46b90b
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| rootPath: context.rootPath, | ||
| projectRoot: project.rootPath, | ||
| runtimeConfig, | ||
| testEnvironmentModule, |
There was a problem hiding this comment.
Add module resolution to the environment affinity key
When two projects use isolate: false with identical testEnvironment options but resolve different jsdom or happy-dom installations, this new reference does not affect environmentKey, which is still derived only from the config. The pool can therefore reuse the first project's pinned worker for the second project, and preparePool skips environment setup on that worker, silently running the second project's tests against the first project's dependency and DOM globals. Include the resolved environment module identity in the affinity key so these projects cannot share a worker.
AGENTS.md reference: packages/core/src/pool/AGENTS.md:L21-L21
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 6aa037ef38
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| config.optimization = { | ||
| ...config.optimization, | ||
| runtimeChunk: false, | ||
| splitChunks: false, |
There was a problem hiding this comment.
Disable NODE_ENV replacement in prebundles
When a supported environment or one of its bundled dependencies branches on process.env.NODE_ENV, setting Rspack's mode to development while leaving optimization.nodeEnv enabled replaces that expression with "development". Rstest workers normally run with NODE_ENV=test or a user/global-setup override, so the default auto prebundle can execute different code than native loading. The normal test-build configuration explicitly sets nodeEnv: false; do the same for this generated bundle.
Useful? React with 👍 / 👎.
| const packageJsonPath = resolveTestEnvironmentModule( | ||
| `${packageName}/package.json`, | ||
| projectRoot, | ||
| root, | ||
| ); |
There was a problem hiding this comment.
Read the version for the resolved environment entry
When a project's environment package does not export its package.json, this independent resolution can fail at the project root and then find a different installation at the workspace or core fallback root. The compatibility check may consequently use that fallback package's major while resolvedPath points to the project's package—for example, a project happy-dom 21 entry can be prebundled because the root exposes happy-dom 20. Locate the package metadata belonging to the already resolved entry so auto never bundles an untested major.
Useful? React with 👍 / 👎.
| const resolved = specifier.startsWith("node:") || specifier.startsWith("file:") | ||
| ? specifier | ||
| : __rstestPathToFileURL( | ||
| __rstestModule.createRequire(origin || import.meta.url).resolve(specifier), | ||
| ).href; |
There was a problem hiding this comment.
Preserve bare built-ins in the dynamic-import hook
When bundled environment code performs a non-literal dynamic import whose value is a bare Node built-in such as fs, createRequire(...).resolve('fs') returns the bare string fs, which this code converts to a file:// URL under the current directory. The import then targets a nonexistent file instead of Node's built-in module; static imports avoid this path, so the failure appears only when the dependency exercises that dynamic branch and may occur after the initial bundle probe. Check isBuiltin for both the original and resolved specifier before applying pathToFileURL.
Useful? React with 👍 / 👎.
…nment-prebundle # Conflicts: # packages/core/src/core/executors/nodeExecutor.ts
Summary
testEnvironment.prebundlewithauto,true, andfalsemodes; prebundling is disabled by default to preserve existing behaviorauto, cover jsdom 15–26 and 29–30 plus happy-dom 20, keep jsdom 27/28 on native loading, preserve optional canvas loading, and support both test runs andrstest listPerformance
Informational local wall-clock benchmark on Apple Silicon macOS with Node.js 24.11.1 and 4 fork workers. jsdom uses 30.0.1 and happy-dom uses 20.11.1. Each result is the median of 3 measured runs after 1 warmup; prebundle and native variants use the same generated test suite and alternate execution order.
These numbers include the one-time prebundle build in the measured Rstest process and are intended as directional data rather than a performance guarantee.
Usage
Available since Rstest 0.11.6. Prebundling is disabled by default in this PR to minimize compatibility risk. Opt in to Rstest's tested compatibility matrix with
prebundle: 'auto':Set
prebundle: trueto force prebundling for a built-in environment. Omit the option or setprebundle: falseto retain native loading. Makingautothe default can be evaluated separately after this opt-in API has shipped.Compatibility, risks, and lifecycle
Environment prebundling is a performance optimization, not a runtime requirement. Native loading remains the default. In
automode, Rstest only prebundles package versions covered by its built-in compatibility matrix; unsupported or known-incompatible versions remain native.A third-party environment can compile and import successfully as a bundle while still behaving differently at runtime. Relevant examples include:
require.resolve(), such as jsdom's synchronous XHR worker;canvas;The last case affects jsdom 27/28: bundling can change how
@acemir/cssomresolves its optionalcssstyledependency under strict package managers. The generated bundle can build and load, butgetComputedStyle()may fail at runtime. Rstest therefore keeps these majors on native loading inautomode. When prebundling is explicitly forced, Rstest runs a minimal runtime probe for this known failure and falls back to the native entry before test environment setup if the probe fails. The probe cannot cover every upstream API, so projects that exercise an unsupported bundler-sensitive path should useprebundle: false.jsdom and happy-dom do not currently expose an official Node-compatible bundled entry that guarantees the same runtime assets, optional dependencies, and Node-specific behavior as their normal entry. If either project provides such an entry in the future, Rstest should prefer that upstream entry and can stop applying its own prebundle optimization for that package in
automode. At that point, enabling Rstest prebundling may no longer be necessary for that environment.Known upstream bundling discussions:
Related Links
Checklist