@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
@@ -0,0 +1,216 @@
1
+ /**
2
+ * Browser shims for Eleventy's RenderPlugin
3
+ * (https://v3.11ty.dev/docs/plugins/render/): `renderTemplate` (paired tag),
4
+ * `renderContent` (filter), `renderFile` (shortcode). Each compiles a body as
5
+ * `templateLang` and renders it with `data`. Only LiquidJS runs in the browser,
6
+ * so: "liquid"/unspecified → real render; "html" → passthrough; else →
7
+ * warn-once and return the body unchanged.
8
+ */
9
+
10
+ import {
11
+ apiLoadedPromise,
12
+ CloudCannon,
13
+ } from "../../../helpers/cloudcannon.mjs";
14
+ import { warnOnce } from "../../liquid/logger.mjs";
15
+ import { evaluateArgs, parseArgs } from "../../liquid/shortcodes.mjs";
16
+
17
+ const supportedEngines = new Set(["liquid", "html"]);
18
+
19
+ /**
20
+ * Tag factory for `{% renderTemplate ... %}…{% endrenderTemplate %}`. Captures
21
+ * the body as raw source (matching upstream — compiled in the requested engine,
22
+ * not pre-rendered) and renders it against `data`.
23
+ *
24
+ * @param {any} _liquidEngine - Unused; reached via `this.liquid`
25
+ * @returns {any}
26
+ */
27
+ export function createRenderTemplateTag(_liquidEngine) {
28
+ return {
29
+ parse(/** @type {any} */ tagToken, /** @type {any[]} */ remainTokens) {
30
+ this.name = tagToken.name;
31
+ this.argTokens = parseArgs(
32
+ tagToken.args,
33
+ this.liquid.options.operatorsTrie,
34
+ );
35
+ this.bodyTokens = [];
36
+
37
+ const endTagName = `end${this.name}`;
38
+ while (remainTokens.length) {
39
+ const token = remainTokens.shift();
40
+ if (token.name === endTagName) return;
41
+
42
+ this.bodyTokens.push(token);
43
+ }
44
+
45
+ throw new Error(`tag ${this.name} not closed`);
46
+ },
47
+
48
+ async render(/** @type {any} */ context) {
49
+ const args = await evaluateArgs(this.argTokens, context);
50
+ const { templateLang, data } = normalizeRenderArgs([args[0], args[1]]);
51
+ const body = this.bodyTokens
52
+ .map((/** @type {any} */ t) => t.getText())
53
+ .join("");
54
+
55
+ if (templateLang && !supportedEngines.has(templateLang)) {
56
+ warnOnce(
57
+ `render-template:${templateLang}`,
58
+ unsupportedEngineMessage(templateLang),
59
+ );
60
+ return body;
61
+ }
62
+
63
+ if (templateLang === "html") return body;
64
+ return await this.liquid.parseAndRender(body, data);
65
+ },
66
+ };
67
+ }
68
+
69
+ /**
70
+ * Builds the `renderContent` filter, capturing the shared engine.
71
+ *
72
+ * @param {any} liquidEngine
73
+ */
74
+ export function createRenderContentFilter(liquidEngine) {
75
+ return async function renderContent(
76
+ /** @type {any} */ content,
77
+ /** @type {any} */ templateLang,
78
+ /** @type {any} */ data,
79
+ ) {
80
+ const normalized = normalizeRenderArgs([templateLang, data]);
81
+ const body = content == null ? "" : String(content);
82
+
83
+ if (
84
+ normalized.templateLang &&
85
+ !supportedEngines.has(normalized.templateLang)
86
+ ) {
87
+ warnOnce(
88
+ `render-content:${normalized.templateLang}`,
89
+ unsupportedEngineMessage(normalized.templateLang),
90
+ );
91
+
92
+ return body;
93
+ }
94
+
95
+ if (normalized.templateLang === "html") return body;
96
+
97
+ return await liquidEngine.parseAndRender(body, normalized.data);
98
+ };
99
+ }
100
+
101
+ /**
102
+ * Builds the `renderFile` shortcode. Fetches `inputPath` via the CloudCannon
103
+ * API and renders its body with `data`. Engine comes from `templateLang`, else
104
+ * inferred from the file extension.
105
+ *
106
+ * @param {any} liquidEngine
107
+ */
108
+ export function createRenderFileShortcode(liquidEngine) {
109
+ return async function renderFile(
110
+ /** @type {any} */ inputPath,
111
+ /** @type {any} */ data,
112
+ /** @type {any} */ templateLang,
113
+ ) {
114
+ if (typeof inputPath !== "string" || !inputPath) {
115
+ warnOnce(
116
+ "render-file:no-path",
117
+ "renderFile: missing or non-string path argument. Returning empty.",
118
+ );
119
+
120
+ return "";
121
+ }
122
+
123
+ // Normalise Eleventy-style paths (`./foo`, `/foo`) to the
124
+ // project-relative shape the CC API expects.
125
+ const normalizedPath = inputPath.replace(/^\.\/+/, "").replace(/^\/+/, "");
126
+
127
+ await apiLoadedPromise;
128
+
129
+ const file = CloudCannon?.file?.(normalizedPath);
130
+ if (!file) {
131
+ warnOnce(
132
+ `render-file-missing:${inputPath}`,
133
+ `renderFile: CloudCannon API not available; cannot load "${inputPath}".`,
134
+ );
135
+ return "";
136
+ }
137
+
138
+ // Mirror 11ty's data cascade: the file's own front matter is the base,
139
+ // the caller's `data` arg overrides on top. `content.get()` strips it.
140
+ let body;
141
+ let frontMatter;
142
+
143
+ try {
144
+ [body, frontMatter] = await Promise.all([
145
+ file.content.get(),
146
+ file.data.get(),
147
+ ]);
148
+ } catch (err) {
149
+ warnOnce(
150
+ `render-file-missing:${inputPath}`,
151
+ `renderFile: failed to load "${inputPath}" via the CloudCannon API ` +
152
+ `(${err instanceof Error ? err.message : String(err)}).`,
153
+ );
154
+
155
+ return "";
156
+ }
157
+
158
+ const engine =
159
+ (typeof templateLang === "string" && templateLang) ||
160
+ inferEngineFromPath(inputPath);
161
+
162
+ if (engine && !supportedEngines.has(engine)) {
163
+ warnOnce(`render-file:${engine}`, unsupportedEngineMessage(engine));
164
+
165
+ return body;
166
+ }
167
+
168
+ if (engine === "html") return body;
169
+
170
+ const mergedData = { ...(frontMatter ?? {}), ...(data ?? {}) };
171
+
172
+ return await liquidEngine.parseAndRender(body, mergedData);
173
+ };
174
+ }
175
+
176
+ /**
177
+ * Normalises the `(templateLang, data)` pair, supporting the `(lang, data)`
178
+ * and lang-omitted `(data)` overloads.
179
+ *
180
+ * @param {[any, any]} args
181
+ * @returns {{templateLang: string | undefined, data: any}}
182
+ */
183
+ function normalizeRenderArgs([templateLang, data]) {
184
+ if (templateLang && typeof templateLang !== "string") {
185
+ data = templateLang;
186
+ templateLang = undefined;
187
+ }
188
+
189
+ return { templateLang, data: data ?? {} };
190
+ }
191
+
192
+ /**
193
+ * Guesses the engine from the extension so the warn-once message can name it;
194
+ * only "liquid"/"html" actually render, the rest fall through to passthrough.
195
+ *
196
+ * @param {string} inputPath
197
+ */
198
+ function inferEngineFromPath(inputPath) {
199
+ const dot = inputPath.lastIndexOf(".");
200
+ if (dot < 0) return undefined;
201
+
202
+ const ext = inputPath.slice(dot + 1).toLowerCase();
203
+ if (ext === "liquid") return "liquid";
204
+ if (ext === "html" || ext === "htm") return "html";
205
+ if (ext === "md") return "md";
206
+ if (ext === "njk") return "njk";
207
+ return undefined;
208
+ }
209
+
210
+ function unsupportedEngineMessage(/** @type {string} */ engineName) {
211
+ return (
212
+ `Eleventy RenderPlugin: engine "${engineName}" is not supported in ` +
213
+ `live editing (only "liquid" and "html" run in the browser). ` +
214
+ "Returning the body unchanged."
215
+ );
216
+ }
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Stand-ins for the Node globals a config reaches for without importing
3
+ * anything, so there's no module to stub — `process.env.X` at the top of a
4
+ * config otherwise kills the bundle with `ReferenceError: process is not
5
+ * defined`. esbuild's `inject` substitutes these for unbound identifiers.
6
+ */
7
+
8
+ /**
9
+ * `"development"` for the same reason `eleventy.env.runMode` is `"serve"`: a
10
+ * config gated on `NODE_ENV === "production"` shouldn't drag build-only
11
+ * plugins into the mirror. Real values belong in `pluginOptions.globals`.
12
+ *
13
+ * esbuild defines the exact expression `process.env.NODE_ENV` itself, and that
14
+ * wins over this object — changing the value here only affects indirect reads
15
+ * like `const e = process.env`.
16
+ */
17
+ export const process = {
18
+ env: { NODE_ENV: "development" },
19
+ argv: [],
20
+ platform: "browser",
21
+ version: "",
22
+ versions: {},
23
+ browser: true,
24
+ cwd: () => "/",
25
+ nextTick: (/** @type {any} */ fn, /** @type {any[]} */ ...args) =>
26
+ queueMicrotask(() => fn(...args)),
27
+ };
28
+
29
+ export const __dirname = "/";
30
+ export const __filename = "/";
31
+
32
+ export default process;
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Strictness switch for the stubs `createBrowserStubPlugin` generates. The two
3
+ * phases want opposite behaviour:
4
+ *
5
+ * - **Config replay** — skip and warn. An argument-side call like
6
+ * `addPlugin(pluginBookshop({…}))` runs before `addPlugin` is reached, so a
7
+ * throw escapes the config function and loses every helper below that line.
8
+ * - **Render time** — throw. That's the documented signal to add a
9
+ * `pluginOptions.liquid.<kind>` override, named by `enhanceLiquidError`.
10
+ *
11
+ * `collect-config.mjs` flips it once the mirror finishes, and the bundle awaits
12
+ * that before publishing components, so the phases can't overlap.
13
+ */
14
+
15
+ import { log, warnOnce } from "../../liquid/logger.mjs";
16
+ import { createInertValue } from "./inert.mjs";
17
+
18
+ /**
19
+ * This plugin's own Eleventy entry — the one entry in `ALWAYS_STUBBED`. Every
20
+ * config calls it via `addPlugin`, and the browser bundle registers its helpers
21
+ * itself, so skipping it is expected and there's nothing to act on.
22
+ */
23
+ const SELF_SPECIFIER = "@cloudcannon/editable-regions/eleventy";
24
+
25
+ let strict = false;
26
+
27
+ /** Called by the mirror once every helper has been registered. */
28
+ export function setStubsStrict() {
29
+ strict = true;
30
+ }
31
+
32
+ /**
33
+ * @param {string} specifier - The stubbed module, e.g. `"node:fs"`
34
+ * @param {"called" | "constructed"} verb
35
+ * @returns {any} An inert stand-in, while we're still replaying the config
36
+ */
37
+ export function onStubInvoked(specifier, verb) {
38
+ if (strict) {
39
+ throw new Error(
40
+ `editable-regions: "${specifier}" was ${verb} in the browser ` +
41
+ "live-editing bundle. It's a Node/build-time module with no browser " +
42
+ "equivalent — provide an override via pluginOptions.liquid.<kind>.",
43
+ );
44
+ }
45
+
46
+ if (specifier === SELF_SPECIFIER) {
47
+ log(
48
+ `[editable-regions] "${specifier}" was ${verb} and skipped, as expected.`,
49
+ );
50
+ } else {
51
+ warnOnce(
52
+ `eleventy-stub:${specifier}`,
53
+ `[editable-regions] "${specifier}" was ${verb} while replaying your ` +
54
+ "Eleventy config for live editing. It's a Node/build-time module, so " +
55
+ "the call was skipped and the rest of the config still mirrored. If a " +
56
+ "filter or shortcode is missing from the editor, this is the reason.",
57
+ );
58
+ }
59
+
60
+ return createInertValue();
61
+ }
@@ -0,0 +1,28 @@
1
+ // Node can `require()` an ES module from v20.19 / v22.12 onward, so this stays
2
+ // a re-export. Older runtimes throw a bare ERR_REQUIRE_ESM naming neither this
3
+ // package nor a way out — translate it.
4
+
5
+ /** @type {any} */
6
+ let editableRegionsPlugin;
7
+
8
+ try {
9
+ editableRegionsPlugin = require("./index.mjs").default;
10
+ } catch (err) {
11
+ const code = /** @type {{ code?: string } | undefined} */ (err)?.code;
12
+ if (code !== "ERR_REQUIRE_ESM") throw err;
13
+
14
+ throw new Error(
15
+ "@cloudcannon/editable-regions/eleventy is an ES module, and Node " +
16
+ `${process.version} can't \`require()\` one (needs v20.19+ or v22.12+).` +
17
+ "\n\nEither upgrade Node, or load the plugin with a dynamic import from " +
18
+ "an async Eleventy config:\n\n" +
19
+ " module.exports = async function (eleventyConfig) {\n" +
20
+ " const { default: editableRegions } = await import(\n" +
21
+ ' "@cloudcannon/editable-regions/eleventy"\n' +
22
+ " );\n" +
23
+ " eleventyConfig.addPlugin(editableRegions);\n" +
24
+ " };\n",
25
+ );
26
+ }
27
+
28
+ module.exports = editableRegionsPlugin;