@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 +0 -22
- package/helpers/hydrate-editable-regions.ts +2 -1
- package/integrations/astro/react-renderer.mjs +94 -19
- package/integrations/astro/svelte-renderer.mjs +72 -15
- package/integrations/astro/vue-renderer.mjs +61 -0
- package/integrations/eleventy/browser/collect-config.mjs +69 -8
- package/integrations/eleventy/browser/inert.mjs +35 -0
- package/integrations/eleventy/browser/process-shim.mjs +32 -0
- package/integrations/eleventy/browser/stub-mode.mjs +61 -0
- package/integrations/eleventy/index.cjs +28 -1
- package/integrations/eleventy/index.mjs +82 -30
- package/integrations/liquid/README.md +92 -3
- package/integrations/liquid/errors.mjs +3 -1
- package/integrations/liquid/fs.mjs +11 -1
- package/integrations/liquid/globals.mjs +124 -25
- package/integrations/liquid/index.mjs +5 -2
- package/integrations/vue.mjs +28 -0
- package/nodes/editable-array-item.ts +6 -1
- package/nodes/editable-component.ts +2 -3
- package/nodes/editable-text.ts +6 -0
- package/nodes/editable.ts +6 -1
- package/package.json +120 -90
- package/types/astro.d.ts +4 -0
- package/types/eleventy.d.ts +11 -4
- package/types/liquid.d.ts +0 -1
- package/types/vue.d.ts +40 -0
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
|
|
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
|
-
|
|
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
|
-
* @
|
|
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
|
-
|
|
20
|
-
|
|
37
|
+
if (
|
|
38
|
+
typeof Component === "function" &&
|
|
39
|
+
Component.$$typeof === Symbol.for("react.forward_ref")
|
|
40
|
+
)
|
|
41
|
+
return false;
|
|
21
42
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
43
|
+
if (
|
|
44
|
+
Component.prototype != null &&
|
|
45
|
+
typeof Component.prototype.render === "function"
|
|
46
|
+
) {
|
|
25
47
|
return (
|
|
26
|
-
|
|
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
|
-
|
|
40
|
-
const
|
|
41
|
-
|
|
42
|
-
|
|
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,
|
|
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 {
|
|
44
|
-
* @
|
|
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
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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
|
-
|
|
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
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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) =>
|
|
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
|
-
*
|
|
82
|
-
*
|
|
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
|
|
134
|
-
// `
|
|
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
|
-
|
|
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
|
-
|
|
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;
|