Skip to content

feat(core): support prebundle test environments - #1663

Open
9aoy wants to merge 15 commits into
mainfrom
9aoy/feat-test-environment-prebundle
Open

feat(core): support prebundle test environments#1663
9aoy wants to merge 15 commits into
mainfrom
9aoy/feat-test-environment-prebundle

Conversation

@9aoy

@9aoy 9aoy commented Aug 3, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • add testEnvironment.prebundle with auto, true, and false modes; prebundling is disabled by default to preserve existing behavior
  • resolve built-in DOM environments from each project and share an Rsbuild-generated ESM bundle across workers, with native loading as the fallback
  • when explicitly using auto, 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 and rstest list
  • document the option and add lifecycle, fallback, CommonJS output, config-hook, environment-comment, and default-behavior coverage

Performance

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.

Environment Suite Native loading Prebundle Improvement
jsdom 10 files / 100 tests 2.11s 1.42s 32.8% faster
jsdom 100 files / 1,000 tests 16.99s 10.57s 37.8% faster
happy-dom 10 files / 100 tests 0.86s 0.54s 37.5% faster
happy-dom 100 files / 1,000 tests 6.35s 2.98s 53.0% faster

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':

import { defineConfig } from '@rstest/core';

export default defineConfig({
  testEnvironment: {
    name: 'jsdom', // or 'happy-dom'
    prebundle: 'auto',
  },
});

Set prebundle: true to force prebundling for a built-in environment. Omit the option or set prebundle: false to retain native loading. Making auto the 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 auto mode, 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:

  • runtime assets resolved relative to the installed package, such as jsdom's default stylesheet;
  • executable helper files resolved through require.resolve(), such as jsdom's synchronous XHR worker;
  • optional or native dependencies that must remain external, such as canvas;
  • conditional or undeclared dependency resolution that changes when a bundler flattens the module graph.

The last case affects jsdom 27/28: bundling can change how @acemir/cssom resolves its optional cssstyle dependency under strict package managers. The generated bundle can build and load, but getComputedStyle() may fail at runtime. Rstest therefore keeps these majors on native loading in auto mode. 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 use prebundle: 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 auto mode. At that point, enabling Rstest prebundling may no longer be necessary for that environment.

Known upstream bundling discussions:

Related Links

Checklist

  • Tests updated (or not required).
  • Documentation updated (or not required).

Copilot AI review requested due to automatic review settings August 3, 2026 03:55
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Aug 3, 2026

Copy link
Copy Markdown

Deploying rstest with  Cloudflare Pages  Cloudflare Pages

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

View logs

@github-actions

github-actions Bot commented Aug 3, 2026

Copy link
Copy Markdown

Rsdoctor Bundle Diff Analysis

Found 13 projects in monorepo, 2 projects with changes.

📊 Quick Summary
Project Total Size Change
adapter-rsbuild 6.1 KB 0
adapter-rslib 50.4 KB 0
adapter-rspack 9.2 KB 0
browser 1.1 MB 0
browser-react 6.4 KB 0
browser-ui 811.5 KB 0
coverage-istanbul 19.2 KB 0
coverage-v8 80.1 KB 0
playwright 48.0 KB 0
core/main 1.8 MB 📈 +20.3 KB (+1.1%)
core/browser 583.3 KB +804.0 B (0.1%)
vscode/extension 1.2 MB 0
vscode/worker 14.8 KB 0
📋 Detailed Reports (Click to expand)

📁 core/main

Path: packages/core/.rsdoctor/main/rsdoctor-data.json

📌 Baseline Commit: f64dfa6dce | PR: #1668

Metric Current Baseline Change
📊 Total Size 1.8 MB 1.8 MB +20.3 KB (+1.1%)
📄 JavaScript 1.4 MB 1.4 MB +17.1 KB (+1.2%)
🎨 CSS 0 B 0 B 0
🌐 HTML 0 B 0 B 0
📁 Other Assets 405.8 KB 402.7 KB +3.1 KB (0.8%)

📦 Download Diff Report: core/main Bundle Diff

📁 core/browser

Path: packages/core/.rsdoctor/browser/rsdoctor-data.json

📌 Baseline Commit: f64dfa6dce | PR: #1668

Metric Current Baseline Change
📊 Total Size 583.3 KB 582.5 KB +804.0 B (0.1%)
📄 JavaScript 435.2 KB 435.2 KB 0
🎨 CSS 0 B 0 B 0
🌐 HTML 0 B 0 B 0
📁 Other Assets 148.1 KB 147.3 KB +804.0 B (0.5%)

📦 Download Diff Report: core/browser Bundle Diff

Generated by Rsdoctor GitHub Action

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread packages/core/src/types/config.ts
Comment thread packages/core/tests/core/testEnvironmentModule.test.ts
Copilot AI review requested due to automatic review settings August 3, 2026 04:07

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 resolvedPath docstring 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

  • applyEnvironmentComment now always copies baseEnvironment.prebundle, even when the file-level environment comment switches to a different environment name. This makes a prebundle setting 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 }),
  };

Copilot AI review requested due to automatic review settings August 3, 2026 04:14

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

  • validateBuiltinDependency only checks that JSDOM exists, but setupEnvironment also requires CookieJar, ResourceLoader, and VirtualConsole. 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 to file:// URLs when dependencyType !== 'commonjs' so the generated ESM import remains portable.
  callback(
    undefined,
    resolvedPath ?? request,
    dependencyType === 'commonjs' ? 'commonjs' : 'module-import',
  );

Copilot AI review requested due to automatic review settings August 3, 2026 04:43

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

  • testEnvironmentExternal only treats Node built-ins via isBuiltin(request). In Yarn PnP setups, pnpapi is also treated as a built-in (see existing ADDITIONAL_NODE_BUILTINS usage elsewhere), so Rspack may try to bundle it into the environment prebundle and fail. Consider treating pnpapi (and node:-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;
  }

Copilot AI review requested due to automatic review settings August 3, 2026 06:07
@9aoy
9aoy marked this pull request as ready for review August 3, 2026 06:11
@9aoy 9aoy changed the title feat(core): prebundle test environments feat(core)!: prebundle test environments Aug 3, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.`,
  );

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment thread packages/core/src/core/executors/nodeExecutor.ts
Comment thread website/docs/en/config/test/test-environment.mdx Outdated
Comment thread packages/core/src/core/listTests.ts Outdated
Copilot AI review requested due to automatic review settings August 3, 2026 06:36

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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. include matches 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-empty entriesCache before preparing the modules map.
      testEnvironmentModules = await prepareTestEnvironmentModules({
        projects,
        rootPath,
      });

@9aoy
9aoy requested a review from fi3ework August 3, 2026 08:27

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge 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 👍 / 👎.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment on lines +241 to +244
config.optimization = {
...config.optimization,
runtimeChunk: false,
splitChunks: false,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Comment on lines +115 to +119
const packageJsonPath = resolveTestEnvironmentModule(
`${packageName}/package.json`,
projectRoot,
root,
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Comment on lines +72 to +76
const resolved = specifier.startsWith("node:") || specifier.startsWith("file:")
? specifier
: __rstestPathToFileURL(
__rstestModule.createRequire(origin || import.meta.url).resolve(specifier),
).href;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

@9aoy 9aoy changed the title feat(core)!: prebundle test environments feat(core): support prebundle test environments Aug 4, 2026
9aoy added 2 commits August 4, 2026 20:26
…nment-prebundle

# Conflicts:
#	packages/core/src/core/executors/nodeExecutor.ts
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants