agent-canvas 0.1.37 → 0.1.38
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/.github/workflows/deploy-worker-prod.yml +3 -4
- package/.github/workflows/deploy-worker-stage.yml +3 -4
- package/.github/workflows/deploy-worker.yml +7 -8
- package/bin/lib/commands/push.ts +5 -1
- package/daemon/src/compiler.test.ts +39 -0
- package/daemon/src/compiler.ts +72 -11
- package/package.json +1 -1
|
@@ -14,7 +14,6 @@ jobs:
|
|
|
14
14
|
uses: ./.github/workflows/deploy-worker.yml
|
|
15
15
|
with:
|
|
16
16
|
env: prod
|
|
17
|
-
secrets
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
SHARE_AUTH_TOKEN: ${{ secrets.SHARE_AUTH_TOKEN_PROD }}
|
|
17
|
+
# inherit so the reusable workflow can resolve secrets; the env-scoped
|
|
18
|
+
# values (set on the `prod` environment) win over repo-level ones.
|
|
19
|
+
secrets: inherit
|
|
@@ -16,7 +16,6 @@ jobs:
|
|
|
16
16
|
uses: ./.github/workflows/deploy-worker.yml
|
|
17
17
|
with:
|
|
18
18
|
env: stage
|
|
19
|
-
secrets
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
SHARE_AUTH_TOKEN: ${{ secrets.SHARE_AUTH_TOKEN_STAGE }}
|
|
19
|
+
# inherit so the reusable workflow can resolve secrets; the env-scoped
|
|
20
|
+
# values (set on the `stage` environment) win over repo-level ones.
|
|
21
|
+
secrets: inherit
|
|
@@ -7,14 +7,13 @@ on:
|
|
|
7
7
|
description: "Target environment (stage or prod)"
|
|
8
8
|
required: true
|
|
9
9
|
type: string
|
|
10
|
-
secrets
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
required: false
|
|
10
|
+
# No `secrets:` inputs on purpose. The deploy job below declares
|
|
11
|
+
# `environment: ${{ inputs.env }}`, so referencing `secrets.X` directly
|
|
12
|
+
# resolves the matching *environment* secret (falling back to a
|
|
13
|
+
# repo-level secret of the same name). Callers pass `secrets: inherit`
|
|
14
|
+
# so org/repo secrets remain reachable too. This is what makes the
|
|
15
|
+
# CLOUDFLARE_* / SHARE_AUTH_TOKEN values environment-scoped — a prod
|
|
16
|
+
# token can never be read by a stage deploy and vice versa.
|
|
18
17
|
|
|
19
18
|
jobs:
|
|
20
19
|
deploy:
|
package/bin/lib/commands/push.ts
CHANGED
|
@@ -59,7 +59,11 @@ export async function handlePush(args: string[]) {
|
|
|
59
59
|
// Skip restart only for clear user errors (syntax errors with line/col,
|
|
60
60
|
// runtime validation errors) — those won't be fixed by a restart.
|
|
61
61
|
const isUserError = !result.ok && result.errors && Object.values(result.errors).every((e: any) =>
|
|
62
|
-
typeof e === "string" && (
|
|
62
|
+
typeof e === "string" && (
|
|
63
|
+
/\(line \d+, col \d+\)/.test(e) ||
|
|
64
|
+
/Syntax Error|Parse error|Mermaid syntax error/i.test(e) ||
|
|
65
|
+
e.startsWith("Runtime error:")
|
|
66
|
+
));
|
|
63
67
|
const shouldRestart = !result.ok && result.error === "Canvas compilation failed" && !isUserError;
|
|
64
68
|
if (shouldRestart) {
|
|
65
69
|
console.error("Compilation failed, restarting daemon and retrying...");
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { test, expect } from "bun:test";
|
|
2
|
+
import { compilePlan } from "./compiler";
|
|
3
|
+
|
|
4
|
+
// Regression coverage for the error-reporting path. Bun surfaces JSX syntax
|
|
5
|
+
// errors through three shapes (Transpiler throw, build logs, AggregateError
|
|
6
|
+
// from Bun.build); all must reach the CLI as an actionable message carrying a
|
|
7
|
+
// position + code frame — never a bare "Parse error" / "Bundle failed".
|
|
8
|
+
|
|
9
|
+
test("syntax error reports line, column and a code frame", async () => {
|
|
10
|
+
// A straight double-quote inside a double-quoted attribute terminates the
|
|
11
|
+
// attribute early — the exact mistake that previously printed only
|
|
12
|
+
// "Parse error" with no location.
|
|
13
|
+
const result = await compilePlan(`<Section title="a "b" c">x</Section>`);
|
|
14
|
+
|
|
15
|
+
expect(result.ok).toBe(false);
|
|
16
|
+
if (result.ok) return;
|
|
17
|
+
expect(result.error).toMatch(/\(line \d+, col \d+\)/);
|
|
18
|
+
// Code frame: offending source line + caret.
|
|
19
|
+
expect(result.error).toContain("^");
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
test("syntax error line maps back to the authored file (1-based, not offset by injected imports)", async () => {
|
|
23
|
+
// Bad token on the authored 3rd line; the reported line must be small/positive,
|
|
24
|
+
// not shifted by the prepended component-import preamble.
|
|
25
|
+
const result = await compilePlan(`<Section title="ok">\n <Item id="a" label="x">\n <span a="1 "2"></span>\n </Item>\n</Section>`);
|
|
26
|
+
|
|
27
|
+
expect(result.ok).toBe(false);
|
|
28
|
+
if (result.ok) return;
|
|
29
|
+
const m = result.error.match(/\(line (\d+), col \d+\)/);
|
|
30
|
+
expect(m).not.toBeNull();
|
|
31
|
+
const line = Number(m![1]);
|
|
32
|
+
expect(line).toBeGreaterThan(0);
|
|
33
|
+
expect(line).toBeLessThan(10);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test("well-formed canvas compiles", async () => {
|
|
37
|
+
const result = await compilePlan(`<Section title="Hello"><Item id="a" label="b">text</Item></Section>`);
|
|
38
|
+
expect(result.ok).toBe(true);
|
|
39
|
+
});
|
package/daemon/src/compiler.ts
CHANGED
|
@@ -130,7 +130,59 @@ async function validateCompiledPlan(js: string): Promise<{ ok: true } | { ok: fa
|
|
|
130
130
|
return { ok: true };
|
|
131
131
|
}
|
|
132
132
|
|
|
133
|
+
/**
|
|
134
|
+
* Normalize a Bun build/transpile failure into a precise, actionable string.
|
|
135
|
+
*
|
|
136
|
+
* Bun surfaces JSX syntax errors through three different shapes, all handled
|
|
137
|
+
* here so the CLI never again prints a bare "Parse error" / "Bundle failed":
|
|
138
|
+
* - `Bun.Transpiler.transformSync` throws a `BuildMessage` (has `.position`).
|
|
139
|
+
* - `Bun.build()` returns `{ success: false, logs: [BuildMessage] }`.
|
|
140
|
+
* - `Bun.build()` throws an `AggregateError` whose `.errors[0]` is a
|
|
141
|
+
* `BuildMessage` — the case the old catch discarded, returning only the
|
|
142
|
+
* useless top-level "Bundle failed".
|
|
143
|
+
*
|
|
144
|
+
* `lineOffset` is the number of lines Canvas prepended (component imports +
|
|
145
|
+
* wrapper) so the reported line maps back to the user's authored file. When a
|
|
146
|
+
* position is available we append `(line N, col M)` plus a one-line code frame
|
|
147
|
+
* with a caret — that suffix is also what lets the CLI classify the error as a
|
|
148
|
+
* user error and skip a pointless daemon restart (see push.ts).
|
|
149
|
+
*/
|
|
150
|
+
function formatBuildError(err: any, lineOffset: number): string {
|
|
151
|
+
// Collect every candidate that might carry a `.position`, most specific first.
|
|
152
|
+
const candidates: any[] = [];
|
|
153
|
+
if (err?.position) candidates.push(err);
|
|
154
|
+
if (Array.isArray(err?.errors)) candidates.push(...err.errors);
|
|
155
|
+
if (!candidates.length && err && typeof err[Symbol.iterator] === "function") {
|
|
156
|
+
try { candidates.push(...err); } catch {}
|
|
157
|
+
}
|
|
158
|
+
const detail = candidates.find((c) => c?.position) ?? candidates[0] ?? err;
|
|
159
|
+
const message = String(detail?.message || err?.message || "Syntax error").split("\n")[0];
|
|
160
|
+
|
|
161
|
+
const pos = detail?.position;
|
|
162
|
+
if (!pos || typeof pos.line !== "number") return message;
|
|
163
|
+
|
|
164
|
+
const loc = ` (line ${pos.line - lineOffset}, col ${pos.column})`;
|
|
165
|
+
let frame = "";
|
|
166
|
+
if (typeof pos.lineText === "string") {
|
|
167
|
+
const caret = " ".repeat(Math.max(0, pos.column ?? 0)) + "^";
|
|
168
|
+
frame = `\n ${pos.lineText}\n ${caret}`;
|
|
169
|
+
}
|
|
170
|
+
return `${message}${loc}${frame}`;
|
|
171
|
+
}
|
|
172
|
+
|
|
133
173
|
export async function compilePlan(jsx: string, projectRoot?: string): Promise<CompileResult> {
|
|
174
|
+
// Strip leading pragma / block / line comments (e.g. `/** @jsxImportSource ... */`).
|
|
175
|
+
// Authored JSX sometimes carries a JSX-runtime pragma from another toolchain;
|
|
176
|
+
// Canvas injects its own imports and uses the React automatic runtime, so the
|
|
177
|
+
// pragma is unused. Left in place it sits at the top level of the wrapped
|
|
178
|
+
// fragment as plain text (JSX only treats `{/* */}` as a comment) and renders
|
|
179
|
+
// literally on the page.
|
|
180
|
+
let stripped: string;
|
|
181
|
+
do {
|
|
182
|
+
stripped = jsx;
|
|
183
|
+
jsx = jsx.replace(/^\s*(?:\/\*[\s\S]*?\*\/|\/\/[^\n]*)\s*/, "");
|
|
184
|
+
} while (jsx !== stripped);
|
|
185
|
+
|
|
134
186
|
// Resolve file contents at compile time
|
|
135
187
|
if (projectRoot) {
|
|
136
188
|
jsx = resolveFilePreviews(jsx, projectRoot);
|
|
@@ -150,18 +202,18 @@ export async function compilePlan(jsx: string, projectRoot?: string): Promise<Co
|
|
|
150
202
|
? `${COMPONENT_IMPORTS}\n${jsx}`
|
|
151
203
|
: `${COMPONENT_IMPORTS}\nexport default function Plan() {\n return (<>${jsx}</>);\n}\n`;
|
|
152
204
|
|
|
205
|
+
// Offset: COMPONENT_IMPORTS lines + wrapper lines (export default function
|
|
206
|
+
// Plan() + return). Used to map Bun's line numbers back to the authored file.
|
|
207
|
+
const wrapperLines = hasDefaultExport ? 1 : 2;
|
|
208
|
+
const lineOffset = COMPONENT_IMPORTS_LINES + wrapperLines;
|
|
209
|
+
|
|
153
210
|
// Pre-validate syntax with Bun.Transpiler — gives precise error positions,
|
|
154
211
|
// unlike Bun.build() which throws opaque "Bundle failed" for syntax errors.
|
|
155
212
|
try {
|
|
156
213
|
const transpiler = new Bun.Transpiler({ loader: "jsx" });
|
|
157
214
|
transpiler.transformSync(source);
|
|
158
215
|
} catch (syntaxError: any) {
|
|
159
|
-
|
|
160
|
-
// Offset: COMPONENT_IMPORTS lines + wrapper lines (export default function Plan() + return)
|
|
161
|
-
const wrapperLines = hasDefaultExport ? 1 : 2;
|
|
162
|
-
const lineOffset = COMPONENT_IMPORTS_LINES + wrapperLines;
|
|
163
|
-
const loc = pos ? ` (line ${pos.line - lineOffset}, col ${pos.column})` : "";
|
|
164
|
-
return { ok: false, error: `${syntaxError?.message || "Syntax error"}${loc}` };
|
|
216
|
+
return { ok: false, error: formatBuildError(syntaxError, lineOffset) };
|
|
165
217
|
}
|
|
166
218
|
|
|
167
219
|
const tmpFile = join(COMPILE_TEMP_DIR, `plan-${randomUUID()}.jsx`);
|
|
@@ -178,9 +230,9 @@ export async function compilePlan(jsx: string, projectRoot?: string): Promise<Co
|
|
|
178
230
|
});
|
|
179
231
|
|
|
180
232
|
if (!result.success) {
|
|
181
|
-
const
|
|
182
|
-
|
|
183
|
-
.map((l) => l
|
|
233
|
+
const errorLogs = result.logs.filter((l) => l.level === "error");
|
|
234
|
+
const errors = errorLogs
|
|
235
|
+
.map((l) => formatBuildError(l, lineOffset))
|
|
184
236
|
.join("\n");
|
|
185
237
|
return { ok: false, error: errors || "Bundle failed" };
|
|
186
238
|
}
|
|
@@ -192,8 +244,17 @@ export async function compilePlan(jsx: string, projectRoot?: string): Promise<Co
|
|
|
192
244
|
}
|
|
193
245
|
return { ok: true, js };
|
|
194
246
|
} catch (buildError: any) {
|
|
195
|
-
//
|
|
196
|
-
//
|
|
247
|
+
// A real syntax error surfaces as an AggregateError carrying BuildMessages
|
|
248
|
+
// (with positions) — format it precisely. The opaque "Unknown Error, TODO"
|
|
249
|
+
// corruption case (internal bundler state gets corrupted in long-running
|
|
250
|
+
// processes) has no sub-errors; pass it through so the CLI restarts the
|
|
251
|
+
// daemon and retries.
|
|
252
|
+
const hasDetail =
|
|
253
|
+
buildError?.position ||
|
|
254
|
+
(Array.isArray(buildError?.errors) && buildError.errors.length > 0);
|
|
255
|
+
if (hasDetail) {
|
|
256
|
+
return { ok: false, error: formatBuildError(buildError, lineOffset) };
|
|
257
|
+
}
|
|
197
258
|
return { ok: false, error: buildError?.message || "Unknown Error" };
|
|
198
259
|
}
|
|
199
260
|
} catch (e: any) {
|