feat(chore): add a lint ratchet for shadcn rules (#51014)

## Problem

shadcn lint rules has been soft landed in #50676 and are now on as
warnings in every app, but nothing stops a PR from adding new violations

linear: FE-4473

## Solution

- moved the ratchet script and its tests from `apps/studio/scripts` to
`packages/eslint-config-supabase` so every app runs one copy
- added a shared rule list,
`packages/eslint-config-supabase/ratchet-rules.json` with the shadcn
rules
- www, docs, design-system, ui-library and learn get `lint-ratchet.yml`
with one job per changed app (triggered by the app, `packages/**` or the
lockfile) + a weekly `lint-ratchet-decrease.yml` (as for studio ratchet)
- package tests run in `eslint-config-supabase-tests.yml`

<!--
## Preview links

If relevant, include links to changed pages for easy review access.

Copy the preview base URL from the Vercel bot comment on this PR. Use
the following table as an example template.

| Site | Live | Preview | Search for |
| -------------- |
-------------------------------------------------------------------------
|
------------------------------------------------------------------------------------------------------------
| ----------------------------- |
| WWW | [/blog/your-post](https://supabase.com/blog/your-post) |
[/blog/your-post](https://zone-www-dot-com-git-branch-name-supabase.vercel.app/blog/your-post)
| unique phrase from the change |
| Docs |
[/docs/guides/your-page](https://supabase.com/docs/guides/your-page) |
[/docs/guides/your-page](https://docs-git-branch-name-supabase.vercel.app/docs/guides/your-page)
| unique phrase from the change |
| Studio | [/dashboard](https://supabase.com/dashboard) |
[/dashboard](https://studio-git-branch-name-supabase.vercel.app/dashboard)
| unique phrase from the change |
| Design system | [/design-system](https://supabase.com/design-system) |
[/design-system](https://design-system-git-branch-name-supabase.vercel.app/design-system)
| unique phrase from the change |
| UI library | [/library](https://supabase.com/library) |
[/library](https://ui-library-git-branch-name-supabase.vercel.app/library)
| unique phrase from the change |
| Knowledge base |
[/kb/guides/your-page](https://supabase.com/kb/guides/your-page) |
[/kb/guides/your-page](https://kb-git-branch-name-supabase.vercel.app/kb/guides/your-page)
| unique phrase from the change |
-->

<!-- ## Additional context

Optionally add any other context or screenshots.

-->

## Review instructions

1. run `pnpm --filter ./apps/www run lint:ratchet`
2. add `p-[13px]` to a `className` in any www component and run it
again. it fails with `shadcn/no-arbitrary-values` and the file name with
`(+1)`
3. revert change
4. run `pnpm --filter eslint-config-supabase test` and see 6 tests pass
5. in ci, check `Ratchet studio lint checks` and the `ratchet (<app>)`
jobs for the apps this pr touches

## Checklist

Check all before review:

- [x] I have read
[CONTRIBUTING.md](https://github.com/supabase/supabase/blob/master/CONTRIBUTING.md)
- [x] If I wrote a new docs topic or edited an existing topic, I used
the `/write-the-docs` or `/edit-the-docs` skill, which applies the docs
[style
guide](https://github.com/supabase/supabase/tree/master/apps/docs/style-guide)


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **Developer Improvements**
* Expanded automated lint checks to cover additional apps and shared
package changes.
* Added checks for arbitrary Tailwind values, unknown classes, and raw
colors across supported apps.
* Added automated baseline updates that can open or update a pull
request when lint counts change.
* Added tests for the lint configuration and support for combining
multiple rule files.
* Updated Studio lint notifications to exclude Shadcn rules with
zero-baseline counts.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
Anthony Lio authored and GitHub committed 2026-10-02 14:13:59 +03:00
1 parent 73260dfaba
commit b13d6c2878
23 files changed
+1021 -20

No files matched your search

+56
View File
@@ -0,0 +1,56 @@
{
"rules": {
"shadcn/no-arbitrary-values": 42,
"shadcn/no-unknown-classes": 8,
"shadcn/no-raw-colors": 22
},
"ruleFiles": {
"shadcn/no-arbitrary-values": {
"components/code-block-wrapper.tsx": 1,
"components/code-fragment.tsx": 7,
"components/color-palette.tsx": 2,
"components/command-menu.tsx": 1,
"components/component-preview.tsx": 12,
"components/mdx-components.tsx": 2,
"registry/default/example/admonition-button-split.tsx": 2,
"registry/default/example/assistant-chat-commands.tsx": 2,
"registry/default/example/button-split-dropdown.tsx": 2,
"registry/default/example/chart-tooltip-demo.tsx": 2,
"registry/default/example/command-dialog.tsx": 1,
"registry/default/example/connect-interstitial-shared.tsx": 2,
"registry/default/example/drawer-demo.tsx": 1,
"registry/default/example/page-layout-edge-function.tsx": 1,
"registry/default/example/toc-demo.tsx": 1,
"registry/default/example/toc-single-demo.tsx": 1,
"registry/default/example/typography-inline-code.tsx": 2
},
"shadcn/no-unknown-classes": {
"components/code-block-wrapper.tsx": 1,
"components/code-fragment.tsx": 1,
"components/component-preview.tsx": 1,
"components/mdx-components.tsx": 2,
"registry/default/example/data-grid-demo.tsx": 1,
"registry/default/example/data-grid-empty-state.tsx": 1,
"registry/default/example/form-patterns-sidepanel.tsx": 1
},
"shadcn/no-raw-colors": {
"components/code-block-wrapper.tsx": 2,
"components/copy-button.tsx": 3,
"registry/default/example/calendar-form.tsx": 1,
"registry/default/example/calendar-react-hook-form.tsx": 1,
"registry/default/example/checkbox-form-multiple.tsx": 1,
"registry/default/example/checkbox-form-single.tsx": 1,
"registry/default/example/combobox-form.tsx": 1,
"registry/default/example/connect-interstitial-shared.tsx": 3,
"registry/default/example/date-picker-form.tsx": 1,
"registry/default/example/input-form.tsx": 1,
"registry/default/example/input-otp-form.tsx": 1,
"registry/default/example/radio-group-card-form.tsx": 1,
"registry/default/example/radio-group-form.tsx": 1,
"registry/default/example/radio-group-stacked-form.tsx": 1,
"registry/default/example/select-form.tsx": 1,
"registry/default/example/switch-form.tsx": 1,
"registry/default/example/textarea-form.tsx": 1
}
}
}
+1
View File
@@ -14,6 +14,7 @@
"build": "run-p build:registry build:content && pnpm build:next",
"start": "next start",
"lint": "eslint .",
"lint:ratchet": "tsx node_modules/eslint-config-supabase/ratchet-eslint-rules.ts --rules-file node_modules/eslint-config-supabase/ratchet-rules.json",
"clean": "rimraf .next .turbo tsconfig.tsbuildinfo .contentlayer .velite __registry__",
"typecheck": "run-p build:registry build:content && tsc --noEmit -p tsconfig.json"
},
+39
View File
@@ -0,0 +1,39 @@
{
"rules": {
"shadcn/no-arbitrary-values": 38,
"shadcn/no-unknown-classes": 19,
"shadcn/no-raw-colors": 3
},
"ruleFiles": {
"shadcn/no-arbitrary-values": {
"app/contributing/ContributingToC.tsx": 1,
"components/Feedback/Feedback.tsx": 15,
"components/GuidesSidebar.tsx": 1,
"components/StepHike/index.tsx": 1,
"features/docs/Reference.sections.tsx": 4,
"features/docs/Reference.ui.tsx": 4,
"features/ui/AgentWatchSchedule.tsx": 2,
"features/ui/CodeBlock/CodeBlock.client.tsx": 7,
"features/ui/PromptPanel.tsx": 3
},
"shadcn/no-unknown-classes": {
"app/guides/getting-started/ai-skills/AiSkillsIndex.tsx": 1,
"app/guides/local-development/cli/config/page.tsx": 1,
"components/GithubCard.tsx": 1,
"components/Navigation/NavigationMenu/GlobalMobileMenu.tsx": 1,
"components/Navigation/NavigationMenu/NavigationMenuCliList.tsx": 2,
"components/Navigation/NavigationMenu/NavigationMenuRefListItems.tsx": 2,
"components/Navigation/SideBar.tsx": 1,
"components/Params.tsx": 1,
"components/ProjectConfigVariables/ProjectConfigVariables.tsx": 2,
"features/docs/Reference.sections.tsx": 2,
"layouts/MainSkeleton.tsx": 1,
"layouts/ref/RefSubLayout.tsx": 2,
"layouts/ref/RefSubLayoutNonFunc.tsx": 2
},
"shadcn/no-raw-colors": {
"features/ui/CodeBlock/CodeBlock.tsx": 1,
"features/ui/PromptPanel.tsx": 2
}
}
}
+1
View File
@@ -33,6 +33,7 @@
"last-changed": "tsx scripts/last-changed.ts",
"last-changed:reset": "pnpm run last-changed -- --reset",
"lint": "eslint .",
"lint:ratchet": "tsx node_modules/eslint-config-supabase/ratchet-eslint-rules.ts --rules-file node_modules/eslint-config-supabase/ratchet-rules.json",
"postbuild": "pnpm run build:sitemap && ./../../scripts/upload-static-assets.sh",
"prebuild": "pnpm run codegen:graphql && pnpm run codegen:references && pnpm run codegen:examples && pnpm build:federated-content && pnpm run build:markdown && pnpm run build:gz-archive",
"predev": "pnpm run codegen:graphql && pnpm run codegen:references && pnpm run codegen:examples",
+25
View File
@@ -0,0 +1,25 @@
{
"rules": {
"shadcn/no-arbitrary-values": 7,
"shadcn/no-unknown-classes": 6,
"shadcn/no-raw-colors": 7
},
"ruleFiles": {
"shadcn/no-arbitrary-values": {
"components/command-menu.tsx": 2,
"components/command.tsx": 2,
"components/course-hero.tsx": 1,
"components/mdx-components.tsx": 2
},
"shadcn/no-unknown-classes": {
"components/mdx-components.tsx": 5,
"components/side-navigation-item.tsx": 1
},
"shadcn/no-raw-colors": {
"components/chapter-completion.tsx": 1,
"components/course-hero.tsx": 1,
"components/mdx-components.tsx": 2,
"components/side-navigation.tsx": 3
}
}
}
+1
View File
@@ -13,6 +13,7 @@
"internal:sync": "tsx ./scripts/sync-internal-content.mts",
"start": "next start",
"lint": "eslint .",
"lint:ratchet": "tsx node_modules/eslint-config-supabase/ratchet-eslint-rules.ts --rules-file node_modules/eslint-config-supabase/ratchet-rules.json",
"clean": "rimraf .next .turbo tsconfig.tsbuildinfo .contentlayer .velite tsconfig.tsbuildinfo",
"typecheck": "pnpm build:content && tsc --noEmit"
},
+260 -1
View File
@@ -11,7 +11,10 @@
"jsx-a11y/no-redundant-roles": 2,
"jsx-a11y/anchor-is-valid": 6,
"jsx-a11y/heading-has-content": 2,
"valtio/state-snapshot-rule": 1
"valtio/state-snapshot-rule": 1,
"shadcn/no-arbitrary-values": 212,
"shadcn/no-unknown-classes": 151,
"shadcn/no-raw-colors": 64
},
"ruleFiles": {
"react-hooks/exhaustive-deps": {
@@ -1000,6 +1003,262 @@
},
"valtio/state-snapshot-rule": {
"state/storage-explorer.tsx": 1
},
"shadcn/no-arbitrary-values": {
"components/grid/components/editor/DateTimeEditor.tsx": 2,
"components/grid/components/editor/JsonEditor.tsx": 4,
"components/grid/components/editor/TextEditor.tsx": 4,
"components/grid/components/editor/TruncatedWarningOverlay.tsx": 1,
"components/grid/components/header/ExportDialog.tsx": 2,
"components/interfaces/Account/AccessTokens/Classic/ExperimentalTokenDropdown.tsx": 2,
"components/interfaces/Account/AccessTokens/Scoped/Form/RiskMarker.tsx": 2,
"components/interfaces/Account/AuditLogs/AuditLogOverview.tsx": 2,
"components/interfaces/Account/Preferences/HotkeyToggle.tsx": 1,
"components/interfaces/Auth/EmailTemplates/CustomEmailTemplateRestrictionAdmonition.tsx": 2,
"components/interfaces/Compute/RuntimeBadge.tsx": 1,
"components/interfaces/ConfigDrift/ConfigurationDriftPage.tsx": 1,
"components/interfaces/Database/Policies/PolicyEditorPanel/LockedQuerySection.tsx": 10,
"components/interfaces/Database/Policies/PolicyEditorPanel/index.tsx": 6,
"components/interfaces/Database/Privileges/PrivilegesHead.tsx": 1,
"components/interfaces/Database/Replication/ReplicationPipelineStatus/ReplicationPipelineStatus.tsx": 2,
"components/interfaces/Database/Roles/RoleRow.tsx": 1,
"components/interfaces/Database/Schemas/DefaultEdge.tsx": 2,
"components/interfaces/Database/Schemas/SchemaGraph.tsx": 2,
"components/interfaces/Database/Schemas/SchemaTableNode.tsx": 10,
"components/interfaces/Database/Tables/ConstraintToken.tsx": 4,
"components/interfaces/DiskManagement/DiskManagementReviewAndSubmitDialog/DiskManagementReviewAndSubmitDialog.tsx": 2,
"components/interfaces/DiskManagement/fields/StorageTypeField.tsx": 1,
"components/interfaces/DiskManagement/ui/DiskSpaceBar.tsx": 1,
"components/interfaces/Explorer/ExplorerOnboardingWireframes.tsx": 5,
"components/interfaces/Explorer/QueryEditor/QueryRunButton.tsx": 2,
"components/interfaces/Functions/EdgeFunctionOverview/EdgeFunctionInvocationsChart.tsx": 1,
"components/interfaces/Home/ProjectList/ProjectCard.tsx": 2,
"components/interfaces/Integrations/Integration/IntegrationOverviewTabV2/InstallIntegrationSheet/InstallationOverview.tsx": 1,
"components/interfaces/Integrations/Marketplace/MarketplaceFeaturedHeroGrid.tsx": 1,
"components/interfaces/Integrations/Marketplace/MarketplaceSidebar.tsx": 1,
"components/interfaces/Integrations/Queues/QueueTab.tsx": 1,
"components/interfaces/Integrations/VercelGithub/ProjectLinker.tsx": 2,
"components/interfaces/Integrations/Warehouse/WarehouseTableStatusList.tsx": 1,
"components/interfaces/Integrations/Wrappers/WrapperRow.tsx": 2,
"components/interfaces/LogDrains/OrgAuditLogDrains.tsx": 2,
"components/interfaces/Organization/BillingSettings/Subscription/PaymentMethodSelection.tsx": 1,
"components/interfaces/Organization/BillingSettings/Subscription/PlanCard.tsx": 11,
"components/interfaces/Organization/CloudMarketplace/AwsMarketplaceOnboarding.components.tsx": 2,
"components/interfaces/Organization/Documents/ISO27001.tsx": 1,
"components/interfaces/Organization/Documents/SOC2.tsx": 1,
"components/interfaces/Organization/HeaderBanner.tsx": 2,
"components/interfaces/Organization/OAuthApps/AuthorizeRequesterDetails.tsx": 1,
"components/interfaces/ProjectCreation/RegionSelector.tsx": 1,
"components/interfaces/ProjectHome/HighAvailabilityBadge.tsx": 5,
"components/interfaces/ProjectHome/PlanUsageCard.tsx": 1,
"components/interfaces/ProjectHome/ServerLightGrid.tsx": 1,
"components/interfaces/QueryInsights/QueryInsightsChart/QueryInsightsChart.tsx": 2,
"components/interfaces/QueryInsights/hooks/useQueryInsightsTableColumns.tsx": 2,
"components/interfaces/Realtime/Inspector/RealtimeFilterPopover/index.tsx": 1,
"components/interfaces/Reports/ReportBlock/ChartBlock.tsx": 2,
"components/interfaces/Reports/ReportFilterBar.tsx": 3,
"components/interfaces/SQLEditor/DownloadSnippetModal.tsx": 1,
"components/interfaces/Settings/General/Infrastructure/RestartServerButton.tsx": 2,
"components/interfaces/Settings/General/TransferProjectPanel/TransferProjectButton.tsx": 1,
"components/interfaces/Settings/Infrastructure/InfrastructureConfiguration/MapView.tsx": 2,
"components/interfaces/Settings/Integrations/VercelIntegration/VercelSection.tsx": 1,
"components/interfaces/Settings/Logs/PreviewFilterPanel.tsx": 1,
"components/interfaces/Sidebar.tsx": 1,
"components/interfaces/SignIn/SignInMfaForm.tsx": 1,
"components/interfaces/Storage/AnalyticsBuckets/AnalyticsBucketDetails/CreateTable/CreateTableInstructionsDialog.tsx": 2,
"components/interfaces/Storage/AnalyticsBuckets/AnalyticsBucketDetails/NamespaceWithTables/TableRowComponent.tsx": 2,
"components/interfaces/Storage/BucketFilePickerDialog/BucketFilePickerHeader.tsx": 2,
"components/interfaces/Storage/StorageExplorer/FileExplorerRow.tsx": 2,
"components/interfaces/Storage/StorageExplorer/MoveItemsFolderPickerRow.tsx": 2,
"components/interfaces/Storage/StoragePolicies/StoragePoliciesEditor.tsx": 1,
"components/interfaces/Support/AttachmentUpload.tsx": 1,
"components/interfaces/TableGridEditor/GridHeaderActions.tsx": 1,
"components/interfaces/TableGridEditor/SidePanelEditor/ForeignKeySelector/ForeignKeySelector.tsx": 4,
"components/interfaces/TableGridEditor/SidePanelEditor/RowEditor/JsonEditor/index.tsx": 1,
"components/interfaces/TableGridEditor/SidePanelEditor/RowEditor/TextEditor.tsx": 1,
"components/interfaces/TableGridEditor/SidePanelEditor/TableEditor/Column.tsx": 1,
"components/interfaces/TableGridEditor/SidePanelEditor/TableEditor/ForeignKeysManagement/ForeignKeyRow.tsx": 1,
"components/interfaces/UnifiedLogs/ServiceFlow/components/shared/DetailRow.tsx": 1,
"components/interfaces/UnifiedLogs/UnifiedLogs.fields.tsx": 5,
"components/interfaces/UnifiedLogs/components/Columns.tsx": 3,
"components/interfaces/UnifiedLogs/components/LogsList.tsx": 1,
"components/interfaces/UnifiedLogs/components/UserLogFilterControl.tsx": 2,
"components/layouts/Navigation/FloatingMobileToolbar/FloatingMobileToolbar.tsx": 2,
"components/layouts/Navigation/NavigationBar/MobileNavigationBar.tsx": 1,
"components/layouts/Navigation/NavigationBar/ProjectBranchSelectorSheetTabTrigger.tsx": 2,
"components/layouts/ProjectLayout/LogicalBackupCliInstructions.tsx": 1,
"components/layouts/SignInLayout/SignInLayout.tsx": 1,
"components/ui/AIEditor/AskAIWidget.tsx": 1,
"components/ui/AiAssistantDropdown.tsx": 2,
"components/ui/BannerStack/Banners/BannerExplorer.tsx": 3,
"components/ui/BannerStack/Banners/BannerSelect2026.tsx": 13,
"components/ui/Charts/BarChart.tsx": 1,
"components/ui/Charts/ComposedChart.utils.tsx": 2,
"components/ui/DataTable/DataTableFilters/DataTableFilterCheckbox.tsx": 2,
"components/ui/DataTable/DataTableFilters/DataTableFilterCheckboxAsync.tsx": 1,
"components/ui/DataTable/DataTableFilters/DataTableFilterResetButton.tsx": 1,
"components/ui/DataTable/primitives/InputWithAddons.tsx": 1,
"components/ui/DataTable/primitives/Kbd.tsx": 1,
"components/ui/EntityTypeIcon.tsx": 1,
"components/ui/ErrorCodeTooltip/ErrorCodeTooltip.tsx": 1,
"components/ui/GlobalShortcuts/ShortcutsReferenceSheet.tsx": 1,
"components/ui/ShortcutBadge.tsx": 1,
"components/ui/ShortcutTooltip.tsx": 1,
"pages/project/[ref]/settings/infrastructure/replica/[replicaId].tsx": 1,
"pages/project/[ref]/settings/log-drains.tsx": 2,
"state/shortcuts/useDynamicShortcut.tsx": 1,
"state/shortcuts/useShortcut.tsx": 1
},
"shadcn/no-unknown-classes": {
"components/grid/SupabaseGrid.tsx": 1,
"components/grid/components/editor/JsonEditor.tsx": 1,
"components/grid/components/editor/TextEditor.tsx": 1,
"components/grid/components/grid/ColumnHeader.tsx": 6,
"components/grid/components/grid/ColumnOverlayItem.tsx": 5,
"components/grid/components/grid/Grid.tsx": 1,
"components/grid/components/grid/SelectColumn.tsx": 5,
"components/interfaces/App/FeaturePreview/FeaturePreviewModal.tsx": 1,
"components/interfaces/Auth/AuthProvidersForm/FormField.tsx": 1,
"components/interfaces/Auth/AuthProvidersForm/index.tsx": 1,
"components/interfaces/Auth/CustomAuthProviders/CreateOrUpdateCustomProviderSheet.tsx": 1,
"components/interfaces/Auth/CustomAuthProviders/CustomAuthProvidersList.tsx": 1,
"components/interfaces/Auth/OAuthApps/CreateOrUpdateOAuthAppSheet.tsx": 1,
"components/interfaces/Auth/OAuthApps/OAuthAppsList.tsx": 1,
"components/interfaces/Auth/Overview/OverviewLearnMore.tsx": 1,
"components/interfaces/Auth/Users/UserPanel.tsx": 1,
"components/interfaces/Auth/Users/UsersGridComponents.tsx": 3,
"components/interfaces/Billing/Payment/AddNewPaymentMethodModal.tsx": 1,
"components/interfaces/BranchManagement/ConnectToGitHub.tsx": 1,
"components/interfaces/ConnectSheet/content/steps/direct-connection/IPv4StatusPanel.tsx": 1,
"components/interfaces/Database/EnumeratedTypes/CreateEnumeratedTypeSidePanel.tsx": 1,
"components/interfaces/Database/EnumeratedTypes/EditEnumeratedTypeSidePanel.tsx": 1,
"components/interfaces/Database/Functions/FunctionsList/FunctionList.tsx": 1,
"components/interfaces/Database/Hooks/EditHookPanel.tsx": 1,
"components/interfaces/Database/Policies/PolicyEditorPanel/PolicyEditorPanelHeader.tsx": 1,
"components/interfaces/Database/Policies/PolicyEditorPanel/PolicyTemplates.tsx": 1,
"components/interfaces/Database/Replication/ReplicationDiagram/Edges.tsx": 2,
"components/interfaces/Database/Replication/ReplicationDiagram/index.tsx": 1,
"components/interfaces/Database/Schemas/SchemaGraph.tsx": 1,
"components/interfaces/Database/Schemas/SchemaTableNode.tsx": 2,
"components/interfaces/Database/Triggers/ChooseFunctionForm.tsx": 1,
"components/interfaces/Database/Triggers/EventTriggersList/EventTriggerList.tsx": 1,
"components/interfaces/Database/Triggers/TriggersList/TriggerList.tsx": 1,
"components/interfaces/Docs/DocSection.tsx": 1,
"components/interfaces/Home/ProjectList/ShimmeringCard.tsx": 2,
"components/interfaces/Integrations/CronJobs/CronJobPage.tsx": 1,
"components/interfaces/Integrations/CronJobs/CronJobTableCell.tsx": 1,
"components/interfaces/Integrations/CronJobs/CronJobsTab.DataGrid.tsx": 1,
"components/interfaces/Integrations/CronJobs/PreviousRunsTab.tsx": 2,
"components/interfaces/Integrations/Queues/QueuesTab.tsx": 1,
"components/interfaces/Integrations/Queues/SingleQueue/QueueDataGrid.tsx": 1,
"components/interfaces/Integrations/Vault/Secrets/SecretsManagement.tsx": 1,
"components/interfaces/JwtSecrets/jwt-secret-keys-table/action-panel.tsx": 1,
"components/interfaces/Organization/BillingSettings/Subscription/UpgradeModal.tsx": 1,
"components/interfaces/Organization/ProjectClaim/confirm.tsx": 1,
"components/interfaces/Platform/Webhooks/PlatformWebhooksEndpointDetails.tsx": 1,
"components/interfaces/ProjectAPIDocs/ContentSnippet.tsx": 4,
"components/interfaces/ProjectAPIDocs/ResourceContent.tsx": 4,
"components/interfaces/QueryPerformance/IndexAdvisor/IndexSuggestionIcon.tsx": 1,
"components/interfaces/QueryPerformance/QueryIndexes.tsx": 2,
"components/interfaces/Realtime/Inspector/MessagesTable.tsx": 2,
"components/interfaces/Realtime/Inspector/SelectedRealtimeMessagePanel.tsx": 1,
"components/interfaces/Reports/ReportBlock/ChartBlock.tsx": 1,
"components/interfaces/Reports/ReportBlock/ReportBlockContainer.tsx": 1,
"components/interfaces/Reports/ReportPadding.tsx": 1,
"components/interfaces/Reports/renderers/ApiRenderers.tsx": 1,
"components/interfaces/Settings/API/HardenAPIModal.tsx": 4,
"components/interfaces/Settings/Addons/Addons.tsx": 2,
"components/interfaces/Settings/Database/DatabaseReadOnlyAlert.tsx": 1,
"components/interfaces/Settings/General/CustomDomainConfig/CustomDomainsShimmerLoader.tsx": 2,
"components/interfaces/Settings/Infrastructure/InfrastructureConfiguration/DiagramFlow.tsx": 1,
"components/interfaces/Settings/Infrastructure/InfrastructureConfiguration/Edge.tsx": 2,
"components/interfaces/Settings/Infrastructure/InfrastructureConfiguration/HaInstanceConfiguration.tsx": 1,
"components/interfaces/Settings/Infrastructure/InfrastructureConfiguration/InstanceConfiguration.tsx": 1,
"components/interfaces/Settings/Infrastructure/InfrastructureConfiguration/MapView.tsx": 1,
"components/interfaces/Settings/Integrations/VercelIntegration/VercelSection.tsx": 1,
"components/interfaces/Settings/Logs/LogTable.tsx": 3,
"components/interfaces/Settings/Logs/LogsFormatters.tsx": 4,
"components/interfaces/Storage/BucketFilePickerDialog/BucketFilePickerExplorer.tsx": 1,
"components/interfaces/Storage/BucketFilePickerDialog/BucketFilePickerRow.tsx": 1,
"components/interfaces/Storage/StorageBucketsError.tsx": 1,
"components/interfaces/Storage/StorageExplorer/FileExplorer.tsx": 1,
"components/interfaces/Storage/StorageExplorer/FileExplorerRow.tsx": 2,
"components/interfaces/Storage/StorageExplorer/FileExplorerRowEditing.tsx": 2,
"components/interfaces/TableGridEditor/SidePanelEditor/RowEditor/ForeignRowSelector/SelectorGrid.tsx": 2,
"components/interfaces/TableGridEditor/SidePanelEditor/RowEditor/TextEditor.tsx": 1,
"components/interfaces/TableGridEditor/SidePanelEditor/TableEditor/Column.tsx": 1,
"components/interfaces/TableGridEditor/SidePanelEditor/TableEditor/ColumnManagement.tsx": 1,
"components/interfaces/TableGridEditor/SidePanelEditor/TableEditor/ForeignKeysManagement/ForeignKeyRow.tsx": 1,
"components/interfaces/UnifiedLogs/components/LogsListPanel.tsx": 2,
"components/layouts/Navigation/FloatingMobileToolbar/FloatingMobileToolbar.tsx": 1,
"components/layouts/Navigation/NavigationBar/MobileNavigationBar.tsx": 1,
"components/layouts/SQLEditorLayout/SQLEditorNavV2/SQLEditorNav.tsx": 1,
"components/layouts/WizardLayout.tsx": 2,
"components/ui/AIAssistantPanel/CollapsibleCodeBlock.tsx": 1,
"components/ui/AIAssistantPanel/MessageMarkdown.tsx": 1,
"components/ui/AIAssistantPanel/SupportAssistantSuccessCardContent.tsx": 1,
"components/ui/AIAssistantPanel/elements/Tool.tsx": 1,
"components/ui/AutoEnableRLSNotice.tsx": 1,
"components/ui/CodeEditor/CodeEditor.tsx": 2,
"components/ui/DataGridResults/CellDetailPanel.tsx": 1,
"components/ui/DataTable/DataTableInfinite.tsx": 1,
"components/ui/EdgeFunctionBlock/EdgeFunctionBlock.tsx": 1,
"components/ui/EditorPanel/EditorPanel.tsx": 1,
"components/ui/FileExplorerAndEditor/index.tsx": 2,
"components/ui/Forms/FormSection.tsx": 2,
"components/ui/QueryBlock/QueryBlock.tsx": 3,
"components/ui/ShimmerLine.tsx": 1,
"components/ui/Shimmers.tsx": 3,
"pages/project/[ref]/functions/[functionSlug]/code.tsx": 1,
"pages/project/[ref]/storage/vectors/buckets/[bucketId].tsx": 1
},
"shadcn/no-raw-colors": {
"components/interfaces/Advisors/AdvisorRuleItem.tsx": 1,
"components/interfaces/Auth/EmailTemplates/TemplateEditor.tsx": 1,
"components/interfaces/BranchManagement/BranchPanels.tsx": 1,
"components/interfaces/BranchManagement/CreateBranchModal.tsx": 6,
"components/interfaces/BranchManagement/DeleteBranchModal.tsx": 1,
"components/interfaces/ConnectSheet/content/steps/direct-connection/IPv4StatusPanel.tsx": 1,
"components/interfaces/Database/Functions/FunctionsList/FunctionsList.tsx": 2,
"components/interfaces/Database/Hooks/DeleteHookModal.tsx": 2,
"components/interfaces/Database/Migrations/Migrations.tsx": 1,
"components/interfaces/Database/Triggers/DeleteEventTrigger.tsx": 1,
"components/interfaces/Database/Triggers/TriggersList/TriggersList.tsx": 2,
"components/interfaces/DiskManagement/ui/DiskSpaceBar.tsx": 1,
"components/interfaces/Functions/FunctionsEmptyState.tsx": 1,
"components/interfaces/Integrations/CronJobs/DeleteCronJob.tsx": 1,
"components/interfaces/Integrations/Marketplace/MarketplaceFeaturedHeroGrid.tsx": 2,
"components/interfaces/Integrations/Queues/SingleQueue/DeleteQueue.tsx": 1,
"components/interfaces/Integrations/Queues/SingleQueue/PurgeQueue.tsx": 1,
"components/interfaces/Integrations/VercelGithub/IntegrationPanels.tsx": 1,
"components/interfaces/JwtSecrets/jwt-secret-keys-table/signing-key-row.tsx": 1,
"components/interfaces/Organization/BillingSettings/BillingBreakdown/BillingMetric.tsx": 2,
"components/interfaces/Organization/BillingSettings/Subscription/ExitSurveyModal.tsx": 1,
"components/interfaces/Organization/BillingSettings/Subscription/UpgradeModal.tsx": 1,
"components/interfaces/Organization/IntegrationSettings/SidePanelVercelProjectLinker.tsx": 1,
"components/interfaces/Organization/TeamSettings/MembersView.tsx": 1,
"components/interfaces/ProjectCreation/ProjectCreationForm.tsx": 1,
"components/interfaces/Reports/renderers/ApiRenderers.tsx": 1,
"components/interfaces/SQLEditor/UtilityPanel/UtilityActions.tsx": 4,
"components/interfaces/Settings/Integrations/IntegrationsSettings.tsx": 1,
"components/interfaces/Settings/Logs/Logs.DatePickers.tsx": 1,
"components/interfaces/SignIn/ForgotPasswordWizard.tsx": 2,
"components/interfaces/Storage/StoragePolicies/PolicyTemplates/TemplatesList.tsx": 1,
"components/interfaces/TableGridEditor/SidePanelEditor/OperationQueueSidePanel/DeleteRowOperationItem.tsx": 1,
"components/interfaces/TableGridEditor/SidePanelEditor/RowEditor/InputField.tsx": 1,
"components/interfaces/TableGridEditor/SidePanelEditor/RowEditor/JsonEditor/DrilldownViewer/DrilldownPane.tsx": 2,
"components/interfaces/TableGridEditor/SidePanelEditor/SpreadsheetImport/SpreadSheetFileUpload.tsx": 2,
"components/interfaces/UnifiedLogs/UnifiedLogs.utils.ts": 1,
"components/layouts/AppLayout/EnableBranchingButton/BranchingPITRNotice.tsx": 1,
"components/layouts/SQLEditorLayout/SQLEditorNavV2/SQLEditorTreeViewItem.tsx": 2,
"components/ui/AIAssistantPanel/Confirm.tsx": 1,
"components/ui/AIAssistantPanel/Message.Actions.tsx": 1,
"components/ui/AIAssistantPanel/MessagePartQueryLogs.tsx": 1,
"components/ui/DatePicker/TimeSplitInput.tsx": 1,
"components/ui/EdgeFunctionBlock/EdgeFunctionBlock.tsx": 2,
"components/ui/FileExplorerAndEditor/index.tsx": 1,
"components/ui/ProjectSettings/ToggleLegacyApiKeys.tsx": 1,
"components/ui/SqlWarningAdmonition.tsx": 1
}
}
}
+1 -1
View File
@@ -16,7 +16,7 @@
"build:tanstack": "vite build --mode ${MODE:-production}",
"start:tanstack": "node --env-file-if-exists=.env --env-file-if-exists=.env.local --env-file-if-exists=.env.${MODE:-production} --env-file-if-exists=.env.${MODE:-production}.local .output/server/index.mjs",
"lint": "eslint .",
"lint:ratchet": "tsx scripts/ratchet-eslint-rules.ts --rules-file scripts/ratchet-rules.json",
"lint:ratchet": "tsx node_modules/eslint-config-supabase/ratchet-eslint-rules.ts --rules-file scripts/ratchet-rules.json --rules-file node_modules/eslint-config-supabase/ratchet-rules.json",
"clean": "rimraf .next .turbo tsconfig.tsbuildinfo",
"test": "vitest --run --coverage",
"test:watch": "vitest watch",
@@ -1,171 +0,0 @@
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
import os from 'node:os'
import path from 'node:path'
import { afterEach, describe, expect, it, vi } from 'vitest'
import { runRatchet } from '../ratchet-eslint-rules'
const studioRoot = path.resolve(__dirname, '../..')
const repoRoot = path.resolve(studioRoot, '..', '..')
const scriptArgvPlaceholder = path.resolve(studioRoot, 'scripts', 'ratchet-eslint-rules.ts')
const tempDirs: string[] = []
afterEach(() => {
vi.restoreAllMocks()
while (tempDirs.length) {
const dir = tempDirs.pop()
if (dir) {
rmSync(dir, { recursive: true, force: true })
}
}
})
describe('ratchet-eslint-rules integration', () => {
it('captures per-file counts when initializing baselines', () => {
const tmp = createTempDir()
const metadataPath = path.join(tmp, 'baseline.json')
const eslintResults = buildEslintResults([
{ filePath: repoPath('apps/studio/src/a.ts'), rules: { 'no-console': 1 } },
{ filePath: repoPath('apps/studio/src/b.ts'), rules: { 'no-console': 2 } },
])
const result = invokeRatchet(
['--metadata', metadataPath, '--rule', 'no-console', '--init'],
eslintResults
)
expect(result).toBe(0)
const metadata = JSON.parse(readFileSync(metadataPath, 'utf8'))
expect(metadata.rules['no-console']).toBe(3)
expect(metadata.ruleFiles['no-console']).toEqual({
[relativeToCwd('apps/studio/src/a.ts')]: 1,
[relativeToCwd('apps/studio/src/b.ts')]: 2,
})
})
it('reports offending files when regressions occur and metadata has per-file data', () => {
const tmp = createTempDir()
const metadataPath = path.join(tmp, 'baseline.json')
writeFileSync(
metadataPath,
JSON.stringify(
{
rules: { 'no-console': 2 },
ruleFiles: {
'no-console': {
[relativeToCwd('apps/studio/src/a.ts')]: 2,
},
},
},
null,
2
)
)
const eslintResults = buildEslintResults([
{ filePath: repoPath('apps/studio/src/a.ts'), rules: { 'no-console': 3 } },
{ filePath: repoPath('apps/studio/src/b.ts'), rules: { 'no-console': 1 } },
])
const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
const result = invokeRatchet(
['--metadata', metadataPath, '--rule', 'no-console'],
eslintResults
)
expect(result).toBe(1)
const combinedErrors = errorSpy.mock.calls.map((args) => args.join(' ')).join('\n')
expect(combinedErrors).toContain(`${relativeToCwd('apps/studio/src/a.ts')} (+1)`)
expect(combinedErrors).toContain(`${relativeToCwd('apps/studio/src/b.ts')} (+1)`)
})
it('reads rule ids from --rules-file', () => {
const tmp = createTempDir()
const metadataPath = path.join(tmp, 'baseline.json')
const rulesFilePath = path.join(tmp, 'rules.json')
writeFileSync(rulesFilePath, JSON.stringify(['no-console', 'no-debugger']))
const eslintResults = buildEslintResults([
{ filePath: repoPath('apps/studio/src/a.ts'), rules: { 'no-console': 1, 'no-debugger': 2 } },
])
const result = invokeRatchet(
['--metadata', metadataPath, '--rules-file', rulesFilePath, '--init'],
eslintResults
)
expect(result).toBe(0)
const metadata = JSON.parse(readFileSync(metadataPath, 'utf8'))
expect(metadata.rules['no-console']).toBe(1)
expect(metadata.rules['no-debugger']).toBe(2)
})
it('falls back gracefully when baseline is missing per-file data', () => {
const tmp = createTempDir()
const metadataPath = path.join(tmp, 'baseline.json')
writeFileSync(
metadataPath,
JSON.stringify(
{
rules: { 'no-console': 1 },
},
null,
2
)
)
const eslintResults = buildEslintResults([
{ filePath: repoPath('apps/studio/src/a.ts'), rules: { 'no-console': 2 } },
])
const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
const result = invokeRatchet(
['--metadata', metadataPath, '--rule', 'no-console'],
eslintResults
)
expect(result).toBe(1)
const combinedErrors = errorSpy.mock.calls.map((args) => args.join(' ')).join('\n')
expect(combinedErrors).toContain('baseline missing file breakdown')
expect(combinedErrors).toContain(`${relativeToCwd('apps/studio/src/a.ts')} (2 current)`)
})
})
function buildEslintResults(
files: Array<{ filePath: string; rules: Record<string, number> }>
): unknown[] {
return files.map(({ filePath, rules }) => ({
filePath,
messages: Object.entries(rules).flatMap(([ruleId, count]) =>
Array.from({ length: count }, () => ({ ruleId }))
),
}))
}
function createTempDir(): string {
const dir = mkdtempSync(path.join(os.tmpdir(), 'ratchet-eslint'))
tempDirs.push(dir)
return dir
}
function repoPath(relPath: string): string {
return path.join(repoRoot, relPath)
}
function invokeRatchet(args: string[], eslintResults: unknown[]): number {
const argv = ['node', scriptArgvPlaceholder, ...args]
return runRatchet(argv, () => ({
results: eslintResults as any,
stderr: '',
}))
}
function relativeToCwd(relPath: string): string {
return path.relative(process.cwd(), repoPath(relPath)).split(path.sep).join('/')
}
-460
View File
@@ -1,460 +0,0 @@
/* eslint-disable turbo/no-undeclared-env-vars */
/**
* Ratchet ESLint violations for selected rules.
*
* Examples:
* # Initialize baselines for two rules
* tsx scripts/ratchet-eslint-rules.ts --init \
* --rule react-hooks/exhaustive-deps --rule no-console
*
* # Compare current counts vs baselines
* tsx scripts/ratchet-eslint-rules.ts \
* --rule react-hooks/exhaustive-deps --rule no-console
*
# Decrease baselines when improvements occur
* tsx scripts/ratchet-eslint-rules.ts \
* --rule react-hooks/exhaustive-deps --rule no-console \
* --decrease-baselines
*
* Flags:
* --metadata <path> Path to baseline file (default .github/eslint-rule-baselines.json)
* --init Write current counts for the provided --rule(s) into metadata and exit 0
* --eslint "<cmd>" ESLint command to run (default "npx eslint"). Do not pass untrusted input.
* --eslint-args "<...>" Extra args/paths for ESLint (e.g., "."). Do not pass untrusted input.
* --rule <id>[,<id>...] Rule id(s). Repeat flag or comma-separate.
* --rules-file <path> Path to a JSON file containing an array of rule id strings.
* Combines with --rule if both are given. One of the two is REQUIRED.
* --decrease-baselines When improvements occur, lower stored baselines to match the new counts.
*
* Notes:
* - Counts occurrences regardless of severity (warn/error).
* - Fails if any selected rule has currentCount > baselineCount.
*/
import { spawnSync } from 'node:child_process'
import { appendFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
import path from 'node:path'
import { pathToFileURL } from 'node:url'
interface Args {
metadata: string
init: boolean
eslint: string
eslintArgs: string
decreaseBaselines: boolean
rules: string[]
rulesFile?: string
}
interface ESLintMessage {
ruleId?: string | null
}
interface ESLintResult {
filePath?: string
messages?: ESLintMessage[]
}
interface ESLintExecutionResult {
results: ESLintResult[]
stderr: string
}
interface BaselineData {
rules: Record<string, number>
ruleFiles?: Record<string, Record<string, number>>
}
interface RuleSnapshot {
total: number
files: Record<string, number>
}
function readRulesFile(filePath: string): string[] {
let raw: string
try {
raw = readFileSync(filePath, 'utf8')
} catch (e) {
console.error(`Error: Could not read --rules-file ${filePath}: ${e}`)
process.exit(2)
}
let parsed: unknown
try {
parsed = JSON.parse(raw)
} catch (e) {
console.error(`Error: Could not parse --rules-file ${filePath} as JSON: ${e}`)
process.exit(2)
}
if (!Array.isArray(parsed) || !parsed.every((r) => typeof r === 'string')) {
console.error(`Error: --rules-file ${filePath} must contain a JSON array of rule id strings.`)
process.exit(2)
}
return parsed
}
function parseArgs(argv: string[]): Args {
const args: Args = {
metadata: '.github/eslint-rule-baselines.json',
init: false,
eslint: 'npx eslint',
eslintArgs: '',
decreaseBaselines: false,
rules: [],
}
for (let i = 2; i < argv.length; i += 1) {
const a = argv[i]
if (a === '--init') {
args.init = true
} else if (a === '--metadata') {
args.metadata = argv[++i]
} else if (a === '--eslint') {
args.eslint = argv[++i]
} else if (a === '--eslint-args') {
args.eslintArgs = argv[++i]
} else if (a === '--rule') {
const val = (argv[++i] ?? '').trim()
if (val) {
args.rules.push(
...val
.split(',')
.map((s) => s.trim())
.filter(Boolean)
)
}
} else if (a === '--rules-file') {
args.rulesFile = argv[++i]
} else if (a === '--decrease-baselines') {
args.decreaseBaselines = true
} else {
console.warn(`Unknown argument: ${a}`)
}
}
if (args.rulesFile) {
args.rules.push(...readRulesFile(args.rulesFile))
}
if (args.rules.length === 0) {
console.error('Error: You must provide at least one --rule <rule-id> or a --rules-file.')
console.error('Example: --rule exhaustive-deps --rule no-console')
console.error('Example: --rules-file scripts/ratchet-rules.json')
process.exit(2)
}
const dedupedRules = new Set(args.rules)
args.rules = Array.from(dedupedRules)
return args
}
/**
* SECURITY:
* Directly spawns a command from its arguments. Should not be called with
* untrusted input.
*/
function dangerouslyRunEsLint(eslintCmd: string, eslintArgs: string): ESLintExecutionResult {
const fullCmd = `${eslintCmd} ${eslintArgs || ''} --format json`.trim()
const proc = spawnSync(fullCmd, {
shell: true,
encoding: 'utf8',
stdio: ['ignore', 'pipe', 'pipe'],
env: process.env,
maxBuffer: 128 * 1024 * 1024, // allow large ESLint JSON payloads (the studio repo regularly emits ~35MB+)
})
const stdout = typeof proc.stdout === 'string' ? proc.stdout : ''
const stderr = typeof proc.stderr === 'string' ? proc.stderr : ''
if (!stdout.trim()) {
console.error('ESLint did not produce JSON output. stderr:\n', stderr)
process.exit(2)
}
let results: ESLintResult[]
try {
results = JSON.parse(stdout) as ESLintResult[]
} catch (e) {
console.error('Failed to parse ESLint JSON output:', e)
console.error('Raw output (truncated to 4k):\n', stdout.slice(0, 4096))
process.exit(2)
}
return { results, stderr }
}
function normalizeFilePath(filePath?: string | null): string | null {
if (!filePath) return null
const rel = path.relative(process.cwd(), filePath)
const normalized = rel || path.basename(filePath)
return normalized.split(path.sep).join('/')
}
function collectRuleSnapshots(
results: ESLintResult[],
ruleIds: string[]
): Record<string, RuleSnapshot> {
const checkedIds = new Set(ruleIds)
const snapshots: Record<string, RuleSnapshot> = {}
for (const id of ruleIds) {
snapshots[id] = { total: 0, files: {} }
}
for (const file of results) {
if (!file || !Array.isArray(file.messages)) continue
const normalizedPath = normalizeFilePath(file.filePath)
for (const msg of file.messages) {
const id = msg?.ruleId ?? ''
if (id && checkedIds.has(id)) {
const snapshot = snapshots[id] ?? { total: 0, files: {} }
snapshot.total += 1
if (normalizedPath) {
snapshot.files[normalizedPath] = (snapshot.files[normalizedPath] ?? 0) + 1
}
snapshots[id] = snapshot
}
}
}
return snapshots
}
function readBaselines(fp: string): BaselineData {
if (!existsSync(fp)) return { rules: {}, ruleFiles: {} }
try {
const data = JSON.parse(readFileSync(fp, 'utf8')) as Partial<BaselineData>
if (data && typeof data === 'object' && data.rules && typeof data.rules === 'object') {
return { rules: data.rules, ruleFiles: data.ruleFiles ?? {} }
}
} catch {
// ignore invalid metadata files and fall back to blank baselines
}
return { rules: {}, ruleFiles: {} }
}
function writeBaselines(fp: string, updates: Record<string, RuleSnapshot>, merge = true): void {
const dir = path.dirname(fp)
mkdirSync(dir, { recursive: true })
let current: BaselineData = { rules: {}, ruleFiles: {} }
if (merge && existsSync(fp)) {
current = readBaselines(fp)
}
const nextRules = merge ? { ...current.rules } : {}
const nextRuleFiles = merge ? { ...(current.ruleFiles ?? {}) } : {}
for (const [rule, snapshot] of Object.entries(updates)) {
nextRules[rule] = snapshot.total
nextRuleFiles[rule] = snapshot.files
}
const next: BaselineData = { rules: nextRules, ruleFiles: nextRuleFiles }
writeFileSync(fp, `${JSON.stringify(next, null, 2)}\n`, 'utf8')
}
function writeSummary(markdown: string): void {
const summaryFile = process.env.GITHUB_STEP_SUMMARY
if (summaryFile) {
try {
appendFileSync(summaryFile, `${markdown}\n`, 'utf8')
} catch {
// ignore summary write errors because they shouldn't block the script
}
}
}
export function runRatchet(argv: string[], runEslint = dangerouslyRunEsLint): number {
const args = parseArgs(argv)
// SECURITY:
// Offloaded to user. Must document that they should not pass untrusted input
// via --eslint or --eslint-args.
const { results, stderr } = runEslint(args.eslint, args.eslintArgs)
// Filter out test files.
const filteredResults = results.filter((result) => !result.filePath?.includes('.test.'))
const currentSnapshots = collectRuleSnapshots(filteredResults, args.rules)
const currentCounts: Record<string, number> = {}
for (const rule of args.rules) {
currentCounts[rule] = currentSnapshots[rule]?.total ?? 0
}
if (args.init) {
writeBaselines(args.metadata, currentSnapshots, true)
const rows = Object.entries(currentCounts)
.map(([rule, count]) => `| \`${rule}\` | **${count}** |`)
.join('\n')
writeSummary(
[
`### ESLint rule baselines initialized`,
`Metadata: \`${args.metadata}\``,
``,
`| Rule | Baseline |`,
`| --- | ---: |`,
rows,
``,
].join('\n')
)
console.log(
`Initialized/updated baselines for: ${args.rules.join(', ')} (saved to ${args.metadata}).`
)
return 0
}
const baselineData = readBaselines(args.metadata)
const baselineRules = baselineData.rules || {}
const baselineRuleFiles = baselineData.ruleFiles || {}
const missing = args.rules.filter((r) => typeof baselineRules[r] !== 'number')
if (missing.length) {
const msg = `Missing baselines for: ${missing.join(', ')} in ${args.metadata}. Run with --init to set them.`
console.error(msg)
writeSummary(`### ESLint rule ratchet\n${msg}`)
console.log(`::error title=Missing baselines::${msg}`)
return 2
}
let failed = false
const tableRows: string[] = []
const improvedRules: string[] = []
const decreasedBaselines: Record<string, { from: number; to: number; snapshot: RuleSnapshot }> =
{}
for (const rule of args.rules) {
const baseline = baselineRules[rule] ?? 0
const current = currentCounts[rule] ?? 0
const delta = current - baseline
const currentSnapshot = currentSnapshots[rule] ?? { total: 0, files: {} }
const baselineFiles = baselineRuleFiles[rule] ?? {}
tableRows.push(
`| \`${rule}\` | **${baseline}** | **${current}** | ${delta >= 0 ? '+' : '-'}${delta} |`
)
if (current > baseline) {
failed = true
const delta = current - baseline
const baselineHasFiles = Object.hasOwn(baselineRuleFiles, rule)
const fileSummary = describeFileRegression(
baselineFiles,
currentSnapshot.files,
baselineHasFiles
)
const msgParts = [
`You added ${delta === 1 ? 'a new violation' : `${delta} new violations`} of ${rule}. Please fix it: baseline=${baseline}, current=${current}`,
]
if (fileSummary) {
msgParts.push(
`Affected files: ${fileSummary}${baselineHasFiles ? '' : ' (baseline missing file breakdown; rerun with --init to capture it)'}`
)
}
const msg = msgParts.join(' ')
console.error(msg)
console.log(`::error title=New violations::${msg}`)
} else if (current < baseline) {
improvedRules.push(rule)
if (args.decreaseBaselines) {
decreasedBaselines[rule] = { from: baseline, to: current, snapshot: currentSnapshot }
}
}
}
const summaryLines = [
`### ESLint rule ratchet`,
`Metadata: \`${args.metadata}\``,
``,
`| Rule | Baseline | Current | Δ |`,
`| --- | ---: | ---: | ---: |`,
...tableRows,
``,
]
if (args.decreaseBaselines && Object.keys(decreasedBaselines).length > 0) {
const updates: Record<string, RuleSnapshot> = {}
const details: string[] = []
const logParts: string[] = []
for (const [rule, { from, to, snapshot }] of Object.entries(decreasedBaselines)) {
updates[rule] = snapshot
details.push(`- \`${rule}\`: ${from} -> ${to}`)
logParts.push(`${rule}: ${from} -> ${to}`)
}
writeBaselines(args.metadata, updates, true)
summaryLines.push('', 'Baselines decreased for improved rules:', ...details, '')
console.log(`Baselines decreased for improved rules: ${logParts.join(', ')}`)
}
writeSummary(summaryLines.join('\n'))
if (failed) {
if (stderr && stderr.trim()) console.error('\nESLint stderr:\n', stderr)
return 1
} else {
console.log(
improvedRules.length > 0
? 'Nice! Some rules improved.'
: 'Stable: No regressions for selected rules.'
)
return 0
}
}
function main(): void {
const exitCode = runRatchet(process.argv, dangerouslyRunEsLint)
process.exit(exitCode)
}
if (process.argv[1]) {
const invokedPath = pathToFileURL(path.resolve(process.argv[1])).href
if (import.meta.url === invokedPath) {
main()
}
}
function describeFileRegression(
baselineFiles: Record<string, number>,
currentFiles: Record<string, number>,
baselineHasFiles: boolean
): string {
const MAX_FILES = 5
if (baselineHasFiles) {
const entries = Object.entries(currentFiles)
.map(([file, count]) => ({
file,
delta: count - (baselineFiles[file] ?? 0),
}))
.filter(({ delta }) => delta > 0)
.sort((a, b) => b.delta - a.delta || a.file.localeCompare(b.file))
if (!entries.length) return ''
return formatFileList(
entries.map(({ file, delta }) => `${file} (+${delta})`),
MAX_FILES
)
}
const currentEntries = Object.entries(currentFiles)
.sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]))
.map(([file, count]) => `${file} (${count} current)`)
if (!currentEntries.length) return ''
return formatFileList(currentEntries, MAX_FILES)
}
function formatFileList(entries: string[], maxFiles: number): string {
if (entries.length <= maxFiles) {
return entries.join(', ')
}
const remainder = entries.length - maxFiles
const plural = remainder === 1 ? 'file' : 'files'
return `${entries.slice(0, maxFiles).join(', ')}, +${remainder} more ${plural}`
}
+37
View File
@@ -0,0 +1,37 @@
{
"rules": {
"shadcn/no-arbitrary-values": 36,
"shadcn/no-unknown-classes": 11,
"shadcn/no-raw-colors": 2
},
"ruleFiles": {
"shadcn/no-arbitrary-values": {
"app/example/headless-app/layout.tsx": 3,
"app/example/oauth-consent/layout.tsx": 3,
"app/example/password-based-auth/layout.tsx": 3,
"app/example/social-auth/layout.tsx": 3,
"components/catalog-preview.tsx": 5,
"components/command-menu.tsx": 2,
"components/command.tsx": 2,
"components/component-preview.tsx": 7,
"components/library-overview.tsx": 3,
"components/mdx-components.tsx": 2,
"components/tanstack-db-generator/index.tsx": 1,
"registry/default/blocks/realtime-flow/components/realtime-flow-overlay.tsx": 1,
"registry/default/platform/platform-kit-nextjs/components/supabase-manager/index.tsx": 1
},
"shadcn/no-unknown-classes": {
"app/(app)/docs/[[...slug]]/page.tsx": 2,
"components/block-overview-tabs.tsx": 2,
"components/block-preview.tsx": 1,
"components/component-preview.tsx": 2,
"components/mdx-components.tsx": 1,
"registry/default/components/ui/popover.tsx": 1,
"registry/default/components/ui/select.tsx": 1,
"registry/default/components/ui/tooltip.tsx": 1
},
"shadcn/no-raw-colors": {
"components/mdx-components.tsx": 2
}
}
}
+1
View File
@@ -18,6 +18,7 @@
"test:headless-tools": "tsx scripts/test-headless-tools.mts",
"start": "next start",
"lint": "eslint .",
"lint:ratchet": "tsx node_modules/eslint-config-supabase/ratchet-eslint-rules.ts --rules-file node_modules/eslint-config-supabase/ratchet-rules.json",
"clean": "rimraf .next .turbo tsconfig.tsbuildinfo .contentlayer .velite",
"typecheck": "run-p build:content build:markdown && next typegen && tsc --noEmit -p tsconfig.json"
},
+277
View File
@@ -0,0 +1,277 @@
{
"rules": {
"shadcn/no-arbitrary-values": 527,
"shadcn/no-unknown-classes": 112,
"shadcn/no-raw-colors": 212
},
"ruleFiles": {
"shadcn/no-arbitrary-values": {
"app/(home)/_components/BuiltWithSupabaseSection.tsx": 2,
"app/(products)/auth/_components/FeaturesSection.tsx": 40,
"app/(products)/database/_components/FeaturesSection.tsx": 15,
"app/(products)/database/_components/Illustrations/SpreadsheetIllustration.tsx": 9,
"app/(products)/database/_components/VisualDemo.tsx": 5,
"app/(products)/edge-functions/_components/Highlights.tsx": 1,
"app/(products)/realtime/_components/FeaturesSection.tsx": 5,
"app/(products)/storage/_components/FeaturesSection.tsx": 16,
"app/contribute/about/page.tsx": 1,
"app/partners/PartnersContent.tsx": 2,
"app/state-of-startups/components/StateOfStartupsAuroraHeader.tsx": 2,
"app/state-of-startups/components/StateOfStartupsHeader.tsx": 2,
"app/state-of-startups/register/RegisterContent.tsx": 12,
"components/Blog/BlogGridItem.tsx": 1,
"components/BuiltWithSupabaseSection.tsx": 2,
"components/Changelog/ChangelogInlineMarkdown.tsx": 1,
"components/Changelog/ChangelogLlmMarkdownButton.tsx": 2,
"components/Changelog/ChangelogTimelineList.tsx": 2,
"components/Contribute/Hero.tsx": 1,
"components/Contribute/Icons.tsx": 2,
"components/Contribute/UnansweredThreadsTable.tsx": 2,
"components/Contribute/UserProfile.tsx": 2,
"components/CustomerStories/CutomsersSliderDesktop.tsx": 1,
"components/Error404.tsx": 3,
"components/Events/new/EventTimeline.tsx": 1,
"components/FeaturesMatrix.tsx": 1,
"components/Footer/index.tsx": 1,
"components/Go/TweetsSection.tsx": 2,
"components/LaunchWeek/11/HackathonCallout.tsx": 1,
"components/LaunchWeek/11/LW11Day1.tsx": 1,
"components/LaunchWeek/11/LW11Meetups.tsx": 1,
"components/LaunchWeek/11/LW11Summary.tsx": 1,
"components/LaunchWeek/11/LaunchWeekLogoHeader.tsx": 4,
"components/LaunchWeek/11/LaunchWeekPrizeSection.tsx": 1,
"components/LaunchWeek/11/Releases/BuildStage.tsx": 1,
"components/LaunchWeek/11/Releases/components/AdventCard.tsx": 2,
"components/LaunchWeek/11/Releases/components/DaySection.tsx": 6,
"components/LaunchWeek/12/HackathonCallout.tsx": 1,
"components/LaunchWeek/12/LWMeetups.tsx": 1,
"components/LaunchWeek/12/LaunchWeekLogoHeader.tsx": 4,
"components/LaunchWeek/12/LaunchWeekPrizeSection.tsx": 1,
"components/LaunchWeek/12/Releases/BuildStage.tsx": 1,
"components/LaunchWeek/12/Releases/Day1.tsx": 1,
"components/LaunchWeek/12/Releases/LWHeader.tsx": 1,
"components/LaunchWeek/12/Releases/components/BuildCard.tsx": 2,
"components/LaunchWeek/12/Releases/components/DaySection.tsx": 2,
"components/LaunchWeek/13/Releases/Day1.tsx": 1,
"components/LaunchWeek/13/Releases/components/BuildCard.tsx": 2,
"components/LaunchWeek/13/Releases/components/DaySection.tsx": 2,
"components/LaunchWeek/14/ActionButton.tsx": 6,
"components/LaunchWeek/14/Header.tsx": 4,
"components/LaunchWeek/14/Releases/components/BuildCard.tsx": 2,
"components/LaunchWeek/14/Releases/components/DaySection.tsx": 2,
"components/LaunchWeek/15/ActionButton.tsx": 6,
"components/LaunchWeek/15/LW15MainStage.tsx": 3,
"components/LaunchWeek/7/BlogLinks.tsx": 2,
"components/LaunchWeek/7/LabelBadge.tsx": 3,
"components/LaunchWeek/7/LaunchSection/LaunchWeekLogoHeader.tsx": 3,
"components/LaunchWeek/7/LaunchWeekPrizeCard.tsx": 1,
"components/LaunchWeek/7/LaunchWeekPrizeSection.tsx": 3,
"components/LaunchWeek/7/Releases/components/index.tsx": 22,
"components/LaunchWeek/7/Releases/index.tsx": 6,
"components/LaunchWeek/8/LWArchive.tsx": 3,
"components/LaunchWeek/8/LaunchWeekLogoHeader.tsx": 4,
"components/LaunchWeek/8/LaunchWeekPrizeCard.tsx": 1,
"components/LaunchWeek/8/Releases/LWAnnouncement.tsx": 1,
"components/LaunchWeek/8/Releases/components/index.tsx": 20,
"components/LaunchWeek/8/Releases/index.tsx": 17,
"components/LaunchWeek/X/Album/Player.tsx": 2,
"components/LaunchWeek/X/LWXMeetups.tsx": 7,
"components/LaunchWeek/X/LWXSummary.tsx": 1,
"components/LaunchWeek/X/LaunchWeekLogoHeader.tsx": 4,
"components/LaunchWeek/X/LaunchWeekPrizeCard.tsx": 2,
"components/LaunchWeek/X/LaunchWeekPrizeSection.tsx": 5,
"components/LaunchWeek/X/Releases/BuildStage.tsx": 1,
"components/LaunchWeek/X/Releases/LWXStickyNav.tsx": 3,
"components/LaunchWeek/X/Releases/MainStage.tsx": 4,
"components/LaunchWeek/X/Releases/components/AdventCard.tsx": 7,
"components/LaunchWeek/X/Releases/components/DaySection.tsx": 9,
"components/Nav/ProductDropdown.tsx": 1,
"components/Nav/index.tsx": 3,
"components/OpenSource/OSSHero.tsx": 1,
"components/OpenSource/Sponsorships.tsx": 2,
"components/Panel/Panel.tsx": 1,
"components/Partners/BackgroundPattern.tsx": 1,
"components/Pricing/ComputePricingCalculator.tsx": 1,
"components/Pricing/PricingAddons.tsx": 4,
"components/Pricing/PricingComparisonTable.tsx": 1,
"components/Pricing/PricingComputeAnimation.tsx": 3,
"components/Pricing/PricingComputeSection.tsx": 4,
"components/Pricing/PricingPlans.tsx": 15,
"components/Products/DataAPIsVisual.tsx": 1,
"components/Products/Functions/CI.tsx": 1,
"components/Products/Functions/RealtimeLogs.tsx": 2,
"components/Products/RealtimeVisual.tsx": 10,
"components/Products/index.tsx": 1,
"components/Regions/RegionTooltip.tsx": 2,
"components/Sections/FeaturesSection.tsx": 1,
"components/Sections/ParagraphSection.tsx": 1,
"components/Sections/ProductHeader2.tsx": 1,
"components/Sections/ProductModulesHeader.tsx": 1,
"components/Sections/TimedTabsSection.tsx": 2,
"components/Sections/TwitterSocialProof.tsx": 2,
"components/Solutions/PostGrid.tsx": 1,
"components/SurveyResults/StateOfStartupsHeader.tsx": 2,
"components/UsersGrowthChart.tsx": 2,
"components/Wrapped/Pages/CustomerStories.tsx": 1,
"components/Wrapped/Pages/Devs.tsx": 1,
"components/Wrapped/Pages/Home.tsx": 1,
"components/Wrapped/Pages/ProductAnnouncements.tsx": 1,
"components/Wrapped/Pages/SupabaseSelect.tsx": 1,
"components/Wrapped/Pages/YearOfAI.tsx": 1,
"components/logos.tsx": 2,
"data/aws-reinvent-2025.tsx": 5,
"data/home/content.tsx": 4,
"data/products/functions/page.tsx": 1,
"data/solutions/agencies.tsx": 9,
"data/solutions/beginners.tsx": 6,
"data/solutions/developers.tsx": 9,
"data/solutions/firebase.tsx": 9,
"data/solutions/hackathon.tsx": 9,
"data/solutions/innovation-teams.tsx": 6,
"data/solutions/neon.tsx": 9,
"data/solutions/no-code.tsx": 5,
"data/solutions/postgres-developers.tsx": 9,
"data/solutions/startups.tsx": 9,
"data/solutions/vibe-coders.tsx": 5,
"pages/careers.tsx": 12,
"pages/changelog.tsx": 1,
"pages/contact-us.tsx": 3,
"pages/features.tsx": 5,
"pages/ga.tsx": 4,
"pages/launch-week/7/index.tsx": 2,
"pages/launch-week/8/index.tsx": 1,
"pages/support.tsx": 2
},
"shadcn/no-unknown-classes": {
"app/partners/PartnersContent.tsx": 1,
"app/state-of-startups/components/ParticipantsCarousel.tsx": 1,
"components/AIDemo/DotGrid.tsx": 1,
"components/AIDemo/Panel.tsx": 1,
"components/Announcement/Badge.tsx": 6,
"components/Avatar.tsx": 1,
"components/Blog/BlogCollapsible.tsx": 2,
"components/Card.tsx": 1,
"components/Changelog/ChangelogTimelineList.tsx": 1,
"components/CodeBlock/CodeBlock.tsx": 1,
"components/CustomerStories/CutomsersSliderDesktop.tsx": 1,
"components/Enterprise/CTAForm.tsx": 1,
"components/Go/TweetsSection.tsx": 2,
"components/LaunchWeek/11/LW11Day1.tsx": 2,
"components/LaunchWeek/11/Releases/BuildStage.tsx": 1,
"components/LaunchWeek/11/Releases/LW11StickyNav.tsx": 1,
"components/LaunchWeek/11/Releases/components/AdventCard.tsx": 1,
"components/LaunchWeek/11/Releases/components/DaySection.tsx": 2,
"components/LaunchWeek/12/Releases/BuildStage.tsx": 1,
"components/LaunchWeek/12/Releases/Day1.tsx": 2,
"components/LaunchWeek/12/Releases/LWStickyNav.tsx": 1,
"components/LaunchWeek/12/Releases/components/BuildCard.tsx": 1,
"components/LaunchWeek/12/Releases/components/DaySection.tsx": 2,
"components/LaunchWeek/13/Releases/BuildStage.tsx": 1,
"components/LaunchWeek/13/Releases/Day1.tsx": 2,
"components/LaunchWeek/13/Releases/LWStickyNav.tsx": 2,
"components/LaunchWeek/13/Releases/components/BuildCard.tsx": 1,
"components/LaunchWeek/13/Releases/components/DaySection.tsx": 2,
"components/LaunchWeek/14/Releases/BuildStage.tsx": 1,
"components/LaunchWeek/14/Releases/LWStickyNav.tsx": 2,
"components/LaunchWeek/14/Releases/components/BuildCard.tsx": 1,
"components/LaunchWeek/14/Releases/components/DaySection.tsx": 2,
"components/LaunchWeek/15/LW15BuildStage.tsx": 1,
"components/LaunchWeek/15/LW15Hackathon.tsx": 1,
"components/LaunchWeek/15/LW15Heading.tsx": 6,
"components/LaunchWeek/7/LaunchSection/LaunchWeekLogoHeader.tsx": 1,
"components/LaunchWeek/7/Releases/Day5.tsx": 2,
"components/LaunchWeek/7/Releases/index.tsx": 1,
"components/LaunchWeek/X/Album/Player.tsx": 1,
"components/LaunchWeek/X/Releases/BuildStage.tsx": 1,
"components/LaunchWeek/X/Releases/LWXStickyNav.tsx": 1,
"components/LaunchWeek/X/Releases/components/DaySection.tsx": 2,
"components/Nav/MobileMenu.tsx": 1,
"components/OpenSource/RepoCard.tsx": 1,
"components/OpenSource/Repos.tsx": 2,
"components/OpenSource/Sponsorships.tsx": 1,
"components/Pricing/CostControlAnimation.tsx": 2,
"components/Pricing/PricingComparisonTable.tsx": 1,
"components/Pricing/PricingTableRow.tsx": 4,
"components/Products/Functions/RealtimeLogs.tsx": 2,
"components/Products/RealtimeVisual.tsx": 1,
"components/Products/StorageVisual.tsx": 1,
"components/Sections/APISection.tsx": 1,
"components/Sections/FunctionsUsecases.tsx": 1,
"components/Sections/HighlightCards.tsx": 1,
"components/Sections/ImageParagraphSection.tsx": 2,
"components/Sections/ProductHeader2.tsx": 2,
"components/Sections/TwitterSocialProof.tsx": 1,
"components/Solutions/CTAForm.tsx": 1,
"components/Solutions/FeaturesGrid.tsx": 2,
"components/Solutions/TwoColumnsSection.tsx": 2,
"components/Tabs/Tabs.tsx": 2,
"components/TabsWithHighlights.tsx": 2,
"components/VideoWithHighlights.tsx": 2,
"components/logos.tsx": 2,
"pages/beta.tsx": 2,
"pages/careers.tsx": 1,
"pages/changelog/[slug].tsx": 1,
"pages/contact/sales.tsx": 1,
"pages/customers/[slug].tsx": 3,
"pages/support.tsx": 2
},
"shadcn/no-raw-colors": {
"app/(home)/_components/logos/FrameworkLogos.tsx": 2,
"app/(home)/_components/logos/PublicityLogos.tsx": 12,
"app/(products)/auth/_components/FeaturesSection.tsx": 9,
"app/(products)/database/_components/FeaturesSection.tsx": 4,
"app/(products)/realtime/_components/FeaturesSection.tsx": 4,
"app/state-of-startups/StateOfStartups2026Content.tsx": 2,
"components/AIDemo/Panel.tsx": 1,
"components/CaseStudies/benchmark.tsx": 1,
"components/Charts/DeveloperGrowthChart.tsx": 2,
"components/ExampleCard.tsx": 3,
"components/LaunchWeek/14/ActionButton.tsx": 12,
"components/LaunchWeek/14/Header.tsx": 6,
"components/LaunchWeek/15/ActionButton.tsx": 12,
"components/LaunchWeek/7/LaunchWeekPrizeSection.tsx": 2,
"components/LaunchWeek/7/Releases/components/index.tsx": 8,
"components/LaunchWeek/7/Releases/index.tsx": 3,
"components/LaunchWeek/8/LWArchive.tsx": 1,
"components/LaunchWeek/8/Releases/components/index.tsx": 11,
"components/LaunchWeek/X/Album/Player.tsx": 1,
"components/LaunchWeek/X/Releases/BuildStage.tsx": 1,
"components/LaunchWeek/X/Releases/data/lwx_advent_days.tsx": 20,
"components/Nav/ProductDropdown.tsx": 1,
"components/OpenSource/RepoCard.tsx": 2,
"components/Panel/Panel.tsx": 1,
"components/Partners/BackgroundPattern.tsx": 1,
"components/Pricing/PricingIcons.tsx": 1,
"components/Pricing/PricingTableRow.tsx": 1,
"components/Products/DatabaseVisual.tsx": 3,
"components/Products/Functions/Metrics.tsx": 2,
"components/Products/VectorAI/OpenAIImage.tsx": 4,
"components/Products/VectorVisual.tsx": 2,
"components/Realtime/sandpack.tsx": 1,
"components/Sections/CustomerQuotesSection.tsx": 1,
"components/SurveyResults/SurveyChart.tsx": 1,
"components/code-display.tsx": 5,
"data/BetaPage.js": 6,
"data/Solutions.tsx": 9,
"data/products/database/pipelines-visual.tsx": 19,
"data/solutions/agencies.tsx": 1,
"data/solutions/ai-builders.tsx": 14,
"data/solutions/beginners.tsx": 1,
"data/solutions/developers.tsx": 1,
"data/solutions/firebase.tsx": 1,
"data/solutions/hackathon.tsx": 1,
"data/solutions/neon.tsx": 1,
"data/solutions/no-code.tsx": 1,
"data/solutions/postgres-developers.tsx": 1,
"data/solutions/startups.tsx": 1,
"data/solutions/vibe-coders.tsx": 1,
"pages/assistant.tsx": 2,
"pages/beta.tsx": 2,
"pages/brand-assets.tsx": 2,
"pages/modules/cron.tsx": 1,
"pages/modules/queues.tsx": 1,
"pages/modules/vector.tsx": 1,
"pages/state-of-startups-2025.tsx": 2
}
}
}
+1
View File
@@ -12,6 +12,7 @@
"export": "next export",
"start": "next start",
"lint": "eslint .",
"lint:ratchet": "tsx node_modules/eslint-config-supabase/ratchet-eslint-rules.ts --rules-file node_modules/eslint-config-supabase/ratchet-rules.json",
"clean": "rimraf .next .turbo tsconfig.tsbuildinfo",
"pretypecheck": "next typegen",
"typecheck": "pnpm run content:build:core && tsc --noEmit",