es build is not loadable by Node's ESM resolver ‚every entry point throws ERR_MODULE_NOT_FOUND
Summary
In @rc-component/picker@1.12.0 the exports map serves the es build to the import condition for every
subpath, but the es build is bundler-only output:
- its relative specifiers are extensionless (
import { commonLocale } from "./common";), and
- there is no
"type": "module" marker ‚neither at the package root nor as a nested es/package.json.
Consequence: any consumer that reaches the package through the import condition ‚ i.e. any "type": "module"
package, or anything running under plain Node ESM ‚I cannot load it at all. require() (the lib build) works fine;
bundlers work fine; Node ESM is completely broken.
Reproduction
mkdir rc-repro && cd rc-repro
npm init -y
npm pkg set type=module
npm i @rc-component/picker@1.12.0 react@19 react-dom@19 moment
node --input-type=module --eval 'import "@rc-component/picker"'
node --input-type=module --eval 'import "@rc-component/picker/locale/en_US"'
Both fail (Node v24.12.0, npm-installed, unpatched):
Error [ERR_MODULE_NOT_FOUND]: Cannot find module '.../@rc-component/picker/es/PickerInput/RangePicker'
imported from .../@rc-component/picker/es/index.js
code: 'ERR_MODULE_NOT_FOUND'
Error [ERR_MODULE_NOT_FOUND]: Cannot find module '.../@rc-component/picker/es/locale/common'
imported from .../@rc-component/picker/es/locale/en_US.js
code: 'ERR_MODULE_NOT_FOUND'
The CJS path is healthy, which shows the problem is isolated to the es build:
node --input-type=commonjs --eval 'console.log(require("@rc-component/picker/locale/en_US").default.locale)'
# -> en_US
Both missing files exist on disk as es/PickerInput/RangePicker.js and es/locale/common.js ‚only the file
extension is missing from the specifier. Counted in 1.12.0:
- 391 extensionless relative specifiers across
es/**/*.js
- 158 extensionless relative type specifiers across
es/**/*.d.ts (from '../interface', from '.', …),
which moduleResolution: "node16"/"nodenext"/"bundler"-style ESM type resolution also cannot follow
Note on Node versions: on Node ‚â• 22.7 the ESM syntax in es/*.js is auto-detected, so the files are parsed as ESM
and fail on the extensionless specifier as above. On older Node (no module-syntax detection) the same files are
parsed as CJS, because the nearest package.json has no "type" field, and fail with
SyntaxError: Cannot use import statement outside a module. Broken either way, just with a different message.
The breakage also looks inconsistent, which makes it hard to diagnose: @rc-component/picker/generate/moment
does load under Node ESM, because es/generate/moment.js happens to import only bare specifiers
(moment, @rc-component/util) and no relative paths.
Second, related problem: the default export needs runtime unwrapping
Because the es build uses export default while the package is not marked as ESM, the shape a consumer receives
depends on which build the resolver picked, so no single import form is portable:
// lib (CJS) build via ESM import ‚module.exports is the namespace
import m from ".../lib/locale/en_US.js"; // m = { __esModule: true, default: locale } -> m.locale is undefined
import * as ns from ".../lib/locale/en_US.js"; // Object.keys(ns) = ['__esModule','default','module.exports']
// the locale object is at ns.default.default
// es build inlined by a bundler ‚the locale object is at ns.default
TypeScript agrees with the pessimistic reading: since the package has no "type": "module", es/locale/en_US.d.ts
is treated as CJS under nodenext, so export default locale types as a default property rather than a module
default. Consumers end up writing this at every rc-picker import site:
import * as momentGenerateConfigModule from "@rc-component/picker/generate/moment";
import { defaultImport } from "default-import";
const momentGenerateConfig = defaultImport(
momentGenerateConfigModule as unknown as { default: MomentGenerateConfig },
);
…i.e. a cast plus a runtime interop helper (default-import) purely to survive the two shapes the same module can
arrive in (real ESM namespace when a bundler inlines it, CJS interop when a test runner or Node externalizes it).
What we tried, and why it doesn't work
Patching the exports map locally:
"./locale/*": {
"types": "./es/locale/*.d.ts",
"module": "./es/locale/*.js",
"import": "./lib/locale/*.js",
"require": "./lib/locale/*.js"
}
"module" is inert for this purpose: Node's resolver only understands node, import, require, default
(and node-addons) ‚"module" is a bundler-only convention, so it changes nothing for Node.
- Pointing
import at lib/ does make Node load the module, but it swaps the ESM namespace for CJS interop, which
is exactly the double-wrapped default described above ‚so consumer code that worked under a bundler now reads
the wrong shape.
- And it would have to be applied to every subpath (
., ./generate/*, ./interface, …), not just ./locale/*.
Impact
A "type": "module" library that statically imports this package becomes un-importable under plain Node ‚the
failure is not local to the picker component, it propagates to the whole barrel/entry point that transitively
reaches it. That breaks SSR, node -e 'import "<pkg>"' package smoke tests, and test runners that externalize
node_modules instead of bundling them.
Our current workaround is to keep every rc-picker import behind a dynamic import() (React lazy) so it never
appears in the Node-reachable static graph, plus the defaultImport casts above.
Suggested fix (any one of these solves the primary issue)
- Emit extensions in the
es build ‚from "./common.js" ‚and add es/package.json containing
{ "type": "module" } (or emit .mjs). Babel: babel-plugin-add-import-extension; tsc:
rewriteRelativeImportExtensions / write specifiers with .js. Apply the same to the relative type specifiers
in es/**/*.d.ts.
- Or point the
import condition at a genuinely Node-resolvable ESM build and keep es behind the bundler-only
"module" condition.
- Also worth adding
"type": "commonjs" explicitly at the root so lib/ is unambiguous and Node's syntax
detection never has to guess.
Fixing (1) additionally removes the need for default-import-style unwrapping, because a real ESM module's default
export resolves identically in Node and in bundlers.
Environment
@rc-component/picker 1.12.0 (unmodified, fresh npm i)
- Node v24.12.0, npm 11.x, macOS (darwin 24.6.0)
- Consumer package:
"type": "module", TypeScript moduleResolution: nodenext, React 19, moment 2.30.1
esbuild is not loadable by Node's ESM resolver ‚every entry point throwsERR_MODULE_NOT_FOUNDSummary
In
@rc-component/picker@1.12.0theexportsmap serves theesbuild to theimportcondition for everysubpath, but the
esbuild is bundler-only output:import { commonLocale } from "./common";), and"type": "module"marker ‚neither at the package root nor as a nestedes/package.json.Consequence: any consumer that reaches the package through the
importcondition ‚ i.e. any"type": "module"package, or anything running under plain Node ESM ‚I cannot load it at all.
require()(thelibbuild) works fine;bundlers work fine; Node ESM is completely broken.
Reproduction
Both fail (Node v24.12.0, npm-installed, unpatched):
The CJS path is healthy, which shows the problem is isolated to the
esbuild:Both missing files exist on disk as
es/PickerInput/RangePicker.jsandes/locale/common.js‚only the fileextension is missing from the specifier. Counted in 1.12.0:
es/**/*.jses/**/*.d.ts(from '../interface',from '.', …),which
moduleResolution: "node16"/"nodenext"/"bundler"-style ESM type resolution also cannot followNote on Node versions: on Node ‚â• 22.7 the ESM syntax in
es/*.jsis auto-detected, so the files are parsed as ESMand fail on the extensionless specifier as above. On older Node (no module-syntax detection) the same files are
parsed as CJS, because the nearest
package.jsonhas no"type"field, and fail withSyntaxError: Cannot use import statement outside a module. Broken either way, just with a different message.The breakage also looks inconsistent, which makes it hard to diagnose:
@rc-component/picker/generate/momentdoes load under Node ESM, because
es/generate/moment.jshappens to import only bare specifiers(
moment,@rc-component/util) and no relative paths.Second, related problem: the default export needs runtime unwrapping
Because the
esbuild usesexport defaultwhile the package is not marked as ESM, the shape a consumer receivesdepends on which build the resolver picked, so no single import form is portable:
TypeScript agrees with the pessimistic reading: since the package has no
"type": "module",es/locale/en_US.d.tsis treated as CJS under
nodenext, soexport default localetypes as adefaultproperty rather than a moduledefault. Consumers end up writing this at every rc-picker import site:
…i.e. a cast plus a runtime interop helper (
default-import) purely to survive the two shapes the same module canarrive in (real ESM namespace when a bundler inlines it, CJS interop when a test runner or Node externalizes it).
What we tried, and why it doesn't work
Patching the
exportsmap locally:"module"is inert for this purpose: Node's resolver only understandsnode,import,require,default(and
node-addons) ‚"module"is a bundler-only convention, so it changes nothing for Node.importatlib/does make Node load the module, but it swaps the ESM namespace for CJS interop, whichis exactly the double-wrapped
defaultdescribed above ‚so consumer code that worked under a bundler now readsthe wrong shape.
.,./generate/*,./interface, …), not just./locale/*.Impact
A
"type": "module"library that statically imports this package becomes un-importable under plain Node ‚thefailure is not local to the picker component, it propagates to the whole barrel/entry point that transitively
reaches it. That breaks SSR,
node -e 'import "<pkg>"'package smoke tests, and test runners that externalizenode_modulesinstead of bundling them.Our current workaround is to keep every rc-picker import behind a dynamic
import()(Reactlazy) so it neverappears in the Node-reachable static graph, plus the
defaultImportcasts above.Suggested fix (any one of these solves the primary issue)
esbuild ‚from "./common.js"‚and addes/package.jsoncontaining{ "type": "module" }(or emit.mjs). Babel:babel-plugin-add-import-extension; tsc:rewriteRelativeImportExtensions/ write specifiers with.js. Apply the same to the relative type specifiersin
es/**/*.d.ts.importcondition at a genuinely Node-resolvable ESM build and keepesbehind the bundler-only"module"condition."type": "commonjs"explicitly at the root solib/is unambiguous and Node's syntaxdetection never has to guess.
Fixing (1) additionally removes the need for
default-import-style unwrapping, because a real ESM module's defaultexport resolves identically in Node and in bundlers.
Environment
@rc-component/picker1.12.0 (unmodified, freshnpm i)"type": "module", TypeScriptmoduleResolution: nodenext, React 19, moment 2.30.1