@cloudcannon/editable-regions 0.0.2
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/LICENSE +5 -0
- package/README.md +5 -0
- package/components/editable-array-component.ts +26 -0
- package/components/editable-array-item-component.ts +26 -0
- package/components/editable-component-component.ts +26 -0
- package/components/editable-image-component.ts +26 -0
- package/components/editable-snippet-component.ts +30 -0
- package/components/editable-source-component.ts +26 -0
- package/components/editable-text-component.ts +26 -0
- package/components/index.ts +35 -0
- package/components/ui/editable-array-item-controls.ts +123 -0
- package/components/ui/editable-component-controls.ts +57 -0
- package/components/ui/editable-region-error-card.ts +77 -0
- package/helpers/checks.ts +126 -0
- package/helpers/cloudcannon.ts +67 -0
- package/helpers/hydrate-editable-regions.ts +71 -0
- package/integrations/astro/astro-integration.mjs +110 -0
- package/integrations/astro/index.mjs +193 -0
- package/integrations/astro/modules/actions.js +74 -0
- package/integrations/astro/modules/assets.js +38 -0
- package/integrations/astro/modules/client-router.astro +5 -0
- package/integrations/astro/modules/content.js +116 -0
- package/integrations/astro/modules/i18n.js +76 -0
- package/integrations/astro/modules/image.astro +33 -0
- package/integrations/astro/modules/middleware.js +27 -0
- package/integrations/astro/modules/picture.astro +7 -0
- package/integrations/astro/modules/transitions.js +63 -0
- package/integrations/astro/react-renderer.mjs +40 -0
- package/integrations/react.mjs +32 -0
- package/nodes/editable-array-item.ts +427 -0
- package/nodes/editable-array.ts +241 -0
- package/nodes/editable-component.ts +273 -0
- package/nodes/editable-image.ts +253 -0
- package/nodes/editable-snippet.ts +148 -0
- package/nodes/editable-source.ts +245 -0
- package/nodes/editable-text.ts +163 -0
- package/nodes/editable.ts +471 -0
- package/nodes/index.ts +8 -0
- package/package.json +64 -0
- package/styles/editable-array-item.css +8 -0
- package/styles/editable-component.css +8 -0
- package/styles/editable-image.css +4 -0
- package/styles/editable-snippet.css +12 -0
- package/styles/editable-source.css +3 -0
- package/styles/editable-text.css +3 -0
- package/styles/index.css +101 -0
- package/styles/index.ts +6 -0
- package/styles/ui/editable-component-controls.css +104 -0
- package/styles/ui/editable-region-error-card.css +25 -0
- package/types/astro.d.ts +19 -0
- package/types/cloudcannon.d.ts +11 -0
- package/types/modules.d.ts +12 -0
- package/types/react.d.ts +5 -0
- package/types/vite.d.ts +9 -0
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type Editable,
|
|
3
|
+
EditableArray,
|
|
4
|
+
EditableArrayItem,
|
|
5
|
+
EditableComponent,
|
|
6
|
+
EditableImage,
|
|
7
|
+
EditableSource,
|
|
8
|
+
EditableText,
|
|
9
|
+
} from "../nodes";
|
|
10
|
+
import { hasEditable } from "./checks";
|
|
11
|
+
|
|
12
|
+
const editableMap: Record<string, typeof Editable | undefined> = {
|
|
13
|
+
array: EditableArray,
|
|
14
|
+
"array-item": EditableArrayItem,
|
|
15
|
+
component: EditableComponent,
|
|
16
|
+
image: EditableImage,
|
|
17
|
+
source: EditableSource,
|
|
18
|
+
text: EditableText,
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export const dehydrateDataEditableRegions = (root: Element) => {
|
|
22
|
+
if (root instanceof HTMLElement && hasEditable(root)) {
|
|
23
|
+
root.editable.disconnect();
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
root.querySelectorAll("[data-editable]").forEach((element) => {
|
|
27
|
+
if (element instanceof HTMLElement && hasEditable(element)) {
|
|
28
|
+
element.editable.disconnect();
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export const hydrateDataEditableRegions = (root: Element) => {
|
|
34
|
+
if (
|
|
35
|
+
root instanceof HTMLElement &&
|
|
36
|
+
root.dataset.editable &&
|
|
37
|
+
!("editable" in root)
|
|
38
|
+
) {
|
|
39
|
+
const Editable = editableMap[root.dataset.editable];
|
|
40
|
+
if (Editable) {
|
|
41
|
+
const editable = new Editable(root);
|
|
42
|
+
editable.connect();
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
root.querySelectorAll("[data-editable]").forEach((element) => {
|
|
47
|
+
if (!(element instanceof HTMLElement) || "editable" in element) {
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (!element.dataset.editable || element.dataset.cloudcannonIgnore) {
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const Editable = editableMap[element.dataset.editable];
|
|
56
|
+
if (!Editable) {
|
|
57
|
+
const error = document.createElement("editable-region-error-card");
|
|
58
|
+
error.setAttribute("heading", "Failed to render editable region");
|
|
59
|
+
error.setAttribute(
|
|
60
|
+
"message",
|
|
61
|
+
`Unrecognized editable type: "${element.dataset.editable}". The supported types are: ${Object.keys(editableMap).join(", ")}`,
|
|
62
|
+
);
|
|
63
|
+
element.replaceWith(error);
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const editable = new Editable(element);
|
|
68
|
+
|
|
69
|
+
editable.connect();
|
|
70
|
+
});
|
|
71
|
+
};
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { dirname, join } from "node:path";
|
|
2
|
+
|
|
3
|
+
const SUPPORTED_VIRTUAL_MODULES = [
|
|
4
|
+
"actions",
|
|
5
|
+
"assets",
|
|
6
|
+
"content",
|
|
7
|
+
"i18n",
|
|
8
|
+
"middleware",
|
|
9
|
+
"transitions",
|
|
10
|
+
];
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* @return {import("astro").AstroIntegration}
|
|
14
|
+
*/
|
|
15
|
+
export default () => {
|
|
16
|
+
/** @type {import("astro").AstroConfig} */
|
|
17
|
+
let astroConfig;
|
|
18
|
+
|
|
19
|
+
return {
|
|
20
|
+
name: "editable-regions",
|
|
21
|
+
hooks: {
|
|
22
|
+
"astro:config:setup": ({ config, updateConfig }) => {
|
|
23
|
+
updateConfig({
|
|
24
|
+
vite: {
|
|
25
|
+
define: {
|
|
26
|
+
ENV_CLIENT: false,
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
});
|
|
30
|
+
astroConfig = config;
|
|
31
|
+
},
|
|
32
|
+
"astro:build:setup": async ({ target, vite }) => {
|
|
33
|
+
if (target === "client") {
|
|
34
|
+
vite.define ??= {};
|
|
35
|
+
vite.define.ENV_CLIENT = true;
|
|
36
|
+
|
|
37
|
+
vite.plugins?.unshift({
|
|
38
|
+
name: "vite-plugin-editable-regions",
|
|
39
|
+
enforce: "pre",
|
|
40
|
+
|
|
41
|
+
resolveId(id) {
|
|
42
|
+
if (id.startsWith("astro:")) {
|
|
43
|
+
const type = id
|
|
44
|
+
.replace("astro:", "")
|
|
45
|
+
.replace("/client", "")
|
|
46
|
+
.replace("/server", "");
|
|
47
|
+
|
|
48
|
+
if (type === "env") {
|
|
49
|
+
return "\0editable-region:env";
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (!SUPPORTED_VIRTUAL_MODULES.includes(type)) {
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
let dir = "";
|
|
57
|
+
if (typeof __dirname !== "undefined") {
|
|
58
|
+
dir = __dirname;
|
|
59
|
+
} else {
|
|
60
|
+
dir = dirname(import.meta.url);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const path = join(dir, "modules", `${type}.js`).replace(
|
|
64
|
+
"file:",
|
|
65
|
+
"",
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
return path;
|
|
69
|
+
}
|
|
70
|
+
},
|
|
71
|
+
|
|
72
|
+
load(id) {
|
|
73
|
+
if (id === "\0editable-region:env") {
|
|
74
|
+
let contents = "";
|
|
75
|
+
Object.entries(astroConfig?.env?.schema ?? {}).forEach(
|
|
76
|
+
([key, schema]) => {
|
|
77
|
+
if (
|
|
78
|
+
schema.context !== "client" ||
|
|
79
|
+
schema.access !== "public"
|
|
80
|
+
) {
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
try {
|
|
85
|
+
switch (schema.type) {
|
|
86
|
+
case "boolean":
|
|
87
|
+
contents += `export const ${key} = ${!!process.env[key]};\n`;
|
|
88
|
+
break;
|
|
89
|
+
case "number":
|
|
90
|
+
contents += `export const ${key} = ${Number(process.env[key])};\n`;
|
|
91
|
+
break;
|
|
92
|
+
default:
|
|
93
|
+
contents += `export const ${key} = ${JSON.stringify(process.env[key] ?? "")};\n`;
|
|
94
|
+
}
|
|
95
|
+
} catch (e) {
|
|
96
|
+
//Error intentionally ignored
|
|
97
|
+
}
|
|
98
|
+
},
|
|
99
|
+
);
|
|
100
|
+
contents +=
|
|
101
|
+
'export const getSecret = () => console.log("[CloudCannon] getSecret is not supported in an editable component. Please use an editing fallback instead.");';
|
|
102
|
+
return contents;
|
|
103
|
+
}
|
|
104
|
+
},
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
},
|
|
108
|
+
},
|
|
109
|
+
};
|
|
110
|
+
};
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import {
|
|
2
|
+
renderSlotToString,
|
|
3
|
+
renderToString,
|
|
4
|
+
} from "astro/runtime/server/index.js";
|
|
5
|
+
import { addEditableComponentRenderer } from "../../helpers/cloudcannon";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Queue of React components waiting to be rendered
|
|
9
|
+
* @type {((node: Element) => void)[]}
|
|
10
|
+
*/
|
|
11
|
+
const renderRoots = [];
|
|
12
|
+
|
|
13
|
+
const renderers = [
|
|
14
|
+
{
|
|
15
|
+
name: "dynamic-tags",
|
|
16
|
+
ssr: {
|
|
17
|
+
/**
|
|
18
|
+
* Checks if the component is a string (HTML tag name).
|
|
19
|
+
* @param {any} Component - The component to check
|
|
20
|
+
* @returns {boolean} True if component is a string tag name
|
|
21
|
+
*/
|
|
22
|
+
check: (Component) => {
|
|
23
|
+
return typeof Component === "string";
|
|
24
|
+
},
|
|
25
|
+
/**
|
|
26
|
+
* Renders a dynamic HTML tag with props and slots.
|
|
27
|
+
* @param {string} Component - The HTML tag name
|
|
28
|
+
* @param {Record<string, any>} props - Props to render as attributes
|
|
29
|
+
* @param {Record<string, string>} slots - Slot content
|
|
30
|
+
* @returns {Promise<string>} The rendered HTML string
|
|
31
|
+
*/
|
|
32
|
+
renderToStaticMarkup: async (Component, props, slots) => {
|
|
33
|
+
const propsString = Object.entries(props)
|
|
34
|
+
.map(([key, value]) => `${key}="${value}"`)
|
|
35
|
+
.join(" ");
|
|
36
|
+
return `<${Component} ${propsString}>${
|
|
37
|
+
slots.default ?? ""
|
|
38
|
+
}</${Component}>`;
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
];
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* @param {*} renderer
|
|
46
|
+
*/
|
|
47
|
+
export const addFrameworkRenderer = (renderer) => {
|
|
48
|
+
renderers.push(renderer);
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* @param {(node: Element) => void} renderFunction
|
|
53
|
+
*/
|
|
54
|
+
export const queueForClientSideRender = (renderFunction) => {
|
|
55
|
+
renderRoots.push(renderFunction);
|
|
56
|
+
return renderRoots.length - 1;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Registers an Astro component with the CloudCannon component system.
|
|
61
|
+
* Creates a wrapper that handles Astro SSR rendering with React hydration support.
|
|
62
|
+
*
|
|
63
|
+
* @param {string} key - Unique identifier for the component
|
|
64
|
+
* @param {unknown} component - The Astro component function to register
|
|
65
|
+
* @returns {void}
|
|
66
|
+
*/
|
|
67
|
+
export const registerAstroComponent = (key, component) => {
|
|
68
|
+
/**
|
|
69
|
+
* Wrapper function that renders the Astro component with SSR and client-side hydration.
|
|
70
|
+
*
|
|
71
|
+
* @param {any} props - Props to pass to the Astro component
|
|
72
|
+
* @returns {Promise<HTMLElement>} The rendered component as an HTMLElement
|
|
73
|
+
*/
|
|
74
|
+
const wrappedComponent = async (props) => {
|
|
75
|
+
/**
|
|
76
|
+
* Encryption key for Astro server islands
|
|
77
|
+
* @type {CryptoKey | undefined}
|
|
78
|
+
*/
|
|
79
|
+
let encryptionKey;
|
|
80
|
+
try {
|
|
81
|
+
encryptionKey = await window.crypto.subtle.generateKey(
|
|
82
|
+
{
|
|
83
|
+
name: "AES-GCM",
|
|
84
|
+
length: 256,
|
|
85
|
+
},
|
|
86
|
+
true,
|
|
87
|
+
["encrypt", "decrypt"],
|
|
88
|
+
);
|
|
89
|
+
} catch (err) {
|
|
90
|
+
console.warn(
|
|
91
|
+
"[CloudCannon] Could not generate a key for Astro component. This may cause issues with Astro components that use server-islands",
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const SSRResult = {
|
|
96
|
+
styles: new Set(),
|
|
97
|
+
scripts: new Set(),
|
|
98
|
+
links: new Set(),
|
|
99
|
+
propagation: new Map(),
|
|
100
|
+
propagators: new Map(),
|
|
101
|
+
inlinedScripts: new Map(),
|
|
102
|
+
serverIslandNameMap: { get: () => "EditableRegions" },
|
|
103
|
+
key: encryptionKey,
|
|
104
|
+
base: "/",
|
|
105
|
+
extraHead: [],
|
|
106
|
+
compressHTML: false,
|
|
107
|
+
partial: false,
|
|
108
|
+
shouldInjectCspMetaTags: false,
|
|
109
|
+
componentMetadata: new Map(),
|
|
110
|
+
renderers,
|
|
111
|
+
_metadata: {
|
|
112
|
+
renderers,
|
|
113
|
+
hasHydrationScript: false,
|
|
114
|
+
hasRenderedHead: true,
|
|
115
|
+
hasRenderedServerIslandRuntime: true,
|
|
116
|
+
hasDirectives: new Set(),
|
|
117
|
+
propagators: new Set(),
|
|
118
|
+
rendererSpecificHydrationScripts: new Set(),
|
|
119
|
+
renderedScripts: new Set(),
|
|
120
|
+
extraHead: [],
|
|
121
|
+
extraStyleHashes: [],
|
|
122
|
+
extraScriptHashes: [],
|
|
123
|
+
},
|
|
124
|
+
clientDirectives: new Map([
|
|
125
|
+
["load", "editable-region-placeholder"],
|
|
126
|
+
["idle", "editable-region-placeholder"],
|
|
127
|
+
["visible", "editable-region-placeholder"],
|
|
128
|
+
["media", "editable-region-placeholder"],
|
|
129
|
+
]),
|
|
130
|
+
slots: null,
|
|
131
|
+
props,
|
|
132
|
+
resolve: () => "editable-region-placeholder",
|
|
133
|
+
/**
|
|
134
|
+
* @param {*} astroGlobal
|
|
135
|
+
* @param {*} props
|
|
136
|
+
* @param {*} slots
|
|
137
|
+
*/
|
|
138
|
+
createAstro(astroGlobal, props, slots) {
|
|
139
|
+
const astroSlots = {
|
|
140
|
+
/**
|
|
141
|
+
* @param {string} name
|
|
142
|
+
* @returns boolean
|
|
143
|
+
*/
|
|
144
|
+
has: (name) => {
|
|
145
|
+
if (!slots) return false;
|
|
146
|
+
return Boolean(slots[name]);
|
|
147
|
+
},
|
|
148
|
+
/**
|
|
149
|
+
* @param {string} name
|
|
150
|
+
* @returns string
|
|
151
|
+
*/
|
|
152
|
+
render: (name) => {
|
|
153
|
+
return renderSlotToString(SSRResult, slots[name]);
|
|
154
|
+
},
|
|
155
|
+
};
|
|
156
|
+
return {
|
|
157
|
+
__proto__: astroGlobal,
|
|
158
|
+
props,
|
|
159
|
+
slots: astroSlots,
|
|
160
|
+
request: new Request(window.location.href),
|
|
161
|
+
};
|
|
162
|
+
},
|
|
163
|
+
};
|
|
164
|
+
// Render the Astro component to HTML string
|
|
165
|
+
const result = await renderToString(SSRResult, component, props, null);
|
|
166
|
+
const doc = document.implementation.createHTMLDocument();
|
|
167
|
+
doc.body.innerHTML = result;
|
|
168
|
+
|
|
169
|
+
doc.querySelectorAll("[data-editable-region-csr-id]").forEach((node) => {
|
|
170
|
+
const csrId = Number(node.getAttribute("data-editable-region-csr-id"));
|
|
171
|
+
renderRoots[csrId]?.(node);
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
// Clear the React roots queue
|
|
175
|
+
renderRoots.length = 0;
|
|
176
|
+
|
|
177
|
+
doc.querySelectorAll("link, [data-island-id]").forEach((node) => {
|
|
178
|
+
node.remove();
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
doc.querySelectorAll("astro-island").forEach((node) => {
|
|
182
|
+
for (const child of node.children) {
|
|
183
|
+
node.before(child);
|
|
184
|
+
}
|
|
185
|
+
node.remove();
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
return doc.body;
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
// Register the wrapped component in the global registry
|
|
192
|
+
addEditableComponentRenderer(key, wrappedComponent);
|
|
193
|
+
};
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
export const actions = new Proxy(
|
|
2
|
+
{},
|
|
3
|
+
{
|
|
4
|
+
get() {
|
|
5
|
+
console.warn(
|
|
6
|
+
"[CloudCannon] actions is not supported in an editable component. Please use an editing fallback instead.",
|
|
7
|
+
);
|
|
8
|
+
return () => {};
|
|
9
|
+
},
|
|
10
|
+
},
|
|
11
|
+
);
|
|
12
|
+
|
|
13
|
+
export const defineAction = () => {
|
|
14
|
+
console.warn(
|
|
15
|
+
"[CloudCannon] defineAction is not supported in an editable component. Please use an editing fallback instead.",
|
|
16
|
+
);
|
|
17
|
+
return {
|
|
18
|
+
handler: () => {},
|
|
19
|
+
input: null,
|
|
20
|
+
};
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export const isInputError = () => {
|
|
24
|
+
console.warn(
|
|
25
|
+
"[CloudCannon] isInputError is not supported in an editable component. Please use an editing fallback instead.",
|
|
26
|
+
);
|
|
27
|
+
return false;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export const isActionError = () => {
|
|
31
|
+
console.warn(
|
|
32
|
+
"[CloudCannon] isActionError is not supported in an editable component. Please use an editing fallback instead.",
|
|
33
|
+
);
|
|
34
|
+
return false;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export class ActionError extends Error {
|
|
38
|
+
/**
|
|
39
|
+
* @param {any} code
|
|
40
|
+
* @param {any} message
|
|
41
|
+
*/
|
|
42
|
+
constructor(code, message) {
|
|
43
|
+
super(message);
|
|
44
|
+
console.warn(
|
|
45
|
+
"[CloudCannon] ActionError is not supported in an editable component. Please use an editing fallback instead.",
|
|
46
|
+
);
|
|
47
|
+
this.code = code;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export const getActionContext = () => {
|
|
52
|
+
console.warn(
|
|
53
|
+
"[CloudCannon] getActionContext is not supported in an editable component. Please use an editing fallback instead.",
|
|
54
|
+
);
|
|
55
|
+
return {
|
|
56
|
+
action: undefined,
|
|
57
|
+
setActionResult: () => {},
|
|
58
|
+
serializeActionResult: () => ({}),
|
|
59
|
+
};
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
export const deserializeActionResult = () => {
|
|
63
|
+
console.warn(
|
|
64
|
+
"[CloudCannon] deserializeActionResult is not supported in an editable component. Please use an editing fallback instead.",
|
|
65
|
+
);
|
|
66
|
+
return {};
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
export const getActionPath = () => {
|
|
70
|
+
console.warn(
|
|
71
|
+
"[CloudCannon] getActionPath is not supported in an editable component. Please use an editing fallback instead.",
|
|
72
|
+
);
|
|
73
|
+
return "";
|
|
74
|
+
};
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import ImageInternal from "./image.astro";
|
|
2
|
+
import PictureInternal from "./picture.astro";
|
|
3
|
+
|
|
4
|
+
export const Image = ImageInternal;
|
|
5
|
+
export const Picture = PictureInternal;
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* @param {{src: any }} options
|
|
9
|
+
* @returns
|
|
10
|
+
*/
|
|
11
|
+
export const getImage = async (options) => {
|
|
12
|
+
const resolvedSrc =
|
|
13
|
+
typeof options.src === "object" && "then" in options.src
|
|
14
|
+
? ((await options.src).default ?? (await options.src))
|
|
15
|
+
: options.src;
|
|
16
|
+
return {
|
|
17
|
+
rawOptions: {
|
|
18
|
+
src: {
|
|
19
|
+
src: resolvedSrc,
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
options: {
|
|
23
|
+
src: {
|
|
24
|
+
src: resolvedSrc,
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
src: resolvedSrc,
|
|
28
|
+
srcSet: { values: [] },
|
|
29
|
+
attributes: {},
|
|
30
|
+
};
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export const inferRemoteSize = async () => {
|
|
34
|
+
console.warn(
|
|
35
|
+
"[CloudCannon] inferRemoteSize is not supported in an editable component. Please use an editing fallback instead.",
|
|
36
|
+
);
|
|
37
|
+
return {};
|
|
38
|
+
};
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { CloudCannon } from "../../../helpers/cloudcannon";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @param {string} collectionKey
|
|
5
|
+
* @param {(value: any) => boolean} [filter]
|
|
6
|
+
* @returns {Promise<Array<any>>}
|
|
7
|
+
*/
|
|
8
|
+
export const getCollection = async (collectionKey, filter) => {
|
|
9
|
+
const collection = CloudCannon.collection(collectionKey);
|
|
10
|
+
let files = await collection.items();
|
|
11
|
+
if (files.length === 0) {
|
|
12
|
+
const allFiles = await CloudCannon.files();
|
|
13
|
+
files = allFiles.filter((file) =>
|
|
14
|
+
file.path.startsWith(`/src/content/${collectionKey}/`),
|
|
15
|
+
);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const promises = files.map(async (file) => {
|
|
19
|
+
const data = await file.data.get();
|
|
20
|
+
let id = file.path.replace(`/src/content/${collectionKey}/`, "");
|
|
21
|
+
let slug = id.replace(/\.[^.]*$/, "");
|
|
22
|
+
if (!id.match(/\.md(x|oc)?$/)) {
|
|
23
|
+
id = slug;
|
|
24
|
+
}
|
|
25
|
+
if (data && "slug" in data) {
|
|
26
|
+
slug = data.slug;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return {
|
|
30
|
+
collection: collectionKey,
|
|
31
|
+
id: id,
|
|
32
|
+
data: data,
|
|
33
|
+
slug: slug,
|
|
34
|
+
body: await file.get(),
|
|
35
|
+
};
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
const result = await Promise.all(promises);
|
|
39
|
+
|
|
40
|
+
return filter ? result.filter(filter) : result;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
*
|
|
45
|
+
* @param {string | {collection: string, slug?: string, id?: string}} objOrString
|
|
46
|
+
* @param {string} [maybeString]
|
|
47
|
+
* @returns
|
|
48
|
+
*/
|
|
49
|
+
export const getEntry = async (objOrString, maybeString) => {
|
|
50
|
+
if (typeof objOrString === "object") {
|
|
51
|
+
const {
|
|
52
|
+
collection: collectionKey,
|
|
53
|
+
slug: entrySlug,
|
|
54
|
+
id: entryId,
|
|
55
|
+
} = objOrString;
|
|
56
|
+
const collection = await getCollection(collectionKey);
|
|
57
|
+
if (entryId) {
|
|
58
|
+
return collection.find(({ id }) => id === entryId);
|
|
59
|
+
}
|
|
60
|
+
if (entrySlug) {
|
|
61
|
+
return collection.find(({ slug }) => slug === entrySlug);
|
|
62
|
+
}
|
|
63
|
+
return console.warn(
|
|
64
|
+
"[CloudCannon] Failed to load entries, invalid arguments: ",
|
|
65
|
+
[objOrString, maybeString],
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (typeof objOrString === "string" && typeof maybeString === "string") {
|
|
70
|
+
const [collectionKey, entryKey] = [objOrString, maybeString];
|
|
71
|
+
const collection = await getCollection(collectionKey);
|
|
72
|
+
|
|
73
|
+
return collection.find(({ id, slug }) => entryKey === (slug ?? id));
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return console.warn(
|
|
77
|
+
"[CloudCannon] Failed to load entries, invalid arguments: ",
|
|
78
|
+
[objOrString, maybeString],
|
|
79
|
+
);
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* @param {{collection: string, slug?: string, id?: string}[]} entries
|
|
84
|
+
* @returns
|
|
85
|
+
*/
|
|
86
|
+
export const getEntries = (entries) => {
|
|
87
|
+
return Promise.all(entries.map((entry) => getEntry(entry)));
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* @param {string} collection
|
|
92
|
+
* @param {string} slug
|
|
93
|
+
* @returns
|
|
94
|
+
*/
|
|
95
|
+
export const getEntryBySlug = (collection, slug) => {
|
|
96
|
+
return getEntry({ collection, slug });
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* @param {any} entry
|
|
101
|
+
* @returns
|
|
102
|
+
*/
|
|
103
|
+
export const render = async (entry) => ({
|
|
104
|
+
Content: () => entry?.body ?? "Content is not available when live editing",
|
|
105
|
+
headings: [],
|
|
106
|
+
remarkPluginFrontmatter: {},
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
export const defineCollection = () =>
|
|
110
|
+
console.warn(
|
|
111
|
+
"[CloudCannon] defineCollection is not supported in an editable component. Make sure you're not importing your config in a component file by mistake.",
|
|
112
|
+
);
|
|
113
|
+
export const reference = () =>
|
|
114
|
+
console.warn(
|
|
115
|
+
"[CloudCannon] reference is not supported in an editable component. Make sure you're not importing your config in a component file by mistake.",
|
|
116
|
+
);
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
export const getRelativeLocaleUrl = () => {
|
|
2
|
+
console.warn(
|
|
3
|
+
"[CloudCannon] i18n routing is not supported in an editable component. Please use an editing fallback instead.",
|
|
4
|
+
);
|
|
5
|
+
return "";
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
export const getAbsoluteLocaleUrl = () => {
|
|
9
|
+
console.warn(
|
|
10
|
+
"[CloudCannon] i18n routing is not supported in an editable component. Please use an editing fallback instead.",
|
|
11
|
+
);
|
|
12
|
+
return "";
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export const getRelativeLocaleUrlList = () => {
|
|
16
|
+
console.warn(
|
|
17
|
+
"[CloudCannon] i18n routing is not supported in an editable component. Please use an editing fallback instead.",
|
|
18
|
+
);
|
|
19
|
+
return [];
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export const getAbsoluteLocaleUrlList = () => {
|
|
23
|
+
console.warn(
|
|
24
|
+
"[CloudCannon] i18n routing is not supported in an editable component. Please use an editing fallback instead.",
|
|
25
|
+
);
|
|
26
|
+
return [];
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export const getPathByLocale = () => {
|
|
30
|
+
console.warn(
|
|
31
|
+
"[CloudCannon] i18n routing is not supported in an editable component. Please use an editing fallback instead.",
|
|
32
|
+
);
|
|
33
|
+
return "";
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export const getLocaleByPath = () => {
|
|
37
|
+
console.warn(
|
|
38
|
+
"[CloudCannon] i18n routing is not supported in an editable component. Please use an editing fallback instead.",
|
|
39
|
+
);
|
|
40
|
+
return "";
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
export const redirectToDefaultLocale = () => {
|
|
44
|
+
console.warn(
|
|
45
|
+
"[CloudCannon] i18n routing is not supported in an editable component. Please use an editing fallback instead.",
|
|
46
|
+
);
|
|
47
|
+
return Promise.resolve(new Response());
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
export const redirectToFallback = () => {
|
|
51
|
+
console.warn(
|
|
52
|
+
"[CloudCannon] i18n routing is not supported in an editable component. Please use an editing fallback instead.",
|
|
53
|
+
);
|
|
54
|
+
return Promise.resolve(new Response());
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
export const notFound = () => {
|
|
58
|
+
console.warn(
|
|
59
|
+
"[CloudCannon] i18n routing is not supported in an editable component. Please use an editing fallback instead.",
|
|
60
|
+
);
|
|
61
|
+
return Promise.resolve(new Response());
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
export const middleware = () => {
|
|
65
|
+
console.warn(
|
|
66
|
+
"[CloudCannon] i18n routing is not supported in an editable component. Please use an editing fallback instead.",
|
|
67
|
+
);
|
|
68
|
+
return () => {};
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
export const requestHasLocale = () => {
|
|
72
|
+
console.warn(
|
|
73
|
+
"[CloudCannon] i18n routing is not supported in an editable component. Please use an editing fallback instead.",
|
|
74
|
+
);
|
|
75
|
+
return false;
|
|
76
|
+
};
|