Skip to content

feat(lint): run ESLint on PRs, and cover every package (#2923) - #2928

Merged
os-zhuang merged 1 commit into
mainfrom
feat/lint-enforcement-2923
Jul 28, 2026
Merged

feat(lint): run ESLint on PRs, and cover every package (#2923)#2928
os-zhuang merged 1 commit into
mainfrom
feat/lint-enforcement-2923

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

Closes #2923. Follow-up: #2927 for the errors this declares.

The thing I got wrong in #2923, and why this PR exists

I originally filed #2923 recommending against wiring lint.yml, on the grounds that it would be a no-op gate: pnpm lint reports 7,724 problems but 0 errors, and ESLint only fails on errors unless --max-warnings is set.

The arithmetic was right; I had missed the bottom of eslint.config.js. It sets three object-ui/* rules to error specifically so a new violation fails CI:

// ADR-0054 Phase 5 ratchet — ban synthetic-event triggers (C1). Error so a
// new violation fails CI; the existing surfaces were converted to direct
// idempotent commands first, so this lints clean today.
'object-ui/no-synthetic-event-trigger': 'error',

// objectui#2879 ratchet — ... Error so a tenth copy fails CI; all known
// sites were converted first, so this lints clean today.
'object-ui/no-try-catch-around-hook': 'error',

// ... Error so a new inline mirror fails CI; ...
'object-ui/no-inline-spec-config': 'error',   // scoped to packages/types/src/objectql.ts

Three separate deliberate efforts, each written assuming CI enforces it, each with its existing violations pre-cleaned so the rule would "lint clean today". The enforcement they were built against did not exist. Same shape as #2911, one layer up: the rule is authored, nothing runs it.

And the same second layer, again

turbo run lint silently skips packages with no lint script — identical to the type-check hole. 7 of 45 packages had none, including apps/console, the largest surface in the repo. So even once wired, the three ratchets would have been blind to console.

before after
packages linted 38 / 45 43 / 45
workflow runs on PRs no yes
ratchets actually enforced 0 of 3 3 of 3

Two config fixes, not code debt

Measuring the unlinted packages first turned up 25 "errors" — most of which were not real:

apps/site: 7 errors → 0. Six were in apps/site/.source/, which is fumadocs-mdx generated output and already gitignored (apps/site/.gitignore:5). Linting codegen only reports on the generator's choices. The seventh was a stale eslint-disable-next-line @next/next/no-img-element — the Next plugin isn't in the flat config, so the directive itself errored as an unknown rule. Replaced with a comment saying why a plain <img> is right for third-party badge endpoints.

apps/console: 15 → 14. One was tailwind.config.js, authored in TypeScript (import type { Config }) despite the .js extension; the TS parser block is scoped to .ts/.tsx, so the base JS parser choked on it. Now ignored as build config.

This is the second time apps/site has looked like debt purely because of generated artifacts — cf. #2924, where its 7 phantom type errors were missing .next/types.

What is genuinely broken, and is not fixed here

Per the plan agreed for this PR, the real errors are declared, not fixed — they touch runtime behaviour and deserve their own review. Tracked in #2927:

  • apps/console — 14 errors. 8x react-hooks/purity (Date.now() during render in ApprovalsInboxPage), 4x react-hooks/static-components (components created during render — "will reset", i.e. state loss / focus loss, and three of the four are Settings pages), plus a dead assignment on an auth path and a prefer-const.
  • packages/runner — 3 errors. react-hooks/static-components in LayoutRenderer at 70/106/168, same class.

Note both purity and static-components are at error severity because eslint.config.js downgrades five other React Compiler rules but deliberately not these two.

These are pre-existing and were simply invisible. This PR does not turn them red — it records them in DEBT so they cannot stay invisible.

Why no --max-warnings

Measured composition of the 7,724 warnings:

rule count share
@typescript-eslint/no-explicit-any 6,262 81%
react-refresh/only-export-components 486 6%
@typescript-eslint/no-unused-vars 322 4%
react-hooks/set-state-in-effect 252 3%
react-hooks/exhaustive-deps 177 2%
react-hooks/refs 111 1%
four more react-hooks/* 79

One stylistic rule is 81% of it, and five of the react-hooks/* rules are downgraded on purpose ("codebase predates these rules"). A blanket cap of 7,724 would read as a health signal while tracking no-explicit-any. This gate is about errors; the warning question is left open in #2923's history rather than answered badly.

Verification

The ratchets now actually bite. Planted a hook inside try/catch in packages/i18n (a package that was already linted):

4:12  error  Do not call the hook 'useMemo' inside try/catch — a caught throw
             desyncs hook order on the next render (objectui#2879)
             object-ui/no-try-catch-around-hook

pnpm lint exit=1
Failed:    @object-ui/i18n#lint

Impossible before this PR, in two independent ways: the workflow never ran, and had it run, console still would not have been linted.

Coverage guard red in all three modes:

tampering result
new package with no lint script exit 1
a DEBT entry outliving its gap (package gains the script) exit 1
lint script removed from a covered package exit 1

Baseline restores green after each; restored via cp, no residual diff.

Green state:

✅  lint coverage: 43/45 packages linted, 2 with outstanding errors (17 total)
✅  type-check coverage: 36/45 via `type-check`, 1 via their own build, 7 known-broken (25 errors outstanding), 1 not compiled
✅  All workspace packages are in the changeset fixed group
$ pnpm lint   →  43/43 successful, exit 0

No changeset: all five packages that gained a lint script are private: true, so no published package changes.

🤖 Generated with Claude Code

`lint.yml` was `workflow_dispatch`-only, so ESLint had never gated a PR. That
mattered more than it looked: eslint.config.js sets three `object-ui/*` rules to
`error` *specifically* so a new violation fails CI — ADR-0054 Phase 5's
no-synthetic-event-trigger, #2879's no-try-catch-around-hook, and the
objectql.ts no-inline-spec-config ratchet. Each author pre-cleaned the existing
sites so the rule would "lint clean today". All three were inert, because
nothing ran them.

Same two-layer hole as #2911's type-check gate, and the second layer bites here
too: `turbo run lint` silently skips packages with no `lint` script, and 7 of 45
had none — including `apps/console`, the largest surface in the repo.

- lint.yml now runs on pull_request + push, keeping workflow_dispatch.
- scripts/check-lint-coverage.mjs: every package must lint or be a declared gap,
  and the list only shrinks. Runs before install (reads package.json only).
- lint scripts added to the 5 packages that were already clean: site,
  vscode-extension, and the three examples. Coverage 38 -> 43 of 45.
- console (14 errors) and runner (3) declared in DEBT, tracked in #2927. These
  are pre-existing and were merely invisible; this does not turn them red.

Two config-level fixes, both generated-or-misparsed rather than code debt — they
are why `apps/site` looked like it had 7 errors and console 15:

- ignore `**/.source` — fumadocs-mdx codegen for apps/site, already gitignored.
  Linting generated output only reports on the generator's choices.
- ignore `**/tailwind.config.js` — authored in TypeScript despite the `.js`
  extension, so the base JS parser failed on `import type`.
- drop a stale `eslint-disable-next-line @next/next/no-img-element` in
  apps/site: the Next plugin is not in the flat config, so the directive itself
  errored as an unknown rule. Replaced with a comment stating why a plain <img>
  is correct there.

`--max-warnings` deliberately not set: the 7,724 warnings repo-wide are 81%
no-explicit-any, plus React Compiler rules the config downgrades on purpose.
This gate is about errors.

Proven before being trusted — planting a hook inside try/catch in a linted
package now yields `pnpm lint` exit 1 and `Failed: @object-ui/i18n#lint`, which
was impossible before. Coverage guard verified red in all three modes (new
scriptless package, DEBT entry outliving its gap, script removed from a covered
package).

Refs #2911, #2923, #2927

Co-Authored-By: Claude <noreply@anthropic.com>
@vercel

vercel Bot commented Jul 28, 2026

Copy link
Copy Markdown

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

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
objectui Ignored Ignored Jul 28, 2026 2:37pm

Request Review

@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Main entry (gzip) 27.9 KB 350 KB
Entry file index-CbPvm_L9.js
Status PASS

📦 Bundle Size Report

Package Size Gzipped
app-shell (index.js) 8.20KB 2.97KB
app-shell (runtime-config.js) 7.42KB 2.32KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 7.57KB 2.97KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 1.17KB 0.53KB
auth (AuthProvider.js) 22.10KB 4.37KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.12KB 3.41KB
auth (LoginForm.js) 17.86KB 5.29KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.43KB 2.09KB
auth (SocialSignInButtons.js) 9.60KB 3.89KB
auth (UserMenu.js) 3.40KB 1.22KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 35.76KB 9.11KB
auth (createAuthenticatedFetch.js) 4.37KB 1.69KB
auth (index.js) 2.25KB 1.01KB
auth (org-roles.js) 6.72KB 2.85KB
auth (phone-identifier.js) 1.11KB 0.66KB
auth (types.js) 0.59KB 0.35KB
auth (useAuth.js) 4.91KB 0.87KB
auth (useIsWorkspaceAdmin.js) 1.61KB 0.85KB
collaboration (CommentThread.js) 18.38KB 4.49KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 3.65KB 1.42KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.25KB 0.53KB
collaboration (useCommentSearch.js) 1.98KB 0.88KB
collaboration (useConflictResolution.js) 7.75KB 1.86KB
collaboration (useMentionNotifications.js) 1.81KB 0.68KB
collaboration (usePresence.js) 6.33KB 1.84KB
collaboration (useRealtimeSubscription.js) 7.91KB 2.01KB
components (index.js) 449.62KB 97.73KB
core (index.js) 2.12KB 0.77KB
create-plugin (index.js) 9.28KB 2.98KB
data-objectstack (index.js) 127.78KB 32.15KB
fields (index.js) 218.37KB 53.54KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (currency.js) 1.22KB 0.64KB
i18n (i18n.js) 4.32KB 1.77KB
i18n (index.js) 2.46KB 0.96KB
i18n (pickLocalized.js) 1.70KB 0.83KB
i18n (provider.js) 5.37KB 1.72KB
i18n (useObjectLabel.js) 25.17KB 5.80KB
i18n (useSafeTranslation.js) 3.26KB 1.44KB
layout (index.js) 38.45KB 10.67KB
mobile (MobileProvider.js) 0.92KB 0.49KB
mobile (ResponsiveContainer.js) 0.94KB 0.38KB
mobile (breakpoints.js) 1.51KB 0.70KB
mobile (createOfflineDataSource.js) 5.61KB 1.74KB
mobile (index.js) 1.50KB 0.62KB
mobile (offlineQueue.js) 3.91KB 1.35KB
mobile (pwa.js) 0.97KB 0.49KB
mobile (serviceWorker.js) 1.48KB 0.62KB
mobile (serviceWorkerSource.js) 3.41KB 1.48KB
mobile (useBreakpoint.js) 1.54KB 0.65KB
mobile (useGesture.js) 4.42KB 1.27KB
mobile (useOfflineSync.js) 1.99KB 0.72KB
mobile (usePullToRefresh.js) 2.53KB 0.85KB
mobile (useResponsive.js) 0.71KB 0.42KB
mobile (useResponsiveConfig.js) 1.36KB 0.63KB
mobile (useSpecGesture.js) 1.77KB 0.77KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 6.84KB 2.42KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 3.67KB 1.12KB
permissions (evaluator.js) 4.00KB 1.23KB
permissions (index.js) 0.91KB 0.41KB
permissions (store.js) 0.91KB 0.42KB
permissions (useFieldPermissions.js) 1.28KB 0.52KB
permissions (usePermissions.js) 1.55KB 0.71KB
plugin-ai (index.js) 15.71KB 3.79KB
plugin-calendar (index.js) 44.90KB 12.35KB
plugin-charts (index.js) 57.26KB 16.24KB
plugin-chatbot (index.js) 179.93KB 42.67KB
plugin-dashboard (index.js) 109.60KB 28.33KB
plugin-designer (index.js) 210.56KB 42.56KB
plugin-detail (index.js) 214.86KB 52.39KB
plugin-editor (index.js) 2.46KB 1.10KB
plugin-form (index.js) 103.47KB 25.10KB
plugin-gantt (index.js) 162.26KB 39.53KB
plugin-grid (index.js) 179.25KB 46.97KB
plugin-kanban (index.js) 47.82KB 13.18KB
plugin-list (index.js) 98.67KB 23.35KB
plugin-map (index.js) 16.80KB 5.24KB
plugin-markdown (index.js) 13.65KB 4.67KB
plugin-report (index.js) 37.07KB 9.81KB
plugin-timeline (index.js) 25.03KB 7.11KB
plugin-tree (index.js) 8.36KB 2.81KB
plugin-view (index.js) 85.68KB 20.85KB
providers (DataSourceProvider.js) 0.75KB 0.39KB
providers (MetadataProvider.js) 1.37KB 0.59KB
providers (ThemeProvider.js) 1.55KB 0.67KB
providers (UploadProvider.js) 11.71KB 3.53KB
providers (index.js) 0.44KB 0.22KB
providers (types.js) 0.01KB 0.04KB
react-runtime (index.js) 3.19KB 1.38KB
react (LazyPluginLoader.js) 3.77KB 1.33KB
react (SchemaRenderer.js) 18.70KB 6.09KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 1.00KB 0.55KB
sdui-parser (codegen.js) 4.09KB 1.74KB
sdui-parser (index.js) 2.16KB 0.94KB
sdui-parser (parse.js) 10.04KB 2.82KB
sdui-parser (types.js) 0.29KB 0.24KB
sdui-parser (validate.js) 4.69KB 1.48KB
types (ai.js) 0.20KB 0.17KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 2.87KB 0.99KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (complex.js) 0.20KB 0.18KB
types (crud.js) 0.20KB 0.18KB
types (data-display.js) 0.20KB 0.18KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (designer.js) 0.77KB 0.41KB
types (disclosure.js) 0.20KB 0.18KB
types (feedback.js) 0.20KB 0.18KB
types (field-types.js) 0.20KB 0.18KB
types (form.js) 0.20KB 0.18KB
types (index.js) 1.86KB 0.91KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 0.20KB 0.18KB
types (navigation.js) 0.20KB 0.18KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 0.20KB 0.18KB
types (plugin-scope.js) 0.20KB 0.18KB
types (record-components.js) 0.20KB 0.19KB
types (record-semantics.js) 1.28KB 0.67KB
types (registry.js) 0.20KB 0.18KB
types (reports.js) 0.20KB 0.18KB
types (spec-report.js) 5.04KB 1.93KB
types (system-fields.js) 2.39KB 1.17KB
types (theme.js) 0.20KB 0.18KB
types (ui-action.js) 0.75KB 0.46KB
types (views.js) 0.20KB 0.18KB
types (widget.js) 0.20KB 0.18KB

Size Limits

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

@os-zhuang
os-zhuang merged commit cf20681 into main Jul 28, 2026
16 checks passed
@os-zhuang
os-zhuang deleted the feat/lint-enforcement-2923 branch July 28, 2026 14:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

lint never ran on PRs, and turbo skipped 7 packages — three error ratchets were inert

1 participant