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.
- package/LICENSE +21 -0
- package/README.md +222 -0
- package/bridge/app-bridge-entry.js +6 -0
- package/mcp-app/LICENSE.md +9 -0
- package/mcp-app/PI_TLDRAW_PROVENANCE.json +32 -0
- package/mcp-app/README.md +129 -0
- package/mcp-app/dev-tunnel.sh +51 -0
- package/mcp-app/dist/editor-api.json +8493 -0
- package/mcp-app/dist/mcp-app.html +643 -0
- package/mcp-app/dist/method-map.json +915 -0
- package/mcp-app/package.json +42 -0
- package/mcp-app/plugins/tldraw-mcp/.cursor-plugin/plugin.json +10 -0
- package/mcp-app/plugins/tldraw-mcp/assets/logo.svg +3 -0
- package/mcp-app/plugins/tldraw-mcp/mcp.json +8 -0
- package/mcp-app/scripts/extract-editor-api.ts +1374 -0
- package/mcp-app/server.json +21 -0
- package/mcp-app/src/logger.ts +45 -0
- package/mcp-app/src/register-tools.ts +368 -0
- package/mcp-app/src/shared/generated-data.ts +160 -0
- package/mcp-app/src/shared/pending-requests.ts +69 -0
- package/mcp-app/src/shared/types.ts +76 -0
- package/mcp-app/src/shared/utils.ts +132 -0
- package/mcp-app/src/tools/exec.ts +120 -0
- package/mcp-app/src/tools/loadCachedCanvasWidgetHtml.ts +16 -0
- package/mcp-app/src/tools/search.ts +150 -0
- package/mcp-app/src/widget/app-context.tsx +29 -0
- package/mcp-app/src/widget/dev-log.tsx +70 -0
- package/mcp-app/src/widget/exec-helpers.ts +232 -0
- package/mcp-app/src/widget/export-tldr.ts +35 -0
- package/mcp-app/src/widget/focused/defaults.ts +141 -0
- package/mcp-app/src/widget/focused/focused-editor-proxy.ts +434 -0
- package/mcp-app/src/widget/focused/format.ts +366 -0
- package/mcp-app/src/widget/focused/to-focused.ts +258 -0
- package/mcp-app/src/widget/focused/to-tldraw.ts +570 -0
- package/mcp-app/src/widget/image-guard.tsx +106 -0
- package/mcp-app/src/widget/index.html +33 -0
- package/mcp-app/src/widget/mcp-app.css +113 -0
- package/mcp-app/src/widget/mcp-app.tsx +857 -0
- package/mcp-app/src/widget/persistence.ts +337 -0
- package/mcp-app/src/widget/snapshot.ts +157 -0
- package/mcp-app/src/worker.ts +305 -0
- package/mcp-app/tsconfig.json +23 -0
- package/mcp-app/vite.config.ts +13 -0
- package/mcp-app/wrangler.toml +45 -0
- package/mcp-app-source.json +36 -0
- package/package.json +90 -0
- package/patches/tldraw-mcp-app/001-pi-runtime.patch +35 -0
- package/scripts/assemble-mcp-app.mjs +193 -0
- package/scripts/build-bridge.mjs +74 -0
- package/scripts/e2e-mcp.mjs +69 -0
- package/scripts/e2e-packaged-mcp-app.mjs +79 -0
- package/scripts/run-mcp-app-dev.mjs +44 -0
- package/scripts/verify-bundle.mjs +41 -0
- package/scripts/verify-mcp-app-source.mjs +51 -0
- package/scripts/verify-mcp-app.mjs +38 -0
- package/scripts/verify-package-files.mjs +50 -0
- package/src/canvas/export.ts +164 -0
- package/src/canvas/state.ts +117 -0
- package/src/canvas/workflow.ts +105 -0
- package/src/commands/tldraw-command.ts +48 -0
- package/src/diagram/guidance.ts +44 -0
- package/src/host/local-host.ts +289 -0
- package/src/index.ts +762 -0
- package/src/mcp/client.ts +126 -0
- package/src/mcp/response.ts +74 -0
- package/src/semantic/layer.ts +309 -0
- package/src/server/server-manager.ts +153 -0
- package/src/store/export-store.ts +33 -0
- package/src/store/project-store.ts +251 -0
- package/src/ui/tldraw-status.ts +88 -0
- package/static/app-bridge-bundle.js +18114 -0
- package/static/app-bridge-bundle.meta.json +164 -0
- package/static/host.html +390 -0
- 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()
|