web-infra-dev / web-infra-dev/rslib

[Bug]: preserveModules output breaks with sideEffects: false

Open
#1,839 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Rspack
Dominant language
TypeScript
Stars
1k
Forks
67
Avg merge
6h 9m
Merged PRs (30d)
57

Description

Version

System:
  OS: macOS 15.7.3 (arm64)
  Node: 24.11.1
  pnpm: 11.20.0

npmPackages:
  @rslib/core: 1.0.0-beta.2
  @rspack/core: 2.1.8
  picocolors: 1.1.1

The issue was first found with @rslib/core@1.0.0-beta.1 and is still reproducible with 1.0.0-beta.2.

Details

When an ESM library uses all of the following:

  • output.library.type: 'modern-module'
  • output.library.preserveModules
  • an extracted runtime chunk (Rslib enables runtimeChunk: { name: 'rslib-runtime' } for multi-entry ESM builds)
  • a bundled CommonJS dependency
  • "sideEffects": false (or a CSS-only sideEffects list) in the published package

Rslib/Rspack can emit a preserved source module that accesses the bundled dependency through __webpack_require__, while loading the dependency registration chunk only through a side-effect import.

A downstream bundler respects the package's sideEffects metadata and removes that import. The resulting bundle then fails at runtime because the module was never registered.

This is not a report that preserveModules must never emit shared/facade chunks. Extra chunks are expected. The correctness issue is that a required data dependency is represented only by an import that downstream tree shaking is allowed to remove.

Source module
import picocolors from 'picocolors';

export function createError(message) {
  return new Error(picocolors.gray(message));
}

picocolors is intentionally in devDependencies, so Rslib bundle mode includes it rather than auto-externalizing it.

Relevant Rslib configuration
export default defineConfig({
  lib: [
    {
      source: {
        // Multiple entries cause Rslib to extract rslib-runtime.
        entry: {
          index: './src/index.ts',
          other: './src/other.ts',
        },
      },
      dts: false,
      tools: {
        rspack: {
          output: {
            library: {
              type: 'modern-module',
              preserveModules: path.resolve('./src'),
            },
          },
        },
      },
    },
  ],
});

The package contains:

{
  "type": "module",
  "sideEffects": false,
  "devDependencies": {
    "@rslib/core": "1.0.0-beta.2",
    "picocolors": "1.1.1"
  }
}
Generated producer output

The preserved module is emitted as:

// dist/error.js
import { __webpack_require__ } from "./rslib-runtime~0.js";
import "./4538~0.js";

const picocolors = __webpack_require__(
  "../node_modules/picocolors/picocolors.js"
);

The shared chunk performs the required registration:

// dist/4538~0.js
import { __webpack_require__ } from "./rslib-runtime~0.js";

__webpack_require__.add({
  "../node_modules/picocolors/picocolors.js"(module) {
    // bundled picocolors implementation
  },
});

Directly importing dist/error.js in Node works because 4538~0.js is evaluated.

When a second Rspack build consumes dist/error.js, it removes import "./4538~0.js" based on the package's sideEffects metadata. The downstream bundle fails with:

TypeError: __webpack_modules__[moduleId] is not a function
    at __nested_rspack_require__(...)
Controls / workarounds verified
  • Disabling side-effects optimization in the downstream minimal reproduction keeps the registration chunk and succeeds.

  • Changing the package to "sideEffects": true succeeds.

  • Explicitly externalizing picocolors succeeds. With Rslib's existing externalsType: 'modern-module', the output becomes a value-bearing ESM dependency:

    import { picocolors } from "./shared.js";
    // shared.js
    export { default as picocolors } from "picocolors";
    
  • In a framework-independent Rspack reproduction, removing optimization.runtimeChunk also avoids this exact failure because the runtime and module registration are no longer separated.

Externalizing every dependency is only a partial workaround: any bundled CommonJS dependency emitted through the same registration pattern can trigger the problem.

Expected behavior

The output of modern-module + preserveModules should remain consumable by downstream bundlers when the package declares its source JavaScript side-effect-free.

Possible directions could be:

  1. avoid representing required bundled-module registration as a removable side-effect-only import;
  2. make the preserved module import a value/initialization binding from the shared chunk;
  3. adjust Rslib's runtime-chunk strategy for preserveModules; or
  4. document and automatically account for the required generated chunks in package sideEffects metadata.

Reproduce link

Real-world reproduction in Rspress:

https://github.com/web-infra-dev/rspress/tree/70b8a9d305dbb556a3f427b4b258b6ba65de9e9b

Relevant files:

Reproduce Steps

  1. Clone the Rspress commit above.
  2. Run pnpm install.
  3. Run pnpm --filter @rspress/core build.
  4. Inspect packages/core/dist/node/utils/error.js and the generated registration chunk.
  5. Create a downstream Rspack 2.1.8 entry that imports createError from that generated file, bundle it in production mode, and execute the result.
  6. Observe TypeError: __webpack_modules__[moduleId] is not a function.
  7. Repeat with package sideEffects: true or with picocolors externalized; the downstream bundle runs successfully.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with the Rspress reproduction and run pnpm install followed by pnpm --filter @rspress/core build. Inspect packages/core/rslib.config.ts, packages/core/package.json, packages/core/src/node/utils/error.ts, and the generated dist/node/utils/error.js plus registration chunk. Reproduce the downstream Rspack build and verify that the generated output remains functional with source JavaScript marked side-effect-free.

Written by the indexing model from the issue text.

Assessment

Tech stack
javascript, node.js, typescript
Domain
build-system, devtools, tooling
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Quiet
Clarity
Mostly clear
Newbie friendliness
45/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.