Skip to content

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

Description

@os-warren

Found while clearing content/docs/guide/deployment.md off the UNGATED_DOCS ledger for #5174 (PR for claude/issue-5174-md-ledger-triage-batch1). Filed unassigned and not fixed there: the guide copy is inside that card's file surface and is fixed; the source JSDoc is not, and it is where the wrong shape is authored.

What

packages/react/src/LazyPluginLoader.tsx:123 declares:

export function createLazyPlugin<P extends object = any>(
  importFn: () => Promise<{ default: React.ComponentType<P> }>,
  options?: LazyPluginOptions
): React.ComponentType<P>

Its own @example block, three times over (:98, :103, :109, carried through to packages/react/dist/LazyPluginLoader.d.ts:40/45/51), passes:

const ObjectGrid = createLazyPlugin(
  () => import('@object-ui/plugin-grid')
);

@object-ui/plugin-grid has no default export. packages/plugin-grid/dist/index.d.ts opens export { ObjectGrid, VirtualGrid, ImportWizard }; and the only default token in the whole file is import { default as React } from 'react'. What the call actually supplies is TypeScript's synthetic namespace default — the module object — so the argument is Promise<{ default: typeof import('@object-ui/plugin-grid'), ObjectGrid: FC<…>, … }>.

Measured

Compiled against the built dist/ by scripts/check-doc-snippet-types.mjs (the same harness, same --strict program):

TS2322: Type 'Promise<{ default: typeof import(".../packages/plugin-grid/dist/index"); ObjectGrid: FC<ObjectGridComponentProps>; … }>'
        is not assignable to type 'Promise<{ default: ComponentType<any>; }>'.
          Type 'typeof import(".../packages/plugin-grid/dist/index")' is not assignable to type 'ComponentType<any>'.

This is not only a type error. At runtime React.lazy would receive the module namespace object as the component and render nothing useful — the documented call is wrong in both dimensions.

The shape that does compile

Verified green in the same program, and this is what the guide now says:

const ObjectGrid = createLazyPlugin(
  // The plugin package has no default export — name the component you want.
  async () => ({ default: (await import('@object-ui/plugin-grid')).ObjectGrid }),
  { fallback: <div>Loading grid...</div> }
);

Note the async form specifically: the .then((m) => ({ default: m.ObjectGrid })) spelling infers P as never on one branch of the then overload and stays red.

Why this is filed rather than folded in

⛔ Do not "fix" this by adding a default export to @object-ui/plugin-grid. That is a contract change on a published package, and the documented surface is the thing that is wrong, not the package. Two candidate shapes for whoever picks this up, and they are not the same decision:

  1. Correct the three JSDoc examples in place (cheapest, matches what the guide now teaches).
  2. Tighten importFn's parameter type so a namespace object cannot satisfy it silently — worth checking whether any in-repo caller currently relies on the loose shape before proposing it.

Reachability

packages/react's README is already covered by check-doc-snippet-types, but a JSDoc @example inside a .tsx source file is not read by any gate — neither this one nor check-doc-component-types. So the wrong call survives in the published .d.ts that editors surface on hover, which is the highest-traffic place a reader meets it. Same class as #5160, one layer in: a documented symbol whose usage does not compile against the surface it is documented on.

Related: #5174 (the ledger card that surfaced it), #5160, #5053.

Activity

  1. added
    domain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seat
    on Aug 24, 2026
  2. added theissue type on Aug 24, 2026
  3. os-zhuang commented on Aug 24, 2026

    @os-zhuang
    Contributor

    Triage (Routine seat, hourly round): → pm:queue + domain:ui, type Bug.

    Rationale: createLazyPlugin's own JSDoc teaches a call that does not compile (import('@object-ui/plugin-grid') has no default export) — a published-library doc surface actively misleading its callers. Mechanical fix at the docblock; verify the corrected example against the real export shape.


    Generated by Claude Code

  4. self-assigned this
    on Aug 24, 2026
  5. yinlianghui commented on Aug 24, 2026

    @yinlianghui
    Collaborator

    Claim: PM loop round 36
    Session: session_01CSoz9uGhaaSgiq3hshtN7L
    Branch: claude/issue-5949-createlazyplugin-jsdoc
    Worktree: objectui-issue-5949
    Domain: domain:ui
    File surface: packages/react/src/LazyPluginLoader.tsx (+ its tests and a changeset if one is owed) (stop on breach; explain in the report)
    Container & model: S, mode:subagent, model: opus — held above the sonnet floor because the correct replacement call has to be derived from the actual export shape, which is a reading, not a transcription.
    Clause-②: no — a JSDoc example. No runtime surface, no schema, no accept/reject behaviour.
    Serial constraints cleared: packages/react/src/** — no in-flight claim in this lane, and git ls-remote showed no claude/issue-* branch for any open lane card at claim time. Concurrent siblings #5930 (packages/components), #3909 + #4600 (packages/types, disjoint files), the #5956 pair (packages/plugin-detail + locales) — all disjoint by full path.

    The defect

    createLazyPlugin's own JSDoc teaches a call that does not compile: it shows import('@object-ui/plugin-grid') being used where a default export is expected, and that package has no default export. Located at packages/react/src/LazyPluginLoader.tsx (verified present on origin/main this round; ⚠️ re-derive the exact line from the tree, the card is hours old but the file is live).

    This is the worst-placed kind of documentation error: it sits on the function it documents, so it is what an author — and an AI reading the package for a few-shot example — copies first, and it fails at their build rather than at ours.

    What the fix has to establish, and it is not a transcription

    ⛔ Do not guess the replacement. Read @object-ui/plugin-grid's actual export shape and write the call that genuinely compiles against it — whether that is a named export, a namespace import, or a .then(m => m.X) unwrap. Then prove it compiles, because a JSDoc example is exactly the kind of code no gate checks:

    • ✅ Preferred: if this repo's check:doc-snippets / check-doc-snippet-types.mjs machinery can reach a JSDoc example, wire it in — that turns a one-off correction into a standing guard, and this card's whole point is that nothing caught it.
    • ⚠️ If it cannot reach JSDoc (its DOCS_ROOT is content/docs, so it very likely cannot), say so explicitly and instead prove the corrected call type-checks by some direct means you name. ⛔ "It looks right" is not evidence, and ⛔ do not enrol a new file into a gate's population as a side effect.

    Worth checking while you are in there

    Whether the same wrong call shape appears in other examples for this API — the neighbouring lazy-plugin docs, the package README, or content/docs. If it does, report the population; ⛔ do not widen this fence to sweep it without saying so. One wrong example usually has siblings.

    premise_still_valid: false with no PR is a legitimate delivery if it has already been corrected. Use a control probe so a zero-hit grep reads as real rather than mis-anchored — ⚠️ two devs hit wrong-anchor false negatives this round, and so did this seat.

    Draft PR, Fixes #5949. Post the terminal report here (plain-text first-line marker — the HTML-comment form does not survive this repo's sanitizer) and return it.


    Generated by Claude Code

  6. yinlianghui commented on Aug 24, 2026

    @yinlianghui
    Collaborator

    os-dev-report

    {
      "issue": 5949,
      "status": "done",
      "branch": "claude/issue-5949-createlazyplugin-jsdoc",
      "pr": "https://github.com/objectstack-ai/objectui/pull/5990",
      "premise_still_valid": true,
      "summary": "Premise confirmed live on origin/main: createLazyPlugin's @example block passed a bare `() => import('@object-ui/plugin-grid')` as importFn, three times. I derived the replacement from plugin-grid's real export shape rather than transcribing it -- packages/plugin-grid/src/index.tsx opens `export { ObjectGrid, VirtualGrid, ImportWizard };`, ObjectGrid is a React.FC, and there is no `export default` and no `as default` re-export (grep with a control probe, so the zero-hit reads as absent, not mis-anchored). I then measured all three candidate spellings against the compiler: the bare form is RED (TS2345, 'Property default is missing'), the `.then((m) => ({default: m.ObjectGrid}))` form is RED (TS2322, infers P as never on one branch of the then overload -- independently reproducing the issue's claim), and the `async` unwrap is GREEN. All three examples now use the async unwrap, each with a distinct binding so the fenced block is one coherent program instead of three redeclarations of `const ObjectGrid`. preloadPlugin's bare example lower in the same file is deliberately untouched: its parameter is a plain Promise of T and expects no `default`, so it is correct as written -- that is the one place an obvious sweep would have been wrong. Patch changeset added (not empty-frontmatter): the docblock ships in the published .d.ts, so this is user-visible.",
      "tests": "All greens below are at fc54843f8, the final commit on the branch (tree clean at time of run). ZONE 2 ANSWER -- the preferred route is NOT available and I did not fake it: scripts/check-doc-snippet-types.mjs states its own scan surface as 'every .mdx and .md page under content/docs, plus every packages/NAME/README.md' -- markdown documents only, so a JSDoc @example in a .tsx source file is categorically outside it. Extending that surface is a contract change on the gate's population (its header requires additions 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. NAMED DIRECT MEANS instead: packages/react/src/__tests__/LazyPluginLoader.jsdocExample.test.ts pins (1) the SHAPE by compilation -- `typeLevelContract` is compiled by `tsc -p tsconfig.test.json`, chained from this package's `type-check` script which is what CI's Type Check job runs, and carries a @ts-expect-error on the old bare form; and (2) the DOCBLOCK TEXT by runtime assertions that extract the code inside the @example fence and assert it uses the unwrap and never the bare form -- so compiled shape and documented text cannot drift. Honest limit, stated: it does NOT compile the docblock's literal text and 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). COMMANDS: `pnpm --filter @object-ui/react type-check` exit 0 with the script name echoed (`tsc --noEmit && tsc -p tsconfig.test.json`), so not a zero-match no-op. `pnpm exec vitest run packages/react/` -> 'Test Files 54 passed (54) / Tests 739 passed (739)'. NOTE: `pnpm --filter @object-ui/react test` is REFUSED by this repo's vitest guard (objectui#3378) exactly as briefed; the repo-root form above is the real run. `node scripts/check-changeset-presence.mjs` -> green, declares .changeset/lazy-plugin-jsdoc-compiles.md. check:control-bytes -> 'OK (scanned 4958 tracked text file(s))'; check:esm-specifiers, check:phantom-deps ('Every in-scope import is declared'), check:self-import -> all exit 0 on their own printed verdict lines. Control-byte self-scan of the three changed files: zero hits, with a control probe proving the grep class actually matches a control byte. REVERSE-VERIFICATION, direction predicted before running, both legs restored: LEG 1 -- reverted the docblock to the bare form, mutation confirmed on disk BEFORE reading (unwrap count 3->0, bare form injected 3, not merely a non-empty diffstat); result RED in the predicted direction, exactly the two shape assertions failing while the control-probe and premise tests correctly stayed green; restored, tree byte-clean, 5/5 green again. LEG 2 -- made the negative call valid; mutation confirmed on disk (bad-call 1->0, good-call now 2); result `TS2578: Unused '@ts-expect-error' directive`, proving the negative is load-bearing rather than decorative; restored, tsc clean again. Both legs ran under an EXIT/INT/TERM trap so a timeout could not leave the tree mutated. DECLARED NARROWING: check:doc-snippets could not be run to completion locally -- it needs most of the workspace built and reported `unbuilt-package` for 9 packages, which is the repo-wide build CI owns. Its verdict provably cannot move because of this diff, on three counts: its population is read from its own config (markdown only), none of the 3 changed files is in that population (checked 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 unchanged in the shapes that gate compiles against. Lint likewise narrowed to the changed files: 0 errors, 1 warning, and that warning (no-explicit-any on the pre-existing `P extends object = any`) is present on origin/main and is not an added line; the eslint config has no type-aware linting, so this diff cannot move the verdict on any untouched file.",
      "open_questions": [],
      "out_of_scope_findings": [
        "Nothing filed. Zone 3 population swept and REPORTED, not swept up -- nothing outside the fence was touched, and each entry was read rather than assumed: content/docs/guide/deployment.md was ALREADY CORRECTED on main and teaches the same async unwrap this PR adopts (that is the sibling that surfaced the bug); LazyPluginLoader.tsx:166 (preloadPlugin) is a bare import and is CORRECT, left alone; Registry.ts:281 plus the many ComponentRegistry.registerLazy call sites are a DIFFERENT API that legitimately takes the whole module namespace; content/docs/guide/objectos-integration.mdx:546-548 builds a map of thunks and merely does `await plugins.grid()`, so nothing there expects a default -- correct as written, verified by reading it rather than pattern-matching the grep hit; packages/react/README.md has no createLazyPlugin example at all. No second instance of the defect exists, so there was nothing to file.",
        "Corroboration, not a finding: real callers already use the unwrap-by-name form against this very package (packages/app-shell/src/views/ObjectView.tsx:23), which independently supports the chosen shape."
      ]
    }

    Generated by Claude Code

  7. yinlianghui commented on Aug 24, 2026

    @yinlianghui
    Collaborator

    ACCEPT — PR #5990 (R36, domain:ui)

    Reviewed against the diff and origin/main, not against the report's self-description.

    Checklist: draft ✅ · base main ✅ · Fixes #5949 first line ✅ · 3 files, fence honoured ✅ · changeset patch, not empty-frontmatter ✅ — correctly reasoned: the docblock ships in the published .d.ts and editors surface it on hover, so this is user-visible. Ratified.

    Spot-checks I ran myself

    claim my reading on origin/main
    the bare import is in the @example, three times ✅ lines 99, 104, 110 — control probe: 4 createLazyPlugin hits, so the file was really read
    @object-ui/plugin-grid has no default export ✅ export default|as default → exit 1, zero hits; control: export { ObjectGrid, VirtualGrid, ImportWizard }; at :23
    preloadPlugin's bare import at :166 is correct and must not be swept ✅ confirmed — preloadPlugin<T = unknown>(importFn: () => Promise<T>): Promise<T>, no default anywhere in the type

    The judgement that earns this ACCEPT

    Not sweeping preloadPlugin. Four occurrences of import('@object-ui/plugin-grid') sit in one file; three are wrong and the fourth is right, and the difference is only visible if you read the signature rather than pattern-matching the string. A grep-and-replace — the obvious shape of this task — would have "corrected" a correct example into one that teaches an unnecessary unwrap for an API that requires none. You called that out as "the one place the obvious sweep would have been wrong", and it is.

    Deriving the replacement instead of guessing it, and then measuring all three candidates against the compiler: the bare form RED (TS2345), the .then(m => ({default: m.ObjectGrid})) form RED (TS2322, P inferred as never on one branch of the then overload), the async unwrap GREEN. The middle one is the spelling most people would reach for first, and knowing it fails is worth more than knowing the right answer.

    Zone 2 answered honestly, which was the actual test

    The brief offered a preferred route and an escape hatch. You took the escape hatch for a stated reason — check-doc-snippet-types.mjs declares its own population as markdown only, so a JSDoc @example in a .tsx is categorically outside it — and ⛔ did not enrol the file as a side effect, which I explicitly forbade. Correctly identified that extending the gate's population is a contract change on the gate, and a different card.

    What you built instead is better than what I asked for: a two-sided pin. The shape is judged by tsc -p tsconfig.test.json (chained from type-check, which is what CI's Type Check job runs), and the documented text is judged by runtime assertions that extract the code inside the @example fence — so compiled shape and prose cannot drift apart. That is the failure mode that produced this card.

    And the honest limit is stated rather than papered over: it cannot compile the docblock's literal text, because @object-ui/plugin-grid depends on @object-ui/react and importing it either way is a workspace cycle. Module identity is pinned by an on-disk read instead — a path read, not an import, with check:phantom-deps confirming no dependency edge appeared. A reader of a green run needs exactly that sentence.

    Reverse-verification leg 2 is the one most people skip: making the negative call valid raises TS2578: Unused '@ts-expect-error' directive, which proves the negative assertion is load-bearing rather than a decorative comment. A @ts-expect-error that would pass even if the thing it guards started compiling is worse than no test, and you measured that it isn't one.

    The population was reported, not swept

    Five candidate sites, each read rather than pattern-matched, and every one left untouched: deployment.md (already corrected on main — the sibling that surfaced this), preloadPlugin (correct), Registry.ts + registerLazy call sites (a different API that legitimately takes the whole namespace), objectos-integration.mdx (a thunk map feeding a different loader), react/README.md (no such example). No second instance exists, so nothing was filed — a nil finding stated with its evidence, which is the right output when the honest answer is "none".

    Landing

    Not governed. Gates get read by name, once, at ~11 minutes past this PR's own run started_at (~11:25Z); enqueue requires every check green, not the required subset. ⚠️ Noting for the seat record: this PR's body lost three angle-bracket fragments to the repo sanitizer even inside backticks, and you respelled generics in prose — third confirmation of that platform fact this session.


    Generated by Claude Code

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

Metadata

Metadata

Assignees

Labels

domain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seatpm:dispatched

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions