decantr 0.9.0
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/AGENTS.md +868 -0
- package/CHANGELOG.md +255 -0
- package/CLAUDE.md +178 -0
- package/LICENSE +21 -0
- package/README.md +229 -0
- package/cli/art.js +127 -0
- package/cli/commands/a11y.js +61 -0
- package/cli/commands/audit.js +225 -0
- package/cli/commands/build.js +38 -0
- package/cli/commands/dev.js +18 -0
- package/cli/commands/doctor.js +197 -0
- package/cli/commands/figma-sync.js +48 -0
- package/cli/commands/figma-tokens.js +55 -0
- package/cli/commands/generate.js +26 -0
- package/cli/commands/init.js +116 -0
- package/cli/commands/lint.js +209 -0
- package/cli/commands/mcp.js +530 -0
- package/cli/commands/migrate.js +175 -0
- package/cli/commands/test.js +38 -0
- package/cli/commands/validate.js +354 -0
- package/cli/index.js +113 -0
- package/package.json +95 -0
- package/reference/atoms.md +517 -0
- package/reference/behaviors.md +384 -0
- package/reference/build-tooling.md +275 -0
- package/reference/color-guidelines.md +965 -0
- package/reference/component-lifecycle.md +137 -0
- package/reference/compound-spacing.md +95 -0
- package/reference/decantation-process.md +499 -0
- package/reference/dev-server-routes.md +93 -0
- package/reference/form-system.md +253 -0
- package/reference/i18n.md +336 -0
- package/reference/icons.md +576 -0
- package/reference/llm-primer.md +953 -0
- package/reference/plugins.md +252 -0
- package/reference/registry-consumption.md +76 -0
- package/reference/router.md +217 -0
- package/reference/shells.md +116 -0
- package/reference/spatial-guidelines.md +541 -0
- package/reference/ssr.md +234 -0
- package/reference/state-data.md +215 -0
- package/reference/state-patterns.md +166 -0
- package/reference/state.md +194 -0
- package/reference/style-system.md +110 -0
- package/reference/tokens.md +460 -0
- package/src/app.js +19 -0
- package/src/chart/_animate.js +266 -0
- package/src/chart/_base.js +109 -0
- package/src/chart/_data.js +209 -0
- package/src/chart/_format.js +106 -0
- package/src/chart/_interact.js +364 -0
- package/src/chart/_palette.js +105 -0
- package/src/chart/_renderer.js +52 -0
- package/src/chart/_scene.js +262 -0
- package/src/chart/_shared.js +371 -0
- package/src/chart/index.js +637 -0
- package/src/chart/layouts/_layout-base.js +328 -0
- package/src/chart/layouts/cartesian.js +148 -0
- package/src/chart/layouts/hierarchy.js +562 -0
- package/src/chart/layouts/polar.js +101 -0
- package/src/chart/renderers/canvas.js +179 -0
- package/src/chart/renderers/svg.js +256 -0
- package/src/chart/renderers/webgpu.js +715 -0
- package/src/chart/types/_type-base.js +26 -0
- package/src/chart/types/area.js +134 -0
- package/src/chart/types/bar.js +173 -0
- package/src/chart/types/box-plot.js +125 -0
- package/src/chart/types/bubble.js +63 -0
- package/src/chart/types/candlestick.js +115 -0
- package/src/chart/types/chord.js +85 -0
- package/src/chart/types/combination.js +108 -0
- package/src/chart/types/funnel.js +68 -0
- package/src/chart/types/gauge.js +163 -0
- package/src/chart/types/heatmap.js +98 -0
- package/src/chart/types/histogram.js +71 -0
- package/src/chart/types/line.js +111 -0
- package/src/chart/types/org-chart.js +93 -0
- package/src/chart/types/pie.js +81 -0
- package/src/chart/types/radar.js +96 -0
- package/src/chart/types/radial.js +68 -0
- package/src/chart/types/range-area.js +55 -0
- package/src/chart/types/range-bar.js +61 -0
- package/src/chart/types/sankey.js +73 -0
- package/src/chart/types/scatter.js +66 -0
- package/src/chart/types/sparkline.js +81 -0
- package/src/chart/types/sunburst.js +69 -0
- package/src/chart/types/swimlane.js +88 -0
- package/src/chart/types/treemap.js +62 -0
- package/src/chart/types/waterfall.js +100 -0
- package/src/components/_base.js +1658 -0
- package/src/components/_behaviors.js +1140 -0
- package/src/components/_primitives.js +534 -0
- package/src/components/_qr-encoder.js +539 -0
- package/src/components/accordion.js +207 -0
- package/src/components/affix.js +62 -0
- package/src/components/alert-dialog.js +75 -0
- package/src/components/alert.js +47 -0
- package/src/components/aspect-ratio.js +24 -0
- package/src/components/avatar-group.js +55 -0
- package/src/components/avatar.js +38 -0
- package/src/components/back-top.js +75 -0
- package/src/components/badge.js +74 -0
- package/src/components/banner.js +68 -0
- package/src/components/breadcrumb.js +162 -0
- package/src/components/button.js +115 -0
- package/src/components/calendar.js +131 -0
- package/src/components/card.js +192 -0
- package/src/components/carousel.js +98 -0
- package/src/components/cascader.js +261 -0
- package/src/components/checkbox.js +80 -0
- package/src/components/chip.js +81 -0
- package/src/components/code-block.js +82 -0
- package/src/components/collapsible.js +50 -0
- package/src/components/color-palette.js +438 -0
- package/src/components/color-picker.js +314 -0
- package/src/components/combobox.js +181 -0
- package/src/components/command.js +174 -0
- package/src/components/comment.js +206 -0
- package/src/components/context-menu.js +76 -0
- package/src/components/data-table.js +724 -0
- package/src/components/date-picker.js +217 -0
- package/src/components/date-range-picker.js +244 -0
- package/src/components/datetime-picker.js +271 -0
- package/src/components/descriptions.js +68 -0
- package/src/components/drawer.js +179 -0
- package/src/components/dropdown.js +88 -0
- package/src/components/empty.js +41 -0
- package/src/components/float-button.js +90 -0
- package/src/components/form.js +106 -0
- package/src/components/hover-card.js +49 -0
- package/src/components/icon.js +87 -0
- package/src/components/image.js +97 -0
- package/src/components/index.js +117 -0
- package/src/components/input-group.js +75 -0
- package/src/components/input-number.js +155 -0
- package/src/components/input-otp.js +178 -0
- package/src/components/input.js +91 -0
- package/src/components/kbd.js +36 -0
- package/src/components/label.js +25 -0
- package/src/components/list.js +118 -0
- package/src/components/masked-input.js +236 -0
- package/src/components/mentions.js +165 -0
- package/src/components/menu.js +259 -0
- package/src/components/message.js +80 -0
- package/src/components/modal.js +147 -0
- package/src/components/navigation-menu.js +166 -0
- package/src/components/notification.js +84 -0
- package/src/components/pagination.js +104 -0
- package/src/components/placeholder.js +132 -0
- package/src/components/popconfirm.js +70 -0
- package/src/components/popover.js +58 -0
- package/src/components/progress.js +61 -0
- package/src/components/qrcode.js +251 -0
- package/src/components/radiogroup.js +120 -0
- package/src/components/range-slider.js +176 -0
- package/src/components/rate.js +186 -0
- package/src/components/resizable.js +83 -0
- package/src/components/result.js +57 -0
- package/src/components/scroll-area.js +43 -0
- package/src/components/segmented.js +97 -0
- package/src/components/select.js +165 -0
- package/src/components/separator.js +31 -0
- package/src/components/shell.js +407 -0
- package/src/components/skeleton.js +39 -0
- package/src/components/slider.js +141 -0
- package/src/components/sortable-list.js +176 -0
- package/src/components/space.js +42 -0
- package/src/components/spinner.js +112 -0
- package/src/components/splitter.js +147 -0
- package/src/components/statistic.js +136 -0
- package/src/components/steps.js +99 -0
- package/src/components/switch.js +95 -0
- package/src/components/table.js +44 -0
- package/src/components/tabs.js +216 -0
- package/src/components/tag.js +115 -0
- package/src/components/textarea.js +82 -0
- package/src/components/time-picker.js +153 -0
- package/src/components/time-range-picker.js +170 -0
- package/src/components/timeline.js +226 -0
- package/src/components/toast.js +71 -0
- package/src/components/toggle.js +213 -0
- package/src/components/tooltip.js +57 -0
- package/src/components/tour.js +159 -0
- package/src/components/transfer.js +163 -0
- package/src/components/tree-select.js +274 -0
- package/src/components/tree.js +141 -0
- package/src/components/typography.js +136 -0
- package/src/components/upload.js +118 -0
- package/src/components/visually-hidden.js +20 -0
- package/src/components/watermark.js +124 -0
- package/src/core/index.js +539 -0
- package/src/core/lifecycle.js +69 -0
- package/src/css/atoms.js +651 -0
- package/src/css/components.js +940 -0
- package/src/css/derive.js +1296 -0
- package/src/css/index.js +265 -0
- package/src/css/runtime.js +268 -0
- package/src/css/styles/addons/bioluminescent.js +93 -0
- package/src/css/styles/addons/clay.js +70 -0
- package/src/css/styles/addons/clean.js +57 -0
- package/src/css/styles/addons/command-center.js +143 -0
- package/src/css/styles/addons/dopamine.js +83 -0
- package/src/css/styles/addons/editorial.js +80 -0
- package/src/css/styles/addons/glassmorphism.js +99 -0
- package/src/css/styles/addons/liquid-glass.js +105 -0
- package/src/css/styles/addons/prismatic.js +100 -0
- package/src/css/styles/addons/retro.js +63 -0
- package/src/css/styles/auradecantism.js +96 -0
- package/src/css/theme-registry.js +444 -0
- package/src/data/entity.js +281 -0
- package/src/data/index.js +13 -0
- package/src/data/persist.js +225 -0
- package/src/data/query.js +839 -0
- package/src/data/realtime.js +299 -0
- package/src/data/url.js +177 -0
- package/src/data/worker.js +134 -0
- package/src/explorer/archetypes.js +243 -0
- package/src/explorer/atoms.js +228 -0
- package/src/explorer/charts.js +497 -0
- package/src/explorer/components.js +129 -0
- package/src/explorer/foundations.js +949 -0
- package/src/explorer/icons.js +178 -0
- package/src/explorer/patterns.js +247 -0
- package/src/explorer/recipes.js +194 -0
- package/src/explorer/shared/pattern-examples.js +1337 -0
- package/src/explorer/shared/showcase-renderer.js +958 -0
- package/src/explorer/shared/spec-table.js +41 -0
- package/src/explorer/shared/usage-links.js +87 -0
- package/src/explorer/shell-config.js +10 -0
- package/src/explorer/shells.js +551 -0
- package/src/explorer/styles.js +161 -0
- package/src/explorer/tokens.js +262 -0
- package/src/explorer/tools.js +525 -0
- package/src/form/index.js +804 -0
- package/src/i18n/index.js +251 -0
- package/src/icons/essential.js +479 -0
- package/src/icons/index.js +53 -0
- package/src/plugins/index.js +282 -0
- package/src/registry/archetypes/content-site.json +71 -0
- package/src/registry/archetypes/docs-explorer.json +23 -0
- package/src/registry/archetypes/ecommerce.json +104 -0
- package/src/registry/archetypes/financial-dashboard.json +77 -0
- package/src/registry/archetypes/index.json +41 -0
- package/src/registry/archetypes/portfolio.json +82 -0
- package/src/registry/archetypes/recipe-community.json +159 -0
- package/src/registry/archetypes/saas-dashboard.json +86 -0
- package/src/registry/architect/cross-cutting.json +45 -0
- package/src/registry/architect/domains/ecommerce.json +294 -0
- package/src/registry/architect/domains/financial-services.json +302 -0
- package/src/registry/architect/index.json +26 -0
- package/src/registry/architect/traits.json +379 -0
- package/src/registry/atoms.json +16 -0
- package/src/registry/chart-showcase.json +160 -0
- package/src/registry/chart.json +136 -0
- package/src/registry/components.json +8616 -0
- package/src/registry/core.json +216 -0
- package/src/registry/css.json +319 -0
- package/src/registry/data.json +135 -0
- package/src/registry/foundations.json +11 -0
- package/src/registry/icons.json +463 -0
- package/src/registry/index.json +101 -0
- package/src/registry/patterns/activity-feed.json +37 -0
- package/src/registry/patterns/article-content.json +27 -0
- package/src/registry/patterns/auth-form.json +37 -0
- package/src/registry/patterns/author-card.json +20 -0
- package/src/registry/patterns/card-grid.json +127 -0
- package/src/registry/patterns/category-nav.json +26 -0
- package/src/registry/patterns/chart-grid.json +36 -0
- package/src/registry/patterns/chat-interface.json +37 -0
- package/src/registry/patterns/checklist-card.json +55 -0
- package/src/registry/patterns/comparison-panel.json +27 -0
- package/src/registry/patterns/component-showcase.json +24 -0
- package/src/registry/patterns/contact-form.json +31 -0
- package/src/registry/patterns/cta-section.json +20 -0
- package/src/registry/patterns/data-table.json +37 -0
- package/src/registry/patterns/detail-header.json +83 -0
- package/src/registry/patterns/detail-panel.json +27 -0
- package/src/registry/patterns/explorer-shell.json +22 -0
- package/src/registry/patterns/filter-bar.json +33 -0
- package/src/registry/patterns/filter-sidebar.json +27 -0
- package/src/registry/patterns/form-sections.json +110 -0
- package/src/registry/patterns/goal-tracker.json +27 -0
- package/src/registry/patterns/hero.json +107 -0
- package/src/registry/patterns/index.json +47 -0
- package/src/registry/patterns/kpi-grid.json +36 -0
- package/src/registry/patterns/media-gallery.json +20 -0
- package/src/registry/patterns/order-history.json +20 -0
- package/src/registry/patterns/pagination.json +19 -0
- package/src/registry/patterns/photo-to-recipe.json +36 -0
- package/src/registry/patterns/pipeline-tracker.json +28 -0
- package/src/registry/patterns/post-list.json +27 -0
- package/src/registry/patterns/pricing-table.json +32 -0
- package/src/registry/patterns/scorecard.json +28 -0
- package/src/registry/patterns/search-bar.json +20 -0
- package/src/registry/patterns/specimen-grid.json +19 -0
- package/src/registry/patterns/stat-card.json +55 -0
- package/src/registry/patterns/stats-bar.json +55 -0
- package/src/registry/patterns/steps-card.json +55 -0
- package/src/registry/patterns/table-of-contents.json +19 -0
- package/src/registry/patterns/testimonials.json +21 -0
- package/src/registry/patterns/timeline.json +27 -0
- package/src/registry/patterns/token-inspector.json +21 -0
- package/src/registry/patterns/wizard.json +27 -0
- package/src/registry/recipe-auradecantism.json +69 -0
- package/src/registry/recipe-clean.json +65 -0
- package/src/registry/recipe-command-center.json +78 -0
- package/src/registry/router.json +73 -0
- package/src/registry/schema/README.md +197 -0
- package/src/registry/skeletons.json +259 -0
- package/src/registry/state.json +137 -0
- package/src/registry/tokens.json +40 -0
- package/src/router/hash.js +17 -0
- package/src/router/history.js +18 -0
- package/src/router/index.js +598 -0
- package/src/ssr/index.js +922 -0
- package/src/state/arrays.js +181 -0
- package/src/state/devtools.js +647 -0
- package/src/state/index.js +498 -0
- package/src/state/middleware.js +288 -0
- package/src/state/scheduler.js +206 -0
- package/src/state/store.js +300 -0
- package/src/tags/index.js +19 -0
- package/src/tannins/auth.js +396 -0
- package/src/test/dom.js +352 -0
- package/src/test/index.js +62 -0
- package/src/test/state.js +306 -0
- package/tools/a11y-audit.js +487 -0
- package/tools/analyzer.js +315 -0
- package/tools/audit.js +706 -0
- package/tools/builder.js +1422 -0
- package/tools/css-extract.js +188 -0
- package/tools/dev-server.js +316 -0
- package/tools/dts-gen.js +1260 -0
- package/tools/figma-components.js +329 -0
- package/tools/figma-patterns.js +516 -0
- package/tools/figma-plugin/code.js +453 -0
- package/tools/figma-plugin/manifest.json +14 -0
- package/tools/figma-plugin/ui.html +268 -0
- package/tools/figma-render.js +293 -0
- package/tools/figma-tokens.js +712 -0
- package/tools/figma-upload.js +318 -0
- package/tools/generate.js +738 -0
- package/tools/icons.js +133 -0
- package/tools/init-templates.js +265 -0
- package/tools/install-hooks.sh +5 -0
- package/tools/migrations/0.5.0.js +53 -0
- package/tools/migrations/0.6.0.js +95 -0
- package/tools/minify.js +170 -0
- package/tools/pre-commit +4 -0
- package/tools/registry.js +662 -0
- package/tools/reset-playground.js +61 -0
- package/tools/starter-templates/content-site/app.js +49 -0
- package/tools/starter-templates/content-site/essence.js +19 -0
- package/tools/starter-templates/content-site/pages.js +31 -0
- package/tools/starter-templates/ecommerce/app.js +50 -0
- package/tools/starter-templates/ecommerce/essence.js +19 -0
- package/tools/starter-templates/ecommerce/pages.js +31 -0
- package/tools/starter-templates/landing-page/app.js +38 -0
- package/tools/starter-templates/landing-page/essence.js +18 -0
- package/tools/starter-templates/landing-page/pages.js +21 -0
- package/tools/starter-templates/portfolio/app.js +45 -0
- package/tools/starter-templates/portfolio/essence.js +19 -0
- package/tools/starter-templates/portfolio/pages.js +33 -0
- package/tools/starter-templates/saas-dashboard/app.js +70 -0
- package/tools/starter-templates/saas-dashboard/essence.js +19 -0
- package/tools/starter-templates/saas-dashboard/pages.js +31 -0
- package/tools/verify-pack.js +203 -0
- package/types/chart.d.ts +77 -0
- package/types/components.d.ts +587 -0
- package/types/core.d.ts +89 -0
- package/types/css.d.ts +149 -0
- package/types/data.d.ts +238 -0
- package/types/form.d.ts +164 -0
- package/types/i18n.d.ts +51 -0
- package/types/icons.d.ts +27 -0
- package/types/index.d.ts +13 -0
- package/types/router.d.ts +116 -0
- package/types/ssr.d.ts +102 -0
- package/types/state.d.ts +83 -0
- package/types/tags.d.ts +62 -0
- package/types/tannins.d.ts +63 -0
- package/types/test.d.ts +48 -0
package/src/ssr/index.js
ADDED
|
@@ -0,0 +1,922 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Decantr SSR — Server-Side Rendering + Hydration
|
|
3
|
+
*
|
|
4
|
+
* Separate entry point that works in Node.js without DOM globals.
|
|
5
|
+
* renderToString/renderToStream build a VNode tree and serialize to HTML.
|
|
6
|
+
* hydrate() walks existing DOM and attaches signal bindings + event listeners.
|
|
7
|
+
*
|
|
8
|
+
* IMPORTANT: This module must NOT import `document` at module level.
|
|
9
|
+
* renderToString and renderToStream work in pure Node.js.
|
|
10
|
+
* hydrate() requires a browser environment (it operates on existing DOM).
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { resolveAtomDecl, ALIASES } from '../css/atoms.js';
|
|
14
|
+
|
|
15
|
+
// ─── SSR Atom Resolution (no DOM injection) ────────────────────
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Resolve atom classes for SSR without injecting CSS into DOM.
|
|
19
|
+
* Mirrors the logic of css() from ../css/index.js but skips inject().
|
|
20
|
+
* @param {...string} classes
|
|
21
|
+
* @returns {string}
|
|
22
|
+
*/
|
|
23
|
+
function ssrCss(...classes) {
|
|
24
|
+
const result = [];
|
|
25
|
+
for (let i = 0; i < classes.length; i++) {
|
|
26
|
+
const cls = classes[i];
|
|
27
|
+
if (!cls) continue;
|
|
28
|
+
const parts = cls.split(/\s+/);
|
|
29
|
+
for (const part of parts) {
|
|
30
|
+
if (!part) continue;
|
|
31
|
+
if (part === '_group') { result.push('d-group'); continue; }
|
|
32
|
+
if (part === '_peer') { result.push('d-peer'); continue; }
|
|
33
|
+
// For SSR we pass through all class names — CSS is extracted at build time
|
|
34
|
+
// or injected by the client-side runtime during hydration.
|
|
35
|
+
result.push(part);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return result.join(' ');
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// ─── HTML Escaping ──────────────────────────────────────────────
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Escape HTML special characters in text content.
|
|
45
|
+
* @param {string} str
|
|
46
|
+
* @returns {string}
|
|
47
|
+
*/
|
|
48
|
+
function escapeHTML(str) {
|
|
49
|
+
if (typeof str !== 'string') return '';
|
|
50
|
+
return str
|
|
51
|
+
.replace(/&/g, '&')
|
|
52
|
+
.replace(/</g, '<')
|
|
53
|
+
.replace(/>/g, '>');
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Escape HTML special characters in attribute values.
|
|
58
|
+
* @param {string} str
|
|
59
|
+
* @returns {string}
|
|
60
|
+
*/
|
|
61
|
+
function escapeAttr(str) {
|
|
62
|
+
if (typeof str !== 'string') return '';
|
|
63
|
+
return str
|
|
64
|
+
.replace(/&/g, '&')
|
|
65
|
+
.replace(/"/g, '"')
|
|
66
|
+
.replace(/</g, '<')
|
|
67
|
+
.replace(/>/g, '>');
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// ─── VNode Types ────────────────────────────────────────────────
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* @typedef {{ tag: string, props: Object|null, children: Array<VNode|TextVNode|string>, _id: number }} VNode
|
|
74
|
+
* @typedef {{ text: string, _id: number }} TextVNode
|
|
75
|
+
*/
|
|
76
|
+
|
|
77
|
+
/** HTML void elements — self-closing, no children */
|
|
78
|
+
const VOID_ELEMENTS = new Set([
|
|
79
|
+
'area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input',
|
|
80
|
+
'link', 'meta', 'param', 'source', 'track', 'wbr',
|
|
81
|
+
]);
|
|
82
|
+
|
|
83
|
+
/** Boolean HTML attributes — rendered as `attr` not `attr="value"` */
|
|
84
|
+
const BOOLEAN_ATTRS = new Set([
|
|
85
|
+
'allowfullscreen', 'async', 'autofocus', 'autoplay', 'checked',
|
|
86
|
+
'controls', 'default', 'defer', 'disabled', 'formnovalidate',
|
|
87
|
+
'hidden', 'inert', 'ismap', 'itemscope', 'loop', 'multiple',
|
|
88
|
+
'muted', 'nomodule', 'novalidate', 'open', 'playsinline',
|
|
89
|
+
'readonly', 'required', 'reversed', 'selected',
|
|
90
|
+
]);
|
|
91
|
+
|
|
92
|
+
// ─── SSR Context ────────────────────────────────────────────────
|
|
93
|
+
|
|
94
|
+
/** Global VNode ID counter — reset per render call */
|
|
95
|
+
let _nextId = 0;
|
|
96
|
+
|
|
97
|
+
/** Whether we are currently inside an SSR render pass */
|
|
98
|
+
let _ssrActive = false;
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Evaluate a value without triggering signal subscriptions.
|
|
102
|
+
* In SSR mode signals are evaluated eagerly without tracking.
|
|
103
|
+
* @template T
|
|
104
|
+
* @param {() => T} fn
|
|
105
|
+
* @returns {T}
|
|
106
|
+
*/
|
|
107
|
+
function ssrUntrack(fn) {
|
|
108
|
+
// In SSR we don't have reactive tracking, so just call the function
|
|
109
|
+
return fn();
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// ─── SSR Primitives ─────────────────────────────────────────────
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* SSR version of h() — creates a VNode, not a DOM element.
|
|
116
|
+
* Event handlers (on*) are stored but not serialized to HTML.
|
|
117
|
+
* Functions in props are eagerly evaluated with ssrUntrack().
|
|
118
|
+
* @param {string} tag
|
|
119
|
+
* @param {Object|null} props
|
|
120
|
+
* @param {...*} children
|
|
121
|
+
* @returns {VNode}
|
|
122
|
+
*/
|
|
123
|
+
function ssrH(tag, props, ...children) {
|
|
124
|
+
const id = _nextId++;
|
|
125
|
+
const resolvedProps = {};
|
|
126
|
+
const handlers = {};
|
|
127
|
+
|
|
128
|
+
if (props) {
|
|
129
|
+
for (const key in props) {
|
|
130
|
+
const val = props[key];
|
|
131
|
+
|
|
132
|
+
// Event handlers — store separately, do not serialize
|
|
133
|
+
if (key.startsWith('on') && typeof val === 'function') {
|
|
134
|
+
handlers[key] = val;
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// ref callbacks — skip in SSR
|
|
139
|
+
if (key === 'ref') continue;
|
|
140
|
+
|
|
141
|
+
// Reactive props — evaluate eagerly
|
|
142
|
+
if (typeof val === 'function') {
|
|
143
|
+
const evaluated = ssrUntrack(val);
|
|
144
|
+
if (key === 'class' || key === 'className') {
|
|
145
|
+
resolvedProps['class'] = evaluated;
|
|
146
|
+
} else if (key === 'style' && typeof evaluated === 'object') {
|
|
147
|
+
resolvedProps['style'] = styleObjToString(evaluated);
|
|
148
|
+
} else if (evaluated !== false && evaluated != null) {
|
|
149
|
+
resolvedProps[key] = evaluated === true ? true : String(evaluated);
|
|
150
|
+
}
|
|
151
|
+
} else if (key === 'class' || key === 'className') {
|
|
152
|
+
resolvedProps['class'] = val;
|
|
153
|
+
} else if (key === 'style' && typeof val === 'object') {
|
|
154
|
+
resolvedProps['style'] = styleObjToString(val);
|
|
155
|
+
} else if (val !== false && val != null) {
|
|
156
|
+
resolvedProps[key] = val === true ? true : String(val);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Flatten children
|
|
162
|
+
const flatChildren = flattenChildren(children);
|
|
163
|
+
|
|
164
|
+
return {
|
|
165
|
+
tag,
|
|
166
|
+
props: resolvedProps,
|
|
167
|
+
children: flatChildren,
|
|
168
|
+
_handlers: handlers,
|
|
169
|
+
_id: id,
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* SSR version of text() — evaluates getter once, returns TextVNode.
|
|
175
|
+
* @param {Function} getter
|
|
176
|
+
* @returns {TextVNode}
|
|
177
|
+
*/
|
|
178
|
+
function ssrText(getter) {
|
|
179
|
+
const id = _nextId++;
|
|
180
|
+
const value = ssrUntrack(getter);
|
|
181
|
+
return { text: String(value), _id: id, _reactive: true };
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* SSR version of cond() — evaluates predicate once, returns the active branch.
|
|
186
|
+
* Wraps result in a d-cond VNode for hydration matching.
|
|
187
|
+
* @param {Function} condition
|
|
188
|
+
* @param {Function} thenFn
|
|
189
|
+
* @param {Function} [elseFn]
|
|
190
|
+
* @returns {VNode}
|
|
191
|
+
*/
|
|
192
|
+
function ssrCond(condition, thenFn, elseFn) {
|
|
193
|
+
const id = _nextId++;
|
|
194
|
+
const result = ssrUntrack(condition);
|
|
195
|
+
const fn = result ? thenFn : elseFn;
|
|
196
|
+
const child = fn ? fn() : null;
|
|
197
|
+
const children = child != null ? [normalizeVNode(child)] : [];
|
|
198
|
+
|
|
199
|
+
return {
|
|
200
|
+
tag: 'd-cond',
|
|
201
|
+
props: {},
|
|
202
|
+
children,
|
|
203
|
+
_handlers: {},
|
|
204
|
+
_id: id,
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* SSR version of list() — evaluates items once, maps through renderFn.
|
|
210
|
+
* Wraps result in a d-list VNode for hydration matching.
|
|
211
|
+
* @param {Function} itemsGetter
|
|
212
|
+
* @param {Function} keyFn
|
|
213
|
+
* @param {Function} renderFn
|
|
214
|
+
* @returns {VNode}
|
|
215
|
+
*/
|
|
216
|
+
function ssrList(itemsGetter, keyFn, renderFn) {
|
|
217
|
+
const id = _nextId++;
|
|
218
|
+
const items = ssrUntrack(itemsGetter);
|
|
219
|
+
const children = [];
|
|
220
|
+
|
|
221
|
+
if (Array.isArray(items)) {
|
|
222
|
+
for (let i = 0; i < items.length; i++) {
|
|
223
|
+
const child = renderFn(items[i], i);
|
|
224
|
+
if (child != null) {
|
|
225
|
+
children.push(normalizeVNode(child));
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
return {
|
|
231
|
+
tag: 'd-list',
|
|
232
|
+
props: {},
|
|
233
|
+
children,
|
|
234
|
+
_handlers: {},
|
|
235
|
+
_id: id,
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* SSR-safe onMount — no-op during SSR (mount callbacks don't run on server).
|
|
241
|
+
* @param {Function} _fn
|
|
242
|
+
*/
|
|
243
|
+
function ssrOnMount(_fn) {
|
|
244
|
+
// No-op in SSR — mount callbacks run only on client
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* SSR-safe onDestroy — no-op during SSR.
|
|
249
|
+
* @param {Function} _fn
|
|
250
|
+
*/
|
|
251
|
+
function ssrOnDestroy(_fn) {
|
|
252
|
+
// No-op in SSR — destroy callbacks run only on client
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// ─── VNode Helpers ──────────────────────────────────────────────
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Normalize a value into a VNode or TextVNode.
|
|
259
|
+
* @param {*} value
|
|
260
|
+
* @returns {VNode|TextVNode|null}
|
|
261
|
+
*/
|
|
262
|
+
function normalizeVNode(value) {
|
|
263
|
+
if (value == null || value === false) return null;
|
|
264
|
+
if (typeof value === 'object' && (value.tag || value.text !== undefined)) return value;
|
|
265
|
+
if (typeof value === 'function') {
|
|
266
|
+
// Reactive text — evaluate eagerly
|
|
267
|
+
const text = String(ssrUntrack(value));
|
|
268
|
+
return { text, _id: _nextId++, _reactive: true };
|
|
269
|
+
}
|
|
270
|
+
return { text: String(value), _id: _nextId++ };
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Flatten nested arrays and normalize children to VNodes.
|
|
275
|
+
* @param {Array} children
|
|
276
|
+
* @returns {Array<VNode|TextVNode>}
|
|
277
|
+
*/
|
|
278
|
+
function flattenChildren(children) {
|
|
279
|
+
const result = [];
|
|
280
|
+
for (let i = 0; i < children.length; i++) {
|
|
281
|
+
const child = children[i];
|
|
282
|
+
if (child == null || child === false) continue;
|
|
283
|
+
if (Array.isArray(child)) {
|
|
284
|
+
const flat = flattenChildren(child);
|
|
285
|
+
for (let j = 0; j < flat.length; j++) result.push(flat[j]);
|
|
286
|
+
} else {
|
|
287
|
+
const normalized = normalizeVNode(child);
|
|
288
|
+
if (normalized) result.push(normalized);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
return result;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Convert a style object to a CSS string.
|
|
296
|
+
* @param {Object} obj
|
|
297
|
+
* @returns {string}
|
|
298
|
+
*/
|
|
299
|
+
function styleObjToString(obj) {
|
|
300
|
+
if (!obj || typeof obj !== 'object') return '';
|
|
301
|
+
const parts = [];
|
|
302
|
+
for (const key in obj) {
|
|
303
|
+
const value = obj[key];
|
|
304
|
+
if (value == null) continue;
|
|
305
|
+
// Convert camelCase to kebab-case
|
|
306
|
+
const cssKey = key.replace(/[A-Z]/g, m => '-' + m.toLowerCase());
|
|
307
|
+
parts.push(`${cssKey}:${value}`);
|
|
308
|
+
}
|
|
309
|
+
return parts.join(';');
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// ─── VNode Serialization ────────────────────────────────────────
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* Serialize a VNode tree to an HTML string.
|
|
316
|
+
* @param {VNode|TextVNode|null} node
|
|
317
|
+
* @returns {string}
|
|
318
|
+
*/
|
|
319
|
+
function serializeVNode(node) {
|
|
320
|
+
if (!node) return '';
|
|
321
|
+
|
|
322
|
+
// Text node
|
|
323
|
+
if (node.text !== undefined) {
|
|
324
|
+
return escapeHTML(node.text);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
const { tag, props, children, _id } = node;
|
|
328
|
+
|
|
329
|
+
// Build opening tag
|
|
330
|
+
let html = '<' + tag;
|
|
331
|
+
|
|
332
|
+
// Add hydration marker
|
|
333
|
+
html += ` data-d-id="${_id}"`;
|
|
334
|
+
|
|
335
|
+
// Serialize attributes
|
|
336
|
+
if (props) {
|
|
337
|
+
for (const key in props) {
|
|
338
|
+
const val = props[key];
|
|
339
|
+
if (val == null || val === false) continue;
|
|
340
|
+
|
|
341
|
+
if (BOOLEAN_ATTRS.has(key)) {
|
|
342
|
+
if (val) html += ' ' + key;
|
|
343
|
+
} else {
|
|
344
|
+
html += ` ${key}="${escapeAttr(String(val))}"`;
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// Void element — self-closing
|
|
350
|
+
if (VOID_ELEMENTS.has(tag)) {
|
|
351
|
+
return html + '>';
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
html += '>';
|
|
355
|
+
|
|
356
|
+
// Serialize children
|
|
357
|
+
for (let i = 0; i < children.length; i++) {
|
|
358
|
+
html += serializeVNode(children[i]);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
html += '</' + tag + '>';
|
|
362
|
+
return html;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* Yield VNode chunks for streaming.
|
|
367
|
+
* @param {VNode|TextVNode|null} node
|
|
368
|
+
* @param {function(string): void} push — called for each chunk
|
|
369
|
+
*/
|
|
370
|
+
function streamVNode(node, push) {
|
|
371
|
+
if (!node) return;
|
|
372
|
+
|
|
373
|
+
// Text node
|
|
374
|
+
if (node.text !== undefined) {
|
|
375
|
+
push(escapeHTML(node.text));
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
const { tag, props, children, _id } = node;
|
|
380
|
+
|
|
381
|
+
// Build opening tag
|
|
382
|
+
let html = '<' + tag;
|
|
383
|
+
html += ` data-d-id="${_id}"`;
|
|
384
|
+
|
|
385
|
+
if (props) {
|
|
386
|
+
for (const key in props) {
|
|
387
|
+
const val = props[key];
|
|
388
|
+
if (val == null || val === false) continue;
|
|
389
|
+
if (BOOLEAN_ATTRS.has(key)) {
|
|
390
|
+
if (val) html += ' ' + key;
|
|
391
|
+
} else {
|
|
392
|
+
html += ` ${key}="${escapeAttr(String(val))}"`;
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
if (VOID_ELEMENTS.has(tag)) {
|
|
398
|
+
push(html + '>');
|
|
399
|
+
return;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
push(html + '>');
|
|
403
|
+
|
|
404
|
+
// Stream children
|
|
405
|
+
for (let i = 0; i < children.length; i++) {
|
|
406
|
+
streamVNode(children[i], push);
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
push('</' + tag + '>');
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
// ─── SSR Execution Context ──────────────────────────────────────
|
|
413
|
+
|
|
414
|
+
/**
|
|
415
|
+
* Execute a function with SSR versions of h, text, cond, list, onMount, onDestroy.
|
|
416
|
+
* Uses import indirection: components call the SSR versions when _ssrActive is true.
|
|
417
|
+
*
|
|
418
|
+
* @template T
|
|
419
|
+
* @param {Function} componentFn — () => VNode tree
|
|
420
|
+
* @returns {VNode|TextVNode|null}
|
|
421
|
+
*/
|
|
422
|
+
function runInSSRContext(componentFn) {
|
|
423
|
+
_nextId = 0;
|
|
424
|
+
_ssrActive = true;
|
|
425
|
+
|
|
426
|
+
// Store original globals if they exist
|
|
427
|
+
const prevH = globalThis.__d_ssr_h;
|
|
428
|
+
const prevText = globalThis.__d_ssr_text;
|
|
429
|
+
const prevCond = globalThis.__d_ssr_cond;
|
|
430
|
+
const prevList = globalThis.__d_ssr_list;
|
|
431
|
+
const prevCss = globalThis.__d_ssr_css;
|
|
432
|
+
const prevOnMount = globalThis.__d_ssr_onMount;
|
|
433
|
+
const prevOnDestroy = globalThis.__d_ssr_onDestroy;
|
|
434
|
+
const prevActive = globalThis.__d_ssr_active;
|
|
435
|
+
|
|
436
|
+
// Install SSR implementations on globalThis
|
|
437
|
+
globalThis.__d_ssr_h = ssrH;
|
|
438
|
+
globalThis.__d_ssr_text = ssrText;
|
|
439
|
+
globalThis.__d_ssr_cond = ssrCond;
|
|
440
|
+
globalThis.__d_ssr_list = ssrList;
|
|
441
|
+
globalThis.__d_ssr_css = ssrCss;
|
|
442
|
+
globalThis.__d_ssr_onMount = ssrOnMount;
|
|
443
|
+
globalThis.__d_ssr_onDestroy = ssrOnDestroy;
|
|
444
|
+
globalThis.__d_ssr_active = true;
|
|
445
|
+
|
|
446
|
+
try {
|
|
447
|
+
const result = componentFn();
|
|
448
|
+
return normalizeVNode(result);
|
|
449
|
+
} finally {
|
|
450
|
+
_ssrActive = false;
|
|
451
|
+
|
|
452
|
+
// Restore previous values
|
|
453
|
+
globalThis.__d_ssr_h = prevH;
|
|
454
|
+
globalThis.__d_ssr_text = prevText;
|
|
455
|
+
globalThis.__d_ssr_cond = prevCond;
|
|
456
|
+
globalThis.__d_ssr_list = prevList;
|
|
457
|
+
globalThis.__d_ssr_css = prevCss;
|
|
458
|
+
globalThis.__d_ssr_onMount = prevOnMount;
|
|
459
|
+
globalThis.__d_ssr_onDestroy = prevOnDestroy;
|
|
460
|
+
globalThis.__d_ssr_active = prevActive;
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
// ─── Public API: renderToString ─────────────────────────────────
|
|
465
|
+
|
|
466
|
+
/**
|
|
467
|
+
* Render a component to an HTML string.
|
|
468
|
+
*
|
|
469
|
+
* The component function is called in an SSR context where:
|
|
470
|
+
* - h() creates VNodes instead of DOM elements
|
|
471
|
+
* - text() evaluates getters once without reactive tracking
|
|
472
|
+
* - cond()/list() evaluate eagerly without creating effects
|
|
473
|
+
* - onMount()/onDestroy() are no-ops
|
|
474
|
+
* - Signals are read once without subscriptions
|
|
475
|
+
*
|
|
476
|
+
* Each element includes a `data-d-id` attribute for hydration matching.
|
|
477
|
+
*
|
|
478
|
+
* @param {Function} component — component function that returns a VNode tree
|
|
479
|
+
* @returns {string} HTML string
|
|
480
|
+
*/
|
|
481
|
+
export function renderToString(component) {
|
|
482
|
+
const vnode = runInSSRContext(component);
|
|
483
|
+
return serializeVNode(vnode);
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
// ─── Public API: renderToStream ─────────────────────────────────
|
|
487
|
+
|
|
488
|
+
/**
|
|
489
|
+
* Render a component to a ReadableStream of HTML chunks.
|
|
490
|
+
*
|
|
491
|
+
* Same SSR semantics as renderToString but yields chunks incrementally.
|
|
492
|
+
* Uses the Web Streams API (available in Node.js 18+).
|
|
493
|
+
*
|
|
494
|
+
* @param {Function} component — component function that returns a VNode tree
|
|
495
|
+
* @returns {ReadableStream}
|
|
496
|
+
*/
|
|
497
|
+
export function renderToStream(component) {
|
|
498
|
+
// Build VNode tree synchronously (same as renderToString)
|
|
499
|
+
const vnode = runInSSRContext(component);
|
|
500
|
+
|
|
501
|
+
return new ReadableStream({
|
|
502
|
+
start(controller) {
|
|
503
|
+
try {
|
|
504
|
+
streamVNode(vnode, chunk => {
|
|
505
|
+
controller.enqueue(chunk);
|
|
506
|
+
});
|
|
507
|
+
controller.close();
|
|
508
|
+
} catch (err) {
|
|
509
|
+
controller.error(err);
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
});
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
// ─── Public API: hydrate ────────────────────────────────────────
|
|
516
|
+
|
|
517
|
+
/**
|
|
518
|
+
* Hydrate an existing DOM tree produced by renderToString/renderToStream.
|
|
519
|
+
*
|
|
520
|
+
* This function:
|
|
521
|
+
* 1. Runs the component function in client mode (creates real DOM structures)
|
|
522
|
+
* 2. Walks existing DOM in parallel with component output
|
|
523
|
+
* 3. Reuses existing DOM nodes instead of creating new ones
|
|
524
|
+
* 4. Attaches event listeners, signal subscriptions, and reactive effects
|
|
525
|
+
* 5. Drains the onMount queue
|
|
526
|
+
*
|
|
527
|
+
* The existing DOM must match the component's initial render output structurally.
|
|
528
|
+
* Matching is done by position (depth-first walk), not by data-d-id.
|
|
529
|
+
*
|
|
530
|
+
* @param {HTMLElement} root — the DOM root containing SSR HTML
|
|
531
|
+
* @param {Function} component — the same component function used for SSR
|
|
532
|
+
*/
|
|
533
|
+
export function hydrate(root, component) {
|
|
534
|
+
// Dynamically import core/state to avoid loading DOM globals at module level
|
|
535
|
+
// These must be available in the browser environment when hydrate() is called
|
|
536
|
+
const { createEffect } = _requireState();
|
|
537
|
+
const { pushScope, popScope, drainMountQueue, runDestroyFns } = _requireLifecycle();
|
|
538
|
+
|
|
539
|
+
pushScope();
|
|
540
|
+
|
|
541
|
+
// Run the component in client mode to get the real DOM tree
|
|
542
|
+
const clientTree = component();
|
|
543
|
+
|
|
544
|
+
const destroyFns = popScope();
|
|
545
|
+
|
|
546
|
+
// Walk the existing SSR DOM and the client-produced DOM in parallel
|
|
547
|
+
// to attach event listeners and reactive bindings
|
|
548
|
+
if (clientTree && root.firstChild) {
|
|
549
|
+
_hydrateNode(root.firstChild, clientTree, root, createEffect);
|
|
550
|
+
} else if (clientTree && !root.firstChild) {
|
|
551
|
+
// SSR HTML is empty but client produced content — append it
|
|
552
|
+
root.appendChild(clientTree);
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
// Drain mount queue
|
|
556
|
+
const mountFns = drainMountQueue();
|
|
557
|
+
for (const fn of mountFns) {
|
|
558
|
+
const cleanup = fn();
|
|
559
|
+
if (typeof cleanup === 'function') {
|
|
560
|
+
destroyFns.push(cleanup);
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
// Store destroy functions for unmount
|
|
565
|
+
root.__d_destroy = destroyFns;
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
// ─── Hydration Walker ───────────────────────────────────────────
|
|
569
|
+
|
|
570
|
+
/**
|
|
571
|
+
* Hydrate a single DOM node by reconciling it with the client-produced node.
|
|
572
|
+
* Attaches event listeners and reactive bindings from the client node
|
|
573
|
+
* onto the existing server-rendered DOM node.
|
|
574
|
+
*
|
|
575
|
+
* @param {Node} ssrNode — existing DOM node from SSR
|
|
576
|
+
* @param {Node} clientNode — freshly created DOM node from client render
|
|
577
|
+
* @param {Node} parent — parent of ssrNode
|
|
578
|
+
* @param {Function} createEffect — reactive effect factory
|
|
579
|
+
*/
|
|
580
|
+
function _hydrateNode(ssrNode, clientNode, parent, createEffect) {
|
|
581
|
+
if (!ssrNode || !clientNode) return;
|
|
582
|
+
|
|
583
|
+
// Text node hydration
|
|
584
|
+
if (ssrNode.nodeType === 3 && clientNode.nodeType === 3) {
|
|
585
|
+
// If the client text node has reactive effects attached via createEffect,
|
|
586
|
+
// they will update ssrNode.nodeValue when signals change.
|
|
587
|
+
// We need to "redirect" client effects to update the SSR node instead.
|
|
588
|
+
// The simplest approach: copy any pending reactive subscription.
|
|
589
|
+
// Since the client node was created by text() which uses createEffect internally,
|
|
590
|
+
// the effect already observes the signal. We just need to ensure updates
|
|
591
|
+
// target the SSR node. We do this by patching nodeValue.
|
|
592
|
+
if (clientNode.nodeValue !== ssrNode.nodeValue) {
|
|
593
|
+
// Initial value mismatch — use client value (more recent)
|
|
594
|
+
ssrNode.nodeValue = clientNode.nodeValue;
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
// Redirect reactive updates: any effect that sets clientNode.nodeValue
|
|
598
|
+
// should instead set ssrNode.nodeValue. We achieve this by making
|
|
599
|
+
// clientNode.nodeValue a proxy to ssrNode.
|
|
600
|
+
_redirectTextUpdates(ssrNode, clientNode);
|
|
601
|
+
return;
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
// Element node hydration
|
|
605
|
+
if (ssrNode.nodeType === 1 && clientNode.nodeType === 1) {
|
|
606
|
+
// Attach event listeners from the client node to the SSR node
|
|
607
|
+
if (clientNode._listeners) {
|
|
608
|
+
for (const [type, fns] of clientNode._listeners) {
|
|
609
|
+
for (const fn of fns) {
|
|
610
|
+
ssrNode.addEventListener(type, fn);
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
// Copy over any special properties the client-side h() set up
|
|
616
|
+
// (reactive class, reactive attributes, etc.)
|
|
617
|
+
// The client-side createEffect calls will be trying to update clientNode.
|
|
618
|
+
// We redirect them to ssrNode.
|
|
619
|
+
_redirectElementUpdates(ssrNode, clientNode, createEffect);
|
|
620
|
+
|
|
621
|
+
// Recursively hydrate children
|
|
622
|
+
const ssrChildren = ssrNode.childNodes || [];
|
|
623
|
+
const clientChildren = clientNode.childNodes || [];
|
|
624
|
+
const maxLen = Math.max(ssrChildren.length, clientChildren.length);
|
|
625
|
+
|
|
626
|
+
for (let i = 0; i < maxLen; i++) {
|
|
627
|
+
const ssrChild = ssrChildren[i];
|
|
628
|
+
const clientChild = clientChildren[i];
|
|
629
|
+
|
|
630
|
+
if (ssrChild && clientChild) {
|
|
631
|
+
_hydrateNode(ssrChild, clientChild, ssrNode, createEffect);
|
|
632
|
+
} else if (!ssrChild && clientChild) {
|
|
633
|
+
// Client has extra node — append it (SSR missed it)
|
|
634
|
+
ssrNode.appendChild(clientChild);
|
|
635
|
+
}
|
|
636
|
+
// If SSR has extra node but client doesn't — leave it (stale SSR content)
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
return;
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
// Comment node (e.g., Portal placeholder) — skip
|
|
643
|
+
if (ssrNode.nodeType === 8) return;
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
/**
|
|
647
|
+
* Redirect text node updates from client node to SSR node.
|
|
648
|
+
* When createEffect updates clientNode.nodeValue, the update
|
|
649
|
+
* should also apply to ssrNode.
|
|
650
|
+
*
|
|
651
|
+
* @param {Text} ssrNode
|
|
652
|
+
* @param {Text} clientNode
|
|
653
|
+
*/
|
|
654
|
+
function _redirectTextUpdates(ssrNode, clientNode) {
|
|
655
|
+
// Observe changes to clientNode.nodeValue and mirror them to ssrNode
|
|
656
|
+
const originalDescriptor = Object.getOwnPropertyDescriptor(
|
|
657
|
+
Object.getPrototypeOf(clientNode), 'nodeValue'
|
|
658
|
+
) || Object.getOwnPropertyDescriptor(clientNode, 'nodeValue');
|
|
659
|
+
|
|
660
|
+
if (originalDescriptor && originalDescriptor.set) {
|
|
661
|
+
// If nodeValue is a setter (real DOM), intercept writes
|
|
662
|
+
Object.defineProperty(clientNode, 'nodeValue', {
|
|
663
|
+
get() {
|
|
664
|
+
return ssrNode.nodeValue;
|
|
665
|
+
},
|
|
666
|
+
set(v) {
|
|
667
|
+
ssrNode.nodeValue = v;
|
|
668
|
+
},
|
|
669
|
+
configurable: true,
|
|
670
|
+
});
|
|
671
|
+
} else {
|
|
672
|
+
// In test environments, nodeValue is a plain property.
|
|
673
|
+
// Use polling or direct assignment check.
|
|
674
|
+
// For simplicity in test DOM: replace clientNode in parent with ssrNode
|
|
675
|
+
// The effects that reference clientNode should be updated.
|
|
676
|
+
// Actually, in the test DOM nodeValue is just a field.
|
|
677
|
+
// We can define a setter on the instance.
|
|
678
|
+
let _val = clientNode.nodeValue;
|
|
679
|
+
Object.defineProperty(clientNode, 'nodeValue', {
|
|
680
|
+
get() { return ssrNode.nodeValue; },
|
|
681
|
+
set(v) { ssrNode.nodeValue = v; },
|
|
682
|
+
configurable: true,
|
|
683
|
+
});
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
/**
|
|
688
|
+
* Redirect element property updates from client node to SSR node.
|
|
689
|
+
* This covers reactive className, setAttribute, style updates
|
|
690
|
+
* that createEffect applies to the client-created element.
|
|
691
|
+
*
|
|
692
|
+
* @param {Element} ssrNode
|
|
693
|
+
* @param {Element} clientNode
|
|
694
|
+
* @param {Function} createEffect
|
|
695
|
+
*/
|
|
696
|
+
function _redirectElementUpdates(ssrNode, clientNode, createEffect) {
|
|
697
|
+
// Intercept className writes
|
|
698
|
+
const classDesc = Object.getOwnPropertyDescriptor(clientNode, 'className') ||
|
|
699
|
+
Object.getOwnPropertyDescriptor(Object.getPrototypeOf(clientNode), 'className');
|
|
700
|
+
|
|
701
|
+
Object.defineProperty(clientNode, 'className', {
|
|
702
|
+
get() { return ssrNode.className; },
|
|
703
|
+
set(v) { ssrNode.className = v; },
|
|
704
|
+
configurable: true,
|
|
705
|
+
});
|
|
706
|
+
|
|
707
|
+
// Intercept setAttribute calls
|
|
708
|
+
const origSetAttribute = clientNode.setAttribute;
|
|
709
|
+
if (origSetAttribute) {
|
|
710
|
+
clientNode.setAttribute = function(name, value) {
|
|
711
|
+
ssrNode.setAttribute(name, value);
|
|
712
|
+
};
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
// Intercept style assignments
|
|
716
|
+
if (clientNode.style && ssrNode.style) {
|
|
717
|
+
const ssrStyle = ssrNode.style;
|
|
718
|
+
clientNode.style = new Proxy(ssrStyle, {
|
|
719
|
+
set(target, prop, value) {
|
|
720
|
+
target[prop] = value;
|
|
721
|
+
return true;
|
|
722
|
+
},
|
|
723
|
+
get(target, prop) {
|
|
724
|
+
return target[prop];
|
|
725
|
+
}
|
|
726
|
+
});
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
// ─── Lazy Module Resolution ─────────────────────────────────────
|
|
731
|
+
// These helpers lazily import core modules so that the SSR entry point
|
|
732
|
+
// does not pull in DOM-dependent code at module level.
|
|
733
|
+
|
|
734
|
+
let _stateModule = null;
|
|
735
|
+
let _lifecycleModule = null;
|
|
736
|
+
|
|
737
|
+
/**
|
|
738
|
+
* @returns {{ createEffect: Function }}
|
|
739
|
+
*/
|
|
740
|
+
function _requireState() {
|
|
741
|
+
// When hydrate() is called in the browser, the state module is available.
|
|
742
|
+
// We import it lazily to avoid loading it during SSR (server-side).
|
|
743
|
+
if (!_stateModule) {
|
|
744
|
+
// In a browser/test environment, this will be available via the module graph
|
|
745
|
+
// We use a dynamic technique to avoid static analysis pulling it into SSR bundles
|
|
746
|
+
try {
|
|
747
|
+
// For Node.js test environments that have already loaded the module
|
|
748
|
+
_stateModule = { createEffect: globalThis.__d_state_createEffect };
|
|
749
|
+
if (!_stateModule.createEffect) {
|
|
750
|
+
throw new Error('Not cached');
|
|
751
|
+
}
|
|
752
|
+
} catch {
|
|
753
|
+
// Fallback: assume the module has been loaded and is available
|
|
754
|
+
_stateModule = { createEffect: function(fn) { fn(); return () => {}; } };
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
return _stateModule;
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
/**
|
|
761
|
+
* @returns {{ pushScope: Function, popScope: Function, drainMountQueue: Function, runDestroyFns: Function }}
|
|
762
|
+
*/
|
|
763
|
+
function _requireLifecycle() {
|
|
764
|
+
if (!_lifecycleModule) {
|
|
765
|
+
try {
|
|
766
|
+
_lifecycleModule = {
|
|
767
|
+
pushScope: globalThis.__d_lifecycle_pushScope,
|
|
768
|
+
popScope: globalThis.__d_lifecycle_popScope,
|
|
769
|
+
drainMountQueue: globalThis.__d_lifecycle_drainMountQueue,
|
|
770
|
+
runDestroyFns: globalThis.__d_lifecycle_runDestroyFns,
|
|
771
|
+
};
|
|
772
|
+
if (!_lifecycleModule.pushScope) throw new Error('Not cached');
|
|
773
|
+
} catch {
|
|
774
|
+
// Fallback no-ops for environments where lifecycle isn't loaded
|
|
775
|
+
_lifecycleModule = {
|
|
776
|
+
pushScope: () => {},
|
|
777
|
+
popScope: () => [],
|
|
778
|
+
drainMountQueue: () => [],
|
|
779
|
+
runDestroyFns: () => {},
|
|
780
|
+
};
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
return _lifecycleModule;
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
// ─── Hydration Bootstrap ────────────────────────────────────────
|
|
787
|
+
|
|
788
|
+
/**
|
|
789
|
+
* Install hydration helpers that make core modules available to hydrate().
|
|
790
|
+
* Call this once on the client before calling hydrate().
|
|
791
|
+
*
|
|
792
|
+
* @param {{ createEffect: Function }} stateMod — the state module
|
|
793
|
+
* @param {{ pushScope: Function, popScope: Function, drainMountQueue: Function, runDestroyFns: Function }} lifecycleMod — lifecycle module
|
|
794
|
+
*/
|
|
795
|
+
export function installHydrationRuntime(stateMod, lifecycleMod) {
|
|
796
|
+
_stateModule = stateMod;
|
|
797
|
+
_lifecycleModule = lifecycleMod;
|
|
798
|
+
|
|
799
|
+
// Also set globals for lazy resolution
|
|
800
|
+
if (stateMod.createEffect) globalThis.__d_state_createEffect = stateMod.createEffect;
|
|
801
|
+
if (lifecycleMod.pushScope) globalThis.__d_lifecycle_pushScope = lifecycleMod.pushScope;
|
|
802
|
+
if (lifecycleMod.popScope) globalThis.__d_lifecycle_popScope = lifecycleMod.popScope;
|
|
803
|
+
if (lifecycleMod.drainMountQueue) globalThis.__d_lifecycle_drainMountQueue = lifecycleMod.drainMountQueue;
|
|
804
|
+
if (lifecycleMod.runDestroyFns) globalThis.__d_lifecycle_runDestroyFns = lifecycleMod.runDestroyFns;
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
// ─── Direct SSR Builders (for components using SSR directly) ────
|
|
808
|
+
|
|
809
|
+
/**
|
|
810
|
+
* Check if we're currently in SSR mode.
|
|
811
|
+
* Components can use this to branch between SSR and client rendering.
|
|
812
|
+
* @returns {boolean}
|
|
813
|
+
*/
|
|
814
|
+
export function isSSR() {
|
|
815
|
+
return _ssrActive || !!globalThis.__d_ssr_active;
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
/**
|
|
819
|
+
* Get the SSR-safe h() function.
|
|
820
|
+
* Returns ssrH during SSR, null otherwise.
|
|
821
|
+
* @returns {Function|null}
|
|
822
|
+
*/
|
|
823
|
+
export function getSSRH() {
|
|
824
|
+
return _ssrActive ? ssrH : (globalThis.__d_ssr_h || null);
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
/**
|
|
828
|
+
* Get the SSR-safe text() function.
|
|
829
|
+
* Returns ssrText during SSR, null otherwise.
|
|
830
|
+
* @returns {Function|null}
|
|
831
|
+
*/
|
|
832
|
+
export function getSSRText() {
|
|
833
|
+
return _ssrActive ? ssrText : (globalThis.__d_ssr_text || null);
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
/**
|
|
837
|
+
* Get the SSR-safe cond() function.
|
|
838
|
+
* @returns {Function|null}
|
|
839
|
+
*/
|
|
840
|
+
export function getSSRCond() {
|
|
841
|
+
return _ssrActive ? ssrCond : (globalThis.__d_ssr_cond || null);
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
/**
|
|
845
|
+
* Get the SSR-safe list() function.
|
|
846
|
+
* @returns {Function|null}
|
|
847
|
+
*/
|
|
848
|
+
export function getSSRList() {
|
|
849
|
+
return _ssrActive ? ssrList : (globalThis.__d_ssr_list || null);
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
/**
|
|
853
|
+
* Get the SSR-safe css() function.
|
|
854
|
+
* @returns {Function|null}
|
|
855
|
+
*/
|
|
856
|
+
export function getSSRCss() {
|
|
857
|
+
return _ssrActive ? ssrCss : (globalThis.__d_ssr_css || null);
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
// ─── SSR Component Wrapper ──────────────────────────────────────
|
|
861
|
+
|
|
862
|
+
/**
|
|
863
|
+
* Create an SSR-compatible component that works in both server and client contexts.
|
|
864
|
+
* Returns a wrapper that delegates to SSR primitives during renderToString
|
|
865
|
+
* and to normal DOM primitives in the browser.
|
|
866
|
+
*
|
|
867
|
+
* Usage:
|
|
868
|
+
* ```js
|
|
869
|
+
* import { ssrComponent } from 'decantr/ssr';
|
|
870
|
+
*
|
|
871
|
+
* const MyComponent = ssrComponent((h, text, cond, list, css) => {
|
|
872
|
+
* return (props) => h('div', { class: css('_flex _p4') }, text(() => props.title));
|
|
873
|
+
* });
|
|
874
|
+
* ```
|
|
875
|
+
*
|
|
876
|
+
* @param {Function} factory — (h, text, cond, list, css) => componentFn
|
|
877
|
+
* @returns {Function}
|
|
878
|
+
*/
|
|
879
|
+
export function ssrComponent(factory) {
|
|
880
|
+
return function(...args) {
|
|
881
|
+
if (isSSR()) {
|
|
882
|
+
const impl = factory(ssrH, ssrText, ssrCond, ssrList, ssrCss);
|
|
883
|
+
return impl(...args);
|
|
884
|
+
}
|
|
885
|
+
// In client mode, we need the real implementations
|
|
886
|
+
// They should be imported normally by the consuming code
|
|
887
|
+
throw new Error(
|
|
888
|
+
'ssrComponent() called in client mode without real implementations. ' +
|
|
889
|
+
'Import h, text, cond, list, css directly for client rendering.'
|
|
890
|
+
);
|
|
891
|
+
};
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
// ─── Exported SSR Primitives ────────────────────────────────────
|
|
895
|
+
|
|
896
|
+
// Export SSR primitives for direct use in universal components
|
|
897
|
+
export {
|
|
898
|
+
ssrH,
|
|
899
|
+
ssrText,
|
|
900
|
+
ssrCond,
|
|
901
|
+
ssrList,
|
|
902
|
+
ssrCss,
|
|
903
|
+
ssrOnMount,
|
|
904
|
+
ssrOnDestroy,
|
|
905
|
+
};
|
|
906
|
+
|
|
907
|
+
// Export internals for testing
|
|
908
|
+
export const _internals = {
|
|
909
|
+
serializeVNode,
|
|
910
|
+
streamVNode,
|
|
911
|
+
flattenChildren,
|
|
912
|
+
normalizeVNode,
|
|
913
|
+
escapeHTML,
|
|
914
|
+
escapeAttr,
|
|
915
|
+
styleObjToString,
|
|
916
|
+
VOID_ELEMENTS,
|
|
917
|
+
BOOLEAN_ATTRS,
|
|
918
|
+
_hydrateNode,
|
|
919
|
+
_redirectTextUpdates,
|
|
920
|
+
_redirectElementUpdates,
|
|
921
|
+
runInSSRContext,
|
|
922
|
+
};
|