Skip to content

ESM build is not loadable by Node: extensionless 'lodash/debounce' specifier in dist/index.mjs #1739

Description

@marblom007

Summary

dist/index.mjs emits an extensionless subpath specifier for lodash, which Node's ESM resolver rejects. The ESM build is therefore not loadable by Node directly.

Reproduce

npm run build
node --input-type=module -e "import('./dist/index.mjs')"
Error [ERR_MODULE_NOT_FOUND]: Cannot find module '<repo>/node_modules/lodash/debounce'
imported from <repo>/dist/index.mjs
Did you mean to import "lodash/debounce.js"?

Cause

src/custom/SearchBar.tsx:3:

import debounce from 'lodash/debounce';

lodash ships no exports map in its package.json, so under Node ESM a subpath import must carry the file extension (lodash/debounce.js). tsup keeps the specifier verbatim in the ESM output because lodash is external, so the unresolvable form reaches the published artifact.

Contrast @mui/icons-material/X, which is also extensionless in the output but resolves fine - that package does declare an exports map. So this is specific to lodash, not a general problem with the build config.

Impact

Low today, but real:

  • Bundler consumers are unaffected - webpack, vite, rollup and Next.js all apply Node-CJS-style resolution to bare specifiers, so Meshery UI and Kanvas do not see this.
  • Node ESM consumers are affected - anything doing import '@sistent/sistent' from a real ESM context (SSR, a script, a test runner in ESM mode, RSC server bundles) fails at load with a message naming lodash, not sistent.

It is the same shape of trap as #1735: the failure surfaces at import time, names a third-party package, and is invisible to CI because the repo always resolves through a bundler or jest's CJS transform.

Suggested fix

Either:

  1. import { debounce } from 'lodash'; - resolves under both module systems. Costs bundle size unless tree-shaking is reliable here.
  2. import debounce from 'lodash/debounce.js'; - smallest change, keeps the subpath, valid for both Node ESM and bundlers.
  3. Move lodash from external to bundled for the ESM output in tsup.config.ts.

(2) is the least invasive. Whichever is chosen, it is worth extending the guard added in #1737 (src/__testing__/optionalPeerDependencies.test.ts) or adding a sibling check that actually imports the built dist/index.mjs under Node ESM, so the build output is verified rather than only the source import graph. That would catch this class directly instead of by inspection.

Notes

Pre-existing on master, unrelated to the optional-peer work in #1732/#1737. Found while content-verifying the v0.21.40 tarball; filed separately rather than folded into an unrelated patch release.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions