@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.
Files changed (66) hide show
  1. package/README.md +33 -67
  2. package/index.ts +19 -83
  3. package/package.json +68 -72
  4. package/src/client/AnnotationProvider.tsx +227 -268
  5. package/src/client/AnnotationSidebar.tsx +392 -338
  6. package/src/client/FileIcon.tsx +106 -65
  7. package/src/client/PlanRenderer.tsx +607 -551
  8. package/src/client/RenderErrorContext.ts +7 -7
  9. package/src/client/ResponsePreview.tsx +192 -173
  10. package/src/client/annotationDom.ts +43 -97
  11. package/src/client/annotationDraft.ts +98 -92
  12. package/src/client/appContext.tsx +40 -40
  13. package/src/client/components/Callout.tsx +20 -20
  14. package/src/client/components/Checklist.tsx +28 -28
  15. package/src/client/components/Choice.tsx +178 -159
  16. package/src/client/components/CodeBlock.tsx +26 -26
  17. package/src/client/components/Diff.tsx +17 -17
  18. package/src/client/components/FilePreview.tsx +99 -74
  19. package/src/client/components/Image.tsx +41 -39
  20. package/src/client/components/Markdown.tsx +33 -32
  21. package/src/client/components/Mermaid.tsx +592 -542
  22. package/src/client/components/Note.tsx +8 -8
  23. package/src/client/components/Priority.tsx +11 -11
  24. package/src/client/components/ResponseNote.tsx +17 -15
  25. package/src/client/components/Section.tsx +28 -26
  26. package/src/client/components/Table.tsx +34 -29
  27. package/src/client/components/Task.tsx +58 -60
  28. package/src/client/components/UserInput.tsx +169 -152
  29. package/src/client/components/index.ts +16 -16
  30. package/src/client/components/renderMarkdownChildren.tsx +86 -68
  31. package/src/client/diffLines.ts +51 -39
  32. package/src/client/generateMarkdown.ts +197 -191
  33. package/src/client/hostContext.tsx +34 -34
  34. package/src/client/index.ts +43 -125
  35. package/src/client/preact-all.ts +4 -4
  36. package/src/client/runtime.ts +58 -111
  37. package/src/client/styles.css +1 -74
  38. package/src/engine/compiler.ts +316 -313
  39. package/src/engine/paths.ts +44 -44
  40. package/src/engine/session.ts +623 -618
  41. package/src/engine/watcher.ts +63 -60
  42. package/src/engine/websocket.ts +123 -121
  43. package/annotate.ts +0 -94
  44. package/daemon.ts +0 -46
  45. package/src/client/AnnotationDraftFooter.tsx +0 -76
  46. package/src/client/AnnotationEditor.tsx +0 -320
  47. package/src/client/AnnotationList.tsx +0 -190
  48. package/src/client/Popover.tsx +0 -361
  49. package/src/client/SessionContext.tsx +0 -3
  50. package/src/client/annotationContext.ts +0 -130
  51. package/src/client/annotationMarkdown.ts +0 -102
  52. package/src/client/annotationTarget.ts +0 -73
  53. package/src/client/blockTarget.ts +0 -127
  54. package/src/client/highlightRange.ts +0 -262
  55. package/src/client/popoverPosition.ts +0 -46
  56. package/src/client/regionTarget.ts +0 -180
  57. package/src/client/testing/dom.ts +0 -74
  58. package/src/client/textTarget.ts +0 -31
  59. package/src/client/useRegionAnnotation.tsx +0 -188
  60. package/src/client/useTextAnnotation.tsx +0 -194
  61. package/src/client/utils.ts +0 -33
  62. package/src/engine/channel.ts +0 -278
  63. package/src/engine/http-utils.ts +0 -14
  64. package/src/engine/lifecycle.ts +0 -302
  65. package/src/engine/router.ts +0 -16
  66. package/src/engine/waiter.ts +0 -188
@@ -1,16 +1,16 @@
1
- import { writeFileSync, unlinkSync, readFileSync, mkdirSync } from "fs";
2
- import { join } from "path";
3
- import { tmpdir } from "os";
4
- import { randomUUID } from "crypto";
5
- import { h, Fragment, type ComponentChildren } from "preact";
6
- import renderToString from "preact-render-to-string";
7
- import { parse as parseMermaid } from "mermaid-parser-bundle";
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
- | { ok: true; js: string }
11
- | { ok: false; error: string };
10
+ | { ok: true; js: string }
11
+ | { ok: false; error: string }
12
12
 
13
- type RequiredPropKind = "array" | "string";
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
- requiredProps?: Record<string, RequiredPropKind>;
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
- 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
- };
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
- /** 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;
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(), "canvas-kernel", "compile");
70
+ const FALLBACK_COMPILE_DIR = join(tmpdir(), 'canvas-kernel', 'compile')
71
71
 
72
72
  function componentImports(components: CanvasComponents): string {
73
- return `import React from 'react';
73
+ return `import React from 'react';
74
74
  import * as C from '#canvas/components';
75
- const { ${Object.keys(components).join(", ")} } = C;
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
- const { children, ...rest } = props || {};
88
- return h(type, { ...rest, key }, children);
87
+ const { children, ...rest } = props || {}
88
+ return h(type, { ...rest, key }, children)
89
89
  }
90
90
 
91
91
  interface StubProps {
92
- children?: ComponentChildren;
93
- [key: string]: unknown;
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
- 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
- };
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
- 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
- };
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
- 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;
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
- 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
- };
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
- 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 };
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
- // 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 { candidates.push(...err); } catch {}
235
- }
236
- const detail = candidates.find((c) => c?.position) ?? candidates[0] ?? err;
237
- const [message = "Syntax error"] = String(detail?.message || err?.message || "Syntax error").split("\n");
238
-
239
- const pos = detail?.position;
240
- if (!pos || typeof pos.line !== "number") return message;
241
-
242
- const loc = ` (line ${pos.line - lineOffset}, col ${pos.column})`;
243
- let frame = "";
244
- if (typeof pos.lineText === "string") {
245
- const caret = " ".repeat(Math.max(0, pos.column ?? 0)) + "^";
246
- frame = `\n ${pos.lineText}\n ${caret}`;
247
- }
248
- return `${message}${loc}${frame}`;
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
- const { projectRoot } = options;
253
- const components = options.components ? { ...KERNEL_COMPONENTS, ...options.components } : KERNEL_COMPONENTS;
254
- const componentPreamble = componentImports(components);
255
- // Strip leading pragma / block / line comments (e.g. `/** @jsxImportSource ... */`).
256
- // Authored JSX sometimes carries a JSX-runtime pragma from another toolchain;
257
- // Canvas injects its own imports and uses the React automatic runtime, so the
258
- // pragma is unused. Left in place it sits at the top level of the wrapped
259
- // fragment as plain text (JSX only treats `{/* */}` as a comment) and renders
260
- // literally on the page.
261
- let stripped: string;
262
- do {
263
- stripped = jsx;
264
- jsx = jsx.replace(/^\s*(?:\/\*[\s\S]*?\*\/|\/\/[^\n]*)\s*/, "");
265
- } while (jsx !== stripped);
266
-
267
- // Resolve file contents at compile time
268
- if (projectRoot) {
269
- jsx = resolveFilePreviews(jsx, projectRoot);
270
- jsx = resolveMarkdownFiles(jsx, projectRoot);
271
- }
272
-
273
- // Strip template literals and strings so `export default` inside CodeBlock
274
- // content (e.g. {`export default defineConfig(...)`}) doesn't false-positive.
275
- const strippedJsx = jsx
276
- .replace(/\{`[^`]*`\}/gs, "") // {`...`} template expressions
277
- .replace(/`[^`]*`/gs, "") // standalone template literals
278
- .replace(/"[^"]*"/g, "") // double-quoted strings
279
- .replace(/'[^']*'/g, ""); // single-quoted strings
280
- const hasDefaultExport = /export\s+default\b/.test(strippedJsx);
281
-
282
- const source = hasDefaultExport
283
- ? `${componentPreamble}\n${jsx}`
284
- : `${componentPreamble}\nexport default function Plan() {\n return (<>${jsx}</>);\n}\n`;
285
-
286
- // Offset: injected import lines + wrapper lines (export default function
287
- // Plan() + return). Used to map Bun's line numbers back to the authored file.
288
- const wrapperLines = hasDefaultExport ? 1 : 2;
289
- const lineOffset = componentPreamble.split("\n").length - 1 + wrapperLines;
290
-
291
- // Pre-validate syntax with Bun.Transpiler — gives precise error positions,
292
- // unlike Bun.build() which throws opaque "Bundle failed" for syntax errors.
293
- try {
294
- const transpiler = new Bun.Transpiler({ loader: "jsx" });
295
- transpiler.transformSync(source);
296
- } catch (syntaxError: any) {
297
- return { ok: false, error: formatBuildError(syntaxError, lineOffset) };
298
- }
299
-
300
- const compileDir = options.tempDir ?? FALLBACK_COMPILE_DIR;
301
- mkdirSync(compileDir, { recursive: true });
302
- const tmpFile = join(compileDir, `plan-${randomUUID()}.jsx`);
303
-
304
- try {
305
- writeFileSync(tmpFile, source);
306
-
307
- try {
308
- const result = await Bun.build({
309
- entrypoints: [tmpFile],
310
- format: "esm",
311
- external: ["react", "react-dom", "#canvas/components", "#canvas/runtime"],
312
- jsx: { runtime: "automatic", importSource: "react" },
313
- });
314
-
315
- if (!result.success) {
316
- const errorLogs = result.logs.filter((l) => l.level === "error");
317
- const errors = errorLogs
318
- .map((l) => formatBuildError(l, lineOffset))
319
- .join("\n");
320
- return { ok: false, error: errors || "Bundle failed" };
321
- }
322
-
323
- const [output] = result.outputs;
324
- if (!output) return { ok: false, error: "Bundle produced no output" };
325
-
326
- const js = await output.text();
327
- const validation = await validateCompiledPlan(js, buildStubs(components));
328
- if (!validation.ok) {
329
- return { ok: false, error: `Runtime error: ${validation.error}` };
330
- }
331
- return { ok: true, js };
332
- } catch (buildError: any) {
333
- // A real syntax error surfaces as an AggregateError carrying BuildMessages
334
- // (with positions) — format it precisely. The opaque "Unknown Error, TODO"
335
- // corruption case (internal bundler state gets corrupted in long-running
336
- // processes) has no sub-errors; pass it through so the CLI restarts the
337
- // daemon and retries.
338
- const hasDetail =
339
- buildError?.position ||
340
- (Array.isArray(buildError?.errors) && buildError.errors.length > 0);
341
- if (hasDetail) {
342
- return { ok: false, error: formatBuildError(buildError, lineOffset) };
343
- }
344
- return { ok: false, error: buildError?.message || "Unknown Error" };
345
- }
346
- } catch (e: any) {
347
- return { ok: false, error: e.message };
348
- } finally {
349
- try { unlinkSync(tmpFile); } catch {}
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
- return jsx.replace(
359
- /<Markdown\b([^>]*?)\/>/g,
360
- (match, attrs: string) => {
361
- const filePath = attrs.match(/file=["']([^"']+)["']/)?.[1];
362
- if (!filePath) return match;
363
-
364
- const absPath = join(projectRoot, filePath);
365
-
366
- try {
367
- const content = readFileSync(absPath, "utf-8");
368
- const escaped = JSON.stringify(content);
369
- return `<Markdown${attrs} __content={${escaped}} />`;
370
- } catch {
371
- return match;
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
- // Match <FilePreview ... /> (self-closing)
383
- return jsx.replace(
384
- /<FilePreview\b([^>]*?)\/>/g,
385
- (match, attrs: string) => {
386
- const filePath = attrs.match(/path=["']([^"']+)["']/)?.[1];
387
- if (!filePath) return match;
388
-
389
- const absPath = join(projectRoot, filePath);
390
-
391
- try {
392
- let content = readFileSync(absPath, "utf-8");
393
-
394
- // Apply lines filter if present
395
- const linesMatch = attrs.match(/lines=\{?\[(\d+)\s*,\s*(\d+)\]\}?/);
396
- if (linesMatch?.[1] && linesMatch[2]) {
397
- const start = parseInt(linesMatch[1], 10);
398
- const end = parseInt(linesMatch[2], 10);
399
- content = content.split("\n").slice(start - 1, end).join("\n");
400
- }
401
-
402
- // Escape for embedding in JSX string
403
- const escaped = JSON.stringify(content);
404
- return `<FilePreview${attrs} __content={${escaped}} />`;
405
- } catch {
406
- return match;
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
- return compileJsx(jsx, { projectRoot });
417
+ return compileJsx(jsx, { projectRoot })
415
418
  }