@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.
@@ -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")) {
@@ -14,24 +14,26 @@
14
14
  * }} ExtendedWindow
15
15
  */
16
16
 
17
- /** @type {ExtendedWindow} */
18
- const extendedWindow = /** @type {any} */ (window);
17
+ /** @type {ExtendedWindow | undefined} */
18
+ const extendedWindow = /** @type {any} */ (
19
+ typeof window === "undefined" ? undefined : window
20
+ );
19
21
 
20
- /** @type {CloudCannonVisualEditorAPIV1} */
22
+ /** @type {CloudCannonVisualEditorAPIV1 | undefined} */
21
23
  let _cloudcannon;
22
24
 
23
25
  /** @type {Promise<void>} */
24
26
  export const apiLoadedPromise = new Promise((resolve) => {
25
- if (extendedWindow.CloudCannonAPI) {
27
+ if (extendedWindow?.CloudCannonAPI) {
26
28
  _cloudcannon = /** @type {any} */ (
27
29
  extendedWindow.CloudCannonAPI.useVersion("v1", true)
28
30
  );
29
31
  resolve();
30
- } else {
32
+ } else if (typeof document !== "undefined") {
31
33
  document.addEventListener(
32
34
  "cloudcannon:load",
33
35
  () => {
34
- if (extendedWindow.CloudCannonAPI) {
36
+ if (extendedWindow?.CloudCannonAPI) {
35
37
  _cloudcannon = /** @type {any} */ (
36
38
  extendedWindow.CloudCannonAPI.useVersion("v1", true)
37
39
  );
@@ -48,6 +50,9 @@ export const apiLoadedPromise = new Promise((resolve) => {
48
50
  * @param {ComponentRenderer} renderer
49
51
  */
50
52
  export const addEditableComponentRenderer = (key, renderer) => {
53
+ if (!extendedWindow) {
54
+ return;
55
+ }
51
56
  extendedWindow.cc_components = extendedWindow.cc_components || {};
52
57
  extendedWindow.cc_components[key] = renderer;
53
58
  };
@@ -57,15 +62,18 @@ export const addEditableComponentRenderer = (key, renderer) => {
57
62
  * @param {ComponentRenderer} renderer
58
63
  */
59
64
  export const addEditableSnippetRenderer = (key, renderer) => {
65
+ if (!extendedWindow) {
66
+ return;
67
+ }
60
68
  extendedWindow.cc_snippets = extendedWindow.cc_snippets || {};
61
69
  extendedWindow.cc_snippets[key] = renderer;
62
70
  };
63
71
 
64
72
  export const getEditableComponentRenderers = () =>
65
- extendedWindow.cc_components ?? {};
73
+ extendedWindow?.cc_components ?? {};
66
74
 
67
75
  export const getEditableSnippetRenderers = () =>
68
- extendedWindow.cc_snippets ?? {};
76
+ extendedWindow?.cc_snippets ?? {};
69
77
 
70
78
  /**
71
79
  * Resolves CloudCannon API objects (collections, files, datasets) to plain data.
@@ -91,13 +99,16 @@ export const realizeAPIValue = async (value) => {
91
99
  };
92
100
 
93
101
  export const addCustomEditableRegion = (key, region) => {
102
+ if (!extendedWindow || typeof document === "undefined") {
103
+ return;
104
+ }
94
105
  extendedWindow.editableRegionMap ??= {};
95
106
  extendedWindow.editableRegionMap[key] = region;
96
107
  extendedWindow.hydrateDataEditableRegions(document.body);
97
108
  };
98
109
 
99
110
  export const getCustomEditableRegions = () => {
100
- return extendedWindow.editableRegionMap ?? {};
111
+ return extendedWindow?.editableRegionMap ?? {};
101
112
  };
102
113
 
103
114
  export { _cloudcannon as CloudCannon };
@@ -10,6 +10,8 @@ import {
10
10
  import { hasEditable } from "../nodes/editable.js";
11
11
  import { isEditableWebcomponent } from "./checks";
12
12
 
13
+ const DYNAMIC_EDITABLE_TYPE = "_dynamic";
14
+
13
15
  const baseEditableMap: Record<string, typeof Editable | undefined> = {
14
16
  array: EditableArray,
15
17
  "array-item": EditableArrayItem,
@@ -20,12 +22,23 @@ const baseEditableMap: Record<string, typeof Editable | undefined> = {
20
22
  };
21
23
 
22
24
  export const dehydrateDataEditableRegions = (root: Element) => {
23
- if (root instanceof HTMLElement && hasEditable(root)) {
25
+ if (
26
+ root instanceof HTMLElement &&
27
+ hasEditable(root) &&
28
+ root.dataset.editable !== DYNAMIC_EDITABLE_TYPE
29
+ ) {
24
30
  root.editable.disconnect();
25
31
  }
26
32
 
27
33
  root.querySelectorAll("[data-editable]").forEach((element) => {
28
- if (element instanceof HTMLElement && hasEditable(element)) {
34
+ if (
35
+ !(element instanceof HTMLElement) ||
36
+ element.dataset.editable === DYNAMIC_EDITABLE_TYPE
37
+ ) {
38
+ return;
39
+ }
40
+
41
+ if (hasEditable(element)) {
29
42
  element.editable.disconnect();
30
43
  }
31
44
  });
@@ -39,6 +52,7 @@ export const hydrateDataEditableRegions = (root: Element) => {
39
52
  if (
40
53
  root instanceof HTMLElement &&
41
54
  root.dataset.editable &&
55
+ root.dataset.editable !== DYNAMIC_EDITABLE_TYPE &&
42
56
  !isEditableWebcomponent(root)
43
57
  ) {
44
58
  if ("editable" in root && root.editable instanceof Editable) {
@@ -64,6 +78,10 @@ export const hydrateDataEditableRegions = (root: Element) => {
64
78
  return;
65
79
  }
66
80
 
81
+ if (element.dataset.editable === DYNAMIC_EDITABLE_TYPE) {
82
+ return;
83
+ }
84
+
67
85
  const Editable = editableMap[element.dataset.editable];
68
86
  if (!Editable) {
69
87
  const error = document.createElement("editable-region-error-card");
@@ -95,4 +113,19 @@ export const hydrateDataEditableRegions = (root: Element) => {
95
113
  });
96
114
  };
97
115
 
98
- (window as any).hydrateDataEditableRegions = hydrateDataEditableRegions;
116
+ const extendedWindow = /** @type {any} */ (window);
117
+
118
+ Object.assign(extendedWindow, {
119
+ editableRegions: {
120
+ Editable,
121
+ EditableArray,
122
+ EditableArrayItem,
123
+ EditableComponent,
124
+ EditableImage,
125
+ EditableSource,
126
+ EditableText,
127
+ },
128
+ hydrateDataEditableRegions,
129
+ });
130
+
131
+ document.dispatchEvent(new CustomEvent("editable-regions:load"));
@@ -3,30 +3,101 @@
3
3
  * on the core's component error card, so it should tell the user what to fix.
4
4
  */
5
5
 
6
- export function enhanceHugoError(message: string, componentKey: string): Error {
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
+
46
+ /** An error enhanced with an optional editor-context hint. */
47
+ export type HugoError = Error & { hint?: string };
48
+
49
+ export function enhanceHugoError(
50
+ message: string,
51
+ componentKey: string,
52
+ ): HugoError {
53
+ const parsed = parseHugoTemplateError(message);
54
+
7
55
  let hint = "";
8
56
 
9
57
  if (/partial .* not found/i.test(message)) {
10
58
  hint =
11
- " This partial isn't in the bundled template snapshot. Check that it " +
59
+ "This partial isn't in the bundled template snapshot. Check that it " +
12
60
  "lives under one of the directories in " +
13
61
  "`params.editable_regions.template_dirs` (by default the partials, " +
14
62
  "render hooks, and shortcodes of your configured layout dir) " +
15
63
  "and rebuild the site.";
16
- } else if (/execute of template failed/i.test(message)) {
64
+ } else if (parsed.frames.length > 0) {
17
65
  hint =
18
- " The partial errored while rendering in the editor. If it depends on " +
19
- "build-only state (page context, resources, .Site.Pages), guard that " +
20
- "code with `if hugo.IsServer` or move it out of the component.";
66
+ "The partial errored while rendering in the editor. If the code " +
67
+ "should only run in the site build, guard it with " +
68
+ "`{{ if not site.Params.env_client }}`, " +
69
+ "or supply an editor-safe version via " +
70
+ "`params.editable_regions.templates_overrides`.";
21
71
  } else if (/logged \d+ errors/i.test(message)) {
22
72
  hint =
23
- " Hugo logged errors during the render — open the browser console " +
73
+ "Hugo logged errors during the render — open the browser console " +
24
74
  "for the underlying messages.";
25
75
  }
26
76
 
27
- return new Error(
28
- `Failed to render Hugo component "${componentKey}": ${message}.${hint}`,
29
- );
77
+ const primary =
78
+ parsed.frames.length > 0 && parsed.call
79
+ ? `${parsed.call}: ${parsed.message}`
80
+ : parsed.message;
81
+ const sentence = /[.!?]$/.test(primary) ? primary : `${primary}.`;
82
+
83
+ const error = new Error(
84
+ `Failed to render Hugo component "${componentKey}": ${sentence}`,
85
+ ) as HugoError;
86
+
87
+ if (hint) {
88
+ error.hint = hint;
89
+ }
90
+
91
+ if (parsed.frames.length > 0) {
92
+ error.stack = parsed.frames
93
+ .slice()
94
+ .reverse()
95
+ .slice(0, 10)
96
+ .map((ref) => ` at ${ref}`)
97
+ .join("\n");
98
+ }
99
+
100
+ return error;
30
101
  }
31
102
 
32
103
  /** 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
+ }