Uh oh!
There was an error while loading. Please reload this page.
fix(types): find an aliased install through the host importer's ESM-only fallback - #15047
Conversation
…nly fallback `createHostImporter`'s #14041 fallback finder verifies the one directory it consults — `<hostRoot>/node_modules/<key>` — by matching that directory's manifest `name` against the declared package name. An aliased install fails that check by construction: `{ "dependencies": { "foo": "npm:bar@1" } }` puts a manifest named `bar` at `node_modules/foo`, so the finder answered `absent` and an ESM-only aliased package kept the pre-#14041 INSTALL wording — a confidently-wrong remedy against an install that is already correct. The declaration is now parsed for the name it promises. `npm:bar@1`, `npm:@acme/x@^2` and the aliased `workspace:bar@*` name the package installed under the key; that is the manifest name the finder expects there. The manifest-name check itself is NOT loosened — it is what keeps the fallback strictly tighter than the CJS resolution it backs up (#4719 from the fallback side). What moves is the EXPECTATION, still authored by the host and still read out of the host's own package.json. A value that is not a bare package name — a `workspace:` range, an alias carrying a subpath — yields no expectation to move to, so the key stays and today's refusal is kept; `link:` and `file:` name a LOCATION rather than a package, so no name is derivable from them at all. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016yfqQh2dBgPAymYd7xipza
📓 Docs Drift Check5 anchor(s) derived from 1 changed package(s); no hand-written page names any of them, so this run has nothing to list — not a clean bill of health. This check sees only pages that NAME a derived anchor: one that documents this change in prose, or enumerates it in an authoring dialect, names none and stays invisible to it on every run. What this run could not see
Coarse fallback — 1 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 05a07e1ea1fae8bd8a6cfd7856f527decd4a81d9 && git checkout 05a07e1ea1fae8bd8a6cfd7856f527decd4a81d9
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin d5b330dc6916b7ca56f4c751cc654c7aaf64bfe2 7bf2ed4080f03ca0d02456bdb86b80f409613122 && git checkout -B drift-repro d5b330dc6916b7ca56f4c751cc654c7aaf64bfe2 && git merge --no-ff 7bf2ed4080f03ca0d02456bdb86b80f409613122
node scripts/docs-audit/affected-docs.mjs --json d5b330dc6916b7ca56f4c751cc654c7aaf64bfe2 |
Uh oh!
There was an error while loading. Please reload this page.
Fixes#14278
The defect
createHostImporter's #14041 fallback finder verifies the one directory it consults —hostRoot/node_modules/KEY— by matching that directory's manifestnameagainst the declared package name. An aliased install fails that check by construction:{ "dependencies": { "foo": "npm:bar@1" } }hostRoot/node_modules/fooholds a manifest namedbar, notfoo. The check rejected it, the fallback answeredabsent, and an ESM-only aliased package therefore kept the pre-#14041 INSTALL wording — the confidently-wrong remedy that sends an operator to runpnpm installagainst an install that is already correct, on a declaration shapepackageNameFromSpecifier's own documentation blesses.The fix: parse the DECLARATION, never widen the check
⛔ The manifest-name check is not loosened. It is what keeps the fallback strictly tighter than the CJS resolution it backs up — the property #14041 rests on and #4719 guards — and widening it would trade a confidently-wrong remedy for a wrong LOAD, the worse direction.
What moves is the expectation, and it is still authored by the host and still read out of the host's own
package.json. A newdeclaredManifestName(declaration)reads the declaration value for the name it promises:^1.0.0,*, a tagnpm:bar@1,npm:@acme/x@^2,npm:barbar/@acme/x/barnpm:values are alwaysname[@range]workspace:bar@*barworkspace:*,workspace:^1.2.3link:../bar,file:../bar.tgz, a git URLThe last three rows are the conservative half: where no name is derivable, the pre-#14278 refusal stands rather than being guessed at.
packageNameFromSpecifieris the single authority on what counts as a package name, so the parse cannot drift from the declaration gate's own idea of one.declaredCjsResolveFallbackandhostInstalledPackageDirnow take theHostDeclarationitself instead of a(packageName, hostRoot)pair, so the key, the root and the expected name cannot be passed out of step with each other.Proof — red-first, and which arm fired
The tests were written and run before the implementation, on unmodified
packages/types/src/node.ts:Six of the seven were the new alias-RESCUE arms, and only those:
with the red text on the card's own case being exactly the defect:
The seventh red was not this change:
the DEFAULT fallback is this package's own resolution — @objectstack/spec and nothing elsefailed withFailed to resolve entry for package "@objectstack/spec"— an unbuilt dependency closure in a fresh worktree. It goes green oncepnpm --filter '@objectstack/types^...' buildhas run, and is green in the run below.⭐ Every tightness control stayed green in the red-first run — the failure the card must not produce (an aliased red that also reddens a non-aliased control) did not occur:
TIGHTNESS: a NON-aliased declaration is unchanged — the key is still the expected nameTIGHTNESS: an alias naming one package does not license a directory holding anotherTIGHTNESS: an alias target carrying a SUBPATH is not a package name, and is refusedTIGHTNESS: an alias does not reopen the hostRoot boundaryBOUNDARY: a link: target whose manifest names something else keeps the refusalthe fallback trusts only a directory whose manifest NAMES the declared package, plus bothTIGHTNESScases of the createHostImporter's declared leg cannot load an ESM-only host package at all, and misreports it as a broken install #14041 block (NODE_PATH, above-hostRoot) and both traversal-span cases.After the fix, with the dependency closure built:
Scope
Expected file face, and nothing wider:
packages/types/src/node.tsplus its test, plus the changeset.declared-no-loadable-entryremedy formatters; this diff touches zero lines ofnoLoadableEntryMessage,unresolvableMessageandundeclaredMessage(git diff origin/main -- packages/types/src/node.ts | grep -c 'noLoadableEntryMessage\|unresolvableMessage\|undeclaredMessage'⇒0). The aliased cases here assert those messages, they do not edit them.Changeset — the fork, measured:
packages/types' ownfilesfield is["dist", "README.md", "CHANGELOG.md"], sosrc/node.tsships (asdist/node.*) and the error kind an aliased install answers with is published behaviour. ⇒ a changeset is owed, and.changeset/aliased-install-host-importer.mdis it (patch).Out-of-scope findings, filed unassigned, not fixed here
packageRootOfmatches the declaration KEY, so an aliased dual-published package silently loads itsrequirebuild on the succeeding (#13330) path #15044 —packageRootOfmatches the declaration KEY too, so an aliased dual-published package silently loads itsrequirebuild on the succeeding (cli: serve's cluster-driver load registers into the CJS registry while the ESM Runtime reads the ESM one — OS_CLUSTER_DRIVER=redis silently downgrades to "not registered" (post-#10645) #13330) path. Not folded in: that path SUCCEEDS today, so correcting it changes a working load's selected entry — a decision, not a mechanical correction, and outside this card's ruled route.link:/file:install whose manifest name differs from the key still keeps the wrong INSTALL wording — the location-carrying sub-case #14278 could not reach #15045 — alink:/file:/ git-URL install whose manifest name differs from the key keeps the INSTALL wording. The ruled route cannot reach it (those specifiers carry no package name); the boundary is pinned by a test in this PR rather than left unnoticed.Verification
Run at final HEAD
7bf2ed4080:pnpm --filter '@objectstack/types^...' build— dependency closure (34/34 declaration files emitted for@objectstack/spec)pnpm --filter @objectstack/types exec vitest run --maxWorkers=2 src/node.test.ts— 58/58pnpm --filter @objectstack/types typecheck—tsc --noEmit, clean.tsc --listFilesOnlyconfirms the program really containssrc/node.tsandsrc/node.test.ts(1 hit each), so the green covers the edited test file rather than merely the sources.node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstackat this HEAD (never a hand-written list): 40 families, harvested with--commandsso no spelling or section is dropped. All 40 were run locally, each exit code captured into a file before any pipe: 36 exit 0, 4 exit 3.NOT MEASURED — the 4 exit-3s (PREREQUISITE NOT MET; neither a pass nor a red):
check:dual-build-cjs-loadsandcheck:type-check-debtboth need the whole workspace built (85 packages have nodist/in this worktree),scripts/check-test-completeness.mjsexited before parsing a single summary line, andscripts/pm/check-half-states.mjsneeds the GitHub API census this seat cannot reach. All four are CI's to run; none of them is reported here as green.ESLint — the full repo run, not a narrowing:
pnpm lint(eslint . --no-inline-config, the whole tree, the same command CI'sLint & Repo Gatesjob runs) completed here with exit code 0 and no findings, so nothing about the lint result is scoped down or deferred.🤖 Generated with Claude Code
https://claude.ai/code/session_016yfqQh2dBgPAymYd7xipza
Generated by Claude Code