Skip to content

chore(lint): update oxlint to ^1.86.0 and align its config with sanity-io/ui - #2049

Draft
stipsan wants to merge 10 commits into
mainfrom
cody/align-oxlint-with-ui-d446
Draft

stipsan wants to merge 10 commits into
mainfrom
cody/align-oxlint-with-ui-d446

Conversation

@stipsan

@stipsan stipsan commented Sep 29, 2026 •

Copy link
Copy Markdown
Member

Brings oxlint in line with sanity-io/ui's main: the same oxlint and oxlint-tsgolint versions and the same resolved rule set, with type-aware linting and type checking on as before. It also runs oxlint on staged files in the pre-commit hook, mirroring sanity-io/sanity's lefthook job.

What's in this PR

  1. Config (chore(lint))

    • oxlint ^1.83.0 → ^1.86.0 and oxlint-tsgolint ^7.0.2001 → ^7.0.2003 in the catalog, ui's ranges. main already resolves 1.86.0 and 7.0.2003 since the lock file maintenance in chore(deps): lock file maintenance #2039; this raises the floor to the versions the config relies on.
    • The rules now resolve to exactly ui's .oxlintrc.json rule set (compared with oxlint --print-config on both):
      • oxc/no-barrel-file is on and the styled-components default import is banned, both in the shared @sanity/plugin-kit/oxlint preset, where AGENTS.md says shared rules live. The react-perf plugin is listed as in ui; all four of its rules stay off in both, so that's a no-op.
      • react/exhaustive-effect-dependencies is off in the root config (it was on). As in ui, react/exhaustive-deps covers effects, memos, callbacks and custom hooks, and the compiler rule only duplicates it for effects.
    • Already identical to ui, so unchanged: typeAware/typeCheck, denyWarnings, reportUnusedDisableDirectives, the categories, the lint/lint:fix/oxlint scripts, and CI running pnpm lint --format github. ui runs that inside its build job; plugins keeps its separate lint job because the main ruleset requires that check name. The Storybook lint wiring (eslint-plugin-storybook through jsPlugins, with ui's story overrides) came with feat(themer): move @sanity/themer from sanity-io/ui, with a Storybook and ui's Changesets setup #2046.
    • Kept on top of ui: this repo's lodash-es/date-fns/date-fns-tz import bans, its file-specific overrides, and its config structure (a root oxlint.config.ts extending the plugin-kit preset that standalone plugins reuse, rather than ui's .oxlintrc.json). oxlint/oxlint-tsgolint also stay in the root dependencies (ui lists them as devDependencies); moving them changes nothing in a private root.
    • A patch changeset for @sanity/plugin-kit (its oxlint peer uses the default catalog, and the preset gained two rules), and AGENTS.md notes on the rule exception and the barrel rule.
  2. Lint fixes (fix(lint) commits): comments only, no code line changes. Against main, this leaves:

    The first fix commit also carried fix(lint): add suppressions for oxlint 1.86.0 diagnostics #2045's fixes for oxlint 1.86 itself: 37 no-unsafe-type-assertion directives moved to the as line, since tsgolint 7.0.2002 narrowed the rule's primary range (feat(no-unsafe-type-assertion): improve diagnostics oxc-project/tsgolint#1111), and 6 react/purity suppressions for zero-argument new Date() (fix(react-compiler): treat zero-argument new Date as impure oxc-project/oxc#26894). fix(lint): add suppressions for oxlint 1.86.0 diagnostics #2045 reached main with chore(deps): lock file maintenance #2039, so those changes merged away.

  3. Pre-commit oxlint (chore(lint)): lefthook.yml's scripts job becomes sanity-io/sanity's format-and-lint-scripts. After oxfmt, it runs oxlint --disable-nested-config --fix --quiet --no-error-on-unmatched-pattern {staged_files}, and stage_fixed re-stages the fixes. Like the oxfmt jobs, it calls node_modules/.bin/oxlint directly, so pnpm 12's implicit install never runs. The glob keeps this repo's .mts/.cts. AGENTS.md documents the step.

  4. Merges of main for chore(deps): lock file maintenance #2039, chore(deps): update pnpm to v12.6.0 with sanity-io/ui's pnpm setup #2048 (pnpm 12.6.0), feat(themer): move @sanity/themer from sanity-io/ui, with a Storybook and ui's Changesets setup #2046 (the themer move), Version Packages #2053 (Version Packages) and chore(format): align oxfmt with sanity-io/ui (^0.71.0, config), pre-commit hook on lefthook #2047 (oxfmt 0.71 and lefthook). Each is a regular merge commit, with nothing force-pushed. For chore(format): align oxfmt with sanity-io/ui (^0.71.0, config), pre-commit hook on lefthook #2047 the adjacent oxfmt/oxlint catalog lines keep both sides, and this PR's two changesets are requoted for oxfmt 0.71 in a separate commit.

Testing the hook

Throwaway commits in a local clone with the lefthook hook installed, dropped afterwards:

  • A staged console.info plus a type error: the commit is blocked. oxlint reports no-console and TS2322 in about 0.6s.
  • debugger or !!value in a condition: the commit is blocked. oxlint classes those fixes as suggestions, which --fix doesn't apply, the same as in sanity-io/sanity.
  • An unformatted file with an unnecessary as assertion and [...new Set(items)].length: the commit goes through. The committed file is formatted, with both safe fixes applied (typescript/no-unnecessary-type-assertion, unicorn/prefer-set-size).
  • pnpm-lock.yaml stayed untouched in every case.

Lockfile

Each merge took main's pnpm-lock.yaml and let pnpm 12.6.0, run directly, re-apply this PR's catalog change. The committed lockfile differs from main only by the oxlint and oxlint-tsgolint catalog specifiers. No resolved version changes, the packageManagerDependencies block is intact, and the Studio stays on main's sanity@6.17.0-next.108.

  • Studio next pin: for the chore(format): align oxfmt with sanity-io/ui (^0.71.0, config), pre-commit hook on lefthook #2047 merge, the next dist-tag had moved to 6.17.0-next.112, so a plain re-resolution upgraded the whole Studio monorepo. As AGENTS.md describes, the regeneration ran through a local registry proxy serving the locked 6.17.0-next.108 as next. The lockfile has no localhost or tarball: entries.
  • Peer toggle: each regeneration also flipped pnpm 12.6.0's known optional-peer toggle on plugins/@sanity/debug-preview-url-secret-plugin, which AGENTS.md says not to commit. A second re-resolution (pnpm dedupe) undid it.

Existing PRs

Sibling PRs

The vitest 5 and tsdown PRs overlap only in pnpm-workspace.yaml catalog lines and the lockfile. If one merges first, main gets merged in here again with the lockfile re-applied the same way, and lint rerun.

What Cody needs to do

Open questions

  1. Suppress or debarrel? ui debarreled before enabling oxc/no-barrel-file; here the 17 existing barrels are suppressed to keep this minimal and leave published APIs untouched. Worth a follow-up to replace them with named re-exports (or export type * where a module only exports types)?
  2. Preset or root config? oxc/no-barrel-file and the styled-components ban are in @sanity/plugin-kit/oxlint, so standalone plugins get them with their next plugin-kit update. They could live in the root config only instead.
  3. Release the comment-only changes? Following Bump oxlint to 1.79 and enable all React Compiler rules #1926, the 17 plugins get patch releases whose published code is unchanged. The changeset can be dropped if you'd rather not release them; fix(lint): add suppressions for oxlint 1.86.0 diagnostics #2045 landed its comment changes without one.

Verification

Local runs on head ac077b1, with pnpm 12.6.0 on Node 22.22:

  • pnpm lint and pnpm knip pass. oxfmt --check passes on every changed file and repo-wide (2,020 files).
  • The lint covers the themer package and dev/storybook.
  • pnpm build builds all 54 turbo tasks.
  • pnpm test run passes: 235 files and 1,509 tests.

On this PR, every CI check passes: lint, build, knip, the four test shards, storybook-test, e2e (chromium), e2e (firefox), Bugbot and both Vercel previews.

Open in Web Open in Cursor 

cursoragent and others added 2 commits September 29, 2026 00:20
…y-io/ui

Bump the oxlint catalog entry to ^1.86.0 and oxlint-tsgolint to ^7.0.2003,
the versions sanity-io/ui uses, and match its resolved rule set:

- enable `oxc/no-barrel-file` and ban the `styled-components` default
  import in the shared `@sanity/plugin-kit/oxlint` preset, and list the
  `react-perf` plugin (its rules stay off)
- turn off `react/exhaustive-effect-dependencies` in the root config; it
  only checks effects and duplicates `react/exhaustive-deps`

The lint findings this surfaces are fixed in the next commit.
- no-unsafe-type-assertion: tsgolint 7.0.2002 narrowed the rule's primary
  range to the `as Type` syntax (oxc-project/tsgolint#1111), so multi-line
  assertions are now suppressed from the line above `as` rather than the
  statement start
- react/purity: oxlint 1.86 treats zero-argument `new Date()` as impure
  (oxc-project/oxc#26894); suppress the existing render-time calls
- oxc/no-barrel-file: suppress the 17 existing `export *` barrels
- react/exhaustive-effect-dependencies: drop the directives that went
  unused when the rule was turned off

Only comments change. The suppression moves and the react/purity
suppressions are carried over from #2045.

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
@changeset-bot

changeset-bot Bot commented Sep 29, 2026 •

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: ac077b1

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 23 packages
Name Type
@sanity/assist Patch
@sanity/block-insert-picker Patch
@sanity/code-input Patch
@sanity/cross-dataset-duplicator Patch
@sanity/dashboard Patch
@sanity/document-internationalization Patch
@sanity/embeddings-index-ui Patch
@sanity/personalization-plugin Patch
@sanity/sfcc Patch
@sanity/themer Patch
sanity-naive-html-serializer Patch
sanity-plugin-internationalized-array Patch
sanity-plugin-markdown Patch
sanity-plugin-media Patch
sanity-plugin-mux-input Patch
sanity-plugin-shopify-assets Patch
sanity-translations-tab Patch
@sanity/plugin-kit Patch
sanity-plugin-dashboard-widget-document-list Patch
sanity-plugin-dashboard-widget-netlify Patch
sanity-plugin-dashboard-widget-vercel Patch
sanity-plugin-studio-smartling Patch
sanity-plugin-transifex Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@vercel

vercel Bot commented Sep 29, 2026 •

Copy link
Copy Markdown

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

Project Deployment Actions Updated
plugins-studio Ready Ready Preview Sep 29, 2026 9:27am UTC
1 Skipped Deployment
Project Deployment Actions Updated
plugins-e2e-test-studio Ignored Ignored Sep 29, 2026 9:27am UTC

Request Review

@github-actions

github-actions Bot commented Sep 29, 2026 •

Copy link
Copy Markdown

✅ E2E Tests

🟢 42 passed • 🟡 2 flaky • view full report • view run

Studio: https://plugins-e2e-test-studio-91ko4z061.sanity.dev

Datasets: pr-2049-chromium-36549162241, pr-2049-firefox-36549162241

@github-actions

github-actions Bot commented Sep 29, 2026 •

Copy link
Copy Markdown

Coverage Report

Status Category Percentage Covered / Total
🔵 Lines 35.3% 6421 / 18187
🔵 Statements 35.22% 6842 / 19426
🔵 Functions 31.45% 1716 / 5455
🔵 Branches 26.69% 3419 / 12810
File Coverage
File Stmts Branches Functions Lines Uncovered Lines
Changed Files
packages/@sanity/plugin-kit/src/oxlint.ts 100% 100% 100% 100%
plugins/@sanity/assist/src/index.ts 0% 0% 0% 0%
plugins/@sanity/assist/src/_lib/form/index.ts 0% 0% 0% 0%
plugins/@sanity/assist/src/assistDocument/components/AssistDocumentForm.tsx 0.98% 0% 0% 1.05% 43-288
plugins/@sanity/assist/src/assistDocument/hooks/useAssistDocumentContextValue.tsx 0% 0% 0% 0% 20-140
plugins/@sanity/block-insert-picker/src/blockInsertPicker.tsx 95.02% 85.98% 97.67% 97.51% 120, 142, 244-245, 264, 274, 346, 358, 371, 456-457
plugins/@sanity/cross-dataset-duplicator/src/index.ts 0% 0% 0% 0%
plugins/@sanity/dashboard/src/index.ts 0% 0% 0% 0%
plugins/@sanity/document-internationalization/src/index.ts 0% 0% 0% 0%
plugins/@sanity/embeddings-index-ui/src/index.ts 0% 0% 0% 0%
plugins/@sanity/personalization-plugin/src/index.ts 0% 0% 0% 0%
plugins/@sanity/personalization-plugin/src/components/experiment/index.ts 0% 0% 0% 0%
plugins/@sanity/personalization-plugin/src/components/personalization/index.ts 0% 0% 0% 0%
plugins/@sanity/sfcc/src/structure/index.ts 100% 100% 100% 100%
plugins/sanity-naive-html-serializer/src/BaseDocumentDeserializer/index.ts 0% 0% 0% 0%
plugins/sanity-plugin-internationalized-array/migrations/index.ts 0% 0% 0% 0%
plugins/sanity-plugin-internationalized-array/src/index.ts 0% 0% 0% 0%
plugins/sanity-plugin-markdown/src/index.ts 0% 0% 0% 0%
plugins/sanity-plugin-markdown/src/indexNext.ts 0% 0% 0% 0%
plugins/sanity-plugin-media/src/components/Browser/useBrowserInit.ts 48.43% 25% 55.55% 47.36% 21-24, 29-42, 49-62, 108, 123-141
plugins/sanity-plugin-mux-input/src/components/EditCaptionDialog.tsx 1.17% 0% 3.84% 0.62% 32-476
plugins/sanity-plugin-shopify-assets/src/index.ts 50% 100% 0% 50% 31-36
plugins/sanity-translations-tab/src/configuration/utils/index.ts 0% 0% 0% 0%
Generated in workflow #10222 for commit ac077b1 by the Vitest Coverage Report Action

Resolves the conflicts with the lock file maintenance in #2039, which also
brought #2045's suppression changes to main:

- pnpm-lock.yaml: took main's lockfile and ran `pnpm install` to re-apply
  this branch's catalog change. main already resolves oxlint 1.86.0 and
  oxlint-tsgolint 7.0.2003, so no resolved version changes; the diff is
  the catalog specifiers plus pnpm recomputing sanity@next peer-variant
  suffixes. A clean main with only this branch's pnpm-workspace.yaml
  change produces the identical lockfile.
- plugins/@sanity/assist/src/_lib/fixedListenQuery.ts: took main's version
  (#2045's), which already fixes the suppression.
…2039

@sanity/presets and sanity-plugin-hotspot-array only had #2045's suppression moves, which reached main with the lock file maintenance in #2039, so this PR no longer changes them.
stipsan added a commit that referenced this pull request Sep 29, 2026
…2048)

<!-- CURSOR_AGENT_PR_BODY_BEGIN -->
Puts this repo on the same pnpm version and settings as
[sanity-io/ui](https://github.com/sanity-io/ui) `main`: `packageManager`
`pnpm@12.6.0`, ui's pnpm settings in `pnpm-workspace.yaml`, ui's CI
setup action (`pnpm/setup@v3`) but with its store cache off, and a
lockfile regenerated with pnpm 12.6.0. The `Dedupe lockfile` workflow
stays, with the one change pnpm 12 needs.

## Builds on #2044, supersedes #2043, #2044 and #1131

- **#2044** (pnpm 12.6.0 + `autoDedupe` + Vercel CLI bump) is the base.
Its commits are cherry-picked with `-x`. #2044's interim
`installCommand` workaround in `dev/e2e-studio/vercel.json`, which it
later removed, is left out, so this PR doesn't touch any Vercel config.
#2044 left CI on `pnpm/action-setup` + `actions/setup-node`; this PR
switches to ui's action.
- **#2043** (Renovate's plain `packageManager` bump) fails `E2E /
deploy-preview`, because `vercel@57` can't parse pnpm 12's lockfile (see
below).
- **#1131** (an early `pnpm/setup@v1` attempt) is covered by the CI
change here.
- **#2018** (lockfile regenerated from scratch) is not superseded: it
re-resolves every dependency, which is out of this PR's scope.

## Changes

| | Before | After |
| --- | --- | --- |
| `packageManager` | `pnpm@11.21.0` | `pnpm@12.6.0` (as in ui) |
| `pnpm-workspace.yaml` | | `autoDedupe: true` and
`strictPeerDependencies: false` (as in ui) |
| `.github/actions/setup` | `pnpm/action-setup@v6` +
`actions/setup-node@v7` + `pnpm install` | ui's action (`pnpm/setup@v3`,
`runtime: node@lts`) with `cache: false`, where ui has `cache: true` |
| `.github/workflows/pnpm-if-needed.yml` (`Dedupe lockfile`) | runs
`pnpm dedupe` once | kept; runs `pnpm dedupe` twice |
| `vercel` (root devDependency) | `^57.0.0` | `^60.1.3` |

- **Lockfile:** main's lockfile after #2039, plus this PR's changes as
pnpm 12.6.0 re-applied them. Those changes are the
`packageManagerDependencies` block, the `vercel@60.1.3` tree with the
optional-peer suffixes it brings, and `autoDedupe`'s dedupes: the same
`zod` and `gaxios` ones as the pending #2052, plus the peer toggle
described below. No other versions change, and `sanity@6.17.0-next.108`
stays put. #2039's refresh had already removed the three duplicates the
first revision collapsed.
- **Vercel CLI:** pnpm 12 writes the lockfile as two YAML documents.
`vercel@57`'s bundled `@vercel/build-utils` reads only the first, falls
back to npm, and fails on `workspace:*`, which is #2043's failure.
`58.9.5` is the first CLI that parses it, and every release from
`58.7.0` on ships without npm provenance (still published from GitHub
Actions OIDC). So `trustPolicy: no-downgrade` needs the exact versions
pnpm reports as `ERR_PNPM_TRUST_DOWNGRADE` in `trustPolicyExclude`:
`vercel@60.1.3` and 31 `@vercel/*` packages.
- **CI setup:** `pnpm/setup` reads `packageManager`, installs Node LTS,
runs `pnpm install`, and caches pnpm's lockfile-verification verdict
across jobs, which saves the ~28s supply-chain check. `ailf-eval.yml`
passes `runtime: node@lts` instead of `node-version: lts/*`.
`release.yml` keeps `pnpm/action-setup` + `actions/setup-node`, as in
ui.
- **Store cache off, unlike ui** (commit `bddef7a4`). With `cache:
true`, `pnpm/setup` uploads a new ~645 MiB store entry after every job,
even for an unchanged lockfile, because its keys include the run id.
That was 12 entries (7.7 GB) per push here, evicting the Playwright
browser caches the e2e jobs reuse. With it off, test shards spend 16–19s
in setup and nothing afterwards, against 19–23s plus 9–12s of upload
with it on. ui keeps `cache: true` and has the same issue. Revert
`bddef7a4` to match ui.
- **`Dedupe lockfile` workflow kept, adapted for pnpm 12** (commit
`a7e1057f`). On the current lockfile, pnpm 12.6.0's `pnpm dedupe` isn't
idempotent. Each run toggles `@vitejs/devtools` and
`oxc-transform-react` in the peer suffixes of
`plugins/@sanity/debug-preview-url-secret-plugin`'s `sanity` and
`@sanity/tsdown-config` entries, without changing any version. Run once,
as before, the workflow would open a toggle-only PR after every lockfile
change on `main`, and each automerged dedupe PR would trigger the next.
A second run undoes the toggle and keeps real dedupes. An earlier commit
in this branch deleted the workflow; `82810b5e` reverts that.
- **Docs:** AGENTS.md, CONTRIBUTING.md and README.md now require pnpm
v12. AGENTS.md and `e2e/README.md` explain the two-document lockfile and
the Vercel CLI floor; per Copilot's review of #2044, the trust-policy
wording only lists versions that pnpm reports as downgrades. AGENTS.md
also says not to commit lockfile diffs that only flip the peer toggle.

No changeset: no published package's manifest or runtime code changes,
as with the pnpm 11 bump in #1579.

## Merge with main

`main` gained one commit after this branch started: Renovate's lock file
maintenance (#2039, `cb68d65e`). It conflicted only in `pnpm-lock.yaml`.
`main` is merged in rather than rebased, so nothing was force-pushed.
The resolution takes main's lockfile and lets pnpm 12.6.0 re-apply this
PR's changes; `pnpm-workspace.yaml` needed no further changes. #2039's
25 source files merged cleanly.

## What Cody needs to do

- **Add the "🤖 bot" label.** The agent's label tool can't edit PRs in
this repo.
- **Close #2044 and #1131** when this lands. Renovate autocloses #2043.
The pending dedupe PR #2052 is redundant, since its four lines are
already in this lockfile. It merges cleanly with this PR in either
order.
- **Vercel:** nothing to set up. No project, env var or `vercel.json`
changes; only the root `vercel` CLI devDependency moves.
- **Secrets and ruleset:** no new secrets, and the required checks
(`lint`, `build`, `test`, Socket) keep their names.

## Open questions

1. **Vercel CLI trust exclusions.** This is #2044's call, which
overrides the reason #1890 was closed: 32 exact versions without
provenance in `trustPolicyExclude`. Every future `vercel` bump has to
add its own versions, so Renovate's `vercel` PRs will fail their
artifact update until someone does. The alternative is e2e previews that
can't build on pnpm 12.
2. **`strictPeerDependencies: false`** matches ui but is already pnpm's
default; ui carried it over from an old `.npmrc`. Keep it for parity, or
drop it?
3. **Store cache in ui.** ui has the same per-job uploads. Turn its
store cache off too?

## Known pnpm 12.6.0 issue

The peer toggle also shows up locally. A fresh `pnpm install`, or the
implicit install pnpm runs before scripts when `node_modules` looks
stale, flips those two suffixes in `pnpm-lock.yaml`. Frozen installs,
which CI uses, never do. The lockfile before #2039 didn't toggle; main's
refreshed resolutions exposed it, and it happens with `autoDedupe` off
too.

## Not in this PR

- vitest 5, oxfmt (#2047), oxlint (#2049) and the tsdown setup are
sibling PRs. Whichever lands later merges `main` and regenerates the
lockfile.
- ui's `packageExtensions` entry for `@testing-library/jest-dom` belongs
to its vitest 5 setup.
- ui's root `engines.node: ">=22.13"` and macOS test job are Node and
test-matrix changes, not pnpm. Plugins' tooling needs Node ≥ 22.18
anyway (AGENTS.md).
- Plugins-specific pnpm settings that ui doesn't have (`trustPolicy`,
`dedupePeers`, `dedupeDirectDeps`, `publicHoistPattern`) stay.

## Verification

- **Lockfile:** regenerated on `main` at `cb68d65e` by pnpm 12.6.0 run
directly on Node 24.21.0. A cold `pnpm install --frozen-lockfile` passes
the supply-chain check (2,615 entries), and without the Vercel
exclusions it fails with `ERR_PNPM_TRUST_DOWNGRADE`.
- **Dedupe workflow:** from the committed lockfile, from its toggled
state, and from the committed lockfile with #2052's duplicates put back,
two `CI=true pnpm dedupe --config.ignore-scripts=true` runs give,
respectively, no change, no change, and exactly #2052's dedupes. A
single run gives the toggle every time.
- **Local checks on the merged tree:** `pnpm lint`, `pnpm knip`, `pnpm
build` (52/52 tasks) and `pnpm test run` (224 files, 1,391 tests) pass.
- **CI:** everything passes on `bddef7a4`, including `deploy-preview`
(Vercel CLI 60.1.3 detects the lockfile and installs with pnpm 12.6.0)
and e2e on Chromium and Firefox. CI on `a7e1057f`, which only touches
the dedupe workflow and AGENTS.md, is running.
- **zizmor:** `zizmor .github` reports the same findings as on `main` at
every severity.

<!-- CURSOR_AGENT_PR_BODY_END -->

<div><a
href="https://cursor.com/agents/bc-0c45b56b-fd33-55f7-b22c-89bde73748e4?cursor_ref=pr_footer&cursor_cta=open_in_web"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://cursor.com/assets/images/open-in-web-dark.png"><source
media="(prefers-color-scheme: light)"
srcset="https://cursor.com/assets/images/open-in-web-light.png"><img
alt="Open in Web" width="114" height="28"
src="https://cursor.com/assets/images/open-in-web-dark.png"></picture></a>&nbsp;<a
href="https://cursor.com/background-agent?bcId=bc-0c45b56b-fd33-55f7-b22c-89bde73748e4&cursor_ref=pr_footer&cursor_cta=open_in_cursor"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://cursor.com/assets/images/open-in-cursor-dark.png"><source
media="(prefers-color-scheme: light)"
srcset="https://cursor.com/assets/images/open-in-cursor-light.png"><img
alt="Open in Cursor" width="131" height="28"
src="https://cursor.com/assets/images/open-in-cursor-dark.png"></picture></a>&nbsp;</div>

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Brings in the pnpm 12.6.0 upgrade (#2048).

- pnpm-workspace.yaml: kept both sides (this branch's oxlint catalog
  entries and #2048's pnpm settings and exclusions); git merged it
  without conflicts.
- pnpm-lock.yaml: took main's lockfile and let pnpm 12.6.0 re-apply this
  branch's catalog change. The result differs from main only by the
  oxlint and oxlint-tsgolint catalog specifiers. The re-resolution also
  flipped the known pnpm 12.6.0 peer toggle on
  plugins/@sanity/debug-preview-url-secret-plugin, which AGENTS.md says
  not to commit; a second re-resolution (`pnpm dedupe`, as the Dedupe
  lockfile workflow does) undid it.
stipsan added a commit that referenced this pull request Sep 29, 2026
… and ui's Changesets setup (#2046)

<!-- CURSOR_AGENT_PR_BODY_BEGIN -->
Moves `@sanity/themer` from sanity-io/ui (`packages/themer`) to
`plugins/@sanity/themer`, adds a React Storybook in `dev/storybook`
modeled on sanity-io/ui's `apps/storybook`, and puts the whole repo on
sanity-io/ui's Changesets setup.

## What's in this PR

1. **Changesets setup identical to sanity-io/ui (repo-wide).**
`.changeset/config.json` gains `"format": "oxfmt"` and
`"bumpVersionsWithWorkspaceProtocolOnly": true` (only the changelog
`repo` differs from ui's). `@changesets/cli` ^3.0.3 and
`@changesets/changelog-github` ^1.0.1 are root devDependencies, and
there's a `changeset` script. `release.yml` drops the npm@11 pin, since
`@changesets/cli` 3.0.3 handles npm 12's `pnpm info --json` output
(changesets/changesets#2274). The Renovate changesets workflow is ui's.
2. **Catalog aligned with sanity-io/ui for what themer needs:**
`@sanity/ui` ^4.2.7, `@sanity/icons` ^5.2.2, `motion` ^13.4.4,
`@sanity/vanilla-extract-vite-plugin` ^0.2.20. These are the ranges
`@sanity/themer@0.8.0` was published with. As AGENTS.md asks for uniform
shared-dependency updates, one combined patch changeset covers the 43
published plugins whose `dependencies` ranges change with the
`@sanity/ui`, `@sanity/icons` and `motion` bumps;
`@sanity/vanilla-extract-vite-plugin` is a devDependency everywhere, so
it doesn't reach a published range.
3. **The move.** Source copied from sanity-io/ui `main` (`b28d66c`),
which is exactly the published 0.8.0. The version and CHANGELOG carry
over, so the next release is 0.8.1. `package.json` follows the monorepo
template (plugins repository links, `build`/`prepack` scripts) with the
published exports, `sideEffects`, and dependency ranges.
`@sanity/themer-legacy` (moving to sanity-io/themer) is the published
^0.2.1. `xstate`, `@xstate/react`, and `scroll-into-view-if-needed` move
to the catalog, since other plugins now share them;
`sanity-plugin-dashboard-widget-vercel` gets its own patch changeset for
its `xstate` range (^5.32.5 → ^5.33.2). The test studio switches from
`^0.8.0` on npm to `workspace:*`; `themerTool()` was already wired in
its Home workspace.
4. **Storybook in `dev/storybook`**, next to the test studios, set up
like sanity-io/ui's `apps/storybook`: Storybook 10.6 with the a11y,
docs, links, themes and vitest addons, the React Compiler through
`@vitejs/plugin-react`, and the vanilla-extract plugin for workspace
plugins' `.css.ts` source. The preview imports `@sanity/ui/styles.css`,
since the published dist doesn't. The global decorator renders every
story in the theme `buildTheme` from `@sanity/themer` builds from a
preset picked in the toolbar, next to the light/dark toggle. It adds
`pnpm dev:storybook`, `pnpm storybook:build`, `pnpm test:browser`, ui's
`apps/storybook/vercel.json` unchanged (as `dev/storybook/vercel.json`),
a `storybook-test` CI job like ui's, and eslint-plugin-storybook's CSF
rules through oxlint's JS plugins, as in ui. Unlike ui's job,
`storybook-test` also installs Playwright's system deps when it restores
the browser cache, as this repo's e2e workflow does (Bugbot flagged that
the cache doesn't hold them). Like the other `dev/*` apps, it's skipped
by `pretest` and the CI build job.

## Layout

No existing folder is renamed or moved, and there's no new top-level
folder. `@sanity/themer` goes into the existing `plugins/@sanity/`
folder next to the other `@sanity/*` plugins, not ui's
`packages/themer`. The Storybook goes into the existing `dev/` folder as
`dev/storybook`, next to `dev/test-studio` and `dev/e2e-studio`, so the
`dev/*` workspace glob and the `./dev/*` build filters already cover it.
The root only gains workspace-level config.

## How faithful the move is

- **Build output vs. the npm tarball of 0.8.0:** JavaScript is identical
apart from chunk hashes, and `bundle.css` is byte-identical. The `.d.ts`
files have the same API; tsdown 0.22 writes `declare …; export {…}`
where 0.23 wrote `export declare …`.
- **Source changes:** `type` modifiers on type-only imports (plugins'
`strictest` tsconfig has `verbatimModuleSyntax`), one inline
`oxlint-disable-next-line react/exhaustive-effect-dependencies` with its
reason (the rule is on here and off in ui; the `[palette]` dependency is
deliberate), and the README license link. The tsconfig is `strictest`
without `noUncheckedIndexedAccess`, which ui never enabled
(`sanity-plugin-graph-view` and `plugin-kit` do the same).
- **No React Compiler typings stub:** ui's
`typings/babel-plugin-react-compiler.d.ts` isn't carried over. Here the
real `babel-plugin-react-compiler` types resolve (it's a linked optional
peer of `@sanity/tsdown-config`), so `defineConfig()` picks the
single-config overload, and a wrong `reactCompiler` value is still
flagged (checked with a bad `target`, a misspelled `transform`, a number
`target` and an unknown key).
- **Tests:** themer's Vitest config sets `testTimeout: 30_000`. PR CI
here runs every project with coverage, and the whole-theme `buildTheme`
tests took 8s there against the 5s default (ui runs them without
coverage).
- **Versions:** every themer runtime dependency and the whole Storybook
stack resolve to the same versions as in ui's lockfile.

## What Cody needs to do

- **npm trusted publishing:** publishing runs from
`.github/workflows/release.yml`, which this repo already had; it now
uses ui's version/publish flow. The trusted publisher for
`@sanity/themer` must be repository `sanity-io/plugins`, workflow
`release.yml`, no environment.
- **Vercel:** the Storybook carries ui's `apps/storybook/vercel.json`
unchanged, as `dev/storybook/vercel.json` (`pnpm build` into
`storybook-static`). To deploy it like ui's `sanity-ui-storybook`,
create a project with root directory `dev/storybook`; the file supplies
the build command and output directory. No other Vercel config is
touched: `dev/e2e-studio/vercel.json` and the `plugins-studio` and
`plugins-e2e-test-studio` projects stay as they are, and both previews
built fine on this PR.
- **Secrets and env vars:** none new. The touched workflows use names
this repo already has: `ECOSPARK_APP_ID`, `ECOSPARK_APP_PRIVATE_KEY`,
`vars.ECOSPARK_CLIENT_ID`, `TURBO_TOKEN` and `vars.TURBO_TEAM`.
`storybook-test` uses none.
- **Ruleset:** no swap needed. The `main` ruleset requires `lint`,
`build`, `test` and the two Socket Security checks, and those job names
are unchanged. Optionally, to match ui (which requires `build`, `knip`,
`Test node@lts / ubuntu-latest`, `storybook-test` and its two Vercel
checks), add `storybook-test` and `knip`, and the Storybook's Vercel
check once that project exists.
- **CODEOWNERS:** untouched, per the transfer convention. Add an owner
for `plugins/@sanity/themer` if you want one.

## Open questions

1. **Patch, not major.** The transfer skill defaults to a major, but
themer is 0.x, nothing breaks, the next release should continue from the
published version, and themer's own deprecation notice reserves 1.0 for
removing `@sanity/themer/legacy`. So the changeset is a patch (0.8.1).
The color move made the same call.
2. **Stories.** sanity-io/ui had no themer stories. I ported the ui
stories that exercise what themer builds (theme
Debug/Build/Canvas/Color, and the color palette story on
`buildPalette`'s output) and added stories for the tool's theme
thumbnails and navbar color wheel. Those two aren't public API, so their
stories import them from `plugins/@sanity/themer/src` by relative path.
Keep them, or public API only?
3. **`sanity` peer range.** I kept 0.8.0's optional `^3 || ^4 || ^5 ||
^6` instead of the monorepo's `catalog:peer` (`^5 || ^6.0.0-0`), which
would drop Studio v3/v4. As with 0.8.0 from npm, it doesn't match the
`sanity@next` prereleases here, so the test studio shows a peer warning.
4. **`bumpVersionsWithWorkspaceProtocolOnly`.** With ui's setting,
Changesets no longer rewrites `@sanity/sfcc`'s intentionally wide
`sanity-plugin-internationalized-array` peer (`^4.0.0 || ^5.0.0`), the
only internal dependency that doesn't use `workspace:`. That seems
right, but it's a behavior change.

## Remaining alignment with sanity-io/ui (not in this PR)

This PR stays scoped to the themer move. Matching ui's tooling happens
in five separate PRs, one per tool, each owned by another worker. All
five exist:

- **pnpm** 11.21.0 → 12.6.0 with ui's pnpm setup, including the
`pnpm/setup@v3` CI setup action: #2048 (merged; this branch is merged
with it)
- **oxfmt** ^0.63 → ^0.71 with ui's config: #2047. Dropping the
`@sanity/plugin-kit/oxfmt` preset's Prettier-era `.changeset/*.md`
`singleQuote: false` override, which ui did when it enabled Changesets'
`format: "oxfmt"`, belongs there too; #2047 doesn't include it yet.
- **vitest** ^4.1 → ^5.0, with `@vitest/coverage-v8`: #2050. If it
merges first, the rebase of this PR raises the Storybook's
`@vitest/browser-playwright` to `^5.0.2` and re-resolves
`@storybook/addon-vitest` in the lockfile, since otherwise two stale
vitest 4 packages stay behind. #2050's worker checked that the Storybook
tests pass on vitest 5 with those two changes (`@storybook/addon-vitest`
10.6 officially peers on vitest 3/4; ui runs it on 5 with loose peers).
- **oxlint** ^1.83 → ^1.86 and **oxlint-tsgolint** ^7.0.2001 → ^7.0.2003
with ui's config: #2049. It turns `react/exhaustive-effect-dependencies`
off, as ui does, so if it merges first, the rebase of this PR drops its
one suppression for that rule in `ImagePaletteSection.tsx`, which would
otherwise be an unused-directive error.
- **tsdown** ^0.22 → ^0.23 with **@sanity/tsdown-config** ^0.27 → ^0.28,
which requires tsdown 0.23: #2051

All five also touch files this PR changes (`pnpm-workspace.yaml`,
`pnpm-lock.yaml` and `AGENTS.md`; `package.json` and `README.md` for
#2047 and #2048; `oxlint.config.ts` for #2049;
`.github/workflows/ci.yml` and `.gitignore` for #2050), so whichever PR
merges later rebases and regenerates the lockfile.

Smaller differences left to Renovate or later PRs: `oxc-transform-react`
^0.147 → ^0.151 (#1988), `@types/node` ^24.13 → ^24.19, `jsdom` ^29 →
^30, `knip` ^6.33 → ^6.38, ui's macOS test run, and ui's root
`engines.node: ">=22.13"`.

## Verification

- `pnpm format`, `pnpm knip`, `pnpm lint`, `pnpm build` (54 tasks,
including both studios and the Storybook), `pnpm test run` (235 files,
1,509 tests) and `pnpm test:browser` (7 stories) all pass locally,
starting from `pnpm install --frozen-lockfile`.
- A `changeset version` dry run in a scratch worktree, with ui's config
and `GITHUB_TOKEN`, bumps themer 0.8.0 → 0.8.1 and
dashboard-widget-vercel 4.0.28 → 4.0.29 (before the combined changeset;
`changeset status` now plans 46 patch releases: the 43 plugins, themer,
and `sanity-plugin-studio-smartling` and `sanity-plugin-transifex`,
which follow `sanity-translations-tab` as internal dependents), credits
@stipsan through the `author:` line, and writes CHANGELOGs that pass
`oxfmt --check`.
- `zizmor .github --min-severity high` reports no findings.

## Transfer verification

- [x] `package.json` dependencies, peerDependencies and exports verified
against the published 0.8.0
- [x] `LICENSE` not added: ui's credits only Sanity.io; the README links
this repo's LICENSE
- [x] Test studio uses the workspace package (`themerTool()` was already
wired)
- [x] `pnpm format`, `pnpm knip`, `pnpm lint`, `pnpm build`, `pnpm test
run` pass
- [x] Changeset added (patch; see open question 1)
- [ ] Trusted publishing for `@sanity/themer` configured with
`sanity-io/plugins` and `release.yml`

## Maintainer follow-up

- [ ] Remove `packages/themer` from sanity-io/ui (with its entries in
ui's root `build`/`test` scripts and `pkg-pr-new.yml`) once this lands,
on your go-ahead
- [ ] Point ui's docs and AGENTS.md mentions of `@sanity/themer` to
`plugins/@sanity/themer`
- [ ] Transfer open themer issues from sanity-io/ui and label them
`themer`

## Screenshots

The theme cards of every preset, in the dark scheme:

[Storybook: themer tool theme thumbnails for every
preset](https://cursor.com/agents/bc-7516e1ad-c642-5cd2-97f4-35288fbda30a/artifacts?path=%2Fopt%2Fcursor%2Fartifacts%2Fstorybook_themer_theme_thumbnails_presets.png)

The palette `buildPalette` generates for the Verdant preset, light
scheme (its backgrounds replace black and white, its accent the blue
scale, its text color the gray scale):

[Storybook: generated Verdant palette with contrast
badges](https://cursor.com/agents/bc-7516e1ad-c642-5cd2-97f4-35288fbda30a/artifacts?path=%2Fopt%2Fcursor%2Fartifacts%2Fstorybook_themer_palette_verdant_light.png)

The Debug theme story in the Pink Synth preset, both schemes:

[Storybook: Debug theme story in the Pink Synth
preset](https://cursor.com/agents/bc-7516e1ad-c642-5cd2-97f4-35288fbda30a/artifacts?path=%2Fopt%2Fcursor%2Fartifacts%2Fstorybook_themer_debug_pink_synth.png)


<sub>To show artifacts inline, <a
href="https://cursor.com/dashboard/cloud-agents#team-pull-requests">enable</a>
in settings.</sub>
<!-- CURSOR_AGENT_PR_BODY_END -->

<div><a
href="https://cursor.com/agents/bc-7516e1ad-c642-5cd2-97f4-35288fbda30a?cursor_ref=pr_footer&cursor_cta=open_in_web"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://cursor.com/assets/images/open-in-web-dark.png"><source
media="(prefers-color-scheme: light)"
srcset="https://cursor.com/assets/images/open-in-web-light.png"><img
alt="Open in Web" width="114" height="28"
src="https://cursor.com/assets/images/open-in-web-dark.png"></picture></a>&nbsp;<a
href="https://cursor.com/background-agent?bcId=bc-7516e1ad-c642-5cd2-97f4-35288fbda30a&cursor_ref=pr_footer&cursor_cta=open_in_cursor"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://cursor.com/assets/images/open-in-cursor-dark.png"><source
media="(prefers-color-scheme: light)"
srcset="https://cursor.com/assets/images/open-in-cursor-light.png"><img
alt="Open in Cursor" width="131" height="28"
src="https://cursor.com/assets/images/open-in-cursor-dark.png"></picture></a>&nbsp;</div>

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Brings in the themer move (#2046). Git merged every file without
conflicts, including this branch's oxlint.config.ts rules next to
#2046's Storybook lint wiring.

pnpm-lock.yaml: took main's lockfile and let pnpm 12.6.0 re-apply this
branch's catalog change, so it differs from main only by the oxlint and
oxlint-tsgolint catalog specifiers. The re-resolution flipped the known
pnpm 12.6.0 peer toggle on plugins/@sanity/debug-preview-url-secret-plugin
again; a second re-resolution (`pnpm dedupe`) undid it.
#2046 added an `oxlint-disable-next-line react/exhaustive-effect-dependencies`
in ImagePaletteSection.tsx because the rule was on at the time. With the
rule off, the directive is reported as unused. Removing it also makes the
file match its last sanity-io/ui version apart from `type` imports.
stipsan added a commit that referenced this pull request Sep 29, 2026
…ommit hook on lefthook (#2047)

<!-- CURSOR_AGENT_PR_BODY_BEGIN -->
Brings oxfmt in line with sanity-io/ui's `main`: the same version, the
same config, and the same scripts and CI wiring. The pre-commit hook
moves from husky and lint-staged to lefthook, set up like
sanity-io/sanity's. Supersedes #1919.

## Commits

1. **`chore(deps): update oxfmt to ^0.71.0`**: the catalog goes from
^0.63.0 to ui's ^0.71.0, with ui's oxfmt resolution: oxfmt 0.71.0 and
`tinypool` 2.2.0, the same versions and integrity hashes as ui's
lockfile. There's a patch changeset for `@sanity/plugin-kit`, whose
optional `oxfmt` peer uses the catalog.
2. **`chore(format): align the oxfmt config with sanity-io/ui`**: root
`oxfmt.config.ts` now ignores `storybook-static/**` and `CHANGELOG.md`,
like ui's `.oxfmtrc.json`. `.vscode/settings.json` points
`oxc.fmt.configPath` at `oxfmt.config.ts` instead of the `.oxfmtrc.json`
that #1565 deleted. AGENTS.md records the parity with ui and the
exceptions.
3. **`chore: drop the husky and lint-staged pre-commit hook, like
sanity-io/ui`**: superseded by commit 6, which brings the hook back on
lefthook.
4. **`chore(format): drop the oxfmt preset's .changeset/*.md override,
like sanity-io/ui`**: the `@sanity/plugin-kit/oxfmt` preset loses its
Prettier-era `singleQuote: false` override for `.changeset/*.md`, as
ui's `.oxfmtrc.json` did when ui switched Changesets to oxfmt formatting
(sanity-io/ui#2957). It gets a patch changeset for `@sanity/plugin-kit`,
since the preset is published. This part moved here from #2046 because
it's formatter config. AGENTS.md now explains how changesets get quoted.
5. **`chore(format): run pnpm format`**: requotes this PR's own
changeset from commit 1. Everything else already passed under oxfmt
0.71.
6. **`chore: run oxfmt on staged files with lefthook, like
sanity-io/sanity`**: see below.

## Pre-commit hook: husky and lint-staged → lefthook

This mirrors how sanity-io/sanity replaced husky and lint-staged
(sanity-io/sanity#12755, then sanity-io/sanity#12767 and
sanity-io/sanity#12953):

- **`lefthook` ^2.1.14** as a root devDependency, sanity's range. It
resolves to 2.1.14, as in sanity's lockfile.
- **`allowBuilds: lefthook: true`** in `pnpm-workspace.yaml`, as in
sanity. Lefthook's own install script runs `lefthook install` during
`pnpm install`, and it skips when `CI` is set, which covers GitHub
Actions and Vercel. There's no root `postinstall` script: sanity dropped
its `lefthook install --reset-hooks-path` postinstall in #12767 for this
package build.
- **`lefthook.yml`** with sanity's layout: a parallel `pre-commit` with
two jobs that run oxfmt on staged files with `stage_fixed: true`. The
jobs are `format-scripts` (`*.{js,jsx,ts,tsx,mjs,cjs,mts,cts}`) and
`format-other` (`*.{md,yml,yaml,css,json,jsonc}`).
- `mts`, `cts` and `jsonc` are additions to sanity's globs. They cover
this repo's `.mts`, `turbo.jsonc` and `knip.jsonc` files, which the old
lint-staged glob partly covered.
- sanity's `oxlint --fix` line is left to the oxlint alignment (#2049).
- **Direct binary calls:** the jobs call `node_modules/.bin/oxfmt`
instead of sanity's `pnpm oxfmt`. Running pnpm in a hook triggers pnpm
12's implicit install, which flips the lockfile's peer toggle (see
AGENTS.md). sanity switched to `pnpm oxfmt` in #12953 because lefthook
doesn't put `node_modules/.bin` on `PATH`; the path solves that too.
- **Lockfile:** `pnpm-lock.yaml` gains only lefthook 2.1.14 and its
platform packages (100 lines). A plain install would also have bumped
the Studio's `next` overrides from `6.17.0-next.108` to
`6.17.0-next.112`, so I resolved it through a local registry proxy that
served the locked versions as `next`, as AGENTS.md describes. The
lockfile has no `localhost` or `tarball:` entries.
- **Docs:** AGENTS.md documents the hook, `LEFTHOOK=0`, the one-time
reset for clones that ran husky, and `pnpm
--config.verify-deps-before-run=false <script>` for local runs. README's
note that formatting also runs in a pre-commit hook is back.

## Merges with `main`

`main` is merged in, not rebased, so nothing was force-pushed. After
each merge, `pnpm-lock.yaml` is `main`'s lockfile regenerated with pnpm
12.6.0, invoked directly so `packageManagerDependencies` stays, minus
the peer toggle that AGENTS.md describes.

- **#2039 (lock file maintenance):** only the lockfile conflicted.
- **#2048 (pnpm 12.6.0):** `package.json` conflicted next to the removed
`lint-staged` block. The resolution keeps both sides: `main`'s
`packageManager: pnpm@12.6.0` and `vercel` ^60.1.3, without the block. A
follow-up commit restores `main`'s side of the peer toggle, which the
regeneration had flipped.
- **#2046 (themer move):** only the lockfile conflicted. `pnpm format`
then requoted #2046's three changesets in a separate commit:
`dashboard-widget-vercel-xstate`, `shared-deps-sanity-ui-icons-motion`
and `themer-plugins-monorepo`, 45 frontmatter lines, quote-only. The new
`plugins/@sanity/themer` package and `dev/storybook` already passed.
- **#2053 (Version Packages):** it released and deleted those three
changesets, so the modify/delete conflicts resolve to `main`'s
deletions. It didn't change the lockfile, and `pnpm format` changes
nothing after the merge.

`pnpm-lock.yaml` differs from `main` only by this PR's own changes:

- oxfmt and its bindings go from 0.63.0 to 0.71.0, and `tinypool` from
2.1.0 to 2.2.0.
- husky, lint-staged and their four exclusive dependencies are removed.
- lefthook 2.1.14 and its platform packages are added.

No other version moves, and the Studio stays on `6.17.0-next.108`.

## How it compares with ui

- **Options:** identical, overrides included (`printWidth`, `semi`,
`singleQuote`, `bracketSpacing`, `quoteProps`, `sortImports`,
`sortPackageJson`). They come from the `@sanity/plugin-kit/oxfmt`
preset.
- **Ignore patterns:** `dist/**`, `storybook-static/**`,
`pnpm-lock.yaml` and `CHANGELOG.md` in both. Two differences remain:
  - `turbo/**/*.hbs` is plugins-only (the turbo generator templates).
- ui's `**/sanity.types.ts` is left out. In ui it was for
`apps/docs/sanity.types.ts` and has matched nothing since the docs app
went static. Here, `pnpm typegen` formats
`dev/test-studio/sanity.types.ts` on purpose: the test studio runs
typegen with `formatGeneratedCode: false` and then `pnpm format`.
Ignoring the file would make the next TypeGen PR unformat it.
- **Config file:** plugins keeps `oxfmt.config.ts` re-exporting the
published preset instead of ui's `.oxfmtrc.json`. #1565 made that switch
so the preset is exactly what the monorepo formats with, and
plugin-kit's `verify-package` expects plugins to use it. oxfmt, the Oxc
extension and Changesets' formatter detection all read
`oxfmt.config.ts`. See the open question.
- **Scripts and CI:** the `format` script is identical to ui's (`oxfmt .
--no-error-on-unmatched-pattern`). Neither repo checks formatting on
PRs, and both run the same shared `format-if-needed.yml` on `main`, so
no workflow changes are needed. `oxfmt .` also formats
`.github/workflows/*.yml`, which that workflow can't push, and 0.71
changes none of them. ui has no pre-commit hook; this repo keeps one, on
lefthook.
- **VS Code:** ui points `oxc.fmt.configPath` at its config file, but
plugins pointed it at a file that doesn't exist. I drove `oxfmt --lsp`
directly: with the old setting, the editor silently formats with oxfmt's
defaults (double quotes, semicolons, bracket spacing, unsorted imports),
and with `oxfmt.config.ts` it matches `pnpm format`.

## Changesets

- **Changesets formats what it writes with oxfmt.**
`.changeset/config.json` sets `"format": "oxfmt"` (from #2046). I called
`@changesets/write` (what `changeset add` uses) directly. With the
override it writes `"@sanity/plugin-kit": patch`; without it,
`'@sanity/plugin-kit': patch`, the same as `pnpm format`.
- **`changeset version` is unaffected.** It only hands `CHANGELOG.md`
files to the formatter. This PR ignores those, and oxfmt skips them and
exits 0 thanks to `--no-error-on-unmatched-pattern`.
- **`changeset status` parses the single-quoted frontmatter.** It lists
this PR's two changesets for `@sanity/plugin-kit` 10.0.11.

## What to expect after merging

- **Renovate changesets get a follow-up format PR, as in ui.** The
shared Renovate changesets workflow uses
`mscharley/dependency-changesets-action`, which always writes double
quotes and has no formatting option. After a Renovate PR merges, the
Auto format workflow opens (or updates) a `chore(format): 🤖 ✨` PR that
requotes the changeset. ui has had one after most Renovate merges since
sanity-io/ui#2957: sanity-io/ui#2971, sanity-io/ui#2980,
sanity-io/ui#2986, sanity-io/ui#2998, sanity-io/ui#3002,
sanity-io/ui#3013 and sanity-io/ui#3024.
- **Clones that ran husky need a one-time reset.** They still have
`core.hooksPath=.husky/_`. There, `lefthook install` refuses to install
and prints how to fix it, and `pnpm install` continues. Until the reset,
husky's leftover runner does nothing, so commits simply aren't
formatted. `git config --unset core.hooksPath && pnpm install` fixes it.

## Related PRs

- **#1919** (Renovate, oxfmt ^0.71.0): superseded. This PR has the same
bump, with the lockfile regenerated on current `main`, plus the config
alignment. Close it if Renovate doesn't once this merges.
- **#2046** (themer move) and **#2048** (pnpm 12): merged, and merged
into this PR.
- **#2049** (oxlint) conflicts with this PR in `pnpm-workspace.yaml`,
where its `oxlint` catalog lines sit next to `oxfmt`. Keep both sides.
It could also add sanity's `oxlint --fix` line to the `format-scripts`
job, calling `node_modules/.bin/oxlint` directly.
- **#2050** (vitest 5) conflicts only in `pnpm-lock.yaml`.
- **#2051** (tsdown) doesn't conflict.
- Whichever PR lands later merges `main` and regenerates the lockfile
without committing the peer toggle.

## What Cody needs to do

- Add the `🤖 bot` label: the agent's label tool couldn't edit PRs in
this repository.
- Triage Socket's alert. It flags `lefthook@2.1.14`'s install script
(`node postinstall.js`) as a low-priority warning. That script is what
installs the hook: `allowBuilds: lefthook: true` enables it, as in
sanity-io/sanity, and it skips when `CI` is set. Mark it as acceptable
risk if you agree. Socket's required checks pass either way.
- After merging, tell contributors to run `git config --unset
core.hooksPath && pnpm install` once, so lefthook replaces husky's hook.
- Merge the Renovate follow-up format PRs as they come, as in ui.
- No Vercel, secrets or ruleset changes, and no new secrets or env vars.

## Open question

**`.oxfmtrc.json`:** should plugins switch to ui's JSON config file,
too? That would mean no longer re-exporting the plugin-kit preset, and
it would reverse #1565. I kept `oxfmt.config.ts`, since the formatting
result is already the same as ui's.

## Verification

- **The hook**, tested on throwaway commits, with `pnpm-lock.yaml`
untouched each time:
  - Staged TS and JSON files get formatted and re-staged.
- A tracked `.vscode/settings.json` (its folder is gitignored) is
re-staged without the `[FAILED]` that lint-staged printed.
  - A staged `CHANGELOG.md`, which oxfmt ignores, passes as-is.
- In a partially staged file, only the staged hunk is formatted and the
unstaged hunk is kept.
- The commit that adds the hook ran it on its own files in 0.4 seconds.
- **Clones that ran husky:** with `core.hooksPath=.husky/_`, `lefthook
install` exits 1 with instructions and installs nothing, as described
above.
- **Local checks** at the head commit, with pnpm 12.6.0 on Node 24.21
and `CI=true` (lefthook's install script skips):
- `pnpm install --frozen-lockfile`, which leaves `pnpm-lock.yaml`
untouched
  - `oxfmt --check` (2,018 files)
  - `oxlint` and `knip`
  - the full turbo build (54 tasks)
  - `vitest run` (235 files, 1,509 tests)
  - `pnpm test:browser` (4 files, 7 stories)
- **CI** is fully green at the head commit (`123af257`): build, lint,
knip, all four test shards, coverage, `storybook-test`, both e2e
browsers, Bugbot, and both Vercel checks, whose installs run lefthook's
install script. The branch is up to date with `main`.
<!-- CURSOR_AGENT_PR_BODY_END -->

<div><a
href="https://cursor.com/agents/bc-0b8c4f47-d9af-5929-8254-d25847e7e2dd?cursor_ref=pr_footer&cursor_cta=open_in_web"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://cursor.com/assets/images/open-in-web-dark.png"><source
media="(prefers-color-scheme: light)"
srcset="https://cursor.com/assets/images/open-in-web-light.png"><img
alt="Open in Web" width="114" height="28"
src="https://cursor.com/assets/images/open-in-web-dark.png"></picture></a>&nbsp;<a
href="https://cursor.com/background-agent?bcId=bc-0b8c4f47-d9af-5929-8254-d25847e7e2dd&cursor_ref=pr_footer&cursor_cta=open_in_cursor"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://cursor.com/assets/images/open-in-cursor-dark.png"><source
media="(prefers-color-scheme: light)"
srcset="https://cursor.com/assets/images/open-in-cursor-light.png"><img
alt="Open in Cursor" width="131" height="28"
src="https://cursor.com/assets/images/open-in-cursor-dark.png"></picture></a>&nbsp;</div>

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Brings in the oxfmt 0.71 alignment with the lefthook pre-commit hook
(#2047) and the Version Packages release (#2053).

- pnpm-workspace.yaml: kept both sides of the adjacent catalog lines
  (main's oxfmt ^0.71.0, this branch's oxlint ^1.86.0 and
  oxlint-tsgolint ^7.0.2003).
- pnpm-lock.yaml: took main's lockfile and let pnpm 12.6.0 re-apply this
  branch's catalog change, so it differs from main only by the oxlint
  and oxlint-tsgolint catalog specifiers. The `next` dist-tag has moved
  to 6.17.0-next.112 since main's lockfile was written, so a plain
  re-resolution would have upgraded the Studio; as AGENTS.md describes,
  it ran through a local registry proxy serving the locked
  6.17.0-next.108 as `next`, and a second re-resolution undid the known
  peer toggle on plugins/@sanity/debug-preview-url-secret-plugin.
#2047 dropped the plugin-kit oxfmt preset's .changeset/*.md override, so changeset frontmatter now uses single quotes like every other file.
…e sanity-io/sanity

Mirror sanity-io/sanity's `format-and-lint-scripts` job: after oxfmt,
run `oxlint --disable-nested-config --fix --quiet` on the staged scripts
and re-stage the fixes. Like the oxfmt jobs, it calls
node_modules/.bin/oxlint directly so pnpm 12's implicit install, which
flips the lockfile's peer toggle, never runs.

This branch was successfully deployed

1 active deployment
Preview – plugins-studio — ac077b16 Deployed Sep 29, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants