pi-tldraw 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (74) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +222 -0
  3. package/bridge/app-bridge-entry.js +6 -0
  4. package/mcp-app/LICENSE.md +9 -0
  5. package/mcp-app/PI_TLDRAW_PROVENANCE.json +32 -0
  6. package/mcp-app/README.md +129 -0
  7. package/mcp-app/dev-tunnel.sh +51 -0
  8. package/mcp-app/dist/editor-api.json +8493 -0
  9. package/mcp-app/dist/mcp-app.html +643 -0
  10. package/mcp-app/dist/method-map.json +915 -0
  11. package/mcp-app/package.json +42 -0
  12. package/mcp-app/plugins/tldraw-mcp/.cursor-plugin/plugin.json +10 -0
  13. package/mcp-app/plugins/tldraw-mcp/assets/logo.svg +3 -0
  14. package/mcp-app/plugins/tldraw-mcp/mcp.json +8 -0
  15. package/mcp-app/scripts/extract-editor-api.ts +1374 -0
  16. package/mcp-app/server.json +21 -0
  17. package/mcp-app/src/logger.ts +45 -0
  18. package/mcp-app/src/register-tools.ts +368 -0
  19. package/mcp-app/src/shared/generated-data.ts +160 -0
  20. package/mcp-app/src/shared/pending-requests.ts +69 -0
  21. package/mcp-app/src/shared/types.ts +76 -0
  22. package/mcp-app/src/shared/utils.ts +132 -0
  23. package/mcp-app/src/tools/exec.ts +120 -0
  24. package/mcp-app/src/tools/loadCachedCanvasWidgetHtml.ts +16 -0
  25. package/mcp-app/src/tools/search.ts +150 -0
  26. package/mcp-app/src/widget/app-context.tsx +29 -0
  27. package/mcp-app/src/widget/dev-log.tsx +70 -0
  28. package/mcp-app/src/widget/exec-helpers.ts +232 -0
  29. package/mcp-app/src/widget/export-tldr.ts +35 -0
  30. package/mcp-app/src/widget/focused/defaults.ts +141 -0
  31. package/mcp-app/src/widget/focused/focused-editor-proxy.ts +434 -0
  32. package/mcp-app/src/widget/focused/format.ts +366 -0
  33. package/mcp-app/src/widget/focused/to-focused.ts +258 -0
  34. package/mcp-app/src/widget/focused/to-tldraw.ts +570 -0
  35. package/mcp-app/src/widget/image-guard.tsx +106 -0
  36. package/mcp-app/src/widget/index.html +33 -0
  37. package/mcp-app/src/widget/mcp-app.css +113 -0
  38. package/mcp-app/src/widget/mcp-app.tsx +857 -0
  39. package/mcp-app/src/widget/persistence.ts +337 -0
  40. package/mcp-app/src/widget/snapshot.ts +157 -0
  41. package/mcp-app/src/worker.ts +305 -0
  42. package/mcp-app/tsconfig.json +23 -0
  43. package/mcp-app/vite.config.ts +13 -0
  44. package/mcp-app/wrangler.toml +45 -0
  45. package/mcp-app-source.json +36 -0
  46. package/package.json +90 -0
  47. package/patches/tldraw-mcp-app/001-pi-runtime.patch +35 -0
  48. package/scripts/assemble-mcp-app.mjs +193 -0
  49. package/scripts/build-bridge.mjs +74 -0
  50. package/scripts/e2e-mcp.mjs +69 -0
  51. package/scripts/e2e-packaged-mcp-app.mjs +79 -0
  52. package/scripts/run-mcp-app-dev.mjs +44 -0
  53. package/scripts/verify-bundle.mjs +41 -0
  54. package/scripts/verify-mcp-app-source.mjs +51 -0
  55. package/scripts/verify-mcp-app.mjs +38 -0
  56. package/scripts/verify-package-files.mjs +50 -0
  57. package/src/canvas/export.ts +164 -0
  58. package/src/canvas/state.ts +117 -0
  59. package/src/canvas/workflow.ts +105 -0
  60. package/src/commands/tldraw-command.ts +48 -0
  61. package/src/diagram/guidance.ts +44 -0
  62. package/src/host/local-host.ts +289 -0
  63. package/src/index.ts +762 -0
  64. package/src/mcp/client.ts +126 -0
  65. package/src/mcp/response.ts +74 -0
  66. package/src/semantic/layer.ts +309 -0
  67. package/src/server/server-manager.ts +153 -0
  68. package/src/store/export-store.ts +33 -0
  69. package/src/store/project-store.ts +251 -0
  70. package/src/ui/tldraw-status.ts +88 -0
  71. package/static/app-bridge-bundle.js +18114 -0
  72. package/static/app-bridge-bundle.meta.json +164 -0
  73. package/static/host.html +390 -0
  74. package/tsconfig.json +13 -0
@@ -0,0 +1,1374 @@
1
+ import fs from 'fs'
2
+ import path from 'path'
3
+ import { fileURLToPath } from 'url'
4
+ import ts from 'typescript'
5
+
6
+ const scriptPath = fileURLToPath(import.meta.url)
7
+ const __dirname = path.dirname(scriptPath)
8
+ const distDir = path.join(__dirname, '..', 'dist')
9
+ const outPath = path.join(distDir, 'editor-api.json')
10
+ const methodMapOutPath = path.join(distDir, 'method-map.json')
11
+ const formatTsPath = path.join(__dirname, '..', 'src', 'widget', 'focused', 'format.ts')
12
+ const execHelpersPath = path.join(__dirname, '..', 'src', 'widget', 'exec-helpers.ts')
13
+
14
+ // In the tldraw monorepo, packages build to .tsbuild/
15
+ const repoRoot = path.resolve(__dirname, '..', '..', '..')
16
+ const editorDtsPath = path.join(
17
+ repoRoot,
18
+ 'packages',
19
+ 'editor',
20
+ '.tsbuild',
21
+ 'lib',
22
+ 'editor',
23
+ 'Editor.d.ts'
24
+ )
25
+ const storeDtsPath = path.join(repoRoot, 'packages', 'store', '.tsbuild', 'index.d.ts')
26
+ const tlschemaDtsPath = path.join(repoRoot, 'packages', 'tlschema', '.tsbuild', 'index.d.ts')
27
+
28
+ for (const p of [editorDtsPath, storeDtsPath, tlschemaDtsPath]) {
29
+ if (!fs.existsSync(p)) {
30
+ console.error(`Missing: ${p}\nRun 'yarn lazy build' first to generate .d.ts files.`)
31
+ process.exit(1)
32
+ }
33
+ }
34
+
35
+ // --- Types ---
36
+
37
+ interface ExtractedParam {
38
+ name: string
39
+ description: string
40
+ }
41
+
42
+ interface ExtractedMember {
43
+ name: string
44
+ kind: 'method' | 'property' | 'getter'
45
+ signature: string
46
+ description: string
47
+ params: ExtractedParam[]
48
+ examples: string[]
49
+ category: string
50
+ }
51
+
52
+ interface ExtractedTypeProperty {
53
+ name: string
54
+ signature: string
55
+ description: string
56
+ optional: boolean
57
+ }
58
+
59
+ interface ExtractedShapeType {
60
+ name: string
61
+ shapeType: string
62
+ signature: string
63
+ description: string
64
+ propsType: string
65
+ propsDescription: string
66
+ props: ExtractedTypeProperty[]
67
+ }
68
+
69
+ interface ExtractedTypesSection {
70
+ shapeTypes: string[]
71
+ shapes: ExtractedShapeType[]
72
+ }
73
+
74
+ interface ExtractedTypeMember {
75
+ name: string
76
+ kind: 'method' | 'property' | 'getter'
77
+ signature: string
78
+ description: string
79
+ params: ExtractedParam[]
80
+ examples: string[]
81
+ optional: boolean
82
+ static: boolean
83
+ }
84
+
85
+ interface ExtractedNamedType {
86
+ name: string
87
+ kind: 'class' | 'interface' | 'type' | 'function' | 'const'
88
+ signature: string
89
+ description: string
90
+ params?: ExtractedParam[]
91
+ examples?: string[]
92
+ members?: ExtractedTypeMember[]
93
+ aliasedTo?: string
94
+ resolvedType?: ExtractedNamedType
95
+ relatedTypes?: ExtractedNamedType[]
96
+ }
97
+
98
+ interface ExtractedExecHelper {
99
+ name: string
100
+ source: 'local' | 'tldraw'
101
+ origin: string
102
+ signature: string
103
+ description: string
104
+ params: ExtractedParam[]
105
+ examples: string[]
106
+ typeInfo?: ExtractedNamedType
107
+ }
108
+
109
+ interface ExtractedExecSection {
110
+ helperCount: number
111
+ helpers: ExtractedExecHelper[]
112
+ }
113
+
114
+ type NamedDeclaration =
115
+ | ts.ClassDeclaration
116
+ | ts.InterfaceDeclaration
117
+ | ts.TypeAliasDeclaration
118
+ | ts.FunctionDeclaration
119
+ | ts.VariableDeclaration
120
+
121
+ interface DeclarationContext {
122
+ program: ts.Program
123
+ checker: ts.TypeChecker
124
+ declarations: Map<string, NamedDeclaration>
125
+ sourceFiles: Map<string, ts.SourceFile>
126
+ }
127
+
128
+ // --- Helpers ---
129
+
130
+ function categorize(name: string): string {
131
+ if (/camera/i.test(name)) return 'camera'
132
+ if (/viewport|screenToPage|pageToScreen|pageToViewport|viewportToPage/i.test(name))
133
+ return 'viewport'
134
+ if (/^(get|set|create|update|delete|reorder|reparent).*shape/i.test(name)) return 'shapes'
135
+ if (/^(get|has)Shape/i.test(name)) return 'shapes'
136
+ if (/shapeUtil/i.test(name)) return 'shapes'
137
+ if (/^(select|deselect|getSelect|setSelect|clearSelect|getSelectedShape)/i.test(name))
138
+ return 'selection'
139
+ if (/selected/i.test(name)) return 'selection'
140
+ if (/^(get|set|create|delete|move|reorder|duplicate).*page/i.test(name)) return 'pages'
141
+ if (/^(undo|redo|mark|bail|squash|run$|history|batch)/i.test(name)) return 'history'
142
+ if (/^(zoom|pan|stopFollowing|startFollowing|slideCamera|resetZoom|zoomTo)/i.test(name))
143
+ return 'zoom'
144
+ if (/binding/i.test(name)) return 'bindings'
145
+ if (/^(group|ungroup)/i.test(name)) return 'grouping'
146
+ if (
147
+ /^(nudge|align|distribute|stack|stretch|pack|flip|rotate|resize|moveShapes|translate)/i.test(
148
+ name
149
+ )
150
+ )
151
+ return 'transform'
152
+ if (/^(isIn|getPath|setCurrentTool|getCurrentTool)/i.test(name)) return 'tools'
153
+ if (/asset/i.test(name)) return 'assets'
154
+ if (/style|opacity|color|font/i.test(name)) return 'styles'
155
+ if (/^(get|set).*Hinting/i.test(name)) return 'hinting'
156
+ if (/^(get|set).*Erasing/i.test(name)) return 'erasing'
157
+ if (/^(get|set).*Cropping/i.test(name)) return 'cropping'
158
+ if (/^(get|set).*Editing/i.test(name)) return 'editing'
159
+ if (/^(get|set).*Hovering/i.test(name)) return 'hovering'
160
+ if (/^(get|set).*Focus/i.test(name)) return 'focus'
161
+ if (/^(get|set).*Dragging/i.test(name)) return 'dragging'
162
+ if (/snap/i.test(name)) return 'snapping'
163
+ if (/export|toImage|toSvg|toBlobPromise/i.test(name)) return 'export'
164
+ if (/cursor/i.test(name)) return 'cursor'
165
+ if (/instance/i.test(name)) return 'instance'
166
+ if (/store/i.test(name)) return 'store'
167
+ if (/^(dispose|isDisposed)/i.test(name)) return 'lifecycle'
168
+ return 'other'
169
+ }
170
+
171
+ function extractJsDoc(
172
+ member: ts.Node,
173
+ sourceFile: ts.SourceFile
174
+ ): { description: string; params: ExtractedParam[]; examples: string[] } {
175
+ const empty = { description: '', params: [], examples: [] }
176
+
177
+ const ranges = ts.getLeadingCommentRanges(sourceFile.text, member.getFullStart())
178
+ if (!ranges) return empty
179
+
180
+ const jsdocRanges = ranges.filter((r) => sourceFile.text.slice(r.pos, r.pos + 3) === '/**')
181
+ const jsdocRange = jsdocRanges[jsdocRanges.length - 1]
182
+ if (!jsdocRange) return empty
183
+
184
+ const raw = sourceFile.text.slice(jsdocRange.pos, jsdocRange.end)
185
+
186
+ if (raw.includes('Excluded from this release type')) {
187
+ return { description: '__EXCLUDED__', params: [], examples: [] }
188
+ }
189
+
190
+ const lines = raw
191
+ .replace(/^\/\*\*\s*/, '')
192
+ .replace(/\s*\*\/$/, '')
193
+ .split('\n')
194
+ .map((l) => l.replace(/^\s*\*\s?/, ''))
195
+
196
+ const descLines: string[] = []
197
+ const tags: Array<{ tag: string; text: string }> = []
198
+
199
+ for (const line of lines) {
200
+ const tagMatch = line.match(/^@(\w+)\s*(.*)/)
201
+ if (tagMatch) {
202
+ tags.push({ tag: tagMatch[1], text: tagMatch[2] })
203
+ } else if (tags.length > 0) {
204
+ tags[tags.length - 1].text += '\n' + line
205
+ } else {
206
+ descLines.push(line)
207
+ }
208
+ }
209
+
210
+ const description = descLines.join('\n').trim()
211
+ const params = tags
212
+ .filter((t) => t.tag === 'param')
213
+ .map((t) => {
214
+ const m = t.text.match(/^(\w+)\s*-\s*(.*)/)
215
+ return m ? { name: m[1], description: m[2].trim() } : null
216
+ })
217
+ .filter((p): p is ExtractedParam => p !== null)
218
+
219
+ const examples = tags
220
+ .filter((t) => t.tag === 'example')
221
+ .map((t) =>
222
+ t.text
223
+ .replace(/```ts\n?/g, '')
224
+ .replace(/```\n?/g, '')
225
+ .trim()
226
+ )
227
+ .filter((e) => e.length > 0)
228
+
229
+ return { description, params, examples }
230
+ }
231
+
232
+ function getPropertyName(name: ts.PropertyName | ts.BindingName | undefined): string | undefined {
233
+ if (!name) return undefined
234
+ if (ts.isIdentifier(name) || ts.isStringLiteral(name) || ts.isNumericLiteral(name)) {
235
+ return name.text
236
+ }
237
+ return undefined
238
+ }
239
+
240
+ function isExcludedComment(member: ts.Node, sourceFile: ts.SourceFile): boolean {
241
+ const ranges = ts.getLeadingCommentRanges(sourceFile.text, member.getFullStart())
242
+ if (!ranges) return false
243
+ const lastRange = ranges[ranges.length - 1]
244
+ return sourceFile.text
245
+ .slice(lastRange.pos, lastRange.end)
246
+ .includes('Excluded from this release type')
247
+ }
248
+
249
+ // --- Declaration context ---
250
+
251
+ function createDeclarationContext(entryPaths: string[]): DeclarationContext {
252
+ const program = ts.createProgram(entryPaths, {
253
+ target: ts.ScriptTarget.ES2020,
254
+ jsx: ts.JsxEmit.ReactJSX,
255
+ moduleResolution: ts.ModuleResolutionKind.Node10,
256
+ })
257
+ const declarations = new Map<string, NamedDeclaration>()
258
+ const sourceFiles = new Map<string, ts.SourceFile>()
259
+ const indexedSourceFiles = new Set(entryPaths.map((entryPath) => path.resolve(entryPath)))
260
+
261
+ for (const sourceFile of program.getSourceFiles()) {
262
+ sourceFiles.set(sourceFile.fileName, sourceFile)
263
+ const shouldIndex =
264
+ indexedSourceFiles.has(path.resolve(sourceFile.fileName)) ||
265
+ sourceFile.fileName.includes('/packages/') ||
266
+ sourceFile.fileName.includes('/node_modules/@tldraw/') ||
267
+ sourceFile.fileName.includes('/node_modules/tldraw/')
268
+ if (!shouldIndex) continue
269
+
270
+ ts.forEachChild(sourceFile, (node) => {
271
+ if (
272
+ (ts.isClassDeclaration(node) ||
273
+ ts.isInterfaceDeclaration(node) ||
274
+ ts.isTypeAliasDeclaration(node) ||
275
+ ts.isFunctionDeclaration(node)) &&
276
+ node.name
277
+ ) {
278
+ declarations.set(node.name.text, node)
279
+ return
280
+ }
281
+
282
+ if (ts.isVariableStatement(node)) {
283
+ for (const declaration of node.declarationList.declarations) {
284
+ if (ts.isIdentifier(declaration.name)) {
285
+ declarations.set(declaration.name.text, declaration)
286
+ }
287
+ }
288
+ }
289
+ })
290
+ }
291
+
292
+ return {
293
+ program,
294
+ checker: program.getTypeChecker(),
295
+ declarations,
296
+ sourceFiles,
297
+ }
298
+ }
299
+
300
+ function getDeclarationSourceFile(declaration: NamedDeclaration): ts.SourceFile {
301
+ return declaration.getSourceFile()
302
+ }
303
+
304
+ function getDeclarationKind(declaration: NamedDeclaration): ExtractedNamedType['kind'] {
305
+ if (ts.isClassDeclaration(declaration)) return 'class'
306
+ if (ts.isInterfaceDeclaration(declaration)) return 'interface'
307
+ if (ts.isFunctionDeclaration(declaration)) return 'function'
308
+ if (ts.isVariableDeclaration(declaration)) return 'const'
309
+ return 'type'
310
+ }
311
+
312
+ function getDeclarationSignature(
313
+ declaration: NamedDeclaration,
314
+ sourceFile: ts.SourceFile,
315
+ checker: ts.TypeChecker
316
+ ): string {
317
+ if (ts.isTypeAliasDeclaration(declaration)) {
318
+ return declaration.type.getText(sourceFile)
319
+ }
320
+
321
+ if (ts.isFunctionDeclaration(declaration)) {
322
+ try {
323
+ const signature = checker.getSignatureFromDeclaration(declaration)
324
+ if (signature) {
325
+ return checker.signatureToString(
326
+ signature,
327
+ declaration,
328
+ ts.TypeFormatFlags.NoTruncation | ts.TypeFormatFlags.WriteArrowStyleSignature
329
+ )
330
+ }
331
+ return checker.typeToString(
332
+ checker.getTypeAtLocation(declaration),
333
+ declaration,
334
+ ts.TypeFormatFlags.NoTruncation | ts.TypeFormatFlags.WriteArrowStyleSignature
335
+ )
336
+ } catch {
337
+ return '(unknown)'
338
+ }
339
+ }
340
+
341
+ if (ts.isVariableDeclaration(declaration)) {
342
+ try {
343
+ if (declaration.type) return declaration.type.getText(sourceFile)
344
+ return checker.typeToString(
345
+ checker.getTypeAtLocation(declaration),
346
+ declaration,
347
+ ts.TypeFormatFlags.NoTruncation | ts.TypeFormatFlags.WriteArrowStyleSignature
348
+ )
349
+ } catch {
350
+ return '(unknown)'
351
+ }
352
+ }
353
+
354
+ const heritage = declaration.heritageClauses
355
+ ?.map((clause) => clause.getText(sourceFile))
356
+ .filter(Boolean)
357
+ .join(' ')
358
+
359
+ if (ts.isClassDeclaration(declaration)) {
360
+ const abstractPrefix = declaration.modifiers?.some(
361
+ (modifier) => modifier.kind === ts.SyntaxKind.AbstractKeyword
362
+ )
363
+ ? 'abstract '
364
+ : ''
365
+ return `${abstractPrefix}class ${declaration.name?.text ?? '(anonymous)'}${
366
+ heritage ? ` ${heritage}` : ''
367
+ }`
368
+ }
369
+
370
+ return `interface ${declaration.name.text}${heritage ? ` ${heritage}` : ''}`
371
+ }
372
+
373
+ function getMemberSignature(
374
+ member: ts.ClassElement | ts.TypeElement,
375
+ sourceFile: ts.SourceFile,
376
+ checker: ts.TypeChecker
377
+ ): string {
378
+ let signature = '(unknown)'
379
+ try {
380
+ if (
381
+ (ts.isPropertyDeclaration(member) ||
382
+ ts.isPropertySignature(member) ||
383
+ ts.isMethodDeclaration(member) ||
384
+ ts.isMethodSignature(member)) &&
385
+ member.type
386
+ ) {
387
+ signature = member.type.getText(sourceFile)
388
+ } else {
389
+ signature = checker.typeToString(
390
+ checker.getTypeAtLocation(member),
391
+ member,
392
+ ts.TypeFormatFlags.NoTruncation | ts.TypeFormatFlags.WriteArrowStyleSignature
393
+ )
394
+ }
395
+ } catch {
396
+ // keep fallback
397
+ }
398
+ return signature
399
+ }
400
+
401
+ function extractTypeMembers(
402
+ declaration: ts.ClassDeclaration | ts.InterfaceDeclaration,
403
+ context: DeclarationContext
404
+ ): ExtractedTypeMember[] {
405
+ const sourceFile = getDeclarationSourceFile(declaration)
406
+ const members: ExtractedTypeMember[] = []
407
+
408
+ for (const member of declaration.members) {
409
+ if (isExcludedComment(member, sourceFile)) continue
410
+
411
+ if (ts.isClassDeclaration(declaration)) {
412
+ const modifiers = ts.canHaveModifiers(member) ? ts.getModifiers(member) : undefined
413
+ if (
414
+ modifiers?.some(
415
+ (modifier) =>
416
+ modifier.kind === ts.SyntaxKind.PrivateKeyword ||
417
+ modifier.kind === ts.SyntaxKind.ProtectedKeyword
418
+ )
419
+ ) {
420
+ continue
421
+ }
422
+ }
423
+
424
+ if (ts.isConstructorDeclaration(member)) continue
425
+
426
+ const name = 'name' in member ? getPropertyName(member.name) : undefined
427
+ if (!name || name.startsWith('_')) continue
428
+
429
+ let kind: ExtractedTypeMember['kind']
430
+ if (ts.isMethodDeclaration(member) || ts.isMethodSignature(member)) {
431
+ kind = 'method'
432
+ } else if (ts.isGetAccessorDeclaration(member) || ts.isGetAccessor(member)) {
433
+ kind = 'getter'
434
+ } else if (ts.isPropertyDeclaration(member) || ts.isPropertySignature(member)) {
435
+ kind = 'property'
436
+ } else {
437
+ continue
438
+ }
439
+
440
+ const jsdoc = extractJsDoc(member, sourceFile)
441
+ if (jsdoc.description === '__EXCLUDED__') continue
442
+
443
+ members.push({
444
+ name,
445
+ kind,
446
+ signature: getMemberSignature(member, sourceFile, context.checker),
447
+ description: jsdoc.description,
448
+ params: jsdoc.params,
449
+ examples: jsdoc.examples,
450
+ optional: 'questionToken' in member && !!member.questionToken,
451
+ static:
452
+ ts.isClassDeclaration(declaration) &&
453
+ (ts.canHaveModifiers(member) ? ts.getModifiers(member) : undefined)?.some(
454
+ (modifier) => modifier.kind === ts.SyntaxKind.StaticKeyword
455
+ ) === true,
456
+ })
457
+ }
458
+
459
+ return members
460
+ }
461
+
462
+ function extractNamedType(
463
+ name: string,
464
+ context: DeclarationContext,
465
+ visited = new Set<string>()
466
+ ): ExtractedNamedType | undefined {
467
+ if (visited.has(name)) return undefined
468
+ const declaration = context.declarations.get(name)
469
+ if (!declaration) return undefined
470
+
471
+ visited.add(name)
472
+
473
+ const sourceFile = getDeclarationSourceFile(declaration)
474
+ const result: ExtractedNamedType = {
475
+ name,
476
+ kind: getDeclarationKind(declaration),
477
+ signature: getDeclarationSignature(declaration, sourceFile, context.checker),
478
+ description: extractJsDoc(declaration, sourceFile).description,
479
+ params: extractJsDoc(declaration, sourceFile).params,
480
+ examples: extractJsDoc(declaration, sourceFile).examples,
481
+ }
482
+
483
+ if (ts.isClassDeclaration(declaration) || ts.isInterfaceDeclaration(declaration)) {
484
+ result.members = extractTypeMembers(declaration, context)
485
+ return result
486
+ }
487
+
488
+ if (ts.isFunctionDeclaration(declaration) || ts.isVariableDeclaration(declaration)) {
489
+ return result
490
+ }
491
+
492
+ result.aliasedTo = declaration.type.getText(sourceFile)
493
+
494
+ if (ts.isTypeReferenceNode(declaration.type)) {
495
+ const baseTypeName = declaration.type.typeName.getText(sourceFile)
496
+ if (baseTypeName !== name) {
497
+ result.resolvedType = extractNamedType(baseTypeName, context, visited)
498
+ }
499
+
500
+ const relatedTypeNames = (declaration.type.typeArguments ?? [])
501
+ .filter((arg): arg is ts.TypeReferenceNode => ts.isTypeReferenceNode(arg))
502
+ .map((arg) => arg.getText(sourceFile))
503
+ .filter((typeName) => {
504
+ if (typeName === baseTypeName) return false
505
+ const relatedDeclaration = context.declarations.get(typeName)
506
+ return !!relatedDeclaration && ts.isInterfaceDeclaration(relatedDeclaration)
507
+ })
508
+
509
+ const relatedTypes = relatedTypeNames
510
+ .map((typeName) => extractNamedType(typeName, context, visited))
511
+ .filter((type): type is ExtractedNamedType => type !== undefined)
512
+
513
+ if (relatedTypes.length > 0) {
514
+ result.relatedTypes = relatedTypes
515
+ }
516
+ }
517
+
518
+ return result
519
+ }
520
+
521
+ function getSymbolDeclaration(
522
+ symbol: ts.Symbol | undefined,
523
+ checker: ts.TypeChecker
524
+ ): NamedDeclaration | undefined {
525
+ if (!symbol) return undefined
526
+ const resolvedSymbol =
527
+ symbol.flags & ts.SymbolFlags.Alias ? checker.getAliasedSymbol(symbol) : symbol
528
+ const declaration = resolvedSymbol.declarations?.find(
529
+ (declaration): declaration is NamedDeclaration =>
530
+ ts.isClassDeclaration(declaration) ||
531
+ ts.isInterfaceDeclaration(declaration) ||
532
+ ts.isTypeAliasDeclaration(declaration) ||
533
+ ts.isFunctionDeclaration(declaration) ||
534
+ ts.isVariableDeclaration(declaration)
535
+ )
536
+ return declaration
537
+ }
538
+
539
+ function extractNamedTypeFromDeclaration(
540
+ declaration: NamedDeclaration,
541
+ context: DeclarationContext
542
+ ): ExtractedNamedType | undefined {
543
+ const name = ts.isVariableDeclaration(declaration)
544
+ ? ts.isIdentifier(declaration.name)
545
+ ? declaration.name.text
546
+ : undefined
547
+ : declaration.name?.text
548
+ if (!name) return undefined
549
+ if (!context.declarations.has(name)) {
550
+ context.declarations.set(name, declaration)
551
+ }
552
+ return extractNamedType(name, context)
553
+ }
554
+
555
+ function findNode<T extends ts.Node>(
556
+ root: ts.Node,
557
+ predicate: (node: ts.Node) => node is T
558
+ ): T | undefined {
559
+ let result: T | undefined
560
+ const visit = (node: ts.Node) => {
561
+ if (result) return
562
+ if (predicate(node)) {
563
+ result = node
564
+ return
565
+ }
566
+ ts.forEachChild(node, visit)
567
+ }
568
+ visit(root)
569
+ return result
570
+ }
571
+
572
+ // --- Extract exec helpers from exec-helpers.ts ---
573
+
574
+ function extractExecHelpers(): ExtractedExecSection {
575
+ const context = createDeclarationContext([
576
+ execHelpersPath,
577
+ editorDtsPath,
578
+ storeDtsPath,
579
+ tlschemaDtsPath,
580
+ ])
581
+ const sourceFile = context.sourceFiles.get(execHelpersPath)
582
+ if (!sourceFile) {
583
+ throw new Error(`Could not load source file: ${execHelpersPath}`)
584
+ }
585
+
586
+ // Find the helpers object inside createExecHelpers function
587
+ const helpersDeclaration = findNode(
588
+ sourceFile,
589
+ (node): node is ts.VariableDeclaration =>
590
+ ts.isVariableDeclaration(node) &&
591
+ ts.isIdentifier(node.name) &&
592
+ node.name.text === 'helpers' &&
593
+ !!node.initializer &&
594
+ ts.isObjectLiteralExpression(node.initializer)
595
+ )
596
+
597
+ if (
598
+ !helpersDeclaration ||
599
+ !helpersDeclaration.initializer ||
600
+ !ts.isObjectLiteralExpression(helpersDeclaration.initializer)
601
+ ) {
602
+ throw new Error('Could not find helpers object in exec-helpers.ts')
603
+ }
604
+
605
+ // Collect tldraw imports
606
+ const tldrawImports = new Map<string, string>()
607
+ for (const statement of sourceFile.statements) {
608
+ if (!ts.isImportDeclaration(statement)) continue
609
+ if (
610
+ !ts.isStringLiteral(statement.moduleSpecifier) ||
611
+ statement.moduleSpecifier.text !== 'tldraw'
612
+ )
613
+ continue
614
+ if (
615
+ !statement.importClause?.namedBindings ||
616
+ !ts.isNamedImports(statement.importClause.namedBindings)
617
+ ) {
618
+ continue
619
+ }
620
+
621
+ for (const element of statement.importClause.namedBindings.elements) {
622
+ const localName = element.name.text
623
+ const importedName = element.propertyName?.text ?? localName
624
+ tldrawImports.set(localName, importedName)
625
+ }
626
+ }
627
+
628
+ const helpers: ExtractedExecHelper[] = []
629
+
630
+ for (const property of helpersDeclaration.initializer.properties) {
631
+ if (!ts.isPropertyAssignment(property) && !ts.isShorthandPropertyAssignment(property)) continue
632
+
633
+ const helperName = getPropertyName(property.name)
634
+ if (!helperName) continue
635
+
636
+ const initializer = ts.isPropertyAssignment(property) ? property.initializer : property.name
637
+ let typeInfo: ExtractedNamedType | undefined
638
+ let source: ExtractedExecHelper['source'] = 'local'
639
+ let origin = helperName
640
+
641
+ if (ts.isIdentifier(initializer)) {
642
+ const importedName = tldrawImports.get(initializer.text)
643
+ if (importedName) {
644
+ source = 'tldraw'
645
+ origin = importedName
646
+ typeInfo = extractNamedType(importedName, context)
647
+ }
648
+
649
+ if (!typeInfo) {
650
+ const symbol = context.checker.getSymbolAtLocation(initializer)
651
+ const declaration = getSymbolDeclaration(symbol, context.checker)
652
+ typeInfo = declaration ? extractNamedTypeFromDeclaration(declaration, context) : undefined
653
+ if (declaration && declaration.getSourceFile().fileName.includes('/packages/')) {
654
+ source = 'tldraw'
655
+ origin = ts.isVariableDeclaration(declaration)
656
+ ? ts.isIdentifier(declaration.name)
657
+ ? declaration.name.text
658
+ : 'tldraw'
659
+ : (declaration.name?.text ?? 'tldraw')
660
+ }
661
+ }
662
+ } else if (ts.isCallExpression(initializer) && ts.isIdentifier(initializer.expression)) {
663
+ // Factory function pattern: someFn(editor) — resolve the inner return type
664
+ const factoryName = initializer.expression.text
665
+ const declaration = context.declarations.get(factoryName)
666
+ if (declaration && ts.isFunctionDeclaration(declaration)) {
667
+ const returnStatement = findNode(
668
+ declaration,
669
+ (node): node is ts.ReturnStatement =>
670
+ ts.isReturnStatement(node) && !!node.expression && ts.isArrowFunction(node.expression)
671
+ )
672
+ if (returnStatement?.expression && ts.isArrowFunction(returnStatement.expression)) {
673
+ const returnJsDoc = extractJsDoc(returnStatement, sourceFile)
674
+ const declarationJsDoc = extractJsDoc(declaration, sourceFile)
675
+ typeInfo = {
676
+ name: helperName,
677
+ kind: 'function',
678
+ signature: context.checker.typeToString(
679
+ context.checker.getTypeAtLocation(returnStatement.expression),
680
+ returnStatement.expression,
681
+ ts.TypeFormatFlags.NoTruncation | ts.TypeFormatFlags.WriteArrowStyleSignature
682
+ ),
683
+ description: returnJsDoc.description || declarationJsDoc.description,
684
+ params: returnJsDoc.params.length > 0 ? returnJsDoc.params : declarationJsDoc.params,
685
+ examples:
686
+ returnJsDoc.examples.length > 0 ? returnJsDoc.examples : declarationJsDoc.examples,
687
+ }
688
+ }
689
+ }
690
+ source = 'local'
691
+ origin = helperName
692
+ }
693
+
694
+ helpers.push({
695
+ name: helperName,
696
+ source,
697
+ origin,
698
+ signature: typeInfo?.signature ?? '(unknown)',
699
+ description: typeInfo?.description ?? '',
700
+ params: typeInfo?.params ?? [],
701
+ examples: typeInfo?.examples ?? [],
702
+ typeInfo,
703
+ })
704
+ }
705
+
706
+ return {
707
+ helperCount: helpers.length,
708
+ helpers,
709
+ }
710
+ }
711
+
712
+ // --- Extract Editor members ---
713
+
714
+ function extract(): ExtractedMember[] {
715
+ const program = ts.createProgram([editorDtsPath, storeDtsPath, tlschemaDtsPath], {
716
+ target: ts.ScriptTarget.ES2020,
717
+ moduleResolution: ts.ModuleResolutionKind.Node10,
718
+ })
719
+ const checker = program.getTypeChecker()
720
+ const sourceFile = program.getSourceFile(editorDtsPath)
721
+ if (!sourceFile) {
722
+ throw new Error(`Could not load source file: ${editorDtsPath}`)
723
+ }
724
+
725
+ let editorClass: ts.ClassDeclaration | undefined
726
+ ts.forEachChild(sourceFile, (node) => {
727
+ if (ts.isClassDeclaration(node) && node.name?.text === 'Editor') {
728
+ editorClass = node
729
+ }
730
+ })
731
+
732
+ if (!editorClass) {
733
+ throw new Error('Could not find Editor class in .d.ts file')
734
+ }
735
+
736
+ const members: ExtractedMember[] = []
737
+
738
+ for (const member of editorClass.members) {
739
+ const modifiers = ts.canHaveModifiers(member) ? ts.getModifiers(member) : undefined
740
+ if (
741
+ modifiers?.some(
742
+ (m) => m.kind === ts.SyntaxKind.PrivateKeyword || m.kind === ts.SyntaxKind.ProtectedKeyword
743
+ )
744
+ ) {
745
+ continue
746
+ }
747
+
748
+ if (isExcludedComment(member, sourceFile)) continue
749
+
750
+ const name = member.name && ts.isIdentifier(member.name) ? member.name.text : undefined
751
+ if (!name) continue
752
+ if (name.startsWith('_')) continue
753
+ if (ts.isConstructorDeclaration(member)) continue
754
+
755
+ let kind: 'method' | 'property' | 'getter'
756
+ if (ts.isMethodDeclaration(member) || ts.isMethodSignature(member)) {
757
+ kind = 'method'
758
+ } else if (ts.isGetAccessorDeclaration(member) || ts.isGetAccessor(member)) {
759
+ kind = 'getter'
760
+ } else if (ts.isPropertyDeclaration(member) || ts.isPropertySignature(member)) {
761
+ kind = 'property'
762
+ } else {
763
+ continue
764
+ }
765
+
766
+ const type = checker.getTypeAtLocation(member)
767
+ let signature: string
768
+ try {
769
+ signature = checker.typeToString(
770
+ type,
771
+ member,
772
+ ts.TypeFormatFlags.NoTruncation | ts.TypeFormatFlags.WriteArrowStyleSignature
773
+ )
774
+ } catch {
775
+ signature = '(unknown)'
776
+ }
777
+
778
+ const jsdoc = extractJsDoc(member, sourceFile)
779
+ if (jsdoc.description === '__EXCLUDED__') continue
780
+
781
+ members.push({
782
+ name,
783
+ kind,
784
+ signature,
785
+ description: jsdoc.description,
786
+ params: jsdoc.params,
787
+ examples: jsdoc.examples,
788
+ category: categorize(name),
789
+ })
790
+ }
791
+
792
+ return members
793
+ }
794
+
795
+ // --- Extract focused shape types from format.ts ---
796
+
797
+ const FOCUSED_SHAPE_INTERFACES = [
798
+ 'FocusedGeoShape',
799
+ 'FocusedTextShape',
800
+ 'FocusedArrowShape',
801
+ 'FocusedLineShape',
802
+ 'FocusedNoteShape',
803
+ 'FocusedDrawShape',
804
+ ]
805
+
806
+ function toPascalCase(value: string) {
807
+ return value
808
+ .split(/[^a-zA-Z0-9]+/)
809
+ .filter(Boolean)
810
+ .map((part) => part[0].toUpperCase() + part.slice(1))
811
+ .join('')
812
+ }
813
+
814
+ function extractFocusedShapeTypes(): ExtractedTypesSection {
815
+ const context = createDeclarationContext([formatTsPath, editorDtsPath, tlschemaDtsPath])
816
+ const sourceFile = context.sourceFiles.get(formatTsPath)
817
+ if (!sourceFile) {
818
+ throw new Error(`Could not load source file: ${formatTsPath}`)
819
+ }
820
+
821
+ const allShapeTypes: string[] = []
822
+ const shapes: ExtractedShapeType[] = []
823
+
824
+ for (const ifaceName of FOCUSED_SHAPE_INTERFACES) {
825
+ const declaration = context.declarations.get(ifaceName)
826
+ if (!declaration || !ts.isInterfaceDeclaration(declaration)) {
827
+ console.error(`Warning: could not find interface ${ifaceName} in format.ts`)
828
+ continue
829
+ }
830
+
831
+ const ifaceSourceFile = declaration.getSourceFile()
832
+ const jsdoc = extractJsDoc(declaration, ifaceSourceFile)
833
+
834
+ const props: ExtractedTypeProperty[] = []
835
+ let shapeType = ''
836
+ const unionShapeTypes: string[] = []
837
+
838
+ for (const member of declaration.members) {
839
+ if (!ts.isPropertySignature(member) && !ts.isPropertyDeclaration(member)) continue
840
+ const propName = getPropertyName(member.name)
841
+ if (!propName) continue
842
+
843
+ let signature = '(unknown)'
844
+ try {
845
+ const memberType = context.checker.getTypeAtLocation(member)
846
+ signature = context.checker.typeToString(
847
+ memberType,
848
+ member,
849
+ ts.TypeFormatFlags.NoTruncation | ts.TypeFormatFlags.InTypeAlias
850
+ )
851
+ } catch {
852
+ if (member.type) signature = member.type.getText(ifaceSourceFile)
853
+ }
854
+
855
+ const propJsdoc = extractJsDoc(member, ifaceSourceFile)
856
+
857
+ if (propName === '_type') {
858
+ const memberType = context.checker.getTypeAtLocation(member)
859
+ if (memberType.isStringLiteral()) {
860
+ shapeType = memberType.value
861
+ } else if (memberType.isUnion()) {
862
+ for (const t of memberType.types) {
863
+ if (t.isStringLiteral()) {
864
+ allShapeTypes.push(t.value)
865
+ unionShapeTypes.push(t.value)
866
+ }
867
+ }
868
+ }
869
+ }
870
+
871
+ props.push({
872
+ name: propName,
873
+ signature,
874
+ description: propJsdoc.description,
875
+ optional: !!member.questionToken,
876
+ })
877
+ }
878
+
879
+ if (shapeType && shapeType !== 'geo') {
880
+ allShapeTypes.push(shapeType)
881
+ }
882
+
883
+ const displayName = ifaceName.replace(/^Focused/, '')
884
+ const propNames = props.map((p) => p.name).join(', ')
885
+ const signature = `{ ${propNames} }`
886
+
887
+ if (unionShapeTypes.length > 0) {
888
+ for (const concreteShapeType of unionShapeTypes) {
889
+ const concreteName = `${toPascalCase(concreteShapeType)}Shape`
890
+ shapes.push({
891
+ name: concreteName,
892
+ shapeType: concreteShapeType,
893
+ signature,
894
+ description: jsdoc.description,
895
+ propsType: `${concreteName}Props`,
896
+ propsDescription: jsdoc.description,
897
+ props: props.map((prop) =>
898
+ prop.name === '_type'
899
+ ? {
900
+ ...prop,
901
+ signature: `"${concreteShapeType}"`,
902
+ }
903
+ : prop
904
+ ),
905
+ })
906
+ }
907
+ continue
908
+ }
909
+
910
+ shapes.push({
911
+ name: displayName,
912
+ shapeType,
913
+ signature,
914
+ description: jsdoc.description,
915
+ propsType: `${displayName}Props`,
916
+ propsDescription: jsdoc.description,
917
+ props,
918
+ })
919
+ }
920
+
921
+ return {
922
+ shapeTypes: allShapeTypes,
923
+ shapes,
924
+ }
925
+ }
926
+
927
+ // --- Generate METHOD_MAP ---
928
+
929
+ type ArgKind =
930
+ | 'id'
931
+ | 'id-or-shape'
932
+ | 'ids-or-shapes'
933
+ | 'spread-ids'
934
+ | 'shape-partial'
935
+ | 'shape-partials'
936
+ | 'update-partial'
937
+ | 'update-partials'
938
+ type RetKind =
939
+ | 'this'
940
+ | 'shape'
941
+ | 'shape-or-null'
942
+ | 'shapes'
943
+ | 'id'
944
+ | 'id-or-null'
945
+ | 'ids'
946
+ | 'id-set'
947
+
948
+ interface MethodMapEntry {
949
+ args: ArgKind[]
950
+ ret: RetKind
951
+ }
952
+
953
+ function generateMethodMap(
954
+ editorClass: ts.ClassDeclaration,
955
+ context: DeclarationContext
956
+ ): Record<string, MethodMapEntry> {
957
+ const map: Record<string, MethodMapEntry> = {}
958
+
959
+ for (const member of editorClass.members) {
960
+ const modifiers = ts.canHaveModifiers(member) ? ts.getModifiers(member) : undefined
961
+ if (
962
+ modifiers?.some(
963
+ (m) => m.kind === ts.SyntaxKind.PrivateKeyword || m.kind === ts.SyntaxKind.ProtectedKeyword
964
+ )
965
+ )
966
+ continue
967
+
968
+ const name = member.name && ts.isIdentifier(member.name) ? member.name.text : undefined
969
+ if (!name || name.startsWith('_')) continue
970
+
971
+ const memberType = context.checker.getTypeAtLocation(member)
972
+ const signatures = memberType.getCallSignatures()
973
+ if (signatures.length === 0) continue
974
+
975
+ const args: ArgKind[] = []
976
+ let ret: RetKind | null = null
977
+
978
+ for (const sig of signatures) {
979
+ for (let i = 0; i < sig.parameters.length; i++) {
980
+ const param = sig.parameters[i]
981
+ const paramType = context.checker.getTypeOfSymbolAtLocation(param, member)
982
+ const paramStr = context.checker.typeToString(
983
+ paramType,
984
+ member,
985
+ ts.TypeFormatFlags.NoTruncation
986
+ )
987
+ const isRest = !!(
988
+ param.declarations?.[0] &&
989
+ ts.isParameter(param.declarations[0]) &&
990
+ param.declarations[0].dotDotDotToken
991
+ )
992
+
993
+ if (args[i]) continue
994
+
995
+ const argKind = classifyParamType(paramStr, isRest)
996
+ if (argKind) {
997
+ while (args.length < i) args.push('id')
998
+ args[i] = argKind
999
+ }
1000
+ }
1001
+
1002
+ if (!ret) {
1003
+ const retType = sig.getReturnType()
1004
+ const retStr = context.checker.typeToString(
1005
+ retType,
1006
+ member,
1007
+ ts.TypeFormatFlags.NoTruncation
1008
+ )
1009
+ ret = classifyReturnType(retType, retStr, context.checker, member)
1010
+ }
1011
+ }
1012
+
1013
+ if (args.length > 0 || ret) {
1014
+ map[name] = { args, ret: ret ?? 'this' }
1015
+ }
1016
+ }
1017
+
1018
+ return map
1019
+ }
1020
+
1021
+ function classifyParamType(typeStr: string, isRest: boolean): ArgKind | null {
1022
+ if (typeStr.includes('TLCreateShapePartial')) {
1023
+ return typeStr.includes('[]') ? 'shape-partials' : 'shape-partial'
1024
+ }
1025
+ if (typeStr.includes('TLShapePartial')) {
1026
+ if (typeStr.includes('[]') || typeStr.includes('Array')) return 'update-partials'
1027
+ return 'update-partial'
1028
+ }
1029
+ if (isRest && (typeStr.includes('TLShapeId') || typeStr.includes('TLShape'))) {
1030
+ return 'spread-ids'
1031
+ }
1032
+ if (
1033
+ (typeStr.includes('TLShape[]') || typeStr.includes('TLShapeId[]')) &&
1034
+ typeStr.includes('[]')
1035
+ ) {
1036
+ return 'ids-or-shapes'
1037
+ }
1038
+ if (
1039
+ typeStr.includes('TLShape') ||
1040
+ typeStr.includes('TLShapeId') ||
1041
+ typeStr.includes('TLParentId')
1042
+ ) {
1043
+ return 'id-or-shape'
1044
+ }
1045
+ return null
1046
+ }
1047
+
1048
+ function classifyReturnType(
1049
+ retType: ts.Type,
1050
+ typeStr: string,
1051
+ checker: ts.TypeChecker,
1052
+ member: ts.ClassElement
1053
+ ): RetKind | null {
1054
+ if (typeStr === 'this') return 'this'
1055
+
1056
+ if (typeStr.includes('Set<') && typeStr.includes('TLShapeId')) return 'id-set'
1057
+
1058
+ let resolvedStr = typeStr
1059
+ if (retType.isTypeParameter()) {
1060
+ const constraint = retType.getConstraint()
1061
+ if (constraint) {
1062
+ resolvedStr = checker.typeToString(constraint, member, ts.TypeFormatFlags.NoTruncation)
1063
+ }
1064
+ }
1065
+
1066
+ if (retType.isUnion()) {
1067
+ let hasShapeType = false
1068
+ let hasShapeIdType = false
1069
+ let hasNullish = false
1070
+ for (const t of retType.types) {
1071
+ let resolved = t
1072
+ if (t.isTypeParameter()) {
1073
+ const c = t.getConstraint()
1074
+ if (c) resolved = c
1075
+ }
1076
+ const s = checker.typeToString(resolved)
1077
+ if (s === 'undefined' || s === 'null') hasNullish = true
1078
+ else if (s.includes('TLShapeId')) hasShapeIdType = true
1079
+ else if (s.includes('Shape')) hasShapeType = true
1080
+ }
1081
+ if (hasShapeType && !hasShapeIdType) return 'shape-or-null'
1082
+ if (hasShapeIdType && hasNullish) return 'id-or-null'
1083
+ }
1084
+
1085
+ if (
1086
+ resolvedStr.includes('TLShape') &&
1087
+ !resolvedStr.includes('TLShapeId') &&
1088
+ (resolvedStr.includes('[]') || typeStr.includes('[]'))
1089
+ )
1090
+ return 'shapes'
1091
+
1092
+ if (resolvedStr.includes('TLShapeId') && resolvedStr.includes('[]')) return 'ids'
1093
+
1094
+ if (resolvedStr.includes('TLShape') && !resolvedStr.includes('TLShapeId')) {
1095
+ if (/\bTLShape\b/.test(resolvedStr)) return 'shape-or-null'
1096
+ }
1097
+
1098
+ if (
1099
+ resolvedStr.includes('TLShapeId') &&
1100
+ (resolvedStr.includes('null') || resolvedStr.includes('undefined'))
1101
+ ) {
1102
+ return 'id-or-null'
1103
+ }
1104
+
1105
+ return null
1106
+ }
1107
+
1108
+ function writeMethodMap(map: Record<string, MethodMapEntry>) {
1109
+ fs.writeFileSync(methodMapOutPath, JSON.stringify(map, null, 2))
1110
+ }
1111
+
1112
+ // --- Post-processing: signature rewrites + example conversion ---
1113
+
1114
+ /**
1115
+ * Read a Record<string, string> from an object literal in a source file,
1116
+ * unwrapping `as const` if present.
1117
+ */
1118
+ function readStringRecord(sourceFile: ts.SourceFile, varName: string): Record<string, string> {
1119
+ const entries: Record<string, string> = {}
1120
+ ts.forEachChild(sourceFile, (node) => {
1121
+ if (!ts.isVariableStatement(node)) return
1122
+ for (const decl of node.declarationList.declarations) {
1123
+ if (!ts.isIdentifier(decl.name) || decl.name.text !== varName) continue
1124
+ let init = decl.initializer
1125
+ if (init && ts.isAsExpression(init)) init = init.expression
1126
+ if (!init || !ts.isObjectLiteralExpression(init)) continue
1127
+ for (const prop of init.properties) {
1128
+ if (!ts.isPropertyAssignment(prop)) continue
1129
+ const key = getPropertyName(prop.name)
1130
+ if (!key) continue
1131
+ entries[key] = prop.initializer.getText(sourceFile).replace(/['"]/g, '')
1132
+ }
1133
+ }
1134
+ })
1135
+ return entries
1136
+ }
1137
+
1138
+ let GEO_TO_FOCUSED: Record<string, string> = {}
1139
+ let TLDRAW_TO_FOCUSED_FILL: Record<string, string> = {}
1140
+
1141
+ const INTERNAL_PROPS = new Set([
1142
+ 'typeName',
1143
+ 'rotation',
1144
+ 'index',
1145
+ 'parentId',
1146
+ 'opacity',
1147
+ 'isLocked',
1148
+ 'meta',
1149
+ 'dash',
1150
+ 'size',
1151
+ 'font',
1152
+ 'scale',
1153
+ 'growY',
1154
+ 'labelColor',
1155
+ 'url',
1156
+ 'verticalAlign',
1157
+ 'autoSize',
1158
+ 'fontSizeAdjustment',
1159
+ 'elbowMidPoint',
1160
+ 'labelPosition',
1161
+ 'arrowheadEnd',
1162
+ 'arrowheadStart',
1163
+ 'spline',
1164
+ ])
1165
+
1166
+ function convertOldFormatExample(example: string): string {
1167
+ if (!example.includes('props:') && !(example.includes('type:') && !example.includes('_type'))) {
1168
+ return example
1169
+ }
1170
+
1171
+ try {
1172
+ const wrapped = `const __ex = ${example.includes(';') ? `(() => { ${example} })()` : example}`
1173
+ const sf = ts.createSourceFile(
1174
+ 'example.ts',
1175
+ wrapped,
1176
+ ts.ScriptTarget.Latest,
1177
+ false,
1178
+ ts.ScriptKind.TS
1179
+ )
1180
+
1181
+ let result = example
1182
+ const replacements: Array<{ start: number; end: number; text: string }> = []
1183
+
1184
+ function visitNode(node: ts.Node) {
1185
+ if (ts.isObjectLiteralExpression(node)) {
1186
+ const converted = tryConvertShapeObject(node, sf)
1187
+ if (converted) {
1188
+ const prefixLen = wrapped.indexOf(example)
1189
+ const start = node.getStart(sf) - prefixLen
1190
+ const end = node.getEnd() - prefixLen
1191
+ if (start >= 0 && end <= example.length) {
1192
+ replacements.push({ start, end, text: converted })
1193
+ }
1194
+ }
1195
+ }
1196
+ ts.forEachChild(node, visitNode)
1197
+ }
1198
+
1199
+ ts.forEachChild(sf, visitNode)
1200
+
1201
+ replacements.sort((a, b) => b.start - a.start)
1202
+ for (const rep of replacements) {
1203
+ result = result.slice(0, rep.start) + rep.text + result.slice(rep.end)
1204
+ }
1205
+ return result
1206
+ } catch {
1207
+ return example
1208
+ }
1209
+ }
1210
+
1211
+ function tryConvertShapeObject(node: ts.ObjectLiteralExpression, sf: ts.SourceFile): string | null {
1212
+ const props = new Map<string, string>()
1213
+ let nestedProps: Map<string, string> | null = null
1214
+ let hasSpread = false
1215
+
1216
+ for (const prop of node.properties) {
1217
+ if (ts.isSpreadAssignment(prop)) {
1218
+ hasSpread = true
1219
+ continue
1220
+ }
1221
+ if (!ts.isPropertyAssignment(prop)) continue
1222
+ const name = getPropertyName(prop.name)
1223
+ if (!name) continue
1224
+
1225
+ if (name === 'props' && ts.isObjectLiteralExpression(prop.initializer)) {
1226
+ nestedProps = new Map()
1227
+ for (const inner of prop.initializer.properties) {
1228
+ if (!ts.isPropertyAssignment(inner)) continue
1229
+ const innerName = getPropertyName(inner.name)
1230
+ if (innerName) nestedProps.set(innerName, inner.initializer.getText(sf))
1231
+ }
1232
+ } else {
1233
+ props.set(name, prop.initializer.getText(sf))
1234
+ }
1235
+ }
1236
+
1237
+ const typeVal = props.get('type')
1238
+ if (!typeVal) return null
1239
+ const typeStr = typeVal.replace(/['"]/g, '')
1240
+
1241
+ const shapeTypes = new Set(['geo', 'text', 'arrow', 'line', 'note', 'draw'])
1242
+ if (!shapeTypes.has(typeStr)) return null
1243
+
1244
+ if (hasSpread) return null
1245
+
1246
+ const out: Array<[string, string]> = []
1247
+
1248
+ if (typeStr === 'geo' && nestedProps?.has('geo')) {
1249
+ const geoVal = nestedProps.get('geo')!.replace(/['"]/g, '')
1250
+ out.push(['_type', `'${GEO_TO_FOCUSED[geoVal] ?? geoVal}'`])
1251
+ nestedProps.delete('geo')
1252
+ } else {
1253
+ out.push(['_type', `'${typeStr}'`])
1254
+ }
1255
+
1256
+ if (props.has('id')) {
1257
+ let idVal = props.get('id')!
1258
+ const match = idVal.match(/createShapeId\(\s*['"]([^'"]*)['"]\s*\)/)
1259
+ if (match) idVal = `'${match[1]}'`
1260
+ out.push(['shapeId', idVal])
1261
+ }
1262
+
1263
+ for (const key of ['x', 'y']) {
1264
+ if (props.has(key)) out.push([key, props.get(key)!])
1265
+ }
1266
+
1267
+ if (nestedProps) {
1268
+ for (const [key, val] of nestedProps) {
1269
+ if (INTERNAL_PROPS.has(key)) continue
1270
+
1271
+ if (key === 'richText') {
1272
+ const rtMatch = val.match(/toRichText\(\s*(['"].*?['"])\s*\)/)
1273
+ if (rtMatch) out.push(['text', rtMatch[1]])
1274
+ continue
1275
+ }
1276
+
1277
+ if (key === 'fill') {
1278
+ const fillStr = val.replace(/['"]/g, '')
1279
+ out.push(['fill', `'${TLDRAW_TO_FOCUSED_FILL[fillStr] ?? fillStr}'`])
1280
+ continue
1281
+ }
1282
+
1283
+ if (key === 'color') {
1284
+ out.push(['color', val])
1285
+ continue
1286
+ }
1287
+
1288
+ out.push([key, val])
1289
+ }
1290
+ }
1291
+
1292
+ const handled = new Set(['type', 'id', 'x', 'y', 'props'])
1293
+ for (const [key, val] of props) {
1294
+ if (handled.has(key) || INTERNAL_PROPS.has(key)) continue
1295
+ out.push([key, val])
1296
+ }
1297
+
1298
+ return '{ ' + out.map(([k, v]) => `${k}: ${v}`).join(', ') + ' }'
1299
+ }
1300
+
1301
+ function rewriteSignature(sig: string): string {
1302
+ return sig
1303
+ .replace(/TLCreateShapePartial(<[^>]*>)?/g, 'TLShape')
1304
+ .replace(/TLShapePartial(<[^>]*>)?/g, 'Partial<TLShape>')
1305
+ .replace(/TLShapeId/g, 'string')
1306
+ .replace(/TLParentId/g, 'string')
1307
+ }
1308
+
1309
+ function postProcessMembers(members: ExtractedMember[]): ExtractedMember[] {
1310
+ return members.map((m) => ({
1311
+ ...m,
1312
+ signature: rewriteSignature(m.signature),
1313
+ examples: m.examples.map(convertOldFormatExample),
1314
+ }))
1315
+ }
1316
+
1317
+ // --- Main ---
1318
+
1319
+ function main() {
1320
+ console.error(
1321
+ `Extracting Editor API from:\n ${editorDtsPath}\n ${storeDtsPath}\n ${tlschemaDtsPath}\n ${formatTsPath}\n ${execHelpersPath}`
1322
+ )
1323
+ fs.mkdirSync(distDir, { recursive: true })
1324
+
1325
+ // Read conversion maps from format.ts via AST
1326
+ const formatSf = ts.createSourceFile(
1327
+ formatTsPath,
1328
+ fs.readFileSync(formatTsPath, 'utf-8'),
1329
+ ts.ScriptTarget.Latest
1330
+ )
1331
+ GEO_TO_FOCUSED = readStringRecord(formatSf, 'GEO_TO_FOCUSED_TYPES')
1332
+ TLDRAW_TO_FOCUSED_FILL = readStringRecord(formatSf, 'SHAPE_TO_FOCUSED_FILLS')
1333
+
1334
+ const members = extract()
1335
+ const types = extractFocusedShapeTypes()
1336
+ const exec = extractExecHelpers()
1337
+ const categories = [...new Set(members.map((m) => m.category))].sort()
1338
+
1339
+ // Generate METHOD_MAP
1340
+ const editorContext = createDeclarationContext([editorDtsPath, storeDtsPath, tlschemaDtsPath])
1341
+ const editorSourceFile = editorContext.sourceFiles.get(editorDtsPath)
1342
+ let editorClass: ts.ClassDeclaration | undefined
1343
+ if (editorSourceFile) {
1344
+ ts.forEachChild(editorSourceFile, (node) => {
1345
+ if (ts.isClassDeclaration(node) && node.name?.text === 'Editor') {
1346
+ editorClass = node
1347
+ }
1348
+ })
1349
+ }
1350
+ if (editorClass) {
1351
+ const methodMap = generateMethodMap(editorClass, editorContext)
1352
+ writeMethodMap(methodMap)
1353
+ console.error(
1354
+ `Wrote ${Object.keys(methodMap).length} method map entries to dist/method-map.json`
1355
+ )
1356
+ }
1357
+
1358
+ const output = {
1359
+ extractedAt: new Date().toISOString(),
1360
+ memberCount: members.length,
1361
+ categories,
1362
+ members: postProcessMembers(members),
1363
+ types,
1364
+ helperCount: exec.helperCount,
1365
+ helpers: exec.helpers,
1366
+ }
1367
+
1368
+ fs.writeFileSync(outPath, JSON.stringify(output, null, 2))
1369
+ console.error(
1370
+ `Wrote ${members.length} members (${categories.length} categories), ${types.shapes.length} shape types, and ${exec.helperCount} exec helpers to dist/editor-api.json`
1371
+ )
1372
+ }
1373
+
1374
+ main()