test(ci): a dist vitest project so built-artifact claims can be pinned - #7291

Merged
yinlianghui merged 3 commits into
mainfrom
claude/issue-7183-dist-vitest-project
Sep 2, 2026
Merged

test(ci): a dist vitest project so built-artifact claims can be pinned#7291
yinlianghui merged 3 commits into
mainfrom
claude/issue-7183-dist-vitest-project

Conversation

@yinlianghui

@yinlianghuiyinlianghui commented Sep 2, 2026

Copy link
Copy Markdown
Collaborator

Fixes#7183

Implements the PM ruling on this card (2026-09-02, option 1): a dedicated dist vitest project with its own turbo task that builds the package under test first.

The gap

The root vitest.config.mts aliases every workspace package to its src. That is right for the ~2000 tests that want fast source feedback, and it makes "does the SHIPPED BUNDLE still do X" structurally unanswerable. Such a test could not be committed either: turbo's test task is dependsOn: ["^build"] — the DEPENDENCIES' builds, never the package's own — so a dist-importing test landed in CI with no dist to import. That is NOT MEASURED rather than a red pin, and the usual repair (delete it, or let it skip when dist is missing) leaves a green suite that measures nothing. Two lanes hit this wall in one morning and each threw a correct measurement away.

The five binding constraints, and where each is met

1. Only built-artifact pins; first resident re-derived from PR #7180; no source-resolved test moves in.
The project collects exactly one glob, packages/*/src/**/*.dist.spec.tsx (spelled in the config, not here — a star-slash pair inside a block comment ends the comment, which this PR learned the hard way, see Corrections below). Its only resident is packages/components/src/__tests__/page-header-action-ids.dist.spec.tsx: an id-authored page:header resolves an action whose definition carries a script body, and the marker reaches neither the rendered DOM nor the authored node. Nothing was moved into the project; no existing test changed.

2. dependsOn: ["build"] for the package under test (self, not ^build), and a loud precondition — not a skip — when dist is absent.
turbo.json gains test:dist with dependsOn: ["build"] and cache: false. The precondition is an explicit assertion that reads the built entry from the package's own package.jsonexports["."].import and names the absolute path when it is missing. It fails; it does not skip.

3. The live control from PR #7180 is part of the delivery.
registers page:header from the BUILT bundle is a bare toBeTruthy() on purpose — a custom message would change the string the control is recorded by. See verdict (iii).

4. Wired where the existing test job runs; no timeout raised, no heavy-test allowlist entry.
One step in the existing test job of .github/workflows/ci.yml, if: … && matrix.shard == 1. timeout-minutes: 20 is untouched, heavyDomTests is untouched, and the three existing projects (unit, dom, dom-heavy) are untouched.

5. The step-2 tripwire. Not tripped, but it was close, and the near-miss is the design: see The opt-in below.

The opt-in — the half that keeps pnpm test unchanged

CI's test job runs pnpm test (vitest run) with no build step anywhere in it. An unconditional fourth project is therefore collected by that run with no dist on disk, and its precondition would fail the whole suite on every PR — which would have forced a build into the path of all ~2000 tests, i.e. exactly the "change to how all tests build" constraint 5 says to stop at.

So the project is declared only when OBJECTUI_DIST_PINS=1. An env var rather than the --project dist flag, because argv is meaningful only in the process that parsed the CLI while the env var is inherited by everything Vitest spawns.

That opens exactly one false-green, and it is closed in the config rather than documented: vitest run --project dist without the env var would match no project, and passWithNoTests is true for a run that names no files — a green that measured nothing. Measured:

$ pnpm exec vitest run --project dist # no env var
exit=1
Error: vitest --project dist was requested, but OBJECTUI_DIST_PINS is not "1", so the
`dist` project is NOT declared and this run would collect ZERO files and exit GREEN.

The three verdicts

Each quotes the run's own output. Both mutating legs restore under a trap with absolute paths and prove the mutation landed on disk by marker count — never by an editor's exit code.

(i) dist present, run the way CI runs it — GREEN.

$ pnpm test:dist
V1_EXIT=0
Test Files 1 passed (1)
Tasks: 9 successful, 9 total

(ii) dist deleted — a loud, named precondition FAILURE, not a skip and not MODULE_NOT_FOUND.

MUTATION PROVEN: test -e /home/user/objectui-issue-7183/packages/components/dist => false
V2_EXIT=1
× precondition: the package under test has been built 9ms
AssertionError: The built entry /home/user/objectui-issue-7183/packages/components/dist/index.js
does not exist, so this built-artifact pin has NOTHING to measure. It must fail rather than
skip. […]: expected false to be true
Test Files 1 failed (1)
Tests 3 failed (3)

Restore proven by state, not by an exit code: sha256 of the restored dist/index.js equals the recorded 5365d829….

(iii) the dist import removed — the recorded control.

PRE target-count=1 PRE_HASH=1f531adf… HEAD_BLOB=1f531adf…
POST target-count=0 inject-count=1 POST_HASH=8b6277f1…
MUTATION PROVEN ON DISK (target 1->0, inject 0->1, blob hash moved)
V3_EXIT=1
× registers page:header from the BUILT bundle 6ms
AssertionError: expected undefined to be truthy
Test Files 1 failed (1)
Tests 2 failed | 1 passed (3)

expected undefined to be truthy — verbatim what PR #7180 recorded. The one case that still passes is the precondition, which is correct: dist is on disk in this leg; only the import is gone. Restore proven by state: restored blob 1f531adf… equals the HEAD blob, and git diff HEAD and git status --short are both empty.

Cost — measured, because the ruling priced this at "one build in one job"

A bare turbo run test:dist does not cost one build. Turbo applies a task definition to every package in scope and resolves dependsOn: ["build"] for each, so it schedules a build of the entire monoreposite and console included:

$ turbo run test:dist --dry=json → 45 tasks (44 builds + 1 test:dist)
$ turbo run test:dist --filter=@object-ui/components --dry=json
→ 9 tasks
components#build components#test:dist core#build data-objectstack#build
i18n#build react#build react-runtime#build sdui-parser#build types#build

The root script therefore carries --filter=@object-ui/components, and the 9 tasks are the package plus the dependency closure its .d.ts emit genuinely needs. Cold-cache wall time, which is what CI pays:

COLD_EXIT=0 COLD_WALL_SECONDS=72
Tasks: 9 successful, 9 total
Cached: 0 cached, 9 total
Time: 1m10.762s

~72 s added to shard 1 of a job whose ceiling is timeout-minutes: 20, unchanged. (Shared-box seconds: four sibling agents build in this container, so this is an upper-ish bound, not an idle-box figure.)

If a second package ever gains a pin without being added to the filter, the failure is the loud precondition naming its missing built entry — not a silent skip.

Collected by exactly one project

vitest list --filesOnly is the instrument, and it has a positive control:

unit: files=810 pin=0
dom: files=1429 pin=0
dom-heavy: files=34 pin=0
dist: [dist] packages/components/src/__tests__/page-header-action-ids.dist.spec.tsx

The suffix does this by construction — unit collects *.test.ts, dom collects *.test.tsx, dom-heavy is an explicit file list — so none of the three needed a character changed.

The same property keeps the file out of packages/components/tsconfig.test.json, and that is deliberate rather than incidental: turbo's type-check waits on ^build, so a type program that read this package's own dist would demand an artifact type-check is not allowed to wait for — the coupling #4801 removed. Measured with the package's own type program, with a control:

$ tsc -p tsconfig.test.json --listFiles
page-header-action-ids.dist.spec.tsx → 0 files (out of the program, as designed)
page-header-action-ids.test.tsx → 1 file (control: siblings ARE in it)

Gates — union re-run at commit 7a6eef7b9

GateVerdict line
vitest run scripts/exit 0 — Test Files 94 passed (94) / Tests 2645 passed (2645)
pnpm --filter @object-ui/components run type-checkexit 0
pnpm check:control-bytesexit 0 — "OK (scanned 6010 tracked text file(s); skipped 85 binary)"
pnpm type-check:scriptsexit 0
pnpm check:doc-fencesexit 0 — "every TypeScript block in 224 document(s) is fenced…"
pnpm check:doc-typesexit 0 — "Every documented component type is registered."
pnpm docs:check-linksexit 0 — "Links are valid across 17 scan roots."
check-changeset-presenceexit 0 — "declares 1 changeset(s) … Every one of them has an EMPTY frontmatter"
check-changeset-overwrite / -no-major / -fixedexit 0 each
eslint on the changed filesexit 0 — 0 errors, 0 warnings

Every exit code was captured before any pipe. The lint run is narrowed and the narrowing is measured: the population is the diff, the count comes from --format json (3 files), and eslint.config.js contains 0projectService / parserOptions / project: occurrences against a live control of 10 rules hits in the same file — not type-aware, so no untouched file's verdict can move under this diff.

Changeset: an empty frontmatter, which the presence gate names as a pass rather than a workaround. The one file it flags is under packages/components/src/ but ships nowhere — the package publishes files: ["dist", …] and its build tsconfig.json excludes src/__tests__ outright.

Corrections and deviations, stated rather than buried

The first run of all three verdicts was invalid and was thrown away. The pin file's docstring contained a glob pairing a star with a slash, which ends a block comment early; the file failed to parse, so verdict (i) was red and (ii)/(iii) reported Tests no tests — a transform error, not an assertion. vitest list --filesOnly had passed because it never transforms. Fixed, and all three verdicts above are from the re-run. This repo already records the same trap in tsconfig.scripts.json's header and in check-changeset-presence.mjs.

Three files beyond the dispatch's declared surface, each forced by a gate or by turbo's shape:

  • packages/components/package.json — one script. Turbo tasks are per-package, so dependsOn: ["build"]for the package under test cannot be expressed without a script in that package. It carries --root ../.. because the invocation guard refuses any run whose Vitest root is not the repo root, and --config vitest.config.mts because Vite resolves configFile relative to root, not cwd (measured: ../../vitest.config.mts resolved to /home/vitest.config.mts).
  • content/docs/guide/ci-cd-pipeline.md — one cell. scripts/__tests__/ci-cd-pipeline-doc.test.ts fails when the workflow runs a first-party command the job table does not name. It also fails if the cell names a command the job does not run, which is why the turbo invocation is described in prose there rather than as a code span.
  • scripts/__tests__/turbo-task-guard-coverage.test.ts — one docstring line. That test enforces the partition "cacheable ⇒ has a derived inputs guard; otherwise cache: false", and its docstring enumerates the partition. test:dist is cache: false — a lane whose subject is a build artifact turbo does not hash must never replay a verdict — so it belongs on the uncached side, and the enumeration now says so.

⛔ No change to the root alias map. ⛔ No timeout raised. ⛔ No heavy-test allowlist entry. ⛔ No skip-changeset label — in this repo that label is inert and the empty-frontmatter changeset is the declaration that counts.

Not in this PR

Nothing migrates into the new lane. Its scarcity is the guard against it becoming a second default test surface, and the ruling asks for exactly one resident.

Fix round — Type Check was red on the first push

What was red.@object-ui/console#type-check, run 33588494358 / job 100117481182. Three errors, all one cause:

../../vitest.config.mts(311,7): error TS2769: No overload matches this call.
../../vitest.config.mts(325,7): error TS2769: No overload matches this call.
vitest.config.ts(11,15): error TS2345: Argument of type 'UserConfig & …' is not assignable to parameter of type 'never'.

Cause, one line. Inside the conditional spread that declares the dist project, the literal extends: true widens to boolean, while TestProjectConfiguration.extends is string | true | undefined — so the element matched no defineConfig overload and the whole projects array degraded to never[], which then took down the console entry on line 325 too.

Fix.extends: true as const, plus a comment recording why the annotation is load-bearing (it reads like removable noise). Nothing else changed; the three pre-existing projects never widened, because their extends: true sits in the plain array rather than in a conditional one.

Why CI saw it and this PR's own gates did not. The root vitest.config.mts is compiled by no type program of its own — apps/console/vitest.config.ts merges it, so the console's tsc --noEmit is the program that reads it. That job was outside the gate set run before the first push. It is in the set now.

Reproduced before fixing, not after. On an unbuilt worktree the real errors are unreachable behind 378 TS2882s, so the dependency closure was built through turbo first (turbo run type-check --filter=@object-ui/console), which is the path CI takes:

exiterror TS lines
before the fix13
after the fix00

Re-run at the fix commit 857c8afc5 (exit codes captured by redirect-then-$?):

CheckVerdict
pnpm --filter @object-ui/console run type-checkexit 0 — 0 error TS lines (was exit 1 / 3)
pnpm --filter @object-ui/components run type-checkexit 0
pnpm type-check:scriptsexit 0
pnpm type-check:vitest-setupexit 0
pnpm type-check (turbo, every package)exit 0 — Tasks: 81 successful, 81 total
pnpm test:distexit 0 — Test Files 1 passed (1), Tasks: 9 successful, 9 total
vitest list --filesOnly --project distexit 0 — exactly 1 pin; unit/dom/dom-heavy still 0 (files 810 / 1429 / 34)
vitest run --project dist without the env varexit 1 — the opt-in refusal still fires

No rebase and no force-push: the fix is a commit on top of the branch.


🤖 Generated with Claude Code

https://claude.ai/code/session_01BGMDbrVa8JjZcCQ7DWYH1b

The root `vitest.config.mts` aliases every workspace package to its `src`,
which is right for the ~2000 tests that want fast source feedback and leaves
"does the SHIPPED BUNDLE still do X" unanswerable. Such a test could not be
committed: turbo's `test` task is `dependsOn: ["^build"]` — the DEPENDENCIES'
builds, not the package's own — so a `dist`-importing test landed in CI with no
`dist` to import. That is NOT MEASURED rather than a red pin, and the usual
repair (delete it, or let it skip when `dist` is missing) leaves a green suite
that measures nothing. Two lanes hit this wall in one morning and each threw a
correct measurement away.
Implements the PM ruling on objectui#7183 (option 1, 2026-09-02):
- `vitest.config.mts` gains a fourth project, `dist`, collecting
`packages/*/src/**/*.dist.spec.tsx`. The suffix keeps these files out of
`unit`, `dom` and `dom-heavy` BY CONSTRUCTION — none of the three needed a
character changed — and out of each package's `tsconfig.test.json`, which
matters because turbo's `type-check` waits on `^build` and must never read
the package's own `dist` (objectui#4801).
- The project is OPT-IN behind `OBJECTUI_DIST_PINS=1`. CI's test job runs
`pnpm test` with no build step, so an unconditional fourth project would be
collected there with no `dist` on disk and would fail every PR. The one
false-green this opens — `--project dist` without the env var collecting zero
files and exiting green — is refused in the config with a message naming the
right command.
- `turbo.json` gains `test:dist`, `dependsOn: ["build"]` (self, not `^build`),
`cache: false`: a lane whose subject is a build artifact turbo does not hash
must not replay a verdict. That places it on the uncached side of the
partition `scripts/__tests__/turbo-task-guard-coverage.test.ts` enforces,
whose docstring is updated to match.
- The light dom setup is deliberate, not a cost optimisation:
`vitest.setup.dom.tsx` registers `page:header` from SOURCE, which would keep
a pin green with the built bundle removed entirely.
First resident: the objectui#6252 acceptance criterion PR objectui#7180 measured
by hand and could not commit — an id-authored `page:header` resolves through the
BUILT renderer and carries no `body.source` into the DOM or the authored node.
Its live control is part of the pin: with the `dist` import removed the run
fails `expected undefined to be truthy`.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BGMDbrVa8JjZcCQ7DWYH1b
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 48 chunks)3160.2 KB3191.4 KB
Main entry chunk (gzip)142.6 KB350 KB
Entry fileindex-BOEfHVe7.js
StatusPASS

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

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)15.33KB5.59KB
app-shell (runtime-config.js)20.68KB7.36KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)514.46KB117.29KB
core (index.js)5.55KB2.23KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)178.20KB49.60KB
fields (index.js)244.25KB61.73KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.98KB10.98KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.92KB12.93KB
plugin-charts (index.js)70.02KB19.44KB
plugin-chatbot (index.js)190.53KB45.18KB
plugin-dashboard (index.js)132.63KB34.56KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)250.65KB63.91KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)132.78KB32.58KB
plugin-gantt (index.js)166.77KB40.76KB
plugin-grid (index.js)208.88KB56.59KB
plugin-kanban (index.js)53.21KB14.66KB
plugin-list (index.js)113.51KB27.67KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)29.34KB8.47KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.90KB21.12KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)81.07KB26.86KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.72KB2.24KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)3.40KB1.71KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

@yinlianghuiClaude

Copy link
Copy Markdown
CollaboratorAuthor

CI red on 7a6eef7b9Type Check (job 100117481182), this PR's own defect, fix in flight.

@object-ui/console#type-check runs tsc --noEmit over apps/console/vitest.config.ts, which imports the root vitest.config.mts. The new dist project entry inside the conditional spread is inferred as { extends: boolean; … } (the true literal widens inside the array in the conditional), and TestProjectConfiguration.extends is string | true | undefined, so ../../vitest.config.mts(311,7) / (325,7) report TS2769 and the console config fails at (11,15). The fix is keeping the literal narrow (extends: true as const); the dev is reproducing on pnpm --filter @object-ui/console run type-check before and after, re-running the type programs, and pushing a fix commit on this branch. The local verification ran the components package's type program only, which cannot see the console's read of the root config — the fix round will say so.


Generated by Claude Code

… still type-checks
The `dist` project entry added for objectui#7183 sits inside a conditional
spread, and inside that array literal `extends: true` widens to `boolean`.
`TestProjectConfiguration.extends` is `string | true | undefined`, so the widened
element matches no `defineConfig` overload and the whole `projects` array
degrades to `never[]`:
../../vitest.config.mts(311,7): error TS2769: No overload matches this call.
../../vitest.config.mts(325,7): error TS2769: No overload matches this call.
vitest.config.ts(11,15): error TS2345: Argument of type 'UserConfig & …'
is not assignable to parameter of type 'never'.
CI reported it on `@object-ui/console#type-check` rather than here, because
`apps/console/vitest.config.ts` merges this config and is the type program that
reads it — the root `.mts` is compiled by nobody on its own.
`as const` keeps the literal narrow. The three pre-existing projects are
unaffected: their `extends: true` sits in the plain array, where it never
widened. A comment records why the annotation is load-bearing, since it reads
like removable noise.
Measured red -> green on the reported program, deps built through turbo first
(the errors are unreachable behind 378 TS2882s on an unbuilt tree):
before exit 1, 3 errors
after exit 0, 0 errors
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BGMDbrVa8JjZcCQ7DWYH1b
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 48 chunks)3160.2 KB3191.4 KB
Main entry chunk (gzip)142.6 KB350 KB
Entry fileindex-BOEfHVe7.js
StatusPASS

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

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)15.33KB5.59KB
app-shell (runtime-config.js)20.68KB7.36KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)514.46KB117.29KB
core (index.js)5.55KB2.23KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)178.20KB49.60KB
fields (index.js)244.25KB61.73KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.98KB10.98KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.92KB12.93KB
plugin-charts (index.js)70.02KB19.44KB
plugin-chatbot (index.js)190.53KB45.18KB
plugin-dashboard (index.js)132.63KB34.56KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)250.65KB63.91KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)132.78KB32.58KB
plugin-gantt (index.js)166.77KB40.76KB
plugin-grid (index.js)208.88KB56.59KB
plugin-kanban (index.js)53.21KB14.66KB
plugin-list (index.js)113.51KB27.67KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)29.34KB8.47KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.90KB21.12KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)81.07KB26.86KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.72KB2.24KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)3.40KB1.71KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

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

Projects

None yet

2 participants

@yinlianghui@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

test(ci): a dist vitest project so built-artifact claims can be pinned - #7291

Merged
yinlianghui merged 3 commits into
mainfrom
claude/issue-7183-dist-vitest-project
Sep 2, 2026
Merged

test(ci): a dist vitest project so built-artifact claims can be pinned#7291
yinlianghui merged 3 commits into
mainfrom
claude/issue-7183-dist-vitest-project

Conversation

@yinlianghui

@yinlianghuiyinlianghui commented Sep 2, 2026

Copy link
Copy Markdown
Collaborator

Fixes#7183

Implements the PM ruling on this card (2026-09-02, option 1): a dedicated dist vitest project with its own turbo task that builds the package under test first.

The gap

The root vitest.config.mts aliases every workspace package to its src. That is right for the ~2000 tests that want fast source feedback, and it makes "does the SHIPPED BUNDLE still do X" structurally unanswerable. Such a test could not be committed either: turbo's test task is dependsOn: ["^build"] — the DEPENDENCIES' builds, never the package's own — so a dist-importing test landed in CI with no dist to import. That is NOT MEASURED rather than a red pin, and the usual repair (delete it, or let it skip when dist is missing) leaves a green suite that measures nothing. Two lanes hit this wall in one morning and each threw a correct measurement away.

The five binding constraints, and where each is met

1. Only built-artifact pins; first resident re-derived from PR #7180; no source-resolved test moves in.
The project collects exactly one glob, packages/*/src/**/*.dist.spec.tsx (spelled in the config, not here — a star-slash pair inside a block comment ends the comment, which this PR learned the hard way, see Corrections below). Its only resident is packages/components/src/__tests__/page-header-action-ids.dist.spec.tsx: an id-authored page:header resolves an action whose definition carries a script body, and the marker reaches neither the rendered DOM nor the authored node. Nothing was moved into the project; no existing test changed.

2. dependsOn: ["build"] for the package under test (self, not ^build), and a loud precondition — not a skip — when dist is absent.
turbo.json gains test:dist with dependsOn: ["build"] and cache: false. The precondition is an explicit assertion that reads the built entry from the package's own package.jsonexports["."].import and names the absolute path when it is missing. It fails; it does not skip.

3. The live control from PR #7180 is part of the delivery.
registers page:header from the BUILT bundle is a bare toBeTruthy() on purpose — a custom message would change the string the control is recorded by. See verdict (iii).

4. Wired where the existing test job runs; no timeout raised, no heavy-test allowlist entry.
One step in the existing test job of .github/workflows/ci.yml, if: … && matrix.shard == 1. timeout-minutes: 20 is untouched, heavyDomTests is untouched, and the three existing projects (unit, dom, dom-heavy) are untouched.

5. The step-2 tripwire. Not tripped, but it was close, and the near-miss is the design: see The opt-in below.

The opt-in — the half that keeps pnpm test unchanged

CI's test job runs pnpm test (vitest run) with no build step anywhere in it. An unconditional fourth project is therefore collected by that run with no dist on disk, and its precondition would fail the whole suite on every PR — which would have forced a build into the path of all ~2000 tests, i.e. exactly the "change to how all tests build" constraint 5 says to stop at.

So the project is declared only when OBJECTUI_DIST_PINS=1. An env var rather than the --project dist flag, because argv is meaningful only in the process that parsed the CLI while the env var is inherited by everything Vitest spawns.

That opens exactly one false-green, and it is closed in the config rather than documented: vitest run --project dist without the env var would match no project, and passWithNoTests is true for a run that names no files — a green that measured nothing. Measured:

$ pnpm exec vitest run --project dist # no env var
exit=1
Error: vitest --project dist was requested, but OBJECTUI_DIST_PINS is not "1", so the
`dist` project is NOT declared and this run would collect ZERO files and exit GREEN.

The three verdicts

Each quotes the run's own output. Both mutating legs restore under a trap with absolute paths and prove the mutation landed on disk by marker count — never by an editor's exit code.

(i) dist present, run the way CI runs it — GREEN.

$ pnpm test:dist
V1_EXIT=0
Test Files 1 passed (1)
Tasks: 9 successful, 9 total

(ii) dist deleted — a loud, named precondition FAILURE, not a skip and not MODULE_NOT_FOUND.

MUTATION PROVEN: test -e /home/user/objectui-issue-7183/packages/components/dist => false
V2_EXIT=1
× precondition: the package under test has been built 9ms
AssertionError: The built entry /home/user/objectui-issue-7183/packages/components/dist/index.js
does not exist, so this built-artifact pin has NOTHING to measure. It must fail rather than
skip. […]: expected false to be true
Test Files 1 failed (1)
Tests 3 failed (3)

Restore proven by state, not by an exit code: sha256 of the restored dist/index.js equals the recorded 5365d829….

(iii) the dist import removed — the recorded control.

PRE target-count=1 PRE_HASH=1f531adf… HEAD_BLOB=1f531adf…
POST target-count=0 inject-count=1 POST_HASH=8b6277f1…
MUTATION PROVEN ON DISK (target 1->0, inject 0->1, blob hash moved)
V3_EXIT=1
× registers page:header from the BUILT bundle 6ms
AssertionError: expected undefined to be truthy
Test Files 1 failed (1)
Tests 2 failed | 1 passed (3)

expected undefined to be truthy — verbatim what PR #7180 recorded. The one case that still passes is the precondition, which is correct: dist is on disk in this leg; only the import is gone. Restore proven by state: restored blob 1f531adf… equals the HEAD blob, and git diff HEAD and git status --short are both empty.

Cost — measured, because the ruling priced this at "one build in one job"

A bare turbo run test:dist does not cost one build. Turbo applies a task definition to every package in scope and resolves dependsOn: ["build"] for each, so it schedules a build of the entire monoreposite and console included:

$ turbo run test:dist --dry=json → 45 tasks (44 builds + 1 test:dist)
$ turbo run test:dist --filter=@object-ui/components --dry=json
→ 9 tasks
components#build components#test:dist core#build data-objectstack#build
i18n#build react#build react-runtime#build sdui-parser#build types#build

The root script therefore carries --filter=@object-ui/components, and the 9 tasks are the package plus the dependency closure its .d.ts emit genuinely needs. Cold-cache wall time, which is what CI pays:

COLD_EXIT=0 COLD_WALL_SECONDS=72
Tasks: 9 successful, 9 total
Cached: 0 cached, 9 total
Time: 1m10.762s

~72 s added to shard 1 of a job whose ceiling is timeout-minutes: 20, unchanged. (Shared-box seconds: four sibling agents build in this container, so this is an upper-ish bound, not an idle-box figure.)

If a second package ever gains a pin without being added to the filter, the failure is the loud precondition naming its missing built entry — not a silent skip.

Collected by exactly one project

vitest list --filesOnly is the instrument, and it has a positive control:

unit: files=810 pin=0
dom: files=1429 pin=0
dom-heavy: files=34 pin=0
dist: [dist] packages/components/src/__tests__/page-header-action-ids.dist.spec.tsx

The suffix does this by construction — unit collects *.test.ts, dom collects *.test.tsx, dom-heavy is an explicit file list — so none of the three needed a character changed.

The same property keeps the file out of packages/components/tsconfig.test.json, and that is deliberate rather than incidental: turbo's type-check waits on ^build, so a type program that read this package's own dist would demand an artifact type-check is not allowed to wait for — the coupling #4801 removed. Measured with the package's own type program, with a control:

$ tsc -p tsconfig.test.json --listFiles
page-header-action-ids.dist.spec.tsx → 0 files (out of the program, as designed)
page-header-action-ids.test.tsx → 1 file (control: siblings ARE in it)

Gates — union re-run at commit 7a6eef7b9

GateVerdict line
vitest run scripts/exit 0 — Test Files 94 passed (94) / Tests 2645 passed (2645)
pnpm --filter @object-ui/components run type-checkexit 0
pnpm check:control-bytesexit 0 — "OK (scanned 6010 tracked text file(s); skipped 85 binary)"
pnpm type-check:scriptsexit 0
pnpm check:doc-fencesexit 0 — "every TypeScript block in 224 document(s) is fenced…"
pnpm check:doc-typesexit 0 — "Every documented component type is registered."
pnpm docs:check-linksexit 0 — "Links are valid across 17 scan roots."
check-changeset-presenceexit 0 — "declares 1 changeset(s) … Every one of them has an EMPTY frontmatter"
check-changeset-overwrite / -no-major / -fixedexit 0 each
eslint on the changed filesexit 0 — 0 errors, 0 warnings

Every exit code was captured before any pipe. The lint run is narrowed and the narrowing is measured: the population is the diff, the count comes from --format json (3 files), and eslint.config.js contains 0projectService / parserOptions / project: occurrences against a live control of 10 rules hits in the same file — not type-aware, so no untouched file's verdict can move under this diff.

Changeset: an empty frontmatter, which the presence gate names as a pass rather than a workaround. The one file it flags is under packages/components/src/ but ships nowhere — the package publishes files: ["dist", …] and its build tsconfig.json excludes src/__tests__ outright.

Corrections and deviations, stated rather than buried

The first run of all three verdicts was invalid and was thrown away. The pin file's docstring contained a glob pairing a star with a slash, which ends a block comment early; the file failed to parse, so verdict (i) was red and (ii)/(iii) reported Tests no tests — a transform error, not an assertion. vitest list --filesOnly had passed because it never transforms. Fixed, and all three verdicts above are from the re-run. This repo already records the same trap in tsconfig.scripts.json's header and in check-changeset-presence.mjs.

Three files beyond the dispatch's declared surface, each forced by a gate or by turbo's shape:

  • packages/components/package.json — one script. Turbo tasks are per-package, so dependsOn: ["build"]for the package under test cannot be expressed without a script in that package. It carries --root ../.. because the invocation guard refuses any run whose Vitest root is not the repo root, and --config vitest.config.mts because Vite resolves configFile relative to root, not cwd (measured: ../../vitest.config.mts resolved to /home/vitest.config.mts).
  • content/docs/guide/ci-cd-pipeline.md — one cell. scripts/__tests__/ci-cd-pipeline-doc.test.ts fails when the workflow runs a first-party command the job table does not name. It also fails if the cell names a command the job does not run, which is why the turbo invocation is described in prose there rather than as a code span.
  • scripts/__tests__/turbo-task-guard-coverage.test.ts — one docstring line. That test enforces the partition "cacheable ⇒ has a derived inputs guard; otherwise cache: false", and its docstring enumerates the partition. test:dist is cache: false — a lane whose subject is a build artifact turbo does not hash must never replay a verdict — so it belongs on the uncached side, and the enumeration now says so.

⛔ No change to the root alias map. ⛔ No timeout raised. ⛔ No heavy-test allowlist entry. ⛔ No skip-changeset label — in this repo that label is inert and the empty-frontmatter changeset is the declaration that counts.

Not in this PR

Nothing migrates into the new lane. Its scarcity is the guard against it becoming a second default test surface, and the ruling asks for exactly one resident.

Fix round — Type Check was red on the first push

What was red.@object-ui/console#type-check, run 33588494358 / job 100117481182. Three errors, all one cause:

../../vitest.config.mts(311,7): error TS2769: No overload matches this call.
../../vitest.config.mts(325,7): error TS2769: No overload matches this call.
vitest.config.ts(11,15): error TS2345: Argument of type 'UserConfig & …' is not assignable to parameter of type 'never'.

Cause, one line. Inside the conditional spread that declares the dist project, the literal extends: true widens to boolean, while TestProjectConfiguration.extends is string | true | undefined — so the element matched no defineConfig overload and the whole projects array degraded to never[], which then took down the console entry on line 325 too.

Fix.extends: true as const, plus a comment recording why the annotation is load-bearing (it reads like removable noise). Nothing else changed; the three pre-existing projects never widened, because their extends: true sits in the plain array rather than in a conditional one.

Why CI saw it and this PR's own gates did not. The root vitest.config.mts is compiled by no type program of its own — apps/console/vitest.config.ts merges it, so the console's tsc --noEmit is the program that reads it. That job was outside the gate set run before the first push. It is in the set now.

Reproduced before fixing, not after. On an unbuilt worktree the real errors are unreachable behind 378 TS2882s, so the dependency closure was built through turbo first (turbo run type-check --filter=@object-ui/console), which is the path CI takes:

exiterror TS lines
before the fix13
after the fix00

Re-run at the fix commit 857c8afc5 (exit codes captured by redirect-then-$?):

CheckVerdict
pnpm --filter @object-ui/console run type-checkexit 0 — 0 error TS lines (was exit 1 / 3)
pnpm --filter @object-ui/components run type-checkexit 0
pnpm type-check:scriptsexit 0
pnpm type-check:vitest-setupexit 0
pnpm type-check (turbo, every package)exit 0 — Tasks: 81 successful, 81 total
pnpm test:distexit 0 — Test Files 1 passed (1), Tasks: 9 successful, 9 total
vitest list --filesOnly --project distexit 0 — exactly 1 pin; unit/dom/dom-heavy still 0 (files 810 / 1429 / 34)
vitest run --project dist without the env varexit 1 — the opt-in refusal still fires

No rebase and no force-push: the fix is a commit on top of the branch.


🤖 Generated with Claude Code

https://claude.ai/code/session_01BGMDbrVa8JjZcCQ7DWYH1b

The root `vitest.config.mts` aliases every workspace package to its `src`,
which is right for the ~2000 tests that want fast source feedback and leaves
"does the SHIPPED BUNDLE still do X" unanswerable. Such a test could not be
committed: turbo's `test` task is `dependsOn: ["^build"]` — the DEPENDENCIES'
builds, not the package's own — so a `dist`-importing test landed in CI with no
`dist` to import. That is NOT MEASURED rather than a red pin, and the usual
repair (delete it, or let it skip when `dist` is missing) leaves a green suite
that measures nothing. Two lanes hit this wall in one morning and each threw a
correct measurement away.
Implements the PM ruling on objectui#7183 (option 1, 2026-09-02):
- `vitest.config.mts` gains a fourth project, `dist`, collecting
`packages/*/src/**/*.dist.spec.tsx`. The suffix keeps these files out of
`unit`, `dom` and `dom-heavy` BY CONSTRUCTION — none of the three needed a
character changed — and out of each package's `tsconfig.test.json`, which
matters because turbo's `type-check` waits on `^build` and must never read
the package's own `dist` (objectui#4801).
- The project is OPT-IN behind `OBJECTUI_DIST_PINS=1`. CI's test job runs
`pnpm test` with no build step, so an unconditional fourth project would be
collected there with no `dist` on disk and would fail every PR. The one
false-green this opens — `--project dist` without the env var collecting zero
files and exiting green — is refused in the config with a message naming the
right command.
- `turbo.json` gains `test:dist`, `dependsOn: ["build"]` (self, not `^build`),
`cache: false`: a lane whose subject is a build artifact turbo does not hash
must not replay a verdict. That places it on the uncached side of the
partition `scripts/__tests__/turbo-task-guard-coverage.test.ts` enforces,
whose docstring is updated to match.
- The light dom setup is deliberate, not a cost optimisation:
`vitest.setup.dom.tsx` registers `page:header` from SOURCE, which would keep
a pin green with the built bundle removed entirely.
First resident: the objectui#6252 acceptance criterion PR objectui#7180 measured
by hand and could not commit — an id-authored `page:header` resolves through the
BUILT renderer and carries no `body.source` into the DOM or the authored node.
Its live control is part of the pin: with the `dist` import removed the run
fails `expected undefined to be truthy`.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BGMDbrVa8JjZcCQ7DWYH1b
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 48 chunks)3160.2 KB3191.4 KB
Main entry chunk (gzip)142.6 KB350 KB
Entry fileindex-BOEfHVe7.js
StatusPASS

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

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)15.33KB5.59KB
app-shell (runtime-config.js)20.68KB7.36KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)514.46KB117.29KB
core (index.js)5.55KB2.23KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)178.20KB49.60KB
fields (index.js)244.25KB61.73KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.98KB10.98KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.92KB12.93KB
plugin-charts (index.js)70.02KB19.44KB
plugin-chatbot (index.js)190.53KB45.18KB
plugin-dashboard (index.js)132.63KB34.56KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)250.65KB63.91KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)132.78KB32.58KB
plugin-gantt (index.js)166.77KB40.76KB
plugin-grid (index.js)208.88KB56.59KB
plugin-kanban (index.js)53.21KB14.66KB
plugin-list (index.js)113.51KB27.67KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)29.34KB8.47KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.90KB21.12KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)81.07KB26.86KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.72KB2.24KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)3.40KB1.71KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

@yinlianghuiClaude

Copy link
Copy Markdown
CollaboratorAuthor

CI red on 7a6eef7b9Type Check (job 100117481182), this PR's own defect, fix in flight.

@object-ui/console#type-check runs tsc --noEmit over apps/console/vitest.config.ts, which imports the root vitest.config.mts. The new dist project entry inside the conditional spread is inferred as { extends: boolean; … } (the true literal widens inside the array in the conditional), and TestProjectConfiguration.extends is string | true | undefined, so ../../vitest.config.mts(311,7) / (325,7) report TS2769 and the console config fails at (11,15). The fix is keeping the literal narrow (extends: true as const); the dev is reproducing on pnpm --filter @object-ui/console run type-check before and after, re-running the type programs, and pushing a fix commit on this branch. The local verification ran the components package's type program only, which cannot see the console's read of the root config — the fix round will say so.


Generated by Claude Code

… still type-checks
The `dist` project entry added for objectui#7183 sits inside a conditional
spread, and inside that array literal `extends: true` widens to `boolean`.
`TestProjectConfiguration.extends` is `string | true | undefined`, so the widened
element matches no `defineConfig` overload and the whole `projects` array
degrades to `never[]`:
../../vitest.config.mts(311,7): error TS2769: No overload matches this call.
../../vitest.config.mts(325,7): error TS2769: No overload matches this call.
vitest.config.ts(11,15): error TS2345: Argument of type 'UserConfig & …'
is not assignable to parameter of type 'never'.
CI reported it on `@object-ui/console#type-check` rather than here, because
`apps/console/vitest.config.ts` merges this config and is the type program that
reads it — the root `.mts` is compiled by nobody on its own.
`as const` keeps the literal narrow. The three pre-existing projects are
unaffected: their `extends: true` sits in the plain array, where it never
widened. A comment records why the annotation is load-bearing, since it reads
like removable noise.
Measured red -> green on the reported program, deps built through turbo first
(the errors are unreachable behind 378 TS2882s on an unbuilt tree):
before exit 1, 3 errors
after exit 0, 0 errors
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BGMDbrVa8JjZcCQ7DWYH1b
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 48 chunks)3160.2 KB3191.4 KB
Main entry chunk (gzip)142.6 KB350 KB
Entry fileindex-BOEfHVe7.js
StatusPASS

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

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)15.33KB5.59KB
app-shell (runtime-config.js)20.68KB7.36KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)514.46KB117.29KB
core (index.js)5.55KB2.23KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)178.20KB49.60KB
fields (index.js)244.25KB61.73KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.98KB10.98KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.92KB12.93KB
plugin-charts (index.js)70.02KB19.44KB
plugin-chatbot (index.js)190.53KB45.18KB
plugin-dashboard (index.js)132.63KB34.56KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)250.65KB63.91KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)132.78KB32.58KB
plugin-gantt (index.js)166.77KB40.76KB
plugin-grid (index.js)208.88KB56.59KB
plugin-kanban (index.js)53.21KB14.66KB
plugin-list (index.js)113.51KB27.67KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)29.34KB8.47KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.90KB21.12KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)81.07KB26.86KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.72KB2.24KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)3.40KB1.71KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

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

Projects

None yet

2 participants

@yinlianghui@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

test(ci): a dist vitest project so built-artifact claims can be pinned - #7291

Merged
yinlianghui merged 3 commits into
mainfrom
claude/issue-7183-dist-vitest-project
Sep 2, 2026
Merged

test(ci): a dist vitest project so built-artifact claims can be pinned#7291
yinlianghui merged 3 commits into
mainfrom
claude/issue-7183-dist-vitest-project

Conversation

@yinlianghui

@yinlianghuiyinlianghui commented Sep 2, 2026

Copy link
Copy Markdown
Collaborator

Fixes#7183

Implements the PM ruling on this card (2026-09-02, option 1): a dedicated dist vitest project with its own turbo task that builds the package under test first.

The gap

The root vitest.config.mts aliases every workspace package to its src. That is right for the ~2000 tests that want fast source feedback, and it makes "does the SHIPPED BUNDLE still do X" structurally unanswerable. Such a test could not be committed either: turbo's test task is dependsOn: ["^build"] — the DEPENDENCIES' builds, never the package's own — so a dist-importing test landed in CI with no dist to import. That is NOT MEASURED rather than a red pin, and the usual repair (delete it, or let it skip when dist is missing) leaves a green suite that measures nothing. Two lanes hit this wall in one morning and each threw a correct measurement away.

The five binding constraints, and where each is met

1. Only built-artifact pins; first resident re-derived from PR #7180; no source-resolved test moves in.
The project collects exactly one glob, packages/*/src/**/*.dist.spec.tsx (spelled in the config, not here — a star-slash pair inside a block comment ends the comment, which this PR learned the hard way, see Corrections below). Its only resident is packages/components/src/__tests__/page-header-action-ids.dist.spec.tsx: an id-authored page:header resolves an action whose definition carries a script body, and the marker reaches neither the rendered DOM nor the authored node. Nothing was moved into the project; no existing test changed.

2. dependsOn: ["build"] for the package under test (self, not ^build), and a loud precondition — not a skip — when dist is absent.
turbo.json gains test:dist with dependsOn: ["build"] and cache: false. The precondition is an explicit assertion that reads the built entry from the package's own package.jsonexports["."].import and names the absolute path when it is missing. It fails; it does not skip.

3. The live control from PR #7180 is part of the delivery.
registers page:header from the BUILT bundle is a bare toBeTruthy() on purpose — a custom message would change the string the control is recorded by. See verdict (iii).

4. Wired where the existing test job runs; no timeout raised, no heavy-test allowlist entry.
One step in the existing test job of .github/workflows/ci.yml, if: … && matrix.shard == 1. timeout-minutes: 20 is untouched, heavyDomTests is untouched, and the three existing projects (unit, dom, dom-heavy) are untouched.

5. The step-2 tripwire. Not tripped, but it was close, and the near-miss is the design: see The opt-in below.

The opt-in — the half that keeps pnpm test unchanged

CI's test job runs pnpm test (vitest run) with no build step anywhere in it. An unconditional fourth project is therefore collected by that run with no dist on disk, and its precondition would fail the whole suite on every PR — which would have forced a build into the path of all ~2000 tests, i.e. exactly the "change to how all tests build" constraint 5 says to stop at.

So the project is declared only when OBJECTUI_DIST_PINS=1. An env var rather than the --project dist flag, because argv is meaningful only in the process that parsed the CLI while the env var is inherited by everything Vitest spawns.

That opens exactly one false-green, and it is closed in the config rather than documented: vitest run --project dist without the env var would match no project, and passWithNoTests is true for a run that names no files — a green that measured nothing. Measured:

$ pnpm exec vitest run --project dist # no env var
exit=1
Error: vitest --project dist was requested, but OBJECTUI_DIST_PINS is not "1", so the
`dist` project is NOT declared and this run would collect ZERO files and exit GREEN.

The three verdicts

Each quotes the run's own output. Both mutating legs restore under a trap with absolute paths and prove the mutation landed on disk by marker count — never by an editor's exit code.

(i) dist present, run the way CI runs it — GREEN.

$ pnpm test:dist
V1_EXIT=0
Test Files 1 passed (1)
Tasks: 9 successful, 9 total

(ii) dist deleted — a loud, named precondition FAILURE, not a skip and not MODULE_NOT_FOUND.

MUTATION PROVEN: test -e /home/user/objectui-issue-7183/packages/components/dist => false
V2_EXIT=1
× precondition: the package under test has been built 9ms
AssertionError: The built entry /home/user/objectui-issue-7183/packages/components/dist/index.js
does not exist, so this built-artifact pin has NOTHING to measure. It must fail rather than
skip. […]: expected false to be true
Test Files 1 failed (1)
Tests 3 failed (3)

Restore proven by state, not by an exit code: sha256 of the restored dist/index.js equals the recorded 5365d829….

(iii) the dist import removed — the recorded control.

PRE target-count=1 PRE_HASH=1f531adf… HEAD_BLOB=1f531adf…
POST target-count=0 inject-count=1 POST_HASH=8b6277f1…
MUTATION PROVEN ON DISK (target 1->0, inject 0->1, blob hash moved)
V3_EXIT=1
× registers page:header from the BUILT bundle 6ms
AssertionError: expected undefined to be truthy
Test Files 1 failed (1)
Tests 2 failed | 1 passed (3)

expected undefined to be truthy — verbatim what PR #7180 recorded. The one case that still passes is the precondition, which is correct: dist is on disk in this leg; only the import is gone. Restore proven by state: restored blob 1f531adf… equals the HEAD blob, and git diff HEAD and git status --short are both empty.

Cost — measured, because the ruling priced this at "one build in one job"

A bare turbo run test:dist does not cost one build. Turbo applies a task definition to every package in scope and resolves dependsOn: ["build"] for each, so it schedules a build of the entire monoreposite and console included:

$ turbo run test:dist --dry=json → 45 tasks (44 builds + 1 test:dist)
$ turbo run test:dist --filter=@object-ui/components --dry=json
→ 9 tasks
components#build components#test:dist core#build data-objectstack#build
i18n#build react#build react-runtime#build sdui-parser#build types#build

The root script therefore carries --filter=@object-ui/components, and the 9 tasks are the package plus the dependency closure its .d.ts emit genuinely needs. Cold-cache wall time, which is what CI pays:

COLD_EXIT=0 COLD_WALL_SECONDS=72
Tasks: 9 successful, 9 total
Cached: 0 cached, 9 total
Time: 1m10.762s

~72 s added to shard 1 of a job whose ceiling is timeout-minutes: 20, unchanged. (Shared-box seconds: four sibling agents build in this container, so this is an upper-ish bound, not an idle-box figure.)

If a second package ever gains a pin without being added to the filter, the failure is the loud precondition naming its missing built entry — not a silent skip.

Collected by exactly one project

vitest list --filesOnly is the instrument, and it has a positive control:

unit: files=810 pin=0
dom: files=1429 pin=0
dom-heavy: files=34 pin=0
dist: [dist] packages/components/src/__tests__/page-header-action-ids.dist.spec.tsx

The suffix does this by construction — unit collects *.test.ts, dom collects *.test.tsx, dom-heavy is an explicit file list — so none of the three needed a character changed.

The same property keeps the file out of packages/components/tsconfig.test.json, and that is deliberate rather than incidental: turbo's type-check waits on ^build, so a type program that read this package's own dist would demand an artifact type-check is not allowed to wait for — the coupling #4801 removed. Measured with the package's own type program, with a control:

$ tsc -p tsconfig.test.json --listFiles
page-header-action-ids.dist.spec.tsx → 0 files (out of the program, as designed)
page-header-action-ids.test.tsx → 1 file (control: siblings ARE in it)

Gates — union re-run at commit 7a6eef7b9

GateVerdict line
vitest run scripts/exit 0 — Test Files 94 passed (94) / Tests 2645 passed (2645)
pnpm --filter @object-ui/components run type-checkexit 0
pnpm check:control-bytesexit 0 — "OK (scanned 6010 tracked text file(s); skipped 85 binary)"
pnpm type-check:scriptsexit 0
pnpm check:doc-fencesexit 0 — "every TypeScript block in 224 document(s) is fenced…"
pnpm check:doc-typesexit 0 — "Every documented component type is registered."
pnpm docs:check-linksexit 0 — "Links are valid across 17 scan roots."
check-changeset-presenceexit 0 — "declares 1 changeset(s) … Every one of them has an EMPTY frontmatter"
check-changeset-overwrite / -no-major / -fixedexit 0 each
eslint on the changed filesexit 0 — 0 errors, 0 warnings

Every exit code was captured before any pipe. The lint run is narrowed and the narrowing is measured: the population is the diff, the count comes from --format json (3 files), and eslint.config.js contains 0projectService / parserOptions / project: occurrences against a live control of 10 rules hits in the same file — not type-aware, so no untouched file's verdict can move under this diff.

Changeset: an empty frontmatter, which the presence gate names as a pass rather than a workaround. The one file it flags is under packages/components/src/ but ships nowhere — the package publishes files: ["dist", …] and its build tsconfig.json excludes src/__tests__ outright.

Corrections and deviations, stated rather than buried

The first run of all three verdicts was invalid and was thrown away. The pin file's docstring contained a glob pairing a star with a slash, which ends a block comment early; the file failed to parse, so verdict (i) was red and (ii)/(iii) reported Tests no tests — a transform error, not an assertion. vitest list --filesOnly had passed because it never transforms. Fixed, and all three verdicts above are from the re-run. This repo already records the same trap in tsconfig.scripts.json's header and in check-changeset-presence.mjs.

Three files beyond the dispatch's declared surface, each forced by a gate or by turbo's shape:

  • packages/components/package.json — one script. Turbo tasks are per-package, so dependsOn: ["build"]for the package under test cannot be expressed without a script in that package. It carries --root ../.. because the invocation guard refuses any run whose Vitest root is not the repo root, and --config vitest.config.mts because Vite resolves configFile relative to root, not cwd (measured: ../../vitest.config.mts resolved to /home/vitest.config.mts).
  • content/docs/guide/ci-cd-pipeline.md — one cell. scripts/__tests__/ci-cd-pipeline-doc.test.ts fails when the workflow runs a first-party command the job table does not name. It also fails if the cell names a command the job does not run, which is why the turbo invocation is described in prose there rather than as a code span.
  • scripts/__tests__/turbo-task-guard-coverage.test.ts — one docstring line. That test enforces the partition "cacheable ⇒ has a derived inputs guard; otherwise cache: false", and its docstring enumerates the partition. test:dist is cache: false — a lane whose subject is a build artifact turbo does not hash must never replay a verdict — so it belongs on the uncached side, and the enumeration now says so.

⛔ No change to the root alias map. ⛔ No timeout raised. ⛔ No heavy-test allowlist entry. ⛔ No skip-changeset label — in this repo that label is inert and the empty-frontmatter changeset is the declaration that counts.

Not in this PR

Nothing migrates into the new lane. Its scarcity is the guard against it becoming a second default test surface, and the ruling asks for exactly one resident.

Fix round — Type Check was red on the first push

What was red.@object-ui/console#type-check, run 33588494358 / job 100117481182. Three errors, all one cause:

../../vitest.config.mts(311,7): error TS2769: No overload matches this call.
../../vitest.config.mts(325,7): error TS2769: No overload matches this call.
vitest.config.ts(11,15): error TS2345: Argument of type 'UserConfig & …' is not assignable to parameter of type 'never'.

Cause, one line. Inside the conditional spread that declares the dist project, the literal extends: true widens to boolean, while TestProjectConfiguration.extends is string | true | undefined — so the element matched no defineConfig overload and the whole projects array degraded to never[], which then took down the console entry on line 325 too.

Fix.extends: true as const, plus a comment recording why the annotation is load-bearing (it reads like removable noise). Nothing else changed; the three pre-existing projects never widened, because their extends: true sits in the plain array rather than in a conditional one.

Why CI saw it and this PR's own gates did not. The root vitest.config.mts is compiled by no type program of its own — apps/console/vitest.config.ts merges it, so the console's tsc --noEmit is the program that reads it. That job was outside the gate set run before the first push. It is in the set now.

Reproduced before fixing, not after. On an unbuilt worktree the real errors are unreachable behind 378 TS2882s, so the dependency closure was built through turbo first (turbo run type-check --filter=@object-ui/console), which is the path CI takes:

exiterror TS lines
before the fix13
after the fix00

Re-run at the fix commit 857c8afc5 (exit codes captured by redirect-then-$?):

CheckVerdict
pnpm --filter @object-ui/console run type-checkexit 0 — 0 error TS lines (was exit 1 / 3)
pnpm --filter @object-ui/components run type-checkexit 0
pnpm type-check:scriptsexit 0
pnpm type-check:vitest-setupexit 0
pnpm type-check (turbo, every package)exit 0 — Tasks: 81 successful, 81 total
pnpm test:distexit 0 — Test Files 1 passed (1), Tasks: 9 successful, 9 total
vitest list --filesOnly --project distexit 0 — exactly 1 pin; unit/dom/dom-heavy still 0 (files 810 / 1429 / 34)
vitest run --project dist without the env varexit 1 — the opt-in refusal still fires

No rebase and no force-push: the fix is a commit on top of the branch.


🤖 Generated with Claude Code

https://claude.ai/code/session_01BGMDbrVa8JjZcCQ7DWYH1b

The root `vitest.config.mts` aliases every workspace package to its `src`,
which is right for the ~2000 tests that want fast source feedback and leaves
"does the SHIPPED BUNDLE still do X" unanswerable. Such a test could not be
committed: turbo's `test` task is `dependsOn: ["^build"]` — the DEPENDENCIES'
builds, not the package's own — so a `dist`-importing test landed in CI with no
`dist` to import. That is NOT MEASURED rather than a red pin, and the usual
repair (delete it, or let it skip when `dist` is missing) leaves a green suite
that measures nothing. Two lanes hit this wall in one morning and each threw a
correct measurement away.
Implements the PM ruling on objectui#7183 (option 1, 2026-09-02):
- `vitest.config.mts` gains a fourth project, `dist`, collecting
`packages/*/src/**/*.dist.spec.tsx`. The suffix keeps these files out of
`unit`, `dom` and `dom-heavy` BY CONSTRUCTION — none of the three needed a
character changed — and out of each package's `tsconfig.test.json`, which
matters because turbo's `type-check` waits on `^build` and must never read
the package's own `dist` (objectui#4801).
- The project is OPT-IN behind `OBJECTUI_DIST_PINS=1`. CI's test job runs
`pnpm test` with no build step, so an unconditional fourth project would be
collected there with no `dist` on disk and would fail every PR. The one
false-green this opens — `--project dist` without the env var collecting zero
files and exiting green — is refused in the config with a message naming the
right command.
- `turbo.json` gains `test:dist`, `dependsOn: ["build"]` (self, not `^build`),
`cache: false`: a lane whose subject is a build artifact turbo does not hash
must not replay a verdict. That places it on the uncached side of the
partition `scripts/__tests__/turbo-task-guard-coverage.test.ts` enforces,
whose docstring is updated to match.
- The light dom setup is deliberate, not a cost optimisation:
`vitest.setup.dom.tsx` registers `page:header` from SOURCE, which would keep
a pin green with the built bundle removed entirely.
First resident: the objectui#6252 acceptance criterion PR objectui#7180 measured
by hand and could not commit — an id-authored `page:header` resolves through the
BUILT renderer and carries no `body.source` into the DOM or the authored node.
Its live control is part of the pin: with the `dist` import removed the run
fails `expected undefined to be truthy`.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BGMDbrVa8JjZcCQ7DWYH1b
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 48 chunks)3160.2 KB3191.4 KB
Main entry chunk (gzip)142.6 KB350 KB
Entry fileindex-BOEfHVe7.js
StatusPASS

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

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)15.33KB5.59KB
app-shell (runtime-config.js)20.68KB7.36KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)514.46KB117.29KB
core (index.js)5.55KB2.23KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)178.20KB49.60KB
fields (index.js)244.25KB61.73KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.98KB10.98KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.92KB12.93KB
plugin-charts (index.js)70.02KB19.44KB
plugin-chatbot (index.js)190.53KB45.18KB
plugin-dashboard (index.js)132.63KB34.56KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)250.65KB63.91KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)132.78KB32.58KB
plugin-gantt (index.js)166.77KB40.76KB
plugin-grid (index.js)208.88KB56.59KB
plugin-kanban (index.js)53.21KB14.66KB
plugin-list (index.js)113.51KB27.67KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)29.34KB8.47KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.90KB21.12KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)81.07KB26.86KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.72KB2.24KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)3.40KB1.71KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

@yinlianghuiClaude

Copy link
Copy Markdown
CollaboratorAuthor

CI red on 7a6eef7b9Type Check (job 100117481182), this PR's own defect, fix in flight.

@object-ui/console#type-check runs tsc --noEmit over apps/console/vitest.config.ts, which imports the root vitest.config.mts. The new dist project entry inside the conditional spread is inferred as { extends: boolean; … } (the true literal widens inside the array in the conditional), and TestProjectConfiguration.extends is string | true | undefined, so ../../vitest.config.mts(311,7) / (325,7) report TS2769 and the console config fails at (11,15). The fix is keeping the literal narrow (extends: true as const); the dev is reproducing on pnpm --filter @object-ui/console run type-check before and after, re-running the type programs, and pushing a fix commit on this branch. The local verification ran the components package's type program only, which cannot see the console's read of the root config — the fix round will say so.


Generated by Claude Code

… still type-checks
The `dist` project entry added for objectui#7183 sits inside a conditional
spread, and inside that array literal `extends: true` widens to `boolean`.
`TestProjectConfiguration.extends` is `string | true | undefined`, so the widened
element matches no `defineConfig` overload and the whole `projects` array
degrades to `never[]`:
../../vitest.config.mts(311,7): error TS2769: No overload matches this call.
../../vitest.config.mts(325,7): error TS2769: No overload matches this call.
vitest.config.ts(11,15): error TS2345: Argument of type 'UserConfig & …'
is not assignable to parameter of type 'never'.
CI reported it on `@object-ui/console#type-check` rather than here, because
`apps/console/vitest.config.ts` merges this config and is the type program that
reads it — the root `.mts` is compiled by nobody on its own.
`as const` keeps the literal narrow. The three pre-existing projects are
unaffected: their `extends: true` sits in the plain array, where it never
widened. A comment records why the annotation is load-bearing, since it reads
like removable noise.
Measured red -> green on the reported program, deps built through turbo first
(the errors are unreachable behind 378 TS2882s on an unbuilt tree):
before exit 1, 3 errors
after exit 0, 0 errors
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BGMDbrVa8JjZcCQ7DWYH1b
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 48 chunks)3160.2 KB3191.4 KB
Main entry chunk (gzip)142.6 KB350 KB
Entry fileindex-BOEfHVe7.js
StatusPASS

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

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)15.33KB5.59KB
app-shell (runtime-config.js)20.68KB7.36KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)514.46KB117.29KB
core (index.js)5.55KB2.23KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)178.20KB49.60KB
fields (index.js)244.25KB61.73KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.98KB10.98KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.92KB12.93KB
plugin-charts (index.js)70.02KB19.44KB
plugin-chatbot (index.js)190.53KB45.18KB
plugin-dashboard (index.js)132.63KB34.56KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)250.65KB63.91KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)132.78KB32.58KB
plugin-gantt (index.js)166.77KB40.76KB
plugin-grid (index.js)208.88KB56.59KB
plugin-kanban (index.js)53.21KB14.66KB
plugin-list (index.js)113.51KB27.67KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)29.34KB8.47KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.90KB21.12KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)81.07KB26.86KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.72KB2.24KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)3.40KB1.71KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

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

Projects

None yet

2 participants

@yinlianghui@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

test(ci): a dist vitest project so built-artifact claims can be pinned - #7291

Merged
yinlianghui merged 3 commits into
mainfrom
claude/issue-7183-dist-vitest-project
Sep 2, 2026
Merged

test(ci): a dist vitest project so built-artifact claims can be pinned#7291
yinlianghui merged 3 commits into
mainfrom
claude/issue-7183-dist-vitest-project

Conversation

@yinlianghui

@yinlianghuiyinlianghui commented Sep 2, 2026

Copy link
Copy Markdown
Collaborator

Fixes#7183

Implements the PM ruling on this card (2026-09-02, option 1): a dedicated dist vitest project with its own turbo task that builds the package under test first.

The gap

The root vitest.config.mts aliases every workspace package to its src. That is right for the ~2000 tests that want fast source feedback, and it makes "does the SHIPPED BUNDLE still do X" structurally unanswerable. Such a test could not be committed either: turbo's test task is dependsOn: ["^build"] — the DEPENDENCIES' builds, never the package's own — so a dist-importing test landed in CI with no dist to import. That is NOT MEASURED rather than a red pin, and the usual repair (delete it, or let it skip when dist is missing) leaves a green suite that measures nothing. Two lanes hit this wall in one morning and each threw a correct measurement away.

The five binding constraints, and where each is met

1. Only built-artifact pins; first resident re-derived from PR #7180; no source-resolved test moves in.
The project collects exactly one glob, packages/*/src/**/*.dist.spec.tsx (spelled in the config, not here — a star-slash pair inside a block comment ends the comment, which this PR learned the hard way, see Corrections below). Its only resident is packages/components/src/__tests__/page-header-action-ids.dist.spec.tsx: an id-authored page:header resolves an action whose definition carries a script body, and the marker reaches neither the rendered DOM nor the authored node. Nothing was moved into the project; no existing test changed.

2. dependsOn: ["build"] for the package under test (self, not ^build), and a loud precondition — not a skip — when dist is absent.
turbo.json gains test:dist with dependsOn: ["build"] and cache: false. The precondition is an explicit assertion that reads the built entry from the package's own package.jsonexports["."].import and names the absolute path when it is missing. It fails; it does not skip.

3. The live control from PR #7180 is part of the delivery.
registers page:header from the BUILT bundle is a bare toBeTruthy() on purpose — a custom message would change the string the control is recorded by. See verdict (iii).

4. Wired where the existing test job runs; no timeout raised, no heavy-test allowlist entry.
One step in the existing test job of .github/workflows/ci.yml, if: … && matrix.shard == 1. timeout-minutes: 20 is untouched, heavyDomTests is untouched, and the three existing projects (unit, dom, dom-heavy) are untouched.

5. The step-2 tripwire. Not tripped, but it was close, and the near-miss is the design: see The opt-in below.

The opt-in — the half that keeps pnpm test unchanged

CI's test job runs pnpm test (vitest run) with no build step anywhere in it. An unconditional fourth project is therefore collected by that run with no dist on disk, and its precondition would fail the whole suite on every PR — which would have forced a build into the path of all ~2000 tests, i.e. exactly the "change to how all tests build" constraint 5 says to stop at.

So the project is declared only when OBJECTUI_DIST_PINS=1. An env var rather than the --project dist flag, because argv is meaningful only in the process that parsed the CLI while the env var is inherited by everything Vitest spawns.

That opens exactly one false-green, and it is closed in the config rather than documented: vitest run --project dist without the env var would match no project, and passWithNoTests is true for a run that names no files — a green that measured nothing. Measured:

$ pnpm exec vitest run --project dist # no env var
exit=1
Error: vitest --project dist was requested, but OBJECTUI_DIST_PINS is not "1", so the
`dist` project is NOT declared and this run would collect ZERO files and exit GREEN.

The three verdicts

Each quotes the run's own output. Both mutating legs restore under a trap with absolute paths and prove the mutation landed on disk by marker count — never by an editor's exit code.

(i) dist present, run the way CI runs it — GREEN.

$ pnpm test:dist
V1_EXIT=0
Test Files 1 passed (1)
Tasks: 9 successful, 9 total

(ii) dist deleted — a loud, named precondition FAILURE, not a skip and not MODULE_NOT_FOUND.

MUTATION PROVEN: test -e /home/user/objectui-issue-7183/packages/components/dist => false
V2_EXIT=1
× precondition: the package under test has been built 9ms
AssertionError: The built entry /home/user/objectui-issue-7183/packages/components/dist/index.js
does not exist, so this built-artifact pin has NOTHING to measure. It must fail rather than
skip. […]: expected false to be true
Test Files 1 failed (1)
Tests 3 failed (3)

Restore proven by state, not by an exit code: sha256 of the restored dist/index.js equals the recorded 5365d829….

(iii) the dist import removed — the recorded control.

PRE target-count=1 PRE_HASH=1f531adf… HEAD_BLOB=1f531adf…
POST target-count=0 inject-count=1 POST_HASH=8b6277f1…
MUTATION PROVEN ON DISK (target 1->0, inject 0->1, blob hash moved)
V3_EXIT=1
× registers page:header from the BUILT bundle 6ms
AssertionError: expected undefined to be truthy
Test Files 1 failed (1)
Tests 2 failed | 1 passed (3)

expected undefined to be truthy — verbatim what PR #7180 recorded. The one case that still passes is the precondition, which is correct: dist is on disk in this leg; only the import is gone. Restore proven by state: restored blob 1f531adf… equals the HEAD blob, and git diff HEAD and git status --short are both empty.

Cost — measured, because the ruling priced this at "one build in one job"

A bare turbo run test:dist does not cost one build. Turbo applies a task definition to every package in scope and resolves dependsOn: ["build"] for each, so it schedules a build of the entire monoreposite and console included:

$ turbo run test:dist --dry=json → 45 tasks (44 builds + 1 test:dist)
$ turbo run test:dist --filter=@object-ui/components --dry=json
→ 9 tasks
components#build components#test:dist core#build data-objectstack#build
i18n#build react#build react-runtime#build sdui-parser#build types#build

The root script therefore carries --filter=@object-ui/components, and the 9 tasks are the package plus the dependency closure its .d.ts emit genuinely needs. Cold-cache wall time, which is what CI pays:

COLD_EXIT=0 COLD_WALL_SECONDS=72
Tasks: 9 successful, 9 total
Cached: 0 cached, 9 total
Time: 1m10.762s

~72 s added to shard 1 of a job whose ceiling is timeout-minutes: 20, unchanged. (Shared-box seconds: four sibling agents build in this container, so this is an upper-ish bound, not an idle-box figure.)

If a second package ever gains a pin without being added to the filter, the failure is the loud precondition naming its missing built entry — not a silent skip.

Collected by exactly one project

vitest list --filesOnly is the instrument, and it has a positive control:

unit: files=810 pin=0
dom: files=1429 pin=0
dom-heavy: files=34 pin=0
dist: [dist] packages/components/src/__tests__/page-header-action-ids.dist.spec.tsx

The suffix does this by construction — unit collects *.test.ts, dom collects *.test.tsx, dom-heavy is an explicit file list — so none of the three needed a character changed.

The same property keeps the file out of packages/components/tsconfig.test.json, and that is deliberate rather than incidental: turbo's type-check waits on ^build, so a type program that read this package's own dist would demand an artifact type-check is not allowed to wait for — the coupling #4801 removed. Measured with the package's own type program, with a control:

$ tsc -p tsconfig.test.json --listFiles
page-header-action-ids.dist.spec.tsx → 0 files (out of the program, as designed)
page-header-action-ids.test.tsx → 1 file (control: siblings ARE in it)

Gates — union re-run at commit 7a6eef7b9

GateVerdict line
vitest run scripts/exit 0 — Test Files 94 passed (94) / Tests 2645 passed (2645)
pnpm --filter @object-ui/components run type-checkexit 0
pnpm check:control-bytesexit 0 — "OK (scanned 6010 tracked text file(s); skipped 85 binary)"
pnpm type-check:scriptsexit 0
pnpm check:doc-fencesexit 0 — "every TypeScript block in 224 document(s) is fenced…"
pnpm check:doc-typesexit 0 — "Every documented component type is registered."
pnpm docs:check-linksexit 0 — "Links are valid across 17 scan roots."
check-changeset-presenceexit 0 — "declares 1 changeset(s) … Every one of them has an EMPTY frontmatter"
check-changeset-overwrite / -no-major / -fixedexit 0 each
eslint on the changed filesexit 0 — 0 errors, 0 warnings

Every exit code was captured before any pipe. The lint run is narrowed and the narrowing is measured: the population is the diff, the count comes from --format json (3 files), and eslint.config.js contains 0projectService / parserOptions / project: occurrences against a live control of 10 rules hits in the same file — not type-aware, so no untouched file's verdict can move under this diff.

Changeset: an empty frontmatter, which the presence gate names as a pass rather than a workaround. The one file it flags is under packages/components/src/ but ships nowhere — the package publishes files: ["dist", …] and its build tsconfig.json excludes src/__tests__ outright.

Corrections and deviations, stated rather than buried

The first run of all three verdicts was invalid and was thrown away. The pin file's docstring contained a glob pairing a star with a slash, which ends a block comment early; the file failed to parse, so verdict (i) was red and (ii)/(iii) reported Tests no tests — a transform error, not an assertion. vitest list --filesOnly had passed because it never transforms. Fixed, and all three verdicts above are from the re-run. This repo already records the same trap in tsconfig.scripts.json's header and in check-changeset-presence.mjs.

Three files beyond the dispatch's declared surface, each forced by a gate or by turbo's shape:

  • packages/components/package.json — one script. Turbo tasks are per-package, so dependsOn: ["build"]for the package under test cannot be expressed without a script in that package. It carries --root ../.. because the invocation guard refuses any run whose Vitest root is not the repo root, and --config vitest.config.mts because Vite resolves configFile relative to root, not cwd (measured: ../../vitest.config.mts resolved to /home/vitest.config.mts).
  • content/docs/guide/ci-cd-pipeline.md — one cell. scripts/__tests__/ci-cd-pipeline-doc.test.ts fails when the workflow runs a first-party command the job table does not name. It also fails if the cell names a command the job does not run, which is why the turbo invocation is described in prose there rather than as a code span.
  • scripts/__tests__/turbo-task-guard-coverage.test.ts — one docstring line. That test enforces the partition "cacheable ⇒ has a derived inputs guard; otherwise cache: false", and its docstring enumerates the partition. test:dist is cache: false — a lane whose subject is a build artifact turbo does not hash must never replay a verdict — so it belongs on the uncached side, and the enumeration now says so.

⛔ No change to the root alias map. ⛔ No timeout raised. ⛔ No heavy-test allowlist entry. ⛔ No skip-changeset label — in this repo that label is inert and the empty-frontmatter changeset is the declaration that counts.

Not in this PR

Nothing migrates into the new lane. Its scarcity is the guard against it becoming a second default test surface, and the ruling asks for exactly one resident.

Fix round — Type Check was red on the first push

What was red.@object-ui/console#type-check, run 33588494358 / job 100117481182. Three errors, all one cause:

../../vitest.config.mts(311,7): error TS2769: No overload matches this call.
../../vitest.config.mts(325,7): error TS2769: No overload matches this call.
vitest.config.ts(11,15): error TS2345: Argument of type 'UserConfig & …' is not assignable to parameter of type 'never'.

Cause, one line. Inside the conditional spread that declares the dist project, the literal extends: true widens to boolean, while TestProjectConfiguration.extends is string | true | undefined — so the element matched no defineConfig overload and the whole projects array degraded to never[], which then took down the console entry on line 325 too.

Fix.extends: true as const, plus a comment recording why the annotation is load-bearing (it reads like removable noise). Nothing else changed; the three pre-existing projects never widened, because their extends: true sits in the plain array rather than in a conditional one.

Why CI saw it and this PR's own gates did not. The root vitest.config.mts is compiled by no type program of its own — apps/console/vitest.config.ts merges it, so the console's tsc --noEmit is the program that reads it. That job was outside the gate set run before the first push. It is in the set now.

Reproduced before fixing, not after. On an unbuilt worktree the real errors are unreachable behind 378 TS2882s, so the dependency closure was built through turbo first (turbo run type-check --filter=@object-ui/console), which is the path CI takes:

exiterror TS lines
before the fix13
after the fix00

Re-run at the fix commit 857c8afc5 (exit codes captured by redirect-then-$?):

CheckVerdict
pnpm --filter @object-ui/console run type-checkexit 0 — 0 error TS lines (was exit 1 / 3)
pnpm --filter @object-ui/components run type-checkexit 0
pnpm type-check:scriptsexit 0
pnpm type-check:vitest-setupexit 0
pnpm type-check (turbo, every package)exit 0 — Tasks: 81 successful, 81 total
pnpm test:distexit 0 — Test Files 1 passed (1), Tasks: 9 successful, 9 total
vitest list --filesOnly --project distexit 0 — exactly 1 pin; unit/dom/dom-heavy still 0 (files 810 / 1429 / 34)
vitest run --project dist without the env varexit 1 — the opt-in refusal still fires

No rebase and no force-push: the fix is a commit on top of the branch.


🤖 Generated with Claude Code

https://claude.ai/code/session_01BGMDbrVa8JjZcCQ7DWYH1b

The root `vitest.config.mts` aliases every workspace package to its `src`,
which is right for the ~2000 tests that want fast source feedback and leaves
"does the SHIPPED BUNDLE still do X" unanswerable. Such a test could not be
committed: turbo's `test` task is `dependsOn: ["^build"]` — the DEPENDENCIES'
builds, not the package's own — so a `dist`-importing test landed in CI with no
`dist` to import. That is NOT MEASURED rather than a red pin, and the usual
repair (delete it, or let it skip when `dist` is missing) leaves a green suite
that measures nothing. Two lanes hit this wall in one morning and each threw a
correct measurement away.
Implements the PM ruling on objectui#7183 (option 1, 2026-09-02):
- `vitest.config.mts` gains a fourth project, `dist`, collecting
`packages/*/src/**/*.dist.spec.tsx`. The suffix keeps these files out of
`unit`, `dom` and `dom-heavy` BY CONSTRUCTION — none of the three needed a
character changed — and out of each package's `tsconfig.test.json`, which
matters because turbo's `type-check` waits on `^build` and must never read
the package's own `dist` (objectui#4801).
- The project is OPT-IN behind `OBJECTUI_DIST_PINS=1`. CI's test job runs
`pnpm test` with no build step, so an unconditional fourth project would be
collected there with no `dist` on disk and would fail every PR. The one
false-green this opens — `--project dist` without the env var collecting zero
files and exiting green — is refused in the config with a message naming the
right command.
- `turbo.json` gains `test:dist`, `dependsOn: ["build"]` (self, not `^build`),
`cache: false`: a lane whose subject is a build artifact turbo does not hash
must not replay a verdict. That places it on the uncached side of the
partition `scripts/__tests__/turbo-task-guard-coverage.test.ts` enforces,
whose docstring is updated to match.
- The light dom setup is deliberate, not a cost optimisation:
`vitest.setup.dom.tsx` registers `page:header` from SOURCE, which would keep
a pin green with the built bundle removed entirely.
First resident: the objectui#6252 acceptance criterion PR objectui#7180 measured
by hand and could not commit — an id-authored `page:header` resolves through the
BUILT renderer and carries no `body.source` into the DOM or the authored node.
Its live control is part of the pin: with the `dist` import removed the run
fails `expected undefined to be truthy`.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BGMDbrVa8JjZcCQ7DWYH1b
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 48 chunks)3160.2 KB3191.4 KB
Main entry chunk (gzip)142.6 KB350 KB
Entry fileindex-BOEfHVe7.js
StatusPASS

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

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)15.33KB5.59KB
app-shell (runtime-config.js)20.68KB7.36KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)514.46KB117.29KB
core (index.js)5.55KB2.23KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)178.20KB49.60KB
fields (index.js)244.25KB61.73KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.98KB10.98KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.92KB12.93KB
plugin-charts (index.js)70.02KB19.44KB
plugin-chatbot (index.js)190.53KB45.18KB
plugin-dashboard (index.js)132.63KB34.56KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)250.65KB63.91KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)132.78KB32.58KB
plugin-gantt (index.js)166.77KB40.76KB
plugin-grid (index.js)208.88KB56.59KB
plugin-kanban (index.js)53.21KB14.66KB
plugin-list (index.js)113.51KB27.67KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)29.34KB8.47KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.90KB21.12KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)81.07KB26.86KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.72KB2.24KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)3.40KB1.71KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

@yinlianghuiClaude

Copy link
Copy Markdown
CollaboratorAuthor

CI red on 7a6eef7b9Type Check (job 100117481182), this PR's own defect, fix in flight.

@object-ui/console#type-check runs tsc --noEmit over apps/console/vitest.config.ts, which imports the root vitest.config.mts. The new dist project entry inside the conditional spread is inferred as { extends: boolean; … } (the true literal widens inside the array in the conditional), and TestProjectConfiguration.extends is string | true | undefined, so ../../vitest.config.mts(311,7) / (325,7) report TS2769 and the console config fails at (11,15). The fix is keeping the literal narrow (extends: true as const); the dev is reproducing on pnpm --filter @object-ui/console run type-check before and after, re-running the type programs, and pushing a fix commit on this branch. The local verification ran the components package's type program only, which cannot see the console's read of the root config — the fix round will say so.


Generated by Claude Code

… still type-checks
The `dist` project entry added for objectui#7183 sits inside a conditional
spread, and inside that array literal `extends: true` widens to `boolean`.
`TestProjectConfiguration.extends` is `string | true | undefined`, so the widened
element matches no `defineConfig` overload and the whole `projects` array
degrades to `never[]`:
../../vitest.config.mts(311,7): error TS2769: No overload matches this call.
../../vitest.config.mts(325,7): error TS2769: No overload matches this call.
vitest.config.ts(11,15): error TS2345: Argument of type 'UserConfig & …'
is not assignable to parameter of type 'never'.
CI reported it on `@object-ui/console#type-check` rather than here, because
`apps/console/vitest.config.ts` merges this config and is the type program that
reads it — the root `.mts` is compiled by nobody on its own.
`as const` keeps the literal narrow. The three pre-existing projects are
unaffected: their `extends: true` sits in the plain array, where it never
widened. A comment records why the annotation is load-bearing, since it reads
like removable noise.
Measured red -> green on the reported program, deps built through turbo first
(the errors are unreachable behind 378 TS2882s on an unbuilt tree):
before exit 1, 3 errors
after exit 0, 0 errors
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BGMDbrVa8JjZcCQ7DWYH1b
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 48 chunks)3160.2 KB3191.4 KB
Main entry chunk (gzip)142.6 KB350 KB
Entry fileindex-BOEfHVe7.js
StatusPASS

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

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)15.33KB5.59KB
app-shell (runtime-config.js)20.68KB7.36KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)514.46KB117.29KB
core (index.js)5.55KB2.23KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)178.20KB49.60KB
fields (index.js)244.25KB61.73KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.98KB10.98KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.92KB12.93KB
plugin-charts (index.js)70.02KB19.44KB
plugin-chatbot (index.js)190.53KB45.18KB
plugin-dashboard (index.js)132.63KB34.56KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)250.65KB63.91KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)132.78KB32.58KB
plugin-gantt (index.js)166.77KB40.76KB
plugin-grid (index.js)208.88KB56.59KB
plugin-kanban (index.js)53.21KB14.66KB
plugin-list (index.js)113.51KB27.67KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)29.34KB8.47KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.90KB21.12KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)81.07KB26.86KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.72KB2.24KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)3.40KB1.71KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

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

Projects

None yet

2 participants

@yinlianghui@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

test(ci): a dist vitest project so built-artifact claims can be pinned - #7291

Merged
yinlianghui merged 3 commits into
mainfrom
claude/issue-7183-dist-vitest-project
Sep 2, 2026
Merged

test(ci): a dist vitest project so built-artifact claims can be pinned#7291
yinlianghui merged 3 commits into
mainfrom
claude/issue-7183-dist-vitest-project

Conversation

@yinlianghui

@yinlianghuiyinlianghui commented Sep 2, 2026

Copy link
Copy Markdown
Collaborator

Fixes#7183

Implements the PM ruling on this card (2026-09-02, option 1): a dedicated dist vitest project with its own turbo task that builds the package under test first.

The gap

The root vitest.config.mts aliases every workspace package to its src. That is right for the ~2000 tests that want fast source feedback, and it makes "does the SHIPPED BUNDLE still do X" structurally unanswerable. Such a test could not be committed either: turbo's test task is dependsOn: ["^build"] — the DEPENDENCIES' builds, never the package's own — so a dist-importing test landed in CI with no dist to import. That is NOT MEASURED rather than a red pin, and the usual repair (delete it, or let it skip when dist is missing) leaves a green suite that measures nothing. Two lanes hit this wall in one morning and each threw a correct measurement away.

The five binding constraints, and where each is met

1. Only built-artifact pins; first resident re-derived from PR #7180; no source-resolved test moves in.
The project collects exactly one glob, packages/*/src/**/*.dist.spec.tsx (spelled in the config, not here — a star-slash pair inside a block comment ends the comment, which this PR learned the hard way, see Corrections below). Its only resident is packages/components/src/__tests__/page-header-action-ids.dist.spec.tsx: an id-authored page:header resolves an action whose definition carries a script body, and the marker reaches neither the rendered DOM nor the authored node. Nothing was moved into the project; no existing test changed.

2. dependsOn: ["build"] for the package under test (self, not ^build), and a loud precondition — not a skip — when dist is absent.
turbo.json gains test:dist with dependsOn: ["build"] and cache: false. The precondition is an explicit assertion that reads the built entry from the package's own package.jsonexports["."].import and names the absolute path when it is missing. It fails; it does not skip.

3. The live control from PR #7180 is part of the delivery.
registers page:header from the BUILT bundle is a bare toBeTruthy() on purpose — a custom message would change the string the control is recorded by. See verdict (iii).

4. Wired where the existing test job runs; no timeout raised, no heavy-test allowlist entry.
One step in the existing test job of .github/workflows/ci.yml, if: … && matrix.shard == 1. timeout-minutes: 20 is untouched, heavyDomTests is untouched, and the three existing projects (unit, dom, dom-heavy) are untouched.

5. The step-2 tripwire. Not tripped, but it was close, and the near-miss is the design: see The opt-in below.

The opt-in — the half that keeps pnpm test unchanged

CI's test job runs pnpm test (vitest run) with no build step anywhere in it. An unconditional fourth project is therefore collected by that run with no dist on disk, and its precondition would fail the whole suite on every PR — which would have forced a build into the path of all ~2000 tests, i.e. exactly the "change to how all tests build" constraint 5 says to stop at.

So the project is declared only when OBJECTUI_DIST_PINS=1. An env var rather than the --project dist flag, because argv is meaningful only in the process that parsed the CLI while the env var is inherited by everything Vitest spawns.

That opens exactly one false-green, and it is closed in the config rather than documented: vitest run --project dist without the env var would match no project, and passWithNoTests is true for a run that names no files — a green that measured nothing. Measured:

$ pnpm exec vitest run --project dist # no env var
exit=1
Error: vitest --project dist was requested, but OBJECTUI_DIST_PINS is not "1", so the
`dist` project is NOT declared and this run would collect ZERO files and exit GREEN.

The three verdicts

Each quotes the run's own output. Both mutating legs restore under a trap with absolute paths and prove the mutation landed on disk by marker count — never by an editor's exit code.

(i) dist present, run the way CI runs it — GREEN.

$ pnpm test:dist
V1_EXIT=0
Test Files 1 passed (1)
Tasks: 9 successful, 9 total

(ii) dist deleted — a loud, named precondition FAILURE, not a skip and not MODULE_NOT_FOUND.

MUTATION PROVEN: test -e /home/user/objectui-issue-7183/packages/components/dist => false
V2_EXIT=1
× precondition: the package under test has been built 9ms
AssertionError: The built entry /home/user/objectui-issue-7183/packages/components/dist/index.js
does not exist, so this built-artifact pin has NOTHING to measure. It must fail rather than
skip. […]: expected false to be true
Test Files 1 failed (1)
Tests 3 failed (3)

Restore proven by state, not by an exit code: sha256 of the restored dist/index.js equals the recorded 5365d829….

(iii) the dist import removed — the recorded control.

PRE target-count=1 PRE_HASH=1f531adf… HEAD_BLOB=1f531adf…
POST target-count=0 inject-count=1 POST_HASH=8b6277f1…
MUTATION PROVEN ON DISK (target 1->0, inject 0->1, blob hash moved)
V3_EXIT=1
× registers page:header from the BUILT bundle 6ms
AssertionError: expected undefined to be truthy
Test Files 1 failed (1)
Tests 2 failed | 1 passed (3)

expected undefined to be truthy — verbatim what PR #7180 recorded. The one case that still passes is the precondition, which is correct: dist is on disk in this leg; only the import is gone. Restore proven by state: restored blob 1f531adf… equals the HEAD blob, and git diff HEAD and git status --short are both empty.

Cost — measured, because the ruling priced this at "one build in one job"

A bare turbo run test:dist does not cost one build. Turbo applies a task definition to every package in scope and resolves dependsOn: ["build"] for each, so it schedules a build of the entire monoreposite and console included:

$ turbo run test:dist --dry=json → 45 tasks (44 builds + 1 test:dist)
$ turbo run test:dist --filter=@object-ui/components --dry=json
→ 9 tasks
components#build components#test:dist core#build data-objectstack#build
i18n#build react#build react-runtime#build sdui-parser#build types#build

The root script therefore carries --filter=@object-ui/components, and the 9 tasks are the package plus the dependency closure its .d.ts emit genuinely needs. Cold-cache wall time, which is what CI pays:

COLD_EXIT=0 COLD_WALL_SECONDS=72
Tasks: 9 successful, 9 total
Cached: 0 cached, 9 total
Time: 1m10.762s

~72 s added to shard 1 of a job whose ceiling is timeout-minutes: 20, unchanged. (Shared-box seconds: four sibling agents build in this container, so this is an upper-ish bound, not an idle-box figure.)

If a second package ever gains a pin without being added to the filter, the failure is the loud precondition naming its missing built entry — not a silent skip.

Collected by exactly one project

vitest list --filesOnly is the instrument, and it has a positive control:

unit: files=810 pin=0
dom: files=1429 pin=0
dom-heavy: files=34 pin=0
dist: [dist] packages/components/src/__tests__/page-header-action-ids.dist.spec.tsx

The suffix does this by construction — unit collects *.test.ts, dom collects *.test.tsx, dom-heavy is an explicit file list — so none of the three needed a character changed.

The same property keeps the file out of packages/components/tsconfig.test.json, and that is deliberate rather than incidental: turbo's type-check waits on ^build, so a type program that read this package's own dist would demand an artifact type-check is not allowed to wait for — the coupling #4801 removed. Measured with the package's own type program, with a control:

$ tsc -p tsconfig.test.json --listFiles
page-header-action-ids.dist.spec.tsx → 0 files (out of the program, as designed)
page-header-action-ids.test.tsx → 1 file (control: siblings ARE in it)

Gates — union re-run at commit 7a6eef7b9

GateVerdict line
vitest run scripts/exit 0 — Test Files 94 passed (94) / Tests 2645 passed (2645)
pnpm --filter @object-ui/components run type-checkexit 0
pnpm check:control-bytesexit 0 — "OK (scanned 6010 tracked text file(s); skipped 85 binary)"
pnpm type-check:scriptsexit 0
pnpm check:doc-fencesexit 0 — "every TypeScript block in 224 document(s) is fenced…"
pnpm check:doc-typesexit 0 — "Every documented component type is registered."
pnpm docs:check-linksexit 0 — "Links are valid across 17 scan roots."
check-changeset-presenceexit 0 — "declares 1 changeset(s) … Every one of them has an EMPTY frontmatter"
check-changeset-overwrite / -no-major / -fixedexit 0 each
eslint on the changed filesexit 0 — 0 errors, 0 warnings

Every exit code was captured before any pipe. The lint run is narrowed and the narrowing is measured: the population is the diff, the count comes from --format json (3 files), and eslint.config.js contains 0projectService / parserOptions / project: occurrences against a live control of 10 rules hits in the same file — not type-aware, so no untouched file's verdict can move under this diff.

Changeset: an empty frontmatter, which the presence gate names as a pass rather than a workaround. The one file it flags is under packages/components/src/ but ships nowhere — the package publishes files: ["dist", …] and its build tsconfig.json excludes src/__tests__ outright.

Corrections and deviations, stated rather than buried

The first run of all three verdicts was invalid and was thrown away. The pin file's docstring contained a glob pairing a star with a slash, which ends a block comment early; the file failed to parse, so verdict (i) was red and (ii)/(iii) reported Tests no tests — a transform error, not an assertion. vitest list --filesOnly had passed because it never transforms. Fixed, and all three verdicts above are from the re-run. This repo already records the same trap in tsconfig.scripts.json's header and in check-changeset-presence.mjs.

Three files beyond the dispatch's declared surface, each forced by a gate or by turbo's shape:

  • packages/components/package.json — one script. Turbo tasks are per-package, so dependsOn: ["build"]for the package under test cannot be expressed without a script in that package. It carries --root ../.. because the invocation guard refuses any run whose Vitest root is not the repo root, and --config vitest.config.mts because Vite resolves configFile relative to root, not cwd (measured: ../../vitest.config.mts resolved to /home/vitest.config.mts).
  • content/docs/guide/ci-cd-pipeline.md — one cell. scripts/__tests__/ci-cd-pipeline-doc.test.ts fails when the workflow runs a first-party command the job table does not name. It also fails if the cell names a command the job does not run, which is why the turbo invocation is described in prose there rather than as a code span.
  • scripts/__tests__/turbo-task-guard-coverage.test.ts — one docstring line. That test enforces the partition "cacheable ⇒ has a derived inputs guard; otherwise cache: false", and its docstring enumerates the partition. test:dist is cache: false — a lane whose subject is a build artifact turbo does not hash must never replay a verdict — so it belongs on the uncached side, and the enumeration now says so.

⛔ No change to the root alias map. ⛔ No timeout raised. ⛔ No heavy-test allowlist entry. ⛔ No skip-changeset label — in this repo that label is inert and the empty-frontmatter changeset is the declaration that counts.

Not in this PR

Nothing migrates into the new lane. Its scarcity is the guard against it becoming a second default test surface, and the ruling asks for exactly one resident.

Fix round — Type Check was red on the first push

What was red.@object-ui/console#type-check, run 33588494358 / job 100117481182. Three errors, all one cause:

../../vitest.config.mts(311,7): error TS2769: No overload matches this call.
../../vitest.config.mts(325,7): error TS2769: No overload matches this call.
vitest.config.ts(11,15): error TS2345: Argument of type 'UserConfig & …' is not assignable to parameter of type 'never'.

Cause, one line. Inside the conditional spread that declares the dist project, the literal extends: true widens to boolean, while TestProjectConfiguration.extends is string | true | undefined — so the element matched no defineConfig overload and the whole projects array degraded to never[], which then took down the console entry on line 325 too.

Fix.extends: true as const, plus a comment recording why the annotation is load-bearing (it reads like removable noise). Nothing else changed; the three pre-existing projects never widened, because their extends: true sits in the plain array rather than in a conditional one.

Why CI saw it and this PR's own gates did not. The root vitest.config.mts is compiled by no type program of its own — apps/console/vitest.config.ts merges it, so the console's tsc --noEmit is the program that reads it. That job was outside the gate set run before the first push. It is in the set now.

Reproduced before fixing, not after. On an unbuilt worktree the real errors are unreachable behind 378 TS2882s, so the dependency closure was built through turbo first (turbo run type-check --filter=@object-ui/console), which is the path CI takes:

exiterror TS lines
before the fix13
after the fix00

Re-run at the fix commit 857c8afc5 (exit codes captured by redirect-then-$?):

CheckVerdict
pnpm --filter @object-ui/console run type-checkexit 0 — 0 error TS lines (was exit 1 / 3)
pnpm --filter @object-ui/components run type-checkexit 0
pnpm type-check:scriptsexit 0
pnpm type-check:vitest-setupexit 0
pnpm type-check (turbo, every package)exit 0 — Tasks: 81 successful, 81 total
pnpm test:distexit 0 — Test Files 1 passed (1), Tasks: 9 successful, 9 total
vitest list --filesOnly --project distexit 0 — exactly 1 pin; unit/dom/dom-heavy still 0 (files 810 / 1429 / 34)
vitest run --project dist without the env varexit 1 — the opt-in refusal still fires

No rebase and no force-push: the fix is a commit on top of the branch.


🤖 Generated with Claude Code

https://claude.ai/code/session_01BGMDbrVa8JjZcCQ7DWYH1b

The root `vitest.config.mts` aliases every workspace package to its `src`,
which is right for the ~2000 tests that want fast source feedback and leaves
"does the SHIPPED BUNDLE still do X" unanswerable. Such a test could not be
committed: turbo's `test` task is `dependsOn: ["^build"]` — the DEPENDENCIES'
builds, not the package's own — so a `dist`-importing test landed in CI with no
`dist` to import. That is NOT MEASURED rather than a red pin, and the usual
repair (delete it, or let it skip when `dist` is missing) leaves a green suite
that measures nothing. Two lanes hit this wall in one morning and each threw a
correct measurement away.
Implements the PM ruling on objectui#7183 (option 1, 2026-09-02):
- `vitest.config.mts` gains a fourth project, `dist`, collecting
`packages/*/src/**/*.dist.spec.tsx`. The suffix keeps these files out of
`unit`, `dom` and `dom-heavy` BY CONSTRUCTION — none of the three needed a
character changed — and out of each package's `tsconfig.test.json`, which
matters because turbo's `type-check` waits on `^build` and must never read
the package's own `dist` (objectui#4801).
- The project is OPT-IN behind `OBJECTUI_DIST_PINS=1`. CI's test job runs
`pnpm test` with no build step, so an unconditional fourth project would be
collected there with no `dist` on disk and would fail every PR. The one
false-green this opens — `--project dist` without the env var collecting zero
files and exiting green — is refused in the config with a message naming the
right command.
- `turbo.json` gains `test:dist`, `dependsOn: ["build"]` (self, not `^build`),
`cache: false`: a lane whose subject is a build artifact turbo does not hash
must not replay a verdict. That places it on the uncached side of the
partition `scripts/__tests__/turbo-task-guard-coverage.test.ts` enforces,
whose docstring is updated to match.
- The light dom setup is deliberate, not a cost optimisation:
`vitest.setup.dom.tsx` registers `page:header` from SOURCE, which would keep
a pin green with the built bundle removed entirely.
First resident: the objectui#6252 acceptance criterion PR objectui#7180 measured
by hand and could not commit — an id-authored `page:header` resolves through the
BUILT renderer and carries no `body.source` into the DOM or the authored node.
Its live control is part of the pin: with the `dist` import removed the run
fails `expected undefined to be truthy`.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BGMDbrVa8JjZcCQ7DWYH1b
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 48 chunks)3160.2 KB3191.4 KB
Main entry chunk (gzip)142.6 KB350 KB
Entry fileindex-BOEfHVe7.js
StatusPASS

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

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)15.33KB5.59KB
app-shell (runtime-config.js)20.68KB7.36KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)514.46KB117.29KB
core (index.js)5.55KB2.23KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)178.20KB49.60KB
fields (index.js)244.25KB61.73KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.98KB10.98KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.92KB12.93KB
plugin-charts (index.js)70.02KB19.44KB
plugin-chatbot (index.js)190.53KB45.18KB
plugin-dashboard (index.js)132.63KB34.56KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)250.65KB63.91KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)132.78KB32.58KB
plugin-gantt (index.js)166.77KB40.76KB
plugin-grid (index.js)208.88KB56.59KB
plugin-kanban (index.js)53.21KB14.66KB
plugin-list (index.js)113.51KB27.67KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)29.34KB8.47KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.90KB21.12KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)81.07KB26.86KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.72KB2.24KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)3.40KB1.71KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

@yinlianghuiClaude

Copy link
Copy Markdown
CollaboratorAuthor

CI red on 7a6eef7b9Type Check (job 100117481182), this PR's own defect, fix in flight.

@object-ui/console#type-check runs tsc --noEmit over apps/console/vitest.config.ts, which imports the root vitest.config.mts. The new dist project entry inside the conditional spread is inferred as { extends: boolean; … } (the true literal widens inside the array in the conditional), and TestProjectConfiguration.extends is string | true | undefined, so ../../vitest.config.mts(311,7) / (325,7) report TS2769 and the console config fails at (11,15). The fix is keeping the literal narrow (extends: true as const); the dev is reproducing on pnpm --filter @object-ui/console run type-check before and after, re-running the type programs, and pushing a fix commit on this branch. The local verification ran the components package's type program only, which cannot see the console's read of the root config — the fix round will say so.


Generated by Claude Code

… still type-checks
The `dist` project entry added for objectui#7183 sits inside a conditional
spread, and inside that array literal `extends: true` widens to `boolean`.
`TestProjectConfiguration.extends` is `string | true | undefined`, so the widened
element matches no `defineConfig` overload and the whole `projects` array
degrades to `never[]`:
../../vitest.config.mts(311,7): error TS2769: No overload matches this call.
../../vitest.config.mts(325,7): error TS2769: No overload matches this call.
vitest.config.ts(11,15): error TS2345: Argument of type 'UserConfig & …'
is not assignable to parameter of type 'never'.
CI reported it on `@object-ui/console#type-check` rather than here, because
`apps/console/vitest.config.ts` merges this config and is the type program that
reads it — the root `.mts` is compiled by nobody on its own.
`as const` keeps the literal narrow. The three pre-existing projects are
unaffected: their `extends: true` sits in the plain array, where it never
widened. A comment records why the annotation is load-bearing, since it reads
like removable noise.
Measured red -> green on the reported program, deps built through turbo first
(the errors are unreachable behind 378 TS2882s on an unbuilt tree):
before exit 1, 3 errors
after exit 0, 0 errors
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BGMDbrVa8JjZcCQ7DWYH1b
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 48 chunks)3160.2 KB3191.4 KB
Main entry chunk (gzip)142.6 KB350 KB
Entry fileindex-BOEfHVe7.js
StatusPASS

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

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)15.33KB5.59KB
app-shell (runtime-config.js)20.68KB7.36KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)514.46KB117.29KB
core (index.js)5.55KB2.23KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)178.20KB49.60KB
fields (index.js)244.25KB61.73KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.98KB10.98KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.92KB12.93KB
plugin-charts (index.js)70.02KB19.44KB
plugin-chatbot (index.js)190.53KB45.18KB
plugin-dashboard (index.js)132.63KB34.56KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)250.65KB63.91KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)132.78KB32.58KB
plugin-gantt (index.js)166.77KB40.76KB
plugin-grid (index.js)208.88KB56.59KB
plugin-kanban (index.js)53.21KB14.66KB
plugin-list (index.js)113.51KB27.67KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)29.34KB8.47KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.90KB21.12KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)81.07KB26.86KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.72KB2.24KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)3.40KB1.71KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

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

Projects

None yet

2 participants

@yinlianghui@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

test(ci): a dist vitest project so built-artifact claims can be pinned - #7291

Merged
yinlianghui merged 3 commits into
mainfrom
claude/issue-7183-dist-vitest-project
Sep 2, 2026
Merged

test(ci): a dist vitest project so built-artifact claims can be pinned#7291
yinlianghui merged 3 commits into
mainfrom
claude/issue-7183-dist-vitest-project

Conversation

@yinlianghui

@yinlianghuiyinlianghui commented Sep 2, 2026

Copy link
Copy Markdown
Collaborator

Fixes#7183

Implements the PM ruling on this card (2026-09-02, option 1): a dedicated dist vitest project with its own turbo task that builds the package under test first.

The gap

The root vitest.config.mts aliases every workspace package to its src. That is right for the ~2000 tests that want fast source feedback, and it makes "does the SHIPPED BUNDLE still do X" structurally unanswerable. Such a test could not be committed either: turbo's test task is dependsOn: ["^build"] — the DEPENDENCIES' builds, never the package's own — so a dist-importing test landed in CI with no dist to import. That is NOT MEASURED rather than a red pin, and the usual repair (delete it, or let it skip when dist is missing) leaves a green suite that measures nothing. Two lanes hit this wall in one morning and each threw a correct measurement away.

The five binding constraints, and where each is met

1. Only built-artifact pins; first resident re-derived from PR #7180; no source-resolved test moves in.
The project collects exactly one glob, packages/*/src/**/*.dist.spec.tsx (spelled in the config, not here — a star-slash pair inside a block comment ends the comment, which this PR learned the hard way, see Corrections below). Its only resident is packages/components/src/__tests__/page-header-action-ids.dist.spec.tsx: an id-authored page:header resolves an action whose definition carries a script body, and the marker reaches neither the rendered DOM nor the authored node. Nothing was moved into the project; no existing test changed.

2. dependsOn: ["build"] for the package under test (self, not ^build), and a loud precondition — not a skip — when dist is absent.
turbo.json gains test:dist with dependsOn: ["build"] and cache: false. The precondition is an explicit assertion that reads the built entry from the package's own package.jsonexports["."].import and names the absolute path when it is missing. It fails; it does not skip.

3. The live control from PR #7180 is part of the delivery.
registers page:header from the BUILT bundle is a bare toBeTruthy() on purpose — a custom message would change the string the control is recorded by. See verdict (iii).

4. Wired where the existing test job runs; no timeout raised, no heavy-test allowlist entry.
One step in the existing test job of .github/workflows/ci.yml, if: … && matrix.shard == 1. timeout-minutes: 20 is untouched, heavyDomTests is untouched, and the three existing projects (unit, dom, dom-heavy) are untouched.

5. The step-2 tripwire. Not tripped, but it was close, and the near-miss is the design: see The opt-in below.

The opt-in — the half that keeps pnpm test unchanged

CI's test job runs pnpm test (vitest run) with no build step anywhere in it. An unconditional fourth project is therefore collected by that run with no dist on disk, and its precondition would fail the whole suite on every PR — which would have forced a build into the path of all ~2000 tests, i.e. exactly the "change to how all tests build" constraint 5 says to stop at.

So the project is declared only when OBJECTUI_DIST_PINS=1. An env var rather than the --project dist flag, because argv is meaningful only in the process that parsed the CLI while the env var is inherited by everything Vitest spawns.

That opens exactly one false-green, and it is closed in the config rather than documented: vitest run --project dist without the env var would match no project, and passWithNoTests is true for a run that names no files — a green that measured nothing. Measured:

$ pnpm exec vitest run --project dist # no env var
exit=1
Error: vitest --project dist was requested, but OBJECTUI_DIST_PINS is not "1", so the
`dist` project is NOT declared and this run would collect ZERO files and exit GREEN.

The three verdicts

Each quotes the run's own output. Both mutating legs restore under a trap with absolute paths and prove the mutation landed on disk by marker count — never by an editor's exit code.

(i) dist present, run the way CI runs it — GREEN.

$ pnpm test:dist
V1_EXIT=0
Test Files 1 passed (1)
Tasks: 9 successful, 9 total

(ii) dist deleted — a loud, named precondition FAILURE, not a skip and not MODULE_NOT_FOUND.

MUTATION PROVEN: test -e /home/user/objectui-issue-7183/packages/components/dist => false
V2_EXIT=1
× precondition: the package under test has been built 9ms
AssertionError: The built entry /home/user/objectui-issue-7183/packages/components/dist/index.js
does not exist, so this built-artifact pin has NOTHING to measure. It must fail rather than
skip. […]: expected false to be true
Test Files 1 failed (1)
Tests 3 failed (3)

Restore proven by state, not by an exit code: sha256 of the restored dist/index.js equals the recorded 5365d829….

(iii) the dist import removed — the recorded control.

PRE target-count=1 PRE_HASH=1f531adf… HEAD_BLOB=1f531adf…
POST target-count=0 inject-count=1 POST_HASH=8b6277f1…
MUTATION PROVEN ON DISK (target 1->0, inject 0->1, blob hash moved)
V3_EXIT=1
× registers page:header from the BUILT bundle 6ms
AssertionError: expected undefined to be truthy
Test Files 1 failed (1)
Tests 2 failed | 1 passed (3)

expected undefined to be truthy — verbatim what PR #7180 recorded. The one case that still passes is the precondition, which is correct: dist is on disk in this leg; only the import is gone. Restore proven by state: restored blob 1f531adf… equals the HEAD blob, and git diff HEAD and git status --short are both empty.

Cost — measured, because the ruling priced this at "one build in one job"

A bare turbo run test:dist does not cost one build. Turbo applies a task definition to every package in scope and resolves dependsOn: ["build"] for each, so it schedules a build of the entire monoreposite and console included:

$ turbo run test:dist --dry=json → 45 tasks (44 builds + 1 test:dist)
$ turbo run test:dist --filter=@object-ui/components --dry=json
→ 9 tasks
components#build components#test:dist core#build data-objectstack#build
i18n#build react#build react-runtime#build sdui-parser#build types#build

The root script therefore carries --filter=@object-ui/components, and the 9 tasks are the package plus the dependency closure its .d.ts emit genuinely needs. Cold-cache wall time, which is what CI pays:

COLD_EXIT=0 COLD_WALL_SECONDS=72
Tasks: 9 successful, 9 total
Cached: 0 cached, 9 total
Time: 1m10.762s

~72 s added to shard 1 of a job whose ceiling is timeout-minutes: 20, unchanged. (Shared-box seconds: four sibling agents build in this container, so this is an upper-ish bound, not an idle-box figure.)

If a second package ever gains a pin without being added to the filter, the failure is the loud precondition naming its missing built entry — not a silent skip.

Collected by exactly one project

vitest list --filesOnly is the instrument, and it has a positive control:

unit: files=810 pin=0
dom: files=1429 pin=0
dom-heavy: files=34 pin=0
dist: [dist] packages/components/src/__tests__/page-header-action-ids.dist.spec.tsx

The suffix does this by construction — unit collects *.test.ts, dom collects *.test.tsx, dom-heavy is an explicit file list — so none of the three needed a character changed.

The same property keeps the file out of packages/components/tsconfig.test.json, and that is deliberate rather than incidental: turbo's type-check waits on ^build, so a type program that read this package's own dist would demand an artifact type-check is not allowed to wait for — the coupling #4801 removed. Measured with the package's own type program, with a control:

$ tsc -p tsconfig.test.json --listFiles
page-header-action-ids.dist.spec.tsx → 0 files (out of the program, as designed)
page-header-action-ids.test.tsx → 1 file (control: siblings ARE in it)

Gates — union re-run at commit 7a6eef7b9

GateVerdict line
vitest run scripts/exit 0 — Test Files 94 passed (94) / Tests 2645 passed (2645)
pnpm --filter @object-ui/components run type-checkexit 0
pnpm check:control-bytesexit 0 — "OK (scanned 6010 tracked text file(s); skipped 85 binary)"
pnpm type-check:scriptsexit 0
pnpm check:doc-fencesexit 0 — "every TypeScript block in 224 document(s) is fenced…"
pnpm check:doc-typesexit 0 — "Every documented component type is registered."
pnpm docs:check-linksexit 0 — "Links are valid across 17 scan roots."
check-changeset-presenceexit 0 — "declares 1 changeset(s) … Every one of them has an EMPTY frontmatter"
check-changeset-overwrite / -no-major / -fixedexit 0 each
eslint on the changed filesexit 0 — 0 errors, 0 warnings

Every exit code was captured before any pipe. The lint run is narrowed and the narrowing is measured: the population is the diff, the count comes from --format json (3 files), and eslint.config.js contains 0projectService / parserOptions / project: occurrences against a live control of 10 rules hits in the same file — not type-aware, so no untouched file's verdict can move under this diff.

Changeset: an empty frontmatter, which the presence gate names as a pass rather than a workaround. The one file it flags is under packages/components/src/ but ships nowhere — the package publishes files: ["dist", …] and its build tsconfig.json excludes src/__tests__ outright.

Corrections and deviations, stated rather than buried

The first run of all three verdicts was invalid and was thrown away. The pin file's docstring contained a glob pairing a star with a slash, which ends a block comment early; the file failed to parse, so verdict (i) was red and (ii)/(iii) reported Tests no tests — a transform error, not an assertion. vitest list --filesOnly had passed because it never transforms. Fixed, and all three verdicts above are from the re-run. This repo already records the same trap in tsconfig.scripts.json's header and in check-changeset-presence.mjs.

Three files beyond the dispatch's declared surface, each forced by a gate or by turbo's shape:

  • packages/components/package.json — one script. Turbo tasks are per-package, so dependsOn: ["build"]for the package under test cannot be expressed without a script in that package. It carries --root ../.. because the invocation guard refuses any run whose Vitest root is not the repo root, and --config vitest.config.mts because Vite resolves configFile relative to root, not cwd (measured: ../../vitest.config.mts resolved to /home/vitest.config.mts).
  • content/docs/guide/ci-cd-pipeline.md — one cell. scripts/__tests__/ci-cd-pipeline-doc.test.ts fails when the workflow runs a first-party command the job table does not name. It also fails if the cell names a command the job does not run, which is why the turbo invocation is described in prose there rather than as a code span.
  • scripts/__tests__/turbo-task-guard-coverage.test.ts — one docstring line. That test enforces the partition "cacheable ⇒ has a derived inputs guard; otherwise cache: false", and its docstring enumerates the partition. test:dist is cache: false — a lane whose subject is a build artifact turbo does not hash must never replay a verdict — so it belongs on the uncached side, and the enumeration now says so.

⛔ No change to the root alias map. ⛔ No timeout raised. ⛔ No heavy-test allowlist entry. ⛔ No skip-changeset label — in this repo that label is inert and the empty-frontmatter changeset is the declaration that counts.

Not in this PR

Nothing migrates into the new lane. Its scarcity is the guard against it becoming a second default test surface, and the ruling asks for exactly one resident.

Fix round — Type Check was red on the first push

What was red.@object-ui/console#type-check, run 33588494358 / job 100117481182. Three errors, all one cause:

../../vitest.config.mts(311,7): error TS2769: No overload matches this call.
../../vitest.config.mts(325,7): error TS2769: No overload matches this call.
vitest.config.ts(11,15): error TS2345: Argument of type 'UserConfig & …' is not assignable to parameter of type 'never'.

Cause, one line. Inside the conditional spread that declares the dist project, the literal extends: true widens to boolean, while TestProjectConfiguration.extends is string | true | undefined — so the element matched no defineConfig overload and the whole projects array degraded to never[], which then took down the console entry on line 325 too.

Fix.extends: true as const, plus a comment recording why the annotation is load-bearing (it reads like removable noise). Nothing else changed; the three pre-existing projects never widened, because their extends: true sits in the plain array rather than in a conditional one.

Why CI saw it and this PR's own gates did not. The root vitest.config.mts is compiled by no type program of its own — apps/console/vitest.config.ts merges it, so the console's tsc --noEmit is the program that reads it. That job was outside the gate set run before the first push. It is in the set now.

Reproduced before fixing, not after. On an unbuilt worktree the real errors are unreachable behind 378 TS2882s, so the dependency closure was built through turbo first (turbo run type-check --filter=@object-ui/console), which is the path CI takes:

exiterror TS lines
before the fix13
after the fix00

Re-run at the fix commit 857c8afc5 (exit codes captured by redirect-then-$?):

CheckVerdict
pnpm --filter @object-ui/console run type-checkexit 0 — 0 error TS lines (was exit 1 / 3)
pnpm --filter @object-ui/components run type-checkexit 0
pnpm type-check:scriptsexit 0
pnpm type-check:vitest-setupexit 0
pnpm type-check (turbo, every package)exit 0 — Tasks: 81 successful, 81 total
pnpm test:distexit 0 — Test Files 1 passed (1), Tasks: 9 successful, 9 total
vitest list --filesOnly --project distexit 0 — exactly 1 pin; unit/dom/dom-heavy still 0 (files 810 / 1429 / 34)
vitest run --project dist without the env varexit 1 — the opt-in refusal still fires

No rebase and no force-push: the fix is a commit on top of the branch.


🤖 Generated with Claude Code

https://claude.ai/code/session_01BGMDbrVa8JjZcCQ7DWYH1b

The root `vitest.config.mts` aliases every workspace package to its `src`,
which is right for the ~2000 tests that want fast source feedback and leaves
"does the SHIPPED BUNDLE still do X" unanswerable. Such a test could not be
committed: turbo's `test` task is `dependsOn: ["^build"]` — the DEPENDENCIES'
builds, not the package's own — so a `dist`-importing test landed in CI with no
`dist` to import. That is NOT MEASURED rather than a red pin, and the usual
repair (delete it, or let it skip when `dist` is missing) leaves a green suite
that measures nothing. Two lanes hit this wall in one morning and each threw a
correct measurement away.
Implements the PM ruling on objectui#7183 (option 1, 2026-09-02):
- `vitest.config.mts` gains a fourth project, `dist`, collecting
`packages/*/src/**/*.dist.spec.tsx`. The suffix keeps these files out of
`unit`, `dom` and `dom-heavy` BY CONSTRUCTION — none of the three needed a
character changed — and out of each package's `tsconfig.test.json`, which
matters because turbo's `type-check` waits on `^build` and must never read
the package's own `dist` (objectui#4801).
- The project is OPT-IN behind `OBJECTUI_DIST_PINS=1`. CI's test job runs
`pnpm test` with no build step, so an unconditional fourth project would be
collected there with no `dist` on disk and would fail every PR. The one
false-green this opens — `--project dist` without the env var collecting zero
files and exiting green — is refused in the config with a message naming the
right command.
- `turbo.json` gains `test:dist`, `dependsOn: ["build"]` (self, not `^build`),
`cache: false`: a lane whose subject is a build artifact turbo does not hash
must not replay a verdict. That places it on the uncached side of the
partition `scripts/__tests__/turbo-task-guard-coverage.test.ts` enforces,
whose docstring is updated to match.
- The light dom setup is deliberate, not a cost optimisation:
`vitest.setup.dom.tsx` registers `page:header` from SOURCE, which would keep
a pin green with the built bundle removed entirely.
First resident: the objectui#6252 acceptance criterion PR objectui#7180 measured
by hand and could not commit — an id-authored `page:header` resolves through the
BUILT renderer and carries no `body.source` into the DOM or the authored node.
Its live control is part of the pin: with the `dist` import removed the run
fails `expected undefined to be truthy`.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BGMDbrVa8JjZcCQ7DWYH1b
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 48 chunks)3160.2 KB3191.4 KB
Main entry chunk (gzip)142.6 KB350 KB
Entry fileindex-BOEfHVe7.js
StatusPASS

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

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)15.33KB5.59KB
app-shell (runtime-config.js)20.68KB7.36KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)514.46KB117.29KB
core (index.js)5.55KB2.23KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)178.20KB49.60KB
fields (index.js)244.25KB61.73KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.98KB10.98KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.92KB12.93KB
plugin-charts (index.js)70.02KB19.44KB
plugin-chatbot (index.js)190.53KB45.18KB
plugin-dashboard (index.js)132.63KB34.56KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)250.65KB63.91KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)132.78KB32.58KB
plugin-gantt (index.js)166.77KB40.76KB
plugin-grid (index.js)208.88KB56.59KB
plugin-kanban (index.js)53.21KB14.66KB
plugin-list (index.js)113.51KB27.67KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)29.34KB8.47KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.90KB21.12KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)81.07KB26.86KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.72KB2.24KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)3.40KB1.71KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

@yinlianghuiClaude

Copy link
Copy Markdown
CollaboratorAuthor

CI red on 7a6eef7b9Type Check (job 100117481182), this PR's own defect, fix in flight.

@object-ui/console#type-check runs tsc --noEmit over apps/console/vitest.config.ts, which imports the root vitest.config.mts. The new dist project entry inside the conditional spread is inferred as { extends: boolean; … } (the true literal widens inside the array in the conditional), and TestProjectConfiguration.extends is string | true | undefined, so ../../vitest.config.mts(311,7) / (325,7) report TS2769 and the console config fails at (11,15). The fix is keeping the literal narrow (extends: true as const); the dev is reproducing on pnpm --filter @object-ui/console run type-check before and after, re-running the type programs, and pushing a fix commit on this branch. The local verification ran the components package's type program only, which cannot see the console's read of the root config — the fix round will say so.


Generated by Claude Code

… still type-checks
The `dist` project entry added for objectui#7183 sits inside a conditional
spread, and inside that array literal `extends: true` widens to `boolean`.
`TestProjectConfiguration.extends` is `string | true | undefined`, so the widened
element matches no `defineConfig` overload and the whole `projects` array
degrades to `never[]`:
../../vitest.config.mts(311,7): error TS2769: No overload matches this call.
../../vitest.config.mts(325,7): error TS2769: No overload matches this call.
vitest.config.ts(11,15): error TS2345: Argument of type 'UserConfig & …'
is not assignable to parameter of type 'never'.
CI reported it on `@object-ui/console#type-check` rather than here, because
`apps/console/vitest.config.ts` merges this config and is the type program that
reads it — the root `.mts` is compiled by nobody on its own.
`as const` keeps the literal narrow. The three pre-existing projects are
unaffected: their `extends: true` sits in the plain array, where it never
widened. A comment records why the annotation is load-bearing, since it reads
like removable noise.
Measured red -> green on the reported program, deps built through turbo first
(the errors are unreachable behind 378 TS2882s on an unbuilt tree):
before exit 1, 3 errors
after exit 0, 0 errors
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BGMDbrVa8JjZcCQ7DWYH1b
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 48 chunks)3160.2 KB3191.4 KB
Main entry chunk (gzip)142.6 KB350 KB
Entry fileindex-BOEfHVe7.js
StatusPASS

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

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)15.33KB5.59KB
app-shell (runtime-config.js)20.68KB7.36KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)514.46KB117.29KB
core (index.js)5.55KB2.23KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)178.20KB49.60KB
fields (index.js)244.25KB61.73KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.98KB10.98KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.92KB12.93KB
plugin-charts (index.js)70.02KB19.44KB
plugin-chatbot (index.js)190.53KB45.18KB
plugin-dashboard (index.js)132.63KB34.56KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)250.65KB63.91KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)132.78KB32.58KB
plugin-gantt (index.js)166.77KB40.76KB
plugin-grid (index.js)208.88KB56.59KB
plugin-kanban (index.js)53.21KB14.66KB
plugin-list (index.js)113.51KB27.67KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)29.34KB8.47KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.90KB21.12KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)81.07KB26.86KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.72KB2.24KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)3.40KB1.71KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

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

Projects

None yet

2 participants

@yinlianghui@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

test(ci): a dist vitest project so built-artifact claims can be pinned - #7291

Merged
yinlianghui merged 3 commits into
mainfrom
claude/issue-7183-dist-vitest-project
Sep 2, 2026
Merged

test(ci): a dist vitest project so built-artifact claims can be pinned#7291
yinlianghui merged 3 commits into
mainfrom
claude/issue-7183-dist-vitest-project

Conversation

@yinlianghui

@yinlianghuiyinlianghui commented Sep 2, 2026

Copy link
Copy Markdown
Collaborator

Fixes#7183

Implements the PM ruling on this card (2026-09-02, option 1): a dedicated dist vitest project with its own turbo task that builds the package under test first.

The gap

The root vitest.config.mts aliases every workspace package to its src. That is right for the ~2000 tests that want fast source feedback, and it makes "does the SHIPPED BUNDLE still do X" structurally unanswerable. Such a test could not be committed either: turbo's test task is dependsOn: ["^build"] — the DEPENDENCIES' builds, never the package's own — so a dist-importing test landed in CI with no dist to import. That is NOT MEASURED rather than a red pin, and the usual repair (delete it, or let it skip when dist is missing) leaves a green suite that measures nothing. Two lanes hit this wall in one morning and each threw a correct measurement away.

The five binding constraints, and where each is met

1. Only built-artifact pins; first resident re-derived from PR #7180; no source-resolved test moves in.
The project collects exactly one glob, packages/*/src/**/*.dist.spec.tsx (spelled in the config, not here — a star-slash pair inside a block comment ends the comment, which this PR learned the hard way, see Corrections below). Its only resident is packages/components/src/__tests__/page-header-action-ids.dist.spec.tsx: an id-authored page:header resolves an action whose definition carries a script body, and the marker reaches neither the rendered DOM nor the authored node. Nothing was moved into the project; no existing test changed.

2. dependsOn: ["build"] for the package under test (self, not ^build), and a loud precondition — not a skip — when dist is absent.
turbo.json gains test:dist with dependsOn: ["build"] and cache: false. The precondition is an explicit assertion that reads the built entry from the package's own package.jsonexports["."].import and names the absolute path when it is missing. It fails; it does not skip.

3. The live control from PR #7180 is part of the delivery.
registers page:header from the BUILT bundle is a bare toBeTruthy() on purpose — a custom message would change the string the control is recorded by. See verdict (iii).

4. Wired where the existing test job runs; no timeout raised, no heavy-test allowlist entry.
One step in the existing test job of .github/workflows/ci.yml, if: … && matrix.shard == 1. timeout-minutes: 20 is untouched, heavyDomTests is untouched, and the three existing projects (unit, dom, dom-heavy) are untouched.

5. The step-2 tripwire. Not tripped, but it was close, and the near-miss is the design: see The opt-in below.

The opt-in — the half that keeps pnpm test unchanged

CI's test job runs pnpm test (vitest run) with no build step anywhere in it. An unconditional fourth project is therefore collected by that run with no dist on disk, and its precondition would fail the whole suite on every PR — which would have forced a build into the path of all ~2000 tests, i.e. exactly the "change to how all tests build" constraint 5 says to stop at.

So the project is declared only when OBJECTUI_DIST_PINS=1. An env var rather than the --project dist flag, because argv is meaningful only in the process that parsed the CLI while the env var is inherited by everything Vitest spawns.

That opens exactly one false-green, and it is closed in the config rather than documented: vitest run --project dist without the env var would match no project, and passWithNoTests is true for a run that names no files — a green that measured nothing. Measured:

$ pnpm exec vitest run --project dist # no env var
exit=1
Error: vitest --project dist was requested, but OBJECTUI_DIST_PINS is not "1", so the
`dist` project is NOT declared and this run would collect ZERO files and exit GREEN.

The three verdicts

Each quotes the run's own output. Both mutating legs restore under a trap with absolute paths and prove the mutation landed on disk by marker count — never by an editor's exit code.

(i) dist present, run the way CI runs it — GREEN.

$ pnpm test:dist
V1_EXIT=0
Test Files 1 passed (1)
Tasks: 9 successful, 9 total

(ii) dist deleted — a loud, named precondition FAILURE, not a skip and not MODULE_NOT_FOUND.

MUTATION PROVEN: test -e /home/user/objectui-issue-7183/packages/components/dist => false
V2_EXIT=1
× precondition: the package under test has been built 9ms
AssertionError: The built entry /home/user/objectui-issue-7183/packages/components/dist/index.js
does not exist, so this built-artifact pin has NOTHING to measure. It must fail rather than
skip. […]: expected false to be true
Test Files 1 failed (1)
Tests 3 failed (3)

Restore proven by state, not by an exit code: sha256 of the restored dist/index.js equals the recorded 5365d829….

(iii) the dist import removed — the recorded control.

PRE target-count=1 PRE_HASH=1f531adf… HEAD_BLOB=1f531adf…
POST target-count=0 inject-count=1 POST_HASH=8b6277f1…
MUTATION PROVEN ON DISK (target 1->0, inject 0->1, blob hash moved)
V3_EXIT=1
× registers page:header from the BUILT bundle 6ms
AssertionError: expected undefined to be truthy
Test Files 1 failed (1)
Tests 2 failed | 1 passed (3)

expected undefined to be truthy — verbatim what PR #7180 recorded. The one case that still passes is the precondition, which is correct: dist is on disk in this leg; only the import is gone. Restore proven by state: restored blob 1f531adf… equals the HEAD blob, and git diff HEAD and git status --short are both empty.

Cost — measured, because the ruling priced this at "one build in one job"

A bare turbo run test:dist does not cost one build. Turbo applies a task definition to every package in scope and resolves dependsOn: ["build"] for each, so it schedules a build of the entire monoreposite and console included:

$ turbo run test:dist --dry=json → 45 tasks (44 builds + 1 test:dist)
$ turbo run test:dist --filter=@object-ui/components --dry=json
→ 9 tasks
components#build components#test:dist core#build data-objectstack#build
i18n#build react#build react-runtime#build sdui-parser#build types#build

The root script therefore carries --filter=@object-ui/components, and the 9 tasks are the package plus the dependency closure its .d.ts emit genuinely needs. Cold-cache wall time, which is what CI pays:

COLD_EXIT=0 COLD_WALL_SECONDS=72
Tasks: 9 successful, 9 total
Cached: 0 cached, 9 total
Time: 1m10.762s

~72 s added to shard 1 of a job whose ceiling is timeout-minutes: 20, unchanged. (Shared-box seconds: four sibling agents build in this container, so this is an upper-ish bound, not an idle-box figure.)

If a second package ever gains a pin without being added to the filter, the failure is the loud precondition naming its missing built entry — not a silent skip.

Collected by exactly one project

vitest list --filesOnly is the instrument, and it has a positive control:

unit: files=810 pin=0
dom: files=1429 pin=0
dom-heavy: files=34 pin=0
dist: [dist] packages/components/src/__tests__/page-header-action-ids.dist.spec.tsx

The suffix does this by construction — unit collects *.test.ts, dom collects *.test.tsx, dom-heavy is an explicit file list — so none of the three needed a character changed.

The same property keeps the file out of packages/components/tsconfig.test.json, and that is deliberate rather than incidental: turbo's type-check waits on ^build, so a type program that read this package's own dist would demand an artifact type-check is not allowed to wait for — the coupling #4801 removed. Measured with the package's own type program, with a control:

$ tsc -p tsconfig.test.json --listFiles
page-header-action-ids.dist.spec.tsx → 0 files (out of the program, as designed)
page-header-action-ids.test.tsx → 1 file (control: siblings ARE in it)

Gates — union re-run at commit 7a6eef7b9

GateVerdict line
vitest run scripts/exit 0 — Test Files 94 passed (94) / Tests 2645 passed (2645)
pnpm --filter @object-ui/components run type-checkexit 0
pnpm check:control-bytesexit 0 — "OK (scanned 6010 tracked text file(s); skipped 85 binary)"
pnpm type-check:scriptsexit 0
pnpm check:doc-fencesexit 0 — "every TypeScript block in 224 document(s) is fenced…"
pnpm check:doc-typesexit 0 — "Every documented component type is registered."
pnpm docs:check-linksexit 0 — "Links are valid across 17 scan roots."
check-changeset-presenceexit 0 — "declares 1 changeset(s) … Every one of them has an EMPTY frontmatter"
check-changeset-overwrite / -no-major / -fixedexit 0 each
eslint on the changed filesexit 0 — 0 errors, 0 warnings

Every exit code was captured before any pipe. The lint run is narrowed and the narrowing is measured: the population is the diff, the count comes from --format json (3 files), and eslint.config.js contains 0projectService / parserOptions / project: occurrences against a live control of 10 rules hits in the same file — not type-aware, so no untouched file's verdict can move under this diff.

Changeset: an empty frontmatter, which the presence gate names as a pass rather than a workaround. The one file it flags is under packages/components/src/ but ships nowhere — the package publishes files: ["dist", …] and its build tsconfig.json excludes src/__tests__ outright.

Corrections and deviations, stated rather than buried

The first run of all three verdicts was invalid and was thrown away. The pin file's docstring contained a glob pairing a star with a slash, which ends a block comment early; the file failed to parse, so verdict (i) was red and (ii)/(iii) reported Tests no tests — a transform error, not an assertion. vitest list --filesOnly had passed because it never transforms. Fixed, and all three verdicts above are from the re-run. This repo already records the same trap in tsconfig.scripts.json's header and in check-changeset-presence.mjs.

Three files beyond the dispatch's declared surface, each forced by a gate or by turbo's shape:

  • packages/components/package.json — one script. Turbo tasks are per-package, so dependsOn: ["build"]for the package under test cannot be expressed without a script in that package. It carries --root ../.. because the invocation guard refuses any run whose Vitest root is not the repo root, and --config vitest.config.mts because Vite resolves configFile relative to root, not cwd (measured: ../../vitest.config.mts resolved to /home/vitest.config.mts).
  • content/docs/guide/ci-cd-pipeline.md — one cell. scripts/__tests__/ci-cd-pipeline-doc.test.ts fails when the workflow runs a first-party command the job table does not name. It also fails if the cell names a command the job does not run, which is why the turbo invocation is described in prose there rather than as a code span.
  • scripts/__tests__/turbo-task-guard-coverage.test.ts — one docstring line. That test enforces the partition "cacheable ⇒ has a derived inputs guard; otherwise cache: false", and its docstring enumerates the partition. test:dist is cache: false — a lane whose subject is a build artifact turbo does not hash must never replay a verdict — so it belongs on the uncached side, and the enumeration now says so.

⛔ No change to the root alias map. ⛔ No timeout raised. ⛔ No heavy-test allowlist entry. ⛔ No skip-changeset label — in this repo that label is inert and the empty-frontmatter changeset is the declaration that counts.

Not in this PR

Nothing migrates into the new lane. Its scarcity is the guard against it becoming a second default test surface, and the ruling asks for exactly one resident.

Fix round — Type Check was red on the first push

What was red.@object-ui/console#type-check, run 33588494358 / job 100117481182. Three errors, all one cause:

../../vitest.config.mts(311,7): error TS2769: No overload matches this call.
../../vitest.config.mts(325,7): error TS2769: No overload matches this call.
vitest.config.ts(11,15): error TS2345: Argument of type 'UserConfig & …' is not assignable to parameter of type 'never'.

Cause, one line. Inside the conditional spread that declares the dist project, the literal extends: true widens to boolean, while TestProjectConfiguration.extends is string | true | undefined — so the element matched no defineConfig overload and the whole projects array degraded to never[], which then took down the console entry on line 325 too.

Fix.extends: true as const, plus a comment recording why the annotation is load-bearing (it reads like removable noise). Nothing else changed; the three pre-existing projects never widened, because their extends: true sits in the plain array rather than in a conditional one.

Why CI saw it and this PR's own gates did not. The root vitest.config.mts is compiled by no type program of its own — apps/console/vitest.config.ts merges it, so the console's tsc --noEmit is the program that reads it. That job was outside the gate set run before the first push. It is in the set now.

Reproduced before fixing, not after. On an unbuilt worktree the real errors are unreachable behind 378 TS2882s, so the dependency closure was built through turbo first (turbo run type-check --filter=@object-ui/console), which is the path CI takes:

exiterror TS lines
before the fix13
after the fix00

Re-run at the fix commit 857c8afc5 (exit codes captured by redirect-then-$?):

CheckVerdict
pnpm --filter @object-ui/console run type-checkexit 0 — 0 error TS lines (was exit 1 / 3)
pnpm --filter @object-ui/components run type-checkexit 0
pnpm type-check:scriptsexit 0
pnpm type-check:vitest-setupexit 0
pnpm type-check (turbo, every package)exit 0 — Tasks: 81 successful, 81 total
pnpm test:distexit 0 — Test Files 1 passed (1), Tasks: 9 successful, 9 total
vitest list --filesOnly --project distexit 0 — exactly 1 pin; unit/dom/dom-heavy still 0 (files 810 / 1429 / 34)
vitest run --project dist without the env varexit 1 — the opt-in refusal still fires

No rebase and no force-push: the fix is a commit on top of the branch.


🤖 Generated with Claude Code

https://claude.ai/code/session_01BGMDbrVa8JjZcCQ7DWYH1b

The root `vitest.config.mts` aliases every workspace package to its `src`,
which is right for the ~2000 tests that want fast source feedback and leaves
"does the SHIPPED BUNDLE still do X" unanswerable. Such a test could not be
committed: turbo's `test` task is `dependsOn: ["^build"]` — the DEPENDENCIES'
builds, not the package's own — so a `dist`-importing test landed in CI with no
`dist` to import. That is NOT MEASURED rather than a red pin, and the usual
repair (delete it, or let it skip when `dist` is missing) leaves a green suite
that measures nothing. Two lanes hit this wall in one morning and each threw a
correct measurement away.
Implements the PM ruling on objectui#7183 (option 1, 2026-09-02):
- `vitest.config.mts` gains a fourth project, `dist`, collecting
`packages/*/src/**/*.dist.spec.tsx`. The suffix keeps these files out of
`unit`, `dom` and `dom-heavy` BY CONSTRUCTION — none of the three needed a
character changed — and out of each package's `tsconfig.test.json`, which
matters because turbo's `type-check` waits on `^build` and must never read
the package's own `dist` (objectui#4801).
- The project is OPT-IN behind `OBJECTUI_DIST_PINS=1`. CI's test job runs
`pnpm test` with no build step, so an unconditional fourth project would be
collected there with no `dist` on disk and would fail every PR. The one
false-green this opens — `--project dist` without the env var collecting zero
files and exiting green — is refused in the config with a message naming the
right command.
- `turbo.json` gains `test:dist`, `dependsOn: ["build"]` (self, not `^build`),
`cache: false`: a lane whose subject is a build artifact turbo does not hash
must not replay a verdict. That places it on the uncached side of the
partition `scripts/__tests__/turbo-task-guard-coverage.test.ts` enforces,
whose docstring is updated to match.
- The light dom setup is deliberate, not a cost optimisation:
`vitest.setup.dom.tsx` registers `page:header` from SOURCE, which would keep
a pin green with the built bundle removed entirely.
First resident: the objectui#6252 acceptance criterion PR objectui#7180 measured
by hand and could not commit — an id-authored `page:header` resolves through the
BUILT renderer and carries no `body.source` into the DOM or the authored node.
Its live control is part of the pin: with the `dist` import removed the run
fails `expected undefined to be truthy`.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BGMDbrVa8JjZcCQ7DWYH1b
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 48 chunks)3160.2 KB3191.4 KB
Main entry chunk (gzip)142.6 KB350 KB
Entry fileindex-BOEfHVe7.js
StatusPASS

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

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)15.33KB5.59KB
app-shell (runtime-config.js)20.68KB7.36KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)514.46KB117.29KB
core (index.js)5.55KB2.23KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)178.20KB49.60KB
fields (index.js)244.25KB61.73KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.98KB10.98KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.92KB12.93KB
plugin-charts (index.js)70.02KB19.44KB
plugin-chatbot (index.js)190.53KB45.18KB
plugin-dashboard (index.js)132.63KB34.56KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)250.65KB63.91KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)132.78KB32.58KB
plugin-gantt (index.js)166.77KB40.76KB
plugin-grid (index.js)208.88KB56.59KB
plugin-kanban (index.js)53.21KB14.66KB
plugin-list (index.js)113.51KB27.67KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)29.34KB8.47KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.90KB21.12KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)81.07KB26.86KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.72KB2.24KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)3.40KB1.71KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

@yinlianghuiClaude

Copy link
Copy Markdown
CollaboratorAuthor

CI red on 7a6eef7b9Type Check (job 100117481182), this PR's own defect, fix in flight.

@object-ui/console#type-check runs tsc --noEmit over apps/console/vitest.config.ts, which imports the root vitest.config.mts. The new dist project entry inside the conditional spread is inferred as { extends: boolean; … } (the true literal widens inside the array in the conditional), and TestProjectConfiguration.extends is string | true | undefined, so ../../vitest.config.mts(311,7) / (325,7) report TS2769 and the console config fails at (11,15). The fix is keeping the literal narrow (extends: true as const); the dev is reproducing on pnpm --filter @object-ui/console run type-check before and after, re-running the type programs, and pushing a fix commit on this branch. The local verification ran the components package's type program only, which cannot see the console's read of the root config — the fix round will say so.


Generated by Claude Code

… still type-checks
The `dist` project entry added for objectui#7183 sits inside a conditional
spread, and inside that array literal `extends: true` widens to `boolean`.
`TestProjectConfiguration.extends` is `string | true | undefined`, so the widened
element matches no `defineConfig` overload and the whole `projects` array
degrades to `never[]`:
../../vitest.config.mts(311,7): error TS2769: No overload matches this call.
../../vitest.config.mts(325,7): error TS2769: No overload matches this call.
vitest.config.ts(11,15): error TS2345: Argument of type 'UserConfig & …'
is not assignable to parameter of type 'never'.
CI reported it on `@object-ui/console#type-check` rather than here, because
`apps/console/vitest.config.ts` merges this config and is the type program that
reads it — the root `.mts` is compiled by nobody on its own.
`as const` keeps the literal narrow. The three pre-existing projects are
unaffected: their `extends: true` sits in the plain array, where it never
widened. A comment records why the annotation is load-bearing, since it reads
like removable noise.
Measured red -> green on the reported program, deps built through turbo first
(the errors are unreachable behind 378 TS2882s on an unbuilt tree):
before exit 1, 3 errors
after exit 0, 0 errors
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BGMDbrVa8JjZcCQ7DWYH1b
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 48 chunks)3160.2 KB3191.4 KB
Main entry chunk (gzip)142.6 KB350 KB
Entry fileindex-BOEfHVe7.js
StatusPASS

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

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)15.33KB5.59KB
app-shell (runtime-config.js)20.68KB7.36KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)514.46KB117.29KB
core (index.js)5.55KB2.23KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)178.20KB49.60KB
fields (index.js)244.25KB61.73KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.98KB10.98KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.92KB12.93KB
plugin-charts (index.js)70.02KB19.44KB
plugin-chatbot (index.js)190.53KB45.18KB
plugin-dashboard (index.js)132.63KB34.56KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)250.65KB63.91KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)132.78KB32.58KB
plugin-gantt (index.js)166.77KB40.76KB
plugin-grid (index.js)208.88KB56.59KB
plugin-kanban (index.js)53.21KB14.66KB
plugin-list (index.js)113.51KB27.67KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)29.34KB8.47KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.90KB21.12KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)81.07KB26.86KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.72KB2.24KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)3.40KB1.71KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

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

Projects

None yet

2 participants

@yinlianghui@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

test(ci): a dist vitest project so built-artifact claims can be pinned - #7291

Merged
yinlianghui merged 3 commits into
mainfrom
claude/issue-7183-dist-vitest-project
Sep 2, 2026
Merged

test(ci): a dist vitest project so built-artifact claims can be pinned#7291
yinlianghui merged 3 commits into
mainfrom
claude/issue-7183-dist-vitest-project

Conversation

@yinlianghui

@yinlianghuiyinlianghui commented Sep 2, 2026

Copy link
Copy Markdown
Collaborator

Fixes#7183

Implements the PM ruling on this card (2026-09-02, option 1): a dedicated dist vitest project with its own turbo task that builds the package under test first.

The gap

The root vitest.config.mts aliases every workspace package to its src. That is right for the ~2000 tests that want fast source feedback, and it makes "does the SHIPPED BUNDLE still do X" structurally unanswerable. Such a test could not be committed either: turbo's test task is dependsOn: ["^build"] — the DEPENDENCIES' builds, never the package's own — so a dist-importing test landed in CI with no dist to import. That is NOT MEASURED rather than a red pin, and the usual repair (delete it, or let it skip when dist is missing) leaves a green suite that measures nothing. Two lanes hit this wall in one morning and each threw a correct measurement away.

The five binding constraints, and where each is met

1. Only built-artifact pins; first resident re-derived from PR #7180; no source-resolved test moves in.
The project collects exactly one glob, packages/*/src/**/*.dist.spec.tsx (spelled in the config, not here — a star-slash pair inside a block comment ends the comment, which this PR learned the hard way, see Corrections below). Its only resident is packages/components/src/__tests__/page-header-action-ids.dist.spec.tsx: an id-authored page:header resolves an action whose definition carries a script body, and the marker reaches neither the rendered DOM nor the authored node. Nothing was moved into the project; no existing test changed.

2. dependsOn: ["build"] for the package under test (self, not ^build), and a loud precondition — not a skip — when dist is absent.
turbo.json gains test:dist with dependsOn: ["build"] and cache: false. The precondition is an explicit assertion that reads the built entry from the package's own package.jsonexports["."].import and names the absolute path when it is missing. It fails; it does not skip.

3. The live control from PR #7180 is part of the delivery.
registers page:header from the BUILT bundle is a bare toBeTruthy() on purpose — a custom message would change the string the control is recorded by. See verdict (iii).

4. Wired where the existing test job runs; no timeout raised, no heavy-test allowlist entry.
One step in the existing test job of .github/workflows/ci.yml, if: … && matrix.shard == 1. timeout-minutes: 20 is untouched, heavyDomTests is untouched, and the three existing projects (unit, dom, dom-heavy) are untouched.

5. The step-2 tripwire. Not tripped, but it was close, and the near-miss is the design: see The opt-in below.

The opt-in — the half that keeps pnpm test unchanged

CI's test job runs pnpm test (vitest run) with no build step anywhere in it. An unconditional fourth project is therefore collected by that run with no dist on disk, and its precondition would fail the whole suite on every PR — which would have forced a build into the path of all ~2000 tests, i.e. exactly the "change to how all tests build" constraint 5 says to stop at.

So the project is declared only when OBJECTUI_DIST_PINS=1. An env var rather than the --project dist flag, because argv is meaningful only in the process that parsed the CLI while the env var is inherited by everything Vitest spawns.

That opens exactly one false-green, and it is closed in the config rather than documented: vitest run --project dist without the env var would match no project, and passWithNoTests is true for a run that names no files — a green that measured nothing. Measured:

$ pnpm exec vitest run --project dist # no env var
exit=1
Error: vitest --project dist was requested, but OBJECTUI_DIST_PINS is not "1", so the
`dist` project is NOT declared and this run would collect ZERO files and exit GREEN.

The three verdicts

Each quotes the run's own output. Both mutating legs restore under a trap with absolute paths and prove the mutation landed on disk by marker count — never by an editor's exit code.

(i) dist present, run the way CI runs it — GREEN.

$ pnpm test:dist
V1_EXIT=0
Test Files 1 passed (1)
Tasks: 9 successful, 9 total

(ii) dist deleted — a loud, named precondition FAILURE, not a skip and not MODULE_NOT_FOUND.

MUTATION PROVEN: test -e /home/user/objectui-issue-7183/packages/components/dist => false
V2_EXIT=1
× precondition: the package under test has been built 9ms
AssertionError: The built entry /home/user/objectui-issue-7183/packages/components/dist/index.js
does not exist, so this built-artifact pin has NOTHING to measure. It must fail rather than
skip. […]: expected false to be true
Test Files 1 failed (1)
Tests 3 failed (3)

Restore proven by state, not by an exit code: sha256 of the restored dist/index.js equals the recorded 5365d829….

(iii) the dist import removed — the recorded control.

PRE target-count=1 PRE_HASH=1f531adf… HEAD_BLOB=1f531adf…
POST target-count=0 inject-count=1 POST_HASH=8b6277f1…
MUTATION PROVEN ON DISK (target 1->0, inject 0->1, blob hash moved)
V3_EXIT=1
× registers page:header from the BUILT bundle 6ms
AssertionError: expected undefined to be truthy
Test Files 1 failed (1)
Tests 2 failed | 1 passed (3)

expected undefined to be truthy — verbatim what PR #7180 recorded. The one case that still passes is the precondition, which is correct: dist is on disk in this leg; only the import is gone. Restore proven by state: restored blob 1f531adf… equals the HEAD blob, and git diff HEAD and git status --short are both empty.

Cost — measured, because the ruling priced this at "one build in one job"

A bare turbo run test:dist does not cost one build. Turbo applies a task definition to every package in scope and resolves dependsOn: ["build"] for each, so it schedules a build of the entire monoreposite and console included:

$ turbo run test:dist --dry=json → 45 tasks (44 builds + 1 test:dist)
$ turbo run test:dist --filter=@object-ui/components --dry=json
→ 9 tasks
components#build components#test:dist core#build data-objectstack#build
i18n#build react#build react-runtime#build sdui-parser#build types#build

The root script therefore carries --filter=@object-ui/components, and the 9 tasks are the package plus the dependency closure its .d.ts emit genuinely needs. Cold-cache wall time, which is what CI pays:

COLD_EXIT=0 COLD_WALL_SECONDS=72
Tasks: 9 successful, 9 total
Cached: 0 cached, 9 total
Time: 1m10.762s

~72 s added to shard 1 of a job whose ceiling is timeout-minutes: 20, unchanged. (Shared-box seconds: four sibling agents build in this container, so this is an upper-ish bound, not an idle-box figure.)

If a second package ever gains a pin without being added to the filter, the failure is the loud precondition naming its missing built entry — not a silent skip.

Collected by exactly one project

vitest list --filesOnly is the instrument, and it has a positive control:

unit: files=810 pin=0
dom: files=1429 pin=0
dom-heavy: files=34 pin=0
dist: [dist] packages/components/src/__tests__/page-header-action-ids.dist.spec.tsx

The suffix does this by construction — unit collects *.test.ts, dom collects *.test.tsx, dom-heavy is an explicit file list — so none of the three needed a character changed.

The same property keeps the file out of packages/components/tsconfig.test.json, and that is deliberate rather than incidental: turbo's type-check waits on ^build, so a type program that read this package's own dist would demand an artifact type-check is not allowed to wait for — the coupling #4801 removed. Measured with the package's own type program, with a control:

$ tsc -p tsconfig.test.json --listFiles
page-header-action-ids.dist.spec.tsx → 0 files (out of the program, as designed)
page-header-action-ids.test.tsx → 1 file (control: siblings ARE in it)

Gates — union re-run at commit 7a6eef7b9

GateVerdict line
vitest run scripts/exit 0 — Test Files 94 passed (94) / Tests 2645 passed (2645)
pnpm --filter @object-ui/components run type-checkexit 0
pnpm check:control-bytesexit 0 — "OK (scanned 6010 tracked text file(s); skipped 85 binary)"
pnpm type-check:scriptsexit 0
pnpm check:doc-fencesexit 0 — "every TypeScript block in 224 document(s) is fenced…"
pnpm check:doc-typesexit 0 — "Every documented component type is registered."
pnpm docs:check-linksexit 0 — "Links are valid across 17 scan roots."
check-changeset-presenceexit 0 — "declares 1 changeset(s) … Every one of them has an EMPTY frontmatter"
check-changeset-overwrite / -no-major / -fixedexit 0 each
eslint on the changed filesexit 0 — 0 errors, 0 warnings

Every exit code was captured before any pipe. The lint run is narrowed and the narrowing is measured: the population is the diff, the count comes from --format json (3 files), and eslint.config.js contains 0projectService / parserOptions / project: occurrences against a live control of 10 rules hits in the same file — not type-aware, so no untouched file's verdict can move under this diff.

Changeset: an empty frontmatter, which the presence gate names as a pass rather than a workaround. The one file it flags is under packages/components/src/ but ships nowhere — the package publishes files: ["dist", …] and its build tsconfig.json excludes src/__tests__ outright.

Corrections and deviations, stated rather than buried

The first run of all three verdicts was invalid and was thrown away. The pin file's docstring contained a glob pairing a star with a slash, which ends a block comment early; the file failed to parse, so verdict (i) was red and (ii)/(iii) reported Tests no tests — a transform error, not an assertion. vitest list --filesOnly had passed because it never transforms. Fixed, and all three verdicts above are from the re-run. This repo already records the same trap in tsconfig.scripts.json's header and in check-changeset-presence.mjs.

Three files beyond the dispatch's declared surface, each forced by a gate or by turbo's shape:

  • packages/components/package.json — one script. Turbo tasks are per-package, so dependsOn: ["build"]for the package under test cannot be expressed without a script in that package. It carries --root ../.. because the invocation guard refuses any run whose Vitest root is not the repo root, and --config vitest.config.mts because Vite resolves configFile relative to root, not cwd (measured: ../../vitest.config.mts resolved to /home/vitest.config.mts).
  • content/docs/guide/ci-cd-pipeline.md — one cell. scripts/__tests__/ci-cd-pipeline-doc.test.ts fails when the workflow runs a first-party command the job table does not name. It also fails if the cell names a command the job does not run, which is why the turbo invocation is described in prose there rather than as a code span.
  • scripts/__tests__/turbo-task-guard-coverage.test.ts — one docstring line. That test enforces the partition "cacheable ⇒ has a derived inputs guard; otherwise cache: false", and its docstring enumerates the partition. test:dist is cache: false — a lane whose subject is a build artifact turbo does not hash must never replay a verdict — so it belongs on the uncached side, and the enumeration now says so.

⛔ No change to the root alias map. ⛔ No timeout raised. ⛔ No heavy-test allowlist entry. ⛔ No skip-changeset label — in this repo that label is inert and the empty-frontmatter changeset is the declaration that counts.

Not in this PR

Nothing migrates into the new lane. Its scarcity is the guard against it becoming a second default test surface, and the ruling asks for exactly one resident.

Fix round — Type Check was red on the first push

What was red.@object-ui/console#type-check, run 33588494358 / job 100117481182. Three errors, all one cause:

../../vitest.config.mts(311,7): error TS2769: No overload matches this call.
../../vitest.config.mts(325,7): error TS2769: No overload matches this call.
vitest.config.ts(11,15): error TS2345: Argument of type 'UserConfig & …' is not assignable to parameter of type 'never'.

Cause, one line. Inside the conditional spread that declares the dist project, the literal extends: true widens to boolean, while TestProjectConfiguration.extends is string | true | undefined — so the element matched no defineConfig overload and the whole projects array degraded to never[], which then took down the console entry on line 325 too.

Fix.extends: true as const, plus a comment recording why the annotation is load-bearing (it reads like removable noise). Nothing else changed; the three pre-existing projects never widened, because their extends: true sits in the plain array rather than in a conditional one.

Why CI saw it and this PR's own gates did not. The root vitest.config.mts is compiled by no type program of its own — apps/console/vitest.config.ts merges it, so the console's tsc --noEmit is the program that reads it. That job was outside the gate set run before the first push. It is in the set now.

Reproduced before fixing, not after. On an unbuilt worktree the real errors are unreachable behind 378 TS2882s, so the dependency closure was built through turbo first (turbo run type-check --filter=@object-ui/console), which is the path CI takes:

exiterror TS lines
before the fix13
after the fix00

Re-run at the fix commit 857c8afc5 (exit codes captured by redirect-then-$?):

CheckVerdict
pnpm --filter @object-ui/console run type-checkexit 0 — 0 error TS lines (was exit 1 / 3)
pnpm --filter @object-ui/components run type-checkexit 0
pnpm type-check:scriptsexit 0
pnpm type-check:vitest-setupexit 0
pnpm type-check (turbo, every package)exit 0 — Tasks: 81 successful, 81 total
pnpm test:distexit 0 — Test Files 1 passed (1), Tasks: 9 successful, 9 total
vitest list --filesOnly --project distexit 0 — exactly 1 pin; unit/dom/dom-heavy still 0 (files 810 / 1429 / 34)
vitest run --project dist without the env varexit 1 — the opt-in refusal still fires

No rebase and no force-push: the fix is a commit on top of the branch.


🤖 Generated with Claude Code

https://claude.ai/code/session_01BGMDbrVa8JjZcCQ7DWYH1b

The root `vitest.config.mts` aliases every workspace package to its `src`,
which is right for the ~2000 tests that want fast source feedback and leaves
"does the SHIPPED BUNDLE still do X" unanswerable. Such a test could not be
committed: turbo's `test` task is `dependsOn: ["^build"]` — the DEPENDENCIES'
builds, not the package's own — so a `dist`-importing test landed in CI with no
`dist` to import. That is NOT MEASURED rather than a red pin, and the usual
repair (delete it, or let it skip when `dist` is missing) leaves a green suite
that measures nothing. Two lanes hit this wall in one morning and each threw a
correct measurement away.
Implements the PM ruling on objectui#7183 (option 1, 2026-09-02):
- `vitest.config.mts` gains a fourth project, `dist`, collecting
`packages/*/src/**/*.dist.spec.tsx`. The suffix keeps these files out of
`unit`, `dom` and `dom-heavy` BY CONSTRUCTION — none of the three needed a
character changed — and out of each package's `tsconfig.test.json`, which
matters because turbo's `type-check` waits on `^build` and must never read
the package's own `dist` (objectui#4801).
- The project is OPT-IN behind `OBJECTUI_DIST_PINS=1`. CI's test job runs
`pnpm test` with no build step, so an unconditional fourth project would be
collected there with no `dist` on disk and would fail every PR. The one
false-green this opens — `--project dist` without the env var collecting zero
files and exiting green — is refused in the config with a message naming the
right command.
- `turbo.json` gains `test:dist`, `dependsOn: ["build"]` (self, not `^build`),
`cache: false`: a lane whose subject is a build artifact turbo does not hash
must not replay a verdict. That places it on the uncached side of the
partition `scripts/__tests__/turbo-task-guard-coverage.test.ts` enforces,
whose docstring is updated to match.
- The light dom setup is deliberate, not a cost optimisation:
`vitest.setup.dom.tsx` registers `page:header` from SOURCE, which would keep
a pin green with the built bundle removed entirely.
First resident: the objectui#6252 acceptance criterion PR objectui#7180 measured
by hand and could not commit — an id-authored `page:header` resolves through the
BUILT renderer and carries no `body.source` into the DOM or the authored node.
Its live control is part of the pin: with the `dist` import removed the run
fails `expected undefined to be truthy`.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BGMDbrVa8JjZcCQ7DWYH1b
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 48 chunks)3160.2 KB3191.4 KB
Main entry chunk (gzip)142.6 KB350 KB
Entry fileindex-BOEfHVe7.js
StatusPASS

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

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)15.33KB5.59KB
app-shell (runtime-config.js)20.68KB7.36KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)514.46KB117.29KB
core (index.js)5.55KB2.23KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)178.20KB49.60KB
fields (index.js)244.25KB61.73KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.98KB10.98KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.92KB12.93KB
plugin-charts (index.js)70.02KB19.44KB
plugin-chatbot (index.js)190.53KB45.18KB
plugin-dashboard (index.js)132.63KB34.56KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)250.65KB63.91KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)132.78KB32.58KB
plugin-gantt (index.js)166.77KB40.76KB
plugin-grid (index.js)208.88KB56.59KB
plugin-kanban (index.js)53.21KB14.66KB
plugin-list (index.js)113.51KB27.67KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)29.34KB8.47KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.90KB21.12KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)81.07KB26.86KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.72KB2.24KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)3.40KB1.71KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

@yinlianghuiClaude

Copy link
Copy Markdown
CollaboratorAuthor

CI red on 7a6eef7b9Type Check (job 100117481182), this PR's own defect, fix in flight.

@object-ui/console#type-check runs tsc --noEmit over apps/console/vitest.config.ts, which imports the root vitest.config.mts. The new dist project entry inside the conditional spread is inferred as { extends: boolean; … } (the true literal widens inside the array in the conditional), and TestProjectConfiguration.extends is string | true | undefined, so ../../vitest.config.mts(311,7) / (325,7) report TS2769 and the console config fails at (11,15). The fix is keeping the literal narrow (extends: true as const); the dev is reproducing on pnpm --filter @object-ui/console run type-check before and after, re-running the type programs, and pushing a fix commit on this branch. The local verification ran the components package's type program only, which cannot see the console's read of the root config — the fix round will say so.


Generated by Claude Code

… still type-checks
The `dist` project entry added for objectui#7183 sits inside a conditional
spread, and inside that array literal `extends: true` widens to `boolean`.
`TestProjectConfiguration.extends` is `string | true | undefined`, so the widened
element matches no `defineConfig` overload and the whole `projects` array
degrades to `never[]`:
../../vitest.config.mts(311,7): error TS2769: No overload matches this call.
../../vitest.config.mts(325,7): error TS2769: No overload matches this call.
vitest.config.ts(11,15): error TS2345: Argument of type 'UserConfig & …'
is not assignable to parameter of type 'never'.
CI reported it on `@object-ui/console#type-check` rather than here, because
`apps/console/vitest.config.ts` merges this config and is the type program that
reads it — the root `.mts` is compiled by nobody on its own.
`as const` keeps the literal narrow. The three pre-existing projects are
unaffected: their `extends: true` sits in the plain array, where it never
widened. A comment records why the annotation is load-bearing, since it reads
like removable noise.
Measured red -> green on the reported program, deps built through turbo first
(the errors are unreachable behind 378 TS2882s on an unbuilt tree):
before exit 1, 3 errors
after exit 0, 0 errors
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BGMDbrVa8JjZcCQ7DWYH1b
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 48 chunks)3160.2 KB3191.4 KB
Main entry chunk (gzip)142.6 KB350 KB
Entry fileindex-BOEfHVe7.js
StatusPASS

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

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)15.33KB5.59KB
app-shell (runtime-config.js)20.68KB7.36KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)514.46KB117.29KB
core (index.js)5.55KB2.23KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)178.20KB49.60KB
fields (index.js)244.25KB61.73KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.98KB10.98KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.92KB12.93KB
plugin-charts (index.js)70.02KB19.44KB
plugin-chatbot (index.js)190.53KB45.18KB
plugin-dashboard (index.js)132.63KB34.56KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)250.65KB63.91KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)132.78KB32.58KB
plugin-gantt (index.js)166.77KB40.76KB
plugin-grid (index.js)208.88KB56.59KB
plugin-kanban (index.js)53.21KB14.66KB
plugin-list (index.js)113.51KB27.67KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)29.34KB8.47KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.90KB21.12KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)81.07KB26.86KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.72KB2.24KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)3.40KB1.71KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

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

Projects

None yet

2 participants

@yinlianghui@claude