Skip to content

preserveModules drops re-exports of imported bindings #9122

@Lookwe69

Description

@Lookwe69

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

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Priority

    None yet

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions