Skip to content

Unexpected module resolution and tree-shaking from dependency package exports #758

@donmccurdy

Description

@donmccurdy

Reproduction link or steps

tsdown-treeshake-repro.zip

From the minimal reproduction repository, run yarn && yarn build.

What is expected?

The 'minimal reproduction' library has a single dependency, keyframe-resample, and its package.json is worth mentioning. The dependency uses package.json#exports with node and default entrypoints. Both entrypoints load a .wasm resource in an intended-to-treeshake way, and the dependency sets sideEffects: false. The minimal reproduction does not import anything that depends on the WASM resource, importing only a single plain-JS resampleDebug function.

When I build with tsdown, I'm intending for this resampleDebug function to be resolved from the 'default' export (not the 'node' export) and inlined into the build, without leaving any traces of platform-specific WASM resources. I use the following tsdown config:

export default defineConfig({
  entry: 'src/index.ts',
  inlineOnly: ['keyframe-resample'],
  target: 'esnext',
  platform: 'browser', // compare: 'node', 'neutral', 'browser'
  treeshake: true,  // workaround: { moduleSideEffects: false, propertyReadSideEffects: false }
});

What is actually happening?

Two things appear to be going wrong here:

  1. Despite platform: 'browser' in my config, tsdown is resolving keyframe-resample from node_modules/keyframe-resample/dist/keyframe-resample-node.modern.js for the CJS build, and from node_modules/keyframe-resample/dist/keyframe-resample-browser.modern.js for the ESM build. Referring to keyframe-resample's package.json linked above, I can't see why the Node.js build is being preferred over the browser-specific CJS or ESM builds.
  2. An unused line remains, in the CJS build output only:
    const wasm = /* @__PURE__ */ (0, node_fs_promises.readFile)(/* @__PURE__ */ new URL("./release.wasm", require("url").pathToFileURL(__filename).href));
    This line does break downstream code if left behind, so I'd like to make sure it tree-shakes.

I have a workaround, I think, which involves configuring:

{
  ...
  treeshake: { moduleSideEffects: false, propertyReadSideEffects: false }
}

With this, the WASM resource is tree-shaken. tsdown is still using the Node.js-specific entrypoint for the CJS build, which surprises me, but with the WASM resource gone it's not breaking anything in this case.

Any additional comments?

I'm the author of the keyframe-resample package, and was previously bundling a library that depended on it using microbundle/rollup. I don't think microbundle fully supports package.json#exports but somehow this was still working alright. Building platform-agnostic libraries depending on WASM dependencies has been tricky for me historically, it's certainly possible there's a better approach than what I've done in keyframe-resample.

The actual project affected by this possible bug, which the minimal reproduction mimics, is https://gltf-transform.dev/.

Metadata

Metadata

Assignees

No one assigned

    Type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions