Skip to content

docs(react): fix createLazyPlugin's JSDoc example to a call that compiles - #5990

Merged
yinlianghui merged 2 commits into
mainfrom
claude/issue-5949-createlazyplugin-jsdoc
Aug 24, 2026
Merged

yinlianghui merged 2 commits into
mainfrom
claude/issue-5949-createlazyplugin-jsdoc

Conversation

@yinlianghui

@yinlianghui yinlianghui commented Aug 24, 2026 •

Copy link
Copy Markdown
Collaborator

Fixes #5949

What was wrong

createLazyPlugin's own @example block passed () => import('@object-ui/plugin-grid') as the importFn, three times over. That package exports its components by name and has no default, so the call supplies the module namespace object where a component is expected. Wrong in both dimensions: rejected by the compiler, and at runtime React.lazy would receive the namespace object as the component and render nothing useful.

This is the worst-placed kind of doc error — it sits on the function it documents, and it is carried into the published .d.ts that editors surface on hover.

The replacement was derived, not guessed

I read @object-ui/plugin-grid's real export shape first: packages/plugin-grid/src/index.tsx opens export { ObjectGrid, VirtualGrid, ImportWizard };, ObjectGrid is declared as a React.FC parameterised by ObjectGridComponentProps, and the file has no export default and no as default re-export (grep confirmed with a control probe, so the zero-hit reads as absent rather than mis-anchored).

Then I measured all three candidate spellings against the compiler, in packages/react's own strict test project:

spelling verdict
() => import('@object-ui/plugin-grid') (what the docblock taught) RED — TS2345: Property 'default' is missing
.then((m) => ({ default: m.ObjectGrid })) RED — TS2322, infers P as never on one branch of the then overload
async () => ({ default: (await import('@object-ui/plugin-grid')).ObjectGrid }) GREEN

So all three examples now use the async unwrap. Each also gets a distinct binding (ObjectGrid, ObjectGridWithFallback, ObjectGridWithRetry) so the fenced block is one coherent program rather than three redeclarations of the same const. The @param line, which previously just asserted "returns a module with default export", now says why a bare plugin import does not satisfy it.

preloadPlugin's example lower in the same file is deliberately untouched. Its parameter is generic over whatever the import resolves to — a plain Promise of T, with no default required anywhere in the type — so the bare import is correct there. That is the one place the obvious sweep would have been wrong.

Proving it compiles — and why not via check:doc-snippets

⚠️ The existing gate cannot reach this. scripts/check-doc-snippet-types.mjs states its own scan surface as "every .mdx and .md page under content/docs, plus every packages/*/README.md" — markdown documents only. A JSDoc @example inside a .tsx source file is categorically outside it, which is exactly why this rotted. Extending that scan surface to source files is a contract change on the gate's population (its header requires anything added to be declared in the same breath) and is a different, larger card. I did not enrol this file into it as a side effect.

Instead the new test packages/react/src/__tests__/LazyPluginLoader.jsdocExample.test.ts pins the two halves that together make the example true, each checked by the thing that can actually judge it:

  1. The shape compiles. typeLevelContract is compiled by tsc -p tsconfig.test.json (chained from this package's type-check, which is what CI's Type Check job runs). It pins that unwrapping a named export satisfies importFn, and carries a @ts-expect-error on the old bare form — so if the compiler ever starts accepting a namespace object there, that line turns the build red.
  2. The docblock teaches that shape. Runtime assertions read the source, extract the code inside the @example fence, and assert it uses the unwrap and never the bare form — so the compiled shape and the documented text cannot drift.

The honest limit, stated because a reader of a green run needs it: this does not compile the docblock's literal text. It cannot — @object-ui/plugin-grid depends on @object-ui/react, so this package cannot import it in either direction without a workspace cycle. The module identity is instead pinned by an on-disk assertion that plugin-grid still has no default export (a path read, not an import; check:phantom-deps confirms no dependency edge was created).

Population of the same wrong shape (issue's "worth checking")

Swept, and reported rather than swept up — nothing outside the fence was touched:

  • content/docs/guide/deployment.md — already corrected on main; it teaches the same async unwrap this PR adopts. That is the sibling that surfaced the bug.
  • packages/react/src/LazyPluginLoader.tsx:166 (preloadPlugin) — bare import, and correct; left alone.
  • packages/core/src/registry/Registry.ts:281 and the many ComponentRegistry.registerLazy(...) call sites — a different API that legitimately takes the whole module namespace. Not the same defect.
  • content/docs/guide/objectos-integration.mdx:546-548 — a plugin map feeding a different loader, not createLazyPlugin. Not swept; flagged here only.
  • packages/react/README.md — no createLazyPlugin example at all.

Real-world callers already use the unwrap-by-name form against this very package (packages/app-shell/src/views/ObjectView.tsx:23), which corroborates the shape independently.

Verification

All run at fc54843f8, the final commit on this branch:

  • pnpm --filter @object-ui/react type-check — exit 0 (script echoed, not a zero-match no-op)
  • pnpm exec vitest run packages/react/ — Test Files 54 passed (54), Tests 739 passed (739)
  • node scripts/check-changeset-presence.mjs — green, declares .changeset/lazy-plugin-jsdoc-compiles.md
  • check:control-bytes, check:esm-specifiers, check:phantom-deps, check:self-import — all exit 0 on their own verdict lines

Reverse-verification, both legs rebuilt and each mutation confirmed on disk before the reading was taken:

  • Reverting the docblock to the bare form (unwrap count 3 to 0, bare form injected 3) turns exactly the two shape assertions red, and the control-probe and premise tests correctly stay green. Restored; tree byte-clean; 5/5 green again.
  • Making the negative call valid raises TS2578: Unused '@ts-expect-error' directive — the negative assertion is load-bearing, not a comment. Restored; tsc clean again.

Declared narrowing: check:doc-snippets was not run to completion locally — it needs most of the workspace built (it reported unbuilt-package for 9 packages), which is the repo-wide build CI owns. Its verdict provably cannot move because of this diff: its population is markdown only and none of the 3 changed files is in it (verified with a control probe on the population regex), and every changed line of the .tsx is a comment line, so the emitted .d.ts type surface is byte-identical in the shapes that gate compiles against.

Lint was likewise narrowed to the changed files: 0 errors, 1 warning, and that warning (no-explicit-any on the P type parameter's any default) is pre-existing on origin/main and not an added line. The config has no type-aware linting, so this diff cannot move the verdict on any untouched file.

Note for readers: this repo's body sanitizer strips short angle-bracket fragments, so generic type arguments above are spelled in prose rather than in brackets.

Related: #5174, #5160, #5053.

Generated by Claude Code

…iles

The `@example` block taught `() => import('@object-ui/plugin-grid')` as the
`importFn`. That package exports its components by NAME and has no `default`,
so the call supplied the module namespace object — TS2345 at compile time
("Property 'default' is missing"), and at runtime `React.lazy` would receive
the namespace object as the component.

Measured against the compiler, all three candidate spellings:
  - bare namespace import          RED  TS2345
  - `.then((m) => ({default: m.X}))` RED  TS2322, infers `P` as `never`
  - `async () => ({default: (await import(...)).X})`  GREEN

All three examples now use the `async` unwrap, and each is given a distinct
binding so the fenced block is one coherent program rather than three
redeclarations of `const ObjectGrid`.

`preloadPlugin`'s example lower in the file is deliberately untouched: its
parameter is `() => Promise<T>`, which expects no `default`, so the bare
import is correct there.

Adds a standing guard. No gate reads a JSDoc `@example` in a `.tsx` source
file — `check-doc-snippet-types.mjs` scans markdown only — so the new test
pins the shape by compilation (`tsc -p tsconfig.test.json`, with a
`@ts-expect-error` on the old form) and pins the docblock text to that shape,
without enrolling this file into another gate's population.
Part of #5949. The docblock ships in the published .d.ts, so this is a
user-visible patch rather than a releases-nothing change.
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 52 chunks) 3232.5 KB 3990.2 KB
Main entry chunk (gzip) 153.6 KB 350 KB
Entry file index-B_FwXNtk.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.38KB 3.90KB
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) 504.75KB 114.32KB
core (index.js) 4.92KB 1.97KB
create-plugin (index.js) 10.08KB 3.26KB
data-objectstack (index.js) 165.30KB 45.79KB
fields (index.js) 238.40KB 59.89KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (currency.js) 1.22KB 0.64KB
i18n (i18n.js) 4.28KB 1.75KB
i18n (index.js) 3.44KB 1.39KB
i18n (pickLocalized.js) 7.62KB 3.26KB
i18n (provider.js) 23.13KB 7.63KB
i18n (useDisplayLocale.js) 2.85KB 1.45KB
i18n (useObjectLabel.js) 33.40KB 8.71KB
i18n (useSafeTranslation.js) 7.77KB 3.13KB
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.44KB
plugin-designer (index.js) 212.30KB 42.80KB
plugin-detail (index.js) 244.00KB 61.86KB
plugin-editor (index.js) 2.46KB 1.10KB
plugin-form (index.js) 125.63KB 30.64KB
plugin-gantt (index.js) 164.15KB 39.88KB
plugin-grid (index.js) 200.79KB 54.26KB
plugin-kanban (index.js) 52.93KB 14.60KB
plugin-list (index.js) 111.86KB 27.22KB
plugin-map (index.js) 20.10KB 6.64KB
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) 8.50KB 2.88KB
plugin-view (index.js) 84.57KB 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) 52.40KB 17.45KB
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) 0.20KB 0.18KB
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 (index.js) 3.88KB 1.85KB
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

@yinlianghui
yinlianghui marked this pull request as ready for review August 24, 2026 11:26
@yinlianghui
yinlianghui added this pull request to the merge queue Aug 24, 2026
Merged via the queue into main with commit 98c3a74 Aug 24, 2026
23 checks passed
@yinlianghui
yinlianghui deleted the claude/issue-5949-createlazyplugin-jsdoc branch August 24, 2026 11:37
Sign up for free to 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.

createLazyPlugin's own JSDoc teaches a call that does not compile — import('@object-ui/plugin-grid') has no default export

2 participants