ferrovue 0.7.0 → 0.7.1

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.
@@ -6,6 +6,10 @@ import { type App, type Plugin } from "vue";
6
6
  * `<input>`'s `value` and `checked`, a `<textarea>`'s `value`, an `<option>`'s `selected`) by its
7
7
  * property, which Vue's client sets where the server writes the attribute. */
8
8
  export declare function renderedShape(container: Element): string;
9
+ /** `renderedShape` of markup as the browser parsed it, untouched since: an `<option>`'s state is
10
+ * the selection the HTML parser gives it, from the `selected` attributes (the last one in a single
11
+ * `<select>`, or its first enabled option without one), which happy-dom's property misreports. */
12
+ export declare function parsedShape(container: Element): string;
9
13
  /** A plugin that takes what the app is mounted on, and its `renderedShape` as `mount` returns,
10
14
  * before any update an `onMounted` hook scheduled has run. */
11
15
  export declare function shapeAtMount(): {
@@ -3,7 +3,29 @@ import { Fragment, Static } from "vue";
3
3
  import { settled, stillLoading } from "./settle.js";
4
4
  const escapeText = (text) => text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
5
5
  const STATE_ATTRS = { INPUT: ["value", "checked"], TEXTAREA: ["value"], OPTION: ["selected"] };
6
- function element(el, scratch) {
6
+ function selectOf(option) {
7
+ const parent = option.parentElement;
8
+ const select = parent?.tagName === "OPTGROUP" ? parent.parentElement : parent;
9
+ return select?.tagName === "SELECT" ? select : null;
10
+ }
11
+ function optionsOf(select) {
12
+ return [...select.children].flatMap((child) => (child.tagName === "OPTION" ? [child] : child.tagName === "OPTGROUP" ? [...child.children].filter((c) => c.tagName === "OPTION") : []));
13
+ }
14
+ function parsedSelected(option) {
15
+ const select = selectOf(option);
16
+ if (!select || select.hasAttribute("multiple") || Number(select.getAttribute("size")) > 1)
17
+ return option.hasAttribute("selected");
18
+ const options = optionsOf(select);
19
+ const disabled = (o) => o.hasAttribute("disabled") || (o.parentElement?.tagName === "OPTGROUP" && o.parentElement.hasAttribute("disabled"));
20
+ const chosen = options.findLast((o) => o.hasAttribute("selected")) ?? options.find((o) => !disabled(o));
21
+ return option === chosen;
22
+ }
23
+ function stateOf(el, name, parsed) {
24
+ if (parsed && el.tagName === "OPTION")
25
+ return parsedSelected(el);
26
+ return el[name];
27
+ }
28
+ function element(el, scratch, parsed) {
7
29
  const state = STATE_ATTRS[el.tagName] ?? [];
8
30
  const attrs = el
9
31
  .getAttributeNames()
@@ -17,8 +39,8 @@ function element(el, scratch) {
17
39
  if (style)
18
40
  attrs.push(` style=${JSON.stringify(style)}`);
19
41
  for (const name of state)
20
- attrs.push(` .${name}=${JSON.stringify(el[name])}`);
21
- const children = el.tagName === "TEXTAREA" ? "" : nodes(el.childNodes, scratch);
42
+ attrs.push(` .${name}=${JSON.stringify(stateOf(el, name, parsed))}`);
43
+ const children = el.tagName === "TEXTAREA" ? "" : nodes(el.childNodes, scratch, parsed);
22
44
  return `<${el.localName}${attrs.join("")}>${children}</${el.localName}>`;
23
45
  }
24
46
  function styleShape(text, scratch) {
@@ -33,7 +55,7 @@ function styleShape(text, scratch) {
33
55
  }
34
56
  return [...own, ...custom.toSorted()].join(" ");
35
57
  }
36
- function nodes(list, scratch) {
58
+ function nodes(list, scratch, parsed) {
37
59
  let out = "";
38
60
  let text = "";
39
61
  for (const node of list) {
@@ -43,7 +65,7 @@ function nodes(list, scratch) {
43
65
  }
44
66
  if (node.nodeType !== 1)
45
67
  continue;
46
- out += escapeText(text) + element(node, scratch);
68
+ out += escapeText(text) + element(node, scratch, parsed);
47
69
  text = "";
48
70
  }
49
71
  return out + escapeText(text);
@@ -55,7 +77,13 @@ function nodes(list, scratch) {
55
77
  * `<input>`'s `value` and `checked`, a `<textarea>`'s `value`, an `<option>`'s `selected`) by its
56
78
  * property, which Vue's client sets where the server writes the attribute. */
57
79
  export function renderedShape(container) {
58
- return nodes(container.childNodes, container.ownerDocument.createElement("div"));
80
+ return nodes(container.childNodes, container.ownerDocument.createElement("div"), false);
81
+ }
82
+ /** `renderedShape` of markup as the browser parsed it, untouched since: an `<option>`'s state is
83
+ * the selection the HTML parser gives it, from the `selected` attributes (the last one in a single
84
+ * `<select>`, or its first enabled option without one), which happy-dom's property misreports. */
85
+ export function parsedShape(container) {
86
+ return nodes(container.childNodes, container.ownerDocument.createElement("div"), true);
59
87
  }
60
88
  /** A plugin that takes what the app is mounted on, and its `renderedShape` as `mount` returns,
61
89
  * before any update an `onMounted` hook scheduled has run. */
@@ -8,7 +8,7 @@ import { allPages, fileRoutes, pagesFolder, routeEntries } from "./file-routes.j
8
8
  import { fixtureApp, peer, readFixture } from "./fixture.js";
9
9
  import { headRendered, settled } from "./settle.js";
10
10
  import { attachSsrRender } from "./ssr.js";
11
- import { applyCssVars, renderDifference, renderedShape } from "./client-render.js";
11
+ import { applyCssVars, parsedShape, renderDifference, renderedShape } from "./client-render.js";
12
12
  export const TELEPORTS = "<!--fv-teleports-->";
13
13
  export const HEAD = "<!--fv-head-->";
14
14
  /** The head a recorded render wrote after its body, if it wrote one. */
@@ -204,7 +204,7 @@ export async function clientRenderDifference(app, html) {
204
204
  throw new Error("rendering a fixture on the client needs a DOM: run the suite with `environment: \"happy-dom\"` (or jsdom)");
205
205
  document.body.innerHTML = hydrationBody(html);
206
206
  const root = document.getElementById("root");
207
- const expected = renderedShape(root);
207
+ const expected = parsedShape(root);
208
208
  root.replaceChildren();
209
209
  app.config.warnHandler = () => { };
210
210
  const { warn } = console;
package/dist/hydration.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { mountPage, readPage, renderPage } from "./client.js";
2
- import { renderDifference, renderedShape, shapeAtMount } from "./client-render.js";
2
+ import { parsedShape, renderDifference, shapeAtMount } from "./client-render.js";
3
3
  import { settled } from "./settle.js";
4
4
  function recordScript(id, record) {
5
5
  const json = JSON.stringify(record).replace(/[<>&\u2028\u2029]/g, (ch) => `\\u${ch.charCodeAt(0).toString(16).padStart(4, "0")}`);
@@ -96,7 +96,7 @@ export async function renderRecordedPage(page, layout, components, options = {})
96
96
  const container = typeof selector === "string" ? doc.querySelector(selector) : selector;
97
97
  if (!container)
98
98
  throw new Error(`the recorded page has no ${selector}`);
99
- const expected = renderedShape(container);
99
+ const expected = parsedShape(container);
100
100
  const problems = [];
101
101
  const watch = {
102
102
  install(app) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ferrovue",
3
- "version": "0.7.0",
3
+ "version": "0.7.1",
4
4
  "description": "Vue templates compiled to Rust render functions: server-side rendering with no JavaScript at run time",
5
5
  "license": "MIT OR Apache-2.0",
6
6
  "repository": {