nodality 1.0.144 → 1.0.146

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.
Files changed (94) hide show
  1. package/layout/animator.js +1 -1
  2. package/layout/audio.js +1 -1
  3. package/layout/audionew.js +1 -1
  4. package/layout/base-2.js +1 -1
  5. package/layout/base.js +1 -1
  6. package/layout/beta-desktop-bar.js +1 -1
  7. package/layout/beta-mobile-bar.js +1 -1
  8. package/layout/box.js +1 -1
  9. package/layout/button.js +1 -1
  10. package/layout/cards.js +1 -1
  11. package/layout/center.js +1 -1
  12. package/layout/checkbox.js +1 -1
  13. package/layout/circle.js +1 -1
  14. package/layout/clean-row.js +1 -1
  15. package/layout/code.js +1 -1
  16. package/layout/container.js +1 -1
  17. package/layout/custom.js +1 -1
  18. package/layout/div-image.js +1 -1
  19. package/layout/dropdown-2025.js +1 -1
  20. package/layout/dropdown.js +1 -1
  21. package/layout/empty-element.js +1 -1
  22. package/layout/external-stylesheet.js +1 -1
  23. package/layout/flex-card.js +1 -1
  24. package/layout/flex-grid.js +1 -1
  25. package/layout/flex-row.js +1 -1
  26. package/layout/footer.js +1 -1
  27. package/layout/form-components/custom.js +1 -1
  28. package/layout/form-components/data-list.js +1 -1
  29. package/layout/form-components/floating-input.js +1 -1
  30. package/layout/form-components/form-all.js +1 -1
  31. package/layout/form-components/form.js +1 -1
  32. package/layout/form-components/image-picker.js +1 -1
  33. package/layout/form-components/picker.js +1 -1
  34. package/layout/form-components/radio.js +1 -1
  35. package/layout/form-components/radiogroup.js +1 -1
  36. package/layout/form-components/range.js +1 -1
  37. package/layout/free.js +1 -1
  38. package/layout/grid-new.js +1 -1
  39. package/layout/grid-switcher.js +1 -1
  40. package/layout/grid.js +1 -1
  41. package/layout/group.js +1 -1
  42. package/layout/header.js +1 -1
  43. package/layout/horizontal-scroller.js +1 -1
  44. package/layout/image-old.js +1 -1
  45. package/layout/image.js +1 -1
  46. package/layout/index.js +1 -1
  47. package/layout/label.js +1 -1
  48. package/layout/link.js +1 -1
  49. package/layout/list-OLD.js +1 -1
  50. package/layout/list.js +1 -1
  51. package/layout/meta-adder.js +1 -1
  52. package/layout/modal-2025.js +1 -1
  53. package/layout/modernwrap.js +1 -1
  54. package/layout/multiswitcher.js +1 -1
  55. package/layout/multiswitcherBeta.js +1 -1
  56. package/layout/nav-bar.js +1 -1
  57. package/layout/nav-factor/custom-div.js +1 -1
  58. package/layout/navBar-OLD.js +1 -1
  59. package/layout/new-flat-adder.js +1 -1
  60. package/layout/new-nav-bar.js +1 -1
  61. package/layout/offset-container.js +1 -1
  62. package/layout/polygon.js +1 -1
  63. package/layout/prerender.js +365 -0
  64. package/layout/progress.js +1 -1
  65. package/layout/row.js +1 -1
  66. package/layout/saved-new-nav-bar.js +1 -1
  67. package/layout/scroll-video.js +1 -1
  68. package/layout/side-bar.js +1 -1
  69. package/layout/side-nav-bar.js +1 -1
  70. package/layout/simple-bar.js +1 -1
  71. package/layout/slider-2025.js +1 -1
  72. package/layout/spacer.js +1 -1
  73. package/layout/stack.js +1 -1
  74. package/layout/styler.js +1 -1
  75. package/layout/svg.js +1 -1
  76. package/layout/switcher.js +1 -1
  77. package/layout/table.js +1 -1
  78. package/layout/text-field.js +1 -1
  79. package/layout/text.js +1 -1
  80. package/layout/ulist.js +1 -1
  81. package/layout/video.js +1 -1
  82. package/layout/without-new.js +1 -1
  83. package/layout/wrap.js +1 -1
  84. package/layout/zoom-card.js +1 -1
  85. package/lib/card-getter.js +1 -1
  86. package/lib/designer.js +1 -1
  87. package/lib/element-mapper.js +1 -1
  88. package/lib/keyframe-animation.js +1 -1
  89. package/lib/link-getter.js +1 -1
  90. package/lib/scroll-video.js +1 -1
  91. package/lib/stacker.js +1 -1
  92. package/lib/theme.js +1 -1
  93. package/lib/transform-anim.js +1 -1
  94. package/package.json +3 -2
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * nodality v1.0.144
2
+ * nodality v1.0.146
3
3
  * (c) 2026 Filip Vabrousek
4
4
  * License: MIT
5
5
  */
package/layout/audio.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * nodality v1.0.144
2
+ * nodality v1.0.146
3
3
  * (c) 2026 Filip Vabrousek
4
4
  * License: MIT
5
5
  */
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * nodality v1.0.144
2
+ * nodality v1.0.146
3
3
  * (c) 2026 Filip Vabrousek
4
4
  * License: MIT
5
5
  */
package/layout/base-2.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * nodality v1.0.144
2
+ * nodality v1.0.146
3
3
  * (c) 2026 Filip Vabrousek
4
4
  * License: MIT
5
5
  */
package/layout/base.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * nodality v1.0.144
2
+ * nodality v1.0.146
3
3
  * (c) 2026 Filip Vabrousek
4
4
  * License: MIT
5
5
  */
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * nodality v1.0.144
2
+ * nodality v1.0.146
3
3
  * (c) 2026 Filip Vabrousek
4
4
  * License: MIT
5
5
  */
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * nodality v1.0.144
2
+ * nodality v1.0.146
3
3
  * (c) 2026 Filip Vabrousek
4
4
  * License: MIT
5
5
  */
package/layout/box.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * nodality v1.0.144
2
+ * nodality v1.0.146
3
3
  * (c) 2026 Filip Vabrousek
4
4
  * License: MIT
5
5
  */
package/layout/button.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * nodality v1.0.144
2
+ * nodality v1.0.146
3
3
  * (c) 2026 Filip Vabrousek
4
4
  * License: MIT
5
5
  */
package/layout/cards.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * nodality v1.0.144
2
+ * nodality v1.0.146
3
3
  * (c) 2026 Filip Vabrousek
4
4
  * License: MIT
5
5
  */
package/layout/center.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * nodality v1.0.144
2
+ * nodality v1.0.146
3
3
  * (c) 2026 Filip Vabrousek
4
4
  * License: MIT
5
5
  */
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * nodality v1.0.144
2
+ * nodality v1.0.146
3
3
  * (c) 2026 Filip Vabrousek
4
4
  * License: MIT
5
5
  */
package/layout/circle.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * nodality v1.0.144
2
+ * nodality v1.0.146
3
3
  * (c) 2026 Filip Vabrousek
4
4
  * License: MIT
5
5
  */
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * nodality v1.0.144
2
+ * nodality v1.0.146
3
3
  * (c) 2026 Filip Vabrousek
4
4
  * License: MIT
5
5
  */
package/layout/code.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * nodality v1.0.144
2
+ * nodality v1.0.146
3
3
  * (c) 2026 Filip Vabrousek
4
4
  * License: MIT
5
5
  */
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * nodality v1.0.144
2
+ * nodality v1.0.146
3
3
  * (c) 2026 Filip Vabrousek
4
4
  * License: MIT
5
5
  */
package/layout/custom.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * nodality v1.0.144
2
+ * nodality v1.0.146
3
3
  * (c) 2026 Filip Vabrousek
4
4
  * License: MIT
5
5
  */
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * nodality v1.0.144
2
+ * nodality v1.0.146
3
3
  * (c) 2026 Filip Vabrousek
4
4
  * License: MIT
5
5
  */
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * nodality v1.0.144
2
+ * nodality v1.0.146
3
3
  * (c) 2026 Filip Vabrousek
4
4
  * License: MIT
5
5
  */
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * nodality v1.0.144
2
+ * nodality v1.0.146
3
3
  * (c) 2026 Filip Vabrousek
4
4
  * License: MIT
5
5
  */
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * nodality v1.0.144
2
+ * nodality v1.0.146
3
3
  * (c) 2026 Filip Vabrousek
4
4
  * License: MIT
5
5
  */
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * nodality v1.0.144
2
+ * nodality v1.0.146
3
3
  * (c) 2026 Filip Vabrousek
4
4
  * License: MIT
5
5
  */
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * nodality v1.0.144
2
+ * nodality v1.0.146
3
3
  * (c) 2026 Filip Vabrousek
4
4
  * License: MIT
5
5
  */
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * nodality v1.0.144
2
+ * nodality v1.0.146
3
3
  * (c) 2026 Filip Vabrousek
4
4
  * License: MIT
5
5
  */
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * nodality v1.0.144
2
+ * nodality v1.0.146
3
3
  * (c) 2026 Filip Vabrousek
4
4
  * License: MIT
5
5
  */
package/layout/footer.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * nodality v1.0.144
2
+ * nodality v1.0.146
3
3
  * (c) 2026 Filip Vabrousek
4
4
  * License: MIT
5
5
  */
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * nodality v1.0.144
2
+ * nodality v1.0.146
3
3
  * (c) 2026 Filip Vabrousek
4
4
  * License: MIT
5
5
  */
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * nodality v1.0.144
2
+ * nodality v1.0.146
3
3
  * (c) 2026 Filip Vabrousek
4
4
  * License: MIT
5
5
  */
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * nodality v1.0.144
2
+ * nodality v1.0.146
3
3
  * (c) 2026 Filip Vabrousek
4
4
  * License: MIT
5
5
  */
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * nodality v1.0.144
2
+ * nodality v1.0.146
3
3
  * (c) 2026 Filip Vabrousek
4
4
  * License: MIT
5
5
  */
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * nodality v1.0.144
2
+ * nodality v1.0.146
3
3
  * (c) 2026 Filip Vabrousek
4
4
  * License: MIT
5
5
  */
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * nodality v1.0.144
2
+ * nodality v1.0.146
3
3
  * (c) 2026 Filip Vabrousek
4
4
  * License: MIT
5
5
  */
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * nodality v1.0.144
2
+ * nodality v1.0.146
3
3
  * (c) 2026 Filip Vabrousek
4
4
  * License: MIT
5
5
  */
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * nodality v1.0.144
2
+ * nodality v1.0.146
3
3
  * (c) 2026 Filip Vabrousek
4
4
  * License: MIT
5
5
  */
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * nodality v1.0.144
2
+ * nodality v1.0.146
3
3
  * (c) 2026 Filip Vabrousek
4
4
  * License: MIT
5
5
  */
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * nodality v1.0.144
2
+ * nodality v1.0.146
3
3
  * (c) 2026 Filip Vabrousek
4
4
  * License: MIT
5
5
  */
package/layout/free.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * nodality v1.0.144
2
+ * nodality v1.0.146
3
3
  * (c) 2026 Filip Vabrousek
4
4
  * License: MIT
5
5
  */
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * nodality v1.0.144
2
+ * nodality v1.0.146
3
3
  * (c) 2026 Filip Vabrousek
4
4
  * License: MIT
5
5
  */
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * nodality v1.0.144
2
+ * nodality v1.0.146
3
3
  * (c) 2026 Filip Vabrousek
4
4
  * License: MIT
5
5
  */
package/layout/grid.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * nodality v1.0.144
2
+ * nodality v1.0.146
3
3
  * (c) 2026 Filip Vabrousek
4
4
  * License: MIT
5
5
  */
package/layout/group.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * nodality v1.0.144
2
+ * nodality v1.0.146
3
3
  * (c) 2026 Filip Vabrousek
4
4
  * License: MIT
5
5
  */
package/layout/header.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * nodality v1.0.144
2
+ * nodality v1.0.146
3
3
  * (c) 2026 Filip Vabrousek
4
4
  * License: MIT
5
5
  */
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * nodality v1.0.144
2
+ * nodality v1.0.146
3
3
  * (c) 2026 Filip Vabrousek
4
4
  * License: MIT
5
5
  */
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * nodality v1.0.144
2
+ * nodality v1.0.146
3
3
  * (c) 2026 Filip Vabrousek
4
4
  * License: MIT
5
5
  */
package/layout/image.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * nodality v1.0.144
2
+ * nodality v1.0.146
3
3
  * (c) 2026 Filip Vabrousek
4
4
  * License: MIT
5
5
  */
package/layout/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * nodality v1.0.144
2
+ * nodality v1.0.146
3
3
  * (c) 2026 Filip Vabrousek
4
4
  * License: MIT
5
5
  */
package/layout/label.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * nodality v1.0.144
2
+ * nodality v1.0.146
3
3
  * (c) 2026 Filip Vabrousek
4
4
  * License: MIT
5
5
  */
package/layout/link.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * nodality v1.0.144
2
+ * nodality v1.0.146
3
3
  * (c) 2026 Filip Vabrousek
4
4
  * License: MIT
5
5
  */
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * nodality v1.0.144
2
+ * nodality v1.0.146
3
3
  * (c) 2026 Filip Vabrousek
4
4
  * License: MIT
5
5
  */
package/layout/list.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * nodality v1.0.144
2
+ * nodality v1.0.146
3
3
  * (c) 2026 Filip Vabrousek
4
4
  * License: MIT
5
5
  */
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * nodality v1.0.144
2
+ * nodality v1.0.146
3
3
  * (c) 2026 Filip Vabrousek
4
4
  * License: MIT
5
5
  */
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * nodality v1.0.144
2
+ * nodality v1.0.146
3
3
  * (c) 2026 Filip Vabrousek
4
4
  * License: MIT
5
5
  */
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * nodality v1.0.144
2
+ * nodality v1.0.146
3
3
  * (c) 2026 Filip Vabrousek
4
4
  * License: MIT
5
5
  */
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * nodality v1.0.144
2
+ * nodality v1.0.146
3
3
  * (c) 2026 Filip Vabrousek
4
4
  * License: MIT
5
5
  */
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * nodality v1.0.144
2
+ * nodality v1.0.146
3
3
  * (c) 2026 Filip Vabrousek
4
4
  * License: MIT
5
5
  */
package/layout/nav-bar.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * nodality v1.0.144
2
+ * nodality v1.0.146
3
3
  * (c) 2026 Filip Vabrousek
4
4
  * License: MIT
5
5
  */
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * nodality v1.0.144
2
+ * nodality v1.0.146
3
3
  * (c) 2026 Filip Vabrousek
4
4
  * License: MIT
5
5
  */
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * nodality v1.0.144
2
+ * nodality v1.0.146
3
3
  * (c) 2026 Filip Vabrousek
4
4
  * License: MIT
5
5
  */
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * nodality v1.0.144
2
+ * nodality v1.0.146
3
3
  * (c) 2026 Filip Vabrousek
4
4
  * License: MIT
5
5
  */
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * nodality v1.0.144
2
+ * nodality v1.0.146
3
3
  * (c) 2026 Filip Vabrousek
4
4
  * License: MIT
5
5
  */
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * nodality v1.0.144
2
+ * nodality v1.0.146
3
3
  * (c) 2026 Filip Vabrousek
4
4
  * License: MIT
5
5
  */
package/layout/polygon.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * nodality v1.0.144
2
+ * nodality v1.0.146
3
3
  * (c) 2026 Filip Vabrousek
4
4
  * License: MIT
5
5
  */
@@ -0,0 +1,365 @@
1
+ /*!
2
+ * nodality v1.0.146
3
+ * (c) 2026 Filip Vabrousek
4
+ * License: MIT
5
+ */
6
+
7
+ // ════════════════════════════════════════════════════════════════════
8
+ // Nodality — static-site prerender helper
9
+ // ════════════════════════════════════════════════════════════════════
10
+ //
11
+ // Adds the ability to capture the HTML produced by Nodality primitives
12
+ // and write it to a file at BUILD TIME, without changing any other
13
+ // library file or the runtime mount path. The caller's existing
14
+ // `app.js` (or per-page script) keeps mounting dynamically in the
15
+ // browser — this module just runs the same builder once in Node,
16
+ // inside a jsdom-simulated DOM, and saves the resulting HTML so
17
+ // crawlers / AI bots / social-unfurl bots see the rendered page
18
+ // before any JavaScript executes.
19
+ //
20
+ // ─── Why this exists ────────────────────────────────────────────────
21
+ // Nodality apps construct the DOM imperatively in the browser
22
+ // (`new Text().set({...}).render("#mount")`). That works for users
23
+ // but leaves the raw HTML body essentially empty — `<div id="mount">
24
+ // </div>` plus a script tag. Most AI crawlers (GPTBot, ClaudeBot,
25
+ // PerplexityBot, CCBot) and every social-unfurl bot (Slack, LinkedIn,
26
+ // WhatsApp, iMessage, X) don't execute JavaScript, so they see nothing.
27
+ // Pre-rendering the same builder in Node gives those crawlers the
28
+ // real content. Users still get the JS-hydrated experience on top.
29
+ //
30
+ // ─── Design constraint ──────────────────────────────────────────────
31
+ // This file is the ONLY new module in the library; index.js,
32
+ // package.json, webpack.config.js, and every existing primitive are
33
+ // untouched. The caller is expected to import from this file path
34
+ // directly, e.g.:
35
+ //
36
+ // import { prerender } from "nodality/layout/prerender.js";
37
+ //
38
+ // If the caller wants to add a top-level export later, that's a
39
+ // one-line addition to `index.js` they can make on their own.
40
+ //
41
+ // ─── Usage ──────────────────────────────────────────────────────────
42
+ //
43
+ // import { prerender } from "nodality/layout/prerender.js";
44
+ //
45
+ // await prerender({
46
+ // template: "./page-template.html", // HTML shell (head + empty body)
47
+ // mount: "#mount", // selector inside the template
48
+ // locale: "de", // optional: pre-seeded into localStorage
49
+ // url: "https://h7active.com/", // jsdom base URL (used for relative paths)
50
+ // build: async (window) => {
51
+ // // Runs inside the jsdom-simulated browser. `window.document`,
52
+ // // `window.localStorage`, etc. are available globally for the
53
+ // // duration of the call. Import or invoke your page builder
54
+ // // here; it can use Nodality primitives exactly as in the
55
+ // // browser.
56
+ // await import("./app.js");
57
+ // },
58
+ // output: "./upload/de/index.html", // file to write
59
+ // });
60
+ //
61
+ // The function spins up jsdom, runs your builder, captures the full
62
+ // `<!DOCTYPE html>…</html>` document (preserving the template's
63
+ // <head>, the <script> tags pointing at runtime JS, AND the now-
64
+ // populated body), and writes it to `output`. When the live browser
65
+ // later loads that file, the runtime `app.js` still executes and
66
+ // remounts — the navigation, dropdowns, language switcher, scroll
67
+ // handlers, etc. all continue to work dynamically.
68
+ //
69
+ // ─── Peer dependency ────────────────────────────────────────────────
70
+ // `jsdom` is imported only by this file. It's listed as an optional
71
+ // peer dependency so the browser bundle stays small. Callers using
72
+ // prerender must `npm install jsdom` in their build environment.
73
+
74
+ import { promises as fs } from "node:fs";
75
+ import path from "node:path";
76
+
77
+ /**
78
+ * Render a Nodality-built page to a static HTML file.
79
+ *
80
+ * @param {object} opts
81
+ * @param {string} opts.template — Path to the HTML shell file. Must contain
82
+ * `<head>` and an empty mount container (e.g. `<div id="mount"></div>`).
83
+ * @param {string} [opts.mount="#mount"] — CSS selector for the mount point
84
+ * inside the template. The function verifies it exists before running
85
+ * the builder; missing selector throws (catches typos early).
86
+ * @param {string} [opts.locale] — If set, written to `window.localStorage`
87
+ * under the key `h7lang` before the builder runs. Lets locale-aware
88
+ * builders (e.g. those calling `detectLang()`) produce the correct
89
+ * translation without further plumbing.
90
+ * @param {string} [opts.localStorageKey="h7lang"] — Override the
91
+ * localStorage key used for the locale handshake if your app uses a
92
+ * different name.
93
+ * @param {string} [opts.url="http://localhost/"] — Base URL for the jsdom
94
+ * window. Affects `window.location`, relative-URL resolution, and the
95
+ * `Document.URL` your code may read.
96
+ * @param {(window: Window) => (void | Promise<void>)} opts.build —
97
+ * Async function that runs inside the simulated browser. Receives the
98
+ * jsdom `window` object. Should construct the page via Nodality
99
+ * primitives and call `.render(mount)`. Awaited so dynamic imports
100
+ * and async setup complete before serialization.
101
+ * @param {string} opts.output — Path where the rendered HTML is written.
102
+ * Parent directories are created automatically.
103
+ *
104
+ * @returns {Promise<{ bytes: number, output: string }>} — Resolves once
105
+ * the file is on disk. `bytes` is the rendered document size.
106
+ */
107
+ export async function prerender({
108
+ template,
109
+ mount = "#mount",
110
+ locale,
111
+ localStorageKey = "h7lang",
112
+ url = "http://localhost/",
113
+ build,
114
+ output,
115
+ }) {
116
+ if (!template) throw new Error("prerender: `template` is required");
117
+ if (!output) throw new Error("prerender: `output` is required");
118
+ if (typeof build !== "function") {
119
+ throw new Error("prerender: `build` must be an async function");
120
+ }
121
+
122
+ // Lazy import jsdom so this file can be imported in environments that
123
+ // don't have it installed (it's a peer dep). The error message is
124
+ // explicit because the default "Cannot find package 'jsdom'" stack
125
+ // trace is opaque to users who haven't seen it before.
126
+ let JSDOM;
127
+ try {
128
+ ({ JSDOM } = await import("jsdom"));
129
+ } catch (err) {
130
+ throw new Error(
131
+ "prerender: jsdom is required at build time. " +
132
+ "Install it in the consuming project: `npm install --save-dev jsdom`. " +
133
+ "Original error: " + err.message
134
+ );
135
+ }
136
+
137
+ // Read the HTML shell. Use absolute path resolution so relative
138
+ // `template` arguments (the common case from a build script) work
139
+ // regardless of process.cwd().
140
+ const templatePath = path.resolve(template);
141
+ const html = await fs.readFile(templatePath, "utf8");
142
+
143
+ // ─── jsdom setup ─────────────────────────────────────────────────
144
+ //
145
+ // `runScripts: "outside-only"` is the critical flag: it gives the
146
+ // window a working JS execution context (so `new Text()` etc. work
147
+ // inside the builder) BUT does NOT auto-execute `<script>` tags
148
+ // from the template. We deliberately don't want the template's
149
+ // `<script src="./app.js">` to run inside jsdom — paths there are
150
+ // production-relative and would 404; instead the caller's `build`
151
+ // function imports the same modules through Node's module system,
152
+ // which resolves them correctly.
153
+ //
154
+ // `pretendToBeVisual: true` enables requestAnimationFrame and a few
155
+ // related visual APIs. Cheap and prevents subtle "rAF is not a
156
+ // function" errors from libraries that schedule layout work.
157
+ const dom = new JSDOM(html, {
158
+ url,
159
+ runScripts: "outside-only",
160
+ pretendToBeVisual: true,
161
+ });
162
+
163
+ const { window } = dom;
164
+
165
+ // Optional locale seed — written before any global proxying so the
166
+ // builder sees it as soon as the proxy is in place.
167
+ if (locale) {
168
+ window.localStorage.setItem(localStorageKey, locale);
169
+ }
170
+
171
+ // ─── Browser API shims jsdom doesn't ship ───────────────────────
172
+ //
173
+ // These are no-ops; their job is to prevent ReferenceErrors when
174
+ // the builder constructs objects that reference observation APIs.
175
+ // Real observation isn't meaningful at build time (no scrolling,
176
+ // no resizing, no layout) and SSG doesn't need it.
177
+ if (!window.IntersectionObserver) {
178
+ window.IntersectionObserver = class {
179
+ constructor() {}
180
+ observe() {}
181
+ unobserve() {}
182
+ disconnect() {}
183
+ takeRecords() { return []; }
184
+ };
185
+ }
186
+ if (!window.ResizeObserver) {
187
+ window.ResizeObserver = class {
188
+ constructor() {}
189
+ observe() {}
190
+ unobserve() {}
191
+ disconnect() {}
192
+ };
193
+ }
194
+ if (!window.matchMedia) {
195
+ window.matchMedia = () => ({
196
+ matches: false,
197
+ media: "",
198
+ onchange: null,
199
+ addListener() {},
200
+ removeListener() {},
201
+ addEventListener() {},
202
+ removeEventListener() {},
203
+ dispatchEvent() { return false; },
204
+ });
205
+ }
206
+ // Some libraries probe these for code-splitting / analytics. Stub
207
+ // them as harmless no-ops so the builder doesn't crash; the live
208
+ // browser still uses the real implementations.
209
+ if (!window.fetch) {
210
+ window.fetch = () =>
211
+ Promise.resolve({ ok: true, status: 200, json: async () => ({}), text: async () => "" });
212
+ }
213
+ if (!window.dataLayer) window.dataLayer = [];
214
+ if (!window.gtag) window.gtag = () => {};
215
+
216
+ // ─── Global proxying ────────────────────────────────────────────
217
+ //
218
+ // Nodality primitives reach for `document`, `window`, etc. as bare
219
+ // globals — they were written for the browser, where those are
220
+ // already present. To run them in Node we hoist the jsdom-provided
221
+ // versions onto `globalThis` for the duration of the build call.
222
+ //
223
+ // We snapshot any prior values and restore them in the `finally`
224
+ // block so the parent process isn't polluted between successive
225
+ // prerender() invocations (e.g. a build loop over all locales).
226
+ const PROXIED = [
227
+ "window", "document", "localStorage", "sessionStorage", "navigator",
228
+ "location", "HTMLElement", "Element", "Node", "DocumentFragment",
229
+ "Event", "CustomEvent", "MouseEvent", "KeyboardEvent", "PointerEvent",
230
+ "IntersectionObserver", "ResizeObserver", "matchMedia",
231
+ "requestAnimationFrame", "cancelAnimationFrame", "getComputedStyle",
232
+ "fetch", "dataLayer", "gtag",
233
+ ];
234
+ // Some Node versions (22+) make certain globals — most notably
235
+ // `navigator` — getter-only on `globalThis`, which means a plain
236
+ // assignment (`globalThis.navigator = window.navigator`) throws
237
+ // "Cannot set property navigator of #<Object> which has only a
238
+ // getter". Inspect the property descriptor and skip read-only
239
+ // ones — the live browser's native object is fine to leave alone
240
+ // since jsdom provides its own copy on `window` that the builder
241
+ // can reach via `window.navigator`. Same defensive check for any
242
+ // other future getter-only globals Node may add.
243
+ const originalGlobals = {};
244
+ for (const key of PROXIED) {
245
+ if (key in globalThis) originalGlobals[key] = globalThis[key];
246
+ if (!(key in window)) continue;
247
+ const desc = Object.getOwnPropertyDescriptor(globalThis, key);
248
+ if (desc && desc.set === undefined && desc.writable === false) {
249
+ // Read-only built-in (e.g. Node 22+ `navigator`). Skip — the
250
+ // builder can still reach the jsdom copy via `window.<key>`.
251
+ continue;
252
+ }
253
+ try {
254
+ globalThis[key] = window[key];
255
+ } catch {
256
+ // Belt-and-braces: if descriptor inspection missed something
257
+ // (host-defined exotic, frozen prototype chain), swallow the
258
+ // assignment failure rather than aborting the whole render.
259
+ }
260
+ }
261
+
262
+ try {
263
+ // Sanity-check the mount point exists. Catches the common typo of
264
+ // changing the mount selector in the template without updating it
265
+ // here (or vice versa). Without this guard the builder runs but
266
+ // silently writes nothing useful.
267
+ if (mount && !window.document.querySelector(mount)) {
268
+ throw new Error(
269
+ `prerender: mount selector "${mount}" not found in template "${templatePath}". ` +
270
+ `Check that the template's <body> contains <div id="${mount.replace(/^#/, "")}"></div>.`
271
+ );
272
+ }
273
+
274
+ // Run the builder. Errors propagate so the caller knows the
275
+ // page didn't render — better than writing a half-built file.
276
+ await build(window);
277
+
278
+ // Drain the microtask queue so any queued promise.then handlers
279
+ // (e.g. dynamic imports inside the builder, fetch().then chains)
280
+ // finish before we serialize. jsdom's macrotask scheduling means
281
+ // setTimeout(0) callbacks may NOT fire — builders that rely on
282
+ // those won't have their output captured. Keep builders sync or
283
+ // promise-based for full coverage.
284
+ await new Promise((resolve) => setImmediate(resolve));
285
+
286
+ // Serialize the full document. `dom.serialize()` returns
287
+ // `<!DOCTYPE html><html>...</html>` including the template's
288
+ // head + the now-populated body. The runtime <script> tags from
289
+ // the template are preserved verbatim, so the live browser will
290
+ // re-execute app.js and re-mount on top of the static DOM.
291
+ const rendered = dom.serialize();
292
+
293
+ // Ensure the output directory exists. Writing to a path like
294
+ // `./upload/de/index.html` would otherwise fail if `de/` doesn't
295
+ // exist yet.
296
+ const outputPath = path.resolve(output);
297
+ await fs.mkdir(path.dirname(outputPath), { recursive: true });
298
+ await fs.writeFile(outputPath, rendered, "utf8");
299
+
300
+ return { bytes: rendered.length, output: outputPath };
301
+ } finally {
302
+ // Restore globalThis to its pre-call state. Critical when the
303
+ // caller runs prerender() in a loop — without this, the second
304
+ // invocation would inherit the FIRST jsdom's window, which has
305
+ // already been closed.
306
+ for (const key of PROXIED) {
307
+ if (key in originalGlobals) {
308
+ globalThis[key] = originalGlobals[key];
309
+ } else {
310
+ delete globalThis[key];
311
+ }
312
+ }
313
+ // Release jsdom's internal resources (timers, parser state).
314
+ // Without this the Node process can keep ticking long after
315
+ // the build script "finishes" because jsdom's setInterval
316
+ // callbacks are still alive.
317
+ window.close();
318
+ }
319
+ }
320
+
321
+ /**
322
+ * Convenience wrapper: prerender the same page once per locale and
323
+ * write each rendered file to a different output path.
324
+ *
325
+ * @param {object} opts — Same as `prerender()`, but `output` is a
326
+ * function `(locale) => string` and `locale` is a list.
327
+ * @param {string[]} opts.locales — Locale codes to render, e.g.
328
+ * `["cs", "en", "sk", "de", "fr"]`. Each value is passed to both
329
+ * the builder (via localStorage) and the `outputFor(locale)` fn.
330
+ * @param {(locale: string) => string} opts.outputFor — Returns the
331
+ * destination path for each locale.
332
+ *
333
+ * @returns {Promise<Array<{ locale: string, bytes: number, output: string }>>}
334
+ */
335
+ export async function prerenderEachLocale({
336
+ template,
337
+ mount,
338
+ url,
339
+ locales,
340
+ outputFor,
341
+ build,
342
+ localStorageKey,
343
+ }) {
344
+ if (!Array.isArray(locales) || locales.length === 0) {
345
+ throw new Error("prerenderEachLocale: `locales` must be a non-empty array");
346
+ }
347
+ if (typeof outputFor !== "function") {
348
+ throw new Error("prerenderEachLocale: `outputFor` must be (locale) => string");
349
+ }
350
+
351
+ const results = [];
352
+ for (const locale of locales) {
353
+ const result = await prerender({
354
+ template,
355
+ mount,
356
+ url,
357
+ locale,
358
+ localStorageKey,
359
+ build,
360
+ output: outputFor(locale),
361
+ });
362
+ results.push({ locale, ...result });
363
+ }
364
+ return results;
365
+ }
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * nodality v1.0.144
2
+ * nodality v1.0.146
3
3
  * (c) 2026 Filip Vabrousek
4
4
  * License: MIT
5
5
  */
package/layout/row.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * nodality v1.0.144
2
+ * nodality v1.0.146
3
3
  * (c) 2026 Filip Vabrousek
4
4
  * License: MIT
5
5
  */
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * nodality v1.0.144
2
+ * nodality v1.0.146
3
3
  * (c) 2026 Filip Vabrousek
4
4
  * License: MIT
5
5
  */
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * nodality v1.0.144
2
+ * nodality v1.0.146
3
3
  * (c) 2026 Filip Vabrousek
4
4
  * License: MIT
5
5
  */
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * nodality v1.0.144
2
+ * nodality v1.0.146
3
3
  * (c) 2026 Filip Vabrousek
4
4
  * License: MIT
5
5
  */
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * nodality v1.0.144
2
+ * nodality v1.0.146
3
3
  * (c) 2026 Filip Vabrousek
4
4
  * License: MIT
5
5
  */
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * nodality v1.0.144
2
+ * nodality v1.0.146
3
3
  * (c) 2026 Filip Vabrousek
4
4
  * License: MIT
5
5
  */
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * nodality v1.0.144
2
+ * nodality v1.0.146
3
3
  * (c) 2026 Filip Vabrousek
4
4
  * License: MIT
5
5
  */
package/layout/spacer.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * nodality v1.0.144
2
+ * nodality v1.0.146
3
3
  * (c) 2026 Filip Vabrousek
4
4
  * License: MIT
5
5
  */
package/layout/stack.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * nodality v1.0.144
2
+ * nodality v1.0.146
3
3
  * (c) 2026 Filip Vabrousek
4
4
  * License: MIT
5
5
  */
package/layout/styler.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * nodality v1.0.144
2
+ * nodality v1.0.146
3
3
  * (c) 2026 Filip Vabrousek
4
4
  * License: MIT
5
5
  */
package/layout/svg.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * nodality v1.0.144
2
+ * nodality v1.0.146
3
3
  * (c) 2026 Filip Vabrousek
4
4
  * License: MIT
5
5
  */
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * nodality v1.0.144
2
+ * nodality v1.0.146
3
3
  * (c) 2026 Filip Vabrousek
4
4
  * License: MIT
5
5
  */
package/layout/table.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * nodality v1.0.144
2
+ * nodality v1.0.146
3
3
  * (c) 2026 Filip Vabrousek
4
4
  * License: MIT
5
5
  */
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * nodality v1.0.144
2
+ * nodality v1.0.146
3
3
  * (c) 2026 Filip Vabrousek
4
4
  * License: MIT
5
5
  */
package/layout/text.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * nodality v1.0.144
2
+ * nodality v1.0.146
3
3
  * (c) 2026 Filip Vabrousek
4
4
  * License: MIT
5
5
  */
package/layout/ulist.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * nodality v1.0.144
2
+ * nodality v1.0.146
3
3
  * (c) 2026 Filip Vabrousek
4
4
  * License: MIT
5
5
  */
package/layout/video.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * nodality v1.0.144
2
+ * nodality v1.0.146
3
3
  * (c) 2026 Filip Vabrousek
4
4
  * License: MIT
5
5
  */
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * nodality v1.0.144
2
+ * nodality v1.0.146
3
3
  * (c) 2026 Filip Vabrousek
4
4
  * License: MIT
5
5
  */
package/layout/wrap.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * nodality v1.0.144
2
+ * nodality v1.0.146
3
3
  * (c) 2026 Filip Vabrousek
4
4
  * License: MIT
5
5
  */
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * nodality v1.0.144
2
+ * nodality v1.0.146
3
3
  * (c) 2026 Filip Vabrousek
4
4
  * License: MIT
5
5
  */
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * nodality v1.0.144
2
+ * nodality v1.0.146
3
3
  * (c) 2026 Filip Vabrousek
4
4
  * License: MIT
5
5
  */
package/lib/designer.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * nodality v1.0.144
2
+ * nodality v1.0.146
3
3
  * (c) 2026 Filip Vabrousek
4
4
  * License: MIT
5
5
  */
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * nodality v1.0.144
2
+ * nodality v1.0.146
3
3
  * (c) 2026 Filip Vabrousek
4
4
  * License: MIT
5
5
  */
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * nodality v1.0.144
2
+ * nodality v1.0.146
3
3
  * (c) 2026 Filip Vabrousek
4
4
  * License: MIT
5
5
  */
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * nodality v1.0.144
2
+ * nodality v1.0.146
3
3
  * (c) 2026 Filip Vabrousek
4
4
  * License: MIT
5
5
  */
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * nodality v1.0.144
2
+ * nodality v1.0.146
3
3
  * (c) 2026 Filip Vabrousek
4
4
  * License: MIT
5
5
  */
package/lib/stacker.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * nodality v1.0.144
2
+ * nodality v1.0.146
3
3
  * (c) 2026 Filip Vabrousek
4
4
  * License: MIT
5
5
  */
package/lib/theme.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * nodality v1.0.144
2
+ * nodality v1.0.146
3
3
  * (c) 2026 Filip Vabrousek
4
4
  * License: MIT
5
5
  */
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * nodality v1.0.144
2
+ * nodality v1.0.146
3
3
  * (c) 2026 Filip Vabrousek
4
4
  * License: MIT
5
5
  */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nodality",
3
- "version": "1.0.144",
3
+ "version": "1.0.146",
4
4
  "description": "A lightweight library for declarative UI elements.",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.esm.js",
@@ -13,7 +13,8 @@
13
13
  ".": {
14
14
  "require": "./dist/index.js",
15
15
  "import": "./dist/index.esm.js"
16
- }
16
+ },
17
+ "./ssg": "./layout/prerender.js"
17
18
  },
18
19
  "scripts": {
19
20
  "build": "webpack --config webpack.config.js",