Description
When building a library with preserveModules: true, Rolldown silently drops re-exports of imported bindings from individual module outputs.
Reproduction
Given a source file src/react/GuiButton.ts:
import { GuiButton as GuiButtonElement } from '../components/button/gui-button.js';
import { createComponent } from '@lit/react';
import * as React from 'react';
const GuiButton = createComponent({
tagName: 'gui-button',
elementClass: GuiButtonElement,
react: React,
events: {},
displayName: 'GuiButton',
});
type GuiButtonProps = React.ComponentProps<typeof GuiButton>;
export { GuiButton, GuiButtonElement, type GuiButtonProps };
With this Vite/Rolldown config:
build: {
lib: {
entry: ['src/react/all.ts'],
formats: ['es'],
},
rolldownOptions: {
external: [/^react/, /^@lit/],
output: {
preserveModules: true,
},
},
}
Actual output (dist/react/GuiButton.js)
import { GuiButton as e } from "../components/button/gui-button.js";
import { createComponent as t } from "@lit/react";
import * as n from "react";
var r = t({
tagName: "gui-button",
elementClass: e,
react: n,
events: {},
displayName: "GuiButton"
});
export { r as GuiButton };
// ^ GuiButtonElement is missing!
Expected output
export { r as GuiButton, e as GuiButtonElement };
Analysis
Rolldown appears to recognize GuiButtonElement as a "pure re-export" of an already-external symbol (GuiButton from the component module) and strips it from the individual module output. The aggregated entry (all.js) does export GuiButtonElement but imports it directly from the original component module, bypassing the wrapper module entirely. This breaks consumers who import from the individual React wrapper path.
Notably, treeshake: false does not fix the issue, suggesting this is a re-export optimization separate from tree-shaking.
Workaround
Assign the imported binding to a local const before exporting. This prevents Rolldown from treating it as a pure re-export:
// Use a _-prefixed import alias to avoid naming conflict with the React component const
import { GuiButton as _GuiButtonElement } from '../components/button/gui-button.js';
const GuiButtonElement = _GuiButtonElement;
type GuiButtonElement = InstanceType<typeof _GuiButtonElement>; // restore type usage
// GuiButtonElement is now a local binding → Rolldown preserves the export
export { GuiButton, GuiButtonElement, type GuiButtonProps };
Environment
- Vite: 6.x (with Rolldown backend)
- Build mode: library with
preserveModules: true
Description
When building a library with
preserveModules: true, Rolldown silently drops re-exports of imported bindings from individual module outputs.Reproduction
Given a source file
src/react/GuiButton.ts:With this Vite/Rolldown config:
Actual output (
dist/react/GuiButton.js)Expected output
Analysis
Rolldown appears to recognize
GuiButtonElementas a "pure re-export" of an already-external symbol (GuiButtonfrom the component module) and strips it from the individual module output. The aggregated entry (all.js) does exportGuiButtonElementbut imports it directly from the original component module, bypassing the wrapper module entirely. This breaks consumers who import from the individual React wrapper path.Notably,
treeshake: falsedoes not fix the issue, suggesting this is a re-export optimization separate from tree-shaking.Workaround
Assign the imported binding to a local
constbefore exporting. This prevents Rolldown from treating it as a pure re-export:Environment
preserveModules: true