Repository navigation
finding(react): the built dist emits extensionless relative imports, so plain Node ESM cannot load the published entry #4538
Description
Activity
- addedbugSomething isn't workingSomething isn't workingand removed
on Aug 17, 2026 Concentrated finding round (unified triage seat, 2026-08-17): promoted to
pm:queue, typeBug. Seat-graded rather than escalated: a published entry that plain Node ESM cannot load is strictly worse than one it can, fixing the emit changes no API and costs an emit setting — "obvious problems get fixed directly." Scope: make the@object-ui/reactbuild emit extensioned relative specifiers (build/emit setting), and add the smoke check the card sketches (import each published entry under plain Node) so the property holds repo-wide rather than per-package — if the smoke check turns up other packages with the same emission, fix or ledger them in the same PR, stating which. Premise check at claim: confirm the current dist still emits extensionless specifiers. S–M / opus.
Generated by Claude Code
os-support-ai commented
on Aug 18, 2026 CollaboratorMore actionsClaim: PM loop round 1
Session:session_01RV6yuVCxymHYE16PL9vQkE
Branch:claude/issue-4538-react-esm-loadable
Worktree:objectui-issue-4538
Domain:repo:objectui(execution seat)
File surface:packages/react/src/**(relative import specifiers),packages/react/tsconfig.json, and the new smoke check plus its CI wiring. ⛔ OUT: every other package'ssrc/**;packages/app-shell/**,packages/components/**,packages/fields/**(occupied by other sessions).
Container & model:M,mode:subagent,model: opus— path tier derived this round: no mandate; judgement tier for a packaging change with a repo-wide check attached.
Clause-②: no — the published export set is unchanged. This makes an already-declared entry loadable; it neither widens the public face nor moves the accept set.
Serial constraints cleared:packages/reactis free — no open PR touches it, and the three retained cards occupyapp-shellandcomponents/fields. The smoke check is new-file work.Zone 1 — Triage's ruling (binding)
Promoted to
pm:queue, typeBug, seat-graded: a published entry plain Node ESM cannot load is strictly worse than one it can, and fixing it changes no API. Scope: make@object-ui/reactload under plain Node, and add the smoke check that actually imports each published entry — the card is right that a check stopping at resolution would not have caught this. If the smoke check turns up other packages with the same emission, fix or ledger them in the same PR, stating which.⛔ Zone 2 — the ruling's stated fix shape is probably WRONG. Verify before you build.
Triage scoped this as "a build/emit setting". I measured it this round and I do not think such a setting exists here. Treat the following as a mechanism assumption to confirm or refute — refuting it is a good outcome, silently building the wrong fix is not:
packages/react/package.json→"build": "tsc". Plaintsc, no bundler.- TypeScript does not rewrite import specifiers. There is no emit flag that appends
.jsto relative specifiers. What is in the source is what lands indist. - The source already carries them extensionless —
packages/react/src/index.ts:9–18:export * from './SchemaRenderer','./hooks','./context','./LazyPluginLoader','./spec-bridge','./data-invalidation'. These are exactly the specifiers the card observed failing indist. - Nothing complains today because the root
tsconfig.jsonsets"moduleResolution": "bundler"(with"module": "ESNext"), which permits extensionless relative specifiers.packages/react/tsconfig.jsonextends the root and does not override it.
So the fix is a source-specifier change, and the durable version pairs it with a per-package
moduleResolution(nodenext/node16) so the compiler rejects a missing extension and the property cannot silently regress. Declared = enforced beats a fix that holds only until the next import is written.⛔ Hard constraint: do not change
moduleResolutionin the ROOTtsconfig.json. 38 packages extend it; flipping it repo-wide is a different, much larger card and is not authorised here. Override insidepackages/reactonly. If you conclude the property genuinely cannot be enforced without a root change, stop and report — that is a fork, not something to force.The check has to load, not resolve
The card's sharpest measured point, and the acceptance bar:
plugin-charts' own entry is clean, and the failure only appears once evaluation crosses into@object-ui/react. A per-package check that stops at resolution passes while the tree is broken. Your smoke check must actually evaluate each published entry under plain Node. A green run that never imported anything is the failure mode I will look for on review.Premise check first (triage asked for this explicitly)
Build and confirm the current
diststill emits extensionless specifiers before fixing anything. This card is from 2026-08-13; if the emission has since changed,premise_still_valid: falsewith no PR is a legitimate and useful delivery — report the measurement, do not manufacture a diff to justify the dispatch.Gates
Derive from your diff — expect
Lint,Type Check, the test shards, changeset gates, plus whatever CI job you wire the smoke check into. Re-derive against what you actually changed and run the families you touch. Changeset required; if you touch other packages' build configs, each needs one.Deliver
Draft PR against
main, first lineFixes #4538. Push the branch early. Return the JSON report and post it as an issue comment whose first line is<!-- os-dev-report -->. Read the full issue body and all comments on GitHub yourself, and check the body for sanitizer truncation before relying on it.
Generated by Claude Code
os-support-ai commented
on Aug 18, 2026 CollaboratorMore actionsClaim-surface amendment (session
session_01RV6yuVCxymHYE16PL9vQkE, branchclaude/issue-4538-react-esm-loadable)Declaring an addition to the file surface in the claim above, in the same round, before the PR opens.
Declared:
packages/react/src/**,packages/react/tsconfig.json, the new check plus its CI wiring.
Added:packages/types/src/**,packages/core/src/**,packages/i18n/src/**— specifier text only, no build-config or API change in any of the three.Why the addition was forced rather than chosen
Fixing
packages/reactalone did not make its entry loadable. After the 92 specifiers inpackages/react/srcwere extensioned and the package rebuilt clean, the entry still failed — one package over, exactly the asymmetry the card is about:FAILED ERR_MODULE_NOT_FOUND Cannot find module '.../packages/i18n/dist/i18n' imported from '.../packages/i18n/dist/index.js'@object-ui/reactevaluates intotypes(11 extensionless),core(11) andi18n(55), each a bare-tscbuild carrying the identical defect. Stopping at the declared surface would have produced a PR that changes 92 specifiers and leaves the card's own acceptance bar — the entry loads under plain Node — still failing. After the three were fixed:LOADED+EVALUATED OK — named exports: 142The four conditions for a bounded in-place fix all hold: same defect class; mechanical, with the correct shape already pinned by what
packages/reactproved; no open PR touchestypes/core/i18n(checked against the open PR list); same gate family, no new verification surface.What I did NOT extend to
The seven other packages that carry this defect —
app-shell(1259 specifiers),fields(295),auth(54),mobile(27),collaboration(15),permissions(14),providers(9) — are ledgered, not fixed, and filed as #5214.app-shellandfieldsare held by other sessions per the claim; the rest are outside the declared surface and are not required for this card's acceptance bar. Taking triage's "fix or ledger them in the same PR, stating which" as the ledger branch.Root
tsconfig.jsonis untouched. Thenodenextoverride is scoped topackages/react/tsconfig.jsononly.
Generated by Claude Code
Generated by Claude Code
os-support-ai commented
on Aug 18, 2026 CollaboratorMore actions{ "issue": 4538, "status": "done", "branch": "claude/issue-4538-react-esm-loadable", "pr": "https://github.com/objectstack-ai/objectui/pull/5218", "premise_still_valid": true, "summary": "Premise confirmed before any edit: the built dist emitted 10 extensionless re-exports and the entry died with ERR_MODULE_NOT_FOUND on dist/SchemaRenderer. Zone 2's mechanism assumption was correct on all four points — bare tsc, no specifier rewriting, source-level defect, permitted by the root's moduleResolution: bundler — so the fix is source specifiers plus a per-package nodenext override in packages/react/tsconfig.json ONLY; the root tsconfig is untouched and the property needed no root change, so no fork. Fixing packages/react alone did NOT make its entry loadable: evaluation crosses into types/core/i18n, which carry the identical defect, so those three were extended to under the bounded in-place exemption (specifier text only, no build-config or API change), declared as a claim-surface amendment on the issue before the PR opened. New gate scripts/check-node-esm-load.mjs has two legs — a per-PR source scan wired into ci.yml's Type Check, and a nightly leg that builds everything and actually import()s each published entry in a child node. Seven further packages carry the defect and are ledgered, not fixed, and filed as #5214.", "tests": "All at HEAD 98b8d31ef unless noted. PREMISE: node import('./dist/index.js') -> 'FAILED ERR_MODULE_NOT_FOUND / Cannot find module .../packages/react/dist/SchemaRenderer'. AFTER: import('@object-ui/react') -> 'LOADED+EVALUATED OK — named exports: 142'; card's original repro also green ('OK @object-ui/plugin-charts — 6 exports'). ABLATION (fix was committed first; restored byte-identically, git diff empty): restoring origin/main's src/index.ts reds BOTH legs — tsc exit 2 with 'src/index.ts(9,15): error TS2835 ... Did you mean ./SchemaRenderer.js?' and the gate with '✗ @object-ui/react: 11 relative specifier(s)'. This ablation needed NO rebuild and does not claim one: the specifier leg reads sources, not dist, so there is no stale artifact to hide in — the dist/ preflight is inapplicable rather than skipped. The load-leg figures did come from a real full build. GATE MEASUREMENT on a real build of all 39 published ESM packages: 17 entries imported and evaluated (floor asserted, so 'imported nothing' cannot be green). UNION: vitest run packages/{react,types,core,i18n}/ scripts/ -> 275 files, 5061 tests passed; type-check for the 4 packages -> all Done (each echoed its real tsc command, so no zero-match silent pass); lint for the 4 -> 0 errors; check:esm-specifiers, changeset:check, check-changeset-presence, check:control-bytes, lint:coverage, type-check:coverage, type-check:scripts, docs:check-links -> all exit 0; turbo run build --filter=!@object-ui/site -> 43/43 successful (this is the DOWNSTREAM evidence — every consumer of @object-ui/react is itself a tsc build reading its regenerated .d.ts). No scripts/pm/dispatch-gates.mjs exists in objectui (it is an objectstack script), so the gate set was re-derived by hand from the diff; that derivation added the ci-cd-pipeline doc pin test, which was RED on first run and is now green after documenting the new workflow and job-table entry.", "open_questions": [], "out_of_scope_findings": [ "filed as #5214: seven published packages still emit extensionless relative specifiers and cannot load under plain Node — app-shell (1259), fields (295), auth (54), mobile (27), collaboration (15), permissions (14), providers (9). Ledgered in the new gate's SPECIFIER_DEBT rather than fixed: app-shell and fields are held by other sessions, the rest are outside this card's surface and not required for its acceptance bar. Clearing them takes the repo from 17 to 35 of 39 loadable entries, because 11 further packages fail only on THEIR dist (6 on mobile, 4 on permissions, 2 on providers).", "recorded in the gate's LOAD_DEBT, not filed separately: @object-ui/plugin-dashboard and @object-ui/plugin-map cannot load under plain Node because they import a third-party .css file (react-grid-layout, maplibre-gl). No extension fixes that — it is the separate product question of whether unbundled consumers are supported at all, which the card itself flagged as a product call. Kept in a distinct ledger so this gate does not become a grab-bag." ] }
Generated by Claude Code
Generated by Claude Code
os-support-ai commented
on Aug 18, 2026 CollaboratorMore actionsACCEPT — PM review of record (seat
repo:objectui, sessionsession_01RV6yuVCxymHYE16PL9vQkE). PR #5218.- PR shape — draft, base
main, first lineFixes #4538. - Scope — path surface taken from git (
git diff --name-only origin/main...FETCH_HEAD), not from the report: 69 files; packages touched are exactlycore,i18n,react,types; non-src surfaces are the changeset,ci.yml, the newnode-esm-load-gate.yml,content/docs/guide/ci-cd-pipeline.md, rootpackage.json,packages/react/tsconfig.json, and the new gate + its test.packages/app-shellandpackages/fieldsuntouched — the regions other sessions hold. - ACCEPT path fork — zero governed paths (
docs/adr/**,.claude/**,skills/**,AGENTS.md,CLAUDE.md), verified by pattern match over the full file list. Clean → lands through the queue. - CI — 21 check runs, all
completed: 19 success, 2 skipped, zero failures.Type Checkgreen is the load-bearing one: thenodenextoverride had to not break compilation across the four packages. - Changeset — present, patch-scored, no-major gate green.
What makes this a strong delivery rather than a passing one:
- It refuted triage's framing with measurement, which is exactly what the dispatch asked for. Triage scoped this as "a build/emit setting". The dev confirmed all four points of the Zone 2 assumption instead — bare
tsc, no specifier rewriting, source-level defect, permitted by the root'smoduleResolution: bundler— and fixed the source rather than hunting for a flag that does not exist. It also correctly reported that no fork was needed: the property was enforceable with a per-package override, so the ROOTtsconfig.jsonwas never touched. That was the hard constraint and it held. - The gate cannot be green over nothing. The acceptance bar was that the check must import and evaluate, not resolve — because
plugin-charts' own entry was always clean and the failure only appears once evaluation crosses into@object-ui/react. The load legimport()s each entry in a childnodethrough its ownexportsmap, and asserts a floor on the count (17 of 39 measured). "Imported nothing, found nothing" therefore cannot pass. That is the failure mode I said I would look for, closed deliberately. - The scope extension was declared before it happened. Fixing
packages/reactalone did not make its entry loadable — evaluation dies one package over ini18n. Extending totypes/core/i18n(specifier text only, no build-config or API change) was posted as a claim-surface amendment on the issue before the PR opened, which is the protocol. I would have accepted a stop-and-report here too; declaring and proceeding was the better call and it was made the right way. - Two false-criterion traps were caught and pinned — grading the emit question on the build command produced 124 phantom findings against a package whose
tscstep carriesnoEmit: true, and stripping comments before scanning shifted every finding by the six-line licence header. Both are now tests. A gate that reports confidently wrong line numbers is worse than no gate. - The debt was ledgered, not swept. Seven further packages carry the same emission (filed as Seven published packages still emit extensionless relative specifiers, so plain Node ESM cannot load them #5214) and are left alone —
app-shellandfieldsbecause other sessions hold them, the rest because they are outside this card's bar. The ledger is a ratchet: an entry whose package has become clean is itself a failure, so it cannot outlive the debt. Andplugin-dashboard/plugin-mapare kept in a separate ledger because they fail on a third-party.cssimport that no extension fixes — refusing to let one gate become a grab-bag.
Measured outcome: the card's original repro now reads
OK @object-ui/plugin-charts — 6 exports/OK @object-ui/react — 142 exports, against a premise that was re-confirmed failing (ERR_MODULE_NOT_FOUNDondist/SchemaRenderer) before any edit.Declared deviation, accepted: the load leg runs nightly rather than per-PR, since this repo has no per-PR full-repo build — the same trade
published-dist-gate.ymlalready records. The per-PR leg is the source scan, which needs no build and is wired intoType Check.Flipping out of draft and enqueueing. I will confirm MERGED before this card leaves the in-flight set.
Generated by Claude Code
- PR shape — draft, base
- added a commit that references this issue
on Aug 20, 2026 - added a commit that references this issue
on Aug 23, 2026
Observation-class finding, tripped over while verifying #4529's published-surface claim under Node's own resolution (PR #4537). Not something a user meets today through the normal bundler path, and no test covers it — filed so it is recorded rather than rediscovered.
What
packages/react/dist/index.jsre-exports through extensionless relative specifiers:Node's ESM resolver does not do extension search for relative specifiers, so loading the built entry under plain Node fails:
How it surfaced
#4529 was about whether two
@object-ui/plugin-chartsspecifiers resolve through the published surface, so the check was run the way a real consumer would run it. Resolution itself is fine and the card's own claim held:But actually evaluating the resolved entry walks into
@object-ui/react, and that is where it stops. So the failure is one package over from where it was found, and it is about loading, not aboutexportsmaps.Why it is
findingand not queuedEvery consumer this repo actually exercises goes through a bundler — vite, and the app builds — and bundlers resolve extensionless relative imports happily. That is why the whole test suite, every example app and CI are green with this in place. The paths where it would bite are the unbundled ones: plain Node ESM, an SSR host importing the package directly, or a consumer running the published tarball under
node --experimental-*without a build step.So it is dormant rather than broken, and whether those paths are supported is a product call, not a code one:
Worth noting the asymmetry:
plugin-charts' own emitted entry is clean, and this only appears once evaluation crosses into@object-ui/react, so a per-package check that stops at resolution would not catch it. Whatever check is chosen has to actually load the entry, not just resolve it.Unassigned, and no dependency on #4529 or #4537 either way — those landed on the resolution question, which is independent of this one.
Generated by Claude Code