@cloudcannon/editable-regions 0.0.15 → 0.0.17

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.
@@ -10,10 +10,25 @@ addFrameworkRenderer({
10
10
  clientEntrypoint: "@astrojs/react/client.js",
11
11
  ssr: {
12
12
  /**
13
- * Always returns true to handle all remaining components as React.
14
- * @returns {boolean} Always true
13
+ * @param {any} Component
14
+ * @returns {boolean}
15
15
  */
16
- check: () => true,
16
+ check: (Component) => {
17
+ if (typeof Component !== "function") return false;
18
+
19
+ // React class components have render on the prototype
20
+ if (typeof Component.prototype?.render === "function") return true;
21
+
22
+ // React functional components return vnodes with $$typeof
23
+ try {
24
+ const vnode = Component({});
25
+ return (
26
+ vnode != null && typeof vnode === "object" && "$$typeof" in vnode
27
+ );
28
+ } catch {
29
+ return false;
30
+ }
31
+ },
17
32
  /**
18
33
  * Renders a React component to static markup or queues for client-side rendering.
19
34
  * @param {any} Component - The React component function
@@ -0,0 +1,65 @@
1
+ import { addFrameworkRenderer, queueForClientSideRender } from "./index.mjs";
2
+
3
+ /** @type{((component: any, args: { target: HTMLElement, props: unknown }) => void) | undefined} */
4
+ let mount;
5
+ /** @type{() => void} */
6
+ let flushSync;
7
+ try {
8
+ ({ mount, flushSync } = await import("svelte"));
9
+ } catch {
10
+ // Svelte 4 — no mount export
11
+ }
12
+
13
+ addFrameworkRenderer({
14
+ name: "@astrojs/svelte",
15
+ clientEntrypoint: "@astrojs/svelte/client.js",
16
+ ssr: {
17
+ /**
18
+ * @param {any} Component
19
+ * @returns {boolean}
20
+ */
21
+ check: (Component) => {
22
+ if (typeof Component !== "function") return false;
23
+
24
+ // Svelte 5: compiled components reference $$payload or $$renderer
25
+ const str = Component.toString();
26
+ if (
27
+ str.includes("$$payload") ||
28
+ str.includes("$$renderer") ||
29
+ str.includes("$$anchor")
30
+ ) {
31
+ return true;
32
+ }
33
+
34
+ // Svelte 4: class-based components have a render static method
35
+ if (typeof Component.render === "function") {
36
+ return true;
37
+ }
38
+
39
+ return false;
40
+ },
41
+ /**
42
+ * @param {any} Component
43
+ * @param {unknown} props
44
+ * @returns {Promise<{html: string}>}
45
+ */
46
+ renderToStaticMarkup: async (Component, props) => {
47
+ const id = queueForClientSideRender((node) => {
48
+ if (mount) {
49
+ mount(Component, {
50
+ target: /** @type{any}*/ (node),
51
+ props,
52
+ });
53
+ } else {
54
+ new Component({ target: node, props });
55
+ }
56
+
57
+ flushSync();
58
+ });
59
+
60
+ return {
61
+ html: `<div data-editable-region-csr-id=${id}></div>`,
62
+ };
63
+ },
64
+ },
65
+ });
@@ -0,0 +1,40 @@
1
+ import { addEditableComponentRenderer } from "../helpers/cloudcannon.mjs";
2
+
3
+ /** @type{((component: any, args: { target: HTMLElement, props: unknown }) => void) | undefined} */
4
+ let mount;
5
+ /** @type{() => void} */
6
+ let flushSync;
7
+ try {
8
+ ({ mount, flushSync } = await import("svelte"));
9
+ } catch {
10
+ // Svelte 4 — no mount export
11
+ }
12
+
13
+ /**
14
+ * @param {string} key
15
+ * @param {any} component
16
+ */
17
+ export const registerSvelteComponent = (key, component) => {
18
+ /**
19
+ * @param {unknown} props
20
+ * @returns
21
+ */
22
+ const wrappedComponent = (props) => {
23
+ const rootEl = document.createElement("div");
24
+
25
+ if (mount) {
26
+ mount(component, {
27
+ target: rootEl,
28
+ props,
29
+ });
30
+ } else {
31
+ new component({ target: rootEl, props });
32
+ }
33
+
34
+ flushSync();
35
+
36
+ return rootEl;
37
+ };
38
+
39
+ addEditableComponentRenderer(key, wrappedComponent);
40
+ };
@@ -215,6 +215,12 @@ export default class EditableComponent extends Editable {
215
215
  continue;
216
216
  }
217
217
 
218
+ // Both children are the same subclass (node/element) but are of different types (e.g. text -> comment)
219
+ if (renderChild.nodeName !== targetChild.nodeName) {
220
+ targetChild.replaceWith(renderChild);
221
+ continue;
222
+ }
223
+
218
224
  // Both existing and rendered children are nodes (i.e. some text)
219
225
  if (
220
226
  !(renderChild instanceof Element) &&
@@ -225,12 +231,6 @@ export default class EditableComponent extends Editable {
225
231
  continue;
226
232
  }
227
233
 
228
- // Both children are elements but are different types
229
- if (renderChild.nodeName !== targetChild.nodeName) {
230
- targetChild.replaceWith(renderChild);
231
- continue;
232
- }
233
-
234
234
  // Both existing and rendered children are the same kind of element, and neither is editable
235
235
  if (!isEditableElement(renderChild) && !isEditableElement(targetChild)) {
236
236
  // Update the existing element to match the rendered element and recurse their subtrees
@@ -247,6 +247,12 @@ export default class EditableSource extends EditableText {
247
247
 
248
248
  onChange(value?: string | null) {
249
249
  this.file?.get().then((source) => {
250
+ if (!source) {
251
+ throw new Error(
252
+ "Source editable region tried to update a non-existent file",
253
+ );
254
+ }
255
+
250
256
  value = beautifyHtml(value ?? "", {
251
257
  indent_char: this.format.indentChar,
252
258
  indent_size: this.format.indentSize,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cloudcannon/editable-regions",
3
- "version": "0.0.15",
3
+ "version": "0.0.17",
4
4
  "type": "module",
5
5
  "description": "Visual Editing for the CloudCannon CMS.",
6
6
  "keywords": [
@@ -49,25 +49,33 @@
49
49
  "types": "./types/react.d.ts",
50
50
  "default": "./integrations/react.mjs"
51
51
  },
52
+ "./svelte": {
53
+ "types": "./types/svelte.d.ts",
54
+ "default": "./integrations/svelte.mjs"
55
+ },
56
+ "./astro-svelte-renderer": {
57
+ "types": "./types/astro.d.ts",
58
+ "default": "./integrations/astro/svelte-renderer.mjs"
59
+ },
52
60
  "./liquid": "./integrations/liquid/index.mjs",
53
61
  "./eleventy": "./integrations/eleventy.mjs",
54
62
  "./internal/components": "./components/index.js",
55
63
  "./internal/styles": "./styles/index.js"
56
64
  },
57
65
  "devDependencies": {
58
- "@biomejs/biome": "2.4.14",
59
- "@cloudcannon/visual-editor-api": "0.0.15",
66
+ "@biomejs/biome": "2.5.1",
67
+ "@cloudcannon/visual-editor-api": "0.0.19",
60
68
  "@sindresorhus/slugify": "3.0.0",
61
69
  "@types/js-beautify": "1.14.3",
62
- "@types/node": "25.6.0",
63
- "@types/react": "19.2.14",
70
+ "@types/node": "25.9.4",
71
+ "@types/react": "19.2.17",
64
72
  "@types/react-dom": "19.2.3",
65
- "astro": "6.2.1",
73
+ "astro": "6.4.8",
66
74
  "js-beautify": "1.15.4",
67
- "liquidjs": "10.25.7",
75
+ "liquidjs": "10.27.1",
68
76
  "typescript": "6.0.3"
69
77
  },
70
78
  "dependencies": {
71
- "esbuild": "0.28.0"
79
+ "esbuild": "0.28.1"
72
80
  }
73
81
  }
package/types/astro.d.ts CHANGED
@@ -17,3 +17,7 @@ declare module "@cloudcannon/editable-regions/astro" {
17
17
  declare module "@cloudcannon/editable-regions/astro-react-renderer" {
18
18
  // Side-effect only module that registers React renderer
19
19
  }
20
+
21
+ declare module "@cloudcannon/editable-regions/astro-svelte-renderer" {
22
+ // Side-effect only module that registers Svelte renderer
23
+ }
@@ -10,3 +10,11 @@ declare module "astro/runtime/server/index.js" {
10
10
  declare module "react-dom/server.browser" {
11
11
  export function renderToStaticMarkup(element: any): string;
12
12
  }
13
+ declare module "svelte" {
14
+ export function mount(
15
+ component: any,
16
+ args: { target: HTMLElement; props: unknown },
17
+ ): void;
18
+
19
+ export function flushSync(): void;
20
+ }
@@ -0,0 +1,3 @@
1
+ /// <reference path="./cloudcannon.d.ts" />
2
+
3
+ export function registerSvelteComponent(key: string, component: unknown): void;