web-infra-dev / web-infra-dev/rslib
[Bug]: preserveModules output breaks with sideEffects: false
Nobody has claimed this yet.
- 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-onlysideEffectslist) 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": truesucceeds. -
Explicitly externalizing
picocolorssucceeds. With Rslib's existingexternalsType: '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.runtimeChunkalso 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:
- avoid representing required bundled-module registration as a removable side-effect-only import;
- make the preserved module import a value/initialization binding from the shared chunk;
- adjust Rslib's runtime-chunk strategy for
preserveModules; or - document and automatically account for the required generated chunks in package
sideEffectsmetadata.
Reproduce link
Real-world reproduction in Rspress:
https://github.com/web-infra-dev/rspress/tree/70b8a9d305dbb556a3f427b4b258b6ba65de9e9b
Relevant files:
- https://github.com/web-infra-dev/rspress/blob/70b8a9d305dbb556a3f427b4b258b6ba65de9e9b/packages/core/rslib.config.ts
- https://github.com/web-infra-dev/rspress/blob/70b8a9d305dbb556a3f427b4b258b6ba65de9e9b/packages/core/package.json
- https://github.com/web-infra-dev/rspress/blob/70b8a9d305dbb556a3f427b4b258b6ba65de9e9b/packages/core/src/node/utils/error.ts
Reproduce Steps
- Clone the Rspress commit above.
- Run
pnpm install. - Run
pnpm --filter @rspress/core build. - Inspect
packages/core/dist/node/utils/error.jsand the generated registration chunk. - Create a downstream Rspack 2.1.8 entry that imports
createErrorfrom that generated file, bundle it in production mode, and execute the result. - Observe
TypeError: __webpack_modules__[moduleId] is not a function. - Repeat with package
sideEffects: trueor withpicocolorsexternalized; the downstream bundle runs successfully.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- 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