Skip to content

legacy: minified as ESM while the output is script (SystemJS) #23296

Description

@liruisen

Describe the bug

Vite 8 + @vitejs/plugin-legacy with the default build.minify (oxc): browsers that execute the legacy chunks get a white screen at first render when the bundle contains a block-scoped function declaration inside a nested scope (antd 6 / @ant-design/cssinjs parseStyle hits this on every page mount). Modern browsers are unaffected, so the breakage is silent for teams that don't test old browsers.

Three layers stack up:

  1. The oxc mangler renames a block-scoped function declaration and an outer binding to the same name ([minifier] mangler assigns the same name to a block-scoped function declaration and an outer binding — changes semantics under Annex B (sloppy mode) oxc-project/oxc#25896). Harmless under block scoping — which is why modern chunks work.
  2. Legacy chunks originate from ES modules (always strict-mode code) but the SystemJS output carries no "use strict" directive, so they run in sloppy mode.
  3. In sloppy mode, Annex B.3.3 hoists the block-level function declaration to a var-like function-scope binding, which clobbers the outer binding. The forEach callback then reads an undefined binding and throws TypeError.

Additionally the minifier strips "use strict" directives (oxc-project/oxc#25897), so the directive cannot be injected at the babel/renderChunk stage — it must be added after all transforms.

Reproduction

https://github.com/liruisen/rolldown-legacy-strict-repro

15 lines of plain JS, zero runtime dependencies. The built legacy chunk is byte-identical to the one from our real app (React 19 + antd 6).

Steps to reproduce

  1. pnpm install && pnpm build && pnpm preview
  2. Open the URL in a browser that executes the legacy chunks (e.g. Chrome 91, or any Electron 13 / Chromium 91 window)

Logs

Uncaught (in promise) TypeError: Cannot read property '.a' of undefined

Minified legacy chunk (note obj → n and function appendStyle → n):

n={".a":`red`},r=``,Object.keys(n).forEach(e=>{let t=n[e];if(typeof t!=`object`){function n(e,t){r+=e}n(e,t)}})

System Info

System: macOS
Node: v22
vite: 8.2.1
@vitejs/plugin-legacy: 8.2.3
rolldown: 1.2.3 / 1.2.4 (both affected)

Additional context

Workaround: build.minify: 'terser' — terser neither creates the collision nor strips directives.

Proposed fix (PR incoming): legacy chunks should keep the strict-mode semantics of the ES modules they derive from. Injecting "use strict"; as the first line of every legacy chunk in generateBundle (after the minifier ran) fixes the crash, restores semantic parity between modern and legacy output, and is independent of the upstream oxc fixes. The emitted sourcemaps only need an empty line prepended to their mappings, which is the exact equivalent of the one-line insertion.

Activity

  1. changed the title [-][plugin-legacy] legacy chunks run in sloppy mode — white screen on browsers that execute them (oxc minify name collision + Annex B hoisting)[/-] [+]legacy: minified as ESM while the output is script (SystemJS)[/+] on Aug 19, 2026
  2. added theissue type on Aug 19, 2026
  3. sapphi-red commented on Aug 19, 2026

    @sapphi-red
    Member

    This is caused by #22468. Rolldown runs Oxc Minifier with ESM mode as the output.format is set to 'es' (the legacy plugin converts that to SystemJS in renderChunk, which runs before the minification).

  4. liruisen commented on Aug 19, 2026

    @liruisen
    Author

    Thanks for the root-cause analysis. I dug into the rolldown side and can confirm the module switch is intentionally locked at three levels:

    • the napi binding exposes module?: boolean on MinifyOptions ("Use when minifying an ES module")
    • rolldown's JS types omit it: type MinifyOptions = Omit<BindingMinifyOptions, "module" | "sourcemap">
    • the runtime output-options validator rejects it outright: minify.module — Invalid key: Expected never (warning, silently ignored)

    So a vite-side fix (minifying the legacy output with script semantics) is currently blocked on rolldown allowing that override. I've opened rolldown/rolldown#10713 to discuss exposing it — happy to help on either side.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    p3-minor-bugAn edge case that only affects very specific usage (priority)plugin: legacy

    Type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions