Skip to content

fix(app-shell): pin nodenext and read Monaco's named Editor export - #6338

Merged
os-warren merged 1 commit into
mainfrom
claude/issue-5440-app-shell-nodenext-pin
Aug 25, 2026
Merged

fix(app-shell): pin nodenext and read Monaco's named Editor export#6338
os-warren merged 1 commit into
mainfrom
claude/issue-5440-app-shell-nodenext-pin

Conversation

@os-warren

Copy link
Copy Markdown
Collaborator

Fixes #5440

Lands the module / moduleResolution: nodenext pin in packages/app-shell/tsconfig.json — the last of the consumer pins, and the largest — and resolves the two @monaco-editor/react interop errors that were the only thing standing between the pin and a green enforcement.

(Generics below are written with a space after the opening angle bracket, following the card's own convention: GitHub's body sanitizer strips an angle bracket followed by a letter as an HTML tag.)

The table, re-derived rather than inherited

Every number in the card was measured on 2026-08-20 at 32ef595f4 and was explicitly not re-measured at dispatch. Re-derived here at f8c70f4f3, closure built first (pnpm --filter '@object-ui/app-shell^...' build, exit 0), via pnpm --filter @object-ui/app-shell exec tsc --noEmit:

state of packages/app-shell/tsconfig.json errors exit
no pin (control) 0 0
nodenext pin, before this change 2 — both TS2345 1
nodenext pin, after this change 0 0

The control row still holds — app-shell type-checks clean unpinned, so every error is produced by the pin, which is what made it usable as a measuring instrument before it was usable as enforcement.

The 21 TS7006 are gone. Triage asked for this specifically: they were attributed to #5439, and #5439 landed. None survived, so there is no new residue to report and nothing was widened. The card's 23 is now 2, and the line/column anchors re-anchored unchanged at JsonSourceEditor.tsx(30,31) and previews/SourcePageEditor.tsx(30,31).

The monaco call: named export, and why m.Editor is verifiably the component

Taken as triage presumed — the named-export shape. The card's worry was that a lazy import can type-check and still render the wrong thing, which a green type-check would not catch, so the name was checked in the installed @monaco-editor/react@4.7.0 rather than assumed:

  • Its own typings alias one declaration to both names. The last line of dist/index.d.ts is export { ..., _default as Editor, ..., _default as default, useMonaco }, and _default is react.MemoExoticComponent< typeof Editor >. Editor and default are not two symbols that happen to agree — they are the same declaration.
  • Runtime identity holds in both module formats. In the CommonJS build (dist/index.js, what nodenext resolves through main, since the package ships no exports map): m.default === m.Editor is true, with $$typeof = Symbol(react.memo). In the ESM build (dist/index.mjs, what the bundler path uses today): the emitted export line is export{we as DiffEditor,de as Editor,Ft as default,...} with Ft=de, and importing it gives m.default === m.Editor true.

So this is the same object the default was resolving to, under both resolutions — the swap cannot change which component renders. That is established by identity, not by a browser run: I did not boot the app or drive Monaco in a browser, and the assertion here is the narrower one that identity permits.

What the ablation contradicted

I predicted that pointing the factory at DiffEditor would type-check green — the "typed but wrong" case that motivated adding a render-level test. That prediction was wrong, and the measurement is reported rather than the template:

mutation tsc --noEmit new test
mod.default (the namespace) red — TS2345 red
mod.DiffEditor red — TS2353, 'tabSize' does not exist in type 'IDiffEditorConstructionOptions' red

The compiler already rejects both wrong components at this call site, because the two components' prop types differ and this call site passes editor-shaped options. So the new test is not what catches a wrong component.

Measuring what it does catch turned up a real hole: an editor that resolves to nothing is invisible to the existing suites, because they assert the textarea fallback and a broken lazy import produces exactly that fallback. Measured — remove Editor from JsonSourceEditor.fallback.test.tsx's stub, leaving the factory reading an export that is not there, and that suite still passes (exit 0): the DOM-poll backstop flips to the textarea before the broken import is ever rendered. Green there means "the fallback works", never "the editor works". JsonSourceEditor.monaco-export.test.tsx is the one test that renders the editor and asserts it painted. Its docstring states this measured reasoning, not my original assumption.

Each ablation leg mutated source only (vitest and tsc both read these sources directly — no dist is involved for app-shell's own tree), proved the mutation landed on disk by grep count of the injected and removed text, and restored under a trap with absolute paths, proving restoration by blob hash against the HEAD blob plus an empty git diff HEAD — not by an exit code.

File surface

Two files beyond the three the claim declared, plus one new test, all inside packages/app-shell/**:

  • packages/app-shell/tsconfig.json — the pin.
  • JsonSourceEditor.tsx, previews/SourcePageEditor.tsx — the named export.
  • JsonSourceEditor.fallback.test.tsx, useMonacoFallback.fastfail.test.tsxadded surface. Both stubs bound the component to default only, so the change would have left them handing React.lazy an undefined export. They are rebound to both names, as the real module does.
  • JsonSourceEditor.monaco-export.test.tsxnew, per the hole measured above.
  • .changeset/app-shell-nodenext-pin-5440.mdpatch, per check-changeset-presence.mjs's own verdict.

No pin asserts the set of packages carrying the nodenext lines, so this change has no second half. Checked rather than assumed: every mention of nodenext / node16 in tracked files outside node_modules is either a package's own tsconfig (8 of them, now 9), a vite.config.ts, a changelog, or prose. The gate that reads these configs — check-node-esm-load.mjs — reads only noEmit, which this change does not touch, so app-shell stays in the specifier leg exactly as before.

Gates

All run at ae4546995, exit codes captured by redirect before any pipe, each quoted from the gate's own verdict line.

gate exit verdict
pnpm --filter @object-ui/app-shell type-check (acceptance) 0 ran both legs: tsc --noEmit && tsc -p tsconfig.test.json
pnpm exec vitest runviews/metadata-admin/ + views/studio-design/ 0 Test Files 239 passed (239), Tests 2225 passed / 1 skipped
check:vi-mock-specifiers 0 OK (3744 tracked source file(s) ... 437 carry a mock ...)
check:esm-specifiers 0 Specifier leg: no un-ledgered package emits an extensionless relative specifier.
check-node-esm-load.test.ts (asserts every tsconfig in the repo parses) 0 Test Files 1 passed, Tests 44 passed
check:control-bytes 0 OK (scanned 5217 tracked text file(s); skipped 85 binary)
type-check:coverage 0 45/46 via type-check ... 41/41 packages compile their tests
check:lint-coverage 0 lint coverage: 46/46 packages linted, 0 with outstanding errors
check:published-dist 0 No published package's build output carries tooling material.
check:eager-closure 0 Console eager closure is 3223.1 KB gzipped across 52 of 508 chunks (budget: 3266.6 KB, headroom: 43.5 KB)
check:changeset-presence / -no-major / -fixed 0 / 0 / 0 declares 1 changeset(s) · No changeset declares a major bump. · All workspace packages are in the changeset fixed group.
pnpm --filter @object-ui/console build 0 consumer still compiles; also what makes the eager-closure gauge real rather than absent

check:eager-closure first came back exit 2 with No eager-closure report at apps/console/dist/eager-closure.json ... This is a broken gauge, not a passing budget — a missing prerequisite, not a finding. It is reported above only after building the console, which is the run that makes it a measurement.

Declared narrowing: repo-wide pnpm lint (turbo run lint, 46 packages) was narrowed to pnpm --filter @object-ui/app-shell lint — exit 0, 0 errors, 2678 warnings (warnings pre-existing). The narrowing is a measurement rather than a gap, on three counts: the population comes from eslint's own config resolution rather than my guess; --format json reports 962 files inspected, all 5 of my touched-or-new files among them; and eslint.config.js enables no type-aware linting (no projectService, no parserOptions.project), so no rule reads types across a package boundary and a diff confined to packages/app-shell/** cannot move any untouched package's verdict. CI runs the full farm regardless.

CI had not converged when this was opened — reporting at draft-PR time is the dispatch contract, and red gates come back as a patch round on this same claim.


Generated by Claude Code

`packages/app-shell/tsconfig.json` now carries the `module` /
`moduleResolution: nodenext` pin that `@object-ui/react`,
`@object-ui/fields` and five other packages already carry. This package
builds with a bare `tsc` and `tsc` never rewrites specifiers, so what the
source writes is what `dist` ships; under `nodenext` a missing relative
extension is TS2835 and a bare directory import is TS2834, which makes the
property the compiler's business instead of review's.

The pin was used as a measuring instrument here long before it could be
used as enforcement. Re-measured on this branch at f8c70f4: unpinned 0
errors, pinned 2 — down from 1097 before the `@object-ui/components`
typings were fixed and 23 before `@object-ui/plugin-chatbot`'s were. Both
survivors are the lazy `@monaco-editor/react` import.

That import now reads the named `Editor` export. `@monaco-editor/react@4.7.0`
is CommonJS with no `exports` map, so under `nodenext` its default is the
module namespace rather than the component and `React.lazy` rejects it
(TS2345). Verified rather than assumed from the name: the package's own
typings alias one declaration to both spellings
(`export { _default as Editor, ..., _default as default }`), and in the
installed 4.7.0 `default === Editor` holds in its CommonJS build and its ESM
build alike, so the component that renders is unchanged.

The two test stubs for the module bound only `default`, so they are rebound
to both names as the real module does. A new test then covers the one case
nothing else did, which is not the case that was assumed: the compiler
already rejects both wrong components at this call site (TS2345 for the
namespace, TS2353 for `DiffEditor`), but an editor that resolves to NOTHING
produces exactly the textarea fallback the neighbouring suites assert —
measured, that suite stays green with `Editor` deleted from its stub. The
new test renders the editor and asserts it painted.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012CZgmFFzqA9cX8tBMhvpFe
@github-actions github-actions Bot added the tests label Aug 25, 2026
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 52 chunks) 3223.5 KB 3266.6 KB
Main entry chunk (gzip) 154.2 KB 350 KB
Entry file index-dM8SJVXj.js
Status PASS

The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it.


📦 Bundle Size Report

Package Size Gzipped
app-shell (consoleActionDispatch.js) 0.20KB 0.19KB
app-shell (index.js) 10.96KB 4.16KB
app-shell (runtime-config.js) 18.10KB 6.51KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 10.06KB 3.86KB
auth (ActiveOrganizationStorage.js) 25.05KB 9.16KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 2.07KB 1.00KB
auth (AuthProvider.js) 40.18KB 10.59KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 18.15KB 5.39KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.65KB 2.22KB
auth (SocialSignInButtons.js) 9.61KB 3.89KB
auth (UserMenu.js) 3.41KB 1.23KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 40.21KB 10.80KB
auth (createAuthenticatedFetch.js) 8.46KB 3.43KB
auth (index.js) 3.19KB 1.44KB
auth (invitation-status.js) 1.22KB 0.70KB
auth (org-roles.js) 6.66KB 2.78KB
auth (phone-identifier.js) 1.11KB 0.66KB
auth (types.js) 0.59KB 0.35KB
auth (useAuth.js) 5.30KB 1.02KB
auth (useWorkspaceAdminStatus.js) 5.13KB 2.35KB
collaboration (CommentThread.js) 26.08KB 7.56KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 6.49KB 2.64KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.68KB 0.73KB
collaboration (useCollaborationTranslation.js) 6.05KB 2.52KB
collaboration (useCommentSearch.js) 1.98KB 0.88KB
collaboration (useConflictResolution.js) 7.75KB 1.86KB
collaboration (useMentionNotifications.js) 1.81KB 0.68KB
collaboration (usePresence.js) 6.33KB 1.84KB
collaboration (useRealtimeSubscription.js) 7.91KB 2.01KB
components (index.js) 506.08KB 114.67KB
core (index.js) 5.30KB 2.13KB
create-plugin (index.js) 10.08KB 3.26KB
data-objectstack (index.js) 171.74KB 47.48KB
fields (index.js) 238.89KB 60.02KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (currency.js) 1.22KB 0.64KB
i18n (fallbackInterpolation.js) 6.25KB 2.77KB
i18n (i18n.js) 4.28KB 1.75KB
i18n (index.js) 3.44KB 1.39KB
i18n (pickLocalized.js) 7.62KB 3.26KB
i18n (provider.js) 26.89KB 9.04KB
i18n (useDisplayLocale.js) 2.85KB 1.45KB
i18n (useObjectLabel.js) 33.40KB 8.71KB
i18n (useSafeTranslation.js) 5.60KB 2.33KB
layout (index.js) 38.95KB 10.97KB
mobile (MobileProvider.js) 0.92KB 0.49KB
mobile (ResponsiveContainer.js) 0.94KB 0.38KB
mobile (breakpoints.js) 1.51KB 0.70KB
mobile (createOfflineDataSource.js) 5.61KB 1.75KB
mobile (index.js) 1.55KB 0.62KB
mobile (offlineQueue.js) 3.91KB 1.35KB
mobile (pwa.js) 0.97KB 0.49KB
mobile (serviceWorker.js) 1.48KB 0.62KB
mobile (serviceWorkerSource.js) 3.41KB 1.48KB
mobile (useBreakpoint.js) 1.54KB 0.65KB
mobile (useGesture.js) 6.96KB 1.98KB
mobile (useOfflineSync.js) 1.99KB 0.72KB
mobile (usePullToRefresh.js) 2.53KB 0.85KB
mobile (useResponsive.js) 0.72KB 0.42KB
mobile (useResponsiveConfig.js) 1.37KB 0.63KB
mobile (useSpecGesture.js) 4.32KB 1.64KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 9.53KB 3.38KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 4.64KB 1.50KB
permissions (evaluator.js) 5.12KB 1.74KB
permissions (index.js) 0.93KB 0.41KB
permissions (store.js) 0.91KB 0.42KB
permissions (useFieldPermissions.js) 1.28KB 0.53KB
permissions (usePermissions.js) 1.93KB 0.88KB
plugin-ai (index.js) 15.75KB 3.80KB
plugin-calendar (index.js) 46.62KB 12.83KB
plugin-charts (index.js) 64.66KB 18.32KB
plugin-chatbot (index.js) 188.21KB 44.67KB
plugin-dashboard (index.js) 133.35KB 34.45KB
plugin-designer (index.js) 211.95KB 42.75KB
plugin-detail (index.js) 245.10KB 62.31KB
plugin-editor (index.js) 2.46KB 1.10KB
plugin-form (index.js) 126.92KB 30.85KB
plugin-gantt (index.js) 164.14KB 39.87KB
plugin-grid (index.js) 201.21KB 54.43KB
plugin-kanban (index.js) 52.83KB 14.55KB
plugin-list (index.js) 111.94KB 27.24KB
plugin-map (index.js) 20.09KB 6.62KB
plugin-markdown (index.js) 13.72KB 4.69KB
plugin-report (index.js) 43.49KB 11.93KB
plugin-timeline (index.js) 26.49KB 7.59KB
plugin-tree (index.js) 9.26KB 3.13KB
plugin-view (index.js) 84.55KB 20.74KB
providers (DataSourceProvider.js) 0.75KB 0.39KB
providers (MetadataProvider.js) 1.37KB 0.59KB
providers (ThemeProvider.js) 1.90KB 0.85KB
providers (UploadProvider.js) 11.66KB 3.50KB
providers (index.js) 0.45KB 0.23KB
providers (types.js) 0.01KB 0.04KB
react-runtime (index.js) 5.62KB 2.34KB
react (LazyPluginLoader.js) 4.47KB 1.63KB
react (SchemaRenderer.js) 54.84KB 18.43KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 1.35KB 0.70KB
react (schema-input.js) 2.32KB 1.24KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (codegen.js) 5.41KB 2.34KB
sdui-parser (dashboard-widget-options.js) 3.08KB 1.30KB
sdui-parser (index.js) 4.93KB 2.24KB
sdui-parser (input-type.js) 2.84KB 1.40KB
sdui-parser (parse.js) 12.13KB 3.65KB
sdui-parser (provenance.js) 3.66KB 1.82KB
sdui-parser (types.js) 0.28KB 0.23KB
sdui-parser (validate.js) 7.54KB 2.63KB
types (ai.js) 0.20KB 0.17KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 2.87KB 0.99KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (complex.js) 2.74KB 1.41KB
types (crud.js) 0.20KB 0.18KB
types (dashboard-filter-alias.js) 6.23KB 2.74KB
types (data-display.js) 0.20KB 0.18KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (designer.js) 1.87KB 0.85KB
types (disclosure.js) 0.20KB 0.18KB
types (error-code.js) 1.54KB 0.88KB
types (feedback.js) 0.20KB 0.18KB
types (field-types.js) 0.20KB 0.18KB
types (form.js) 0.20KB 0.18KB
types (http-inflight.js) 8.87KB 3.73KB
types (http-retry.js) 4.32KB 2.02KB
types (icon-key-migration.js) 4.26KB 1.63KB
types (index.js) 4.49KB 2.14KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 2.59KB 1.31KB
types (navigation.js) 0.20KB 0.18KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 0.20KB 0.18KB
types (plugin-scope.js) 0.20KB 0.18KB
types (record-components.js) 0.20KB 0.19KB
types (record-semantics.js) 1.28KB 0.67KB
types (registry.js) 0.20KB 0.18KB
types (reports.js) 0.20KB 0.18KB
types (spec-report.js) 5.05KB 1.93KB
types (spec-ui-namespace.js) 0.20KB 0.19KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 6.28KB 2.87KB
types (ui-action.js) 3.40KB 1.71KB
types (views.js) 0.20KB 0.18KB
types (widget.js) 0.20KB 0.18KB

Size Limits

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

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

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

@object-ui/app-shell still cannot take the nodenext pin: @monaco-editor/react lazy import is TS2345 under Node16/NodeNext

2 participants