Found while implementing #13330 (same seam, different half). Filed unassigned, not fixed there — the remedy needs a different mechanism with its own blast radius.
Symptom
A host app that DECLARES a package, has it correctly installed, and whose package publishes only an import condition cannot be loaded through createHostImporter's declared leg. It fails as MODULE_NOT_FOUND, worded as an install problem:
Cannot find module '@fixture/esm-only': the host app DECLARES it (dependencies: "1")
but it could not be resolved.
...
This is an INSTALL problem, not a declaration problem -- the declaration is
already there, so re-reading the package.json will not help. Check:
- dependencies never installed ... run `pnpm install` in HOST_ROOT
The install is fine. Nothing the message tells the operator to do will help.
Measured
Node v22.22.2, against packages/types/src/node.ts on the #13330 branch (the #13330 fix does not change this leg's finder, so main behaves identically):
fixture package.json: { "type": "module", "exports": { ".": { "import": "./dist/index.js" } } }
host package.json: { "dependencies": { "@fixture/esm-only": "1" } }
outcome: THREW MODULE_NOT_FOUND
message first line: Cannot find module '@fixture/esm-only': the host app DECLARES it
(dependencies: "1") but it could not be resolved.
Cause
The declared leg finds the package with hostRequire.resolve(pkg) — a CommonJS resolution. With an exports map naming no require condition, Node throws ERR_PACKAGE_PATH_NOT_EXPORTED, and the leg classifies every resolver throw as declared-unresolvable. The two cases are genuinely different and currently indistinguishable:
- the app declared it and the install is broken (the message is right), versus
- the app declared it, the install is fine, and the package simply publishes no CommonJS entry (the message is wrong, and there is no remedy an operator can apply).
Why #13330 did not fix it
#13330 re-decides the exports CONDITION for a package the CJS resolver already located; it deliberately leaves the FINDER alone, because hostRequire.resolve is also the declared-unresolvable classifier and the #4719 declaration gate's companion. Rescuing this case needs a different finder — a node_modules walk from hostRoot, which is strictly tighter than CJS resolution (it does not honour NODE_PATH, so it cannot reopen the #4719 hole) but is a new mechanism with its own tests and its own failure modes. That belongs on its own card rather than riding along with a p1.
Population
Pure-ESM publishing is common outside this workspace and is where this seam points: every package importFromHost loads is app-supplied. No workspace package is affected today — every dual build here publishes both conditions (pnpm check:dual-build-cjs-loads measures 102 require entry points across 66 packages, all loading). So this is latent for the framework and live for any customer or cloud-private package that ships ESM-only.
Suggested shape
Fall back to a node_modules walk from hostRoot only when hostRequire.resolve throws — strictly additive, since that path is a hard failure today — and split the failure kind so an ESM-only package gets a message about its own shape rather than about the install.
Generated by Claude Code
Found while implementing #13330 (same seam, different half). Filed unassigned, not fixed there — the remedy needs a different mechanism with its own blast radius.
Symptom
A host app that DECLARES a package, has it correctly installed, and whose package publishes only an
importcondition cannot be loaded throughcreateHostImporter's declared leg. It fails asMODULE_NOT_FOUND, worded as an install problem:The install is fine. Nothing the message tells the operator to do will help.
Measured
Node v22.22.2, against
packages/types/src/node.tson the #13330 branch (the #13330 fix does not change this leg's finder, somainbehaves identically):Cause
The declared leg finds the package with
hostRequire.resolve(pkg)— a CommonJS resolution. With anexportsmap naming norequirecondition, Node throwsERR_PACKAGE_PATH_NOT_EXPORTED, and the leg classifies every resolver throw asdeclared-unresolvable. The two cases are genuinely different and currently indistinguishable:Why #13330 did not fix it
#13330 re-decides the exports CONDITION for a package the CJS resolver already located; it deliberately leaves the FINDER alone, because
hostRequire.resolveis also thedeclared-unresolvableclassifier and the #4719 declaration gate's companion. Rescuing this case needs a different finder — anode_moduleswalk fromhostRoot, which is strictly tighter than CJS resolution (it does not honourNODE_PATH, so it cannot reopen the #4719 hole) but is a new mechanism with its own tests and its own failure modes. That belongs on its own card rather than riding along with a p1.Population
Pure-ESM publishing is common outside this workspace and is where this seam points: every package
importFromHostloads is app-supplied. No workspace package is affected today — every dual build here publishes both conditions (pnpm check:dual-build-cjs-loadsmeasures 102 require entry points across 66 packages, all loading). So this is latent for the framework and live for any customer or cloud-private package that ships ESM-only.Suggested shape
Fall back to a
node_moduleswalk fromhostRootonly whenhostRequire.resolvethrows — strictly additive, since that path is a hard failure today — and split the failure kind so an ESM-only package gets a message about its own shape rather than about the install.Generated by Claude Code