@cloudcannon/editable-regions 0.0.20-rc.1 → 0.0.20-rc.3
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/components/ui/editable-region-error-card.ts +9 -0
- package/integrations/hugo/browser/errors.ts +74 -9
- package/integrations/hugo/browser/hugo-renderer.ts +282 -0
- package/integrations/hugo/browser/hugo-wasm.ts +116 -0
- package/integrations/hugo/browser/index.ts +31 -211
- package/integrations/hugo/browser/worker-protocol.ts +18 -0
- package/integrations/hugo/browser/worker.ts +51 -0
- package/integrations/hugo/hugo-module/layouts/partials/editable-regions/resources.html +11 -0
- package/integrations/hugo/renderer/go.mod +4 -4
- package/integrations/hugo/renderer/go.sum +6 -6
- package/integrations/hugo/renderer/main.go +41 -14
- package/package.json +1 -1
- package/types/hugo.d.ts +6 -2
|
@@ -64,6 +64,15 @@ export default class EditableRegionErrorCard extends HTMLElement {
|
|
|
64
64
|
|
|
65
65
|
body.appendChild(stack);
|
|
66
66
|
}
|
|
67
|
+
|
|
68
|
+
if ("hint" in error && typeof error.hint === "string") {
|
|
69
|
+
const hint = error.hint;
|
|
70
|
+
const hintElement = document.createElement("p");
|
|
71
|
+
hintElement.className = "hint";
|
|
72
|
+
hintElement.innerHTML = hint;
|
|
73
|
+
|
|
74
|
+
body.appendChild(hintElement);
|
|
75
|
+
}
|
|
67
76
|
}
|
|
68
77
|
|
|
69
78
|
if (this.hasAttribute("hint")) {
|
|
@@ -3,30 +3,95 @@
|
|
|
3
3
|
* on the core's component error card, so it should tell the user what to fix.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
+
const TEMPLATE_FRAME_RE =
|
|
7
|
+
/^template: (.+?): executing ".*?" at .*?: error calling (.+?): /;
|
|
8
|
+
const FAILED_SEPARATOR = ": execute of template failed: ";
|
|
9
|
+
|
|
10
|
+
export interface ParsedHugoError {
|
|
11
|
+
/** Template refs (file:line:column), outermost call first. */
|
|
12
|
+
frames: string[];
|
|
13
|
+
/** The template function whose call failed in the innermost frame. */
|
|
14
|
+
call?: string;
|
|
15
|
+
/** The underlying error, with the template-call chain stripped. */
|
|
16
|
+
message: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function parseHugoTemplateError(raw: string): ParsedHugoError {
|
|
20
|
+
const frames: string[] = [];
|
|
21
|
+
let call: string | undefined;
|
|
22
|
+
let rest = raw.trim();
|
|
23
|
+
for (;;) {
|
|
24
|
+
const match = rest.match(TEMPLATE_FRAME_RE);
|
|
25
|
+
if (!match) break;
|
|
26
|
+
frames.push(match[1]);
|
|
27
|
+
call = match[2];
|
|
28
|
+
rest = rest.slice(match[0].length);
|
|
29
|
+
const quotedSeparator = rest.match(
|
|
30
|
+
/^"[^"]*": execute of template failed: /,
|
|
31
|
+
);
|
|
32
|
+
if (quotedSeparator) {
|
|
33
|
+
rest = rest.slice(quotedSeparator[0].length);
|
|
34
|
+
} else {
|
|
35
|
+
const separator = rest.indexOf(FAILED_SEPARATOR);
|
|
36
|
+
const after =
|
|
37
|
+
separator === -1 ? "" : rest.slice(separator + FAILED_SEPARATOR.length);
|
|
38
|
+
if (after.startsWith("template: ")) {
|
|
39
|
+
rest = after;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return { frames, call, message: rest };
|
|
44
|
+
}
|
|
45
|
+
|
|
6
46
|
export function enhanceHugoError(message: string, componentKey: string): Error {
|
|
47
|
+
const parsed = parseHugoTemplateError(message);
|
|
48
|
+
|
|
7
49
|
let hint = "";
|
|
8
50
|
|
|
9
51
|
if (/partial .* not found/i.test(message)) {
|
|
10
52
|
hint =
|
|
11
|
-
"
|
|
53
|
+
"This partial isn't in the bundled template snapshot. Check that it " +
|
|
12
54
|
"lives under one of the directories in " +
|
|
13
55
|
"`params.editable_regions.template_dirs` (by default the partials, " +
|
|
14
56
|
"render hooks, and shortcodes of your configured layout dir) " +
|
|
15
57
|
"and rebuild the site.";
|
|
16
|
-
} else if (
|
|
58
|
+
} else if (parsed.frames.length > 0) {
|
|
17
59
|
hint =
|
|
18
|
-
"
|
|
19
|
-
"
|
|
20
|
-
"
|
|
60
|
+
"The partial errored while rendering in the editor. If the code " +
|
|
61
|
+
"should only run in the site build, guard it with " +
|
|
62
|
+
"`{{ if not site.Params.env_client }}`, " +
|
|
63
|
+
"or supply an editor-safe version via " +
|
|
64
|
+
"`params.editable_regions.templates_overrides`.";
|
|
21
65
|
} else if (/logged \d+ errors/i.test(message)) {
|
|
22
66
|
hint =
|
|
23
|
-
"
|
|
67
|
+
"Hugo logged errors during the render — open the browser console " +
|
|
24
68
|
"for the underlying messages.";
|
|
25
69
|
}
|
|
26
70
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
71
|
+
const primary =
|
|
72
|
+
parsed.frames.length > 0 && parsed.call
|
|
73
|
+
? `${parsed.call}: ${parsed.message}`
|
|
74
|
+
: parsed.message;
|
|
75
|
+
const sentence = /[.!?]$/.test(primary) ? primary : `${primary}.`;
|
|
76
|
+
|
|
77
|
+
const error = new Error(
|
|
78
|
+
`Failed to render Hugo component "${componentKey}": ${sentence}`,
|
|
79
|
+
) as Error & { hint?: string };
|
|
80
|
+
|
|
81
|
+
if (hint) {
|
|
82
|
+
error.hint = hint;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (parsed.frames.length > 0) {
|
|
86
|
+
error.stack = parsed.frames
|
|
87
|
+
.slice()
|
|
88
|
+
.reverse()
|
|
89
|
+
.slice(0, 10)
|
|
90
|
+
.map((ref) => ` at ${ref}`)
|
|
91
|
+
.join("\n");
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return error;
|
|
30
95
|
}
|
|
31
96
|
|
|
32
97
|
/** Error for a partial missing from the editor's template bundle; raised when
|
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
import { enhanceHugoError, missingComponentError } from "./errors.ts";
|
|
2
|
+
import { HugoWasm } from "./hugo-wasm.ts";
|
|
3
|
+
import { group, groupEnd, log, warn } from "./logger.ts";
|
|
4
|
+
import type { WorkerRequest, WorkerResponse } from "./worker-protocol.ts";
|
|
5
|
+
|
|
6
|
+
export interface EngineTransport {
|
|
7
|
+
boot(args: {
|
|
8
|
+
wasmUrl: string;
|
|
9
|
+
env: string;
|
|
10
|
+
files: Record<string, string>;
|
|
11
|
+
}): Promise<void>;
|
|
12
|
+
writeFiles(json: string): Promise<void>;
|
|
13
|
+
removeFiles(json: string): Promise<void>;
|
|
14
|
+
init(): Promise<string | undefined>;
|
|
15
|
+
render(payload: string): Promise<{ html?: string; error?: string }>;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
class WorkerClient implements EngineTransport {
|
|
19
|
+
private pending = new Map<string, (response: WorkerResponse) => void>();
|
|
20
|
+
private nextId = 0;
|
|
21
|
+
|
|
22
|
+
constructor(private worker: Worker) {
|
|
23
|
+
worker.onmessage = (event: MessageEvent<WorkerResponse>) => {
|
|
24
|
+
const response = event.data;
|
|
25
|
+
this.pending.get(response.id)?.(response);
|
|
26
|
+
this.pending.delete(response.id);
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
private send(request: WorkerRequest): Promise<WorkerResponse> {
|
|
31
|
+
return new Promise((resolve) => {
|
|
32
|
+
this.pending.set(request.id, resolve);
|
|
33
|
+
this.worker.postMessage(request);
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async boot({
|
|
38
|
+
wasmUrl,
|
|
39
|
+
env,
|
|
40
|
+
files,
|
|
41
|
+
}: {
|
|
42
|
+
wasmUrl: string;
|
|
43
|
+
env: string;
|
|
44
|
+
files: Record<string, string>;
|
|
45
|
+
}): Promise<void> {
|
|
46
|
+
const response = await this.send({
|
|
47
|
+
id: `cc-worker-${this.nextId++}`,
|
|
48
|
+
kind: "boot",
|
|
49
|
+
wasmUrl,
|
|
50
|
+
env,
|
|
51
|
+
files,
|
|
52
|
+
});
|
|
53
|
+
if (response.error) {
|
|
54
|
+
throw new Error(response.error);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async writeFiles(json: string): Promise<void> {
|
|
59
|
+
await this.send({
|
|
60
|
+
id: `cc-worker-${this.nextId++}`,
|
|
61
|
+
kind: "writeFiles",
|
|
62
|
+
json,
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async removeFiles(json: string): Promise<void> {
|
|
67
|
+
await this.send({
|
|
68
|
+
id: `cc-worker-${this.nextId++}`,
|
|
69
|
+
kind: "removeFiles",
|
|
70
|
+
json,
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async init(): Promise<string | undefined> {
|
|
75
|
+
const response = await this.send({
|
|
76
|
+
id: `cc-worker-${this.nextId++}`,
|
|
77
|
+
kind: "init",
|
|
78
|
+
});
|
|
79
|
+
return response.error;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async render(payload: string): Promise<{ html?: string; error?: string }> {
|
|
83
|
+
const response = await this.send({
|
|
84
|
+
id: `cc-worker-${this.nextId++}`,
|
|
85
|
+
kind: "render",
|
|
86
|
+
payload,
|
|
87
|
+
});
|
|
88
|
+
return { html: response.html, error: response.error };
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
interface QueuedRender {
|
|
93
|
+
id: string;
|
|
94
|
+
partial: string;
|
|
95
|
+
props: Record<string, any>;
|
|
96
|
+
resolve: (el: HTMLElement) => void;
|
|
97
|
+
reject: (err: unknown) => void;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const BATCH_WINDOW_MS = 10;
|
|
101
|
+
|
|
102
|
+
export interface HugoRendererOptions {
|
|
103
|
+
wasmUrl: string;
|
|
104
|
+
env: string;
|
|
105
|
+
workerUrl?: string;
|
|
106
|
+
getTarget: () => string;
|
|
107
|
+
getFiles: () => Record<string, string>;
|
|
108
|
+
onBooted?: () => Promise<void>;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Main-thread facade over the Hugo engine: owns batching, the render-id demux
|
|
113
|
+
* into real elements, and transport selection. Runs the engine in a worker
|
|
114
|
+
* when one is provided and constructible, falling back to an in-page
|
|
115
|
+
* HugoWasm; the interface returns HTMLElements either way.
|
|
116
|
+
*/
|
|
117
|
+
export class HugoRenderer {
|
|
118
|
+
private enginePromise: Promise<void> | null = null;
|
|
119
|
+
private transportPromise: Promise<EngineTransport> | null = null;
|
|
120
|
+
private batch: QueuedRender[] = [];
|
|
121
|
+
private batchTimer: ReturnType<typeof setTimeout> | null = null;
|
|
122
|
+
private nextRenderId = 0;
|
|
123
|
+
|
|
124
|
+
constructor(private options: HugoRendererOptions) {}
|
|
125
|
+
|
|
126
|
+
async ready(): Promise<void> {
|
|
127
|
+
this.enginePromise ??= this.start().catch((err) => {
|
|
128
|
+
this.enginePromise = null;
|
|
129
|
+
this.transportPromise = null;
|
|
130
|
+
throw err;
|
|
131
|
+
});
|
|
132
|
+
await this.enginePromise;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
async writeFiles(json: string): Promise<void> {
|
|
136
|
+
await (await this.transport()).writeFiles(json);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
async removeFiles(json: string): Promise<void> {
|
|
140
|
+
await (await this.transport()).removeFiles(json);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
async renderPartial(
|
|
144
|
+
partial: string,
|
|
145
|
+
props: Record<string, any> = {},
|
|
146
|
+
): Promise<HTMLElement> {
|
|
147
|
+
await this.ready();
|
|
148
|
+
return new Promise<HTMLElement>((resolve, reject) => {
|
|
149
|
+
const id = `cc-render-${this.nextRenderId++}`;
|
|
150
|
+
log("Queueing Hugo component:", partial, "Props:", props);
|
|
151
|
+
this.batch.push({ id, partial, props, resolve, reject });
|
|
152
|
+
if (this.batchTimer === null) {
|
|
153
|
+
this.batchTimer = setTimeout(() => this.flushBatch(), BATCH_WINDOW_MS);
|
|
154
|
+
}
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
private transport(): Promise<EngineTransport> {
|
|
159
|
+
this.transportPromise ??= Promise.resolve(this.createEngine());
|
|
160
|
+
return this.transportPromise;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
private createEngine(): EngineTransport {
|
|
164
|
+
if (this.options.workerUrl && typeof Worker === "function") {
|
|
165
|
+
try {
|
|
166
|
+
log("Starting Hugo renderer in a worker");
|
|
167
|
+
return new WorkerClient(new Worker(this.options.workerUrl));
|
|
168
|
+
} catch {
|
|
169
|
+
warn("Hugo worker unavailable, rendering on the main thread");
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
return new HugoWasm();
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
private async start(): Promise<void> {
|
|
176
|
+
const engine = await this.transport();
|
|
177
|
+
|
|
178
|
+
group("Starting Hugo renderer");
|
|
179
|
+
await engine.boot({
|
|
180
|
+
wasmUrl: this.options.wasmUrl,
|
|
181
|
+
env: this.options.env,
|
|
182
|
+
files: this.options.getFiles(),
|
|
183
|
+
});
|
|
184
|
+
await this.options.onBooted?.();
|
|
185
|
+
|
|
186
|
+
const initError = await engine.init();
|
|
187
|
+
if (initError) {
|
|
188
|
+
groupEnd();
|
|
189
|
+
throw new Error(`Hugo editor site failed to build: ${initError}`);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
log("Hugo renderer ready");
|
|
193
|
+
groupEnd();
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
private flushBatch(): void {
|
|
197
|
+
this.batchTimer = null;
|
|
198
|
+
const queued = this.batch;
|
|
199
|
+
this.batch = [];
|
|
200
|
+
if (queued.length === 0) return;
|
|
201
|
+
|
|
202
|
+
group(`Rendering ${queued.length} Hugo component(s)`);
|
|
203
|
+
this.runBatch(queued);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
private async runBatch(queued: QueuedRender[]): Promise<void> {
|
|
207
|
+
try {
|
|
208
|
+
const engine = await this.ready().then(() => this.transport());
|
|
209
|
+
const result = await engine.render(
|
|
210
|
+
JSON.stringify({
|
|
211
|
+
target: this.options.getTarget(),
|
|
212
|
+
requests: queued.map(({ id, partial, props }) => ({
|
|
213
|
+
id,
|
|
214
|
+
partial,
|
|
215
|
+
props,
|
|
216
|
+
})),
|
|
217
|
+
}),
|
|
218
|
+
);
|
|
219
|
+
this.demuxBatch(queued, result);
|
|
220
|
+
} catch (err) {
|
|
221
|
+
for (const { partial, reject } of queued) {
|
|
222
|
+
reject(enhanceHugoError(String(err), partial));
|
|
223
|
+
}
|
|
224
|
+
} finally {
|
|
225
|
+
groupEnd();
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
private demuxBatch(
|
|
230
|
+
queued: QueuedRender[],
|
|
231
|
+
result: { html?: string; error?: string } | null,
|
|
232
|
+
): void {
|
|
233
|
+
if (result?.error || typeof result?.html !== "string") {
|
|
234
|
+
log("Render error:", result?.error);
|
|
235
|
+
for (const { partial, reject } of queued) {
|
|
236
|
+
reject(enhanceHugoError(result?.error ?? "no output", partial));
|
|
237
|
+
}
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const holder = document.createElement("div");
|
|
242
|
+
holder.innerHTML = result.html;
|
|
243
|
+
for (const render of queued) {
|
|
244
|
+
const keyed = holder.querySelector<HTMLElement>(
|
|
245
|
+
`[data-cc-render="${render.id}"]`,
|
|
246
|
+
);
|
|
247
|
+
if (!keyed) {
|
|
248
|
+
render.reject(
|
|
249
|
+
new Error(
|
|
250
|
+
`Hugo render produced no output for component "${render.partial}"`,
|
|
251
|
+
),
|
|
252
|
+
);
|
|
253
|
+
continue;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const missing = keyed.querySelector("cc-missing-partial");
|
|
257
|
+
if (missing) {
|
|
258
|
+
render.reject(
|
|
259
|
+
missingComponentError(
|
|
260
|
+
missing.getAttribute("data-name") || render.partial,
|
|
261
|
+
),
|
|
262
|
+
);
|
|
263
|
+
continue;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
const failed = keyed.querySelector("cc-failed-partial");
|
|
267
|
+
if (failed) {
|
|
268
|
+
render.reject(
|
|
269
|
+
enhanceHugoError(
|
|
270
|
+
failed.getAttribute("data-message") || "unknown error",
|
|
271
|
+
failed.getAttribute("data-name") || render.partial,
|
|
272
|
+
),
|
|
273
|
+
);
|
|
274
|
+
continue;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
keyed.removeAttribute("data-cc-render");
|
|
278
|
+
log("Rendered HTML preview:", keyed.innerHTML.substring(0, 200));
|
|
279
|
+
render.resolve(keyed);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import "./wasm_exec.js";
|
|
2
|
+
|
|
3
|
+
export interface HugoWasmBootArgs {
|
|
4
|
+
wasmUrl: string;
|
|
5
|
+
env: string;
|
|
6
|
+
files: Record<string, string>;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
interface PendingOp {
|
|
10
|
+
run: () => Promise<unknown>;
|
|
11
|
+
resolve: (value: unknown) => void;
|
|
12
|
+
reject: (err: unknown) => void;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export class HugoWasm {
|
|
16
|
+
private pending: PendingOp[] = [];
|
|
17
|
+
private draining = false;
|
|
18
|
+
|
|
19
|
+
async boot(args: HugoWasmBootArgs): Promise<void> {
|
|
20
|
+
await this.enqueue(() => this.runBoot(args));
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
writeFiles(json: string): Promise<void> {
|
|
24
|
+
return this.enqueue(async () => {
|
|
25
|
+
writeHugoFiles(json);
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
removeFiles(json: string): Promise<void> {
|
|
30
|
+
return this.enqueue(async () => {
|
|
31
|
+
removeHugoFiles(json);
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async init(): Promise<string | undefined> {
|
|
36
|
+
return this.enqueue(async () => initHugoEditorSite()?.error);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
render(payload: string): Promise<{ html?: string; error?: string }> {
|
|
40
|
+
return this.enqueue(
|
|
41
|
+
() =>
|
|
42
|
+
new Promise((resolve) => {
|
|
43
|
+
renderHugoPartials(payload, resolve);
|
|
44
|
+
}),
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
private enqueue<T>(run: () => Promise<T>): Promise<T> {
|
|
49
|
+
return new Promise<T>((resolve, reject) => {
|
|
50
|
+
this.pending.push({
|
|
51
|
+
run,
|
|
52
|
+
resolve: resolve as (value: unknown) => void,
|
|
53
|
+
reject,
|
|
54
|
+
});
|
|
55
|
+
this.pump();
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
private pump(): void {
|
|
60
|
+
if (this.draining) return;
|
|
61
|
+
this.draining = true;
|
|
62
|
+
this.drain();
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
private async drain(): Promise<void> {
|
|
66
|
+
while (this.pending.length > 0) {
|
|
67
|
+
const op = this.pending.shift();
|
|
68
|
+
if (!op) break;
|
|
69
|
+
try {
|
|
70
|
+
op.resolve(await op.run());
|
|
71
|
+
} catch (err) {
|
|
72
|
+
op.reject(err);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
this.draining = false;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
private async runBoot({
|
|
79
|
+
wasmUrl,
|
|
80
|
+
env,
|
|
81
|
+
files,
|
|
82
|
+
}: HugoWasmBootArgs): Promise<void> {
|
|
83
|
+
const response = await fetch(wasmUrl);
|
|
84
|
+
if (!response.ok || !response.body) {
|
|
85
|
+
throw new Error(
|
|
86
|
+
`Failed to fetch Hugo WASM from ${wasmUrl}: HTTP ${response.status}`,
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
let wasmBuffer: ArrayBuffer;
|
|
91
|
+
if (wasmUrl.endsWith(".gz")) {
|
|
92
|
+
const decompressed = response.body.pipeThrough(
|
|
93
|
+
new DecompressionStream("gzip"),
|
|
94
|
+
);
|
|
95
|
+
wasmBuffer = await new Response(decompressed).arrayBuffer();
|
|
96
|
+
} else {
|
|
97
|
+
wasmBuffer = await response.arrayBuffer();
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const go = new (globalThis as any).Go();
|
|
101
|
+
const { instance } = await WebAssembly.instantiate(
|
|
102
|
+
wasmBuffer,
|
|
103
|
+
go.importObject,
|
|
104
|
+
);
|
|
105
|
+
go.run(instance);
|
|
106
|
+
|
|
107
|
+
while (
|
|
108
|
+
typeof (globalThis as { renderHugoPartials?: unknown })
|
|
109
|
+
.renderHugoPartials !== "function"
|
|
110
|
+
) {
|
|
111
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
writeHugoFiles(JSON.stringify({ ...files, "cc-env": env }));
|
|
115
|
+
}
|
|
116
|
+
}
|
|
@@ -1,10 +1,9 @@
|
|
|
1
|
-
import "./wasm_exec.js";
|
|
2
1
|
import {
|
|
3
2
|
apiLoadedPromise,
|
|
4
3
|
CloudCannon,
|
|
5
4
|
} from "../../../helpers/cloudcannon.mjs";
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
5
|
+
import { HugoRenderer } from "./hugo-renderer.ts";
|
|
6
|
+
import { log, setVerbose, warn } from "./logger.ts";
|
|
8
7
|
|
|
9
8
|
/** A `(props) => HTMLElement` renderer installed on `window.cc_components`. */
|
|
10
9
|
type HugoComponentRenderer = (
|
|
@@ -15,8 +14,19 @@ type HugoComponentRenderer = (
|
|
|
15
14
|
* no associated file (the renderer then falls back to the home page). */
|
|
16
15
|
let currentFilePath = "";
|
|
17
16
|
|
|
18
|
-
|
|
19
|
-
|
|
17
|
+
let renderer: HugoRenderer | null = null;
|
|
18
|
+
|
|
19
|
+
function getRenderer(): HugoRenderer {
|
|
20
|
+
renderer ??= new HugoRenderer({
|
|
21
|
+
wasmUrl: window.cc_hugo?.wasmUrl ?? "/_cloudcannon/hugo_renderer.wasm.gz",
|
|
22
|
+
env: window.cc_hugo?.env ?? "production",
|
|
23
|
+
workerUrl: window.cc_hugo?.workerUrl,
|
|
24
|
+
getTarget: () => currentFilePath,
|
|
25
|
+
getFiles: () => ({ ...(window.cc_hugo_files ?? {}) }),
|
|
26
|
+
onBooted: () => loadAPIData(getRenderer()),
|
|
27
|
+
});
|
|
28
|
+
return renderer;
|
|
29
|
+
}
|
|
20
30
|
|
|
21
31
|
/**
|
|
22
32
|
* Entry point, called by the prebuilt runtime bundle. Installs the component
|
|
@@ -36,84 +46,15 @@ export function initHugoLiveEditing(): void {
|
|
|
36
46
|
initComponentProxy();
|
|
37
47
|
|
|
38
48
|
apiLoadedPromise.then(() => {
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
49
|
+
getRenderer()
|
|
50
|
+
.ready()
|
|
51
|
+
.catch((err) => {
|
|
52
|
+
warn("Failed to start the Hugo renderer:", err);
|
|
53
|
+
});
|
|
42
54
|
});
|
|
43
55
|
}
|
|
44
56
|
|
|
45
|
-
|
|
46
|
-
export function ensureEngine(): Promise<void> {
|
|
47
|
-
if (!enginePromise) {
|
|
48
|
-
enginePromise = startEngine().catch((err) => {
|
|
49
|
-
// Allow a retry on transient failures (e.g. a dropped WASM fetch).
|
|
50
|
-
enginePromise = null;
|
|
51
|
-
throw err;
|
|
52
|
-
});
|
|
53
|
-
}
|
|
54
|
-
return enginePromise;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
async function startEngine(): Promise<void> {
|
|
58
|
-
const wasmUrl =
|
|
59
|
-
window.cc_hugo?.wasmUrl ?? "/_cloudcannon/hugo_renderer.wasm.gz";
|
|
60
|
-
|
|
61
|
-
group("Starting Hugo renderer");
|
|
62
|
-
log("Fetching WASM from", wasmUrl);
|
|
63
|
-
|
|
64
|
-
const response = await fetch(wasmUrl);
|
|
65
|
-
if (!response.ok || !response.body) {
|
|
66
|
-
groupEnd();
|
|
67
|
-
throw new Error(
|
|
68
|
-
`Failed to fetch Hugo WASM from ${wasmUrl}: HTTP ${response.status}`,
|
|
69
|
-
);
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
let wasmBuffer: ArrayBuffer;
|
|
73
|
-
if (wasmUrl.endsWith(".gz")) {
|
|
74
|
-
const decompressed = response.body.pipeThrough(
|
|
75
|
-
new DecompressionStream("gzip"),
|
|
76
|
-
);
|
|
77
|
-
wasmBuffer = await new Response(decompressed).arrayBuffer();
|
|
78
|
-
} else {
|
|
79
|
-
wasmBuffer = await response.arrayBuffer();
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
const go = new (globalThis as any).Go();
|
|
83
|
-
const { instance } = await WebAssembly.instantiate(
|
|
84
|
-
wasmBuffer,
|
|
85
|
-
go.importObject,
|
|
86
|
-
);
|
|
87
|
-
go.run(instance);
|
|
88
|
-
|
|
89
|
-
// The Go side registers its globals synchronously at startup.
|
|
90
|
-
while (
|
|
91
|
-
typeof (globalThis as { renderHugoPartials?: unknown })
|
|
92
|
-
.renderHugoPartials !== "function"
|
|
93
|
-
) {
|
|
94
|
-
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
const files = {
|
|
98
|
-
...(window.cc_hugo_files ?? {}),
|
|
99
|
-
"cc-env": window.cc_hugo?.env ?? "production",
|
|
100
|
-
};
|
|
101
|
-
writeHugoFiles(JSON.stringify(files));
|
|
102
|
-
|
|
103
|
-
await loadAPIData();
|
|
104
|
-
|
|
105
|
-
const initError = initHugoEditorSite();
|
|
106
|
-
|
|
107
|
-
if (initError?.error) {
|
|
108
|
-
groupEnd();
|
|
109
|
-
throw new Error(`Hugo editor site failed to build: ${initError.error}`);
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
log("Hugo renderer ready");
|
|
113
|
-
groupEnd();
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
async function loadAPIData(): Promise<void> {
|
|
57
|
+
async function loadAPIData(engine: HugoRenderer): Promise<void> {
|
|
117
58
|
const files: Record<string, string> = {};
|
|
118
59
|
try {
|
|
119
60
|
currentFilePath = CloudCannon.currentFile().path;
|
|
@@ -131,7 +72,7 @@ async function loadAPIData(): Promise<void> {
|
|
|
131
72
|
return;
|
|
132
73
|
}
|
|
133
74
|
|
|
134
|
-
|
|
75
|
+
engine.writeFiles(
|
|
135
76
|
JSON.stringify({
|
|
136
77
|
[path]: `---\n${JSON.stringify(frontMatter)}\n---\n`,
|
|
137
78
|
}),
|
|
@@ -139,7 +80,7 @@ async function loadAPIData(): Promise<void> {
|
|
|
139
80
|
});
|
|
140
81
|
collection.addEventListener("delete", (event) => {
|
|
141
82
|
if (currentPath !== event.detail.sourcePath) {
|
|
142
|
-
|
|
83
|
+
engine.removeFiles(JSON.stringify([event.detail.sourcePath]));
|
|
143
84
|
}
|
|
144
85
|
});
|
|
145
86
|
|
|
@@ -156,14 +97,16 @@ async function loadAPIData(): Promise<void> {
|
|
|
156
97
|
dataset.addEventListener("change", async (event) => {
|
|
157
98
|
const data = await CloudCannon.file(event.detail.sourcePath).data.get();
|
|
158
99
|
if (data === undefined || data === null) return;
|
|
159
|
-
|
|
100
|
+
engine.writeFiles(
|
|
160
101
|
JSON.stringify({
|
|
161
102
|
[datasetPath(event.detail.sourcePath)]: `${JSON.stringify(data)}\n`,
|
|
162
103
|
}),
|
|
163
104
|
);
|
|
164
105
|
});
|
|
165
106
|
dataset.addEventListener("delete", (event) => {
|
|
166
|
-
|
|
107
|
+
engine.removeFiles(
|
|
108
|
+
JSON.stringify([datasetPath(event.detail.sourcePath)]),
|
|
109
|
+
);
|
|
167
110
|
});
|
|
168
111
|
|
|
169
112
|
const result = await dataset.items();
|
|
@@ -178,7 +121,7 @@ async function loadAPIData(): Promise<void> {
|
|
|
178
121
|
log(
|
|
179
122
|
`Loading editor content: ${Object.keys(files).length} files (editing ${currentPath})`,
|
|
180
123
|
);
|
|
181
|
-
|
|
124
|
+
await engine.writeFiles(JSON.stringify(files));
|
|
182
125
|
}
|
|
183
126
|
}
|
|
184
127
|
|
|
@@ -192,137 +135,14 @@ function datasetPath(apiPath: string): string {
|
|
|
192
135
|
return `${apiPath.replace(/\.[^./]*$/, "")}.json`;
|
|
193
136
|
}
|
|
194
137
|
|
|
195
|
-
/**
|
|
196
|
-
* Queues a partial render, resolving with the rendered element once the batch
|
|
197
|
-
* window closes; all callers get batching transparently.
|
|
198
|
-
*/
|
|
199
|
-
interface QueuedRender {
|
|
200
|
-
id: string;
|
|
201
|
-
partial: string;
|
|
202
|
-
props: Record<string, any>;
|
|
203
|
-
resolve: (el: HTMLElement) => void;
|
|
204
|
-
reject: (err: unknown) => void;
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
const BATCH_WINDOW_MS = 10;
|
|
208
|
-
|
|
209
|
-
let batch: QueuedRender[] = [];
|
|
210
|
-
let batchTimer: ReturnType<typeof setTimeout> | null = null;
|
|
211
|
-
let nextRenderId = 0;
|
|
212
|
-
|
|
213
|
-
/**
|
|
214
|
-
* Queues a partial render, resolving with the rendered element once the batch
|
|
215
|
-
* window closes; all callers get batching transparently.
|
|
216
|
-
*/
|
|
217
|
-
export async function renderHugoPartial(
|
|
218
|
-
partial: string,
|
|
219
|
-
props: Record<string, any> = {},
|
|
220
|
-
): Promise<HTMLElement> {
|
|
221
|
-
await ensureEngine();
|
|
222
|
-
|
|
223
|
-
return new Promise<HTMLElement>((resolve, reject) => {
|
|
224
|
-
const id = `cc-render-${nextRenderId++}`;
|
|
225
|
-
log("Queueing Hugo component:", partial, "Props:", props);
|
|
226
|
-
batch.push({ id, partial, props, resolve, reject });
|
|
227
|
-
if (batchTimer === null) {
|
|
228
|
-
batchTimer = setTimeout(flushBatch, BATCH_WINDOW_MS);
|
|
229
|
-
}
|
|
230
|
-
});
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
/** Flushes the batch: one renderer call, then demux the keyed output. */
|
|
234
|
-
function flushBatch(): void {
|
|
235
|
-
batchTimer = null;
|
|
236
|
-
const queued = batch;
|
|
237
|
-
batch = [];
|
|
238
|
-
if (queued.length === 0) return;
|
|
239
|
-
|
|
240
|
-
group(`Rendering ${queued.length} Hugo component(s)`);
|
|
241
|
-
try {
|
|
242
|
-
const result = renderHugoPartials(
|
|
243
|
-
JSON.stringify({
|
|
244
|
-
target: currentFilePath,
|
|
245
|
-
requests: queued.map(({ id, partial, props }) => ({
|
|
246
|
-
id,
|
|
247
|
-
partial,
|
|
248
|
-
props,
|
|
249
|
-
})),
|
|
250
|
-
}),
|
|
251
|
-
);
|
|
252
|
-
demuxBatch(queued, result);
|
|
253
|
-
} catch (err) {
|
|
254
|
-
for (const { partial, reject } of queued) {
|
|
255
|
-
reject(enhanceHugoError(String(err), partial));
|
|
256
|
-
}
|
|
257
|
-
}
|
|
258
|
-
groupEnd();
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
/**
|
|
262
|
-
* Resolves each request's keyed element from the combined output; errors and
|
|
263
|
-
* missing partials fail only the calls they apply to, the rest still resolves.
|
|
264
|
-
*/
|
|
265
|
-
function demuxBatch(
|
|
266
|
-
queued: QueuedRender[],
|
|
267
|
-
result: { html?: string; error?: string } | null,
|
|
268
|
-
): void {
|
|
269
|
-
if (result?.error || typeof result?.html !== "string") {
|
|
270
|
-
log("Render error:", result?.error);
|
|
271
|
-
for (const { partial, reject } of queued) {
|
|
272
|
-
reject(enhanceHugoError(result?.error ?? "no output", partial));
|
|
273
|
-
}
|
|
274
|
-
return;
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
const holder = document.createElement("div");
|
|
278
|
-
holder.innerHTML = result.html;
|
|
279
|
-
for (const render of queued) {
|
|
280
|
-
const keyed = holder.querySelector<HTMLElement>(
|
|
281
|
-
`[data-cc-render="${render.id}"]`,
|
|
282
|
-
);
|
|
283
|
-
if (!keyed) {
|
|
284
|
-
render.reject(
|
|
285
|
-
new Error(
|
|
286
|
-
`Hugo render produced no output for component "${render.partial}"`,
|
|
287
|
-
),
|
|
288
|
-
);
|
|
289
|
-
continue;
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
const missing = keyed.querySelector("cc-missing-partial");
|
|
293
|
-
if (missing) {
|
|
294
|
-
render.reject(
|
|
295
|
-
missingComponentError(
|
|
296
|
-
missing.getAttribute("data-name") || render.partial,
|
|
297
|
-
),
|
|
298
|
-
);
|
|
299
|
-
continue;
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
const failed = keyed.querySelector("cc-failed-partial");
|
|
303
|
-
if (failed) {
|
|
304
|
-
render.reject(
|
|
305
|
-
enhanceHugoError(
|
|
306
|
-
failed.getAttribute("data-message") || "unknown error",
|
|
307
|
-
failed.getAttribute("data-name") || render.partial,
|
|
308
|
-
),
|
|
309
|
-
);
|
|
310
|
-
continue;
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
keyed.removeAttribute("data-cc-render");
|
|
314
|
-
log("Rendered HTML preview:", keyed.innerHTML.substring(0, 200));
|
|
315
|
-
render.resolve(keyed);
|
|
316
|
-
}
|
|
317
|
-
}
|
|
318
|
-
|
|
319
138
|
/** Builds the `(props) => HTMLElement` renderer the shared core calls. */
|
|
320
139
|
function createComponentRenderer(key: string): HugoComponentRenderer {
|
|
321
140
|
return async (props: Record<string, any> = {}) => {
|
|
322
141
|
// Render only once the engine is ready; the flush then sends the
|
|
323
142
|
// boot-captured currentFilePath as the batch's shared target.
|
|
324
|
-
|
|
325
|
-
|
|
143
|
+
const engine = getRenderer();
|
|
144
|
+
await engine.ready();
|
|
145
|
+
return engine.renderPartial(key, props);
|
|
326
146
|
};
|
|
327
147
|
}
|
|
328
148
|
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export type WorkerRequest =
|
|
2
|
+
| {
|
|
3
|
+
id: string;
|
|
4
|
+
kind: "boot";
|
|
5
|
+
wasmUrl: string;
|
|
6
|
+
env: string;
|
|
7
|
+
files: Record<string, string>;
|
|
8
|
+
}
|
|
9
|
+
| { id: string; kind: "writeFiles"; json: string }
|
|
10
|
+
| { id: string; kind: "removeFiles"; json: string }
|
|
11
|
+
| { id: string; kind: "init" }
|
|
12
|
+
| { id: string; kind: "render"; payload: string };
|
|
13
|
+
|
|
14
|
+
export interface WorkerResponse {
|
|
15
|
+
id: string;
|
|
16
|
+
html?: string;
|
|
17
|
+
error?: string;
|
|
18
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { HugoWasm } from "./hugo-wasm.ts";
|
|
2
|
+
import type { WorkerRequest } from "./worker-protocol.ts";
|
|
3
|
+
|
|
4
|
+
let engine: HugoWasm | null = null;
|
|
5
|
+
|
|
6
|
+
export async function handleWorkerMessage(
|
|
7
|
+
event: MessageEvent<WorkerRequest>,
|
|
8
|
+
): Promise<void> {
|
|
9
|
+
const request = event.data;
|
|
10
|
+
try {
|
|
11
|
+
switch (request.kind) {
|
|
12
|
+
case "boot": {
|
|
13
|
+
engine = new HugoWasm();
|
|
14
|
+
await engine.boot(request);
|
|
15
|
+
break;
|
|
16
|
+
}
|
|
17
|
+
case "writeFiles": {
|
|
18
|
+
await engine?.writeFiles(request.json);
|
|
19
|
+
break;
|
|
20
|
+
}
|
|
21
|
+
case "removeFiles": {
|
|
22
|
+
await engine?.removeFiles(request.json);
|
|
23
|
+
break;
|
|
24
|
+
}
|
|
25
|
+
case "init": {
|
|
26
|
+
const error = await engine?.init();
|
|
27
|
+
if (error) {
|
|
28
|
+
postMessage({ id: request.id, error });
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
break;
|
|
32
|
+
}
|
|
33
|
+
case "render": {
|
|
34
|
+
const result = await engine?.render(request.payload);
|
|
35
|
+
postMessage({
|
|
36
|
+
id: request.id,
|
|
37
|
+
html: result?.html,
|
|
38
|
+
error: result?.error,
|
|
39
|
+
});
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
postMessage({ id: request.id });
|
|
44
|
+
} catch (err) {
|
|
45
|
+
postMessage({ id: request.id, error: String(err) });
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
self.onmessage = (event) => {
|
|
50
|
+
handleWorkerMessage(event);
|
|
51
|
+
};
|
|
@@ -71,15 +71,26 @@
|
|
|
71
71
|
{{- end -}}
|
|
72
72
|
{{- end -}}
|
|
73
73
|
|
|
74
|
+
{{- $workerSrc := resources.Get "integrations/hugo/browser/worker.ts" -}}
|
|
75
|
+
{{- $workerUrl := "" -}}
|
|
76
|
+
{{- if $workerSrc -}}
|
|
77
|
+
{{- $workerUrl = ($workerSrc | js.Build (dict
|
|
78
|
+
"minify" (not hugo.IsDevelopment)
|
|
79
|
+
"targetPath" "_cloudcannon/hugo-worker.js"
|
|
80
|
+
) | fingerprint).RelPermalink -}}
|
|
81
|
+
{{- end -}}
|
|
82
|
+
|
|
74
83
|
{{- $ctx := dict
|
|
75
84
|
"meta" (dict
|
|
76
85
|
"generator" (printf "Hugo %s" hugo.Version)
|
|
77
86
|
"wasmUrl" $wasmUrl
|
|
87
|
+
"workerUrl" $workerUrl
|
|
78
88
|
"verbose" ($cfg.verbose | default false)
|
|
79
89
|
"env" (hugo.Environment | default "production")
|
|
80
90
|
)
|
|
81
91
|
"files" (merge $templateFiles $configFiles $additionalFiles)
|
|
82
92
|
-}}
|
|
93
|
+
|
|
83
94
|
{{- $entry := resources.Get "integrations/hugo/browser/entry.js" -}}
|
|
84
95
|
{{- if not $entry -}}
|
|
85
96
|
{{- errorf "editable-regions: integrations/hugo/browser/entry.js not found in assets — is the editable-regions module imported and mounted?" -}}
|
|
@@ -4,7 +4,6 @@ go 1.26.0
|
|
|
4
4
|
|
|
5
5
|
require (
|
|
6
6
|
github.com/fsnotify/fsnotify v1.9.0
|
|
7
|
-
github.com/goccy/go-yaml v1.19.2
|
|
8
7
|
github.com/gohugoio/hugo v0.164.0
|
|
9
8
|
github.com/spf13/afero v1.15.0
|
|
10
9
|
)
|
|
@@ -40,11 +39,12 @@ require (
|
|
|
40
39
|
github.com/evanw/esbuild v0.28.1 // indirect
|
|
41
40
|
github.com/fatih/color v1.18.0 // indirect
|
|
42
41
|
github.com/frankban/quicktest v1.14.6 // indirect
|
|
43
|
-
github.com/getkin/kin-openapi v0.
|
|
42
|
+
github.com/getkin/kin-openapi v0.144.0 // indirect
|
|
44
43
|
github.com/go-openapi/jsonpointer v0.22.5 // indirect
|
|
45
44
|
github.com/go-openapi/swag/jsonname v0.25.5 // indirect
|
|
46
45
|
github.com/gobuffalo/flect v1.0.3 // indirect
|
|
47
46
|
github.com/gobwas/glob v0.2.3 // indirect
|
|
47
|
+
github.com/goccy/go-yaml v1.19.2 // indirect
|
|
48
48
|
github.com/gohugoio/gift v0.2.0 // indirect
|
|
49
49
|
github.com/gohugoio/go-i18n/v2 v2.1.3-0.20251018145728-cfcc22d823c6 // indirect
|
|
50
50
|
github.com/gohugoio/go-radix v1.2.0 // indirect
|
|
@@ -69,8 +69,8 @@ require (
|
|
|
69
69
|
github.com/mitchellh/mapstructure v1.5.1-0.20231216201459-8508981c8b6c // indirect
|
|
70
70
|
github.com/muesli/smartcrop v0.3.0 // indirect
|
|
71
71
|
github.com/niklasfasching/go-org v1.9.1 // indirect
|
|
72
|
-
github.com/oasdiff/yaml v0.1.
|
|
73
|
-
github.com/oasdiff/yaml3 v0.0.
|
|
72
|
+
github.com/oasdiff/yaml v0.1.1 // indirect
|
|
73
|
+
github.com/oasdiff/yaml3 v0.0.14 // indirect
|
|
74
74
|
github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6 // indirect
|
|
75
75
|
github.com/olekukonko/errors v1.2.0 // indirect
|
|
76
76
|
github.com/olekukonko/ll v0.1.6 // indirect
|
|
@@ -74,8 +74,8 @@ github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHk
|
|
|
74
74
|
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
|
75
75
|
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
|
|
76
76
|
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
|
|
77
|
-
github.com/getkin/kin-openapi v0.
|
|
78
|
-
github.com/getkin/kin-openapi v0.
|
|
77
|
+
github.com/getkin/kin-openapi v0.144.0 h1:hIRcTH+KjLfkLpYU6bSSfdFpi0fZi1fp+hSPi4aQu9Y=
|
|
78
|
+
github.com/getkin/kin-openapi v0.144.0/go.mod h1:3BH9M9XDe/y9M5DSvEocVYAYq1w0qrhJHjC/vZi0AaY=
|
|
79
79
|
github.com/go-openapi/jsonpointer v0.22.5 h1:8on/0Yp4uTb9f4XvTrM2+1CPrV05QPZXu+rvu2o9jcA=
|
|
80
80
|
github.com/go-openapi/jsonpointer v0.22.5/go.mod h1:gyUR3sCvGSWchA2sUBJGluYMbe1zazrYWIkWPjjMUY0=
|
|
81
81
|
github.com/go-openapi/swag/jsonname v0.25.5 h1:8p150i44rv/Drip4vWI3kGi9+4W9TdI3US3uUYSFhSo=
|
|
@@ -151,10 +151,10 @@ github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6
|
|
|
151
151
|
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8=
|
|
152
152
|
github.com/niklasfasching/go-org v1.9.1 h1:/3s4uTPOF06pImGa2Yvlp24yKXZoTYM+nsIlMzfpg/0=
|
|
153
153
|
github.com/niklasfasching/go-org v1.9.1/go.mod h1:ZAGFFkWvUQcpazmi/8nHqwvARpr1xpb+Es67oUGX/48=
|
|
154
|
-
github.com/oasdiff/yaml v0.1.
|
|
155
|
-
github.com/oasdiff/yaml v0.1.
|
|
156
|
-
github.com/oasdiff/yaml3 v0.0.
|
|
157
|
-
github.com/oasdiff/yaml3 v0.0.
|
|
154
|
+
github.com/oasdiff/yaml v0.1.1 h1:6nHx+pn9gBRM6YpBlFZFQGCCd1nuvqOBtTD3KKTgGxY=
|
|
155
|
+
github.com/oasdiff/yaml v0.1.1/go.mod h1:EYJNoyktvWMJ0Hmhx+6qTaqMOsalUaRGT8Sj1hNcegU=
|
|
156
|
+
github.com/oasdiff/yaml3 v0.0.14 h1:aLJee3hxBK2H5wdXd9iPcIXb93Nty1Ge0pT171eHtkw=
|
|
157
|
+
github.com/oasdiff/yaml3 v0.0.14/go.mod h1:csto2xfDjYccdUn/yw/bPjj/cYTdp6HtFA0J4TWG+gg=
|
|
158
158
|
github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6 h1:zrbMGy9YXpIeTnGj4EljqMiZsIcE09mmF8XsD5AYOJc=
|
|
159
159
|
github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6/go.mod h1:rEKTHC9roVVicUIfZK7DYrdIoM0EOr8mK1Hj5s3JjH0=
|
|
160
160
|
github.com/olekukonko/errors v1.2.0 h1:10Zcn4GeV59t/EGqJc8fUjtFT/FuUh5bTMzZ1XwmCRo=
|
|
@@ -3,11 +3,13 @@ package main
|
|
|
3
3
|
import (
|
|
4
4
|
"context"
|
|
5
5
|
"encoding/json"
|
|
6
|
+
"errors"
|
|
6
7
|
"fmt"
|
|
7
8
|
"io"
|
|
8
9
|
"log"
|
|
9
10
|
"path/filepath"
|
|
10
11
|
"strings"
|
|
12
|
+
"sync"
|
|
11
13
|
"syscall/js"
|
|
12
14
|
_ "time/tzdata"
|
|
13
15
|
|
|
@@ -96,6 +98,7 @@ func (builder *editorSiteBuilder) loadConfig() error {
|
|
|
96
98
|
cfg.Base.WorkingDir = ""
|
|
97
99
|
cfg.Base.Internal.Running = true
|
|
98
100
|
cfg.Base.Internal.Watch = true
|
|
101
|
+
cfg.Base.EnableGitInfo = false
|
|
99
102
|
|
|
100
103
|
for _, languageConfig := range cfg.LanguageConfigMap {
|
|
101
104
|
languageConfig.Internal.Running = true
|
|
@@ -235,6 +238,8 @@ func normalizeMemfsPath(p string) string {
|
|
|
235
238
|
|
|
236
239
|
var builder editorSiteBuilder
|
|
237
240
|
|
|
241
|
+
var renderMutex sync.Mutex
|
|
242
|
+
|
|
238
243
|
func main() {
|
|
239
244
|
builder = editorSiteBuilder{Afs: afero.NewMemMapFs()}
|
|
240
245
|
|
|
@@ -340,32 +345,54 @@ func (builder *editorSiteBuilder) targetPage(target string) page.Page {
|
|
|
340
345
|
}
|
|
341
346
|
|
|
342
347
|
func renderHugoPartials(this js.Value, args []js.Value) interface{} {
|
|
348
|
+
if len(args) != 2 || args[1].Type() != js.TypeFunction {
|
|
349
|
+
return errorValue("renderHugoPartials requires a callback function argument")
|
|
350
|
+
}
|
|
351
|
+
callback := args[1]
|
|
352
|
+
|
|
353
|
+
deliver := func(html string, err error) {
|
|
354
|
+
result := map[string]interface{}{}
|
|
355
|
+
if err != nil {
|
|
356
|
+
result["error"] = err.Error()
|
|
357
|
+
} else {
|
|
358
|
+
result["html"] = html
|
|
359
|
+
}
|
|
360
|
+
callback.Invoke(js.ValueOf(result))
|
|
361
|
+
}
|
|
362
|
+
|
|
343
363
|
var payload renderRequests
|
|
344
364
|
if err := json.Unmarshal([]byte(args[0].String()), &payload); err != nil {
|
|
345
|
-
|
|
365
|
+
deliver("", fmt.Errorf("bad renderHugoPartials payload: %s", err))
|
|
366
|
+
return nil
|
|
346
367
|
}
|
|
347
368
|
reqs := payload.Requests
|
|
348
369
|
if len(reqs) == 0 {
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
if err := builder.buildIfDirty(); err != nil {
|
|
352
|
-
return errorValue("editor site build failed: %s", err)
|
|
370
|
+
deliver("", errors.New("renderHugoPartials requires at least one request"))
|
|
371
|
+
return nil
|
|
353
372
|
}
|
|
354
|
-
|
|
355
373
|
for _, req := range reqs {
|
|
356
374
|
if req.Partial == "" {
|
|
357
|
-
|
|
375
|
+
deliver("", errors.New("renderHugoPartials requires a \"partial\" name on every request"))
|
|
376
|
+
return nil
|
|
358
377
|
}
|
|
359
378
|
}
|
|
360
379
|
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
}
|
|
380
|
+
go func() {
|
|
381
|
+
html, err := builder.buildAndRender(payload.Target, reqs)
|
|
382
|
+
deliver(html, err)
|
|
383
|
+
}()
|
|
365
384
|
|
|
366
|
-
return
|
|
367
|
-
|
|
368
|
-
|
|
385
|
+
return nil
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
func (builder *editorSiteBuilder) buildAndRender(target string, reqs []renderRequest) (string, error) {
|
|
389
|
+
renderMutex.Lock()
|
|
390
|
+
defer renderMutex.Unlock()
|
|
391
|
+
|
|
392
|
+
if err := builder.buildIfDirty(); err != nil {
|
|
393
|
+
return "", fmt.Errorf("editor site build failed: %s", err)
|
|
394
|
+
}
|
|
395
|
+
return builder.renderBatch(target, reqs)
|
|
369
396
|
}
|
|
370
397
|
|
|
371
398
|
func (builder *editorSiteBuilder) renderBatch(target string, reqs []renderRequest) (string, error) {
|
package/package.json
CHANGED
package/types/hugo.d.ts
CHANGED
|
@@ -29,6 +29,7 @@ declare global {
|
|
|
29
29
|
interface HugoRuntimeMeta {
|
|
30
30
|
generator?: string;
|
|
31
31
|
wasmUrl?: string;
|
|
32
|
+
workerUrl?: string;
|
|
32
33
|
verbose?: boolean;
|
|
33
34
|
env?: string;
|
|
34
35
|
}
|
|
@@ -53,7 +54,7 @@ declare global {
|
|
|
53
54
|
CloudCannonAPI?: CloudCannonVisualEditorAPIRouter;
|
|
54
55
|
/** The installed v0/v1 CloudCannon API for this page. */
|
|
55
56
|
CloudCannon?: CloudCannonVisualEditorAPIV0 | CloudCannonVisualEditorAPIV1;
|
|
56
|
-
/** Emitter metadata: generator, wasmUrl, verbose, env. */
|
|
57
|
+
/** Emitter metadata: generator, wasmUrl, workerUrl, verbose, env. */
|
|
57
58
|
cc_hugo?: HugoRuntimeMeta;
|
|
58
59
|
/** Template/config snapshot keyed by physical path. */
|
|
59
60
|
cc_hugo_files?: Record<string, string>;
|
|
@@ -65,5 +66,8 @@ declare global {
|
|
|
65
66
|
function removeHugoFiles(json: string): HugoEditorResult | null;
|
|
66
67
|
function readHugoFiles(json: string): Record<string, string>;
|
|
67
68
|
function initHugoEditorSite(): HugoEditorResult | null;
|
|
68
|
-
function renderHugoPartials(
|
|
69
|
+
function renderHugoPartials(
|
|
70
|
+
json: string,
|
|
71
|
+
callback: (result: HugoRenderResult) => void,
|
|
72
|
+
): void;
|
|
69
73
|
}
|