@symbo.ls/brender 3.5.1 → 3.6.3
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/README.md +18 -2
- package/dist/cjs/index.js +4 -1
- package/dist/cjs/metadata.js +5 -81
- package/dist/cjs/render.js +252 -10
- package/dist/cjs/sitemap.js +41 -0
- package/dist/esm/index.js +4 -1
- package/dist/esm/metadata.js +4 -80
- package/dist/esm/render.js +252 -10
- package/dist/esm/sitemap.js +22 -0
- package/index.js +5 -2
- package/metadata.js +3 -115
- package/package.json +9 -8
- package/render.js +306 -11
- package/sitemap.js +28 -0
package/README.md
CHANGED
|
@@ -63,7 +63,7 @@ Browser DOM (static) DOMQL Tree (no nodes) After hydrate()
|
|
|
63
63
|
| `hydrate.js` | `collectBrNodes()` — scans DOM for data-br nodes; `hydrate()` — reconnects DOMQL tree to DOM |
|
|
64
64
|
| `env.js` | `createEnv()` — linkedom virtual DOM with browser API stubs (requestAnimationFrame, history, location, etc.) |
|
|
65
65
|
| `keys.js` | `resetKeys()`, `assignKeys()` — stamps data-br on DOM nodes; `mapKeysToElements()` — builds registry |
|
|
66
|
-
| `metadata.js` | `extractMetadata()`, `generateHeadHtml()`
|
|
66
|
+
| `metadata.js` | Re-exports from [`@symbo.ls/helmet`](../helmet/) — `extractMetadata()`, `generateHeadHtml()`, `resolveMetadata()`, `applyMetadata()` |
|
|
67
67
|
| `load.js` | `loadProject()` — imports a symbols/ directory; `loadAndRenderAll()` — renders every route |
|
|
68
68
|
| `index.js` | Re-exports everything |
|
|
69
69
|
|
|
@@ -168,13 +168,15 @@ Creates a linkedom virtual DOM environment with stubs for browser APIs that DOMQ
|
|
|
168
168
|
|
|
169
169
|
### `generateHeadHtml(metadata)`
|
|
170
170
|
|
|
171
|
-
Converts a metadata object into HTML head tags:
|
|
171
|
+
Converts a metadata object into HTML head tags. Provided by [`@symbo.ls/helmet`](../helmet/):
|
|
172
172
|
|
|
173
173
|
```js
|
|
174
174
|
generateHeadHtml({ title: 'My Page', description: 'About', 'og:image': '/img.png' })
|
|
175
175
|
// -> '<meta charset="UTF-8">\n<title>My Page</title>\n<meta name="description" content="About">\n<meta property="og:image" content="/img.png">'
|
|
176
176
|
```
|
|
177
177
|
|
|
178
|
+
Metadata values can also be functions — see the [helmet plugin](../helmet/) for details.
|
|
179
|
+
|
|
178
180
|
## Examples
|
|
179
181
|
|
|
180
182
|
The `examples/` directory contains runnable experiments. Copy a project's source into `examples/` first (gitignored), then run:
|
|
@@ -280,3 +282,17 @@ This means the server and client don't need to exchange the registry — as long
|
|
|
280
282
|
- `hydrate.js` is browser-only code (no linkedom dependency) — it's exported separately via `@symbo.ls/brender/hydrate`
|
|
281
283
|
- `createEnv()` sets `globalThis.window/document/Node/HTMLElement` because `@domql/utils` `isDOMNode` uses `instanceof` checks against global constructors
|
|
282
284
|
- `onRender` callbacks that do network requests or call `s.update()` will error during SSR — this is expected and harmless since the HTML is already produced before those callbacks fire
|
|
285
|
+
|
|
286
|
+
## Theme support
|
|
287
|
+
|
|
288
|
+
Brender defaults to `globalTheme: 'auto'`, generating CSS with both `prefers-color-scheme` media queries and `[data-theme]` selectors. The SSR output includes theme-switching CSS variables that work without JavaScript:
|
|
289
|
+
|
|
290
|
+
```css
|
|
291
|
+
@media (prefers-color-scheme: dark) {
|
|
292
|
+
:root:not([data-theme]) { --theme-document-background: #000; }
|
|
293
|
+
}
|
|
294
|
+
[data-theme="dark"] { --theme-document-background: #000; }
|
|
295
|
+
[data-theme="ocean"] { --theme-document-background: #0a2e4e; }
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
Custom themes beyond dark/light are activated via `data-theme` attribute on the root element.
|
package/dist/cjs/index.js
CHANGED
|
@@ -23,6 +23,7 @@ __export(index_exports, {
|
|
|
23
23
|
default: () => index_default,
|
|
24
24
|
extractMetadata: () => import_metadata.extractMetadata,
|
|
25
25
|
generateHeadHtml: () => import_metadata.generateHeadHtml,
|
|
26
|
+
generateSitemap: () => import_sitemap.generateSitemap,
|
|
26
27
|
hydrate: () => import_hydrate.hydrate,
|
|
27
28
|
loadAndRenderAll: () => import_load.loadAndRenderAll,
|
|
28
29
|
loadProject: () => import_load.loadProject,
|
|
@@ -40,6 +41,7 @@ var import_load = require("./load.js");
|
|
|
40
41
|
var import_render = require("./render.js");
|
|
41
42
|
var import_metadata = require("./metadata.js");
|
|
42
43
|
var import_hydrate = require("./hydrate.js");
|
|
44
|
+
var import_sitemap = require("./sitemap.js");
|
|
43
45
|
var index_default = {
|
|
44
46
|
createEnv: import_env.createEnv,
|
|
45
47
|
resetKeys: import_keys.resetKeys,
|
|
@@ -54,5 +56,6 @@ var index_default = {
|
|
|
54
56
|
extractMetadata: import_metadata.extractMetadata,
|
|
55
57
|
generateHeadHtml: import_metadata.generateHeadHtml,
|
|
56
58
|
collectBrNodes: import_hydrate.collectBrNodes,
|
|
57
|
-
hydrate: import_hydrate.hydrate
|
|
59
|
+
hydrate: import_hydrate.hydrate,
|
|
60
|
+
generateSitemap: import_sitemap.generateSitemap
|
|
58
61
|
};
|
package/dist/cjs/metadata.js
CHANGED
|
@@ -17,86 +17,10 @@ var __copyProps = (to, from, except, desc) => {
|
|
|
17
17
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
18
18
|
var metadata_exports = {};
|
|
19
19
|
__export(metadata_exports, {
|
|
20
|
-
|
|
21
|
-
|
|
20
|
+
applyMetadata: () => import_helmet.applyMetadata,
|
|
21
|
+
extractMetadata: () => import_helmet.extractMetadata,
|
|
22
|
+
generateHeadHtml: () => import_helmet.generateHeadHtml,
|
|
23
|
+
resolveMetadata: () => import_helmet.resolveMetadata
|
|
22
24
|
});
|
|
23
25
|
module.exports = __toCommonJS(metadata_exports);
|
|
24
|
-
|
|
25
|
-
const pages = data.pages || {};
|
|
26
|
-
const page = pages[route];
|
|
27
|
-
let metadata = {};
|
|
28
|
-
if (data.integrations?.seo) {
|
|
29
|
-
metadata = { ...data.integrations.seo };
|
|
30
|
-
}
|
|
31
|
-
if (page) {
|
|
32
|
-
const pageMeta = page.metadata || page.helmet || {};
|
|
33
|
-
metadata = { ...metadata, ...pageMeta };
|
|
34
|
-
if (!metadata.title && page.state?.title) {
|
|
35
|
-
metadata.title = page.state.title;
|
|
36
|
-
}
|
|
37
|
-
if (!metadata.description && page.state?.description) {
|
|
38
|
-
metadata.description = page.state.description;
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
if (!metadata.title) {
|
|
42
|
-
metadata.title = data.name || "Symbols";
|
|
43
|
-
}
|
|
44
|
-
return metadata;
|
|
45
|
-
};
|
|
46
|
-
const generateHeadHtml = (metadata) => {
|
|
47
|
-
const esc = (text) => {
|
|
48
|
-
if (text === null || text === void 0) return "";
|
|
49
|
-
const map = { "&": "&", "<": "<", ">": ">", '"': """, "'": "'" };
|
|
50
|
-
return text.toString().replace(/[&<>"']/g, (m) => map[m]);
|
|
51
|
-
};
|
|
52
|
-
const tags = [
|
|
53
|
-
'<meta charset="UTF-8">',
|
|
54
|
-
'<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1, maximum-scale=1, user-scalable=no">'
|
|
55
|
-
];
|
|
56
|
-
for (const [key, value] of Object.entries(metadata)) {
|
|
57
|
-
if (!value && value !== 0 && value !== false) continue;
|
|
58
|
-
if (key === "title") {
|
|
59
|
-
tags.push(`<title>${esc(value)}</title>`);
|
|
60
|
-
continue;
|
|
61
|
-
}
|
|
62
|
-
if (key === "canonical") {
|
|
63
|
-
tags.push(`<link rel="canonical" href="${esc(value)}">`);
|
|
64
|
-
continue;
|
|
65
|
-
}
|
|
66
|
-
if (key === "alternate" && Array.isArray(value)) {
|
|
67
|
-
value.forEach((alt) => {
|
|
68
|
-
if (typeof alt === "object") {
|
|
69
|
-
const attrs = Object.entries(alt).map(([k, v]) => `${k}="${esc(v)}"`).join(" ");
|
|
70
|
-
tags.push(`<link rel="alternate" ${attrs}>`);
|
|
71
|
-
}
|
|
72
|
-
});
|
|
73
|
-
continue;
|
|
74
|
-
}
|
|
75
|
-
const propertyPrefixes = ["og:", "article:", "product:", "fb:", "profile:", "book:", "business:", "music:", "video:"];
|
|
76
|
-
const namePrefixes = ["twitter:", "DC:", "DCTERMS:"];
|
|
77
|
-
const isProperty = propertyPrefixes.some((p) => key.startsWith(p));
|
|
78
|
-
const isName = namePrefixes.some((p) => key.startsWith(p));
|
|
79
|
-
if (key.startsWith("http-equiv:")) {
|
|
80
|
-
const httpKey = key.replace("http-equiv:", "");
|
|
81
|
-
tags.push(`<meta http-equiv="${esc(httpKey)}" content="${esc(value)}">`);
|
|
82
|
-
} else if (key.startsWith("itemprop:")) {
|
|
83
|
-
const itemKey = key.replace("itemprop:", "");
|
|
84
|
-
tags.push(`<meta itemprop="${esc(itemKey)}" content="${esc(value)}">`);
|
|
85
|
-
} else if (isProperty) {
|
|
86
|
-
if (Array.isArray(value)) {
|
|
87
|
-
value.forEach((v) => tags.push(`<meta property="${esc(key)}" content="${esc(v)}">`));
|
|
88
|
-
} else {
|
|
89
|
-
tags.push(`<meta property="${esc(key)}" content="${esc(value)}">`);
|
|
90
|
-
}
|
|
91
|
-
} else if (isName) {
|
|
92
|
-
tags.push(`<meta name="${esc(key)}" content="${esc(value)}">`);
|
|
93
|
-
} else if (key !== "favicon" && key !== "favicons") {
|
|
94
|
-
if (Array.isArray(value)) {
|
|
95
|
-
value.forEach((v) => tags.push(`<meta name="${esc(key)}" content="${esc(v)}">`));
|
|
96
|
-
} else if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
|
|
97
|
-
tags.push(`<meta name="${esc(key)}" content="${esc(value)}">`);
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
return tags.join("\n");
|
|
102
|
-
};
|
|
26
|
+
var import_helmet = require("@symbo.ls/helmet");
|
package/dist/cjs/render.js
CHANGED
|
@@ -30,9 +30,11 @@ __export(render_exports, {
|
|
|
30
30
|
render: () => render,
|
|
31
31
|
renderElement: () => renderElement,
|
|
32
32
|
renderPage: () => renderPage,
|
|
33
|
-
renderRoute: () => renderRoute
|
|
33
|
+
renderRoute: () => renderRoute,
|
|
34
|
+
resetGlobalCSSCache: () => resetGlobalCSSCache
|
|
34
35
|
});
|
|
35
36
|
module.exports = __toCommonJS(render_exports);
|
|
37
|
+
var import_path = require("path");
|
|
36
38
|
var import_env = require("./env.js");
|
|
37
39
|
var import_keys = require("./keys.js");
|
|
38
40
|
var import_metadata = require("./metadata.js");
|
|
@@ -92,7 +94,15 @@ const UIKIT_STUBS = {
|
|
|
92
94
|
H3: { tag: "h3" },
|
|
93
95
|
H4: { tag: "h4" },
|
|
94
96
|
H5: { tag: "h5" },
|
|
95
|
-
H6: { tag: "h6" }
|
|
97
|
+
H6: { tag: "h6" },
|
|
98
|
+
Svg: {
|
|
99
|
+
tag: "svg",
|
|
100
|
+
attr: {
|
|
101
|
+
xmlns: "http://www.w3.org/2000/svg",
|
|
102
|
+
"xmlns:xlink": "http://www.w3.org/1999/xlink"
|
|
103
|
+
}
|
|
104
|
+
},
|
|
105
|
+
Text: { tag: "span" }
|
|
96
106
|
};
|
|
97
107
|
const render = async (data, options = {}) => {
|
|
98
108
|
const { route = "/", state: stateOverrides, context: contextOverrides } = options;
|
|
@@ -150,9 +160,169 @@ const renderElement = async (elementDef, options = {}) => {
|
|
|
150
160
|
}
|
|
151
161
|
(0, import_keys.assignKeys)(body);
|
|
152
162
|
const registry = element ? (0, import_keys.mapKeysToElements)(element) : {};
|
|
153
|
-
const html = body.innerHTML;
|
|
163
|
+
const html = fixSvgContent(body.innerHTML);
|
|
154
164
|
return { html, registry, element };
|
|
155
165
|
};
|
|
166
|
+
const fixSvgContent = (html) => {
|
|
167
|
+
return html.replace(
|
|
168
|
+
/(<svg\b[^>]*>)([\s\S]*?)(<\/svg>)/gi,
|
|
169
|
+
(match, open, content, close) => {
|
|
170
|
+
if (content.includes("<")) {
|
|
171
|
+
const unescaped = content.replace(/</g, "<").replace(/>/g, ">").replace(/&/g, "&").replace(/"/g, '"').replace(/'/g, "'");
|
|
172
|
+
return open + unescaped + close;
|
|
173
|
+
}
|
|
174
|
+
return match;
|
|
175
|
+
}
|
|
176
|
+
);
|
|
177
|
+
};
|
|
178
|
+
let _cachedGlobalCSS = null;
|
|
179
|
+
const generateGlobalCSS = async (ds, config) => {
|
|
180
|
+
if (_cachedGlobalCSS) return _cachedGlobalCSS;
|
|
181
|
+
try {
|
|
182
|
+
const { existsSync, writeFileSync, unlinkSync } = await import("fs");
|
|
183
|
+
const { tmpdir } = await import("os");
|
|
184
|
+
const { randomBytes } = await import("crypto");
|
|
185
|
+
const esbuild = await import("esbuild");
|
|
186
|
+
const dsJson = JSON.stringify(ds || {});
|
|
187
|
+
const cfgJson = JSON.stringify(config || {});
|
|
188
|
+
const tmpEntry = (0, import_path.join)(tmpdir(), `br_global_${randomBytes(6).toString("hex")}.mjs`);
|
|
189
|
+
const tmpOut = (0, import_path.join)(tmpdir(), `br_global_${randomBytes(6).toString("hex")}_out.mjs`);
|
|
190
|
+
writeFileSync(tmpEntry, `
|
|
191
|
+
import { set, getActiveConfig, getFontFaceString } from '@symbo.ls/scratch'
|
|
192
|
+
import { DEFAULT_CONFIG } from '@symbo.ls/default-config'
|
|
193
|
+
|
|
194
|
+
const ds = ${dsJson}
|
|
195
|
+
const cfg = ${cfgJson}
|
|
196
|
+
|
|
197
|
+
// Merge with defaults (same as initEmotion)
|
|
198
|
+
const merged = {}
|
|
199
|
+
for (const k in DEFAULT_CONFIG) merged[k] = DEFAULT_CONFIG[k]
|
|
200
|
+
for (const k in ds) {
|
|
201
|
+
if (typeof ds[k] === 'object' && !Array.isArray(ds[k]) && typeof merged[k] === 'object' && !Array.isArray(merged[k])) {
|
|
202
|
+
merged[k] = { ...merged[k], ...ds[k] }
|
|
203
|
+
} else {
|
|
204
|
+
merged[k] = ds[k]
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const conf = set({
|
|
209
|
+
useReset: true,
|
|
210
|
+
useVariable: true,
|
|
211
|
+
useFontImport: true,
|
|
212
|
+
useDocumentTheme: true,
|
|
213
|
+
useDefaultConfig: true,
|
|
214
|
+
globalTheme: 'auto',
|
|
215
|
+
...merged,
|
|
216
|
+
...cfg
|
|
217
|
+
}, { newConfig: {} })
|
|
218
|
+
|
|
219
|
+
const result = {
|
|
220
|
+
CSS_VARS: conf.CSS_VARS || {},
|
|
221
|
+
CSS_MEDIA_VARS: conf.CSS_MEDIA_VARS || {},
|
|
222
|
+
RESET: conf.RESET || conf.reset || {},
|
|
223
|
+
ANIMATION: conf.animation || conf.ANIMATION || {}
|
|
224
|
+
}
|
|
225
|
+
// Export as globalThis so we can read it
|
|
226
|
+
globalThis.__BR_GLOBAL_CSS__ = result
|
|
227
|
+
export default result
|
|
228
|
+
`);
|
|
229
|
+
const brenderDir = new URL(".", import_meta.url).pathname;
|
|
230
|
+
const monorepoRoot = (0, import_path.resolve)(brenderDir, "../..");
|
|
231
|
+
const workspacePlugin = {
|
|
232
|
+
name: "workspace-resolve",
|
|
233
|
+
setup(build) {
|
|
234
|
+
build.onResolve({ filter: /^@symbo\.ls\// }, (args) => {
|
|
235
|
+
const pkg = args.path.replace("@symbo.ls/", "");
|
|
236
|
+
for (const dir of ["packages", "plugins"]) {
|
|
237
|
+
const src = (0, import_path.resolve)(monorepoRoot, dir, pkg, "src", "index.js");
|
|
238
|
+
if (existsSync(src)) return { path: src };
|
|
239
|
+
const dist = (0, import_path.resolve)(monorepoRoot, dir, pkg, "index.js");
|
|
240
|
+
if (existsSync(dist)) return { path: dist };
|
|
241
|
+
}
|
|
242
|
+
const blank = (0, import_path.resolve)(monorepoRoot, "packages", "default-config", "blank", "index.js");
|
|
243
|
+
if (pkg === "default-config" && existsSync(blank)) return { path: blank };
|
|
244
|
+
});
|
|
245
|
+
build.onResolve({ filter: /^@domql\// }, (args) => {
|
|
246
|
+
const pkg = args.path.replace("@domql/", "");
|
|
247
|
+
const src = (0, import_path.resolve)(monorepoRoot, "packages", "domql", "packages", pkg, "src", "index.js");
|
|
248
|
+
if (existsSync(src)) return { path: src };
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
};
|
|
252
|
+
await esbuild.build({
|
|
253
|
+
entryPoints: [tmpEntry],
|
|
254
|
+
bundle: true,
|
|
255
|
+
format: "esm",
|
|
256
|
+
platform: "node",
|
|
257
|
+
outfile: tmpOut,
|
|
258
|
+
write: true,
|
|
259
|
+
logLevel: "silent",
|
|
260
|
+
plugins: [workspacePlugin],
|
|
261
|
+
external: ["fs", "path", "os", "crypto", "url", "http", "https", "stream", "util", "events", "buffer", "child_process", "worker_threads", "net", "tls", "dns", "dgram", "zlib", "assert", "querystring", "string_decoder", "readline", "perf_hooks", "async_hooks", "v8", "vm", "cluster", "inspector", "module", "process", "tty", "color-contrast-checker"]
|
|
262
|
+
});
|
|
263
|
+
const mod = await import(`file://${tmpOut}`);
|
|
264
|
+
const data = mod.default || {};
|
|
265
|
+
try {
|
|
266
|
+
unlinkSync(tmpEntry);
|
|
267
|
+
} catch {
|
|
268
|
+
}
|
|
269
|
+
try {
|
|
270
|
+
unlinkSync(tmpOut);
|
|
271
|
+
} catch {
|
|
272
|
+
}
|
|
273
|
+
const cssVars = data.CSS_VARS || {};
|
|
274
|
+
const cssMediaVars = data.CSS_MEDIA_VARS || {};
|
|
275
|
+
const reset = data.RESET || {};
|
|
276
|
+
const animations = data.ANIMATION || {};
|
|
277
|
+
const varDecls = Object.entries(cssVars).map(([k, v]) => ` ${k}: ${v}`).join(";\n");
|
|
278
|
+
let rootRule = varDecls ? `:root {
|
|
279
|
+
${varDecls};
|
|
280
|
+
}` : "";
|
|
281
|
+
const themeVarRules = Object.entries(cssMediaVars).map(([key, vars]) => {
|
|
282
|
+
const decls = Object.entries(vars).map(([k, v]) => ` ${k}: ${v}`).join(";\n");
|
|
283
|
+
if (!decls) return "";
|
|
284
|
+
if (key.startsWith("@media")) {
|
|
285
|
+
return `${key} {
|
|
286
|
+
:root:not([data-theme]) {
|
|
287
|
+
${decls};
|
|
288
|
+
}
|
|
289
|
+
}`;
|
|
290
|
+
}
|
|
291
|
+
return `${key} {
|
|
292
|
+
${decls};
|
|
293
|
+
}`;
|
|
294
|
+
}).filter(Boolean).join("\n\n");
|
|
295
|
+
if (themeVarRules) rootRule += "\n\n" + themeVarRules;
|
|
296
|
+
const resetRules = generateResetCSS(reset);
|
|
297
|
+
const keyframeRules = [];
|
|
298
|
+
for (const name in animations) {
|
|
299
|
+
const frames = animations[name];
|
|
300
|
+
if (!frames || typeof frames !== "object") continue;
|
|
301
|
+
const frameRules = Object.entries(frames).map(([step, p]) => {
|
|
302
|
+
if (typeof p !== "object") return "";
|
|
303
|
+
const decls = Object.entries(p).map(([k, v]) => `${camelToKebab(k)}: ${v}`).join("; ");
|
|
304
|
+
return ` ${step} { ${decls}; }`;
|
|
305
|
+
}).join("\n");
|
|
306
|
+
keyframeRules.push(`@keyframes ${name} {
|
|
307
|
+
${frameRules}
|
|
308
|
+
}`);
|
|
309
|
+
}
|
|
310
|
+
_cachedGlobalCSS = {
|
|
311
|
+
rootRule,
|
|
312
|
+
resetRules,
|
|
313
|
+
fontFaceCSS: "",
|
|
314
|
+
keyframeRules: keyframeRules.join("\n")
|
|
315
|
+
};
|
|
316
|
+
return _cachedGlobalCSS;
|
|
317
|
+
} catch (err) {
|
|
318
|
+
console.warn("generateGlobalCSS failed:", err.message, err.stack);
|
|
319
|
+
_cachedGlobalCSS = { rootRule: "", resetRules: "", fontFaceCSS: "", keyframeRules: "" };
|
|
320
|
+
return _cachedGlobalCSS;
|
|
321
|
+
}
|
|
322
|
+
};
|
|
323
|
+
const resetGlobalCSSCache = () => {
|
|
324
|
+
_cachedGlobalCSS = null;
|
|
325
|
+
};
|
|
156
326
|
const renderRoute = async (data, options = {}) => {
|
|
157
327
|
const { route = "/" } = options;
|
|
158
328
|
const ds = data.designSystem || {};
|
|
@@ -182,34 +352,66 @@ const renderRoute = async (data, options = {}) => {
|
|
|
182
352
|
emotion: emotionInstance,
|
|
183
353
|
designSystem: ds
|
|
184
354
|
});
|
|
355
|
+
const globalCSS = await generateGlobalCSS(ds, data.config || data.settings);
|
|
185
356
|
return {
|
|
186
357
|
html: cssDoc.body.innerHTML,
|
|
187
358
|
css: extractCSS(result.element, ds),
|
|
188
|
-
|
|
359
|
+
globalCSS,
|
|
360
|
+
resetCss: globalCSS.resetRules || generateResetCSS(ds.reset),
|
|
189
361
|
fontLinks: generateFontLinks(ds),
|
|
190
362
|
metadata: (0, import_metadata.extractMetadata)(data, route),
|
|
191
363
|
brKeyCount: Object.keys(result.registry).length
|
|
192
364
|
};
|
|
193
365
|
};
|
|
194
366
|
const renderPage = async (data, route = "/", options = {}) => {
|
|
195
|
-
const { lang = "en", themeColor } = options;
|
|
367
|
+
const { lang = "en", themeColor, isr } = options;
|
|
196
368
|
const result = await renderRoute(data, { route });
|
|
197
369
|
if (!result) return null;
|
|
198
370
|
const metadata = { ...result.metadata };
|
|
199
371
|
if (themeColor) metadata["theme-color"] = themeColor;
|
|
200
372
|
const headTags = (0, import_metadata.generateHeadHtml)(metadata);
|
|
373
|
+
const globalCSS = result.globalCSS || {};
|
|
374
|
+
let isrBody = "";
|
|
375
|
+
if (isr && isr.clientScript) {
|
|
376
|
+
const depth = route === "/" ? 0 : route.replace(/^\/|\/$/g, "").split("/").length;
|
|
377
|
+
const prefix = depth > 0 ? "../".repeat(depth) : "./";
|
|
378
|
+
isrBody = `<script type="module">
|
|
379
|
+
{
|
|
380
|
+
const brEls = document.querySelectorAll('body > :not(script):not(style)')
|
|
381
|
+
const observer = new MutationObserver((mutations) => {
|
|
382
|
+
for (const m of mutations) {
|
|
383
|
+
for (const node of m.addedNodes) {
|
|
384
|
+
if (node.nodeType === 1 && node.tagName !== 'SCRIPT' && node.tagName !== 'STYLE' && !node.hasAttribute('data-br')) {
|
|
385
|
+
brEls.forEach(el => { if (el.hasAttribute('data-br') || el.querySelector('[data-br]')) el.remove() })
|
|
386
|
+
observer.disconnect()
|
|
387
|
+
return
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
})
|
|
392
|
+
observer.observe(document.body, { childList: true })
|
|
393
|
+
}
|
|
394
|
+
<\/script>
|
|
395
|
+
<script type="module" src="${prefix}${isr.clientScript}"><\/script>`;
|
|
396
|
+
}
|
|
201
397
|
const html = `<!DOCTYPE html>
|
|
202
398
|
<html lang="${lang}">
|
|
203
399
|
<head>
|
|
204
400
|
${headTags}
|
|
205
401
|
${result.fontLinks}
|
|
206
|
-
|
|
402
|
+
${globalCSS.fontFaceCSS ? `<style>${globalCSS.fontFaceCSS}</style>` : ""}
|
|
403
|
+
<style>
|
|
404
|
+
${globalCSS.rootRule || ""}
|
|
405
|
+
${result.resetCss}
|
|
406
|
+
${globalCSS.keyframeRules || ""}
|
|
407
|
+
</style>
|
|
207
408
|
<style data-emotion="smbls">
|
|
208
409
|
${result.css}
|
|
209
410
|
</style>
|
|
210
411
|
</head>
|
|
211
412
|
<body>
|
|
212
413
|
${result.html}
|
|
414
|
+
${isrBody}
|
|
213
415
|
</body>
|
|
214
416
|
</html>`;
|
|
215
417
|
return { html, route, brKeyCount: result.brKeyCount };
|
|
@@ -538,6 +740,34 @@ const getExtendsCSS = (el) => {
|
|
|
538
740
|
}
|
|
539
741
|
return null;
|
|
540
742
|
};
|
|
743
|
+
const resolveElementProps = (el) => {
|
|
744
|
+
const { props } = el;
|
|
745
|
+
if (!props) return props;
|
|
746
|
+
let resolved;
|
|
747
|
+
for (const key in props) {
|
|
748
|
+
if (typeof props[key] !== "function") continue;
|
|
749
|
+
if (NON_CSS_PROPS.has(key)) continue;
|
|
750
|
+
if (key.charCodeAt(0) >= 65 && key.charCodeAt(0) <= 90) continue;
|
|
751
|
+
if (key.startsWith("on")) continue;
|
|
752
|
+
if (!resolved) resolved = { ...props };
|
|
753
|
+
let result;
|
|
754
|
+
try {
|
|
755
|
+
result = props[key](el, el.state || {});
|
|
756
|
+
} catch {
|
|
757
|
+
try {
|
|
758
|
+
const mockState = { root: {}, ...el.state || {} };
|
|
759
|
+
result = props[key](el, mockState);
|
|
760
|
+
} catch {
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
if (result !== void 0 && result !== null && result !== false) {
|
|
764
|
+
resolved[key] = result;
|
|
765
|
+
} else {
|
|
766
|
+
delete resolved[key];
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
return resolved || props;
|
|
770
|
+
};
|
|
541
771
|
const extractCSS = (element, ds) => {
|
|
542
772
|
const mediaMap = ds?.media || {};
|
|
543
773
|
const animations = ds?.animation || {};
|
|
@@ -546,7 +776,7 @@ const extractCSS = (element, ds) => {
|
|
|
546
776
|
const usedAnimations = /* @__PURE__ */ new Set();
|
|
547
777
|
const walk = (el) => {
|
|
548
778
|
if (!el || !el.__ref) return;
|
|
549
|
-
const
|
|
779
|
+
const props = resolveElementProps(el);
|
|
550
780
|
if (props && el.node) {
|
|
551
781
|
const cls = el.node.getAttribute?.("class");
|
|
552
782
|
if (cls && !seen.has(cls)) {
|
|
@@ -568,7 +798,7 @@ const extractCSS = (element, ds) => {
|
|
|
568
798
|
}
|
|
569
799
|
}
|
|
570
800
|
}
|
|
571
|
-
if (el.__ref
|
|
801
|
+
if (el.__ref?.__children) {
|
|
572
802
|
for (const ck of el.__ref.__children) {
|
|
573
803
|
if (el[ck]?.__ref) walk(el[ck]);
|
|
574
804
|
}
|
|
@@ -593,8 +823,20 @@ const generateResetCSS = (reset) => {
|
|
|
593
823
|
const rules = [];
|
|
594
824
|
for (const [selector, props] of Object.entries(reset)) {
|
|
595
825
|
if (!props || typeof props !== "object") continue;
|
|
596
|
-
const
|
|
597
|
-
|
|
826
|
+
const baseDecls = [];
|
|
827
|
+
const mediaRules = [];
|
|
828
|
+
for (const [k, v] of Object.entries(props)) {
|
|
829
|
+
if (typeof v === "object" && v !== null) {
|
|
830
|
+
if (k.startsWith("@media") || k.startsWith("@")) {
|
|
831
|
+
const inner = Object.entries(v).filter(([, iv]) => typeof iv !== "object").map(([ik, iv]) => `${camelToKebab(ik)}: ${iv}`).join("; ");
|
|
832
|
+
if (inner) mediaRules.push(`${k} { ${selector} { ${inner}; } }`);
|
|
833
|
+
}
|
|
834
|
+
continue;
|
|
835
|
+
}
|
|
836
|
+
baseDecls.push(`${camelToKebab(k)}: ${v}`);
|
|
837
|
+
}
|
|
838
|
+
if (baseDecls.length) rules.push(`${selector} { ${baseDecls.join("; ")}; }`);
|
|
839
|
+
rules.push(...mediaRules);
|
|
598
840
|
}
|
|
599
841
|
return rules.join("\n");
|
|
600
842
|
};
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
var __defProp = Object.defineProperty;
|
|
2
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
3
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
4
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
5
|
+
var __export = (target, all) => {
|
|
6
|
+
for (var name in all)
|
|
7
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
8
|
+
};
|
|
9
|
+
var __copyProps = (to, from, except, desc) => {
|
|
10
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
11
|
+
for (let key of __getOwnPropNames(from))
|
|
12
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
13
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
14
|
+
}
|
|
15
|
+
return to;
|
|
16
|
+
};
|
|
17
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
18
|
+
var sitemap_exports = {};
|
|
19
|
+
__export(sitemap_exports, {
|
|
20
|
+
generateSitemap: () => generateSitemap
|
|
21
|
+
});
|
|
22
|
+
module.exports = __toCommonJS(sitemap_exports);
|
|
23
|
+
function generateSitemap(baseUrl, routes) {
|
|
24
|
+
const urls = Object.entries(routes).map(([path, config]) => {
|
|
25
|
+
const metadata = config.metadata || {};
|
|
26
|
+
const canonical = metadata.canonical || `${baseUrl}${path === "/" ? "" : path}`;
|
|
27
|
+
return `
|
|
28
|
+
<url>
|
|
29
|
+
<loc>${canonical}</loc>
|
|
30
|
+
<lastmod>${(/* @__PURE__ */ new Date()).toISOString()}</lastmod>
|
|
31
|
+
<changefreq>weekly</changefreq>
|
|
32
|
+
<priority>${path === "/" ? "1.0" : "0.8"}</priority>
|
|
33
|
+
</url>`;
|
|
34
|
+
});
|
|
35
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
36
|
+
<urlset
|
|
37
|
+
xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"
|
|
38
|
+
xmlns:xhtml="http://www.w3.org/1999/xhtml">
|
|
39
|
+
${urls.join("\n")}
|
|
40
|
+
</urlset>`;
|
|
41
|
+
}
|
package/dist/esm/index.js
CHANGED
|
@@ -4,6 +4,7 @@ import { loadProject, loadAndRenderAll } from "./load.js";
|
|
|
4
4
|
import { render, renderElement, renderRoute, renderPage } from "./render.js";
|
|
5
5
|
import { extractMetadata, generateHeadHtml } from "./metadata.js";
|
|
6
6
|
import { collectBrNodes, hydrate } from "./hydrate.js";
|
|
7
|
+
import { generateSitemap } from "./sitemap.js";
|
|
7
8
|
var index_default = {
|
|
8
9
|
createEnv,
|
|
9
10
|
resetKeys,
|
|
@@ -18,7 +19,8 @@ var index_default = {
|
|
|
18
19
|
extractMetadata,
|
|
19
20
|
generateHeadHtml,
|
|
20
21
|
collectBrNodes,
|
|
21
|
-
hydrate
|
|
22
|
+
hydrate,
|
|
23
|
+
generateSitemap
|
|
22
24
|
};
|
|
23
25
|
export {
|
|
24
26
|
assignKeys,
|
|
@@ -27,6 +29,7 @@ export {
|
|
|
27
29
|
index_default as default,
|
|
28
30
|
extractMetadata,
|
|
29
31
|
generateHeadHtml,
|
|
32
|
+
generateSitemap,
|
|
30
33
|
hydrate,
|
|
31
34
|
loadAndRenderAll,
|
|
32
35
|
loadProject,
|
package/dist/esm/metadata.js
CHANGED
|
@@ -1,83 +1,7 @@
|
|
|
1
|
-
|
|
2
|
-
const pages = data.pages || {};
|
|
3
|
-
const page = pages[route];
|
|
4
|
-
let metadata = {};
|
|
5
|
-
if (data.integrations?.seo) {
|
|
6
|
-
metadata = { ...data.integrations.seo };
|
|
7
|
-
}
|
|
8
|
-
if (page) {
|
|
9
|
-
const pageMeta = page.metadata || page.helmet || {};
|
|
10
|
-
metadata = { ...metadata, ...pageMeta };
|
|
11
|
-
if (!metadata.title && page.state?.title) {
|
|
12
|
-
metadata.title = page.state.title;
|
|
13
|
-
}
|
|
14
|
-
if (!metadata.description && page.state?.description) {
|
|
15
|
-
metadata.description = page.state.description;
|
|
16
|
-
}
|
|
17
|
-
}
|
|
18
|
-
if (!metadata.title) {
|
|
19
|
-
metadata.title = data.name || "Symbols";
|
|
20
|
-
}
|
|
21
|
-
return metadata;
|
|
22
|
-
};
|
|
23
|
-
const generateHeadHtml = (metadata) => {
|
|
24
|
-
const esc = (text) => {
|
|
25
|
-
if (text === null || text === void 0) return "";
|
|
26
|
-
const map = { "&": "&", "<": "<", ">": ">", '"': """, "'": "'" };
|
|
27
|
-
return text.toString().replace(/[&<>"']/g, (m) => map[m]);
|
|
28
|
-
};
|
|
29
|
-
const tags = [
|
|
30
|
-
'<meta charset="UTF-8">',
|
|
31
|
-
'<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1, maximum-scale=1, user-scalable=no">'
|
|
32
|
-
];
|
|
33
|
-
for (const [key, value] of Object.entries(metadata)) {
|
|
34
|
-
if (!value && value !== 0 && value !== false) continue;
|
|
35
|
-
if (key === "title") {
|
|
36
|
-
tags.push(`<title>${esc(value)}</title>`);
|
|
37
|
-
continue;
|
|
38
|
-
}
|
|
39
|
-
if (key === "canonical") {
|
|
40
|
-
tags.push(`<link rel="canonical" href="${esc(value)}">`);
|
|
41
|
-
continue;
|
|
42
|
-
}
|
|
43
|
-
if (key === "alternate" && Array.isArray(value)) {
|
|
44
|
-
value.forEach((alt) => {
|
|
45
|
-
if (typeof alt === "object") {
|
|
46
|
-
const attrs = Object.entries(alt).map(([k, v]) => `${k}="${esc(v)}"`).join(" ");
|
|
47
|
-
tags.push(`<link rel="alternate" ${attrs}>`);
|
|
48
|
-
}
|
|
49
|
-
});
|
|
50
|
-
continue;
|
|
51
|
-
}
|
|
52
|
-
const propertyPrefixes = ["og:", "article:", "product:", "fb:", "profile:", "book:", "business:", "music:", "video:"];
|
|
53
|
-
const namePrefixes = ["twitter:", "DC:", "DCTERMS:"];
|
|
54
|
-
const isProperty = propertyPrefixes.some((p) => key.startsWith(p));
|
|
55
|
-
const isName = namePrefixes.some((p) => key.startsWith(p));
|
|
56
|
-
if (key.startsWith("http-equiv:")) {
|
|
57
|
-
const httpKey = key.replace("http-equiv:", "");
|
|
58
|
-
tags.push(`<meta http-equiv="${esc(httpKey)}" content="${esc(value)}">`);
|
|
59
|
-
} else if (key.startsWith("itemprop:")) {
|
|
60
|
-
const itemKey = key.replace("itemprop:", "");
|
|
61
|
-
tags.push(`<meta itemprop="${esc(itemKey)}" content="${esc(value)}">`);
|
|
62
|
-
} else if (isProperty) {
|
|
63
|
-
if (Array.isArray(value)) {
|
|
64
|
-
value.forEach((v) => tags.push(`<meta property="${esc(key)}" content="${esc(v)}">`));
|
|
65
|
-
} else {
|
|
66
|
-
tags.push(`<meta property="${esc(key)}" content="${esc(value)}">`);
|
|
67
|
-
}
|
|
68
|
-
} else if (isName) {
|
|
69
|
-
tags.push(`<meta name="${esc(key)}" content="${esc(value)}">`);
|
|
70
|
-
} else if (key !== "favicon" && key !== "favicons") {
|
|
71
|
-
if (Array.isArray(value)) {
|
|
72
|
-
value.forEach((v) => tags.push(`<meta name="${esc(key)}" content="${esc(v)}">`));
|
|
73
|
-
} else if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
|
|
74
|
-
tags.push(`<meta name="${esc(key)}" content="${esc(value)}">`);
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
return tags.join("\n");
|
|
79
|
-
};
|
|
1
|
+
import { extractMetadata, generateHeadHtml, resolveMetadata, applyMetadata } from "@symbo.ls/helmet";
|
|
80
2
|
export {
|
|
3
|
+
applyMetadata,
|
|
81
4
|
extractMetadata,
|
|
82
|
-
generateHeadHtml
|
|
5
|
+
generateHeadHtml,
|
|
6
|
+
resolveMetadata
|
|
83
7
|
};
|