Skip to content

fix(vscode-extension): Export to React wrote a phantom registerDefaultRenderers import into every generated file (objectui#7837) - #7863

Merged
os-sam merged 2 commits into
mainfrom
claude/issue-7837-vscode-export-react-phantom
Sep 6, 2026
Merged

fix(vscode-extension): Export to React wrote a phantom registerDefaultRenderers import into every generated file (objectui#7837)#7863
os-sam merged 2 commits into
mainfrom
claude/issue-7837-vscode-export-react-phantom

Conversation

@claude

@claude claude Bot commented Sep 6, 2026

Copy link
Copy Markdown
Contributor

Fixes #7837

The VS Code extension's Export to React command wrote a phantom import into
every file it generated for a user. generateReactComponent() emitted

import { registerDefaultRenderers } from '@object-ui/components';

// Register default components once
registerDefaultRenderers();

and that symbol is on no export of the package, so the file the user received did
not compile — and the error named a symbol they never typed.

The preamble lives inside a template literal, which is why this survived: this
package's tsc --noEmit sees a string, tsup copies it through, and no doc gate's
scan surface reaches this file. The package's own type-check is green before this
PR and green after; on this defect it proves nothing in either direction.

Re-verified on today's tree, not taken from the card

claim measured at 3faaa7d0f
registerDefaultRenderers is not an export of @object-ui/components 0 occurrences in the built packages/components/dist/index.d.ts and 0 in dist/index.js; the only register* name it exports is registerPlaceholders
the package registers by import side effect instead sideEffects: true in its manifest; import './renderers'; in src/index.ts under the comment Register all ObjectUI renderers (side-effects); 114 register( call sites in the built dist/index.js, at module scope (e.g. n.register("div", …) at column 0 of the ESM bundle)
the template also emits ObjectRenderer it does not. The only other import is SchemaRenderer from @object-ui/react, which is a real export and type-checks. The ObjectRenderer row of the ruling has no site here

Same spelling the root README landed for objectui#7417.

The reading that matters: the OUTPUT, before and after

The package's own type-check cannot see inside the template, so it is not evidence.
What is: taking the file a user would actually receive and compiling it.

generateReactComponent() is module-private and this change may not export it, so
the harness brace-matches the function out of the source file on disk, strips
the two type annotations from its signature, evaluates it, and calls it with a
representative page schema. The emitted file is then compiled against the built
dist/index.d.ts of @object-ui/react and @object-ui/components
(jsx: react-jsx, strict: true, skipLibCheck).

Beforetsc exit 2, one diagnostic, and it is exactly the defect:

Generated.before.tsx(3,10): error TS2305: Module '"@object-ui/components"' has no exported member 'registerDefaultRenderers'.

After, at bf51ca4eftsc exit 0, no output. The generated preamble is now:

import React from 'react';
import { SchemaRenderer } from '@object-ui/react';
// Importing the package registers every default renderer as a side effect —
// there is no separate registration call.
import '@object-ui/components';

End to end: the built dist/extension.js that goes into the .vsix carries
0 occurrences of registerDefaultRenderers and one import '@object-ui/components';.

Ships with it: DESIGN.md

packages/vscode-extension/DESIGN.md documented the identical two lines. It moves
in the same commit, so the design record does not freeze the defect the moment this
lands.

A pin, and what it cost

packages/vscode-extension/src/__tests__/export-to-react-preamble.test.ts reads the
template out of the source and asserts three things: the identifier appears nowhere
in the module; the template imports @object-ui/components for its side effect and
carries no named import from that specifier at all (the shape, so the next
phantom off that specifier fails here too); and the SchemaRenderer import it must
keep is still there.

It is a source-text pin, deliberately. Compiling the emitted output under test
would require exporting generateReactComponent(), and this card's clause 2 is
no — no new export, no signature change. So it can catch the return of this
phantom and the loss of its replacement; it cannot catch a new one. The
stronger instrument is a proposal in the report, not built here.

Ablation, run against the committed fix. Restoring the pre-fix
extension.ts from 3faaa7d0f turned 2 of the 3 assertions red (the third,
which pins the untouched SchemaRenderer line, correctly stayed green); the
mutation was confirmed on disk before the run (registerDefaultRenderers count
2, side-effect-import count 0) and the restore was verified by git diff HEAD
being empty and git hash-object reproducing the HEAD blob
e52b0abc15a2d84305e2ab8080819e8e15029139. No build is involved on either leg —
the pin reads source text, not dist.

The pin's cost is the second commit: scripts/check-type-check-coverage.mjs exited
1 on it, because tsconfig.json is the build program and correctly excludes
**/*.test.ts, so nothing compiled the new test — the same "verifier not bound to
the thing verified" shape this card is about. A tsconfig.test.json in the shape 35
sibling packages already use, chained off the package's type-check script, closes
it; tsc -p tsconfig.test.json --listFiles confirms the project reads the pin, and
the gate now reports 42/42 packages compiling their tests.

Gates, all at bf51ca4ef, exit code captured by redirecting before any pipe

gate exit note
object-ui type-check 0 echoed tsc --noEmit && tsc -p tsconfig.test.json — both legs
object-ui test 0 Test Files 1 passed (1) · Tests 3 passed (3)
object-ui lint 0 13 problems (0 errors, 13 warnings), unchanged from before this PR; the new test file is linted (eslint JSON reports it, 0/0) and not ignored
object-ui build 0 tsup CJS build success
check-changeset-presence 0 verbatim: 2 source file(s) of 1 released package(s) changed, and this change declares 1 changeset(s): .changeset/7837-vscode-export-react-side-effect-import.md.
check-changeset-fixed 0
check-changeset-no-major 0
check-type-check-coverage 0 42/42 packages compile their tests
check-lint-coverage 0 46/46 packages linted
check-control-bytes 0 plus a direct grep -naP control-byte scan of all six changed files: clean
check-unreferenced-sources 0
check-vi-mock-specifiers / check-vi-mock-inherit 0
check-doc-fence-languages 0
check-package-self-import 0
check-published-tsconfig-tooling-exclude 0
check-entry-guard 0
check-shell-escape-residue 0
check-phantom-dependencies 0
check-governed-queue-guard --test 0 NOT GOVERNED — 6 path(s) checked against 5 governed surface(s); none matched.

A changeset is owed here, against the card's expectation

The dispatch expected none, since packages/vscode-extension is private: true.
The gate disagreed and it is right: object-ui is private because it ships to the
VS Code marketplace rather than npm, but it sits in the changesets fixed group
with privatePackages.version: true, so changesets versions it and writes its
CHANGELOG. Its first run said so verbatim:

❌  1 source file(s) of 1 released package(s) changed, and this change adds no changeset:
      object-ui
          packages/vscode-extension/src/extension.ts

.changeset/7837-vscode-export-react-side-effect-import.md declares patch,
never major per the fixed-group rule.

Not run, stated plainly

  • The whole gate farm. CI runs it regardless; the set above is what this diff's
    paths derive to plus everything cheap that plausibly reads them.
  • pnpm lint / turbo run lint across all 46 packages and every other package's
    test suite. This diff does not leave packages/vscode-extension, and that
    package's own lint and test are its complete population. eslint.config.js
    declares no project / projectService, so no rule reads across files and this
    diff cannot move an untouched file's verdict.
  • check-published-dist-tooling — it needs a full repo build, and it skips
    private packages by construction (manifest.private === true is checked before
    release-group membership), so object-ui is outside it either way.
  • The extension in a running VS Code instance. No headless VS Code host here;
    the end-to-end reading substituted for it is compiling the command's own output,
    above.

Out of scope, filed rather than corrected in passing

objectui#7862 — the same generated file opens with import React from 'react';
and its only JSX is a single SchemaRenderer element (written in words: GitHub
deletes tag-shaped fragments from PR bodies, code fences included). Under
jsx: react-jsx the identifier is never read, so the same output compiled with
noUnusedLocals: true added is tsc exit 2 with
TS6133: 'React' is declared but its value is never read. Different defect class
from this card — a real import that is unused, not a symbol that does not exist —
and correcting it is a decision about classic-transform users, so it is filed with
no assignee.

Also reported to the PM seat rather than filed, because the dispatch asked for the
reading and a proposal only: packages/vscode-extension had zero test files
before this PR
, and its test script is
vitest run --passWithNoTests, so it was structurally green with nothing to run.


🤖 Generated with Claude Code

https://claude.ai/code/session_01KbJQ1y1J12nZxYzFWhP8Q3


Generated by Claude Code

…every generated file (objectui#7837)

`generateReactComponent()` — the body of the `Export to React` command — wrote
`import { registerDefaultRenderers } from '@object-ui/components';` plus a call
to it into every file a user received. That symbol is on no export of that
package: its built `dist/index.d.ts` carries exactly one `register*` name,
`registerPlaceholders`, and `registerDefaultRenderers` occurs 0 times in either
`dist/index.d.ts` or `dist/index.js` (measured at 3faaa7d). So the command's
output failed to compile with TS2305 naming a symbol the user never typed.

The package registers its renderers as an import side effect — `sideEffects:
true`, `import './renderers'` in the barrel, 114 module-scope `register(` call
sites in the built `dist/index.js` — so there is no registration function to
call. The preamble now imports the package for the side effect and says so, the
spelling the root README landed for objectui#7417.

`DESIGN.md`, which documented the identical two lines, moves in the same commit
so the design record does not freeze the defect.

A pin on the template's own text is added, because the preamble lives inside a
template literal where `tsc --noEmit` sees only a string and no doc gate's
surface reaches this file. It asserts source text rather than compiling the
output: compiling would require exporting `generateReactComponent()`, which this
change is not allowed to do.

No export added, no signature changed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KbJQ1y1J12nZxYzFWhP8Q3
…tui#7837)

`scripts/check-type-check-coverage.mjs` exited 1 on the pin added in the
previous commit: `packages/vscode-extension/tsconfig.json` is the BUILD program
and correctly excludes `**/*.test.ts`, so nothing compiled the test — and an
unchecked test can assert a contract the compiler never checked and then read as
evidence that the contract holds, which is the same defect class this card is
about.

Adds `tsconfig.test.json` in the shape 35 sibling packages already use, chained
off the package's `type-check` script as the gate requires. Its `module` /
`moduleResolution` are ESNext/Bundler rather than the build's node16 CommonJS
because that is the program vitest actually runs these files in — `import.meta.url`
is legal there and TS1470 under the build's target. `include` and `exclude` are
overridden rather than merged, so the project is not left green while reading
nothing.

Verified with `tsc -p tsconfig.test.json --listFiles`: the project reads the pin.
`check-type-check-coverage` now reports 42/42 packages compiling their tests.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KbJQ1y1J12nZxYzFWhP8Q3
@github-actions

github-actions Bot commented Sep 6, 2026

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 50 chunks) 3187.4 KB 3191.4 KB
Main entry chunk (gzip) 143.2 KB 350 KB
Entry file index-DRjAR2je.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) 15.67KB 5.75KB
app-shell (runtime-config.js) 20.68KB 7.36KB
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) 510.60KB 116.20KB
core (index.js) 6.96KB 2.79KB
create-plugin (index.js) 10.08KB 3.26KB
data-objectstack (index.js) 182.08KB 50.62KB
fields (index.js) 242.44KB 61.25KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (builtinAggregateLabels.js) 0.86KB 0.49KB
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.65KB 1.47KB
i18n (pickLocalized.js) 7.62KB 3.26KB
i18n (provider.js) 26.89KB 9.04KB
i18n (useDisplayLocale.js) 2.85KB 1.45KB
i18n (useObjectLabel.js) 34.34KB 9.17KB
i18n (useSafeTranslation.js) 5.60KB 2.33KB
layout (index.js) 38.98KB 10.98KB
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.99KB 0.87KB
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 (useSpecGesture.js) 4.39KB 1.66KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 11.71KB 4.29KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 6.24KB 2.16KB
permissions (discardProofCache.js) 1.04KB 0.55KB
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) 4.83KB 2.27KB
plugin-ai (index.js) 15.75KB 3.80KB
plugin-calendar (index.js) 47.87KB 13.31KB
plugin-charts (index.js) 70.92KB 19.75KB
plugin-chatbot (index.js) 196.19KB 46.37KB
plugin-dashboard (index.js) 132.88KB 34.69KB
plugin-designer (index.js) 212.86KB 43.19KB
plugin-detail (index.js) 250.55KB 64.06KB
plugin-editor (index.js) 2.46KB 1.10KB
plugin-form (index.js) 132.87KB 32.66KB
plugin-gantt (index.js) 167.26KB 41.00KB
plugin-grid (index.js) 209.29KB 56.78KB
plugin-kanban (index.js) 52.71KB 14.55KB
plugin-list (index.js) 113.28KB 27.59KB
plugin-map (index.js) 20.44KB 6.78KB
plugin-markdown (index.js) 13.93KB 4.81KB
plugin-report (index.js) 43.59KB 11.97KB
plugin-timeline (index.js) 30.84KB 8.85KB
plugin-tree (index.js) 9.20KB 3.19KB
plugin-view (index.js) 85.24KB 20.94KB
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) 81.07KB 26.86KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 4.63KB 2.18KB
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) 20.57KB 5.88KB
sdui-parser (provenance.js) 3.66KB 1.82KB
sdui-parser (types.js) 0.28KB 0.23KB
sdui-parser (validate.js) 10.35KB 3.60KB
types (ai.js) 0.20KB 0.17KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 2.87KB 1.00KB
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) 3.75KB 1.85KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (designer.js) 1.85KB 0.85KB
types (disclosure.js) 0.20KB 0.18KB
types (error-code.js) 1.54KB 0.88KB
types (expression.js) 0.20KB 0.18KB
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.74KB 2.25KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 4.73KB 2.28KB
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 (select-option.js) 0.20KB 0.19KB
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) 8.11KB 3.32KB
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

Projects

None yet

2 participants