Skip to content

fix(react): make the published entry loadable by Node's own ESM resolver, and gate it - #5218

Merged
os-support-ai merged 3 commits into
mainfrom
claude/issue-4538-react-esm-loadable
Aug 18, 2026
Merged

fix(react): make the published entry loadable by Node's own ESM resolver, and gate it#5218
os-support-ai merged 3 commits into
mainfrom
claude/issue-4538-react-esm-loadable

Conversation

@os-support-ai

Copy link
Copy Markdown
Collaborator

Fixes#4538

Premise check first, as triage asked

The card is from 2026-08-13. Confirmed still true before anything was changed — built dist/index.js carried 10 extensionless re-exports, and the entry did not load:

FAILED ERR_MODULE_NOT_FOUND
Cannot find module '.../packages/react/dist/SchemaRenderer'
imported from '.../packages/react/dist/index.js'

Triage scoped this as "a build/emit setting". It is not

packages/react's build is a bare tsc, and TypeScript never rewrites import specifiers — there is no emit flag that appends .js. What the source writes is what dist ships. The extensionless form was permitted only because the root tsconfig.json sets "moduleResolution": "bundler". So this is a source change, not a build setting.

Enforcement is pinned in packages/react/tsconfig.jsononlymodule/moduleResolution: nodenext, which makes a missing extension a compile error rather than a review item. The root tsconfig.json is untouched; 38 packages extend it and flipping it there is a different change.

Reverse-verified. Restoring the old src/index.ts reds both enforcement legs, and restores byte-identically afterwards:

tsc → src/index.ts(9,15): error TS2835: Relative import paths need explicit file
extensions ... Did you mean './SchemaRenderer.js'? (exit 2)
gate → ✗ @object-ui/react: 11 relative specifier(s) without an explicit extension
packages/react/src/index.ts:9 './SchemaRenderer' → './SchemaRenderer.js'

That ablation needed no rebuild and does not claim one: the specifier leg reads sources, so there is no dist/ for a stale artifact to hide in. The load-leg numbers below are the ones that came from a real build.

Why three more packages are in the diff

Fixing packages/react alone did not make its entry loadable. Rebuilt clean, it still failed one package over:

Cannot find module '.../packages/i18n/dist/i18n'

@object-ui/react evaluates into types, core and i18n, each a bare-tsc build with the identical defect. Those three are specifier text only — no build-config change, no API change. Declared as a claim-surface amendment on the issue before this PR opened. After them:

LOADED+EVALUATED OK — named exports: 142

And the card's original repro — plugin-charts, whose own entry was always clean, failing only once evaluation crossed into @object-ui/react:

OK @object-ui/plugin-charts — 6 exports
OK @object-ui/react — 142 exports

Specifiers changed: react 92, types 95, core 12, i18n 56. Applied by a codemod that resolves every specifier against the filesystem, so a commented-out import to a deleted module is left alone rather than rewritten.

The check imports and evaluates — it does not stop at resolution

scripts/check-node-esm-load.mjs, two legs, because neither is honest alone.

Specifier leg (pnpm check:esm-specifiers) reads sources, needs no build, runs per PR in ci.yml's Type Check job. For a specifier-preserving build the emitted specifier is the source specifier, so the defect is judgeable without building anything.

Load leg (pnpm check:node-esm-load) builds every published package and import()s each entry in a child node, through its own exports map, no bundler and no loader hooks. Nightly workflow, not per-PR — the same trade published-dist-gate.yml records, since this repo has no per-PR full-repo build.

Measured on a full build of all 39 published ESM packages: 17 entries imported and evaluated. The gate asserts a floor on that count, so "imported nothing, found nothing" cannot be a green verdict.

The load leg attributes a missing module to the package that owns it. Four plugins died on packages/mobile/dist/useBreakpoint — not their file. Without attribution that is a dozen findings for one cause; with it, one finding against @object-ui/mobile.

Two false-criterion traps the measurements caught, both now pinned by tests:

  • Grading the emit question on the build command reported 124 findings against @object-ui/console, whose tsc step carries "noEmit": true and emits none of them. The gate reads the tsconfig, following extends.
  • Stripping comments before scanning shifted every finding in react/src/index.ts up by six lines — the license header — so the gate pointed at line 3 for what tsc reported at line 9.

Other packages with the same emission: ledgered, and stated

Seven carry it and are not fixed here — app-shell (1259), fields (295), auth (54), mobile (27), collaboration (15), permissions (14), providers (9). Filed as #5214. app-shell and fields are held by other sessions; the rest are outside this card's surface and not needed for its acceptance bar.

The ledger is a ratchet, not a mute button: an entry whose package has become clean is itself a failure, so it cannot outlive the debt. Clearing those seven would take the repo from 17 to 35 of 39 loadable entries — 11 more packages fail only because of them. Separately ledgered as a different question: plugin-dashboard and plugin-map fail on a third-party .css import, which no extension fixes.

Verification, all at 98b8d31ef

vitest run packages/{react,types,core,i18n}/ scripts/275 files, 5061 tests passed
type-check (react, types, core, i18n)all 4 Done
lint (same 4)0 errors
check:esm-specifiers, changeset:check, changeset-presence, check:control-bytes, lint:coverage, type-check:coverage, type-check:scripts, docs:check-linksall exit 0
turbo run build --filter=!@object-ui/site43/43 successful

The full build is the downstream evidence: every consumer of @object-ui/react is itself a tsc build reading its regenerated .d.ts, and all 43 build tasks pass.

content/docs/guide/ci-cd-pipeline.md gained the workflow section and job-table entry its pin test requires.


Generated by Claude Code

… load the built entry
`packages/react/dist/index.js` re-exported through extensionless relative
specifiers (`./SchemaRenderer`, `./hooks`, ...). Node's ESM resolver does not
extension-search relative specifiers, so importing the published entry under
plain Node failed with ERR_MODULE_NOT_FOUND.
`tsc` never rewrites import specifiers, so the defect is in the source, not in
a build setting: the root tsconfig's `moduleResolution: "bundler"` permits the
extensionless form and bare `tsc` copies it straight into dist.
- packages/react: 92 specifiers extensioned; tsconfig pinned to
module/moduleResolution `nodenext` so a missing extension is a compile error
(TS2835) rather than a review item.
- packages/types, core, i18n: 163 specifiers extensioned. These are react's
dependency closure — react's own entry stayed unloadable until they were
fixed too, because evaluation crosses into them.
Two legs, because neither is honest alone (objectui#4538):
- `pnpm check:esm-specifiers` reads SOURCES and needs no build, so it runs per
pull request in ci.yml's Type Check job. For a package whose build preserves
specifiers (a bare emitting `tsc`), the emitted specifier IS the source
specifier, so the defect is judgeable without building anything.
- `pnpm check:node-esm-load` builds every published package and actually
`import()`s each entry in a child node. Wired to a nightly workflow, not to
pull requests, for the reason published-dist-gate.yml records: this repo has
no per-PR full-repo build.
The load leg EVALUATES rather than resolves, which is the card's measured
point: plugin-charts' own entry resolves fine and the tree was still broken.
A missing module is attributed to the package that OWNS it, so the four plugins
failing on packages/mobile/dist/useBreakpoint produce one finding against
@object-ui/mobile rather than four against innocent importers.
Seven packages still carry the defect and are ledgered with a reason each;
two of them were held by other sessions. The ledger is a ratchet — an entry
whose package is now clean is itself a failure.
Each entry now names the card that clears it, and the three that block other
packages say how many — mobile blocks 6, permissions 4, providers 2. A ledger
entry whose reason is just "follow-up" tells the next reader nothing about
whether it is worth picking up.
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Main entry (gzip)25.3 KB350 KB
Entry fileindex-DfcNaYIf.js
StatusPASS

📦 Bundle Size Report

PackageSizeGzipped
app-shell (index.js)9.83KB3.70KB
app-shell (runtime-config.js)7.42KB2.32KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)8.92KB3.41KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)1.17KB0.53KB
auth (AuthProvider.js)25.13KB5.40KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.13KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.64KB2.21KB
auth (SocialSignInButtons.js)9.60KB3.89KB
auth (UserMenu.js)3.40KB1.22KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.79KB
auth (createAuthenticatedFetch.js)6.34KB2.43KB
auth (index.js)2.71KB1.22KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.02KB0.88KB
auth (useIsWorkspaceAdmin.js)1.61KB0.85KB
collaboration (CommentThread.js)26.07KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.65KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)505.95KB113.30KB
core (index.js)4.11KB1.62KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)159.03KB44.08KB
fields (index.js)236.61KB59.36KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.42KB1.39KB
i18n (pickLocalized.js)3.69KB1.73KB
i18n (provider.js)23.13KB7.63KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)27.60KB6.63KB
i18n (useSafeTranslation.js)7.77KB3.13KB
layout (index.js)39.16KB10.97KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.74KB
mobile (index.js)1.50KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.71KB0.42KB
mobile (useResponsiveConfig.js)1.36KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)9.35KB3.31KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)4.42KB1.42KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.91KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.52KB
permissions (usePermissions.js)1.81KB0.83KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.62KB12.83KB
plugin-charts (index.js)64.75KB18.37KB
plugin-chatbot (index.js)181.21KB43.14KB
plugin-dashboard (index.js)127.85KB32.73KB
plugin-designer (index.js)212.39KB42.83KB
plugin-detail (index.js)241.46KB60.56KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)123.77KB30.07KB
plugin-gantt (index.js)164.10KB39.87KB
plugin-grid (index.js)198.22KB53.27KB
plugin-kanban (index.js)52.93KB14.60KB
plugin-list (index.js)111.39KB27.03KB
plugin-map (index.js)20.02KB6.58KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)41.97KB11.33KB
plugin-timeline (index.js)26.68KB7.66KB
plugin-tree (index.js)8.50KB2.88KB
plugin-view (index.js)83.81KB20.49KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.44KB0.22KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)3.77KB1.33KB
react (SchemaRenderer.js)31.56KB10.70KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)1.33KB0.69KB
react (schema-input.js)1.45KB0.83KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (index.js)4.77KB2.16KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)10.76KB3.17KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.29KB0.24KB
sdui-parser (validate.js)6.92KB2.40KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)0.20KB0.18KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)0.20KB0.18KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.87KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-retry.js)4.32KB2.02KB
types (index.js)3.08KB1.53KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)0.20KB0.18KB
types (ui-action.js)3.40KB1.71KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

finding(react): the built dist emits extensionless relative imports, so plain Node ESM cannot load the published entry

2 participants

@os-support-ai@claude