Skip to content

fix(ui): repair scope SSR and shrink the ui pre-bundle 58MB to 24MB - #10628

Merged
GiladShoham merged 3 commits into
masterfrom
fix/ui-ssr-bundle-size-and-invalid-tag
Aug 18, 2026
Merged

fix(ui): repair scope SSR and shrink the ui pre-bundle 58MB to 24MB#10628
GiladShoham merged 3 commits into
masterfrom
fix/ui-ssr-bundle-size-and-invalid-tag

Conversation

@GiladShoham

Copy link
Copy Markdown
Member

Part 1 of #10596. Cuts the shipped @teambit/ui pre-bundle from 58 MB to 24 MB, and fixes scope SSR, which turned out to have been throwing on every request for months.

The workspace/scope single-compilation dedupe (~8 MB) is deliberately left for a follow-up PR — it needs an artifact-layout and .hash redesign, and mixing it in would make the SSR fix hard to verify.

Size: 58 MB → 24 MB

before after
ui-bundle/scope/public/bit/ssr/index.js 37 MB 6.8 MB
ui-bundle/scope 50 MB 16 MB
ui-bundle/workspace 8.2 MB 8.2 MB
total 58 MB 24 MB

Two causes, both in rspack.ssr.config.ts:

  • devtool: 'eval-cheap-module-source-map' wrapped every module in eval('…') with an inlined base64 source map — 2,566 of them, 23.4 MB, 60% of the file. It entered as 'eval-cheap-source-map', // TODO and was later retuned in chore(webpack): better development sourcemaps #7147 as a development sourcemap change; it was never a deliberate choice for a shipped artifact. Now shouldUseSourceMap ? 'source-map' : false, matching the browser config's opt-in.
  • The SSR build had no optimization.minimizer at all. It now mirrors the browser build's SwcJsMinimizerRspackPlugin.

Scope SSR was dead, and it took three fixes

Only scope.ui-root.ts sets ssr: true. The middleware catches any render error and calls next(), falling through to the static index.html — which looks completely normal in a browser. So the failure was invisible: curl / and curl '/?rendering=client' returned byte-identical 1,027-byte responses with an empty <div id="root"></div>.

Three independent defects were stacked:

  1. cjs missing from the SSR asset catch-all. The browser config excludes /\.(cjs|js|mjs|jsx|ts|tsx)$/; the SSR config omitted cjs, so every .cjs module was emitted as an asset/resource whose module value is the file's URL. A component imported from one reached React as a tag name → Minified React error #65 = "Invalid tag: /public/ssr/<hash>.cjs". Introduced by the webpack→rspack migration (feat(workspace | preview): migrate from webpack to rspack #10187), which replaced a shared base config with two hand-written ones and added cjs to only one.
  2. use-user-agent present at two versions (0.0.199 and 0.0.200). Each copy calls createContext, so ui.ui.runtime.tsx provided ssrBrowserContext on one instance while Tooltip's useUserAgent read the other, got undefined, took the browser fallback and dereferenced window on the server. Fixed by adding it to resolveAlias — the list that exists for exactly this class of bug. Aliasing the hook fixes every consumer, so the duplicated Tooltip copies need no aliasing of their own.
  3. window in a useEffect dependency arrayuseCurrentUser had }, [window.location.href]);. The effect body never runs on the server, but a dependency array is evaluated on every render, including the server one.

All routes now render server-side and the log shows zero SSR failures:

route before after
/ 1,027 B (fallback) 27,178 B
/ui/button 1,027 B (fallback) 51,726 B
/ui/button/~code 1,027 B (fallback) 61,246 B

A visible side effect: the document title was the build-time placeholder (bit-local-88bfe855) and is now the actual scope name.

Browser runtime effect

This turns SSR on for the first time in months, so it is a real runtime change. Bare scope on localhost, 7 runs, median, SSR vs ?rendering=client:

SSR client delta
FCP, warm cache 72 ms 384 ms −312 ms
FCP, cold cache 584 ms 376 ms +208 ms
TTFB 24 ms 2 ms +22 ms

SSR is ~5× faster to first paint on a repeat visit, and ~200 ms slower on a cold first load where the 6.4 MB of JS dominates and the larger HTML delays stylesheet discovery. Server startup is unchanged (~1.77 s); the first request drops from 0.79 s to 0.49 s while now doing a real render instead of throwing. Note localhost has ~0 latency, which flatters client rendering — over a network SSR's margin widens.

Testing

New e2e e2e/harmony/ui-ssr.e2e.ts asserts on the served HTML, since a browser cannot distinguish a working SSR render from the fallback.

It runs bit start --rebuild, which matters: getBundleUiPath resolves through getAspectDirFromBvm, so without --rebuild the server serves the pre-bundle from the installed bvm version and the test would assert on whatever bit release happens to be installed rather than on this code. HttpHelper gained an optional extra-args parameter for this.

Verified the test actually catches the bug — with the cjs fix reverted, 2 of the 3 assertions fail. (The third, an assertion that the scope name appears anywhere in the document, passed in both states because --rebuild puts the scope name in the static <title>; it now asserts inside #root instead.)

Manual verification on both UI roots, with the freshly built artifact swapped into the bvm install and the released .hash files preserved (a .hash mismatch makes bit start silently rebuild locally instead of serving the artifact). Confirmed the new bundle was genuinely served by diffing content-hashed asset names against the originals. Both roots checked in a real browser: workspace and scope home, component page, code tab, API reference — no console errors. The one error found (componentChanged subscription missing on a bare scope) reproduces identically on the original bundle and is pre-existing.

Bundle analysis tooling

BIT_UI_BUNDLE_STATS=1 makes the UI build write an rspack stats file per compilation, and scripts/analyze-bundle.mjs (npm run analyze-bundle) summarizes assets plus the heaviest packages and workspace scopes. No new dependency. This is what found defect 2 above.

It also surfaced that 81–86 packages appear at more than one version in a single bundle (@teambit/design.ui.tooltip at eight). That is only ~0.6 MB (~3% of module bytes), so it is a React-context correctness risk rather than a size lever — worth tracking separately.

Not in scope

The six env preview pre-bundles (@teambit/{react,node,mdx,env,aspect,readme}/artifacts/env-template) are 37.9 MB with only 6.4 MB of unique content — 31.5 MB is byte-identical across the six. That is now the largest single remaining item, bigger than the workspace/scope dedupe, and is not covered by #10596 as written.

@qodo-free-for-open-source-projects

Copy link
Copy Markdown

PR Summary by Qodo

Fix scope SSR failures and shrink shipped UI SSR bundle

🐞 Bug fix ✨ Enhancement 🧪 Tests 🕐 40+ Minutes

Grey Divider

AI Description

• Fix scope SSR by excluding .cjs from asset emission, aliasing use-user-agent, and guarding
 window access.
• Reduce shipped SSR bundle size by removing eval-* sourcemaps and enabling SWC minification.
• Add opt-in rspack bundle stats + analyzer script and an e2e test asserting SSR-rendered HTML.
Diagram

graph TD
  T(["E2E SSR test"]) --> S["bit start --rebuild"] --> B["UiMain build()"]; 
  B --> C[["rspack.ssr.config"]] --> O[("SSR bundle artifacts")] --> R["Scope SSR middleware"]; 
  B --> X[("bundle-stats/*.json")] --> A[["analyze-bundle.mjs"]];

  subgraph Legend
    direction LR
    _test(["Test"]) ~~~ _proc["Process"] ~~~ _art[("Artifact")] ~~~ _tool[["Tool/script"]]
  end
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Enforce single versions via package-manager overrides
  • ➕ Eliminates duplicate-context bugs without maintaining bundler aliases
  • ➕ Can reduce bundle variance across roots/compilations
  • ➖ May be hard to roll out safely across all workspaces/consumers
  • ➖ Doesn’t protect against future duplication via different entrypoints/exports conditions
2. Share a single base rspack config for browser+SSR
  • ➕ Prevents config drift (e.g., .cjs exclusion mismatch)
  • ➕ Makes future optimizations easier to apply consistently
  • ➖ Requires refactor of config composition; higher change surface
  • ➖ Harder to isolate SSR-specific differences and validate fixes quickly
3. Make consumers SSR-safe even when context is missing
  • ➕ More resilient to duplication/ordering issues
  • ➕ Can reduce reliance on global aliasing
  • ➖ Still leaves correctness risk when providers/consumers split contexts
  • ➖ Can hide real dependency duplication problems

Recommendation: The PR’s approach is appropriate for a targeted, verifiable SSR repair: fix the .cjs rule mismatch, alias the context-carrying hook package, and guard window usage. As follow-ups, consider consolidating browser/SSR config composition to prevent future drift, and evaluate package-manager overrides if duplicate versions remain a recurring source of React context split-brain.

Files changed (9) +270 / -6

Enhancement (3) +161 / -0
bundle-stats.tsAdd opt-in rspack stats writer for bundle diagnostics +40/-0

Add opt-in rspack stats writer for bundle diagnostics

• Adds 'BIT_UI_BUNDLE_STATS' support to write rspack stats JSON per compilation to a directory outside shipped artifacts. Exposes helpers to resolve the output dir and write stats safely.

scopes/ui-foundation/ui/rspack/bundle-stats.ts

ui.main.runtime.tsEmit optional bundle stats for browser and SSR builds +16/-0

Emit optional bundle stats for browser and SSR builds

• Integrates stats writing into the UI build pipeline for both browser and SSR compilations. Uses a best-effort 'try/catch' so diagnostics never fail a build, and logs the written stats path when enabled.

scopes/ui-foundation/ui/ui.main.runtime.ts

analyze-bundle.mjsAdd CLI to summarize rspack bundle stats and largest dependency buckets +105/-0

Add CLI to summarize rspack bundle stats and largest dependency buckets

• Adds a Node CLI that reads rspack stats JSON, prints largest assets, and attributes module sizes to actionable buckets (npm packages or workspace scopes). Helps identify both size drivers and multi-version duplication hotspots.

scripts/analyze-bundle.mjs

Bug fix (3) +36 / -4
use-current-user.tsFix SSR crash by removing 'window' from effect dependency evaluation +7/-2

Fix SSR crash by removing 'window' from effect dependency evaluation

• Moves 'window.location.href' access out of the 'useEffect' dependency array and guards it for server renders. The effect now exits early when no browser 'window' exists.

scopes/cloud/hooks/use-current-user/use-current-user.ts

rspack.common.tsAlias 'use-user-agent' hook package to prevent React context duplication +5/-0

Alias 'use-user-agent' hook package to prevent React context duplication

• Adds a resolve alias for '@teambit/ui-foundation.ui.hooks.use-user-agent' to force a single resolved copy in the UI graph. Prevents provider/consumer context splits that can lead to SSR 'window' dereferences.

scopes/ui-foundation/ui/rspack/rspack.common.ts

rspack.ssr.config.tsFix SSR bundling: drop eval sourcemaps, enable minimization, exclude '.cjs' from asset rule +24/-2

Fix SSR bundling: drop eval sourcemaps, enable minimization, exclude '.cjs' from asset rule

• Changes SSR devtool to opt-in 'source-map' (otherwise disabled) to avoid shipping 'eval-*' inlined maps. Adds SWC JS minimizer mirroring the browser build. Fixes the catch-all asset rule to exclude '.cjs', preventing '.cjs' modules from being emitted as asset URLs and reaching React as invalid tag names.

scopes/ui-foundation/ui/rspack/rspack.ssr.config.ts

Tests (2) +72 / -2
ui-ssr.e2e.tsAdd e2e coverage asserting scope SSR renders real HTML +64/-0

Add e2e coverage asserting scope SSR renders real HTML

• Introduces a dedicated e2e test that fetches '/' HTML and asserts SSR markup exists under '#root', and that no emitted SSR asset URL reaches React as a tag name. Runs 'bit start' with '--rebuild' to ensure the bundle is built from the repo under test.

e2e/harmony/ui-ssr.e2e.ts

http-helper.tsAllow passing extra 'bit start' flags from e2e tests +8/-2

Allow passing extra 'bit start' flags from e2e tests

• Extends HttpHelper to accept extra CLI args and appends them to the 'bit start' invocation. Enables tests to force '--rebuild' so assertions apply to the current branch’s UI bundle output.

e2e/http-helper.ts

Other (1) +1 / -0
package.jsonAdd 'analyze-bundle' npm script +1/-0

Add 'analyze-bundle' npm script

• Registers 'npm run analyze-bundle' to execute the new bundle stats summarizer script.

package.json

@qodo-free-for-open-source-projects

qodo-free-for-open-source-projects Bot commented Aug 18, 2026

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (1) 📘 Rule violations (1) 📜 Skill insights (0)

Grey Divider


Remediation recommended

1. Ad-hoc chalk in writeStats 📘 Rule violation ⚙ Maintainability
Description
The new writeStats() CLI output uses direct chalk formatting and a hardcoded [Rspack] prefix
instead of the repository’s shared CLI output formatting toolkit. This risks inconsistent CLI output
styling across commands and bypasses the documented style guide.
Code

scopes/ui-foundation/ui/ui.main.runtime.ts[R311-312]

+      const filePath = writeBundleStats(stats, name);
+      if (filePath) this.logger.console(`${chalk.magenta('[Rspack]')} wrote bundle stats to ${chalk.cyan(filePath)}`);
Evidence
PR Compliance ID 1 requires CLI output to follow the style guide and use the shared output
formatting utilities instead of ad-hoc chalk styling. The added log line formats output with
direct chalk calls and a hardcoded prefix, bypassing the shared formatter.

CLAUDE.md: CLI Output Must Follow the Repository CLI Output Style Guide and Shared Formatting Toolkit: CLAUDE.md: CLI Output Must Follow the Repository CLI Output Style Guide and Shared Formatting Toolkit: CLAUDE.md: CLI Output Must Follow the Repository CLI Output Style Guide and Shared Formatting Toolkit: CLAUDE.md: CLI Output Must Follow the Repository CLI Output Style Guide and Shared Formatting Toolkit
scopes/ui-foundation/ui/ui.main.runtime.ts[309-313]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`UiMain.writeStats()` prints CLI output using ad-hoc `chalk` formatting (e.g. `chalk.magenta('[Rspack]')`) rather than using the shared CLI output formatting toolkit.
## Issue Context
The repo requires CLI output to follow `scopes/harmony/cli/cli-output-style-guide.md` and use the shared formatter utilities from `@teambit/cli` (`scopes/harmony/cli/output-formatter.ts`) to keep output consistent and maintainable.
## Fix Focus Areas
- scopes/ui-foundation/ui/ui.main.runtime.ts[309-315]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. Stats path can break 🐞 Bug ◔ Observability
Description
writeBundleStats() builds the output path with the unsanitized name, so names containing path
separators can create an unintended nested path and make writeFileSync() fail (ENOENT).
UiMain.writeStats() swallows that failure (debug-only), so enabling BIT_UI_BUNDLE_STATS may produce
no stats files without any visible signal.
Code

scopes/ui-foundation/ui/rspack/bundle-stats.ts[R37-38]

+  const filePath = join(dir, `${name}.stats.json`);
+  writeFileSync(filePath, JSON.stringify(json));
Evidence
The stats writer only mkdirp’s the base dir, then writes to a filePath derived directly from
name; if name contains separators, the parent path won’t exist and the write will throw.
UiMain.writeStats wraps writeBundleStats in a try/catch and only logs at debug level, so the failure
is silent in normal output.

scopes/ui-foundation/ui/rspack/bundle-stats.ts[24-39]
scopes/ui-foundation/ui/ui.main.runtime.ts[305-316]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`writeBundleStats(stats, name)` uses `join(dir, `${name}.stats.json`)` and only creates `dir`. If `name` contains `/` or `\\`, the resulting `filePath` includes intermediate directories that do not exist, causing `writeFileSync()` to throw. The caller (`UiMain.writeStats`) catches and logs only at debug level, making the failure effectively silent when diagnostics are explicitly enabled.
## Issue Context
This is an opt-in diagnostics path (`BIT_UI_BUNDLE_STATS`), so it must be robust to odd names and should reliably write a file when enabled.
## Fix Focus Areas
- scopes/ui-foundation/ui/rspack/bundle-stats.ts[24-39]
- scopes/ui-foundation/ui/ui.main.runtime.ts[305-316]
## Suggested fix
- Sanitize `name` into a filename-safe value (e.g., replace `/` and `\\` with `_`, and prevent `..` segments).
- Ensure the parent directory of `filePath` exists (e.g., `mkdirpSync(dirname(filePath))`) before writing.
- (Optional) If writing fails, consider logging a `warn` (still not failing the build) so the user who opted in understands why no file appeared.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Tip of the day
💡 Did you know, you can keep summaries lean with Finding overflow, which tucks the rest behind 'View more'

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Previous reviews

Review updated until commit c1f9286 ⚖️ Balanced

Results up to commit 557eef4


🐞 Bugs (1) 📘 Rule violations (1) 📜 Skill insights (0)


Remediation recommended
1. Ad-hoc chalk in writeStats 📘 Rule violation ⚙ Maintainability
Description
The new writeStats() CLI output uses direct chalk formatting and a hardcoded [Rspack] prefix
instead of the repository’s shared CLI output formatting toolkit. This risks inconsistent CLI output
styling across commands and bypasses the documented style guide.
Code

scopes/ui-foundation/ui/ui.main.runtime.ts[R311-312]

+      const filePath = writeBundleStats(stats, name);
+      if (filePath) this.logger.console(`${chalk.magenta('[Rspack]')} wrote bundle stats to ${chalk.cyan(filePath)}`);
Evidence
PR Compliance ID 1 requires CLI output to follow the style guide and use the shared output
formatting utilities instead of ad-hoc chalk styling. The added log line formats output with
direct chalk calls and a hardcoded prefix, bypassing the shared formatter.

CLAUDE.md: CLI Output Must Follow the Repository CLI Output Style Guide and Shared Formatting Toolkit: CLAUDE.md: CLI Output Must Follow the Repository CLI Output Style Guide and Shared Formatting Toolkit
scopes/ui-foundation/ui/ui.main.runtime.ts[309-313]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`UiMain.writeStats()` prints CLI output using ad-hoc `chalk` formatting (e.g. `chalk.magenta('[Rspack]')`) rather than using the shared CLI output formatting toolkit.
## Issue Context
The repo requires CLI output to follow `scopes/harmony/cli/cli-output-style-guide.md` and use the shared formatter utilities from `@teambit/cli` (`scopes/harmony/cli/output-formatter.ts`) to keep output consistent and maintainable.
## Fix Focus Areas
- scopes/ui-foundation/ui/ui.main.runtime.ts[309-315]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. Stats path can break 🐞 Bug ◔ Observability
Description
writeBundleStats() builds the output path with the unsanitized name, so names containing path
separators can create an unintended nested path and make writeFileSync() fail (ENOENT).
UiMain.writeStats() swallows that failure (debug-only), so enabling BIT_UI_BUNDLE_STATS may produce
no stats files without any visible signal.
Code

scopes/ui-foundation/ui/rspack/bundle-stats.ts[R37-38]

+  const filePath = join(dir, `${name}.stats.json`);
+  writeFileSync(filePath, JSON.stringify(json));
Evidence
The stats writer only mkdirp’s the base dir, then writes to a filePath derived directly from
name; if name contains separators, the parent path won’t exist and the write will throw.
UiMain.writeStats wraps writeBundleStats in a try/catch and only logs at debug level, so the failure
is silent in normal output.

scopes/ui-foundation/ui/rspack/bundle-stats.ts[24-39]
scopes/ui-foundation/ui/ui.main.runtime.ts[305-316]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`writeBundleStats(stats, name)` uses `join(dir, `${name}.stats.json`)` and only creates `dir`. If `name` contains `/` or `\\`, the resulting `filePath` includes intermediate directories that do not exist, causing `writeFileSync()` to throw. The caller (`UiMain.writeStats`) catches and logs only at debug level, making the failure effectively silent when diagnostics are explicitly enabled.
## Issue Context
This is an opt-in diagnostics path (`BIT_UI_BUNDLE_STATS`), so it must be robust to odd names and should reliably write a file when enabled.
## Fix Focus Areas
- scopes/ui-foundation/ui/rspack/bundle-stats.ts[24-39]
- scopes/ui-foundation/ui/ui.main.runtime.ts[305-316]
## Suggested fix
- Sanitize `name` into a filename-safe value (e.g., replace `/` and `\\` with `_`, and prevent `..` segments).
- Ensure the parent directory of `filePath` exists (e.g., `mkdirpSync(dirname(filePath))`) before writing.
- (Optional) If writing fails, consider logging a `warn` (still not failing the build) so the user who opted in understands why no file appeared.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Results up to commit ea3def7


🐞 Bugs (1) 📘 Rule violations (1) 📜 Skill insights (0)


Remediation recommended
1. Ad-hoc chalk in writeStats 📘 Rule violation ⚙ Maintainability
Description
The new writeStats() CLI output uses direct chalk formatting and a hardcoded [Rspack] prefix
instead of the repository’s shared CLI output formatting toolkit. This risks inconsistent CLI output
styling across commands and bypasses the documented style guide.
Code

scopes/ui-foundation/ui/ui.main.runtime.ts[R311-312]

+      const filePath = writeBundleStats(stats, name);
+      if (filePath) this.logger.console(`${chalk.magenta('[Rspack]')} wrote bundle stats to ${chalk.cyan(filePath)}`);
Evidence
PR Compliance ID 1 requires CLI output to follow the style guide and use the shared output
formatting utilities instead of ad-hoc chalk styling. The added log line formats output with
direct chalk calls and a hardcoded prefix, bypassing the shared formatter.

CLAUDE.md: CLI Output Must Follow the Repository CLI Output Style Guide and Shared Formatting Toolkit
scopes/ui-foundation/ui/ui.main.runtime.ts[309-313]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`UiMain.writeStats()` prints CLI output using ad-hoc `chalk` formatting (e.g. `chalk.magenta('[Rspack]')`) rather than using the shared CLI output formatting toolkit.

## Issue Context
The repo requires CLI output to follow `scopes/harmony/cli/cli-output-style-guide.md` and use the shared formatter utilities from `@teambit/cli` (`scopes/harmony/cli/output-formatter.ts`) to keep output consistent and maintainable.

## Fix Focus Areas
- scopes/ui-foundation/ui/ui.main.runtime.ts[309-315]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. Stats path can break 🐞 Bug ◔ Observability
Description
writeBundleStats() builds the output path with the unsanitized name, so names containing path
separators can create an unintended nested path and make writeFileSync() fail (ENOENT).
UiMain.writeStats() swallows that failure (debug-only), so enabling BIT_UI_BUNDLE_STATS may produce
no stats files without any visible signal.
Code

scopes/ui-foundation/ui/rspack/bundle-stats.ts[R37-38]

+  const filePath = join(dir, `${name}.stats.json`);
+  writeFileSync(filePath, JSON.stringify(json));
Evidence
The stats writer only mkdirp’s the base dir, then writes to a filePath derived directly from
name; if name contains separators, the parent path won’t exist and the write will throw.
UiMain.writeStats wraps writeBundleStats in a try/catch and only logs at debug level, so the failure
is silent in normal output.

scopes/ui-foundation/ui/rspack/bundle-stats.ts[24-39]
scopes/ui-foundation/ui/ui.main.runtime.ts[305-316]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`writeBundleStats(stats, name)` uses `join(dir, `${name}.stats.json`)` and only creates `dir`. If `name` contains `/` or `\\`, the resulting `filePath` includes intermediate directories that do not exist, causing `writeFileSync()` to throw. The caller (`UiMain.writeStats`) catches and logs only at debug level, making the failure effectively silent when diagnostics are explicitly enabled.

## Issue Context
This is an opt-in diagnostics path (`BIT_UI_BUNDLE_STATS`), so it must be robust to odd names and should reliably write a file when enabled.

## Fix Focus Areas
- scopes/ui-foundation/ui/rspack/bundle-stats.ts[24-39]
- scopes/ui-foundation/ui/ui.main.runtime.ts[305-316]

## Suggested fix
- Sanitize `name` into a filename-safe value (e.g., replace `/` and `\\` with `_`, and prevent `..` segments).
- Ensure the parent directory of `filePath` exists (e.g., `mkdirpSync(dirname(filePath))`) before writing.
- (Optional) If writing fails, consider logging a `warn` (still not failing the build) so the user who opted in understands why no file appeared.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Qodo Logo

Comment on lines +311 to +312
const filePath = writeBundleStats(stats, name);
if (filePath) this.logger.console(`${chalk.magenta('[Rspack]')} wrote bundle stats to ${chalk.cyan(filePath)}`);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Remediation recommended

1. Ad-hoc chalk in writestats 📘 Rule violation ⚙ Maintainability

The new writeStats() CLI output uses direct chalk formatting and a hardcoded [Rspack] prefix
instead of the repository’s shared CLI output formatting toolkit. This risks inconsistent CLI output
styling across commands and bypasses the documented style guide.
Agent Prompt
## Issue description
`UiMain.writeStats()` prints CLI output using ad-hoc `chalk` formatting (e.g. `chalk.magenta('[Rspack]')`) rather than using the shared CLI output formatting toolkit.

## Issue Context
The repo requires CLI output to follow `scopes/harmony/cli/cli-output-style-guide.md` and use the shared formatter utilities from `@teambit/cli` (`scopes/harmony/cli/output-formatter.ts`) to keep output consistent and maintainable.

## Fix Focus Areas
- scopes/ui-foundation/ui/ui.main.runtime.ts[309-315]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines +37 to +38
const filePath = join(dir, `${name}.stats.json`);
writeFileSync(filePath, JSON.stringify(json));

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Remediation recommended

2. Stats path can break 🐞 Bug ◔ Observability

writeBundleStats() builds the output path with the unsanitized name, so names containing path
separators can create an unintended nested path and make writeFileSync() fail (ENOENT).
UiMain.writeStats() swallows that failure (debug-only), so enabling BIT_UI_BUNDLE_STATS may produce
no stats files without any visible signal.
Agent Prompt
## Issue description
`writeBundleStats(stats, name)` uses `join(dir, `${name}.stats.json`)` and only creates `dir`. If `name` contains `/` or `\\`, the resulting `filePath` includes intermediate directories that do not exist, causing `writeFileSync()` to throw. The caller (`UiMain.writeStats`) catches and logs only at debug level, making the failure effectively silent when diagnostics are explicitly enabled.

## Issue Context
This is an opt-in diagnostics path (`BIT_UI_BUNDLE_STATS`), so it must be robust to odd names and should reliably write a file when enabled.

## Fix Focus Areas
- scopes/ui-foundation/ui/rspack/bundle-stats.ts[24-39]
- scopes/ui-foundation/ui/ui.main.runtime.ts[305-316]

## Suggested fix
- Sanitize `name` into a filename-safe value (e.g., replace `/` and `\\` with `_`, and prevent `..` segments).
- Ensure the parent directory of `filePath` exists (e.g., `mkdirpSync(dirname(filePath))`) before writing.
- (Optional) If writing fails, consider logging a `warn` (still not failing the build) so the user who opted in understands why no file appeared.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

@GiladShoham
GiladShoham enabled auto-merge (squash) August 18, 2026 18:00
@qodo-free-for-open-source-projects

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit 557eef4

@qodo-free-for-open-source-projects

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit c1f9286

@GiladShoham
GiladShoham merged commit 3f8ee8d into master Aug 18, 2026
14 checks passed
@GiladShoham
GiladShoham deleted the fix/ui-ssr-bundle-size-and-invalid-tag branch August 18, 2026 21:25
GiladShoham added a commit that referenced this pull request Aug 19, 2026
Part 2 of #10596 — the workspace/scope dedupe. Builds both UI roots in
**one** rspack compilation with an entry each, so the chunks they share
are emitted once. Takes the shipped `@teambit/ui` pre-bundle from **24
MB to 16 MB**.

> Stacked on #10628 and targets that branch. Retarget to `master` once
#10628 merges.

## The roots are the same app

The issue estimated a small win from file-level comparison ("only 25
files / 1.9 MB byte-identical"). At the *module* level they are all but
identical:

| | modules | bytes |
| --- | --- | --- |
| workspace root | 3,403 | 16.00 MB |
| scope root | 3,403 | 16.00 MB |
| **shared by both** | **3,402** | **15.96 MB** |
| workspace-only | 1 | 40 KB |
| scope-only | 1 | 40 KB |

The one differing module is the generated root entry
(`ui.root<hash>.js`) — same aspect graph, different root aspect id baked
in. Both the `workspace` and `scope` aspects are already in *both*
bundles; the root id only selects which one renders. So the duplicate is
essentially total, and so is the saving.

| | before | after |
| --- | --- | --- |
| browser chunks | 16.4 MB (2 copies) | **8.2 MB** (1 copy) |
| ssr | 7.9 MB | 7.9 MB |
| **artifact total** | **24 MB** | **16 MB** |

The 6.53 MB vendor chunk and 0.53 MB CSS are now shared; each root adds
~40 KB of its own. Combined with #10628 this is 58 MB → 16 MB.

It also lowers peak memory during `bit build` — the concern raised on
#10612 — since there is one module graph instead of two, and the
`BundleUI` task got slightly faster (17s vs 18-19s).

## Layout

```
artifacts/ui-bundle/
  .hash                     JSON: { "<rootAspectId>": "<sha1>", … }
  public/bit/
    workspace.html          only the chunks the workspace entry needs
    scope.html              only the chunks the scope entry needs
    asset-manifest.json
    static/js|css/…         shared chunks, emitted once
    ssr/index.js            scope only
```

Three things had to change to support it:

1. **`.hash` is now a map.** `shouldServeBundleUi` compares a *per-root*
hash, but there is now one bundle. `.hash` holds one hash per root
aspect id and `readBundleUiHash` looks up the root being served. A root
missing from the map — or a bundle in the old layout — reads as "no
pre-bundle" and falls back to a local build.
2. **No more `index.html`.** With two roots in one compilation there is
no single default document, so the server falls back to `<root>.html`.
This is the sharpest edge in the PR: get the name wrong and every
*client-side* route 404s while the SSR-rendered ones keep working. There
is a test for exactly that (below).
3. **The asset manifest is entry-aware.** `generateAssetManifest`
hardcoded `entrypoints.main`, and it is shared with the preview aspect's
rspack config, so renaming was not an option. It now also emits
`entrypointsByName`; `entrypoints` keeps its old meaning for
single-entry compilations, so the preview side is untouched. The SSR
middleware prefers its root's entry and falls back to `entrypoints`.

`build()` now builds every registered root rather than one, so
`uiRootAspectIdOrName` only selects the output location. That is nearly
free — the module graph is shared — and it keeps one layout everywhere
instead of one for the build task and another for `bit start`. A bare
scope registers only the scope root, so it emits just `scope.html`.

`bit start --dev` is unaffected; the dev config is separate and still
single-entry.

## Deletes machinery

Because the two roots bundled concurrently, #10612 had to defer closing
the compilers (`openBuildCompilers` + a `deferClose` option on
`UiMain.build`) instead of closing inside `build()`. With one compiler
there is no concurrent sibling, and it collapses into a single close in
`build()`'s `finally`. `buildIfNoBundle` also stopped constructing a
whole rspack config — resolving every root's aspects — just to read
`output.path`.

## Testing

Both roots verified in a real browser, on **both** paths that can serve
them:

- *local build* (`bit start --rebuild`): bare scope emits `scope.html`
and SSRs every route; workspace emits both htmls with the 6.53 MB vendor
chunk present once.
- *pre-bundle* (the production path): built artifact placed in the bvm
install, `bit start` logged `returned from ui bundle cache` and `bundle
will be served from …/ui-bundle/public/bit`, with no local build
directory created. Scope SSR renders (`/` 27 KB, `/ui/button` 52 KB);
workspace serves `workspace.html` through the fallback for client-side
routes.

Workspace and scope home pages, component page, code tab and API
reference all render with no console errors.

New e2e assertion covers change 2 — `?rendering=client` makes the SSR
middleware call `next()`, which is the only way to reach the history-api
fallback. Verified it catches the bug: pointing the fallback back at
`index.html` fails that test and only that test, which is precisely the
failure mode described above.
GiladShoham added a commit that referenced this pull request Aug 19, 2026
…10628/#10629 (#10631)

Sanity e2e for `bit start` itself, covering both UI roots. Part 3 of
#10596 follow-up work.

> Rebased onto `master` now that #10628 and #10629 have merged; targets
`master` directly.

Until now nothing exercised `bit start` end to end. That is how the
scope SSR bundle managed to throw on every request for months (#10628),
and the layout change in #10629 has a matching failure mode: name the
fallback document wrong and every *client-side* route 404s while
SSR-rendered ones keep working.

`e2e/harmony/ui-start.e2e.ts` starts a real server per root and asserts,
over http:

- startup writes nothing matching `/error|exception|unhandled/i` to
stderr — a server can listen fine with an aspect that failed to load
- the served document has a react root
- **every script and stylesheet the document references actually
resolves 200** — this is the one that catches assets emitted under a
path the server does not expose
- a deep client-side route returns a document (the history-api fallback)
- `/graphql` answers without errors
- workspace only: the document loads the *workspace* entry and not the
scope one — both roots are entries of one bundle now, so serving the
wrong document would still look like a working page, just booting the
other root's app
- scope only: the markup is server-rendered and contains the exported
component

12 assertions, ~1 min. All `--rebuild`, so they describe this repo's
code rather than whichever bit release is installed.

## Two supporting changes

**`HttpHelper` can start either root.** It was hardcoded to the bare
scope (`scopes.remotePath`, and a ready-message string naming
`teambit.scope/scope`). It now takes `{ extraArgs, uiRootAspectId }`,
derives the cwd from the root, and builds the ready message per root.
Existing callers use the unchanged two-arg form. It also records stderr
so tests can assert on a clean startup.

**`portHolders()` now filters to listening sockets** (`lsof -ti tcp:PORT
-sTCP:LISTEN`). Without it lsof also reports processes holding a
*client* socket to the port — including the mocha process itself, since
node keeps connections alive after a test fetches from the server.
`waitForPortToBeFree` read that as a foreign process squatting the port
and refused to continue, failing the `after` hooks. Only a listener can
actually hold a port. This was a latent bug in the helper; the new tests
hit it because they fetch every referenced asset.



---

## Also: Qodo review findings from #10628 / #10629

Both of those merged before their review findings were addressed, so the
actionable ones land here. Each was verified against the code rather
than taken on trust.

**`bit start` 404s on an existing local UI build (from #10629) — the
important one.** `buildIfNoBundle()` treated *any* existing `public/bit`
directory as a valid build, but the server now falls back to
`<root>.html`, which a build made before #10629 does not contain.
Reproduced end to end: with the pre-fix check the whole UI returns
**404** on `/` and on deep routes; with the fix it detects the missing
document, rebuilds, and serves 200. This would have hit every user
upgrading past #10629 with a previously-built local UI. It now checks
for the root's document rather than the directory.

**Hash written for roots that were never built (from #10629).**
`generateHash()` walked a hardcoded root list and threw when one was not
registered. Beyond failing in a scope-only runtime, it could record a
hash for a root whose document was never emitted — which reads at
startup as "a pre-bundle exists" and then 404s, the same failure as
above. It now walks the same registered roots `build()` turns into
entries, via a new `UiMain.getUiRoots()`.

**Service worker bound to a document that is not emitted (from
#10629).** Confirmed in the built artifact: `service-worker.js`
contained `createHandlerBoundToURL("public/index.html")` while the build
emits only `scope.html` / `workspace.html`. With an entry per root there
is no single app shell, so `navigateFallback` is removed — the express
history-api fallback already serves the right document. Verified the
built service worker no longer contains that binding.

**Entry name collisions (from #10629).** `Object.fromEntries` would
silently keep only the last of two entries sharing a sanitized name,
leaving a root with no chunks and no document while still looking built.
Now throws instead.

**Stats filename could break (from #10628).** `writeBundleStats`
interpolated an unsanitized name into a path, so a root name containing
`/` would fail with ENOENT into a swallowed debug log. Now sanitized.

Not changed: the "ad-hoc chalk in `writeStats`" rule violation. That
line matches the surrounding `[Rspack]` log statements in the same file;
the style guide it cites covers section titles and symbols in command
output, not diagnostic log lines. Happy to switch it if you'd rather be
strict.

The `preview/bundle-stats.ts` copy of the sanitization fix lands with
#10632, which is where that file lives.
GiladShoham added a commit that referenced this pull request Aug 19, 2026
…10628/#10629 (#10631)

Sanity e2e for `bit start` itself, covering both UI roots. Part 3 of

> Rebased onto `master` now that #10628 and #10629 have merged; targets
`master` directly.

Until now nothing exercised `bit start` end to end. That is how the
scope SSR bundle managed to throw on every request for months (#10628),
and the layout change in #10629 has a matching failure mode: name the
fallback document wrong and every *client-side* route 404s while
SSR-rendered ones keep working.

`e2e/harmony/ui-start.e2e.ts` starts a real server per root and asserts,
over http:

- startup writes nothing matching `/error|exception|unhandled/i` to
stderr — a server can listen fine with an aspect that failed to load
- the served document has a react root
- **every script and stylesheet the document references actually
resolves 200** — this is the one that catches assets emitted under a
path the server does not expose
- a deep client-side route returns a document (the history-api fallback)
- `/graphql` answers without errors
- workspace only: the document loads the *workspace* entry and not the
scope one — both roots are entries of one bundle now, so serving the
wrong document would still look like a working page, just booting the
other root's app
- scope only: the markup is server-rendered and contains the exported
component

12 assertions, ~1 min. All `--rebuild`, so they describe this repo's
code rather than whichever bit release is installed.

**`HttpHelper` can start either root.** It was hardcoded to the bare
scope (`scopes.remotePath`, and a ready-message string naming
`teambit.scope/scope`). It now takes `{ extraArgs, uiRootAspectId }`,
derives the cwd from the root, and builds the ready message per root.
Existing callers use the unchanged two-arg form. It also records stderr
so tests can assert on a clean startup.

**`portHolders()` now filters to listening sockets** (`lsof -ti tcp:PORT
-sTCP:LISTEN`). Without it lsof also reports processes holding a
*client* socket to the port — including the mocha process itself, since
node keeps connections alive after a test fetches from the server.
`waitForPortToBeFree` read that as a foreign process squatting the port
and refused to continue, failing the `after` hooks. Only a listener can
actually hold a port. This was a latent bug in the helper; the new tests
hit it because they fetch every referenced asset.

---

Both of those merged before their review findings were addressed, so the
actionable ones land here. Each was verified against the code rather
than taken on trust.

**`bit start` 404s on an existing local UI build (from #10629) — the
important one.** `buildIfNoBundle()` treated *any* existing `public/bit`
directory as a valid build, but the server now falls back to
`<root>.html`, which a build made before #10629 does not contain.
Reproduced end to end: with the pre-fix check the whole UI returns
**404** on `/` and on deep routes; with the fix it detects the missing
document, rebuilds, and serves 200. This would have hit every user
upgrading past #10629 with a previously-built local UI. It now checks
for the root's document rather than the directory.

**Hash written for roots that were never built (from #10629).**
`generateHash()` walked a hardcoded root list and threw when one was not
registered. Beyond failing in a scope-only runtime, it could record a
hash for a root whose document was never emitted — which reads at
startup as "a pre-bundle exists" and then 404s, the same failure as
above. It now walks the same registered roots `build()` turns into
entries, via a new `UiMain.getUiRoots()`.

**Service worker bound to a document that is not emitted (from
contained `createHandlerBoundToURL("public/index.html")` while the build
emits only `scope.html` / `workspace.html`. With an entry per root there
is no single app shell, so `navigateFallback` is removed — the express
history-api fallback already serves the right document. Verified the
built service worker no longer contains that binding.

**Entry name collisions (from #10629).** `Object.fromEntries` would
silently keep only the last of two entries sharing a sanitized name,
leaving a root with no chunks and no document while still looking built.
Now throws instead.

**Stats filename could break (from #10628).** `writeBundleStats`
interpolated an unsanitized name into a path, so a root name containing
`/` would fail with ENOENT into a swallowed debug log. Now sanitized.

Not changed: the "ad-hoc chalk in `writeStats`" rule violation. That
line matches the surrounding `[Rspack]` log statements in the same file;
the style guide it cites covers section titles and symbols in command
output, not diagnostic log lines. Happy to switch it if you'd rather be
strict.

The `preview/bundle-stats.ts` copy of the sanitization fix lands with

(cherry picked from commit 59bd5c2)
GiladShoham added a commit that referenced this pull request Aug 19, 2026
…SSR gap

Rebuilt the UI/preview pre-bundle from current source and refreshed
.bundle-cache/ - UI artifact 80 MB -> 16 MB, matching upstream #10629's
single-compilation dedupe now that it's reflected on this branch. Verified
end to end against a real `npm run bundle` build: 16/16 UI-bundling sanity
tests passing, including SSR. Total shipped distribution 216 MB / 2,933
files -> 160 MB / 2,839 files.

Also documents a scope-UI SSR crash found while validating (window is not
defined in useUserAgent), confirmed scoped to local --rebuild mode only -
the shipped, forPreBundle-filtered pre-bundle is unaffected. Not fixed
this session; tracked as a known gap.

See PRs #10628, #10629, #10631 for the upstream work behind the numbers.
GiladShoham added a commit that referenced this pull request Aug 19, 2026
Adds `docs/ui-bundle-size-analysis.md` — where the remaining bundle size
sits after #10628 and #10629, what I measured, and what I tried that did
not work. Written to be picked up cold in a later session.

> Rebased onto `master` now that #10628, #10629 and #10631 have merged;
targets `master` directly.

Env preview duplication is deliberately excluded — the core envs are
being removed, which takes it along.

## Also in here

- **Preview bundle stats.** `BIT_UI_BUNDLE_STATS=1` now covers the
preview pre-bundle too, so one build produces `browser`, `scope-ssr` and
`preview` stats together. It is a small copy of the UI helper rather
than an import: `@teambit/ui`'s index is imported by browser code, and
re-exporting a node-only module through it pulled `fs` polyfills into
the UI bundle (caught by the build failing on `Can't resolve
'constants'`).
- **Stats were being under-reported.** rspack's `toJson` groups assets
and modules into summary rows ("assets by status") that carry a size but
no name. That showed up as a single unattributable 3.3 MB / 17.9%
bucket, and `assets: 0`. All `groupModulesBy*` / `groupAssetsBy*` flags
are now off.
- **`analyze-bundle.mjs` crashed** on assets without a `name` (those
same grouped rows).
- **`code-view.tsx`** imported `createElement` from the
`react-syntax-highlighter` package root, which defeats its own
`prism-light` import two lines later. Changed to the deep path.
Behaviour-neutral, and worth stating plainly: **it saves nothing today**
— see below.

## Headline findings

**The two eagerly-loaded syntax highlighting registries are the biggest
single item** — `highlight.js` (1.34 MB, every language) plus
`refractor` (0.85 MB, every Prism language), in both the browser and ssr
bundles. Neither is imported directly anywhere in this repo. They come
in through `react-syntax-highlighter`'s package root, which re-exports
every build including the full-language ones.

The blocker is that the remaining root imports are in *published*
`@teambit` components in `node_modules`
(`api-reference.renderers.schema-node-member-summary`,
`documenter.ui.code-snippet`), whose source is not in this repo. I
verified this: fixing the in-repo imports changes the artifact by **0
bytes**. Filed separately as #10633.

I tried the bundler-level workaround (alias `lowlight` →
`lowlight/lib/core`, `refractor` → `refractor/core`) and **rejected
it**: bit fails the build because both are transitive and would have to
be declared dependencies of `@teambit/ui`, and it silently degrades any
consumer relying on auto-registered languages to plain text. That is a
product call, not a build one.

**`lodash` is the best effort-to-reward item.** It is CJS-only (no
`module` field), so it cannot be tree-shaken, and the repo has 280 `from
'lodash'` imports and zero cherry-picked ones. It is 0.52 MB of the 1.96
MB preview bundle — 28% — for six functions. Fixing it pays out in the
browser, ssr and preview bundles at once.

Also documented: `graphql` shipping whole into preview (28%), `sucrase`
(0.47 MB) arriving via `react-live` and never lazy-loaded, `date-fns` at
302 modules, and the fact that one 6.23 MB chunk is the entire eager
payload — which is what makes the cold-cache first paint slower than
client-only rendering (measured in #10628).

`@shikijs/langs` is 1.45 MB but already lazy-loaded per language, and is
called out as the pattern the rest of the UI should copy.



## Update after the stack merged

Rebased onto `master`. Two follow-ups folded in:

- **`preview/bundle-stats.ts` gets the filename sanitization** that
#10631 applied to its UI twin (a Qodo finding from #10628). This file
only exists on this branch, which is why it was carried over rather than
fixed there.
- **The doc records that the service worker no longer claims
navigations.** #10631 removed the `navigateFallback` that still pointed
at an `index.html` the multi-entry build stopped emitting, so the
analysis notes that re-adding an offline shell now has to answer what
that means for two roots.

Qodo reviewed this PR and found no issues.

Re-validated on the rebased branch: full compile, fresh-capsule build of
both bundles with `BIT_UI_BUNDLE_STATS=1`, and `analyze-bundle.mjs`
reproducing the figures the doc quotes — 6.23 MB eager chunk,
`@shikijs/langs` 1.45 MB / 9.5%, `highlight.js` 1.34 MB / 8.7%, and in
preview `graphql` 0.52 MB / 27.9% next to `lodash` 0.52 MB / 27.7%.
GiladShoham added a commit that referenced this pull request Aug 19, 2026
`useUserAgent` reads `ssrBrowserContext`, and that context is provided
in exactly one place — wrapping only `{hudItems}` and `{routes}`,
*inside* `ClientContext`. Everything rendered outside that subtree
therefore gets `undefined`, falls back to `window.navigator`, and throws
`ReferenceError: window is not defined` on the server. The ssr
middleware swallows it and serves the empty client shell, so the page
still looks fine in a browser.

`ClientContext` is not a bare provider: it renders icons, the theme
switcher, the loader ribbon and the tooltip mount point. Any of those
growing a `useIsMobile` call — directly or through a dependency bump —
takes scope SSR down. Swapping the nesting so `SSRBrowserProvider` wraps
`ClientContext` closes the whole class.

## Why this surfaced now

Reported from a branch that removes core envs from the manifest, where
the crash reproduces deterministically:

```
[ssr] failed at '/'
ReferenceError: window is not defined
    at useUserAgent → useIsMobile → Tooltip
```

That branch already has all of #10628's fixes, so this is not a
regression of them — it is a second, independent gap that #10628 did not
cover. It was latent on `master` only because nothing inside
`ClientContext` currently happens to call `useIsMobile`; changing which
aspects load changes what renders there.

Worth being explicit that the `.cjs` fix and the `use-user-agent` alias
from #10628 are still doing their jobs. This is a tree-position bug, not
a duplicate-context one — the alias pins the module to a single copy, so
provider and consumer share the context object; the consumer is simply
rendered where no provider is above it.

## Proof

Rendering a `Tooltip` inside `ClientContext` — what the reporting branch
effectively does — and changing nothing else:

| nesting | `ui-ssr.e2e.ts` |
| --- | --- |
| `ClientContext` > `SSRBrowserProvider` (before) | **2 failing** —
`#root` empty, ssr fell back to the client shell |
| `SSRBrowserProvider` > `ClientContext` (after) | **4 passing** |

The synthetic `Tooltip` was then removed; both UI suites pass on the
final diff (16 assertions).

## Residual gap

Render-plugin `reactContext`s (pubsub, lanes, notifications, user-agent)
are applied by `ServerRenderer` *outside* the JSX this file passes, so
they remain above the provider. A component rendered by one of those
contexts — as opposed to inside its children — would still hit this.
Covering that needs `ssrBrowserContext` supplied as a render plugin
rather than as JSX, which is a larger change and not what the reported
failure needs.

Also worth noting separately: `scopes/ui-foundation/user-agent` provides
`userAgentContext`, which `useUserAgent` never reads — it only reads
`ssrBrowserContext`. That aspect looks vestigial for this purpose.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

2 participants