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 objn and function appendStylen):

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.

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