Skip to content

fix(build): make dts type errors fail the build instead of exiting 0 - #5482

Merged
os-support-ai merged 3 commits into
mainfrom
claude/issue-5370-dts-build-exit-code
Aug 21, 2026
Merged

fix(build): make dts type errors fail the build instead of exiting 0#5482
os-support-ai merged 3 commits into
mainfrom
claude/issue-5370-dts-build-exit-code

Conversation

@os-support-ai

Copy link
Copy Markdown
Collaborator

Part of #5370.

The mechanism lands here and @object-ui/layout — the package the card measured — is wired to
it. The other 20 packages that still exit 0 on a dts type error each need a one-line spread in
their own vite.config.ts, which is outside this PR's declared file surface (the shared
vite/dts config + packages/layout). Deliberately not Fixes, so merging this does not close
a card that is only one package deep. The width reading and the follow-up are below.

Before / after, measured

Both readings are ${PIPESTATUS[0]} — pnpm's own exit code, never a pipe's.

Beforeorigin/main 478ec54ce, plus one deliberate TS2353 in
packages/layout/src/AppSchemaRenderer.tsx (the card's own injection: a logo key on an object
literal typed AppShellBranding):

src/AppSchemaRenderer.tsx:581:5 - error TS2353: Object literal may only specify known
  properties, and 'logo' does not exist in type 'AppShellBranding'.
[unplugin:dts] Declaration files built in 4349ms.
✓ built in 4.62s
BUILD_EXIT=0

Control on that same tree — the error IS fatal where a gate looks at it:
pnpm --filter @object-ui/layout type-check → exit 2.

After — this branch, same injected error, same command:

src/AppSchemaRenderer.tsx:581:5 - error TS2353: Object literal may only specify known
  properties, and 'logo' does not exist in type 'AppShellBranding'.
Error: [dts-fail-on-type-errors] the declaration build of `/…/packages/layout` reported
1 type error, so it must not exit 0:
  src/AppSchemaRenderer.tsx(581,5): error TS2353: Object literal may only specify known
    properties, and 'logo' does not exist in type 'AppShellBranding'.

The declaration emit was not attempted. Fix the errors, or run
`pnpm --filter PACKAGE type-check` for the full report.
 ERR_PNPM_RECURSIVE_RUN_FIRST_FAIL  @object-ui/layout@17.6.0 build: `vite build`
BUILD_EXIT=1

Unmodified source with the fix wired: BUILD_EXIT=0, ✓ built in 5.03s — unchanged.

The failure message repeats the diagnostics on purpose. The plugin logs them once, at program
creation, hundreds of lines above the failure, and a | tail scrolling them out of view is
half of what the card is about.

Blast radius — nothing currently red, measured rather than assumed

22 packages build their typings through vite-plugin-dts. On unmodified 478ec54ce, every
one of them was built with pnpm --filter PACKAGE build and its log scanned (ANSI stripped) for
TypeScript diagnostics:

22 / 22 exit 0 with zero diagnostics of any category. So making dts errors fatal reds
nothing that is green today — for the one package wired here, and for the 20 that a follow-up
would wire.

Every one of those 22 zeros is counter-probed rather than trusted: a second sweep appended one
deliberate TS2322 to each package's entry file and rebuilt. All 22 reported it, so no zero
above is the vacuous kind where a package's dts program never looked at its own src.

That second sweep is also the width reading, in one table (exit code with the probe in place):

packages exit why
@object-ui/layout 1 wired to the shared factory by this PR
@object-ui/fields 2 already honest, but by a different mechanism — its build script is tsc && vite build && …, so the leading tsc fails first and the dts leg never runs
the other 20 (components, plugin-ai, plugin-calendar, plugin-charts, plugin-chatbot, plugin-dashboard, plugin-designer, plugin-detail, plugin-editor, plugin-form, plugin-gantt, plugin-grid, plugin-kanban, plugin-list, plugin-map, plugin-markdown, plugin-report, plugin-timeline, plugin-tree, plugin-view) 0 still print the error and exit 0

Reach — what "the shared vite/dts config" actually is today

Measured, not assumed from the filter list: there is no shared vite config in this repo. Each
of the 22 packages calls dts({ … }) in its own vite.config.ts. The only shared dts module is
scripts/vite-dts-explicit-extensions.ts, imported by exactly 2 of them (components,
layout). turbo.json already treats $TURBO_ROOT$/scripts/vite-*.ts as a build input, so a
new module there invalidates the right caches on its own.

So "fix it once in the shared config" can only mean: implement once under scripts/, then spread
it at each call site. This PR does the first half and one call site. Wiring the remaining 20 is
mechanical and measured safe, but it is 20 files this seat was not given, and a gate that keeps
them wired (so a new package cannot land unwired) is a further step — filed separately.

Why afterDiagnostic, and what it does not reach

vite-plugin-dts documents afterDiagnostic for exactly this ("according to the
diagnostics.length, you can judge whether there is any type error"). It is also the only hook
the two shared factories do not both want: this one returns afterDiagnostic and nothing else,
so it spreads beside createDtsExplicitExtensions's beforeWriteFile / afterBuild with no key
collision — a duplicate key in an object literal keeps the last one and reports nothing. A test
pins that disjointness so a hook added to either factory later goes red at the pin instead of
silently at the call site.

Errors only (ts.DiagnosticCategory.Error); warnings and suggestions stay non-fatal. Diagnostics
raised by the emit itself (the plugin's emitSkipped path, appended after this hook has run) are
outside its reach and the module header says so.

Reverse-verification

Predictions recorded before running; both matched.

  1. Unwire the factory at the layout call site, keep the injected TS2353. Predicted: the
    card's exact defect returns — BUILD_EXIT=0, plugin still prints TS2353, no
    [dts-fail-on-type-errors] line. Observed: exactly that (0 / printed / 0 hook lines).
    Restored: BUILD_EXIT=1 again.
  2. Make the fatal-diagnostic predicate select nothing and run the pin test. Predicted: red,
    4 failed / 3 passed (cases 1, 3, 4, 5 depend on it; 2, 6, 7 do not). Observed: Tests 4 failed | 3 passed (7). Restored: 7 passed.

No rebuild leg is involved in either: both the vite config and the test import the subject
through a relative source specifier (../../scripts/vite-dts-fail-on-type-errors.ts), not
through a package's exports into a dist/, so there is no stale-artifact path that could keep
an ablation alive. Ablation 1 changing behaviour is itself the proof of that.

Tree clean after both (git status --porcelain empty).

Gates run locally, on a15034dff

pnpm type-check:scripts · node scripts/check-type-check-coverage.mjs ·
node scripts/check-lint-coverage.mjs · pnpm check:control-bytes ·
pnpm exec eslint on both new files · pnpm --filter @object-ui/layout lint ·
pnpm --filter @object-ui/layout type-check · pnpm --filter @object-ui/layout build ·
node scripts/check-changeset-presence.mjs · check-changeset-no-major.mjs ·
check-changeset-fixed.mjs · pnpm exec vitest run scripts/__tests__/… (25 passed).
All exit 0.

Changeset is an empty frontmatter: build tooling only, no package src/ touched, nothing to
publish — declared explicitly rather than left undeclared.

No test was skipped, disabled or quarantined, and the type-check leg is untouched.


Generated by Claude Code

claude added 3 commits August 21, 2026 02:13
`vite-plugin-dts` builds a full type program, computes its diagnostics,
prints them, and lets the build finish green. Measured on
`@object-ui/layout` at `478ec54ce` with one deliberate TS2353: the error
is in the log, `vite build` exits 0, and anything branching on that exit
code reads the package as clean.

`scripts/vite-dts-fail-on-type-errors.ts` turns the diagnostics the build
already has into the exit code it already implies, via the plugin's
documented `afterDiagnostic` hook. Errors only; warnings stay non-fatal.
The thrown message quotes the diagnostics because the plugin logged them
hundreds of lines earlier, which is the defect this is about.

Wired into `packages/layout` beside the existing explicit-extensions
factory; the two return disjoint hooks so neither spread can overwrite
the other.

Part of #5370
Feeds the subject diagnostics from a real `ts.createProgram` rather than
hand-rolled objects, so the predicate is pinned against the shape the
plugin actually hands it. Also pins that the two dts factories share no
hook name — `packages/layout/vite.config.ts` spreads both into one object
literal, where a duplicate key silently keeps the last one.

Part of #5370
Build tooling only; no package src/ changes and nothing to publish.

Part of #5370
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 58 chunks) 3792.5 KB 3867.2 KB
Main entry chunk (gzip) 25.3 KB 350 KB
Entry file index-yF0I_KUa.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 (index.js) 10.04KB 3.72KB
app-shell (runtime-config.js) 7.42KB 2.32KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 10.06KB 3.86KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 1.17KB 0.53KB
auth (AuthProvider.js) 29.34KB 7.05KB
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) 6.35KB 2.43KB
auth (index.js) 2.77KB 1.22KB
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.02KB 0.89KB
auth (useIsWorkspaceAdmin.js) 3.04KB 1.45KB
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.94KB 113.63KB
core (index.js) 4.11KB 1.62KB
create-plugin (index.js) 10.08KB 3.26KB
data-objectstack (index.js) 159.80KB 44.34KB
fields (index.js) 237.21KB 59.50KB
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.22KB 3.08KB
i18n (provider.js) 23.13KB 7.63KB
i18n (useDisplayLocale.js) 2.85KB 1.45KB
i18n (useObjectLabel.js) 30.51KB 7.57KB
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.35KB 3.31KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 4.42KB 1.42KB
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.81KB 0.83KB
plugin-ai (index.js) 15.75KB 3.80KB
plugin-calendar (index.js) 46.62KB 12.83KB
plugin-charts (index.js) 64.75KB 18.37KB
plugin-chatbot (index.js) 181.21KB 43.14KB
plugin-dashboard (index.js) 128.51KB 32.94KB
plugin-designer (index.js) 212.39KB 42.83KB
plugin-detail (index.js) 242.15KB 60.89KB
plugin-editor (index.js) 2.46KB 1.10KB
plugin-form (index.js) 124.40KB 30.26KB
plugin-gantt (index.js) 164.10KB 39.87KB
plugin-grid (index.js) 200.75KB 54.24KB
plugin-kanban (index.js) 52.93KB 14.60KB
plugin-list (index.js) 111.64KB 27.13KB
plugin-map (index.js) 20.08KB 6.62KB
plugin-markdown (index.js) 13.72KB 4.69KB
plugin-report (index.js) 43.49KB 11.93KB
plugin-timeline (index.js) 26.68KB 7.66KB
plugin-tree (index.js) 8.50KB 2.88KB
plugin-view (index.js) 84.52KB 20.67KB
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) 3.77KB 1.33KB
react (SchemaRenderer.js) 36.10KB 12.26KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 1.33KB 0.69KB
react (schema-input.js) 1.45KB 0.83KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (codegen.js) 5.41KB 2.34KB
sdui-parser (index.js) 4.77KB 2.16KB
sdui-parser (input-type.js) 2.84KB 1.40KB
sdui-parser (parse.js) 10.76KB 3.17KB
sdui-parser (provenance.js) 3.66KB 1.82KB
sdui-parser (types.js) 0.29KB 0.24KB
sdui-parser (validate.js) 6.92KB 2.40KB
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-retry.js) 4.32KB 2.02KB
types (index.js) 3.08KB 1.53KB
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

@os-support-ai
os-support-ai marked this pull request as ready for review August 21, 2026 02:54
@os-support-ai
os-support-ai added this pull request to the merge queue Aug 21, 2026
Merged via the queue into main with commit 70bb68e Aug 21, 2026
23 checks passed
@os-support-ai
os-support-ai deleted the claude/issue-5370-dts-build-exit-code branch August 21, 2026 02:54
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.

2 participants