# Figma Typings Audit

> Upgrade @figma/plugin-typings and absorb what the new version exposes. Diffs the .d.ts between the installed and the target version (that package ships no changelog), sorts the changes into breakage / new API / silently-added fields, maps each onto the sandbox handlers, the hand-written Zod mirrors in shared, and the tool registry — then bumps the package and builds whatever the user picks. Use whenever the user wants @figma/plugin-typings updated or audited, or asks what a new plugin-typings version would break or newly enable — including a Renovate bump PR for that package.

- Skill: `awdr74100/figma-typings-audit` (Agent Skill)
- Install (CLI): `npx skillmds@latest add awdr74100/figma-typings-audit`
- Raw SKILL.md: https://api.skillmd.com/api/skills/awdr74100/figma-typings-audit/raw
- Safety review: pending (external: skill-scanner PASS, skillspector PASS)
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Design & Media
- Author: awdr74100 (https://skillmd.com/u/awdr74100)
- Updated: 2026-09-09
- Page: https://skillmd.com/skills/awdr74100/figma-typings-audit

---


Absorbing a `@figma/plugin-typings` release into Figwright, end to end: audit → upgrade →
implement what's worth having.

**One hard ordering constraint: diff before upgrading.** The audit compares the *installed* version
against the target, so upgrading first destroys the baseline. (Recoverable — the old version is in
git history and `npm pack` can still fetch it — but don't create the problem.)

Target version: whatever the user named, otherwise the latest on npm.

## Stage 0 — Resolve versions

```bash
grep '@figma/plugin-typings' packages/plugin/package.json          # the declared range
grep '"version"' packages/plugin/node_modules/@figma/plugin-typings/package.json  # what is installed
npm view @figma/plugin-typings version                              # latest
npm view @figma/plugin-typings versions --json                      # how many releases are being skipped
```

Compare against the **installed** version, not the declared range — a caret range can already be
satisfied by something newer than what the lockfile pinned.

If installed and target are equal, say so and stop. Don't spend tokens on the rest.

## Stage 1 — Get the authoritative diff

There is no other source. `figma/plugin-typings` ships **no CHANGELOG and no GitHub Releases** (the
releases API returns an empty array), and its commit messages are content-free (`1.132.0`,
`Release v1.132 updates`). The `.d.ts` diff is the only ground truth.

Work in the session scratchpad directory (`$SCRATCH` below):

```bash
cd "$SCRATCH" && mkdir -p typings-audit && cd typings-audit
npm pack @figma/plugin-typings@<installed> @figma/plugin-typings@<target> --pack-destination .
for v in <installed> <target>; do
  mkdir -p "$v" && tar -xzf "figma-plugin-typings-$v.tgz" -C "$v" --strip-components=1
done
diff -rq <installed> <target>            # which files differ at all — start here
diff -u <installed>/package.json <target>/package.json
diff -u <installed>/index.d.ts <target>/index.d.ts
```

`diff -rq` first, so the set of changed files is observed rather than assumed. Every file it names
has to be accounted for — including `plugin-api-standalone.d.ts`, whose trailing `export { ... }` is
one enormous single line where a removed symbol is easy to miss.

`index.d.ts` declares the `figma` global, `fetch` and timers — small, but high blast radius.
`package.json` looks irrelevant and isn't: **its `devDependencies.prettier` explains formatting
noise.** When that version moves, the `.d.ts` diff fills with reflowed unions and `extends` clauses
that mean nothing. Check it before reading a single hunk.

### Normalize before diffing the `.d.ts`

Never classify hunks straight out of `diff -u`. Reformat **both** versions with the target's own
formatter first, so what survives is guaranteed to be semantic:

```bash
mkdir -p norm
for v in <installed> <target>; do
  for f in plugin-api.d.ts plugin-api-standalone.d.ts; do cp "$v/$f" "norm/$v.$f"; done
done
cp <target>/.prettierrc norm/.prettierrc          # the package ships its own config
npx --yes prettier@<version from target package.json devDeps> --write "norm/*.d.ts"
diff -u norm/<installed>.plugin-api.d.ts norm/<target>.plugin-api.d.ts
diff -u norm/<installed>.plugin-api-standalone.d.ts norm/<target>.plugin-api-standalone.d.ts
```

Watch prettier's own per-file output: the **target** files should report `unchanged`. That confirms
you picked the right formatter version, and turns "the noise is upstream reformatting" from a guess
into an observation.

Classify from these normalized diffs. `plugin-api.d.ts` is the one that matters —
`packages/plugin/tsconfig.json` sets `types: ["@figma/plugin-typings"]`, whose `index.d.ts`
references it — and standalone serves as a cross-check that nothing was missed.

Measured on 1.134.0 → 1.135.0: 328 raw lines collapsed to 32, all of them one change.
**Never estimate the size of a release from raw diff line count** — 1.134.0 was smaller (228 lines)
and carried three real buckets.

When the jump spans several releases, diff the two endpoints; the cumulative effect is what matters.
Go release-by-release only to attribute a specific change to a version.

## Stage 2 — Classify every hunk

Drop hunks that are **JSDoc-only** (comment/example churn, no signature change) — the file is mostly
documentation and these dominate the line count without meaning anything.

Sort the rest into three buckets:

1. **Breaking** — a removed symbol, a narrowed type (optional → required, union → smaller union), a
   changed signature, or a newly-added `@deprecated`. Look at `-` lines first.
2. **New API** — a new method, node type, enum member, or namespace. A new *capability*, so a
   candidate for a new tool or a new argument on an existing one.
3. **New field on an existing type** — an added property on something Figwright already reads
   (`Paint`, `Effect`, `TextStyle`, a node interface…). ⚠️ **The dangerous bucket.** No compiler will
   ever flag it and the read path will keep silently omitting the dimension. It is this repo's
   recurring bug class: a multi-dimensional Figma property collapsed to one field or dropped on the
   way out.

## Stage 3 — Map the diff onto the repo

The three source trees have completely different exposure. Don't treat them alike:

| Tree                      | Coupling to typings                                                                                                                                                        | Who catches a break |
| ------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------- |
| `packages/plugin/src/**`  | **Real.** The only tree that calls `figma.*` and loads the global types (`tsconfig.json` → `types: ["@figma/plugin-typings"]`)                                              | `tsc`               |
| `packages/plugin/ui/**`   | None — the Vue panel never touches the Figma API                                                                                                                            | n/a, skip           |
| `packages/shared/src/**`  | ★ **None — and that's the trap.** `serialized-node.ts`, `styles.ts`, `queries.ts` are *hand-written Zod mirrors* of Figma shapes; `shared/tsconfig.json` never loads typings | **Nobody but this audit** |

- **Bucket 1** — grep the affected symbols under `packages/plugin/src/` (handlers, `serializer.ts`,
  `traverse.ts`, `reveal.ts`). Confirm with Stage 4 rather than reasoning about it.
- **Bucket 3** — for each added field, check whether the Zod mirror in `packages/shared/src/` carries
  it, and whether `serializer.ts` or `handlers/get-design-context.ts` projects it. A miss here is a
  real fidelity gap with every gate green.
- **Bucket 2** — check coverage against `packages/mcp/src/tools/registry.ts` (`ALL_TOOL_SPECS` is the
  authority on the tool count; prose in READMEs is hand-written and stale).

## Stage 4 — Sandbox typecheck (only if Stage 2 found bucket 1)

Type-check the real sandbox sources against the **new** typings without touching the repo. Never
overwrite the copy in `node_modules` — pnpm hard-links it into the global store, so writing there
corrupts the store for every project on the machine.

Write `$SCRATCH/typings-audit/tsconfig.probe.json`:

```json
{
  "compilerOptions": {
    "target": "ES2023",
    "module": "ESNext",
    "moduleResolution": "Bundler",
    "lib": ["ES2023", "DOM", "DOM.Iterable"],
    "strict": true,
    "noUncheckedIndexedAccess": true,
    "noImplicitOverride": true,
    "noFallthroughCasesInSwitch": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "exactOptionalPropertyTypes": true,
    "esModuleInterop": true,
    "forceConsistentCasingInFileNames": true,
    "isolatedModules": true,
    "verbatimModuleSyntax": true,
    "resolveJsonModule": true,
    "skipLibCheck": true,
    "noEmit": true,
    "types": []
  },
  "files": ["./<target>/index.d.ts"],
  "include": [
    "<REPO_ROOT>/packages/plugin/src/**/*.ts",
    "<REPO_ROOT>/packages/plugin/protocol/**/*.ts"
  ]
}
```

Expand `<REPO_ROOT>` to `git rev-parse --show-toplevel` — the tsconfig lives outside the repo, so
those entries have to be absolute. Never hard-code a machine-specific path into a committed file.

`types: []` keeps the installed (old) typings out; the `files` entry pulls the new ones in via its
`/// <reference path="./plugin-api.d.ts" />`. Module resolution still works because it is anchored to
each source file's own location, not to the tsconfig's.

```bash
./node_modules/.bin/tsc -p "$SCRATCH/typings-audit/tsconfig.probe.json"
```

**Run the same probe against the installed version first.** A clean baseline is what makes a failure
on the new version mean something.

## Stage 5 — Decide, then upgrade

- **Anything in bucket 1 → stop and report.** Present what breaks and what it would cost to absorb,
  and let the user decide. Never upgrade past a breaking change on your own initiative.
- **Otherwise upgrade** and prove the tree is still green:

```bash
pnpm -C packages/plugin add -D @figma/plugin-typings@^<target>
pnpm typecheck && pnpm lint && pnpm format:check && pnpm knip && pnpm build && pnpm test
```

This is the first step that writes to the repo — `packages/plugin/package.json` and
`pnpm-lock.yaml`. Everything before it was read-only.

## Stage 6 — Report, then let the user pick

Report in 繁體中文台灣用語, ordered by consequence:

1. **會壞掉的** — symbol, call sites under `packages/plugin/src/`, probe verdict. Say so explicitly
   when the list is empty.
2. **靜默漏接風險** (bucket 3) — added field → the `shared` mirror that lacks it → the projection
   that would need to carry it. Flag that no gate catches these.
3. **新能力** (bucket 2) — one line each: what Figma now exposes, and the smallest change that would
   surface it.

Report only what the diff and the probe actually showed. Don't pad with plausible-sounding changes,
and never claim a live verification you didn't run.

Then `AskUserQuestion` (multi-select) over the items from 2 and 3.

## Stage 7 — Implement the picks

Choosing where a new capability lands is the part worth thinking about:

- **Follow the real usage path, not the closest-sounding tool name.** Ask when an agent would
  actually want this value, and put it on the tool that is already being called at that moment.
- **Keep the semantics honest.** Editor-wide state does not belong inside a per-node object — make it
  a sibling field, and say so in the JSDoc.
- **Smallest change that closes the gap**: a field on an existing result < a new argument < a new
  tool. The tool count is already large; a new tool needs to earn itself.
- A new read dimension usually touches four places: the Zod schema in `packages/shared/src/`, the
  sandbox handler in `packages/plugin/src/handlers/`, the tool description in
  `packages/mcp/src/tools/` (it is the LLM's only spec — fix it if it misreports the shape), and a
  test.
- `exactOptionalPropertyTypes` is on: assign optional fields conditionally
  (`if (x !== undefined) result.x = x`), never `result.x = undefined`.
- Guard editor-dependent APIs on `figma.editorType` rather than try/catch, so reads that
  deliberately don't assert the editor stay non-throwing in FigJam / Dev Mode without swallowing real
  errors.
- **Several handlers have no test file at all.** If the one you touch is bare, write the test file.
- **Attack your own test.** Break the guard on purpose and confirm the test goes red; a test that
  passes either way is decoration. Then put it back.

Re-run all six gates when done.

## Stage 8 — Hand off the live verification

Unit tests don't prove a read path works against real Figma. Before claiming anything:

- The MCP server runs the **built `dist`** → `pnpm build`, then the user reconnects the MCP server.
- The plugin sandbox runs the dist it loaded **at launch** → the user must close and reopen the
  plugin inside Figma. Reconnecting MCP alone is not enough.
- **Some values can't be verified automatically at all** — anything reflecting editor UI state (an
  open timeline, a playhead, a viewport) can be read but not staged through the plugin API. Design a
  contrast instead of a single reading: measure once in the neutral state, have the user set up the
  state, then measure again. A lone "field is absent" is unreadable — it can't distinguish "the
  plugin never reloaded" from "the state genuinely isn't there".
- Building probe material on the canvas writes to the user's file. Ask first, and delete it after.

Report what was actually run. If the live step didn't happen, say that.

