@fabrika/canvas-kernel 0.1.45 → 0.2.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/README.md +33 -67
- package/index.ts +19 -83
- package/package.json +68 -72
- package/src/client/AnnotationProvider.tsx +227 -268
- package/src/client/AnnotationSidebar.tsx +392 -338
- package/src/client/FileIcon.tsx +106 -65
- package/src/client/PlanRenderer.tsx +607 -551
- package/src/client/RenderErrorContext.ts +7 -7
- package/src/client/ResponsePreview.tsx +192 -173
- package/src/client/annotationDom.ts +43 -97
- package/src/client/annotationDraft.ts +98 -92
- package/src/client/appContext.tsx +40 -40
- package/src/client/components/Callout.tsx +20 -20
- package/src/client/components/Checklist.tsx +28 -28
- package/src/client/components/Choice.tsx +178 -159
- package/src/client/components/CodeBlock.tsx +26 -26
- package/src/client/components/Diff.tsx +17 -17
- package/src/client/components/FilePreview.tsx +99 -74
- package/src/client/components/Image.tsx +41 -39
- package/src/client/components/Markdown.tsx +33 -32
- package/src/client/components/Mermaid.tsx +592 -542
- package/src/client/components/Note.tsx +8 -8
- package/src/client/components/Priority.tsx +11 -11
- package/src/client/components/ResponseNote.tsx +17 -15
- package/src/client/components/Section.tsx +28 -26
- package/src/client/components/Table.tsx +34 -29
- package/src/client/components/Task.tsx +58 -60
- package/src/client/components/UserInput.tsx +169 -152
- package/src/client/components/index.ts +16 -16
- package/src/client/components/renderMarkdownChildren.tsx +86 -68
- package/src/client/diffLines.ts +51 -39
- package/src/client/generateMarkdown.ts +197 -191
- package/src/client/hostContext.tsx +34 -34
- package/src/client/index.ts +43 -125
- package/src/client/preact-all.ts +4 -4
- package/src/client/runtime.ts +58 -111
- package/src/client/styles.css +1 -74
- package/src/engine/compiler.ts +316 -313
- package/src/engine/paths.ts +44 -44
- package/src/engine/session.ts +623 -618
- package/src/engine/watcher.ts +63 -60
- package/src/engine/websocket.ts +123 -121
- package/annotate.ts +0 -94
- package/daemon.ts +0 -46
- package/src/client/AnnotationDraftFooter.tsx +0 -76
- package/src/client/AnnotationEditor.tsx +0 -320
- package/src/client/AnnotationList.tsx +0 -190
- package/src/client/Popover.tsx +0 -361
- package/src/client/SessionContext.tsx +0 -3
- package/src/client/annotationContext.ts +0 -130
- package/src/client/annotationMarkdown.ts +0 -102
- package/src/client/annotationTarget.ts +0 -73
- package/src/client/blockTarget.ts +0 -127
- package/src/client/highlightRange.ts +0 -262
- package/src/client/popoverPosition.ts +0 -46
- package/src/client/regionTarget.ts +0 -180
- package/src/client/testing/dom.ts +0 -74
- package/src/client/textTarget.ts +0 -31
- package/src/client/useRegionAnnotation.tsx +0 -188
- package/src/client/useTextAnnotation.tsx +0 -194
- package/src/client/utils.ts +0 -33
- package/src/engine/channel.ts +0 -278
- package/src/engine/http-utils.ts +0 -14
- package/src/engine/lifecycle.ts +0 -302
- package/src/engine/router.ts +0 -16
- package/src/engine/waiter.ts +0 -188
package/src/engine/compiler.ts
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import
|
|
7
|
-
import
|
|
1
|
+
import { randomUUID } from 'crypto'
|
|
2
|
+
import { mkdirSync, readFileSync, unlinkSync, writeFileSync } from 'fs'
|
|
3
|
+
import { parse as parseMermaid } from 'mermaid-parser-bundle'
|
|
4
|
+
import { tmpdir } from 'os'
|
|
5
|
+
import { join } from 'path'
|
|
6
|
+
import { type ComponentChildren, Fragment, h } from 'preact'
|
|
7
|
+
import renderToString from 'preact-render-to-string'
|
|
8
8
|
|
|
9
9
|
export type CompileResult =
|
|
10
|
-
|
|
11
|
-
|
|
10
|
+
| { ok: true; js: string }
|
|
11
|
+
| { ok: false; error: string }
|
|
12
12
|
|
|
13
|
-
type RequiredPropKind =
|
|
13
|
+
type RequiredPropKind = 'array' | 'string'
|
|
14
14
|
|
|
15
15
|
/**
|
|
16
16
|
* One name the compiler makes available to authored JSX without an import.
|
|
@@ -20,60 +20,60 @@ type RequiredPropKind = "array" | "string";
|
|
|
20
20
|
* of rendering blank in the browser.
|
|
21
21
|
*/
|
|
22
22
|
export interface CanvasComponentSpec {
|
|
23
|
-
|
|
23
|
+
requiredProps?: Record<string, RequiredPropKind>
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
-
export type CanvasComponents = Record<string, CanvasComponentSpec
|
|
26
|
+
export type CanvasComponents = Record<string, CanvasComponentSpec>
|
|
27
27
|
|
|
28
28
|
/**
|
|
29
29
|
* The components every canvas host provides. Order is the order they appear in
|
|
30
30
|
* the injected destructure, so keep it stable for readable generated code.
|
|
31
31
|
*/
|
|
32
32
|
export const KERNEL_COMPONENTS: CanvasComponents = {
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
}
|
|
33
|
+
Section: { requiredProps: { title: 'string' } },
|
|
34
|
+
Item: { requiredProps: { id: 'string', label: 'string' } },
|
|
35
|
+
Task: { requiredProps: { id: 'string', label: 'string' } },
|
|
36
|
+
FilePreview: { requiredProps: { path: 'string' } },
|
|
37
|
+
CodeBlock: {},
|
|
38
|
+
Callout: {},
|
|
39
|
+
Mermaid: {},
|
|
40
|
+
Table: { requiredProps: { headers: 'array', rows: 'array' } },
|
|
41
|
+
Priority: { requiredProps: { level: 'string' } },
|
|
42
|
+
Checklist: { requiredProps: { items: 'array' } },
|
|
43
|
+
Note: {},
|
|
44
|
+
Diff: { requiredProps: { before: 'string', after: 'string' } },
|
|
45
|
+
Choice: { requiredProps: { id: 'string', label: 'string', options: 'array' } },
|
|
46
|
+
MultiChoice: { requiredProps: { id: 'string', label: 'string', options: 'array' } },
|
|
47
|
+
UserInput: { requiredProps: { id: 'string', label: 'string' } },
|
|
48
|
+
RangeInput: { requiredProps: { id: 'string', label: 'string' } },
|
|
49
|
+
ImageView: { requiredProps: { src: 'string' } },
|
|
50
|
+
Markdown: {},
|
|
51
|
+
useFeedback: {},
|
|
52
|
+
useAnnotations: {},
|
|
53
|
+
}
|
|
54
54
|
|
|
55
55
|
export interface CompileOptions {
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
56
|
+
/** Resolves `<FilePreview>` / `<Markdown file>` references at compile time. */
|
|
57
|
+
projectRoot?: string
|
|
58
|
+
/** Scratch dir for the temp `.jsx` handed to Bun.build. Created on demand. */
|
|
59
|
+
tempDir?: string
|
|
60
|
+
/**
|
|
61
|
+
* Host components to add on top of KERNEL_COMPONENTS, keyed by name. Each one
|
|
62
|
+
* must also be exported from the host's `#canvas/components` barrel — that is
|
|
63
|
+
* what the injected import actually reads at runtime. Re-declaring a kernel
|
|
64
|
+
* name overrides its spec.
|
|
65
|
+
*/
|
|
66
|
+
components?: CanvasComponents
|
|
67
67
|
}
|
|
68
68
|
|
|
69
69
|
/** Fallback scratch dir for hosts that compile without wiring CanvasPaths. */
|
|
70
|
-
const FALLBACK_COMPILE_DIR = join(tmpdir(),
|
|
70
|
+
const FALLBACK_COMPILE_DIR = join(tmpdir(), 'canvas-kernel', 'compile')
|
|
71
71
|
|
|
72
72
|
function componentImports(components: CanvasComponents): string {
|
|
73
|
-
|
|
73
|
+
return `import React from 'react';
|
|
74
74
|
import * as C from '#canvas/components';
|
|
75
|
-
const { ${Object.keys(components).join(
|
|
76
|
-
|
|
75
|
+
const { ${Object.keys(components).join(', ')} } = C;
|
|
76
|
+
`
|
|
77
77
|
}
|
|
78
78
|
|
|
79
79
|
// --- Validation via Preact --------------------------------------------------
|
|
@@ -84,128 +84,128 @@ const { ${Object.keys(components).join(", ")} } = C;
|
|
|
84
84
|
|
|
85
85
|
/** jsxDEV(type, props, key, isStaticChildren, source, self) → VNode */
|
|
86
86
|
function mockJsxDEV(type: any, props: any, key?: any) {
|
|
87
|
-
|
|
88
|
-
|
|
87
|
+
const { children, ...rest } = props || {}
|
|
88
|
+
return h(type, { ...rest, key }, children)
|
|
89
89
|
}
|
|
90
90
|
|
|
91
91
|
interface StubProps {
|
|
92
|
-
|
|
93
|
-
|
|
92
|
+
children?: ComponentChildren
|
|
93
|
+
[key: string]: unknown
|
|
94
94
|
}
|
|
95
95
|
|
|
96
96
|
/** Passthrough component — just renders its children */
|
|
97
|
-
const Stub = ({ children }: StubProps) => h(Fragment, null, children)
|
|
97
|
+
const Stub = ({ children }: StubProps) => h(Fragment, null, children)
|
|
98
98
|
|
|
99
99
|
function validatingStub(component: string, requiredProps: Record<string, RequiredPropKind>) {
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
100
|
+
return (props: StubProps) => {
|
|
101
|
+
for (const [prop, kind] of Object.entries(requiredProps)) {
|
|
102
|
+
const value = props[prop]
|
|
103
|
+
const valid = kind === 'array' ? Array.isArray(value) : typeof value === kind
|
|
104
|
+
if (!valid) {
|
|
105
|
+
throw new Error(`${component}: required prop \`${prop}\` must be ${kind === 'array' ? 'an array' : `a ${kind}`}`)
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return h(Fragment, null, props.children)
|
|
109
|
+
}
|
|
110
110
|
}
|
|
111
111
|
|
|
112
112
|
// Mermaid stub collects diagram sources for post-render validation
|
|
113
|
-
let collectedMermaidSources: string[] = []
|
|
113
|
+
let collectedMermaidSources: string[] = []
|
|
114
114
|
|
|
115
115
|
/** Names whose stub does more than check props, so a manifest entry cannot
|
|
116
116
|
* describe them. Hooks return shapes that survive destructuring. */
|
|
117
117
|
const SPECIAL_STUBS: Record<string, any> = {
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
}
|
|
118
|
+
Mermaid: ({ children }: any) => {
|
|
119
|
+
const source = typeof children === 'string' ? children : String(children ?? '')
|
|
120
|
+
if (source.trim()) collectedMermaidSources.push(source.trim())
|
|
121
|
+
return null
|
|
122
|
+
},
|
|
123
|
+
useFeedback: () => ({ submit: () => {}, value: null }),
|
|
124
|
+
useAnnotations: () => ({ annotations: [], addAnnotation: () => {} }),
|
|
125
|
+
}
|
|
126
126
|
|
|
127
127
|
function buildStubs(components: CanvasComponents): Record<string, any> {
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
128
|
+
const stubs: Record<string, any> = {}
|
|
129
|
+
for (const [name, spec] of Object.entries(components)) {
|
|
130
|
+
const special = SPECIAL_STUBS[name]
|
|
131
|
+
stubs[name] = special ?? (spec.requiredProps ? validatingStub(name, spec.requiredProps) : Stub)
|
|
132
|
+
}
|
|
133
|
+
return stubs
|
|
134
134
|
}
|
|
135
135
|
|
|
136
136
|
const MOCK_REACT = {
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
}
|
|
137
|
+
createElement: h,
|
|
138
|
+
Fragment,
|
|
139
|
+
useState: (init: any) => [init, () => {}],
|
|
140
|
+
useEffect: () => {},
|
|
141
|
+
useRef: (init: any) => ({ current: init }),
|
|
142
|
+
useMemo: (fn: any) => fn(),
|
|
143
|
+
useCallback: (fn: any) => fn,
|
|
144
|
+
useReducer: (r: any, init: any) => [init, () => {}],
|
|
145
|
+
useContext: () => ({}),
|
|
146
|
+
createContext: () => ({ Provider: Stub, Consumer: Stub }),
|
|
147
|
+
useLayoutEffect: () => {},
|
|
148
|
+
useImperativeHandle: () => {},
|
|
149
|
+
useDebugValue: () => {},
|
|
150
|
+
useDeferredValue: (v: any) => v,
|
|
151
|
+
useTransition: () => [false, (fn: any) => fn()],
|
|
152
|
+
useId: () => 'mock',
|
|
153
|
+
useSyncExternalStore: (sub: any, get: any) => get(),
|
|
154
|
+
memo: (c: any) => c,
|
|
155
|
+
forwardRef: (c: any) => c,
|
|
156
|
+
lazy: (c: any) => c,
|
|
157
|
+
Children: { map: (c: any, fn: any) => (Array.isArray(c) ? c : [c]).map(fn), toArray: (c: any) => (Array.isArray(c) ? c : [c]) },
|
|
158
|
+
isValidElement: () => true,
|
|
159
|
+
cloneElement: h,
|
|
160
|
+
Suspense: Stub,
|
|
161
|
+
StrictMode: Stub,
|
|
162
|
+
}
|
|
163
163
|
|
|
164
164
|
async function validateCompiledPlan(js: string, stubs: Record<string, any>): Promise<{ ok: true } | { ok: false; error: string }> {
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
165
|
+
let code = js
|
|
166
|
+
|
|
167
|
+
// Strip ESM import statements
|
|
168
|
+
code = code.replace(/import\s+\w+\s+from\s+"[^"]+";?/g, '')
|
|
169
|
+
code = code.replace(/import\s+\*\s+as\s+\w+\s+from\s+"[^"]+";?/g, '')
|
|
170
|
+
code = code.replace(/import\s*\{[^}]*\}\s*from\s+"[^"]+";?/g, '')
|
|
171
|
+
|
|
172
|
+
// Strip export, extract default name
|
|
173
|
+
let defaultName: string | null = null
|
|
174
|
+
code = code.replace(/export\s*\{\s*(\w+)\s+as\s+default\s*\};?/g, (_, name) => {
|
|
175
|
+
defaultName = name
|
|
176
|
+
return ''
|
|
177
|
+
})
|
|
178
|
+
if (!defaultName) {
|
|
179
|
+
code = code.replace(/export\s+default\s+(\w+);?/g, (_, name) => {
|
|
180
|
+
defaultName = name
|
|
181
|
+
return ''
|
|
182
|
+
})
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const callDefault = defaultName
|
|
186
|
+
? `\nreturn typeof ${defaultName} === "function" ? ${defaultName}() : null;`
|
|
187
|
+
: '\nreturn null;'
|
|
188
|
+
|
|
189
|
+
try {
|
|
190
|
+
collectedMermaidSources = []
|
|
191
|
+
const fn = new Function('React', 'C', 'jsxDEV', 'jsx', 'Fragment', code + callDefault)
|
|
192
|
+
const vnode = fn(MOCK_REACT, stubs, mockJsxDEV, mockJsxDEV, Fragment)
|
|
193
|
+
if (vnode) renderToString(vnode)
|
|
194
|
+
} catch (e: any) {
|
|
195
|
+
return { ok: false, error: e?.message || String(e) }
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Validate collected Mermaid diagram sources
|
|
199
|
+
for (const source of collectedMermaidSources) {
|
|
200
|
+
try {
|
|
201
|
+
await parseMermaid(source)
|
|
202
|
+
} catch (e: any) {
|
|
203
|
+
const msg = (e?.message || 'Invalid diagram').split('\n')[0]
|
|
204
|
+
return { ok: false, error: `Mermaid syntax error: ${msg}` }
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
return { ok: true }
|
|
209
209
|
}
|
|
210
210
|
|
|
211
211
|
/**
|
|
@@ -226,128 +226,131 @@ async function validateCompiledPlan(js: string, stubs: Record<string, any>): Pro
|
|
|
226
226
|
* user error and skip a pointless daemon restart (see push.ts).
|
|
227
227
|
*/
|
|
228
228
|
function formatBuildError(err: any, lineOffset: number): string {
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
229
|
+
// Collect every candidate that might carry a `.position`, most specific first.
|
|
230
|
+
const candidates: any[] = []
|
|
231
|
+
if (err?.position) candidates.push(err)
|
|
232
|
+
if (Array.isArray(err?.errors)) candidates.push(...err.errors)
|
|
233
|
+
if (!candidates.length && err && typeof err[Symbol.iterator] === 'function') {
|
|
234
|
+
try {
|
|
235
|
+
candidates.push(...err)
|
|
236
|
+
} catch {}
|
|
237
|
+
}
|
|
238
|
+
const detail = candidates.find((c) => c?.position) ?? candidates[0] ?? err
|
|
239
|
+
const [message = 'Syntax error'] = String(detail?.message || err?.message || 'Syntax error').split('\n')
|
|
240
|
+
|
|
241
|
+
const pos = detail?.position
|
|
242
|
+
if (!pos || typeof pos.line !== 'number') return message
|
|
243
|
+
|
|
244
|
+
const loc = ` (line ${pos.line - lineOffset}, col ${pos.column})`
|
|
245
|
+
let frame = ''
|
|
246
|
+
if (typeof pos.lineText === 'string') {
|
|
247
|
+
const caret = ' '.repeat(Math.max(0, pos.column ?? 0)) + '^'
|
|
248
|
+
frame = `\n ${pos.lineText}\n ${caret}`
|
|
249
|
+
}
|
|
250
|
+
return `${message}${loc}${frame}`
|
|
249
251
|
}
|
|
250
252
|
|
|
251
253
|
export async function compileJsx(jsx: string, options: CompileOptions = {}): Promise<CompileResult> {
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
254
|
+
const { projectRoot } = options
|
|
255
|
+
const components = options.components ? { ...KERNEL_COMPONENTS, ...options.components } : KERNEL_COMPONENTS
|
|
256
|
+
const componentPreamble = componentImports(components)
|
|
257
|
+
// Strip leading pragma / block / line comments (e.g. `/** @jsxImportSource ... */`).
|
|
258
|
+
// Authored JSX sometimes carries a JSX-runtime pragma from another toolchain;
|
|
259
|
+
// Canvas injects its own imports and uses the React automatic runtime, so the
|
|
260
|
+
// pragma is unused. Left in place it sits at the top level of the wrapped
|
|
261
|
+
// fragment as plain text (JSX only treats `{/* */}` as a comment) and renders
|
|
262
|
+
// literally on the page.
|
|
263
|
+
let stripped: string
|
|
264
|
+
do {
|
|
265
|
+
stripped = jsx
|
|
266
|
+
jsx = jsx.replace(/^\s*(?:\/\*[\s\S]*?\*\/|\/\/[^\n]*)\s*/, '')
|
|
267
|
+
} while (jsx !== stripped)
|
|
268
|
+
|
|
269
|
+
// Resolve file contents at compile time
|
|
270
|
+
if (projectRoot) {
|
|
271
|
+
jsx = resolveFilePreviews(jsx, projectRoot)
|
|
272
|
+
jsx = resolveMarkdownFiles(jsx, projectRoot)
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// Strip template literals and strings so `export default` inside CodeBlock
|
|
276
|
+
// content (e.g. {`export default defineConfig(...)`}) doesn't false-positive.
|
|
277
|
+
const strippedJsx = jsx
|
|
278
|
+
.replace(/\{`[^`]*`\}/gs, '') // {`...`} template expressions
|
|
279
|
+
.replace(/`[^`]*`/gs, '') // standalone template literals
|
|
280
|
+
.replace(/"[^"]*"/g, '') // double-quoted strings
|
|
281
|
+
.replace(/'[^']*'/g, '') // single-quoted strings
|
|
282
|
+
const hasDefaultExport = /export\s+default\b/.test(strippedJsx)
|
|
283
|
+
|
|
284
|
+
const source = hasDefaultExport
|
|
285
|
+
? `${componentPreamble}\n${jsx}`
|
|
286
|
+
: `${componentPreamble}\nexport default function Plan() {\n return (<>${jsx}</>);\n}\n`
|
|
287
|
+
|
|
288
|
+
// Offset: injected import lines + wrapper lines (export default function
|
|
289
|
+
// Plan() + return). Used to map Bun's line numbers back to the authored file.
|
|
290
|
+
const wrapperLines = hasDefaultExport ? 1 : 2
|
|
291
|
+
const lineOffset = componentPreamble.split('\n').length - 1 + wrapperLines
|
|
292
|
+
|
|
293
|
+
// Pre-validate syntax with Bun.Transpiler — gives precise error positions,
|
|
294
|
+
// unlike Bun.build() which throws opaque "Bundle failed" for syntax errors.
|
|
295
|
+
try {
|
|
296
|
+
const transpiler = new Bun.Transpiler({ loader: 'jsx' })
|
|
297
|
+
transpiler.transformSync(source)
|
|
298
|
+
} catch (syntaxError: any) {
|
|
299
|
+
return { ok: false, error: formatBuildError(syntaxError, lineOffset) }
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
const compileDir = options.tempDir ?? FALLBACK_COMPILE_DIR
|
|
303
|
+
mkdirSync(compileDir, { recursive: true })
|
|
304
|
+
const tmpFile = join(compileDir, `plan-${randomUUID()}.jsx`)
|
|
305
|
+
|
|
306
|
+
try {
|
|
307
|
+
writeFileSync(tmpFile, source)
|
|
308
|
+
|
|
309
|
+
try {
|
|
310
|
+
const result = await Bun.build({
|
|
311
|
+
entrypoints: [tmpFile],
|
|
312
|
+
format: 'esm',
|
|
313
|
+
external: ['react', 'react-dom', '#canvas/components', '#canvas/runtime'],
|
|
314
|
+
jsx: { runtime: 'automatic', importSource: 'react' },
|
|
315
|
+
})
|
|
316
|
+
|
|
317
|
+
if (!result.success) {
|
|
318
|
+
const errorLogs = result.logs.filter((l) => l.level === 'error')
|
|
319
|
+
const errors = errorLogs
|
|
320
|
+
.map((l) => formatBuildError(l, lineOffset))
|
|
321
|
+
.join('\n')
|
|
322
|
+
return { ok: false, error: errors || 'Bundle failed' }
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
const [output] = result.outputs
|
|
326
|
+
if (!output) return { ok: false, error: 'Bundle produced no output' }
|
|
327
|
+
|
|
328
|
+
const js = await output.text()
|
|
329
|
+
const validation = await validateCompiledPlan(js, buildStubs(components))
|
|
330
|
+
if (!validation.ok) {
|
|
331
|
+
return { ok: false, error: `Runtime error: ${validation.error}` }
|
|
332
|
+
}
|
|
333
|
+
return { ok: true, js }
|
|
334
|
+
} catch (buildError: any) {
|
|
335
|
+
// A real syntax error surfaces as an AggregateError carrying BuildMessages
|
|
336
|
+
// (with positions) — format it precisely. The opaque "Unknown Error, TODO"
|
|
337
|
+
// corruption case (internal bundler state gets corrupted in long-running
|
|
338
|
+
// processes) has no sub-errors; pass it through so the CLI restarts the
|
|
339
|
+
// daemon and retries.
|
|
340
|
+
const hasDetail = buildError?.position
|
|
341
|
+
|| (Array.isArray(buildError?.errors) && buildError.errors.length > 0)
|
|
342
|
+
if (hasDetail) {
|
|
343
|
+
return { ok: false, error: formatBuildError(buildError, lineOffset) }
|
|
344
|
+
}
|
|
345
|
+
return { ok: false, error: buildError?.message || 'Unknown Error' }
|
|
346
|
+
}
|
|
347
|
+
} catch (e: any) {
|
|
348
|
+
return { ok: false, error: e.message }
|
|
349
|
+
} finally {
|
|
350
|
+
try {
|
|
351
|
+
unlinkSync(tmpFile)
|
|
352
|
+
} catch {}
|
|
353
|
+
}
|
|
351
354
|
}
|
|
352
355
|
|
|
353
356
|
/**
|
|
@@ -355,23 +358,23 @@ export async function compileJsx(jsx: string, options: CompileOptions = {}): Pro
|
|
|
355
358
|
* resolved at compile time.
|
|
356
359
|
*/
|
|
357
360
|
function resolveMarkdownFiles(jsx: string, projectRoot: string): string {
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
361
|
+
return jsx.replace(
|
|
362
|
+
/<Markdown\b([^>]*?)\/>/g,
|
|
363
|
+
(match, attrs: string) => {
|
|
364
|
+
const filePath = attrs.match(/file=["']([^"']+)["']/)?.[1]
|
|
365
|
+
if (!filePath) return match
|
|
366
|
+
|
|
367
|
+
const absPath = join(projectRoot, filePath)
|
|
368
|
+
|
|
369
|
+
try {
|
|
370
|
+
const content = readFileSync(absPath, 'utf-8')
|
|
371
|
+
const escaped = JSON.stringify(content)
|
|
372
|
+
return `<Markdown${attrs} __content={${escaped}} />`
|
|
373
|
+
} catch {
|
|
374
|
+
return match
|
|
375
|
+
}
|
|
376
|
+
},
|
|
377
|
+
)
|
|
375
378
|
}
|
|
376
379
|
|
|
377
380
|
/**
|
|
@@ -379,37 +382,37 @@ function resolveMarkdownFiles(jsx: string, projectRoot: string): string {
|
|
|
379
382
|
* resolved at compile time so the browser doesn't need to fetch.
|
|
380
383
|
*/
|
|
381
384
|
function resolveFilePreviews(jsx: string, projectRoot: string): string {
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
385
|
+
// Match <FilePreview ... /> (self-closing)
|
|
386
|
+
return jsx.replace(
|
|
387
|
+
/<FilePreview\b([^>]*?)\/>/g,
|
|
388
|
+
(match, attrs: string) => {
|
|
389
|
+
const filePath = attrs.match(/path=["']([^"']+)["']/)?.[1]
|
|
390
|
+
if (!filePath) return match
|
|
391
|
+
|
|
392
|
+
const absPath = join(projectRoot, filePath)
|
|
393
|
+
|
|
394
|
+
try {
|
|
395
|
+
let content = readFileSync(absPath, 'utf-8')
|
|
396
|
+
|
|
397
|
+
// Apply lines filter if present
|
|
398
|
+
const linesMatch = attrs.match(/lines=\{?\[(\d+)\s*,\s*(\d+)\]\}?/)
|
|
399
|
+
if (linesMatch?.[1] && linesMatch[2]) {
|
|
400
|
+
const start = parseInt(linesMatch[1], 10)
|
|
401
|
+
const end = parseInt(linesMatch[2], 10)
|
|
402
|
+
content = content.split('\n').slice(start - 1, end).join('\n')
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
// Escape for embedding in JSX string
|
|
406
|
+
const escaped = JSON.stringify(content)
|
|
407
|
+
return `<FilePreview${attrs} __content={${escaped}} />`
|
|
408
|
+
} catch {
|
|
409
|
+
return match
|
|
410
|
+
}
|
|
411
|
+
},
|
|
412
|
+
)
|
|
410
413
|
}
|
|
411
414
|
|
|
412
415
|
/** @deprecated Use `compileJsx(jsx, { projectRoot, tempDir })`. */
|
|
413
416
|
export async function compilePlan(jsx: string, projectRoot?: string): Promise<CompileResult> {
|
|
414
|
-
|
|
417
|
+
return compileJsx(jsx, { projectRoot })
|
|
415
418
|
}
|