@cloudcannon/editable-regions 0.0.17 → 0.0.19

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.
Files changed (42) hide show
  1. package/helpers/checks.ts +0 -22
  2. package/helpers/cloudcannon.mjs +7 -22
  3. package/helpers/hydrate-editable-regions.ts +2 -1
  4. package/integrations/astro/astro-integration.mjs +1 -4
  5. package/integrations/astro/index.mjs +15 -41
  6. package/integrations/astro/modules/assets.js +1 -4
  7. package/integrations/astro/modules/content.js +2 -11
  8. package/integrations/astro/react-renderer.mjs +98 -24
  9. package/integrations/astro/svelte-renderer.mjs +72 -15
  10. package/integrations/astro/vue-renderer.mjs +61 -0
  11. package/integrations/eleventy/browser/collect-config.mjs +249 -0
  12. package/integrations/eleventy/browser/index.mjs +9 -0
  13. package/integrations/eleventy/browser/inert.mjs +35 -0
  14. package/integrations/eleventy/browser/liquid-builtins.mjs +306 -0
  15. package/integrations/eleventy/browser/liquid-render.mjs +216 -0
  16. package/integrations/eleventy/browser/process-shim.mjs +32 -0
  17. package/integrations/eleventy/browser/stub-mode.mjs +61 -0
  18. package/integrations/eleventy/index.cjs +28 -0
  19. package/integrations/eleventy/index.mjs +634 -0
  20. package/integrations/liquid/README.md +677 -0
  21. package/integrations/liquid/errors.mjs +41 -0
  22. package/integrations/liquid/fs.mjs +36 -75
  23. package/integrations/liquid/globals.mjs +308 -0
  24. package/integrations/liquid/include-with-tag.mjs +84 -0
  25. package/integrations/liquid/index.mjs +166 -170
  26. package/integrations/liquid/logger.mjs +15 -78
  27. package/integrations/liquid/page-map.mjs +32 -0
  28. package/integrations/liquid/shortcodes.mjs +62 -99
  29. package/integrations/react.mjs +5 -9
  30. package/integrations/vue.mjs +28 -0
  31. package/nodes/editable-array-item.ts +6 -1
  32. package/nodes/editable-component.ts +2 -3
  33. package/nodes/editable-text.ts +6 -0
  34. package/nodes/editable.ts +6 -1
  35. package/package.json +120 -79
  36. package/types/astro.d.ts +4 -0
  37. package/types/eleventy.d.cts +20 -0
  38. package/types/eleventy.d.ts +88 -0
  39. package/types/liquid.d.ts +14 -12
  40. package/types/vue.d.ts +40 -0
  41. package/integrations/eleventy.mjs +0 -294
  42. package/integrations/liquid/11ty-filters.mjs +0 -69
@@ -1,20 +1,30 @@
1
- import { evalToken, Liquid, Tokenizer, toPromise } from "liquidjs";
2
- import { eleventyFilters } from "./11ty-filters.mjs";
1
+ import { Liquid } from "liquidjs";
2
+ import { enhanceLiquidError } from "./errors.mjs";
3
3
  import { inMemoryFs } from "./fs.mjs";
4
+ import {
5
+ buildCollectionsData,
6
+ buildPageData,
7
+ setEleventyData,
8
+ } from "./globals.mjs";
9
+ import { createIncludeWithTag } from "./include-with-tag.mjs";
4
10
  import { group, groupEnd, log } from "./logger.mjs";
5
11
  import { createPairedShortcodeTag, createShortcodeTag } from "./shortcodes.mjs";
6
12
 
7
- // Re-export logger utilities for external use
13
+ // Re-exported from their own modules (which avoid this file's browser-runtime
14
+ // side-effects) so the Node-side plugin can reach them via the package root.
15
+ export { createIncludeWithTag } from "./include-with-tag.mjs";
8
16
  export { group, groupEnd, log, setVerbose } from "./logger.mjs";
17
+ export { registerPageMap } from "./page-map.mjs";
9
18
 
10
19
  /** @type {import("liquidjs").Liquid | null} */
11
20
  let sharedLiquidEngine = null;
12
21
 
13
22
  /**
14
- * Creates and configures the shared Liquid engine instance.
23
+ * Creates the shared Liquid engine with the built-in `includeWith` tag. The
24
+ * host wires up its filters/shortcodes/ports afterwards (e.g.
25
+ * `registerEleventyBuiltins(engine)`).
15
26
  *
16
- * @param {{componentDirs?: string[]}} options - Liquid engine options
17
- * @returns {void}
27
+ * @param {import("liquidjs").LiquidOptions} [options] - Spread into `new Liquid(...)`
18
28
  */
19
29
  export function createSharedLiquidEngine(options) {
20
30
  log("Creating shared Liquid engine");
@@ -26,141 +36,178 @@ export function createSharedLiquidEngine(options) {
26
36
  },
27
37
  ...options,
28
38
  });
29
- log("Liquid engine instantiated");
30
-
31
- // Register Eleventy's built-in filters
32
- for (const [name, fn] of Object.entries(eleventyFilters)) {
33
- sharedLiquidEngine.registerFilter(name, fn);
34
- }
35
- log(
36
- "Registered",
37
- Object.keys(eleventyFilters).length,
38
- "built-in 11ty filters",
39
- );
40
39
 
41
40
  log(
42
- "Available files in window.cc_files:",
43
- Object.keys(window.cc_files || {}),
41
+ "Available files in window.cc_liquid_files:",
42
+ Object.keys(window.cc_liquid_files || {}),
44
43
  );
45
44
 
46
45
  sharedLiquidEngine.registerTag(
47
- "bind_include",
48
- createBindIncludeTag(sharedLiquidEngine),
46
+ "includeWith",
47
+ createIncludeWithTag(sharedLiquidEngine),
49
48
  );
50
- log("bind_include tag registered");
49
+
50
+ return sharedLiquidEngine;
51
51
  }
52
52
 
53
53
  /**
54
- * Registers a Liquid component with the CloudCannon component system.
55
- * Creates a wrapper that renders the Liquid template to an HTMLElement.
54
+ * Pins a Liquid component under `key`, taking precedence over the
55
+ * include-resolution proxy. Used for `pluginOptions.liquid.components`.
56
56
  *
57
- * @param {string} key - Unique identifier for the component
58
- * @param {string} contents - The Liquid template contents
59
- * @returns {void}
57
+ * @param {string} key
58
+ * @param {string} contents
60
59
  */
61
60
  export function registerLiquidComponent(key, contents) {
62
61
  log("Registering component:", key);
63
- log("Component contents preview:", contents?.substring?.(0, 200) || contents);
64
62
 
65
63
  if (!sharedLiquidEngine) {
66
64
  throw new Error(
67
65
  `sharedLiquidEngine not defined when registering component ${key}`,
68
66
  );
69
67
  }
70
- const liquidEngine = sharedLiquidEngine;
71
-
72
- /**
73
- * Wrapper function that renders the Liquid component to an HTMLElement.
74
- *
75
- * @param {Object} props - Props to pass to the Liquid template
76
- * @returns {Promise<HTMLElement>} The rendered component as an HTMLElement
77
- */
78
- const wrappedComponent = async (props) => {
79
- group(`Rendering component: ${key}`);
80
- log("Props:", props);
81
- log("Parsing and rendering template...");
82
- const htmlString = await liquidEngine.parseAndRender(contents, props);
83
- log(
84
- "Rendered HTML preview:",
85
- htmlString?.substring?.(0, 200) || htmlString,
86
- );
87
- const rootEl = document.createElement("div");
88
- rootEl.innerHTML = htmlString;
89
- groupEnd();
90
- return rootEl;
91
- };
92
68
 
93
69
  window.cc_components = window.cc_components || {};
94
- window.cc_components[key] = wrappedComponent;
95
- log(`Component registered, ${key}`);
70
+ window.cc_components[key] = createComponentRenderer(key, contents);
96
71
  }
97
72
 
98
73
  /**
99
- * Registers a custom Liquid filter.
74
+ * Wraps `window.cc_components` in a Proxy that resolves any component name on
75
+ * demand via `{% include %}` — the primary resolution path. Names registered
76
+ * via `registerLiquidComponent` take precedence.
77
+ *
78
+ * Call after `createSharedLiquidEngine()` and last of the `register*` calls:
79
+ * publishing `cc_components` is what tells the editor every helper is in
80
+ * place, and an empty one reads as a missing registration script.
100
81
  *
101
- * @param {string} name - The filter name
102
- * @param {any} fn - The filter function
103
82
  * @returns {void}
104
83
  */
105
- export function registerCustomFilter(name, fn) {
84
+ export function initComponentProxy() {
85
+ if (!sharedLiquidEngine) {
86
+ throw new Error(
87
+ "sharedLiquidEngine not defined when initializing component proxy",
88
+ );
89
+ }
90
+
91
+ const target = window.cc_components || {};
92
+
93
+ window.cc_components = new Proxy(target, {
94
+ get(registered, key, receiver) {
95
+ if (Reflect.has(registered, key)) {
96
+ return Reflect.get(registered, key, receiver);
97
+ }
98
+ if (typeof key === "string") {
99
+ return createComponentRenderer(key, `{% include "${key}" %}`);
100
+ }
101
+ return undefined;
102
+ },
103
+ });
104
+ }
105
+
106
+ /**
107
+ * Sets the `eleventy` global on the shared engine. Built at build time by the
108
+ * bundle generator; this is a thin setter.
109
+ *
110
+ * @param {{version: string, generator: string, env: {runMode: string, source: string}, directories: Record<string, string>}} data
111
+ */
112
+ export function registerEleventyData(data) {
113
+ if (!sharedLiquidEngine) {
114
+ throw new Error(
115
+ "sharedLiquidEngine not defined when registering eleventy data",
116
+ );
117
+ }
118
+ /** @type {any} */ (sharedLiquidEngine).options.globals.eleventy = data;
119
+ // Also surface to `globals` so the page proxy can derive `outputPath`.
120
+ setEleventyData(data);
121
+ log("Registered eleventy data, version:", data?.version);
122
+ }
123
+
124
+ /**
125
+ * Merges user-supplied globals (`pluginOptions.globals`) onto the engine. The
126
+ * built-in globals (`page`, `collections`, `eleventy`, `pkg`) are applied
127
+ * separately and take precedence per render.
128
+ *
129
+ * @param {Record<string, unknown>} globals
130
+ */
131
+ export function registerGlobals(globals) {
132
+ if (!sharedLiquidEngine) {
133
+ throw new Error("sharedLiquidEngine not defined when registering globals");
134
+ }
135
+ Object.assign(
136
+ /** @type {any} */ (sharedLiquidEngine).options.globals,
137
+ globals ?? {},
138
+ );
139
+ log("Registered", Object.keys(globals ?? {}).length, "custom globals");
140
+ }
141
+
142
+ /**
143
+ * Sets the `pkg` global (11ty exposes `package.json` this way by default).
144
+ *
145
+ * @param {Record<string, any>} pkg
146
+ */
147
+ export function registerPkg(pkg) {
148
+ if (!sharedLiquidEngine) {
149
+ throw new Error("sharedLiquidEngine not defined when registering pkg");
150
+ }
151
+ /** @type {any} */ (sharedLiquidEngine).options.globals.pkg = pkg ?? {};
152
+ log("Registered pkg, fields:", Object.keys(pkg ?? {}).length);
153
+ }
154
+
155
+ /**
156
+ * Registers a Liquid filter. Called by both the auto-mirror pass and
157
+ * user-supplied overrides (`pluginOptions.liquid.filters`).
158
+ *
159
+ * @param {string} name
160
+ * @param {any} fn
161
+ */
162
+ export function registerFilter(name, fn) {
106
163
  log("Registering filter:", name);
107
164
  if (!sharedLiquidEngine) {
108
165
  throw new Error(
109
- `sharedLiquidEngine not defined when registering custom filter ${name}`,
166
+ `sharedLiquidEngine not defined when registering filter ${name}`,
110
167
  );
111
168
  }
112
169
  sharedLiquidEngine.registerFilter(name, fn);
113
170
  }
114
171
 
115
172
  /**
116
- * Registers a custom shortcode.
173
+ * Registers a Liquid shortcode. Usage: {% shortcodeName arg1, arg2 %}
117
174
  *
118
- * Usage in templates: {% shortcodeName arg1, arg2 %}
119
- *
120
- * @param {string} name - The shortcode name (used as the tag name)
121
- * @param {any} fn - The shortcode function (arg1, arg2, ...) => string
122
- * @returns {void}
175
+ * @param {string} name
176
+ * @param {any} fn - (arg1, arg2, ...) => string
123
177
  */
124
- export function registerCustomShortcode(name, fn) {
178
+ export function registerShortcode(name, fn) {
125
179
  log("Registering shortcode:", name);
126
180
  if (!sharedLiquidEngine) {
127
181
  throw new Error(
128
- `sharedLiquidEngine not defined when registering custom shortcode ${name}`,
182
+ `sharedLiquidEngine not defined when registering shortcode ${name}`,
129
183
  );
130
184
  }
131
- sharedLiquidEngine.registerTag(name, createShortcodeTag(fn, name));
185
+ sharedLiquidEngine.registerTag(name, createShortcodeTag(name, fn));
132
186
  }
133
187
 
134
188
  /**
135
- * Registers a custom paired shortcode (with content between tags).
136
- *
137
- * Usage in templates: {% shortcodeName arg %}content{% endshortcodeName %}
189
+ * Registers a Liquid paired shortcode.
190
+ * Usage: {% shortcodeName arg %}content{% endshortcodeName %}
138
191
  *
139
- * @param {string} name - The shortcode name (used as the tag name)
140
- * @param {any} fn - The shortcode function (content, arg1, ...) => string
141
- * @returns {void}
192
+ * @param {string} name
193
+ * @param {any} fn - (content, arg1, ...) => string
142
194
  */
143
- export function registerCustomPairedShortcode(name, fn) {
195
+ export function registerPairedShortcode(name, fn) {
144
196
  log("Registering paired shortcode:", name);
145
197
  if (!sharedLiquidEngine) {
146
198
  throw new Error(
147
- `sharedLiquidEngine not defined when registering custom paired shortcode ${name}`,
199
+ `sharedLiquidEngine not defined when registering paired shortcode ${name}`,
148
200
  );
149
201
  }
150
202
  sharedLiquidEngine.registerTag(name, createPairedShortcodeTag(name, fn));
151
203
  }
152
204
 
153
205
  /**
154
- * Registers a custom tag with full LiquidJS parser access.
155
- *
156
- * Custom tags are more powerful than shortcodes - they receive full access to
157
- * the LiquidJS parser and can implement complex parsing/rendering logic.
158
- *
159
- * Usage in templates: {% tagName args %}
206
+ * Registers a custom tag with full LiquidJS parser access (the factory
207
+ * receives the engine). Usage: {% tagName args %}
160
208
  *
161
- * @param {string} name - The tag name
162
- * @param {any} factory - Factory function (liquidEngine) => { parse(), render() }
163
- * @returns {void}
209
+ * @param {string} name
210
+ * @param {any} factory - (liquidEngine) => { parse(), render() }
164
211
  */
165
212
  export function registerCustomTag(name, factory) {
166
213
  log("Registering custom tag:", name);
@@ -173,97 +220,46 @@ export function registerCustomTag(name, factory) {
173
220
  }
174
221
 
175
222
  /**
176
- * Creates a bind_include tag for spreading object props into includes.
177
- * Like Astro's {...props} spread for Liquid includes.
178
- *
179
- * Usage: {% bind_include "path/to/partial", objectToSpread %}
223
+ * Wraps `parseAndRender` with logging, error mapping, and HTMLElement output.
180
224
  *
181
- * @param {any} _liquidEngine - The LiquidJS engine instance (provided by LiquidJS, accessed via this.liquid)
182
- * @returns {any} Tag implementation with parse and render methods
225
+ * @param {string} name
226
+ * @param {string} templateSource - A literal template, or `{% include "name" %}`
227
+ * @returns {(props: Record<string, any>) => Promise<HTMLElement>}
183
228
  */
184
- export function createBindIncludeTag(_liquidEngine) {
185
- return {
186
- /**
187
- * Parses the bind_include tag arguments.
188
- * @param {any} tagToken - The tag token from LiquidJS parser
189
- */
190
- parse(tagToken) {
191
- log("bind_include parsing tag with args:", tagToken.args);
192
- const tokenizer = new Tokenizer(
193
- tagToken.args,
194
- this.liquid.options.operatorsTrie,
229
+ function createComponentRenderer(name, templateSource) {
230
+ return async (props) => {
231
+ if (!sharedLiquidEngine) {
232
+ throw new Error(
233
+ `sharedLiquidEngine not defined when rendering component ${name}`,
195
234
  );
235
+ }
236
+ group(`Rendering component: ${name}`);
237
+ log("Props:", props);
196
238
 
197
- this.pathToken = tokenizer.readValue();
198
- if (!this.pathToken)
199
- throw new Error("bind_include: missing path argument");
200
- log("bind_include parsed path token:", this.pathToken);
201
-
202
- tokenizer.skipBlank();
203
- if (tokenizer.peek() !== ",")
204
- throw new Error("bind_include: expected comma separator");
205
- tokenizer.advance();
206
- tokenizer.skipBlank();
207
-
208
- this.objectToken = tokenizer.readValue();
209
- if (!this.objectToken)
210
- throw new Error("bind_include: missing object argument");
211
- log("bind_include parsed object token:", this.objectToken);
212
- },
213
-
214
- /**
215
- * Renders the included template with spread props.
216
- * @param {any} context - The LiquidJS render context
217
- */
218
- async render(context) {
219
- group("bind_include rendering");
220
- log("Evaluating path token...");
221
- const path = await toPromise(evalToken(this.pathToken, context));
222
- log("Path resolved to:", path);
223
-
224
- log("Evaluating object token...");
225
- const obj = await toPromise(evalToken(this.objectToken, context));
226
- log("Object resolved to:", obj);
227
-
228
- if (!path || typeof path !== "string") {
229
- groupEnd();
230
- throw new Error(`bind_include: invalid path "${path}"`);
231
- }
232
- if (!obj || typeof obj !== "object") {
233
- log("Object is not valid, returning empty");
234
- groupEnd();
235
- return;
236
- }
237
-
238
- log(
239
- "Including:",
240
- path,
241
- "with",
242
- Object.keys(obj).length,
243
- "props:",
244
- Object.keys(obj),
239
+ let htmlString;
240
+ try {
241
+ htmlString = await sharedLiquidEngine.parseAndRender(
242
+ templateSource,
243
+ // `page`/`collections` spread last so props can't shadow them
244
+ // (mirroring 11ty); they resolve at the top-level globals level.
245
+ {
246
+ ...props,
247
+ page: buildPageData(),
248
+ collections: buildCollectionsData(),
249
+ },
245
250
  );
246
-
247
- context.push(obj);
248
- try {
249
- log("Parsing file:", path);
250
- const templates = await this.liquid.parseFile(path);
251
- log("File parsed, template count:", templates?.length || 0);
252
-
253
- log("Rendering templates...");
254
- const result = await this.liquid.render(templates, context);
255
- log("Rendered result preview:", result?.substring?.(0, 200) || result);
256
- groupEnd();
257
- return result;
258
- } catch (err) {
259
- const error = /** @type {Error} */ (err);
260
- log("Error during render:", error.message);
261
- log("Full error:", error);
262
- groupEnd();
263
- throw error;
264
- } finally {
265
- context.pop();
266
- }
267
- },
251
+ } catch (err) {
252
+ log("Error during render:", err);
253
+ groupEnd();
254
+ throw enhanceLiquidError(err, name);
255
+ }
256
+ log(
257
+ "Rendered HTML preview:",
258
+ htmlString?.substring?.(0, 200) || htmlString,
259
+ );
260
+ const rootEl = document.createElement("div");
261
+ rootEl.innerHTML = htmlString;
262
+ groupEnd();
263
+ return rootEl;
268
264
  };
269
265
  }
@@ -1,16 +1,8 @@
1
- /**
2
- * Simple logger for live editing integration.
3
- * Enable verbose mode to see detailed logs in browser console.
4
- */
1
+ // Logging is gated on verbose mode, except `warn`/`warnOnce`.
5
2
 
6
3
  let verboseEnabled = false;
7
4
 
8
- /**
9
- * Enables or disables verbose logging.
10
- *
11
- * @param {boolean} enabled - Whether to enable verbose logging
12
- * @returns {void}
13
- */
5
+ /** @param {boolean} enabled */
14
6
  export function setVerbose(enabled) {
15
7
  verboseEnabled = enabled;
16
8
  if (enabled) {
@@ -18,91 +10,36 @@ export function setVerbose(enabled) {
18
10
  }
19
11
  }
20
12
 
21
- /**
22
- * Returns whether verbose logging is enabled.
23
- *
24
- * @returns {boolean}
25
- */
26
- export function isVerbose() {
27
- return verboseEnabled;
28
- }
29
-
30
- /**
31
- * Log only when verbose mode is enabled.
32
- * Use for diagnostic information during development.
33
- *
34
- * @param {...any} args - Arguments to log
35
- * @returns {void}
36
- */
37
- export function log(...args) {
13
+ export function log(/** @type {any[]} */ ...args) {
38
14
  if (verboseEnabled) {
39
15
  console.log(...args);
40
16
  }
41
17
  }
42
18
 
43
- /**
44
- * Always log warnings.
45
- *
46
- * @param {...any} args - Arguments to log
47
- * @returns {void}
48
- */
49
- export function warn(...args) {
19
+ export function warn(/** @type {any[]} */ ...args) {
50
20
  console.warn(...args);
51
21
  }
52
22
 
53
- /**
54
- * Always log errors.
55
- *
56
- * @param {...any} args - Arguments to log
57
- * @returns {void}
58
- */
59
- export function error(...args) {
60
- console.error(...args);
23
+ const warnedKeys = new Set();
24
+
25
+ /** Warns once per key for the lifetime of the page. */
26
+ export function warnOnce(
27
+ /** @type {string} */ key,
28
+ /** @type {any[]} */ ...args
29
+ ) {
30
+ if (warnedKeys.has(key)) return;
31
+ warnedKeys.add(key);
32
+ warn(...args);
61
33
  }
62
34
 
63
- /**
64
- * Group logs (only in verbose mode).
65
- *
66
- * @param {string} label - Group label
67
- * @returns {void}
68
- */
69
- export function group(label) {
35
+ export function group(/** @type {string} */ label) {
70
36
  if (verboseEnabled) {
71
37
  console.group(label);
72
38
  }
73
39
  }
74
40
 
75
- /**
76
- * End a console group (only in verbose mode).
77
- *
78
- * @returns {void}
79
- */
80
41
  export function groupEnd() {
81
42
  if (verboseEnabled) {
82
43
  console.groupEnd();
83
44
  }
84
45
  }
85
-
86
- /**
87
- * Start timing an operation (only in verbose mode).
88
- *
89
- * @param {string} label - Timer label
90
- * @returns {void}
91
- */
92
- export function time(label) {
93
- if (verboseEnabled) {
94
- console.time(label);
95
- }
96
- }
97
-
98
- /**
99
- * End timing an operation (only in verbose mode).
100
- *
101
- * @param {string} label - Timer label
102
- * @returns {void}
103
- */
104
- export function timeEnd(label) {
105
- if (verboseEnabled) {
106
- console.timeEnd(label);
107
- }
108
- }
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Build-time snapshot of every page Eleventy produced, keyed by normalized
3
+ * input path. Holds permalinks computed by JS config or `eleventyComputed`,
4
+ * which the live front-matter read can't see. Lives in its own module so
5
+ * consumers can import it without the side-effects of `helpers/cloudcannon.mjs`.
6
+ *
7
+ * @typedef {{ url?: string, outputPath?: string }} PageMapEntry
8
+ */
9
+
10
+ /** @type {Record<string, PageMapEntry>} */
11
+ let pageMap = {};
12
+
13
+ /** @param {Record<string, PageMapEntry> | null | undefined} map */
14
+ export function registerPageMap(map) {
15
+ pageMap = map ?? {};
16
+ }
17
+
18
+ export function getPageMap() {
19
+ return pageMap;
20
+ }
21
+
22
+ /**
23
+ * Normalises an Eleventy input path (`./src/foo.md`, `/src/foo.md`) to the
24
+ * no-leading-slash form used as the map's keys, so paths from different
25
+ * sources (11ty `results`, CC `currentFile().path`) compare equal.
26
+ *
27
+ * @param {string | null | undefined} p
28
+ */
29
+ export function normalizeInputPath(p) {
30
+ if (typeof p !== "string" || !p) return "";
31
+ return p.replace(/^\.\//, "").replace(/^\/+/, "");
32
+ }