@cloudcannon/editable-regions 0.0.20-rc.2 → 0.0.20
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 +885 -0
- package/components/ui/editable-region-error-card.ts +9 -0
- package/helpers/cloudcannon.mjs +20 -9
- package/helpers/hydrate-editable-regions.ts +36 -3
- package/integrations/hugo/browser/errors.ts +81 -10
- 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/main.go +41 -14
- package/integrations/react.mjs +63 -1
- package/integrations/svelte/EditableRegions.svelte +80 -0
- package/integrations/{svelte.mjs → svelte/index.mjs} +3 -1
- package/integrations/vue.mjs +97 -1
- package/nodes/editable-array-item.ts +5 -1
- package/nodes/editable-component.ts +10 -4
- package/nodes/editable-image.ts +80 -75
- package/nodes/editable.ts +28 -5
- package/package.json +17 -16
- package/types/hugo.d.ts +6 -2
- package/types/react.d.ts +20 -6
- package/types/svelte.d.ts +10 -0
- package/types/vue.d.ts +14 -1
- package/integrations/liquid/README.md +0 -677
|
@@ -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?" -}}
|
|
@@ -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/integrations/react.mjs
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { createElement, useEffect, useRef } from "react";
|
|
2
4
|
import { flushSync } from "react-dom";
|
|
3
5
|
import { createRoot } from "react-dom/client";
|
|
4
6
|
import { addEditableComponentRenderer } from "../helpers/cloudcannon.mjs";
|
|
@@ -26,3 +28,63 @@ export const registerReactComponent = (key, component) => {
|
|
|
26
28
|
|
|
27
29
|
addEditableComponentRenderer(key, wrappedComponent);
|
|
28
30
|
};
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Root editable region component for React (e.g. Next.js App Router pages).
|
|
34
|
+
* Keeps editable regions from mutating the DOM before frameworks like Next.js finish hydrating.
|
|
35
|
+
*
|
|
36
|
+
* @param {{ tag?: string } & Record<string, any>} props
|
|
37
|
+
* All other props and attributes are forwarded to the rendered element.
|
|
38
|
+
*/
|
|
39
|
+
export const EditableRegions = ({ tag: Tag = "div", ...attrs }) => {
|
|
40
|
+
const elementRef = useRef(null);
|
|
41
|
+
/** @type {{ current: any | null }} */
|
|
42
|
+
const editableRef = useRef(null);
|
|
43
|
+
|
|
44
|
+
useEffect(() => {
|
|
45
|
+
/** @param {any} regions */
|
|
46
|
+
const connectEditable = (regions) => {
|
|
47
|
+
if (!regions || editableRef.current) {
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
editableRef.current = new regions.Editable(elementRef.current);
|
|
52
|
+
editableRef.current.connect();
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const onLoad = () => {
|
|
56
|
+
connectEditable(/** @type {any} */ (window).editableRegions);
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
if (/** @type {any} */ (window).editableRegions) {
|
|
60
|
+
connectEditable(/** @type {any} */ (window).editableRegions);
|
|
61
|
+
} else {
|
|
62
|
+
document.addEventListener("editable-regions:load", onLoad, {
|
|
63
|
+
once: true,
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return () => {
|
|
68
|
+
document.removeEventListener("editable-regions:load", onLoad);
|
|
69
|
+
editableRef.current?.disconnect();
|
|
70
|
+
editableRef.current = null;
|
|
71
|
+
};
|
|
72
|
+
}, []);
|
|
73
|
+
|
|
74
|
+
/** @type {Record<string, any>} */
|
|
75
|
+
const forwarded = {};
|
|
76
|
+
for (const [name, value] of Object.entries(attrs)) {
|
|
77
|
+
if (/^data-(prop|literal)/i.test(name)) {
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
forwarded[name] = value;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return createElement(Tag, {
|
|
85
|
+
...forwarded,
|
|
86
|
+
ref: elementRef,
|
|
87
|
+
"data-editable": "_dynamic",
|
|
88
|
+
"data-prop": "",
|
|
89
|
+
});
|
|
90
|
+
};
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
<script>
|
|
2
|
+
import { onDestroy, onMount } from "svelte";
|
|
3
|
+
|
|
4
|
+
// Element type to render. Defaults to "div".
|
|
5
|
+
/** @type {string | undefined} */
|
|
6
|
+
export let tag = "div";
|
|
7
|
+
|
|
8
|
+
/** @type {any} */
|
|
9
|
+
let editable = null;
|
|
10
|
+
/** @type {HTMLElement | undefined} */
|
|
11
|
+
let element;
|
|
12
|
+
/** @type {(() => void) | null} */
|
|
13
|
+
let pendingLoadHandler = null;
|
|
14
|
+
|
|
15
|
+
// data-prop and data-literal attributes are controlled by this component
|
|
16
|
+
// itself; user-supplied ones are ignored.
|
|
17
|
+
$: restProps = filterProps($$restProps);
|
|
18
|
+
|
|
19
|
+
function filterProps(props) {
|
|
20
|
+
const filtered = {};
|
|
21
|
+
for (const [name, value] of Object.entries(props)) {
|
|
22
|
+
if (/^data-(prop|literal)/i.test(name)) {
|
|
23
|
+
continue;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
filtered[name] = value;
|
|
27
|
+
}
|
|
28
|
+
return filtered;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
onMount(() => {
|
|
32
|
+
/**
|
|
33
|
+
* @param {any} regions
|
|
34
|
+
*/
|
|
35
|
+
const connectEditable = (regions) => {
|
|
36
|
+
if (!regions || editable) {
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
editable = new regions.Editable(element);
|
|
41
|
+
editable.connect();
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
if (window.editableRegions) {
|
|
45
|
+
connectEditable(window.editableRegions);
|
|
46
|
+
} else {
|
|
47
|
+
pendingLoadHandler = () => {
|
|
48
|
+
connectEditable(window.editableRegions);
|
|
49
|
+
};
|
|
50
|
+
document.addEventListener(
|
|
51
|
+
"editable-regions:load",
|
|
52
|
+
pendingLoadHandler,
|
|
53
|
+
{ once: true },
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
onDestroy(() => {
|
|
59
|
+
if (pendingLoadHandler) {
|
|
60
|
+
document.removeEventListener(
|
|
61
|
+
"editable-regions:load",
|
|
62
|
+
pendingLoadHandler,
|
|
63
|
+
);
|
|
64
|
+
pendingLoadHandler = null;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
editable?.disconnect();
|
|
68
|
+
editable = null;
|
|
69
|
+
});
|
|
70
|
+
</script>
|
|
71
|
+
|
|
72
|
+
<svelte:element
|
|
73
|
+
this={tag}
|
|
74
|
+
{...restProps}
|
|
75
|
+
bind:this={element}
|
|
76
|
+
data-editable="_dynamic"
|
|
77
|
+
data-prop=""
|
|
78
|
+
>
|
|
79
|
+
<slot />
|
|
80
|
+
</svelte:element>
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
|
|
1
|
+
export { default as EditableRegions } from "./EditableRegions.svelte";
|
|
2
|
+
|
|
3
|
+
import { addEditableComponentRenderer } from "../../helpers/cloudcannon.mjs";
|
|
2
4
|
|
|
3
5
|
/** @type{((component: any, args: { target: HTMLElement, props: unknown }) => void) | undefined} */
|
|
4
6
|
let mount;
|