@cloudcannon/editable-regions 0.0.19 → 0.0.20-rc.2

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.
@@ -0,0 +1,412 @@
1
+ package main
2
+
3
+ import (
4
+ "context"
5
+ "encoding/json"
6
+ "fmt"
7
+ "io"
8
+ "log"
9
+ "path/filepath"
10
+ "strings"
11
+ "syscall/js"
12
+ _ "time/tzdata"
13
+
14
+ "github.com/fsnotify/fsnotify"
15
+ "github.com/gohugoio/hugo/common/hmaps"
16
+ "github.com/gohugoio/hugo/config"
17
+ "github.com/gohugoio/hugo/config/allconfig"
18
+ "github.com/gohugoio/hugo/deps"
19
+ "github.com/gohugoio/hugo/hugofs"
20
+ "github.com/gohugoio/hugo/hugolib"
21
+ "github.com/gohugoio/hugo/parser/metadecoders"
22
+ "github.com/gohugoio/hugo/resources/page"
23
+ "github.com/spf13/afero"
24
+ )
25
+
26
+ // dispatchView is the view renderBatch executes for each batch: the batch's
27
+ // requests arrive as template data (keyed by render id), and each request's
28
+ // partial renders with `page` bound through the execution context, keying
29
+ // every result with its request id for the browser to demultiplex.
30
+ const dispatchView = `{{- with .cc_requests -}}
31
+ {{- /* cc_requests is a MAP keyed by render id, prepared on the Go side with
32
+ Hugo's params preparation: it recurses into nested maps (not arrays),
33
+ keeping case-insensitive Params semantics for props. Range order is
34
+ key-sorted; the browser demuxes by id, not position. */ -}}
35
+ {{- range $id, $req := . -}}
36
+ {{- $partial := $req.partial -}}
37
+ {{- $found := templates.Exists (printf "partials/%s" $partial) -}}
38
+ {{- $found = or $found (templates.Exists (printf "partials/%s.html" $partial)) -}}
39
+ {{- $found = or $found (templates.Exists (printf "partials/%s.htm" $partial)) -}}
40
+ {{- if not $found -}}
41
+ <div data-cc-render="{{ $id }}"><cc-missing-partial data-name="{{ $partial }}"></cc-missing-partial></div>
42
+ {{- else -}}
43
+ <div data-cc-render="{{ $id }}">{{- $result := try (partial $partial $req.props) -}}
44
+ {{- if $result.Err -}}
45
+ <cc-failed-partial data-name="{{ $partial }}" data-message="{{ $result.Err }}"></cc-failed-partial>
46
+ {{- else -}}
47
+ {{- $result.Value -}}
48
+ {{- end -}}</div>
49
+ {{- end -}}
50
+ {{- end -}}
51
+ {{- end -}}`
52
+
53
+ const dispatchViewPath = "/_default/__cc-dispatch.html"
54
+
55
+ type editorSiteBuilder struct {
56
+ Cfg *allconfig.Configs
57
+ Afs afero.Fs
58
+ Fs *hugofs.Fs
59
+ Sites *hugolib.HugoSites
60
+ changedFiles []string
61
+ removedFiles []string
62
+ }
63
+
64
+ func editorFlags() config.Provider {
65
+ flags := config.New()
66
+ flags.Set("disableKinds", []string{"taxonomy", "term", "RSS", "sitemap", "robotsTXT", "404"})
67
+ flags.Set("cascade", []interface{}{
68
+ map[string]interface{}{
69
+ "build": map[string]interface{}{"render": "link"},
70
+ },
71
+ })
72
+ return flags
73
+ }
74
+
75
+ func (builder *editorSiteBuilder) loadConfig() error {
76
+ env := "production"
77
+ if contents, err := builder.readFile("cc-env"); err == nil {
78
+ env = strings.TrimSpace(contents)
79
+ }
80
+ cfg, err := allconfig.LoadConfig(allconfig.ConfigSourceDescriptor{
81
+ Fs: builder.Afs,
82
+ Flags: editorFlags(),
83
+ ConfigDir: "config",
84
+ Environment: env,
85
+ // Only mirrored modules (theme, vendored) can resolve in the in-memory fs;
86
+ // skip imports whose replacement points at the repo on disk.
87
+ IgnoreModuleDoesNotExist: true,
88
+ })
89
+ if err != nil {
90
+ return err
91
+ }
92
+
93
+ // Rebuilds run through Hugo's incremental change-event pipeline, so enable
94
+ // Running/Watch and propagate to per-language configs (read by the `hugo.*`
95
+ // template namespace).
96
+ cfg.Base.WorkingDir = ""
97
+ cfg.Base.Internal.Running = true
98
+ cfg.Base.Internal.Watch = true
99
+
100
+ for _, languageConfig := range cfg.LanguageConfigMap {
101
+ languageConfig.Internal.Running = true
102
+ languageConfig.Internal.Watch = true
103
+ languageConfig.Build.BuildStats.Enable = false
104
+ if languageConfig.Params == nil {
105
+ languageConfig.Params = hmaps.Params{}
106
+ }
107
+ languageConfig.Params["env_client"] = true
108
+ }
109
+ builder.Cfg = cfg
110
+ builder.Fs = hugofs.NewFrom(builder.Afs, cfg.GetFirstLanguageConfig().BaseConfig())
111
+
112
+ return nil
113
+ }
114
+
115
+ func (builder *editorSiteBuilder) createSites() error {
116
+ if err := builder.loadConfig(); err != nil {
117
+ return fmt.Errorf("failed to load config: %w", err)
118
+ }
119
+
120
+ builder.Fs.PublishDir = hugofs.NewCreateCountingFs(builder.Fs.PublishDir)
121
+
122
+ sites, err := hugolib.NewHugoSites(deps.DepsCfg{
123
+ Fs: builder.Fs,
124
+ Configs: builder.Cfg,
125
+ })
126
+ if err != nil {
127
+ return fmt.Errorf("failed to create sites: %w", err)
128
+ }
129
+ builder.Sites = sites
130
+
131
+ return nil
132
+ }
133
+
134
+ func (builder *editorSiteBuilder) build() error {
135
+ var events []fsnotify.Event
136
+ if builder.Sites == nil {
137
+ if err := builder.createSites(); err != nil {
138
+ return err
139
+ }
140
+ } else {
141
+ events = builder.changeEvents()
142
+ }
143
+
144
+ err := builder.Sites.Build(hugolib.BuildCfg{NoBuildLock: true}, events...)
145
+ if err != nil {
146
+ return err
147
+ }
148
+
149
+ builder.changedFiles = nil
150
+ builder.removedFiles = nil
151
+
152
+ if n := builder.Sites.NumLogErrors(); n > 0 {
153
+ err = fmt.Errorf("logged %d errors", n)
154
+ }
155
+ return err
156
+ }
157
+
158
+ // buildIfDirty runs the initial build when the site doesn't exist yet, then
159
+ // an incremental build only when files changed since the last build, so
160
+ // renders with nothing pending skip the build entirely.
161
+ func (builder *editorSiteBuilder) buildIfDirty() error {
162
+ if builder.Sites == nil {
163
+ return builder.build()
164
+ }
165
+
166
+ if len(builder.changedFiles) == 0 && len(builder.removedFiles) == 0 {
167
+ return nil
168
+ }
169
+
170
+ if err := builder.build(); err != nil {
171
+ return fmt.Errorf("build after pending content changes: %w", err)
172
+ }
173
+ return nil
174
+ }
175
+
176
+ func (builder *editorSiteBuilder) writeFile(filename, content string) {
177
+ filename = normalizeMemfsPath(filename)
178
+ if err := afero.WriteFile(builder.Afs, filepath.FromSlash(filename), []byte(content), 0755); err != nil {
179
+ fmt.Println(fmt.Sprintf("Failed to write file: %s", err))
180
+ return
181
+ }
182
+
183
+ builder.changedFiles = append(builder.changedFiles, filename)
184
+ }
185
+
186
+ func (builder *editorSiteBuilder) removeFile(filename string) {
187
+ filename = normalizeMemfsPath(filename)
188
+ if err := builder.Afs.Remove(filename); err != nil {
189
+ fmt.Println(fmt.Sprintf("Failed to remove file: %s", err))
190
+ return
191
+ }
192
+
193
+ // Publish-dir files aren't Hugo source files, so their removal shouldn't
194
+ // be fed back in as a change event.
195
+ if !strings.HasPrefix(filepath.ToSlash(filename), "public/") {
196
+ builder.removedFiles = append(builder.removedFiles, filename)
197
+ }
198
+ }
199
+
200
+ func (builder *editorSiteBuilder) readFile(filename string) (string, error) {
201
+ filename = normalizeMemfsPath(filename)
202
+ b, err := afero.ReadFile(builder.Afs, filepath.Clean(filename))
203
+ if err != nil {
204
+ return "", err
205
+ }
206
+ return string(b), nil
207
+ }
208
+
209
+ func (builder *editorSiteBuilder) changeEvents() []fsnotify.Event {
210
+ var events []fsnotify.Event
211
+
212
+ for _, v := range builder.changedFiles {
213
+ events = append(events, fsnotify.Event{
214
+ Name: v,
215
+ Op: fsnotify.Write,
216
+ })
217
+ }
218
+
219
+ for _, v := range builder.removedFiles {
220
+ events = append(events, fsnotify.Event{
221
+ Name: v,
222
+ Op: fsnotify.Remove,
223
+ })
224
+ }
225
+
226
+ return events
227
+ }
228
+
229
+ // normalizeMemfsPath maps the browser's site-root-relative source paths
230
+ // ("/content/blog/one.md") to the in-memory fs's slash-less relative form,
231
+ // which Hugo's contentDir/dataDir/layoutDir also use.
232
+ func normalizeMemfsPath(p string) string {
233
+ return strings.TrimPrefix(filepath.ToSlash(p), "/")
234
+ }
235
+
236
+ var builder editorSiteBuilder
237
+
238
+ func main() {
239
+ builder = editorSiteBuilder{Afs: afero.NewMemMapFs()}
240
+
241
+ log.SetOutput(io.Discard)
242
+
243
+ c := make(chan struct{}, 0)
244
+ js.Global().Set("writeHugoFiles", js.FuncOf(writeHugoFiles))
245
+ js.Global().Set("removeHugoFiles", js.FuncOf(removeHugoFiles))
246
+ js.Global().Set("readHugoFiles", js.FuncOf(readHugoFiles))
247
+ js.Global().Set("initHugoEditorSite", js.FuncOf(initHugoEditorSite))
248
+ js.Global().Set("renderHugoPartials", js.FuncOf(renderHugoPartials))
249
+ <-c
250
+ }
251
+
252
+ func errorValue(format string, args ...interface{}) js.Value {
253
+ return js.ValueOf(map[string]interface{}{
254
+ "error": fmt.Sprintf(format, args...),
255
+ })
256
+ }
257
+
258
+ func writeHugoFiles(this js.Value, args []js.Value) interface{} {
259
+ var writeFiles map[string]string
260
+ if err := json.Unmarshal([]byte(args[0].String()), &writeFiles); err != nil {
261
+ return errorValue("bad writeHugoFiles payload: %s", err)
262
+ }
263
+
264
+ for fileName, fileContents := range writeFiles {
265
+ builder.writeFile(fileName, fileContents)
266
+ }
267
+ return nil
268
+ }
269
+
270
+ func removeHugoFiles(this js.Value, args []js.Value) interface{} {
271
+ var removeFiles []string
272
+ if err := json.Unmarshal([]byte(args[0].String()), &removeFiles); err != nil {
273
+ return errorValue("bad removeHugoFiles payload: %s", err)
274
+ }
275
+
276
+ for _, fileName := range removeFiles {
277
+ builder.removeFile(fileName)
278
+ }
279
+ return nil
280
+ }
281
+
282
+ func readHugoFiles(this js.Value, args []js.Value) interface{} {
283
+ var readFiles []string
284
+ if err := json.Unmarshal([]byte(args[0].String()), &readFiles); err != nil {
285
+ return errorValue("bad readHugoFiles payload: %s", err)
286
+ }
287
+
288
+ fileContents := make(map[string]interface{})
289
+ for _, fileName := range readFiles {
290
+ contents, err := builder.readFile(fileName)
291
+ if err != nil {
292
+ continue
293
+ }
294
+ fileContents[fileName] = contents
295
+ }
296
+
297
+ return js.ValueOf(fileContents)
298
+ }
299
+
300
+ func initHugoEditorSite(this js.Value, args []js.Value) interface{} {
301
+ if err := builder.loadConfig(); err != nil {
302
+ return errorValue("failed to load config: %s", err)
303
+ }
304
+
305
+ builder.writeFile(filepath.Join(
306
+ builder.Cfg.Base.LayoutDir, "_default", "__cc-dispatch.html"), dispatchView)
307
+ return nil
308
+ }
309
+
310
+ // renderRequests is the browser's queued batch: the one render target every
311
+ // request shares, plus the requests to render against it.
312
+ type renderRequests struct {
313
+ Target string `json:"target"`
314
+ Requests []renderRequest `json:"requests"`
315
+ }
316
+
317
+ type renderRequest struct {
318
+ ID string `json:"id"`
319
+ Partial string `json:"partial"`
320
+ Props json.RawMessage `json:"props"`
321
+ }
322
+
323
+ func (builder *editorSiteBuilder) targetPage(target string) page.Page {
324
+ target = normalizeMemfsPath(target)
325
+ if target != "" && builder.Sites != nil {
326
+ for _, s := range builder.Sites.Sites {
327
+ for _, p := range s.Pages() {
328
+ f := p.File()
329
+ if f == nil {
330
+ continue
331
+ }
332
+ if filepath.Join(builder.Cfg.Base.ContentDir, f.Path()) != target {
333
+ continue
334
+ }
335
+ return p
336
+ }
337
+ }
338
+ }
339
+ return nil
340
+ }
341
+
342
+ func renderHugoPartials(this js.Value, args []js.Value) interface{} {
343
+ var payload renderRequests
344
+ if err := json.Unmarshal([]byte(args[0].String()), &payload); err != nil {
345
+ return errorValue("bad renderHugoPartials payload: %s", err)
346
+ }
347
+ reqs := payload.Requests
348
+ if len(reqs) == 0 {
349
+ return errorValue("renderHugoPartials requires at least one request")
350
+ }
351
+ if err := builder.buildIfDirty(); err != nil {
352
+ return errorValue("editor site build failed: %s", err)
353
+ }
354
+
355
+ for _, req := range reqs {
356
+ if req.Partial == "" {
357
+ return errorValue("renderHugoPartials requires a \"partial\" name on every request")
358
+ }
359
+ }
360
+
361
+ html, err := builder.renderBatch(payload.Target, reqs)
362
+ if err != nil {
363
+ return errorValue("%s", err)
364
+ }
365
+
366
+ return js.ValueOf(map[string]interface{}{
367
+ "html": html,
368
+ })
369
+ }
370
+
371
+ func (builder *editorSiteBuilder) renderBatch(target string, reqs []renderRequest) (string, error) {
372
+ requests := make(map[string]interface{}, len(reqs))
373
+ for _, req := range reqs {
374
+ var props interface{}
375
+ if len(req.Props) > 0 {
376
+ if err := json.Unmarshal(req.Props, &props); err != nil {
377
+ return "", fmt.Errorf("bad props for %s: %s", req.Partial, err)
378
+ }
379
+ }
380
+ requests[req.ID] = map[string]interface{}{
381
+ "partial": req.Partial,
382
+ "props": props,
383
+ }
384
+ }
385
+ // Decode the batch through Hugo's YAML decoder so props decode with identical types
386
+ encoded, err := json.Marshal(requests)
387
+ if err != nil {
388
+ return "", fmt.Errorf("failed to encode render batch: %s", err)
389
+ }
390
+ decoded, err := metadecoders.Decoder{}.UnmarshalToMap(encoded, metadecoders.YAML)
391
+ if err != nil {
392
+ return "", fmt.Errorf("failed to decode render batch: %s", err)
393
+ }
394
+ hmaps.PrepareParams(decoded)
395
+
396
+ renderTarget := builder.targetPage(target)
397
+ if renderTarget == nil {
398
+ renderTarget = page.NopPage
399
+ }
400
+ store := builder.Sites.GetTemplateStore()
401
+ ctx := store.PrepareTopLevelRenderCtx(context.Background(), renderTarget)
402
+ tmpl := store.LookupByPath(dispatchViewPath)
403
+ if tmpl == nil {
404
+ return "", fmt.Errorf("dispatch view %q not found", dispatchViewPath)
405
+ }
406
+ var out strings.Builder
407
+ if err := store.ExecuteWithContext(ctx, tmpl, &out,
408
+ map[string]interface{}{"cc_requests": decoded}); err != nil {
409
+ return "", fmt.Errorf("failed to render the dispatch view for %q: %w", target, err)
410
+ }
411
+ return out.String(), nil
412
+ }
@@ -136,7 +136,13 @@ async function materialiseFile(file) {
136
136
  */
137
137
  export async function buildPageData() {
138
138
  await apiLoadedPromise;
139
- const file = CloudCannon?.currentFile?.();
139
+ let file;
140
+ try {
141
+ file = CloudCannon?.currentFile?.();
142
+ } catch {
143
+ // No current file (page with no associated source).
144
+ return {};
145
+ }
140
146
  if (!file) return {};
141
147
  const inputPath = file.path;
142
148
  const data = (await file.data.get()) ?? {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cloudcannon/editable-regions",
3
- "version": "0.0.19",
3
+ "version": "0.0.20-rc.2",
4
4
  "type": "module",
5
5
  "description": "Visual Editing for the CloudCannon CMS.",
6
6
  "keywords": [
@@ -25,13 +25,19 @@
25
25
  "typecheck:watch": "tsc --noEmit --watch",
26
26
  "lint-autofix": "biome check --fix",
27
27
  "lint": "biome check",
28
+ "build:hugo": "bash integrations/hugo/renderer/build.sh",
28
29
  "test": "vitest run",
29
30
  "test:watch": "vitest",
30
31
  "test:update-snapshots": "vitest run -u",
31
- "test:build-fixtures": "npm run test:build-astro-fixture && npm run test:build-eleventy-fixture && npm run test:build-eleventy-plugin-config",
32
+ "test:build-fixtures": "npm run test:build-astro-fixture && npm run test:build-eleventy-fixture && npm run test:build-eleventy-plugin-config && npm run test:build-hugo-fixture && npm run test:build-hugo-custom-dirs && npm run test:build-hugo-config-options && npm run test:build-hugo-templates-overrides",
33
+ "test:build-hugo-custom-dirs": "npm --prefix test/unit/_fixtures/hugo-custom-dirs run build",
34
+ "test:build-hugo-config-options": "npm --prefix test/unit/_fixtures/hugo-config-options run build",
35
+ "test:build-hugo-templates-overrides": "npm --prefix test/unit/_fixtures/hugo-templates-overrides run build",
36
+ "test:build-hugo-fixtures": "npm run test:build-hugo-fixture && npm run test:build-hugo-custom-dirs && npm run test:build-hugo-config-options && npm run test:build-hugo-templates-overrides",
32
37
  "test:build-astro-fixture": "npm --prefix test/unit/_fixtures/astro install --silent && npm --prefix test/unit/_fixtures/astro run build",
33
38
  "test:build-eleventy-fixture": "npm --prefix test/unit/_fixtures/eleventy install --silent && npm --prefix test/unit/_fixtures/eleventy run build",
34
- "test:build-eleventy-plugin-config": "npm --prefix test/unit/_fixtures/eleventy-plugin-config install --silent && npm --prefix test/unit/_fixtures/eleventy-plugin-config run build"
39
+ "test:build-eleventy-plugin-config": "npm --prefix test/unit/_fixtures/eleventy-plugin-config install --silent && npm --prefix test/unit/_fixtures/eleventy-plugin-config run build",
40
+ "test:build-hugo-fixture": "npm --prefix test/unit/_fixtures/hugo run build"
35
41
  },
36
42
  "files": [
37
43
  "components",
@@ -39,7 +45,9 @@
39
45
  "integrations",
40
46
  "nodes",
41
47
  "styles",
42
- "types"
48
+ "types",
49
+ "!integrations/hugo/renderer/hugo_renderer.wasm",
50
+ "!integrations/hugo/hugo-module/assets/_cloudcannon"
43
51
  ],
44
52
  "exports": {
45
53
  "./*": null,
@@ -87,36 +95,40 @@
87
95
  }
88
96
  },
89
97
  "./eleventy/browser": "./integrations/eleventy/browser/index.mjs",
98
+ "./hugo/browser": {
99
+ "types": "./types/hugo.d.ts",
100
+ "default": "./integrations/hugo/browser/index.ts"
101
+ },
90
102
  "./internal/components": "./components/index.js",
91
103
  "./internal/styles": "./styles/index.js"
92
104
  },
93
105
  "devDependencies": {
94
- "@astrojs/react": "6.0.1",
106
+ "@astrojs/react": "6.0.2",
95
107
  "@astrojs/svelte": "9.0.1",
96
- "@astrojs/vue": "7.0.1",
97
- "@biomejs/biome": "2.5.5",
98
- "@cloudcannon/visual-editor-api": "0.0.19",
108
+ "@astrojs/vue": "7.0.2",
109
+ "@biomejs/biome": "2.5.8",
110
+ "@cloudcannon/visual-editor-api": "0.0.20",
99
111
  "@sindresorhus/slugify": "3.0.0",
100
- "@sveltejs/vite-plugin-svelte": "7.2.0",
112
+ "@sveltejs/vite-plugin-svelte": "7.3.0",
101
113
  "@types/js-beautify": "1.14.3",
102
- "@types/node": "26.1.2",
103
- "@types/react": "19.2.17",
104
- "@types/react-dom": "19.2.3",
114
+ "@types/node": "26.2.0",
115
+ "@types/react": "19.2.18",
116
+ "@types/react-dom": "19.2.4",
105
117
  "@vitejs/plugin-vue": "6.0.8",
106
- "astro": "7.1.4",
107
- "happy-dom": "20.11.1",
118
+ "astro": "7.2.2",
119
+ "happy-dom": "20.11.2",
108
120
  "js-beautify": "2.0.3",
109
- "liquidjs": "10.27.2",
121
+ "liquidjs": "10.29.0",
110
122
  "react": "19.2.8",
111
123
  "react-dom": "19.2.8",
112
124
  "slugify": "1.6.9",
113
- "svelte": "5.56.8",
125
+ "svelte": "5.56.9",
114
126
  "typescript": "6.0.3",
115
127
  "vitest": "4.1.10",
116
- "vue": "3.5.40",
117
- "vue-tsc": "3.3.8"
128
+ "vue": "3.5.41",
129
+ "vue-tsc": "3.3.10"
118
130
  },
119
131
  "dependencies": {
120
- "esbuild": "0.28.1"
132
+ "esbuild": "0.28.2"
121
133
  }
122
134
  }
@@ -0,0 +1,69 @@
1
+ import type {
2
+ CloudCannonVisualEditorAPIRouter,
3
+ CloudCannonVisualEditorAPIV0,
4
+ CloudCannonVisualEditorAPIV1,
5
+ } from "@cloudcannon/visual-editor-api";
6
+
7
+ declare module "@cloudcannon/editable-regions/hugo/browser" {
8
+ /**
9
+ * Boots Hugo live editing from the `window.cc_hugo*` globals emitted by
10
+ * the Hugo module's snapshot prelude. Installs the component proxy
11
+ * immediately; the WASM renderer loads once the CloudCannon Visual Editor
12
+ * API appears.
13
+ */
14
+ export function initHugoLiveEditing(): void;
15
+
16
+ /** Starts (or returns the in-flight start of) the WASM renderer. */
17
+ export function ensureEngine(): Promise<void>;
18
+
19
+ /**
20
+ * Wraps `window.cc_components` in a Proxy manufacturing a renderer for
21
+ * any component name on demand; partial existence is decided by the Hugo
22
+ * renderer at render time. Called by `initHugoLiveEditing`.
23
+ */
24
+ export function initComponentProxy(): void;
25
+ }
26
+
27
+ declare global {
28
+ /** Snapshot metadata emitted onto `window.cc_hugo` by the module's prelude. */
29
+ interface HugoRuntimeMeta {
30
+ generator?: string;
31
+ wasmUrl?: string;
32
+ verbose?: boolean;
33
+ env?: string;
34
+ }
35
+
36
+ /** Result from the editor-site mutation entry points. */
37
+ interface HugoEditorResult {
38
+ error?: string;
39
+ }
40
+
41
+ /** Result from `renderHugoPartials`. */
42
+ interface HugoRenderResult {
43
+ html?: string;
44
+ error?: string;
45
+ }
46
+
47
+ /**
48
+ * The Hugo runtime's `window` doubles as the CloudCannon Visual Editor
49
+ * window and carries the snapshot globals emitted by the module's prelude.
50
+ */
51
+ interface Window {
52
+ /** CloudCannon's versioned API router (present inside the Visual Editor). */
53
+ CloudCannonAPI?: CloudCannonVisualEditorAPIRouter;
54
+ /** The installed v0/v1 CloudCannon API for this page. */
55
+ CloudCannon?: CloudCannonVisualEditorAPIV0 | CloudCannonVisualEditorAPIV1;
56
+ /** Emitter metadata: generator, wasmUrl, verbose, env. */
57
+ cc_hugo?: HugoRuntimeMeta;
58
+ /** Template/config snapshot keyed by physical path. */
59
+ cc_hugo_files?: Record<string, string>;
60
+ }
61
+
62
+ // The Hugo WASM renderer exposes these functions on `globalThis` once it
63
+ // boots; the runtime calls them directly after the engine is ready.
64
+ function writeHugoFiles(json: string): HugoEditorResult | null;
65
+ function removeHugoFiles(json: string): HugoEditorResult | null;
66
+ function readHugoFiles(json: string): Record<string, string>;
67
+ function initHugoEditorSite(): HugoEditorResult | null;
68
+ function renderHugoPartials(json: string): HugoRenderResult | null;
69
+ }