Skip to content

Commit 0167b26

Browse files
authored
chore: add tldraw-migrate skill (tldraw#8765)
In order to help projects upgrade to newer tldraw SDK versions, this PR adds a `tldraw-migrate` skill that an agent can invoke as `/tldraw-migrate` (or `/tldraw-migrate <previous-version>` to override version detection). The skill auto-detects the previous tldraw version from git history, fetches and filters the public release notes between that version and the latest, downloads the full SDK docs for ad-hoc lookup, and walks through a structured migration: 1. Inspect the project (package manager, tldraw packages installed, import style, typecheck/build/lint setup). 2. Upgrade tldraw packages. 3. Categorize TypeScript errors by code, file, and pattern. 4. Fix in order of impact: React types → custom shape and binding registration → API renames → TipTap → remaining errors. 5. Address `@deprecated` symbols. 6. Verify with typecheck and build, plus a sanity check that no new `as` casts crept in. The skill is marked `disable-model-invocation: true` and `user-invocable: true`, so it only runs when the user types `/tldraw-migrate`. ### Bundled helpers - `detect-versions.mjs` — resolves the "from" version. Order: explicit CLI arg → `git show main:package.json` (only when it differs from the working tree, so on-main migrations don't return the new version) → `HEAD~1` → working-tree fallback. Probes 11 tldraw packages so projects that don't depend on the umbrella `tldraw` package are still detected. - `filter-changelog.mjs` — filters `https://tldraw.dev/llms-releases.txt` to versions between `from` and `to` (inclusive of `to`). The downloaded references live under `skills/tldraw-migrate/references/` and are gitignored. The full docs (~1.5MB) are cached across invocations; the changelog is always re-fetched since it's small and version-dependent. ### Change type - [x] \`other\` ### Test plan 1. In a tldraw-using project where the working tree has bumped tldraw versions but `main` (or `HEAD~1`) still has the old ones, invoke \`/tldraw-migrate\`. 2. Confirm the skill prints the detected previous version, writes the filtered changelog to \`references/tldraw-releases.txt\`, downloads the full docs to \`references/tldraw-full-docs.txt\`, and re-uses the cached docs on a second invocation. 3. Override detection with \`/tldraw-migrate 4.0.0\` and confirm the explicit version is used. ### Code changes | Section | LOC change | | -------------- | ---------- | | Config/tooling | +339 / -0 |
1 parent 420fad8 commit 0167b26

4 files changed

Lines changed: 339 additions & 0 deletions

File tree

skills/tldraw-migrate/.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
references/

skills/tldraw-migrate/SKILL.md

Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
---
2+
name: tldraw-migrate
3+
description: Migrate a project to a newer version of the tldraw SDK. Use when upgrading tldraw packages, fixing TypeScript errors after a tldraw upgrade, or when the user mentions tldraw migration.
4+
argument-hint: '[previous-version]'
5+
disable-model-invocation: true
6+
user-invocable: true
7+
---
8+
9+
# tldraw migration assistant
10+
11+
You are helping migrate a project to a newer version of the tldraw SDK. Follow this process carefully.
12+
13+
Previous version (auto-detected from git history; pass an explicit version as `/tldraw-migrate <version>` to override): !`node ${CLAUDE_SKILL_DIR}/detect-versions.mjs $ARGUMENTS`
14+
15+
## Resources (auto-fetched on invocation)
16+
17+
!`mkdir -p ${CLAUDE_SKILL_DIR}/references && CHANGELOG=${CLAUDE_SKILL_DIR}/references/tldraw-releases.txt && PREV=$(node ${CLAUDE_SKILL_DIR}/detect-versions.mjs $ARGUMENTS) && curl --fail -sS https://tldraw.dev/llms-releases.txt | node ${CLAUDE_SKILL_DIR}/filter-changelog.mjs "$PREV" > "$CHANGELOG" && echo "Saved changelog (from $PREV) to $CHANGELOG ($(wc -l < "$CHANGELOG") lines)"`
18+
19+
!`DOCS=${CLAUDE_SKILL_DIR}/references/tldraw-full-docs.txt && if [ -s "$DOCS" ]; then echo "Using cached full docs at $DOCS ($(wc -l < "$DOCS") lines) — delete the file to refresh"; else curl --fail -sS https://tldraw.dev/llms-full.txt -o "$DOCS" && echo "Saved full docs to $DOCS ($(wc -l < "$DOCS") lines)"; fi`
20+
21+
- **[Filtered changelog](references/tldraw-releases.txt)** — release notes for versions between the previous version and now. Read this first to understand what changed.
22+
- **[Full docs](references/tldraw-full-docs.txt)** — complete tldraw SDK docs (~1.5MB). Do NOT read this upfront. Use Grep or Read with line ranges to search for specific topics as needed (e.g., custom shapes, TLTextOptions).
23+
24+
## Step 1: Understand the environment
25+
26+
Before making any changes, scan the project to understand what you're working with. Run these in parallel:
27+
28+
- **Package manager**: Check for lock files (`yarn.lock`, `pnpm-lock.yaml`, `bun.lockb`, `package-lock.json`). Use the corresponding tool throughout.
29+
- **tldraw packages**: `grep -E "tldraw|@tldraw" package.json` — which packages are installed, and at what versions? Note: not every project uses all tldraw packages.
30+
- **Import style**: `grep -r "from '@tldraw/" src/ --include="*.ts" --include="*.tsx" -l | head -5` — does the project import from `'tldraw'`, `'@tldraw/editor'`, or both? This affects module augmentation targets.
31+
- **TypeScript**: Check `package.json` for a `typecheck` or `tsc` script. Also check the TypeScript version — is it a direct dependency or does the project rely on a global install?
32+
- **Build tool**: Check `package.json` scripts for the build command (vite, next, webpack, esbuild, etc.)
33+
- **Linter**: Check for eslint/biome config files (`.eslintrc*`, `eslint.config.*`, `biome.json`). A linter may help catch deprecations later.
34+
- **Monorepo**: Is `package.json` at the working directory root, or is this a nested package? Check for workspaces config.
35+
36+
## Step 2: Upgrade packages
37+
38+
Using the detected package manager, upgrade all tldraw packages that are already in the project's dependencies to the latest version. Don't add new packages the project doesn't already use.
39+
40+
## Step 3: Identify all TypeScript errors
41+
42+
Run the project's typecheck command (from Step 1) and categorize the errors:
43+
44+
1. **Count errors by TS code** (e.g., TS2344, TS2786) to understand the distribution
45+
2. **List unique files with errors** to understand the scope
46+
3. **Identify error patterns** — common categories in tldraw upgrades:
47+
- React types mismatch (TS2786 "cannot be used as JSX component") — usually means `@types/react` version doesn't match what tldraw bundles. To find the bundled version: with npm or yarn classic, check `node_modules/@tldraw/editor/node_modules/@types/react/package.json`; with pnpm or yarn berry, run the package manager's "why" command (e.g. `pnpm why @types/react`, `yarn why @types/react`) to see the resolved range.
48+
- Custom shape type registration (TS2344 "does not satisfy constraint TLShape/TLBaseBoxShape") — tldraw v4.3+ requires module augmentation of `TLGlobalShapePropsMap`
49+
- Custom binding type registration — same pattern with `TLGlobalBindingPropsMap`
50+
- `BaseBoxShapeTool.shapeType` type mismatch (TS2416) — needs `as const`
51+
- API renames/removals (TS2305, TS2724) — check changelog for specific migrations
52+
- `createShapes`/`updateShapes` type widening (TS2345) — mapped arrays need `TLShapePartial` or `TLCreateShapePartial` casts
53+
54+
## Step 4: Fix errors in order of impact
55+
56+
Fix in this order (each fix eliminates many downstream errors):
57+
58+
### 4a. Fix React types
59+
60+
If you see TS2786 "bigint not assignable to ReactNode" errors, upgrade `@types/react` and `@types/react-dom` to match tldraw's bundled version.
61+
62+
### 4b. Register custom shapes and bindings
63+
64+
For every `TLBaseShape<'name', Props>` in the codebase, add module augmentation. Use the import style detected in Step 1 as the module target:
65+
66+
```ts
67+
declare module 'tldraw' {
68+
interface TLGlobalShapePropsMap {
69+
'shape-name': {
70+
/* props */
71+
}
72+
}
73+
}
74+
type MyShape = TLShape<'shape-name'>
75+
```
76+
77+
Same pattern for bindings with `TLGlobalBindingPropsMap` and `TLBinding<'name'>`.
78+
79+
**IMPORTANT**: If multiple files use the same shape type name with different props, rename them to be unique — they all share the global type registry.
80+
81+
Add `as const` to all `static override shapeType = '...'` properties.
82+
83+
### 4c. Fix API renames and removals
84+
85+
Cross-reference each TS2305/TS2724/TS2339 error with the changelog. Common patterns:
86+
87+
- Removed exports: find replacement by grepping the tldraw type definitions
88+
- Renamed properties: check changelog for the new name
89+
- Changed method signatures: check the current type definitions
90+
91+
To find tldraw type definitions, check which paths exist — the location varies by version:
92+
93+
- `node_modules/tldraw/dist-cjs/index.d.ts`
94+
- `node_modules/tldraw/dist/index.d.ts`
95+
- `node_modules/@tldraw/editor/dist-cjs/index.d.ts`
96+
- `node_modules/@tldraw/tlschema/dist-cjs/index.d.ts`
97+
98+
### 4d. Fix TipTap imports if needed
99+
100+
If the project uses TipTap (`@tiptap/*` in dependencies or imports), the tldraw upgrade may require upgrading TipTap as well. Starting with tldraw v4.2, tldraw bundles TipTap v3 as a transitive dependency. If the project pins TipTap v2 packages directly, this will cause version conflicts and broken imports. **Upgrade the project's direct `@tiptap/*` dependencies to v3** to match what tldraw expects — leaving them on v2 will not work.
101+
102+
After upgrading, fix any breaking TipTap v2 → v3 changes:
103+
104+
- **Default → named exports**: If a default import fails, switch to a named import: `import StarterKit from '@tiptap/starter-kit'` → `import { StarterKit } from '@tiptap/starter-kit'`
105+
- **Renames**: If a named import fails, check the package's actual exports: `grep 'export' node_modules/@tiptap/<package>/dist/index.js | head` — some extensions were renamed in TipTap v3 (e.g., `TextStyle` → `TextStyleKit`).
106+
- **Transaction handler types**: If TipTap event handler types break, import the proper types: `import { EditorEvents } from '@tiptap/core'` and type handlers as `(props: EditorEvents['transaction']) => void` rather than inline type annotations.
107+
108+
### 4e. Fix remaining type errors
109+
110+
- `createShapes`/`updateShapes` with `.map()`: The proper fix is to add `as const` to the `type` field in the mapped object literal so TypeScript narrows it to the string literal type. If that's not sufficient, use a `satisfies` annotation on the return value. Only as a last resort, cast the result to `TLCreateShapePartial` or `TLShapePartial`.
111+
- TipTap extension commands not on `ChainedCommands`: use `declare module '@tiptap/core'` augmentation to register custom commands.
112+
- **General rule**: every `as` cast you add is tech debt. Before adding one, exhaust these alternatives in order:
113+
1. `as const` on object literals to narrow string literal types
114+
2. `satisfies` annotations to check types without widening
115+
3. Proper generic type parameters on the call site
116+
4. Module augmentation to teach TypeScript about your types
117+
5. Only then, a targeted `as` cast with a comment explaining why it's needed
118+
119+
## Step 5: Fix deprecations
120+
121+
After all type errors are resolved, find and fix deprecated API usage. These still compile but should be migrated.
122+
123+
1. **Find deprecated symbols** — grep the tldraw type definitions for `@deprecated` annotations. Also search the changelog for "deprecated" in `${CLAUDE_SKILL_DIR}/references/tldraw-releases.txt`.
124+
125+
2. **Check if a linter is configured** — look for eslint, biome, or other linting config in the project. If available, run the linter — it may flag deprecated usage automatically (e.g., eslint's `deprecation/deprecation` rule). If no linter is configured, skip to step 3.
126+
127+
3. **Search the project source** for each deprecated symbol found in step 1. Replace with the recommended alternative from the `@deprecated` JSDoc comment or changelog.
128+
129+
## Step 6: Verify
130+
131+
Run the project's typecheck and build commands (as discovered in Step 1).
132+
133+
If errors remain, repeat the categorize-and-fix cycle.
134+
135+
### Post-migration sanity check
136+
137+
After all errors are resolved, do a quick audit:
138+
139+
1. **Count `as` casts before vs after**: Run `grep -rn ' as ' --include='*.ts' --include='*.tsx' src/` and compare against the same grep on the pre-migration code (use `git stash` or `git show HEAD:path`). **The migration should add no more than a small handful of new casts** (ideally zero). If you added more than ~5 new `as` casts across the entire migration, go back and fix them — you are almost certainly using the new API incorrectly.
140+
2. **Review every cast you added**: For each new `as` cast, verify it's truly necessary by checking whether `as const`, `satisfies`, generic type parameters, or module augmentation could replace it. Remove or replace any that have a cleaner alternative.
141+
3. **Verify no stubs or dead code**: If you stubbed out removed APIs (e.g., replaced a removed function with a no-op), make sure the calling code doesn't depend on the return value. If it does, find the proper replacement in the changelog or docs.
142+
143+
## Quality checks
144+
145+
- **Type safety is paramount.** The goal is a migration that is as type-safe as the original code. Do NOT add `as any`, `as unknown`, or broad type casts. Do NOT add `@ts-ignore` or `@ts-expect-error`. These are never acceptable — if you can't make the types work, you don't understand the new API yet. Stop and read the changelog and type definitions before continuing.
146+
- **Prefer TypeScript's narrowing features over casts.** Use `as const` for literal types, `satisfies` for type-checking without widening, generic parameters for call sites, and module augmentation for extending interfaces. These are the right tools for a migration — `as` casts are not.
147+
- Use parallel agents for fixing large batches of files with the same pattern.
148+
- **Don't just make errors go away — understand the new API.** When a method signature changes (e.g., new parameters added, property renamed to a richer type), read the changelog AND the current type definitions to understand _why_ it changed. A fix that compiles but passes hardcoded/dummy values where the new API expects real data is worse than a type error — it silently degrades behavior. For example, if a function gains new required parameters, check what shape props or editor state should feed those parameters rather than passing `0` or `1`.
149+
- **When unsure about an API pattern**, grep the full docs (`${CLAUDE_SKILL_DIR}/references/tldraw-full-docs.txt`) for usage examples of that specific API. The docs contain code samples that show the canonical way to use each API.
150+
151+
## Tips
152+
153+
- Read `${CLAUDE_SKILL_DIR}/references/tldraw-releases.txt` for the filtered changelog — this is the primary source for what changed
154+
- Grep `${CLAUDE_SKILL_DIR}/references/tldraw-full-docs.txt` when you need docs on a specific API
155+
- When searching tldraw types, try multiple paths — the dist directory structure varies by version and package manager
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
#!/usr/bin/env node
2+
/**
3+
* Detects the previous tldraw version and writes it to stdout.
4+
*
5+
* Resolution order:
6+
* 1. An explicit version passed as argv[2] (e.g., "/tldraw-migrate 4.0.0").
7+
* 2. The version of any tldraw package on the main/master branch — but only
8+
* if it differs from the working tree (otherwise the user is migrating
9+
* directly on main and main already reflects the new version).
10+
* 3. The version in HEAD~1.
11+
* 4. The version in the current working tree (last resort: assumes the user
12+
* hasn't bumped yet, so the working tree IS the previous version).
13+
*
14+
* Outputs just the version string (no trailing newline) so the SKILL.md can
15+
* interpolate it into other commands.
16+
*/
17+
18+
import { execSync } from 'child_process'
19+
import { readFileSync } from 'fs'
20+
21+
function exec(cmd) {
22+
try {
23+
return execSync(cmd, { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] }).trim()
24+
} catch {
25+
return null
26+
}
27+
}
28+
29+
const TLDRAW_PACKAGES = [
30+
'tldraw',
31+
'@tldraw/editor',
32+
'@tldraw/tldraw',
33+
'@tldraw/store',
34+
'@tldraw/tlschema',
35+
'@tldraw/state',
36+
'@tldraw/state-react',
37+
'@tldraw/sync',
38+
'@tldraw/sync-core',
39+
'@tldraw/utils',
40+
'@tldraw/validate',
41+
]
42+
43+
function extractTldrawVersion(packageJsonStr) {
44+
try {
45+
const pkg = JSON.parse(packageJsonStr)
46+
const deps = { ...(pkg.dependencies || {}), ...(pkg.devDependencies || {}) }
47+
for (const name of TLDRAW_PACKAGES) {
48+
const raw = deps[name]
49+
if (!raw) continue
50+
const cleaned = String(raw).replace(/[\^~>=<\s]/g, '')
51+
const match = cleaned.match(/(\d+\.\d+\.\d+)/)
52+
if (match) return match[1]
53+
}
54+
return null
55+
} catch {
56+
return null
57+
}
58+
}
59+
60+
function readWorkingTreeVersion() {
61+
try {
62+
return extractTldrawVersion(readFileSync('package.json', 'utf8'))
63+
} catch {
64+
return null
65+
}
66+
}
67+
68+
function emit(version) {
69+
process.stdout.write(version)
70+
process.exit(0)
71+
}
72+
73+
const explicit = process.argv[2]?.trim()
74+
if (explicit) {
75+
const cleaned = explicit.replace(/^v/, '')
76+
const match = cleaned.match(/^(\d+\.\d+\.\d+)/)
77+
if (match) emit(match[1])
78+
}
79+
80+
const workingTreeVersion = readWorkingTreeVersion()
81+
82+
for (const branch of ['main', 'master']) {
83+
const content = exec(`git show ${branch}:package.json`)
84+
if (!content) continue
85+
const ver = extractTldrawVersion(content)
86+
if (!ver) continue
87+
// If the branch version matches the working tree, the user likely bumped
88+
// directly on this branch — main isn't a useful "before" reference.
89+
if (workingTreeVersion && ver === workingTreeVersion) continue
90+
emit(ver)
91+
}
92+
93+
const parent = exec('git show HEAD~1:package.json')
94+
if (parent) {
95+
const ver = extractTldrawVersion(parent)
96+
if (ver && ver !== workingTreeVersion) emit(ver)
97+
}
98+
99+
if (workingTreeVersion) emit(workingTreeVersion)
100+
101+
console.error(
102+
'Could not detect previous tldraw version. Pass it as an argument: /tldraw-migrate 4.0.0'
103+
)
104+
process.exit(1)
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
#!/usr/bin/env node
2+
/**
3+
* Filters tldraw changelog to only versions between `from` and `to` (inclusive of `to`).
4+
* Usage: node filter-changelog.mjs <from-version> [to-version]
5+
*
6+
* If to-version is omitted, includes everything newer than from-version.
7+
* Versions should be semver like "4.0.0" or "v4.0.0" (the "v" prefix is optional).
8+
*
9+
* Reads changelog from stdin, writes filtered output to stdout.
10+
*/
11+
12+
const [, , fromArg, toArg] = process.argv
13+
14+
if (!fromArg) {
15+
console.error('Usage: node filter-changelog.mjs <from-version> [to-version]')
16+
console.error(
17+
' e.g.: curl -s https://tldraw.dev/llms-releases.txt | node filter-changelog.mjs 4.0.0 4.6.0'
18+
)
19+
process.exit(1)
20+
}
21+
22+
function parseVersion(str) {
23+
const m = str.replace(/^v/, '').match(/^(\d+)\.(\d+)\.(\d+)/)
24+
if (!m) return null
25+
return [+m[1], +m[2], +m[3]]
26+
}
27+
28+
function compareVersions(a, b) {
29+
for (let i = 0; i < 3; i++) {
30+
if (a[i] !== b[i]) return a[i] - b[i]
31+
}
32+
return 0
33+
}
34+
35+
const fromVer = parseVersion(fromArg)
36+
const toVer = toArg ? parseVersion(toArg) : null
37+
38+
if (!fromVer) {
39+
console.error(`Invalid from-version: ${fromArg}`)
40+
process.exit(1)
41+
}
42+
if (toArg && !toVer) {
43+
console.error(`Invalid to-version: ${toArg}`)
44+
process.exit(1)
45+
}
46+
47+
let input = ''
48+
process.stdin.setEncoding('utf8')
49+
process.stdin.on('data', (chunk) => (input += chunk))
50+
process.stdin.on('end', () => {
51+
// Split into sections by top-level headings (# vX.Y.Z)
52+
const sections = []
53+
let current = null
54+
55+
for (const line of input.split('\n')) {
56+
const headingMatch = line.match(/^# (v?\d+\.\d+\.\d+)/)
57+
if (headingMatch) {
58+
if (current) sections.push(current)
59+
current = { version: parseVersion(headingMatch[1]), lines: [line] }
60+
} else if (current) {
61+
current.lines.push(line)
62+
}
63+
}
64+
if (current) sections.push(current)
65+
66+
// Filter: include versions where from < version <= to
67+
const filtered = sections.filter((s) => {
68+
const afterFrom = compareVersions(s.version, fromVer) > 0
69+
const beforeTo = toVer ? compareVersions(s.version, toVer) <= 0 : true
70+
return afterFrom && beforeTo
71+
})
72+
73+
if (filtered.length === 0) {
74+
console.error(`No versions found between ${fromArg} and ${toArg || 'latest'}`)
75+
process.exit(0)
76+
}
77+
78+
process.stdout.write(filtered.map((s) => s.lines.join('\n')).join('\n\n'))
79+
})

0 commit comments

Comments
 (0)