Skip to content

fix(cli): make the generated temp app pass its own strict tsconfig, and gate it with real tsc (#3853) - #4113

Merged
yinlianghui merged 1 commit into
mainfrom
claude/issue-3853-generated-app-tsc
Aug 10, 2026
Merged

yinlianghui merged 1 commit into
mainfrom
claude/issue-3853-generated-app-tsc

Conversation

@yinlianghui

Copy link
Copy Markdown
Collaborator

Fixes #3853

Both app generators in packages/cli/src/utils/app-generator.ts write a tsconfig.json carrying strict, noUnusedLocals and noUnusedParameters, and until this PR nothing had ever run it — dev/serve/build all go through Vite, which transpiles without type-checking, and the config itself says noEmit. Declared strictness, never enforced.

Premise check (current origin/main @ 877385a76)

Reproduced by generating with the real entry points and running the workspace's tsc -p with the generated tsconfig.json. The issue's table holds verbatim, and measuring found 17 errors rather than 12:

class code count notes
unused imports TS6133 5 exactly as reported
untyped destructured props TS7031 4 exactly as reported
untyped map callbacks TS7006 5 issue lists 3; Layout.tsx(117,50) child and (117,57) cIdx were not measured
required-prop call sites TS2741 2 DynamicIcon invoked without the className its inferred type made required
missing ambient declaration TS2882 1 import './index.css' in the entry — and this one hits both generators, not just the routed one

#4099 changed template dep ranges since filing; as expected it moved none of these — measured, not assumed.

The theme-provider.tsx UMD note is 待核 → not reproducible, and no fix was made

React.ReactNode without a React namespace import produces no diagnostic. TypeScript permits a UMD global namespace in a type position; allowUmdGlobalAccess governs value access, which this file never does. Verified that the namespace really resolves rather than silently degrading — a probe referencing a bogus member of the same namespace reports TS2694: Namespace 'React' has no exported member …, and assigning a symbol to React.ReactNode reports TS2322. So the type is being checked, and it is correct. Reported rather than "fixed".

The fix, at the template source

  • dropped the five imports declared and never used: Link in src/App.tsx (the only link in the app is in src/Layout.tsx, which imports it itself), and cn / Button / SidebarGroupContent / SidebarGroupLabel in src/Layout.tsx;
  • typed DynamicIcon's and AppLayout's destructured props. The five TS7006 callbacks then follow by inference from the AppConfig / MenuItem shapes rather than needing annotations of their own, and making className optional retires the two TS2741 call sites;
  • narrowed the Lucide namespace to the component-by-name shape the layout actually indexes, which retires the @ts-expect-error as well. No any was added anywhere;
  • added src/vite-env.d.ts (/// reference types="vite/client") to both generators. vite is already in the generated devDependencies, and this is the file create-vite's own React+TS template writes for exactly this reason.

AppConfig is applied to a variable, not to a fresh object literal, so a user app.json carrying extra keys is unaffected by excess-property checking.

The gate

Three real tsc -p runs over apps the generators really write, in packages/cli's own suite, spawning the workspace's TypeScript (createRequire(...).resolve('typescript/bin/tsc'), resolved from this package — never a global).

Cost, measured: routed 1012ms + plain 770ms + self-test 981ms = 2.76s; the whole app-generator.test.ts file runs in 3.19s and the packages/cli suite in ~22s.

Module resolution. The temp app is generated under the repo root inside .objectui-tmp/ — which is what commands/dev.ts itself does ("always in cwd to keep node_modules access"), is already gitignored, and is what makes react, react-dom, react-router-dom, @types/react and vite/client resolve from the root node_modules the way they do for the real command. This is the established mechanism, not a new linker: nothing is installed or symlinked. Under os.tmpdir() nothing resolves at all and React.ReactNode degrades into Cannot find namespace 'React' — a diagnostic about the test's setup wearing the costume of a defect in the generated source.

What is exempt, and why it is not a hole. @object-ui/* and lucide-react genuinely do not resolve here, because nothing installs the temp app's dependencies (the generation onto disk block says why an install would prove nothing). Those are exempt — but only when the specifier is bare, and only after each one is checked against the manifest the generator itself writes. A relative specifier is never exempt, which is what keeps ./index.css, ./theme-provider and ./Layout under the gate. That partition is the issue's own: its table is the errors "unrelated to whether @object-ui/* is installed".

Reverse verification, direction predicted first. Reverting app-generator.ts to origin/main and re-running turns both gates red with the full pre-fix inventory — TS6133 ×5, TS7031 ×4, TS7006 ×5, TS2741 ×2, TS2882 ×1 on the routed app, and TS2882 on the plain one. One diagnostic differs by environment and is worth naming: at repo root the pre-fix @ts-expect-error reports TS2578: Unused '@ts-expect-error' directive, because unresolved lucide-react makes the indexed expression any and there is nothing left to suppress; with lucide-react resolvable (probed by generating under packages/components/) the directive is used instead and TS2578 does not appear. The fix removes the directive, so the gate is stable either way.

The in-suite self-test plants the pre-fix templates back and pins the resulting codes. Its predicted set was right in 5 of 6 codes; the miss is recorded in the test rather than papered over: an unused import type binding reports TS6196 ("declared but never used"), not TS6133 ("its value is never read"), because a type-only binding is not a value.

Reachability gate. It gains exactly one exemption, .d.ts: an ambient declaration is pulled into the program by the tsconfig's include, never by the module graph, so reachability-from-main.tsx was the wrong measure for it. The exemption's width is pinned in both directions by a new test (src/vite-env.d.ts exempt, a src/vite-env.ts orphan beside it still reported), and it opens no hole because the tsc gate goes red the moment that file stops carrying its weight.

Verification

  • vitest run packages/cli/ from the repo root: 101 passed (4 files), 22s.
  • tsc --noEmit in packages/cli: exit 0.
  • eslint packages/cli/src: 0 errors (15 warnings, all pre-existing on untouched lines).
  • node scripts/check-changeset-presence.mjs and check-changeset-no-major.mjs: both green.

Changeset: patch on @object-ui/cli — template source changes ship inside the released package, and the Bump Policy forbids major in this fixed group.

Out of scope

Filed as #4111: commands/init.ts is a third generator with its own tsconfig object, also strict + noUnusedLocals and also lacking a vite-env.d.ts, so its src/main.tsx has the same ./index.css TS2882 (measured, not inferred). It is outside this card's two generators, so it is filed rather than fixed here — but it is more severe than the defect this PR fixes: that scaffold's generated package.json runs build: "tsc && vite build", so the error is live for any user who runs objectui init followed by npm run build.


Generated by Claude Code

…nd gate it with real tsc (#3853)

Both app generators write a tsconfig.json carrying strict, noUnusedLocals and
noUnusedParameters, and nothing had ever run it: dev/serve/build all go through
Vite, which transpiles without checking, and the config itself says noEmit. The
strictness was declared and never enforced, and the generated sources had
drifted 17 errors past it — the issue's table is 12 of them.

Fixed at the templates:

- dropped the five imports declared and never used (TS6133): `Link` in
  src/App.tsx, and `cn` / `Button` / `SidebarGroupContent` /
  `SidebarGroupLabel` in src/Layout.tsx;
- typed DynamicIcon's and AppLayout's destructured props (TS7031). The five
  map-callback parameters (TS7006) follow by inference from the AppConfig /
  MenuItem shapes rather than needing annotations of their own, and making
  `className` optional retires the two TS2741 call sites the issue had not
  measured;
- narrowed the Lucide namespace to the component-by-name shape the layout
  actually indexes, which retires the `@ts-expect-error` without adding `any`;
- added src/vite-env.d.ts to BOTH generators — without `vite/client`'s ambient
  declarations the entry's `import './index.css'` has nothing behind it (TS2882),
  which is the one defect of the set that was never routed-specific.

The gate: three real `tsc -p` runs over apps the generators really write
(2.76s total, measured). Bare-specifier resolution failures are exempt, since
nothing installs the temp app's dependencies here, but only after being checked
against the manifest the generator itself writes — so the exemption cannot cover
a typo or an undeclared import, and relative specifiers are never exempt. Paired
with a self-test that plants the pre-fix templates back and pins the codes.

The reachability gate gains one exemption, `.d.ts`: an ambient declaration is
reached through the tsconfig's `include`, not the module graph. The tsc gate is
what proves the exempted file carries its weight.

The issue's 待核 note about `React.ReactNode` in src/theme-provider.tsx is NOT a
defect: TypeScript allows a UMD global namespace in a TYPE position without
allowUmdGlobalAccess (that flag governs value access). Verified against real
@types/react — a bogus member of the same namespace reports TS2694, so the
namespace really is resolving.
@vercel

vercel Bot commented Aug 10, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
objectui Ignored Ignored Aug 10, 2026 10:46am

Request Review

@github-actions github-actions Bot added the tests label Aug 10, 2026
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Main entry (gzip) 28.3 KB 350 KB
Entry file index-BG4s-BOk.js —
Status PASS —

📦 Bundle Size Report

Package Size Gzipped
app-shell (index.js) 8.66KB 3.13KB
app-shell (runtime-config.js) 7.42KB 2.32KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 7.57KB 2.97KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 1.17KB 0.53KB
auth (AuthProvider.js) 22.10KB 4.37KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 18.13KB 5.39KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.64KB 2.21KB
auth (SocialSignInButtons.js) 9.60KB 3.89KB
auth (UserMenu.js) 3.40KB 1.22KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 35.76KB 9.11KB
auth (createAuthenticatedFetch.js) 4.37KB 1.69KB
auth (index.js) 2.35KB 1.07KB
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) 4.91KB 0.87KB
auth (useIsWorkspaceAdmin.js) 1.61KB 0.85KB
collaboration (CommentThread.js) 26.07KB 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.65KB 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) 484.15KB 106.78KB
core (index.js) 3.04KB 1.15KB
create-plugin (index.js) 10.08KB 3.26KB
data-objectstack (index.js) 140.66KB 36.25KB
fields (index.js) 229.40KB 56.93KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (currency.js) 1.22KB 0.64KB
i18n (i18n.js) 4.32KB 1.77KB
i18n (index.js) 2.65KB 1.06KB
i18n (pickLocalized.js) 1.70KB 0.83KB
i18n (provider.js) 9.48KB 3.27KB
i18n (useObjectLabel.js) 27.59KB 6.63KB
i18n (useSafeTranslation.js) 4.52KB 1.96KB
layout (index.js) 38.87KB 10.80KB
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.74KB
mobile (index.js) 1.50KB 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.71KB 0.42KB
mobile (useResponsiveConfig.js) 1.36KB 0.63KB
mobile (useSpecGesture.js) 4.32KB 1.64KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 8.75KB 3.06KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 3.67KB 1.12KB
permissions (evaluator.js) 4.41KB 1.44KB
permissions (index.js) 0.91KB 0.41KB
permissions (store.js) 0.91KB 0.42KB
permissions (useFieldPermissions.js) 1.28KB 0.52KB
permissions (usePermissions.js) 1.55KB 0.71KB
plugin-ai (index.js) 15.71KB 3.79KB
plugin-calendar (index.js) 45.23KB 12.45KB
plugin-charts (index.js) 61.49KB 17.48KB
plugin-chatbot (index.js) 180.33KB 42.79KB
plugin-dashboard (index.js) 118.50KB 30.66KB
plugin-designer (index.js) 210.51KB 42.51KB
plugin-detail (index.js) 237.80KB 59.48KB
plugin-editor (index.js) 2.46KB 1.10KB
plugin-form (index.js) 114.58KB 27.68KB
plugin-gantt (index.js) 162.81KB 39.67KB
plugin-grid (index.js) 188.04KB 49.91KB
plugin-kanban (index.js) 48.60KB 13.41KB
plugin-list (index.js) 110.04KB 26.67KB
plugin-map (index.js) 17.00KB 5.32KB
plugin-markdown (index.js) 13.72KB 4.69KB
plugin-report (index.js) 40.58KB 10.58KB
plugin-timeline (index.js) 26.21KB 7.52KB
plugin-tree (index.js) 8.50KB 2.88KB
plugin-view (index.js) 84.03KB 20.55KB
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.71KB 3.53KB
providers (index.js) 0.44KB 0.22KB
providers (types.js) 0.01KB 0.04KB
react-runtime (index.js) 5.67KB 2.37KB
react (LazyPluginLoader.js) 3.77KB 1.33KB
react (SchemaRenderer.js) 23.71KB 7.95KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 1.23KB 0.66KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (codegen.js) 4.09KB 1.74KB
sdui-parser (index.js) 4.47KB 2.03KB
sdui-parser (parse.js) 10.04KB 2.82KB
sdui-parser (types.js) 0.29KB 0.24KB
sdui-parser (validate.js) 4.69KB 1.48KB
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 (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-retry.js) 4.32KB 2.02KB
types (index.js) 2.71KB 1.34KB
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 (system-fields.js) 3.33KB 1.54KB
types (theme.js) 0.20KB 0.18KB
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

2 participants