# Agent Guide for Sanity Plugins Monorepo This guide is for AI agents working on this codebase. Follow these instructions to ensure successful contributions. > **Self-Improvement:** If you discover undocumented requirements, commands, or workflows during your work (e.g., a reviewer asks you to run something not covered here), update this file on the same PR. Keep this guide accurate and helpful for future agents. ## Quick Reference | Task | Command | | -------------------- | -------------------- | | Install dependencies | `pnpm install` | | Format code | `pnpm format` | | Run linters | `pnpm lint` | | Build all packages | `pnpm build` | | Run tests | `pnpm test` | | Add changeset | `pnpm changeset add` | | Start dev server | `pnpm dev` | ## Environment Setup ### Node.js Version Use the **latest LTS** release of Node.js. ### pnpm Version The exact pnpm version is managed via the `packageManager` field in root `package.json`. You only need pnpm **v10 or later** installed globally—corepack or pnpm itself will auto-install the exact version specified. ```bash # Enable corepack to automatically use the correct pnpm version corepack enable # Install all dependencies pnpm install ``` ## Before Submitting a PR Run these commands in order. **All must pass** or CI will fail: ```bash # 1. Format code (oxfmt) pnpm format # 2. Run linters (oxlint) pnpm lint # 3. Build all packages pnpm build # 4. Run tests pnpm test ``` ### Required: Add Changesets Every PR that changes published packages **must** include changesets. **Important:** Create a separate changeset file for each plugin you modify. #### Why Separate Changesets? Each plugin has its own changelog that consumers read. A combined changeset would pollute individual plugin changelogs with irrelevant information. For example, `@sanity/code-input`'s changelog should not mention workflow-specific changes. #### How to Add Changesets **Option 1: Manual Creation (Recommended for Multiple Plugins)** Create individual `.md` files in `.changeset/` directory: ```markdown --- 'plugin-name': patch --- Brief description of the change specific to this plugin ``` **Example:** If you modified 3 plugins, create 3 files: ```bash # .changeset/code-input-fix.md --- "@sanity/code-input": patch --- Fix input border styling with v2 theme API # .changeset/workflow-hooks.md --- "sanity-plugin-workflow": patch --- Replace deprecated useTimeAgo hook with useRelativeTime # .changeset/markdown-theme.md --- "sanity-plugin-markdown": patch --- Migrate to v2 theme API for color properties ``` **Option 2: Interactive CLI (For Single Plugin)** ```bash pnpm changeset add ``` Follow the prompts to: 1. Select **one** package that changed 2. Choose the version bump type (patch/minor/major) 3. Write a summary of changes **specific to that package** 4. Repeat for each modified package #### Changeset Guidelines - **Scope**: Each changeset should only mention changes relevant to that specific plugin - **Clarity**: Write from the consumer's perspective—what do they need to know? - **Version bumps**: - `patch`: Bug fixes, dependency updates, internal refactors - `minor`: New features, non-breaking API additions - `major`: Breaking changes - **Format**: Use clear, concise language without technical jargon when possible #### Bad vs Good Examples ❌ **Bad** (combined changeset): ```markdown --- '@sanity/code-input': patch 'sanity-plugin-workflow': patch --- - Migrated to v2 theme APIs - Replaced useTimeAgo with useRelativeTime - Fixed input styling ``` _Problem: Workflow's changelog will say "Fixed input styling" which is irrelevant_ ✅ **Good** (separate changesets): ```markdown # .changeset/code-input-theme.md --- ## "@sanity/code-input": patch Migrate to v2 theme API for input styling # .changeset/workflow-hooks.md --- ## "sanity-plugin-workflow": patch Replace deprecated useTimeAgo hook with useRelativeTime ``` _Each plugin's changelog only shows relevant changes_ #### Crediting Original Authors (Ported/Migrated PRs) When you port or migrate someone else's PR (for example, bringing a community PR from a legacy single-plugin repo into this monorepo), make sure the **original author gets the credit** in the changelog — not the bot or whoever opened the port PR. Two things are required: 1. **Author the commits as the original author** so Git authorship is preserved: ```bash git commit --author="their-name " -m "..." ``` 2. **Add `author:` directives to the changeset summary.** Changelogs are generated by `@changesets/changelog-github`, which otherwise credits the PR author — the `🤖 bot` for a ported PR. An `author:` (or `user:`) line on its own line overrides this and is stripped from the rendered text. Add one line per contributor to thank **everyone** who worked on the plugin, not just the latest author ([multiple authors are supported](https://github.com/changesets/changesets/blob/c2db1dd5d2da6c6eb514d86bbe05cbb7227b067f/packages/changelog-github/src/index.test.ts#L229-L243)). Use GitHub usernames and always include the leading `@`: ```markdown --- '@sanity/table': minor --- author: @stipsan author: @rexxars Port @sanity/table to the Sanity plugins monorepo ``` ## CI Checks The CI pipeline runs on every PR: | Job | What it checks | | --------- | ------------------------------------------------------ | | **build** | `pnpm build` - All packages compile successfully | | **lint** | `pnpm lint --format github` - Code passes oxlint | | **test** | `pnpm test` - All tests pass (runs after build + lint) | ### Lint Specifics - **oxlint**: Type-aware linting with `--deny-warnings` (warnings are errors). React Compiler rules run via the react-hooks-js plugin. - **TypeScript type checking** is included in `pnpm lint` via oxlint — no separate `tsc` needed - Run `pnpm lint:fix` to auto-fix issues when possible ## Testing The monorepo uses [Vitest v4](https://vitest.dev) for testing. ### Running Tests ```bash # Run all tests (non-watch mode) pnpm test run # Run tests in watch mode (default vitest behavior) pnpm test # Update snapshots pnpm test -u ``` ### Writing Tests Tests are co-located with source code in the `src/` directory: - Test files use `.test.ts` or `.spec.ts` extensions - Each plugin has a minimal `vitest.config.ts` that inlines `vitest-package-exports` - All plugins include a package exports test using `vitest-package-exports` to verify all exports are valid When creating a new plugin with `pnpm generate`, test files are automatically created. The root `vitest.config.ts` uses glob patterns to automatically discover all plugins, so new plugins are included without manual configuration. Example test file (`src/index.test.ts`): ```ts import {fileURLToPath} from 'node:url' import {expect, test} from 'vitest' import {getPackageExportsManifest} from 'vitest-package-exports' test('package exports', {timeout: 30_000}, async () => { const manifest = await getPackageExportsManifest({ importMode: 'dist', cwd: fileURLToPath(import.meta.url), }) expect(manifest.exports).toMatchInlineSnapshot() }) ``` #### Test Timeouts Always use the options object syntax for timeouts, placing it as the second argument: ```ts // Correct - options object as second argument test('my test', {timeout: 30_000}, async () => { // ... test code }) // Wrong - timeout as third argument (deprecated style) test('my test', async () => { // ... }, 30000) ``` Use numeric separators (`30_000` instead of `30000`) for readability. ### Test Configuration - Root `vitest.config.ts` uses glob patterns `['plugins/@sanity/*', 'plugins/sanity-plugin-*']` to automatically find all plugin projects - Individual plugins have minimal `vitest.config.ts` starting with just inline deps configuration for vitest-package-exports - Plugins can expand their vitest configs and test suites over time as needed (unit tests, integration tests, etc.) - Tests run against built `dist/` output after `pnpm build` - Snapshots are generated with `pnpm test -u` ## Pull Request Workflow ### 1. Create as Draft PR First Always open PRs as **draft** initially. The prompter (person who requested the work) reviews first before marking ready for team review. ### 2. Apply the "🤖 bot" Label **All PRs created by AI agents must be labeled with "🤖 bot".** This label helps the team identify bot-created PRs and ensures proper tracking and review workflows. When creating or updating a PR, always ensure the `🤖 bot` label is applied. ### 3. Move Out of Draft Once the prompter approves, convert from draft to ready-for-review so the team can review. ### 4. Merge Process After approval, PRs merge to `main`. The release workflow automatically: 1. Creates a "Version Packages" PR that bumps versions 2. When that PR merges, packages publish to npm with provenance ## Development Server ### Starting the Test Studio ```bash pnpm dev ``` This starts the Sanity Studio at `http://localhost:3333`. ### ⚠️ Authentication Required The test studio is a real Sanity Studio that connects to Sanity APIs. You must: 1. Have a Sanity account 2. **Log in via the browser** when prompted 3. Have access to the configured project (`ppsg7ml5` by default) Simply accessing `http://localhost:3333` is not enough—the studio requires browser-based Sanity authentication to function. ### Inspecting Production Builds with Vite DevTools The test studio can run with [Vite DevTools](https://devtools.vite.dev) enabled, which lets you inspect the output of `sanity build` runs (module graph, chunks, plugin timings, bundle treemaps, session diffing) from inside a long-running `sanity dev` server—no restart needed. ```bash # Builds the test studio with devtools enabled, then starts the dev server # (so there's a build session to inspect right away) pnpm devtools:test-studio ``` Open `http://localhost:3333` and use the Vite DevTools dock to explore the recorded Rolldown build session, or open the full-page DevTools UI directly at `http://localhost:3333/__devtools/`. See the [DevTools for Rolldown features guide](https://devtools.vite.dev/rolldown/features.html) for how to use the module graph, chunk, asset, and plugin panels. The first time a browser connects, the dock asks for a one-time permission grant (a prompt in the `sanity dev` terminal, or a manual auth URL printed there). Trusted clients are remembered in `~/.vite/devtools/auth.json`. To inspect a **new** build after making changes—while `pnpm devtools:test-studio` is still running—run in a second terminal: ```bash # Creates a fresh build session that shows up in the running DevTools dock pnpm devtools:test-studio:build ``` Builds are not hooked into HMR; `sanity build` must be invoked manually (via the command above) each time you want a new session to inspect. Sessions can be compared against each other in the DevTools UI to diff bundle changes. How it works: - Both commands set `ENABLE_VITE_DEVTOOLS=true`, which makes `dev/test-studio/sanity.cli.ts` add the `DevTools()` Vite plugin and enable `build.rolldownOptions.devtools` - Build sessions are written to `dev/test-studio/node_modules/.rolldown` (gitignored) - The flag is declared in `dev/test-studio/turbo.jsonc` so turbo-cached builds are invalidated when it changes (and so turbo's strict env mode passes it through to `pnpm dev`) - Enabling devtools makes `sanity build` noticeably slower; that's why it's opt-in via the env flag ## Creating a New Plugin Use the generator: ```bash pnpm generate "new plugin" ``` When adding a new plugin, also update the root `README.md` `## Current Plugins` table so the monorepo plugin list stays current. Or to migrate an existing plugin: ```bash pnpm generate "copy plugin" ``` For agent-specific transfer guidance, use the `plugin-transfer` skill in `.agents/skills/plugin-transfer/SKILL.md`. When migrating a plugin, agents should ensure: - `README.md` from the original repository is preserved, but cleaned up for the monorepo: - Remove stale standalone-repo sections such as `## Develop & test` (usually references `@sanity/plugin-kit`) and `### Release new version` (references the original repo's release workflow / semantic-release). Building, testing, and releasing are handled centrally by the monorepo. - Remove links to old/forked versions (e.g. "for the v2 version, see this other repo"). - Do not reference a specific Sanity Studio major: drop mentions of the current latest major (e.g. "Sanity Studio v6", which ages quickly) and of long-gone majors like v3. Reword to be version-agnostic (e.g. "maintained by Sanity.io"); only mention a version when truly necessary, at most as `v2 - legacy`, and usually not at all. - `LICENSE` from the original repository is preserved when it credits authors beyond Sanity.io alone (the copy-plugin generator removes it by default); if kept, update the copyright year(s) to the current year - A test-studio example is present and wired in `dev/test-studio/sanity.config.ts` - `.github/CODEOWNERS` is not updated unless explicitly requested - The transfer includes a **major** changeset - Only monorepo-required plugin config files are maintained (`package.json`, `package.config.ts`, `tsconfig.json`, `vitest.config.ts`) See [CONTRIBUTING.md](./CONTRIBUTING.md) for detailed instructions on: - Setting up npm trusted publishing (different process for new vs existing packages) - Creating initial release changesets - Migrating existing plugins ### Trusted Publishing Quick Reference **For brand new packages (not yet on npm):** - Use the "Setup a new npm package with Trusted Publishing" GitHub Actions workflow - Then run locally: `npm trust github --file=release.yml --repository=sanity-io/plugins` (requires npm >= 11.10.0) **For existing packages (already on npm):** - ⚠️ DO NOT use the setup workflow - Run: `npm trust github --file=release.yml --repository=sanity-io/plugins` (requires npm >= 11.10.0) ## Code Style ### Dependencies **Always use `lodash-es` instead of `lodash`** When working with lodash utility functions, always use the `lodash-es` package instead of `lodash`. The `lodash-es` package is the ES module version that supports tree-shaking and works correctly with modern build tools. ```bash # Correct pnpm add lodash-es # Wrong pnpm add lodash ``` ### Formatting We use [oxfmt](https://oxc.rs/docs/formatter.html): ```bash pnpm format ``` ### Linting We use [oxlint](https://oxc.rs/docs/linter.html) for all linting (type-aware, includes TypeScript type checking and React Compiler rules via the react-hooks-js plugin): ```bash pnpm lint # Run the linter (includes type checking) pnpm lint:fix # Auto-fix what's possible ``` ## Project Structure ``` plugins/ ├── dev/test-studio/ # Test Sanity Studio (localhost:3333) ├── packages/@repo/ # Internal shared packages ├── packages/@sanity/ # Published tooling packages (e.g. @sanity/plugin-kit) ├── plugins/ # Published plugins │ ├── @sanity/ # @sanity/* scoped packages │ └── sanity-plugin-* # Community-style packages ├── turbo/generators/ # Plugin scaffolding templates └── .changeset/ # Changeset files for releases ``` ## Common Issues ### Build fails with "tsgo did not generate dts file" Declarations are generated with tsgo (`tsgo: true` in `@repo/package.config`), which requires exported types to be portable. A `TS2883` error printed above the failure means an inferred exported type references a module that is not publicly addressable (for example a deep `.pnpm` or dts-chunk path). Fix it by adding an explicit type annotation at the reported site so the emitted declaration can use a locally imported name (see the actor annotations in `plugins/sanity-plugin-dashboard-widget-vercel/src/machines/form.ts` for an example). Note that test files are part of the declaration program too, since the single `tsconfig.json` includes them. ### Lint Errors About Missing Types Run `pnpm build` first—some packages need to be built for type information to be available. ### "Command not found: pnpm" Ensure you have pnpm v10+ installed, then run: ```bash corepack enable ``` ### Test Studio Won't Load 1. Check you're logged into Sanity in the browser 2. Verify you have access to the project 3. Check the browser console for specific errors ## Cursor Cloud specific instructions ### Services | Service | Port | Purpose | | ------------------------ | ---- | --------------------------------------------- | | Test Studio (`pnpm dev`) | 3333 | Local Sanity Studio for manual plugin testing | No Docker, databases, or other local services are required. CI-style verification (`pnpm lint`, `pnpm build`, `pnpm test run`) runs entirely in-process. ### Starting the dev server The test studio dev script uses `sanity dev --no-auto-updates`, so `pnpm dev` starts non-interactively (no version-upgrade prompt). The studio serves at `http://localhost:3333`. ### Sanity authentication The test studio connects to Sanity Cloud (project `ppsg7ml5`, dataset `plugins` by default). For cloud agents, use the injected `STUDIO_AUTH_TOKEN` secret instead of interactive login. Navigate to the studio with the token in the URL hash (Sanity consumes it on load and removes it from the address bar): ``` http://localhost:3333/home#token= ``` Build the URL from the secret: ```bash node -e "const t=process.env.STUDIO_AUTH_TOKEN; console.log('http://localhost:3333/home#token=' + encodeURIComponent(t))" ``` Open that URL in the browser to authenticate and land directly in the Home workspace (the merged "kitchen sink"). Without a token, workspaces show as "Signed out". ### Node.js version notes `dev/test-studio` declares `engines.node: "24"`; the monorepo otherwise targets latest LTS. Node 24 is preferred when available, and **Node >= 22.18 is required for a full `pnpm build`**: the `@repo/generators` build runs `tsdown`, which loads its `.mts` config through Node's native TypeScript support. On older Node 22.x (e.g. the `v22.14.0` that may be the VM default) that build fails with `Failed to import module "unrun"`. A new enough runtime is usually available via `nvm` (e.g. `export PATH="$HOME/.nvm/versions/node/v22.22.2/bin:$PATH"`). `pnpm lint` and `pnpm test` work on older Node 22 too. ### Lint / build / test Standard commands from the Quick Reference table apply. Run `pnpm build` before `pnpm lint` if type errors reference missing `dist/` output. `pretest` automatically builds all packages except `dev/*` before Vitest runs. ## Related Documentation - [CONTRIBUTING.md](./CONTRIBUTING.md) - Human contributor guide - [Changesets documentation](https://github.com/changesets/changesets) - [Turborepo documentation](https://turbo.build/docs)