Skip to content

createHostImporter's declared leg cannot load an ESM-only host package at all, and misreports it as a broken install #14041

Description

@os-steve

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions