@uniweb/runtime 0.2.13 → 0.2.15
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/dist/ssr.js +274 -0
- package/dist/ssr.js.map +1 -0
- package/package.json +12 -3
- package/src/prepare-props.js +97 -3
- package/src/ssr.js +49 -0
package/dist/ssr.js
ADDED
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { jsxs, jsx, Fragment } from "react/jsx-runtime";
|
|
3
|
+
function guaranteeItemStructure(item) {
|
|
4
|
+
return {
|
|
5
|
+
title: item.title || "",
|
|
6
|
+
pretitle: item.pretitle || "",
|
|
7
|
+
subtitle: item.subtitle || "",
|
|
8
|
+
paragraphs: item.paragraphs || [],
|
|
9
|
+
links: item.links || [],
|
|
10
|
+
imgs: item.imgs || [],
|
|
11
|
+
lists: item.lists || [],
|
|
12
|
+
icons: item.icons || [],
|
|
13
|
+
videos: item.videos || [],
|
|
14
|
+
buttons: item.buttons || [],
|
|
15
|
+
data: item.data || {},
|
|
16
|
+
cards: item.cards || [],
|
|
17
|
+
documents: item.documents || [],
|
|
18
|
+
forms: item.forms || [],
|
|
19
|
+
quotes: item.quotes || [],
|
|
20
|
+
headings: item.headings || []
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
function guaranteeContentStructure(parsedContent) {
|
|
24
|
+
const content = parsedContent || {};
|
|
25
|
+
return {
|
|
26
|
+
// Flat header fields
|
|
27
|
+
title: content.title || "",
|
|
28
|
+
pretitle: content.pretitle || "",
|
|
29
|
+
subtitle: content.subtitle || "",
|
|
30
|
+
subtitle2: content.subtitle2 || "",
|
|
31
|
+
alignment: content.alignment || null,
|
|
32
|
+
// Flat body fields
|
|
33
|
+
paragraphs: content.paragraphs || [],
|
|
34
|
+
links: content.links || [],
|
|
35
|
+
imgs: content.imgs || [],
|
|
36
|
+
lists: content.lists || [],
|
|
37
|
+
icons: content.icons || [],
|
|
38
|
+
videos: content.videos || [],
|
|
39
|
+
buttons: content.buttons || [],
|
|
40
|
+
data: content.data || {},
|
|
41
|
+
cards: content.cards || [],
|
|
42
|
+
documents: content.documents || [],
|
|
43
|
+
forms: content.forms || [],
|
|
44
|
+
quotes: content.quotes || [],
|
|
45
|
+
headings: content.headings || [],
|
|
46
|
+
// Items with guaranteed structure
|
|
47
|
+
items: (content.items || []).map(guaranteeItemStructure),
|
|
48
|
+
// Sequence for ordered rendering
|
|
49
|
+
sequence: content.sequence || [],
|
|
50
|
+
// Preserve raw content if present
|
|
51
|
+
raw: content.raw
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
function applySchemaToObject(obj, schema) {
|
|
55
|
+
if (!obj || typeof obj !== "object" || Array.isArray(obj)) {
|
|
56
|
+
return obj;
|
|
57
|
+
}
|
|
58
|
+
const result = { ...obj };
|
|
59
|
+
for (const [field, fieldDef] of Object.entries(schema)) {
|
|
60
|
+
const defaultValue = typeof fieldDef === "object" ? fieldDef.default : void 0;
|
|
61
|
+
if (result[field] === void 0 && defaultValue !== void 0) {
|
|
62
|
+
result[field] = defaultValue;
|
|
63
|
+
}
|
|
64
|
+
if (typeof fieldDef === "object" && fieldDef.options && Array.isArray(fieldDef.options)) {
|
|
65
|
+
if (result[field] !== void 0 && !fieldDef.options.includes(result[field])) {
|
|
66
|
+
if (defaultValue !== void 0) {
|
|
67
|
+
result[field] = defaultValue;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
if (typeof fieldDef === "object" && fieldDef.type === "object" && fieldDef.schema && result[field]) {
|
|
72
|
+
result[field] = applySchemaToObject(result[field], fieldDef.schema);
|
|
73
|
+
}
|
|
74
|
+
if (typeof fieldDef === "object" && fieldDef.type === "array" && fieldDef.of && result[field]) {
|
|
75
|
+
if (typeof fieldDef.of === "object") {
|
|
76
|
+
result[field] = result[field].map((item) => applySchemaToObject(item, fieldDef.of));
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return result;
|
|
81
|
+
}
|
|
82
|
+
function applySchemaToValue(value, schema) {
|
|
83
|
+
if (Array.isArray(value)) {
|
|
84
|
+
return value.map((item) => applySchemaToObject(item, schema));
|
|
85
|
+
}
|
|
86
|
+
return applySchemaToObject(value, schema);
|
|
87
|
+
}
|
|
88
|
+
function applySchemas(data, schemas) {
|
|
89
|
+
if (!schemas || !data || typeof data !== "object") {
|
|
90
|
+
return data || {};
|
|
91
|
+
}
|
|
92
|
+
const result = { ...data };
|
|
93
|
+
for (const [tag, rawValue] of Object.entries(data)) {
|
|
94
|
+
const schema = schemas[tag];
|
|
95
|
+
if (!schema) continue;
|
|
96
|
+
result[tag] = applySchemaToValue(rawValue, schema);
|
|
97
|
+
}
|
|
98
|
+
return result;
|
|
99
|
+
}
|
|
100
|
+
function applyDefaults(params, defaults) {
|
|
101
|
+
if (!defaults || Object.keys(defaults).length === 0) {
|
|
102
|
+
return params || {};
|
|
103
|
+
}
|
|
104
|
+
return {
|
|
105
|
+
...defaults,
|
|
106
|
+
...params || {}
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
function prepareProps(block, meta) {
|
|
110
|
+
const defaults = meta?.defaults || {};
|
|
111
|
+
const params = applyDefaults(block.properties, defaults);
|
|
112
|
+
const content = guaranteeContentStructure(block.parsedContent);
|
|
113
|
+
const schemas = meta?.schemas || null;
|
|
114
|
+
if (schemas && content.data) {
|
|
115
|
+
content.data = applySchemas(content.data, schemas);
|
|
116
|
+
}
|
|
117
|
+
return { content, params };
|
|
118
|
+
}
|
|
119
|
+
function getComponentMeta(componentName) {
|
|
120
|
+
return globalThis.uniweb?.getComponentMeta?.(componentName) || null;
|
|
121
|
+
}
|
|
122
|
+
function getComponentDefaults(componentName) {
|
|
123
|
+
return globalThis.uniweb?.getComponentDefaults?.(componentName) || {};
|
|
124
|
+
}
|
|
125
|
+
const hexToRgba = (hex, opacity) => {
|
|
126
|
+
const r = parseInt(hex.slice(1, 3), 16);
|
|
127
|
+
const g = parseInt(hex.slice(3, 5), 16);
|
|
128
|
+
const b = parseInt(hex.slice(5, 7), 16);
|
|
129
|
+
return `rgba(${r},${g},${b},${opacity})`;
|
|
130
|
+
};
|
|
131
|
+
const getWrapperProps = (block) => {
|
|
132
|
+
const theme = block.themeName;
|
|
133
|
+
const blockClassName = block.state?.className || "";
|
|
134
|
+
let className = theme || "";
|
|
135
|
+
if (blockClassName) {
|
|
136
|
+
className = className ? `${className} ${blockClassName}` : blockClassName;
|
|
137
|
+
}
|
|
138
|
+
const { background = {}, colors = {} } = block.standardOptions;
|
|
139
|
+
const style = {};
|
|
140
|
+
if (background.mode === "gradient") {
|
|
141
|
+
const {
|
|
142
|
+
enabled = false,
|
|
143
|
+
start = "transparent",
|
|
144
|
+
end = "transparent",
|
|
145
|
+
angle = 0,
|
|
146
|
+
startPosition = 0,
|
|
147
|
+
endPosition = 100,
|
|
148
|
+
startOpacity = 0.7,
|
|
149
|
+
endOpacity = 0.3
|
|
150
|
+
} = background.gradient || {};
|
|
151
|
+
if (enabled) {
|
|
152
|
+
style["--bg-color"] = `linear-gradient(${angle}deg,
|
|
153
|
+
${hexToRgba(start, startOpacity)} ${startPosition}%,
|
|
154
|
+
${hexToRgba(end, endOpacity)} ${endPosition}%)`;
|
|
155
|
+
}
|
|
156
|
+
} else if (background.mode === "image" || background.mode === "video") {
|
|
157
|
+
const settings = background[background.mode] || {};
|
|
158
|
+
const { url = "", file = "" } = settings;
|
|
159
|
+
if (url || file) {
|
|
160
|
+
style["--bg-color"] = "transparent";
|
|
161
|
+
style.position = "relative";
|
|
162
|
+
style.maxWidth = "100%";
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
return {
|
|
166
|
+
id: `Section${block.id}`,
|
|
167
|
+
style,
|
|
168
|
+
className
|
|
169
|
+
};
|
|
170
|
+
};
|
|
171
|
+
function BlockRenderer({ block, pure = false, extra = {} }) {
|
|
172
|
+
const Component = block.initComponent();
|
|
173
|
+
if (!Component) {
|
|
174
|
+
return /* @__PURE__ */ jsxs("div", { className: "block-error", style: { padding: "1rem", background: "#fef2f2", color: "#dc2626" }, children: [
|
|
175
|
+
"Component not found: ",
|
|
176
|
+
block.type
|
|
177
|
+
] });
|
|
178
|
+
}
|
|
179
|
+
let content, params;
|
|
180
|
+
if (block.parsedContent?._isPoc) {
|
|
181
|
+
content = block.parsedContent._pocContent;
|
|
182
|
+
params = block.properties;
|
|
183
|
+
} else {
|
|
184
|
+
const meta = getComponentMeta(block.type);
|
|
185
|
+
const prepared = prepareProps(block, meta);
|
|
186
|
+
params = prepared.params;
|
|
187
|
+
content = {
|
|
188
|
+
...prepared.content,
|
|
189
|
+
...block.properties,
|
|
190
|
+
// Frontmatter params overlay (legacy support)
|
|
191
|
+
_prosemirror: block.parsedContent
|
|
192
|
+
// Keep original for components that need raw access
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
const componentProps = {
|
|
196
|
+
content,
|
|
197
|
+
params,
|
|
198
|
+
block,
|
|
199
|
+
input: block.input
|
|
200
|
+
};
|
|
201
|
+
if (pure) {
|
|
202
|
+
return /* @__PURE__ */ jsx(Component, { ...componentProps, extra });
|
|
203
|
+
}
|
|
204
|
+
const wrapperProps = getWrapperProps(block);
|
|
205
|
+
return /* @__PURE__ */ jsx("div", { ...wrapperProps, children: /* @__PURE__ */ jsx(Component, { ...componentProps }) });
|
|
206
|
+
}
|
|
207
|
+
function Blocks({ blocks, extra = {} }) {
|
|
208
|
+
if (!blocks || blocks.length === 0) return null;
|
|
209
|
+
return blocks.map((block, index) => /* @__PURE__ */ jsx(React.Fragment, { children: /* @__PURE__ */ jsx(BlockRenderer, { block, extra }) }, block.id || index));
|
|
210
|
+
}
|
|
211
|
+
function DefaultLayout({ header, body, footer }) {
|
|
212
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
213
|
+
header,
|
|
214
|
+
body,
|
|
215
|
+
footer
|
|
216
|
+
] });
|
|
217
|
+
}
|
|
218
|
+
function Layout({ page, website }) {
|
|
219
|
+
const RemoteLayout = website.getRemoteLayout();
|
|
220
|
+
const headerBlocks = page.getHeaderBlocks();
|
|
221
|
+
const bodyBlocks = page.getBodyBlocks();
|
|
222
|
+
const footerBlocks = page.getFooterBlocks();
|
|
223
|
+
const leftBlocks = page.getLeftBlocks();
|
|
224
|
+
const rightBlocks = page.getRightBlocks();
|
|
225
|
+
const headerElement = headerBlocks ? /* @__PURE__ */ jsx(Blocks, { blocks: headerBlocks }) : null;
|
|
226
|
+
const bodyElement = bodyBlocks ? /* @__PURE__ */ jsx(Blocks, { blocks: bodyBlocks }) : null;
|
|
227
|
+
const footerElement = footerBlocks ? /* @__PURE__ */ jsx(Blocks, { blocks: footerBlocks }) : null;
|
|
228
|
+
const leftElement = leftBlocks ? /* @__PURE__ */ jsx(Blocks, { blocks: leftBlocks }) : null;
|
|
229
|
+
const rightElement = rightBlocks ? /* @__PURE__ */ jsx(Blocks, { blocks: rightBlocks }) : null;
|
|
230
|
+
if (RemoteLayout) {
|
|
231
|
+
return /* @__PURE__ */ jsx(
|
|
232
|
+
RemoteLayout,
|
|
233
|
+
{
|
|
234
|
+
page,
|
|
235
|
+
website,
|
|
236
|
+
header: headerElement,
|
|
237
|
+
body: bodyElement,
|
|
238
|
+
footer: footerElement,
|
|
239
|
+
left: leftElement,
|
|
240
|
+
right: rightElement,
|
|
241
|
+
leftPanel: leftElement,
|
|
242
|
+
rightPanel: rightElement
|
|
243
|
+
}
|
|
244
|
+
);
|
|
245
|
+
}
|
|
246
|
+
return /* @__PURE__ */ jsx(
|
|
247
|
+
DefaultLayout,
|
|
248
|
+
{
|
|
249
|
+
header: headerElement,
|
|
250
|
+
body: bodyElement,
|
|
251
|
+
footer: footerElement
|
|
252
|
+
}
|
|
253
|
+
);
|
|
254
|
+
}
|
|
255
|
+
function PageElement({ page, website }) {
|
|
256
|
+
return React.createElement(
|
|
257
|
+
"main",
|
|
258
|
+
null,
|
|
259
|
+
React.createElement(Layout, { page, website })
|
|
260
|
+
);
|
|
261
|
+
}
|
|
262
|
+
export {
|
|
263
|
+
BlockRenderer,
|
|
264
|
+
Blocks,
|
|
265
|
+
Layout,
|
|
266
|
+
PageElement,
|
|
267
|
+
applyDefaults,
|
|
268
|
+
applySchemas,
|
|
269
|
+
getComponentDefaults,
|
|
270
|
+
getComponentMeta,
|
|
271
|
+
guaranteeContentStructure,
|
|
272
|
+
prepareProps
|
|
273
|
+
};
|
|
274
|
+
//# sourceMappingURL=ssr.js.map
|
package/dist/ssr.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ssr.js","sources":["../src/prepare-props.js","../src/components/BlockRenderer.jsx","../src/components/Blocks.jsx","../src/components/Layout.jsx","../src/ssr.js"],"sourcesContent":["/**\n * Props Preparation for Runtime Guarantees\n *\n * Prepares props for foundation components with:\n * - Param defaults from runtime schema\n * - Guaranteed content structure (no null checks needed)\n *\n * This enables simpler component code by ensuring predictable prop shapes.\n */\n\n/**\n * Guarantee item has flat content structure\n *\n * @param {Object} item - Raw item from parser\n * @returns {Object} Item with guaranteed flat structure\n */\nfunction guaranteeItemStructure(item) {\n return {\n title: item.title || '',\n pretitle: item.pretitle || '',\n subtitle: item.subtitle || '',\n paragraphs: item.paragraphs || [],\n links: item.links || [],\n imgs: item.imgs || [],\n lists: item.lists || [],\n icons: item.icons || [],\n videos: item.videos || [],\n buttons: item.buttons || [],\n data: item.data || {},\n cards: item.cards || [],\n documents: item.documents || [],\n forms: item.forms || [],\n quotes: item.quotes || [],\n headings: item.headings || [],\n }\n}\n\n/**\n * Guarantee content structure exists\n * Returns a flat content object with all standard fields guaranteed to exist\n *\n * @param {Object} parsedContent - Raw parsed content from semantic parser (flat structure)\n * @returns {Object} Content with guaranteed flat structure\n */\nexport function guaranteeContentStructure(parsedContent) {\n const content = parsedContent || {}\n\n return {\n // Flat header fields\n title: content.title || '',\n pretitle: content.pretitle || '',\n subtitle: content.subtitle || '',\n subtitle2: content.subtitle2 || '',\n alignment: content.alignment || null,\n\n // Flat body fields\n paragraphs: content.paragraphs || [],\n links: content.links || [],\n imgs: content.imgs || [],\n lists: content.lists || [],\n icons: content.icons || [],\n videos: content.videos || [],\n buttons: content.buttons || [],\n data: content.data || {},\n cards: content.cards || [],\n documents: content.documents || [],\n forms: content.forms || [],\n quotes: content.quotes || [],\n headings: content.headings || [],\n\n // Items with guaranteed structure\n items: (content.items || []).map(guaranteeItemStructure),\n\n // Sequence for ordered rendering\n sequence: content.sequence || [],\n\n // Preserve raw content if present\n raw: content.raw,\n }\n}\n\n/**\n * Apply a schema to a single object\n * Only processes fields defined in the schema, preserves unknown fields\n *\n * @param {Object} obj - The object to process\n * @param {Object} schema - Schema definition (fieldName -> fieldDef)\n * @returns {Object} Object with schema defaults applied\n */\nfunction applySchemaToObject(obj, schema) {\n if (!obj || typeof obj !== 'object' || Array.isArray(obj)) {\n return obj\n }\n\n const result = { ...obj }\n\n for (const [field, fieldDef] of Object.entries(schema)) {\n // Get the default value - handle both shorthand and full form\n const defaultValue = typeof fieldDef === 'object' ? fieldDef.default : undefined\n\n // Apply default if field is missing and default exists\n if (result[field] === undefined && defaultValue !== undefined) {\n result[field] = defaultValue\n }\n\n // For select fields with options, apply default if value is not among valid options\n if (typeof fieldDef === 'object' && fieldDef.options && Array.isArray(fieldDef.options)) {\n if (result[field] !== undefined && !fieldDef.options.includes(result[field])) {\n // Value exists but is not valid - apply default if available\n if (defaultValue !== undefined) {\n result[field] = defaultValue\n }\n }\n }\n\n // Handle nested object schema\n if (typeof fieldDef === 'object' && fieldDef.type === 'object' && fieldDef.schema && result[field]) {\n result[field] = applySchemaToObject(result[field], fieldDef.schema)\n }\n\n // Handle array with inline schema\n if (typeof fieldDef === 'object' && fieldDef.type === 'array' && fieldDef.of && result[field]) {\n if (typeof fieldDef.of === 'object') {\n result[field] = result[field].map(item => applySchemaToObject(item, fieldDef.of))\n }\n }\n }\n\n return result\n}\n\n/**\n * Apply a schema to a value (object or array of objects)\n *\n * @param {Object|Array} value - The value to process\n * @param {Object} schema - Schema definition\n * @returns {Object|Array} Value with schema defaults applied\n */\nfunction applySchemaToValue(value, schema) {\n if (Array.isArray(value)) {\n return value.map(item => applySchemaToObject(item, schema))\n }\n return applySchemaToObject(value, schema)\n}\n\n/**\n * Apply schemas to content.data\n * Only processes tags that have a matching schema, leaves others untouched\n *\n * @param {Object} data - The data object from content\n * @param {Object} schemas - Schema definitions from runtime meta\n * @returns {Object} Data with schemas applied\n */\nexport function applySchemas(data, schemas) {\n if (!schemas || !data || typeof data !== 'object') {\n return data || {}\n }\n\n const result = { ...data }\n\n for (const [tag, rawValue] of Object.entries(data)) {\n const schema = schemas[tag]\n if (!schema) continue // No schema for this tag - leave as-is\n\n result[tag] = applySchemaToValue(rawValue, schema)\n }\n\n return result\n}\n\n/**\n * Apply param defaults from runtime schema\n *\n * @param {Object} params - Params from frontmatter\n * @param {Object} defaults - Default values from runtime schema\n * @returns {Object} Merged params with defaults applied\n */\nexport function applyDefaults(params, defaults) {\n if (!defaults || Object.keys(defaults).length === 0) {\n return params || {}\n }\n\n return {\n ...defaults,\n ...(params || {}),\n }\n}\n\n/**\n * Prepare props for a component with runtime guarantees\n *\n * @param {Object} block - The block instance\n * @param {Object} meta - Runtime metadata for the component (from meta[componentName])\n * @returns {Object} Prepared props: { content, params }\n */\nexport function prepareProps(block, meta) {\n // Apply param defaults\n const defaults = meta?.defaults || {}\n const params = applyDefaults(block.properties, defaults)\n\n // Guarantee content structure\n const content = guaranteeContentStructure(block.parsedContent)\n\n // Apply schemas to content.data\n const schemas = meta?.schemas || null\n if (schemas && content.data) {\n content.data = applySchemas(content.data, schemas)\n }\n\n return { content, params }\n}\n\n/**\n * Get runtime metadata for a component from the global uniweb instance\n *\n * @param {string} componentName\n * @returns {Object|null}\n */\nexport function getComponentMeta(componentName) {\n return globalThis.uniweb?.getComponentMeta?.(componentName) || null\n}\n\n/**\n * Get default param values for a component\n *\n * @param {string} componentName\n * @returns {Object}\n */\nexport function getComponentDefaults(componentName) {\n return globalThis.uniweb?.getComponentDefaults?.(componentName) || {}\n}\n","/**\n * BlockRenderer\n *\n * Bridges Block data to foundation components.\n * Handles theming, wrapper props, and runtime guarantees.\n */\n\nimport React from 'react'\nimport { prepareProps, getComponentMeta } from '../prepare-props.js'\n\n/**\n * Convert hex color to rgba\n */\nconst hexToRgba = (hex, opacity) => {\n const r = parseInt(hex.slice(1, 3), 16)\n const g = parseInt(hex.slice(3, 5), 16)\n const b = parseInt(hex.slice(5, 7), 16)\n return `rgba(${r},${g},${b},${opacity})`\n}\n\n/**\n * Build wrapper props from block configuration\n */\nconst getWrapperProps = (block) => {\n const theme = block.themeName\n const blockClassName = block.state?.className || ''\n\n let className = theme || ''\n if (blockClassName) {\n className = className ? `${className} ${blockClassName}` : blockClassName\n }\n\n const { background = {}, colors = {} } = block.standardOptions\n const style = {}\n\n // Handle background modes\n if (background.mode === 'gradient') {\n const {\n enabled = false,\n start = 'transparent',\n end = 'transparent',\n angle = 0,\n startPosition = 0,\n endPosition = 100,\n startOpacity = 0.7,\n endOpacity = 0.3\n } = background.gradient || {}\n\n if (enabled) {\n style['--bg-color'] = `linear-gradient(${angle}deg,\n ${hexToRgba(start, startOpacity)} ${startPosition}%,\n ${hexToRgba(end, endOpacity)} ${endPosition}%)`\n }\n } else if (background.mode === 'image' || background.mode === 'video') {\n const settings = background[background.mode] || {}\n const { url = '', file = '' } = settings\n\n if (url || file) {\n style['--bg-color'] = 'transparent'\n style.position = 'relative'\n style.maxWidth = '100%'\n }\n }\n\n return {\n id: `Section${block.id}`,\n style,\n className\n }\n}\n\n/**\n * BlockRenderer component\n */\nexport default function BlockRenderer({ block, pure = false, extra = {} }) {\n const Component = block.initComponent()\n\n if (!Component) {\n return (\n <div className=\"block-error\" style={{ padding: '1rem', background: '#fef2f2', color: '#dc2626' }}>\n Component not found: {block.type}\n </div>\n )\n }\n\n // Build content and params with runtime guarantees\n // Sources:\n // 1. parsedContent._isPoc - simple PoC format (hardcoded content)\n // 2. parsedContent - semantic parser output (flat: title, paragraphs, links, etc.)\n // 3. block.properties - params from frontmatter (theme, alignment, etc.)\n // 4. meta - defaults from component meta.js\n let content, params\n\n if (block.parsedContent?._isPoc) {\n // Simple PoC format - content was passed directly\n content = block.parsedContent._pocContent\n params = block.properties\n } else {\n // Get runtime metadata for this component (has defaults, data binding, etc.)\n const meta = getComponentMeta(block.type)\n\n // Prepare props with runtime guarantees:\n // - Apply param defaults from meta.js\n // - Guarantee content structure exists\n const prepared = prepareProps(block, meta)\n params = prepared.params\n\n // Merge prepared content with raw access for components that need it\n content = {\n ...prepared.content,\n ...block.properties, // Frontmatter params overlay (legacy support)\n _prosemirror: block.parsedContent // Keep original for components that need raw access\n }\n }\n\n const componentProps = {\n content,\n params,\n block,\n input: block.input\n }\n\n if (pure) {\n return <Component {...componentProps} extra={extra} />\n }\n\n const wrapperProps = getWrapperProps(block)\n\n return (\n <div {...wrapperProps}>\n <Component {...componentProps} />\n </div>\n )\n}\n","/**\n * Blocks\n *\n * Renders an array of blocks for a layout area (header, body, footer, panels).\n * Used by the Layout component to pre-render each area.\n */\n\nimport React from 'react'\nimport BlockRenderer from './BlockRenderer.jsx'\n\n/**\n * Render a list of blocks\n *\n * @param {Object} props\n * @param {Block[]} props.blocks - Array of Block instances to render\n * @param {Object} [props.extra] - Extra props to pass to each block\n */\nexport default function Blocks({ blocks, extra = {} }) {\n if (!blocks || blocks.length === 0) return null\n\n return blocks.map((block, index) => (\n <React.Fragment key={block.id || index}>\n <BlockRenderer block={block} extra={extra} />\n </React.Fragment>\n ))\n}\n","/**\n * Layout\n *\n * Orchestrates page rendering by assembling layout areas (header, body, footer, panels).\n * Supports foundation-provided custom Layout components via website.getRemoteLayout().\n *\n * Layout Areas:\n * - header: Top navigation, branding (from @header page)\n * - body: Main page content (from page sections)\n * - footer: Bottom navigation, copyright (from @footer page)\n * - left: Left sidebar/panel (from @left page)\n * - right: Right sidebar/panel (from @right page)\n *\n * Custom Layouts:\n * Foundations can provide a custom Layout via src/exports.js:\n *\n * ```jsx\n * // src/exports.js\n * import Layout from './components/Layout'\n *\n * export default {\n * Layout,\n * props: {\n * themeToggleEnabled: true,\n * }\n * }\n * ```\n *\n * The Layout component receives pre-rendered areas as props:\n * - page, website: Runtime context\n * - header, body, footer: Pre-rendered React elements\n * - left, right (or leftPanel, rightPanel): Sidebar panels\n */\n\nimport Blocks from './Blocks.jsx'\n\n/**\n * Default layout - renders header, body, footer in sequence\n * (no panels in default layout)\n */\nfunction DefaultLayout({ header, body, footer }) {\n return (\n <>\n {header}\n {body}\n {footer}\n </>\n )\n}\n\n/**\n * Layout component\n *\n * @param {Object} props\n * @param {Page} props.page - Current page instance\n * @param {Website} props.website - Website instance\n */\nexport default function Layout({ page, website }) {\n // Check if foundation provides a custom Layout\n const RemoteLayout = website.getRemoteLayout()\n\n // Get block groups from page (respects layout preferences)\n const headerBlocks = page.getHeaderBlocks()\n const bodyBlocks = page.getBodyBlocks()\n const footerBlocks = page.getFooterBlocks()\n const leftBlocks = page.getLeftBlocks()\n const rightBlocks = page.getRightBlocks()\n\n // Pre-render each area as React elements\n const headerElement = headerBlocks ? <Blocks blocks={headerBlocks} /> : null\n const bodyElement = bodyBlocks ? <Blocks blocks={bodyBlocks} /> : null\n const footerElement = footerBlocks ? <Blocks blocks={footerBlocks} /> : null\n const leftElement = leftBlocks ? <Blocks blocks={leftBlocks} /> : null\n const rightElement = rightBlocks ? <Blocks blocks={rightBlocks} /> : null\n\n // Use foundation's custom Layout if provided\n if (RemoteLayout) {\n return (\n <RemoteLayout\n page={page}\n website={website}\n header={headerElement}\n body={bodyElement}\n footer={footerElement}\n left={leftElement}\n right={rightElement}\n // Aliases for backwards compatibility\n leftPanel={leftElement}\n rightPanel={rightElement}\n />\n )\n }\n\n // Default layout\n return (\n <DefaultLayout\n header={headerElement}\n body={bodyElement}\n footer={footerElement}\n />\n )\n}\n","/**\n * @uniweb/runtime/ssr - Server-Side Rendering Entry Point\n *\n * Node.js-compatible exports for SSG/prerendering.\n * This module is built to a standalone bundle that can be imported\n * directly by Node.js without Vite transpilation.\n *\n * Usage in prerender.js:\n * import { renderPage, Blocks, BlockRenderer } from '@uniweb/runtime/ssr'\n */\n\nimport React from 'react'\n\n// Props preparation (no browser APIs)\nexport {\n prepareProps,\n applySchemas,\n applyDefaults,\n guaranteeContentStructure,\n getComponentMeta,\n getComponentDefaults\n} from './prepare-props.js'\n\n// Components for rendering\nexport { default as BlockRenderer } from './components/BlockRenderer.jsx'\nexport { default as Blocks } from './components/Blocks.jsx'\nexport { default as Layout } from './components/Layout.jsx'\n\n// Re-export Layout's DefaultLayout for direct use\nimport LayoutComponent from './components/Layout.jsx'\n\n/**\n * Render a page to React elements\n *\n * This is the main entry point for SSG. It returns a React element\n * that can be passed to renderToString().\n *\n * @param {Object} props\n * @param {Page} props.page - The page instance to render\n * @param {Website} props.website - The website instance\n * @returns {React.ReactElement}\n */\nexport function PageElement({ page, website }) {\n return React.createElement(\n 'main',\n null,\n React.createElement(LayoutComponent, { page, website })\n )\n}\n"],"names":["LayoutComponent"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@uniweb/runtime",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.15",
|
|
4
4
|
"description": "Minimal runtime for loading Uniweb foundations",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
7
|
-
".": "./src/index.jsx"
|
|
7
|
+
".": "./src/index.jsx",
|
|
8
|
+
"./ssr": "./dist/ssr.js"
|
|
8
9
|
},
|
|
9
10
|
"files": [
|
|
10
|
-
"src"
|
|
11
|
+
"src",
|
|
12
|
+
"dist"
|
|
11
13
|
],
|
|
12
14
|
"keywords": [
|
|
13
15
|
"uniweb",
|
|
@@ -31,9 +33,16 @@
|
|
|
31
33
|
"dependencies": {
|
|
32
34
|
"@uniweb/core": "0.1.12"
|
|
33
35
|
},
|
|
36
|
+
"devDependencies": {
|
|
37
|
+
"@vitejs/plugin-react": "^4.5.2",
|
|
38
|
+
"vite": "^7.3.1"
|
|
39
|
+
},
|
|
34
40
|
"peerDependencies": {
|
|
35
41
|
"react": "^18.0.0 || ^19.0.0",
|
|
36
42
|
"react-dom": "^18.0.0 || ^19.0.0",
|
|
37
43
|
"react-router-dom": "^6.0.0 || ^7.0.0"
|
|
44
|
+
},
|
|
45
|
+
"scripts": {
|
|
46
|
+
"build:ssr": "vite build --config vite.config.ssr.js"
|
|
38
47
|
}
|
|
39
48
|
}
|
package/src/prepare-props.js
CHANGED
|
@@ -26,7 +26,7 @@ function guaranteeItemStructure(item) {
|
|
|
26
26
|
icons: item.icons || [],
|
|
27
27
|
videos: item.videos || [],
|
|
28
28
|
buttons: item.buttons || [],
|
|
29
|
-
|
|
29
|
+
data: item.data || {},
|
|
30
30
|
cards: item.cards || [],
|
|
31
31
|
documents: item.documents || [],
|
|
32
32
|
forms: item.forms || [],
|
|
@@ -61,8 +61,7 @@ export function guaranteeContentStructure(parsedContent) {
|
|
|
61
61
|
icons: content.icons || [],
|
|
62
62
|
videos: content.videos || [],
|
|
63
63
|
buttons: content.buttons || [],
|
|
64
|
-
|
|
65
|
-
propertyBlocks: content.propertyBlocks || [],
|
|
64
|
+
data: content.data || {},
|
|
66
65
|
cards: content.cards || [],
|
|
67
66
|
documents: content.documents || [],
|
|
68
67
|
forms: content.forms || [],
|
|
@@ -80,6 +79,95 @@ export function guaranteeContentStructure(parsedContent) {
|
|
|
80
79
|
}
|
|
81
80
|
}
|
|
82
81
|
|
|
82
|
+
/**
|
|
83
|
+
* Apply a schema to a single object
|
|
84
|
+
* Only processes fields defined in the schema, preserves unknown fields
|
|
85
|
+
*
|
|
86
|
+
* @param {Object} obj - The object to process
|
|
87
|
+
* @param {Object} schema - Schema definition (fieldName -> fieldDef)
|
|
88
|
+
* @returns {Object} Object with schema defaults applied
|
|
89
|
+
*/
|
|
90
|
+
function applySchemaToObject(obj, schema) {
|
|
91
|
+
if (!obj || typeof obj !== 'object' || Array.isArray(obj)) {
|
|
92
|
+
return obj
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const result = { ...obj }
|
|
96
|
+
|
|
97
|
+
for (const [field, fieldDef] of Object.entries(schema)) {
|
|
98
|
+
// Get the default value - handle both shorthand and full form
|
|
99
|
+
const defaultValue = typeof fieldDef === 'object' ? fieldDef.default : undefined
|
|
100
|
+
|
|
101
|
+
// Apply default if field is missing and default exists
|
|
102
|
+
if (result[field] === undefined && defaultValue !== undefined) {
|
|
103
|
+
result[field] = defaultValue
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// For select fields with options, apply default if value is not among valid options
|
|
107
|
+
if (typeof fieldDef === 'object' && fieldDef.options && Array.isArray(fieldDef.options)) {
|
|
108
|
+
if (result[field] !== undefined && !fieldDef.options.includes(result[field])) {
|
|
109
|
+
// Value exists but is not valid - apply default if available
|
|
110
|
+
if (defaultValue !== undefined) {
|
|
111
|
+
result[field] = defaultValue
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Handle nested object schema
|
|
117
|
+
if (typeof fieldDef === 'object' && fieldDef.type === 'object' && fieldDef.schema && result[field]) {
|
|
118
|
+
result[field] = applySchemaToObject(result[field], fieldDef.schema)
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Handle array with inline schema
|
|
122
|
+
if (typeof fieldDef === 'object' && fieldDef.type === 'array' && fieldDef.of && result[field]) {
|
|
123
|
+
if (typeof fieldDef.of === 'object') {
|
|
124
|
+
result[field] = result[field].map(item => applySchemaToObject(item, fieldDef.of))
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return result
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Apply a schema to a value (object or array of objects)
|
|
134
|
+
*
|
|
135
|
+
* @param {Object|Array} value - The value to process
|
|
136
|
+
* @param {Object} schema - Schema definition
|
|
137
|
+
* @returns {Object|Array} Value with schema defaults applied
|
|
138
|
+
*/
|
|
139
|
+
function applySchemaToValue(value, schema) {
|
|
140
|
+
if (Array.isArray(value)) {
|
|
141
|
+
return value.map(item => applySchemaToObject(item, schema))
|
|
142
|
+
}
|
|
143
|
+
return applySchemaToObject(value, schema)
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Apply schemas to content.data
|
|
148
|
+
* Only processes tags that have a matching schema, leaves others untouched
|
|
149
|
+
*
|
|
150
|
+
* @param {Object} data - The data object from content
|
|
151
|
+
* @param {Object} schemas - Schema definitions from runtime meta
|
|
152
|
+
* @returns {Object} Data with schemas applied
|
|
153
|
+
*/
|
|
154
|
+
export function applySchemas(data, schemas) {
|
|
155
|
+
if (!schemas || !data || typeof data !== 'object') {
|
|
156
|
+
return data || {}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const result = { ...data }
|
|
160
|
+
|
|
161
|
+
for (const [tag, rawValue] of Object.entries(data)) {
|
|
162
|
+
const schema = schemas[tag]
|
|
163
|
+
if (!schema) continue // No schema for this tag - leave as-is
|
|
164
|
+
|
|
165
|
+
result[tag] = applySchemaToValue(rawValue, schema)
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return result
|
|
169
|
+
}
|
|
170
|
+
|
|
83
171
|
/**
|
|
84
172
|
* Apply param defaults from runtime schema
|
|
85
173
|
*
|
|
@@ -113,6 +201,12 @@ export function prepareProps(block, meta) {
|
|
|
113
201
|
// Guarantee content structure
|
|
114
202
|
const content = guaranteeContentStructure(block.parsedContent)
|
|
115
203
|
|
|
204
|
+
// Apply schemas to content.data
|
|
205
|
+
const schemas = meta?.schemas || null
|
|
206
|
+
if (schemas && content.data) {
|
|
207
|
+
content.data = applySchemas(content.data, schemas)
|
|
208
|
+
}
|
|
209
|
+
|
|
116
210
|
return { content, params }
|
|
117
211
|
}
|
|
118
212
|
|
package/src/ssr.js
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @uniweb/runtime/ssr - Server-Side Rendering Entry Point
|
|
3
|
+
*
|
|
4
|
+
* Node.js-compatible exports for SSG/prerendering.
|
|
5
|
+
* This module is built to a standalone bundle that can be imported
|
|
6
|
+
* directly by Node.js without Vite transpilation.
|
|
7
|
+
*
|
|
8
|
+
* Usage in prerender.js:
|
|
9
|
+
* import { renderPage, Blocks, BlockRenderer } from '@uniweb/runtime/ssr'
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import React from 'react'
|
|
13
|
+
|
|
14
|
+
// Props preparation (no browser APIs)
|
|
15
|
+
export {
|
|
16
|
+
prepareProps,
|
|
17
|
+
applySchemas,
|
|
18
|
+
applyDefaults,
|
|
19
|
+
guaranteeContentStructure,
|
|
20
|
+
getComponentMeta,
|
|
21
|
+
getComponentDefaults
|
|
22
|
+
} from './prepare-props.js'
|
|
23
|
+
|
|
24
|
+
// Components for rendering
|
|
25
|
+
export { default as BlockRenderer } from './components/BlockRenderer.jsx'
|
|
26
|
+
export { default as Blocks } from './components/Blocks.jsx'
|
|
27
|
+
export { default as Layout } from './components/Layout.jsx'
|
|
28
|
+
|
|
29
|
+
// Re-export Layout's DefaultLayout for direct use
|
|
30
|
+
import LayoutComponent from './components/Layout.jsx'
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Render a page to React elements
|
|
34
|
+
*
|
|
35
|
+
* This is the main entry point for SSG. It returns a React element
|
|
36
|
+
* that can be passed to renderToString().
|
|
37
|
+
*
|
|
38
|
+
* @param {Object} props
|
|
39
|
+
* @param {Page} props.page - The page instance to render
|
|
40
|
+
* @param {Website} props.website - The website instance
|
|
41
|
+
* @returns {React.ReactElement}
|
|
42
|
+
*/
|
|
43
|
+
export function PageElement({ page, website }) {
|
|
44
|
+
return React.createElement(
|
|
45
|
+
'main',
|
|
46
|
+
null,
|
|
47
|
+
React.createElement(LayoutComponent, { page, website })
|
|
48
|
+
)
|
|
49
|
+
}
|