@cloudcannon/editable-regions 0.0.18 → 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.
package/helpers/checks.ts CHANGED
@@ -1,7 +1,3 @@
1
- import Editable from "../nodes/editable.js";
2
- import EditableArrayItem from "../nodes/editable-array-item.js";
3
- import EditableText from "../nodes/editable-text.js";
4
-
5
1
  const getEditableType = (el: HTMLElement): string | undefined => {
6
2
  if (el.tagName.startsWith("EDITABLE-")) {
7
3
  return el.tagName.slice(9).toLowerCase();
@@ -9,24 +5,6 @@ const getEditableType = (el: HTMLElement): string | undefined => {
9
5
  return el.dataset.editable;
10
6
  };
11
7
 
12
- export const hasEditable = <T extends object>(
13
- el: T,
14
- ): el is T & { editable: Editable } => {
15
- return "editable" in el && el.editable instanceof Editable;
16
- };
17
-
18
- export const hasEditableText = <T extends object>(
19
- el: T,
20
- ): el is T & { editable: EditableText } => {
21
- return "editable" in el && el.editable instanceof EditableText;
22
- };
23
-
24
- export const hasEditableArrayItem = <T extends object>(
25
- el: T,
26
- ): el is T & { editable: EditableArrayItem } => {
27
- return "editable" in el && el.editable instanceof EditableArrayItem;
28
- };
29
-
30
8
  export const isEditableWebcomponent = (el: unknown): boolean => {
31
9
  if (!(el instanceof HTMLElement)) {
32
10
  return false;
@@ -7,7 +7,8 @@ import {
7
7
  EditableSource,
8
8
  EditableText,
9
9
  } from "../nodes";
10
- import { hasEditable, isEditableWebcomponent } from "./checks";
10
+ import { hasEditable } from "../nodes/editable.js";
11
+ import { isEditableWebcomponent } from "./checks";
11
12
 
12
13
  const baseEditableMap: Record<string, typeof Editable | undefined> = {
13
14
  array: EditableArray,
@@ -1,54 +1,129 @@
1
- import { createElement } from "react";
1
+ /** biome-ignore-all lint/suspicious/noPrototypeBuiltins: Matches the behaviour of @astrojs/react */
2
+ import ssr from "@astrojs/react/server.js";
3
+ import * as React from "react";
2
4
  import { flushSync } from "react-dom";
3
5
  import { createRoot } from "react-dom/client";
4
- import { renderToStaticMarkup } from "react-dom/server.browser";
5
-
6
6
  import { addFrameworkRenderer, queueForClientSideRender } from "./index.mjs";
7
7
 
8
+ /**
9
+ * @param {string} str
10
+ * @returns {string}
11
+ */
12
+ const slotName = (str) =>
13
+ str.trim().replace(/[-_]([a-z])/g, (_, w) => w.toUpperCase());
14
+ const reactTypeof = Symbol.for("react.element");
15
+ const reactTransitionalTypeof = Symbol.for("react.transitional.element");
16
+
8
17
  addFrameworkRenderer({
9
18
  name: "@astrojs/react",
10
19
  clientEntrypoint: "@astrojs/react/client.js",
11
20
  ssr: {
12
21
  /**
13
22
  * @param {any} Component
14
- * @returns {boolean}
23
+ * @param {any} props
24
+ * @param {Record<string, string>} children
25
+ * @returns {Promise<boolean>}
15
26
  */
16
- check: (Component) => {
27
+ check: async (Component, props, children) => {
28
+ if (typeof Component === "object") {
29
+ return !!Component.$$typeof
30
+ ?.toString()
31
+ .slice("Symbol(".length)
32
+ .startsWith("react");
33
+ }
17
34
  if (typeof Component !== "function") return false;
35
+ if (Component.name === "QwikComponent") return false;
18
36
 
19
- // React class components have render on the prototype
20
- if (typeof Component.prototype?.render === "function") return true;
37
+ if (
38
+ typeof Component === "function" &&
39
+ Component.$$typeof === Symbol.for("react.forward_ref")
40
+ )
41
+ return false;
21
42
 
22
- // React functional components return vnodes with $$typeof
23
- try {
24
- const vnode = Component({});
43
+ if (
44
+ Component.prototype != null &&
45
+ typeof Component.prototype.render === "function"
46
+ ) {
25
47
  return (
26
- vnode != null && typeof vnode === "object" && "$$typeof" in vnode
48
+ React.Component.isPrototypeOf(Component) ||
49
+ React.PureComponent.isPrototypeOf(Component)
27
50
  );
28
- } catch {
29
- return false;
30
51
  }
52
+
53
+ let isReactComponent = false;
54
+ /** @param {...any} args */
55
+ function Tester(...args) {
56
+ try {
57
+ const vnode = Component(...args);
58
+ if (
59
+ vnode &&
60
+ (vnode.$$typeof === reactTypeof ||
61
+ vnode.$$typeof === reactTransitionalTypeof)
62
+ ) {
63
+ isReactComponent = true;
64
+ }
65
+ } catch {}
66
+
67
+ return React.createElement("div");
68
+ }
69
+
70
+ await ssr.renderToStaticMarkup.call(this, Tester, props, children);
71
+
72
+ return isReactComponent;
31
73
  },
32
74
  /**
33
75
  * Renders to static markup, falling back to a client-side render queue.
34
76
  * @param {any} Component
35
77
  * @param {any} props
78
+ * @param {Record<string, string>} inputSlotted
79
+ * @param {any} metadata
36
80
  * @returns {Promise<{ html: string }>}
37
81
  */
38
- renderToStaticMarkup: async (Component, props) => {
39
- try {
40
- const reactNode = Component(props);
41
- return { html: renderToStaticMarkup(reactNode) };
42
- } catch (_err) {
82
+ renderToStaticMarkup: async (Component, props, inputSlotted, metadata) => {
83
+ if (metadata?.hydrate) {
84
+ const { default: children, ...slotted } = inputSlotted;
85
+ /** @type{Record<string, React.ReactNode>} */
86
+ const slots = {};
87
+ for (const [key, value] of Object.entries(slotted)) {
88
+ const name = slotName(key);
89
+ slots[name] = React.createElement("astro-static-slot", {
90
+ suppressHydrationWarning: true,
91
+ // biome-ignore lint/security/noDangerouslySetInnerHtml: Intentionally rendering static html
92
+ dangerouslySetInnerHTML: { __html: value },
93
+ });
94
+ }
95
+
96
+ const newProps = {
97
+ ...props,
98
+ ...slots,
99
+ };
100
+ const newChildren = children ?? props.children;
101
+ if (newChildren != null) {
102
+ newProps.children = React.createElement("astro-static-slot", {
103
+ suppressHydrationWarning: true,
104
+ // biome-ignore lint/security/noDangerouslySetInnerHtml: Intentionally rendering static html
105
+ dangerouslySetInnerHTML: { __html: newChildren },
106
+ });
107
+ }
108
+
43
109
  const id = queueForClientSideRender((node) => {
44
- const reactNode = createElement(Component, props, null);
110
+ const reactNode = React.createElement(Component, newProps);
45
111
  const root = createRoot(node);
46
112
  flushSync(() => root.render(reactNode));
47
113
  });
114
+
48
115
  return {
49
116
  html: `<div data-editable-region-csr-id=${id}></div>`,
50
117
  };
51
118
  }
119
+
120
+ return ssr.renderToStaticMarkup.call(
121
+ this,
122
+ Component,
123
+ props,
124
+ inputSlotted,
125
+ { ...metadata, astroStaticSlot: true, hydrate: false },
126
+ );
52
127
  },
53
128
  },
54
129
  });
@@ -1,3 +1,4 @@
1
+ import { createRawSnippet } from "svelte";
1
2
  import { addFrameworkRenderer, queueForClientSideRender } from "./index.mjs";
2
3
 
3
4
  /** @type{((component: any, args: { target: HTMLElement, props: unknown }) => void) | undefined} */
@@ -39,27 +40,83 @@ addFrameworkRenderer({
39
40
  return false;
40
41
  },
41
42
  /**
43
+ * Renders to static markup, falling back to a client-side render queue.
42
44
  * @param {any} Component
43
- * @param {unknown} props
44
- * @returns {Promise<{html: string}>}
45
+ * @param {any} props
46
+ * @param {Record<string, string>} slots
47
+ * @param {any} metadata
48
+ * @returns {Promise<{ html: string }>}
45
49
  */
46
- renderToStaticMarkup: async (Component, props) => {
47
- const id = queueForClientSideRender((node) => {
48
- if (mount) {
49
- mount(Component, {
50
- target: /** @type{any}*/ (node),
51
- props,
52
- });
50
+ renderToStaticMarkup: async (Component, props, slots, metadata) => {
51
+ /** @type{Record<string, any>} */
52
+ const renderProps = {};
53
+ /** @type{Record<string, any> | undefined} */
54
+ let $$slots;
55
+ /** @type{import("svelte").Snippet | undefined} */
56
+ let children;
57
+ for (const [key, value] of Object.entries(slots)) {
58
+ $$slots ??= {};
59
+ if (key === "default") {
60
+ $$slots.default = true;
61
+ children = createRawSnippet(() => ({
62
+ render: () => value,
63
+ }));
53
64
  } else {
54
- new Component({ target: node, props });
65
+ $$slots[key] = createRawSnippet(() => ({
66
+ render: () => value,
67
+ }));
55
68
  }
69
+ const slotName = key === "default" ? "children" : key;
70
+ renderProps[slotName] = createRawSnippet(() => ({
71
+ render: () => value,
72
+ }));
73
+ }
56
74
 
57
- flushSync();
58
- });
59
-
60
- return {
61
- html: `<div data-editable-region-csr-id=${id}></div>`,
75
+ const newProps = {
76
+ ...props,
77
+ children,
78
+ $$slots,
79
+ ...renderProps,
62
80
  };
81
+
82
+ if (metadata?.hydrate) {
83
+ const id = queueForClientSideRender((node) => {
84
+ if (mount) {
85
+ mount(Component, {
86
+ target: /** @type{any}*/ (node),
87
+ props: newProps,
88
+ });
89
+ } else {
90
+ new Component({
91
+ target: node,
92
+ props: newProps,
93
+ });
94
+ }
95
+
96
+ flushSync();
97
+ });
98
+
99
+ return {
100
+ html: `<div data-editable-region-csr-id=${id}></div>`,
101
+ };
102
+ }
103
+
104
+ const doc = document.implementation.createHTMLDocument();
105
+ if (mount) {
106
+ mount(Component, {
107
+ target: /** @type{any}*/ (doc.body),
108
+ props: newProps,
109
+ });
110
+ } else {
111
+ new Component({
112
+ target: doc.body,
113
+ props: newProps,
114
+ });
115
+ }
116
+
117
+ flushSync();
118
+
119
+ return { html: doc.body.innerHTML };
63
120
  },
64
121
  },
65
122
  });
@@ -0,0 +1,61 @@
1
+ import { createApp, h } from "vue";
2
+ import { renderToString } from "vue/server-renderer";
3
+
4
+ import { addFrameworkRenderer, queueForClientSideRender } from "./index.mjs";
5
+
6
+ addFrameworkRenderer({
7
+ name: "@astrojs/vue",
8
+ clientEntrypoint: "@astrojs/vue/client.js",
9
+ ssr: {
10
+ /**
11
+ * Checks if the component is a Vue component (object with Vue-specific markers).
12
+ * @param {any} Component - The component to check
13
+ * @returns {boolean} True if component looks like a Vue component
14
+ */
15
+ check: (Component) => {
16
+ return (
17
+ typeof Component === "object" &&
18
+ Component !== null &&
19
+ ("render" in Component ||
20
+ "setup" in Component ||
21
+ "template" in Component ||
22
+ "ssrRender" in Component ||
23
+ "__ssrInlineRender" in Component ||
24
+ "__file" in Component)
25
+ );
26
+ },
27
+
28
+ /**
29
+ * Renders to static markup, falling back to a client-side render queue.
30
+ * @param {any} Component
31
+ * @param {any} inputProps
32
+ * @param {Record<string, string>} slotted
33
+ * @param {any} metadata
34
+ * @returns {Promise<{ html: string }>}
35
+ */
36
+ renderToStaticMarkup: async (Component, inputProps, slotted, metadata) => {
37
+ /** @type{Record<string, Function>} */
38
+ const slots = {};
39
+ const props = { ...inputProps };
40
+ delete props.slot;
41
+ for (const [key, value] of Object.entries(slotted)) {
42
+ slots[key] = () => h("astro-static-slot", { innerHTML: value });
43
+ }
44
+
45
+ if (metadata?.hydrate) {
46
+ const id = queueForClientSideRender((node) => {
47
+ const app = createApp({ render: () => h(Component, props, slots) });
48
+ app.mount(node);
49
+ });
50
+
51
+ return {
52
+ html: `<div data-editable-region-csr-id=${id}></div>`,
53
+ };
54
+ }
55
+
56
+ const app = createApp({ render: () => h(Component, props, slots) });
57
+ const html = await renderToString(app);
58
+ return { html };
59
+ },
60
+ },
61
+ });
@@ -18,10 +18,12 @@ import {
18
18
  registerShortcode,
19
19
  } from "../../liquid/index.mjs";
20
20
  import { warnOnce } from "../../liquid/logger.mjs";
21
+ import { createInertValue } from "./inert.mjs";
21
22
  import {
22
23
  builtinFilterNames,
23
24
  builtinShortcodeNames,
24
25
  } from "./liquid-builtins.mjs";
26
+ import { setStubsStrict } from "./stub-mode.mjs";
25
27
 
26
28
  /** @type {Record<HelperKind, (name: string, fn: any) => void>} */
27
29
  const KIND_REGISTRARS = {
@@ -58,7 +60,8 @@ const METHOD_TARGETS = {
58
60
  * if it's neither (so the caller can skip it).
59
61
  *
60
62
  * @param {any} plugin
61
- * @returns {((config: any, opts: any) => void) | null}
63
+ * @returns {((config: any, opts: any) => any) | null} Async plugins return a
64
+ * promise the caller must await.
62
65
  */
63
66
  function resolvePluginFunction(plugin) {
64
67
  if (typeof plugin === "function") return plugin;
@@ -78,15 +81,34 @@ function createEmptyLayer() {
78
81
  }
79
82
 
80
83
  /**
81
- * Replays `configFn` against a recording stand-in and registers every
82
- * collected helper that isn't skipped.
84
+ * Mirrors the config's helpers into the live-editing engine.
85
+ *
86
+ * The mirror is async because configs commonly are — `await
87
+ * import("@11ty/eleventy")` is how a CommonJS config reaches the ESM-only
88
+ * exports, and such a config registers nothing until that settles. The bundle
89
+ * awaits the returned promise before registering anything else.
83
90
  *
84
91
  * @param {unknown} config - The config's default export (a function), or a
85
92
  * module namespace whose `.default` is that function (ESM/CJS interop).
86
93
  * @param {{ skip?: Partial<Record<HelperKind, string[]>> }} [options] - Per-kind
87
94
  * override names to skip; builtin browser-port names are skipped automatically.
95
+ * @returns {Promise<void>} Resolves once every mirrored helper is registered.
88
96
  */
89
97
  export function collectAndRegisterEleventyHelpers(config, options = {}) {
98
+ // `finally` so a mirror that failed still leaves render-time stub calls
99
+ // throwing; see `stub-mode.mjs`.
100
+ return mirrorConfig(config, options).finally(setStubsStrict);
101
+ }
102
+
103
+ /**
104
+ * Replays `configFn` against a recording stand-in and registers every
105
+ * collected helper that isn't skipped.
106
+ *
107
+ * @param {unknown} config
108
+ * @param {{ skip?: Partial<Record<HelperKind, string[]>> }} options
109
+ * @returns {Promise<void>}
110
+ */
111
+ async function mirrorConfig(config, options) {
90
112
  const configFn =
91
113
  typeof config === "function"
92
114
  ? config
@@ -130,15 +152,22 @@ export function collectAndRegisterEleventyHelpers(config, options = {}) {
130
152
  };
131
153
  }
132
154
 
133
- // Unrecorded methods are no-ops so running the real config (which calls
134
- // `addPassthroughCopy`, `on`, sets `dir`, ...) doesn't throw.
155
+ // Unrecorded members are inert so the real config (`addPassthroughCopy`,
156
+ // `on`, `ignores.add(…)`, setting `dir`, ...) doesn't throw. Chainable, so
157
+ // reaching through a property before calling still works.
158
+ const unrecorded = createInertValue();
135
159
  const configRecorder = new Proxy(recorder, {
136
160
  get(target, prop, receiver) {
137
161
  if (prop in target) return Reflect.get(target, prop, receiver);
138
- return () => {};
162
+ // Thenable-looking recorder would hang `pendingPlugins`; see `inert.mjs`.
163
+ if (prop === "then" || typeof prop === "symbol") return undefined;
164
+ return unrecorded;
139
165
  },
140
166
  });
141
167
 
168
+ /** Async plugins, drained before the registration pass. @type {Promise<void>[]} */
169
+ const pendingPlugins = [];
170
+
142
171
  recorder.addPlugin = (/** @type {any} */ plugin, /** @type {any} */ opts) => {
143
172
  const pluginFn = resolvePluginFunction(plugin);
144
173
  if (!pluginFn) return;
@@ -146,16 +175,28 @@ export function collectAndRegisterEleventyHelpers(config, options = {}) {
146
175
  // A plugin is itself a config function, so replay it against the same
147
176
  // recorder to capture the helpers it registers.
148
177
  try {
149
- pluginFn(configRecorder, opts);
178
+ const result = pluginFn(configRecorder, opts);
179
+ if (typeof result?.then === "function") {
180
+ // `Promise.resolve` normalizes: a native promise is returned as-is,
181
+ // a bare thenable gains `.catch`. Async stubs reject; swallow both.
182
+ pendingPlugins.push(Promise.resolve(result).catch(() => {}));
183
+ }
150
184
  } catch {
151
185
  // Node-only plugins are stubbed at bundle time and throw when called.
152
186
  // Helpers registered before the throw are kept; the config continues.
153
187
  }
154
188
  };
155
189
 
190
+ let replayFailed = false;
191
+
156
192
  try {
157
- configFn(configRecorder);
193
+ await configFn(configRecorder);
194
+ // A plugin can register further plugins, so drain until nothing new lands.
195
+ while (pendingPlugins.length > 0) {
196
+ await Promise.all(pendingPlugins.splice(0));
197
+ }
158
198
  } catch (err) {
199
+ replayFailed = true;
159
200
  warnOnce(
160
201
  "eleventy-config-replay",
161
202
  "Replaying the Eleventy config to mirror its helpers threw: " +
@@ -165,12 +206,15 @@ export function collectAndRegisterEleventyHelpers(config, options = {}) {
165
206
  );
166
207
  }
167
208
 
209
+ let mirroredCount = 0;
210
+
168
211
  for (const kind of /** @type {HelperKind[]} */ (
169
212
  Object.keys(KIND_REGISTRARS)
170
213
  )) {
171
214
  const register = KIND_REGISTRARS[kind];
172
215
  // Liquid layer spread last so it wins on a name collision.
173
216
  const merged = new Map([...layers.universal[kind], ...layers.liquid[kind]]);
217
+ mirroredCount += merged.size;
174
218
 
175
219
  for (const [name, helperFn] of merged) {
176
220
  if (skip[kind].has(name)) continue;
@@ -185,4 +229,21 @@ export function collectAndRegisterEleventyHelpers(config, options = {}) {
185
229
  }
186
230
  }
187
231
  }
232
+
233
+ // An async config mirroring nothing otherwise surfaces as a `strictFilters`
234
+ // "unknown filter" error inside an unrelated template. A replay that threw
235
+ // has already warned, and explains the empty result.
236
+ if (
237
+ mirroredCount === 0 &&
238
+ !replayFailed &&
239
+ configFn.constructor?.name === "AsyncFunction"
240
+ ) {
241
+ warnOnce(
242
+ "eleventy-async-config",
243
+ "Your Eleventy config is async and registered no helpers when replayed " +
244
+ "for live editing. If it defines filters/shortcodes they won't be " +
245
+ "available — define a browser override via " +
246
+ "`pluginOptions.liquid.<kind>` for any that are needed.",
247
+ );
248
+ }
188
249
  }
@@ -0,0 +1,35 @@
1
+ /**
2
+ * A stand-in that survives whatever a config does to it — property access,
3
+ * calls and `new` all return it again. Used for unrecorded config methods
4
+ * (`collect-config.mjs`) and stubbed Node modules (`stub-mode.mjs`).
5
+ *
6
+ * @returns {any}
7
+ */
8
+ export function createInertValue() {
9
+ // `function`, not an arrow: arrows have no [[Construct]], so `new inert()`
10
+ // would throw before the trap runs.
11
+ const handler = {
12
+ /** @param {any} _target @param {string | symbol} prop */
13
+ get(_target, prop) {
14
+ // Must not look thenable: `collect-config.mjs` treats a plugin result
15
+ // with a callable `.then` as a promise, and this one would never
16
+ // settle — hanging the mirror, so no component is ever published.
17
+ if (prop === "then") return undefined;
18
+
19
+ if (typeof prop === "symbol") {
20
+ // Else `String(inert)` walks toPrimitive → valueOf → toString,
21
+ // gets a proxy from each, and throws.
22
+ if (prop === Symbol.toPrimitive) return () => "";
23
+ // Else `for…of` / spread over a stubbed call throws.
24
+ if (prop === Symbol.iterator) return function* () {};
25
+ return undefined;
26
+ }
27
+ return inert;
28
+ },
29
+ apply: () => inert,
30
+ construct: () => inert,
31
+ };
32
+
33
+ const inert = new Proxy(function () {}, handler);
34
+ return inert;
35
+ }
@@ -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
+ }
@@ -1 +1,28 @@
1
- module.exports = require("./index.mjs").default;
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;