Skip to content

[chat] Fix global preload error from the remend import - #23263

Open
hasdfa wants to merge 6 commits into
mui:masterfrom
hasdfa:t3code/fix-chat-remend-import-23160
Open

[chat] Fix global preload error from the remend import#23263
hasdfa wants to merge 6 commits into
mui:masterfrom
hasdfa:t3code/fix-chat-remend-import-23160

Conversation

@hasdfa

@hasdfa hasdfa commented Aug 1, 2026

Copy link
Copy Markdown
Member

Closes #23160

The bug

remend was imported through a variable specifier plus @vite-ignore / webpackIgnore hints:

const REMEND_SPECIFIER = 'remend';
return import(/* @vite-ignore */ /* webpackIgnore: true */ REMEND_SPECIFIER);

No bundler can statically analyze that, so browser bundles were left with a bare import('remend') that can never resolve without an import map. Two consequences, both reported in the issue:

  1. The remend upgrade was dead code in every bundled app.catch(() => fallbackRepair) always ran, regardless of bundler.
  2. Under Vite 8 (rolldown) the unresolvable import is wrapped in __vitePreload, which ends in baseModule().catch(handlePreloadError) and dispatches a global vite:preloadError. Apps following Vite's documented reload recipe reload on every assistant reply.

I reproduced both by building @mui/x-chat and consuming it from a real Vite 8 production build. The output bundle contained c = "remend"; __vitePreload(() => import(c), []) with no remend code bundled, and the page logged:

PRELOAD_ERROR: TypeError: Failed to resolve module specifier 'remend'
REPAIR: fallbackRepair (remend did NOT load)
OUTPUT: "a **bold"

Why not just inline the specifier

The variable was guarding something real. remend is ESM-only — its exports map declares no require condition — so a plain import('remend') downlevels to require('remend') in the CJS build, and webpack then hard-fails:

Module not found: "." is not exported under the conditions ["require", ...] from package remend

I confirmed this by trying it. The specifier genuinely has to differ per output format; the previous code just picked an escape hatch that broke the browser instead.

The fix

A #remend subpath import in packages/x-chat/package.json:

"#remend": {
  "import": "remend",
  "default": "./src/internals/remendUnavailable.ts"
}

code-infra already rewrites #-prefixed imports that point at source files into built paths with import/require/types conditions (createPackageImports), producing:

"#remend": {
  "import": "remend",
  "default": { "import": {...}, "require": { "default": "./internals/remendUnavailable.js" }, ... }
}

So the loader becomes a plain, statically analyzable import('#remend'). ESM resolves the real package; CJS resolves a stub whose non-function export trips the existing typeof remend !== 'function' branch and degrades to fallbackRepair, exactly as before.

Verification

Since this is a packaging bug, unit tests alone can't confirm it. I built the package and exercised all four consumption paths:

Path Before After
Vite 8 browser production preloadError, remend not bundled no event, 0 console errors, remend code-split (11.8 kB) and working
webpack 5 target: node over CJS output build error (with the naive fix) compiles; degrades gracefully at runtime
Node ESM remend remend
Node CJS fallbackRepair fallbackRepair, no uncaught rejection

The repro page now shows REPAIR: remend / OUTPUT: "a **bold**".

Tests

Four cases added to streamingMarkdownRepair.test.ts:

  • the default importer (no injection) resolving the real remend end-to-end
  • the CJS stub degrading to fallbackRepair
  • a guard that the specifier stays statically analyzable
  • the package.json per-format contract

I mutation-tested these by restoring the original buggy specifier — the guard goes red, so it catches the actual regression rather than just passing.

pnpm test:unit --project "x-chat*" → 940 passed / 33 skipped. Typecheck and lint clean.

Note for reviewers

remend has never actually run in a bundled browser app until now, so this also changes rendering output for partial inline markdown — a **bold mid-stream now completes to a **bold** instead of rendering raw. That is the intended behaviour of the feature, but it is a real visible change, not just a silenced event.

Follow-up tracked separately: remend being ESM-only means CJS/SSR consumers still get fallbackRepair. Vendoring it (or upstreaming a require condition) would remove the #remend branch entirely.

Changelog

Fixed the remend markdown-repair import so it resolves in bundled applications. It previously used a specifier no bundler could analyze, which left the repair inactive in every browser bundle and dispatched a global vite:preloadError on each assistant reply under Vite 8.

`remend` was imported through a variable specifier plus `@vite-ignore` /
`webpackIgnore` hints, which no bundler can statically analyze. Browser
bundles were left with a bare `import('remend')` that can never resolve
without an import map, so the richer repair was dead code in every bundled
app, and under Vite 8 (rolldown) the unresolvable import is wrapped in
`__vitePreload` — dispatching a global `vite:preloadError` on every
assistant reply.

Inlining the specifier alone is not enough: `remend` is ESM-only, so the
CJS build downlevels to `require('remend')` and webpack hard-fails with
`"." is not exported under the conditions [...require...]`. The specifier
has to differ per output format.

Use a `#remend` subpath import that resolves to the real package under the
`import` condition and to a local stub under `require`. The loader becomes
a plain, statically analyzable `import('#remend')`, so bundlers resolve and
code-split `remend` like any other dependency, while the CJS output never
names it and degrades to `fallbackRepair` as before.
Copilot AI review requested due to automatic review settings August 1, 2026 21:48
@hasdfa hasdfa added the scope: chat Changes related to the AI chat. label Aug 1, 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

This PR fixes @mui/x-chat’s lazy remend import so bundlers can statically resolve it, preventing unresolved import('remend') leftovers in browser bundles (and avoiding Vite 8 vite:preloadError on each assistant render) by switching to a #remend package subpath import that maps differently for ESM vs CJS.

Changes:

  • Replace the unanalyzable variable-based dynamic import with a static import('#remend') in streamingMarkdownRepair.ts.
  • Add a #remend entry to packages/x-chat/package.json#imports to resolve to remend for ESM and a local stub for other conditions.
  • Add regression tests and a remendUnavailable stub module to validate the specifier and per-format contract.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 1 comment.

File Description
packages/x-chat/src/internals/streamingMarkdownRepair.ts Switches lazy loader to import('#remend') so bundlers can resolve and bundle remend.
packages/x-chat/src/internals/streamingMarkdownRepair.test.ts Adds regression coverage for specifier analyzability and package.json import mapping (note: contains a path bug in packageRoot).
packages/x-chat/src/internals/remendUnavailable.ts Adds CJS/“non-import condition” stub target used by #remend to force graceful fallback behavior.
packages/x-chat/package.json Adds #remend subpath import mapping to support ESM real dependency and CJS stub resolution.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

import { describe, expect, it, vi, beforeEach } from 'vitest';
import { fallbackRepair, loadRemend, resetRemendCache } from './streamingMarkdownRepair';

const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../..');

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Thanks for looking, but I don't think this one holds — it's one .. level off.

The test file is at packages/x-chat/src/internals/streamingMarkdownRepair.test.ts, so path.dirname(fileURLToPath(import.meta.url)) is packages/x-chat/src/internals, not packages/x-chat/src. Resolving '../..' from there walks up twice — internalssrcx-chat — landing on the package root:

packageRoot        = /…/packages/x-chat
package.json       exists: true
src/internals/streamingMarkdownRepair.ts  exists: true

The reads resolve to packages/x-chat/package.json and packages/x-chat/src/internals/streamingMarkdownRepair.ts, both of which exist. It's also confirmed empirically: these tests pass locally and CI's test_unit job is green — a src/src/... path would have failed the suite outright.

Note the code has since moved to src/tests/packagingGuard/remendSpecifier.test.ts (to keep Node-only imports out of the browser project), where the same resolution is '../../..' for the extra directory level.

@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: de36528a4d

ℹ️ 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 +1 to +3
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';

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 Keep Node-only checks out of browser tests

The x-chat browser configuration includes this test file, so pnpm test:browser --project "x-chat" --run will load these top-level node:fs, node:path, and node:url imports in Chromium. Vite externalizes Node built-ins for browser compatibility, and the top-level path.resolve(...) or later fs.readFileSync(...) will fail the suite before these assertions can run. Move the filesystem-based regression checks into a separately excluded Node-only test file, as is already done for docsCorrectnessGuard, while leaving the runtime tests browser-compatible.

AGENTS.md reference: AGENTS.md:L40-L47

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Good catch — this was the cause of the test_browser / test_browser_react_18 failures, and you're right that the file never even got to the assertions: it failed to collect in Chromium (streamingMarkdownRepair.test.ts (0 test)), while the other 284 browser tests passed. I'd only run test:unit locally, which is why it slipped through.

Fixed in e12a218 exactly as suggested, following the docsCorrectnessGuard precedent:

  • moved the two filesystem-based checks to src/tests/packagingGuard/remendSpecifier.test.ts
  • added **/packagingGuard/** to the exclude list in vitest.config.browser.mts
  • left the runtime tests in streamingMarkdownRepair.test.ts

Keeping the runtime tests browser-compatible turned out to be worth more than just unblocking CI — they now assert in Chromium that #remend resolves and that remend actually loads, which is precisely the behaviour #23160 was about. That file went from 0 collected tests to 11.

Verified: browser 27 files / 295 tests passing, unit 940 passing / 33 skipped. I also re-ran the mutation check (restoring the old variable specifier) to confirm the guard still fails from its new location.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Correction to my reply above: I attributed the test_browser failures to this issue, and that was wrong. I inferred it from timing instead of reading the CI log. Pulling the log showed the original run had failed identically — all 27 x-chat suites, same setupVitest error — both before and after this fix. Your finding was a real defect (the file did fail to collect in Chromium locally) but it was not what CI was red on.

The actual cause was a side effect of the fix working. Now that import('#remend') genuinely resolves, the browser suite loads remend for real, and Vite's scanner can't see it up front behind a lazy subpath import:

[vite] (client) dependency optimized: remend
[vite] (client) optimized dependencies changed. reloading
[vitest] Vite unexpectedly reloaded a test.

The mid-run re-optimization reloads the page and drops every in-flight suite in the project. It passed locally only because my optimizer cache was warm — I reproduced it by clearing node_modules/.vite. Fixed in 862a26f by adding remend to optimizeDeps.include, as that warning recommends.

test_browser now gets through the whole x-chat project; the only remaining failure is an unrelated flaky DataGridPro data-source test (expected 11 to equal 12, retried 4x).

@code-infra-dashboard

code-infra-dashboard Bot commented Aug 1, 2026

Copy link
Copy Markdown

Deploy preview

https://deploy-preview-23263--material-ui-x.netlify.app/
QR code for https://deploy-preview-23263--material-ui-x.netlify.app/

Bundle size

Bundle Parsed size Gzip size
@mui/x-data-grid 0B(0.00%) 0B(0.00%)
@mui/x-data-grid-pro 0B(0.00%) 0B(0.00%)
@mui/x-data-grid-premium 0B(0.00%) 0B(0.00%)
@mui/x-charts 0B(0.00%) 0B(0.00%)
@mui/x-charts-pro 0B(0.00%) 0B(0.00%)
@mui/x-charts-premium 0B(0.00%) 0B(0.00%)
@mui/x-date-pickers 0B(0.00%) 0B(0.00%)
@mui/x-date-pickers-pro 0B(0.00%) 0B(0.00%)
@mui/x-tree-view 0B(0.00%) 0B(0.00%)
@mui/x-tree-view-pro 0B(0.00%) 0B(0.00%)
@mui/x-scheduler 0B(0.00%) 0B(0.00%)
@mui/x-scheduler-premium 0B(0.00%) 0B(0.00%)
@mui/x-chat ▼-3B(0.00%) ▼-2B(0.00%)
@mui/x-license 0B(0.00%) 0B(0.00%)

Details of bundle changes


Check out the code infra dashboard for more information about this PR.

@hasdfa hasdfa self-assigned this Aug 1, 2026
The two filesystem-based regression checks imported `node:fs`, `node:path`
and `node:url` at the top level of a file the browser project collects, so
`test:browser` failed to import the module before any assertion could run.

Move them to `src/tests/packagingGuard/remendSpecifier.test.ts` and exclude
that directory from the browser config, mirroring `docsCorrectnessGuard`.
The runtime tests stay in `streamingMarkdownRepair.test.ts`, where they are
worth more: they now also assert in Chromium that `#remend` resolves.
@hasdfa hasdfa added the type: bug It doesn't behave as expected. label Aug 1, 2026
Making the specifier statically analyzable also made it genuinely loadable,
so the browser suite now imports `remend` for real. Vite's dependency
scanner cannot see it up front — it sits behind a lazy `import('#remend')` —
so it was discovered mid-run, and the re-optimization reloaded the page and
dropped every in-flight suite in the project:

    [vite] dependency optimized: remend
    [vite] optimized dependencies changed. reloading
    [vitest] Vite unexpectedly reloaded a test.

Add it to `optimizeDeps.include`, as that warning recommends. Reproduced and
verified against a cold optimizer cache, which is why it only showed on CI.
@hasdfa

hasdfa commented Aug 2, 2026

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Can't wait for the next one!

Reviewed commit: 862a26f5f8

ℹ️ 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".

hasdfa added 3 commits August 2, 2026 11:04
mui#23160 was invisible to every in-process test: `loadRemend` resolved `remend`
fine under Node and the Vitest dev server, while the bundled output shipped to
users carried a bare `import('remend')` that could never resolve in a browser.
The upgrade was dead code everywhere it mattered and nothing failed.

Bundle the loader with Vite the way a consumer would, then run the bundle and
assert the markdown comes back repaired (`a **bold` -> `a **bold**`), plus that
the dynamic import points at an emitted chunk rather than a bare specifier.

Restoring the original variable specifier fails the structural assertion here
and the Chromium run of `streamingMarkdownRepair.test.ts` at runtime.
Leaving React and `@mui/*` external meant the emitted module still had to
resolve bare specifiers from a temp directory at import time, which worked
locally but not under CI's install layout:

    Cannot find package '@mui/x-chat-headless' imported from
    node_modules/.tmp-esm-bundle-test/bundle.mjs

Bundle everything instead — the hook and React tree-shake away, so the build
gets faster rather than slower — and assert the output has no bare specifiers
left, so the same fragility cannot come back unnoticed. The generated entry
moves out of the output directory so reading the build back never sees it.
The self-containment check scanned the bundle with a regex, which is unsound:
the bundle inlines third-party source, and a plain string literal in there
reads exactly like an import specifier. React 18 ships one — its element
validator builds a warning out of `... from " + getComponentNameFromType(...)`
— so `test_unit_react_18` reported it as a leftover external.

Use Rollup's own parsed records instead. They state the fact directly: the
entry chunk's `dynamicImports` is `['dist-<hash>.mjs']` with the fix and `[]`
with the original variable specifier, and no chunk imports anything that was
not emitted. Verified against both React 19 and a React 18 bundle.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

scope: chat Changes related to the AI chat. type: bug It doesn't behave as expected.

Projects

None yet

2 participants