@gusnips/react 0.4.2 → 0.4.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/hydrate.d.ts CHANGED
@@ -30,6 +30,16 @@ export { PRERENDERED_ROUTE_ATTR, SHELL_ROUTE } from "./prerender-contract.ts";
30
30
  * serves `/pt/pricing` from a file rendered for `/pricing`, with the locale carried in the
31
31
  * router's `basename` — so the address and the route are different strings, and only the caller
32
32
  * knows which one the build wrote.
33
+ *
34
+ * **A trailing slash is not a different page.** The router renders `/precos/` and `/precos` alike,
35
+ * and which one is in the address bar is the host's call: a host serving `precos/index.html`
36
+ * redirects `/precos` to `/precos/` by default (Firebase Hosting does). Compared exactly, that was
37
+ * false on every page of one adopter: every prerendered body was thrown away and drawn again, on a
38
+ * page that looks the same either way.
39
+ *
40
+ * **`null` means nothing the build wrote can be this reader's page.** The case is a site that
41
+ * prerenders one language and detects the reader's at runtime: Portuguese markup hydrated with
42
+ * Spanish strings is the wrong-page mismatch again, spread across every string on the page.
33
43
  */
34
- export declare function hydrateOrMount(rootEl: Element, tree: ReactNode, route: string): void;
44
+ export declare function hydrateOrMount(rootEl: Element, tree: ReactNode, route: string | null): void;
35
45
  //# sourceMappingURL=hydrate.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"hydrate.d.ts","sourceRoot":"","sources":["../src/hydrate.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AACH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAIvC,OAAO,EAAE,sBAAsB,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAC;AAE9E;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,cAAc,CAAC,MAAM,EAAE,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI,CAGpF"}
1
+ {"version":3,"file":"hydrate.d.ts","sourceRoot":"","sources":["../src/hydrate.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AACH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAIvC,OAAO,EAAE,sBAAsB,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAC;AAE9E;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,wBAAgB,cAAc,CAAC,MAAM,EAAE,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,IAAI,CAI3F"}
package/dist/hydrate.js CHANGED
@@ -15,11 +15,27 @@ export { PRERENDERED_ROUTE_ATTR, SHELL_ROUTE } from "./prerender-contract.js";
15
15
  * serves `/pt/pricing` from a file rendered for `/pricing`, with the locale carried in the
16
16
  * router's `basename` — so the address and the route are different strings, and only the caller
17
17
  * knows which one the build wrote.
18
+ *
19
+ * **A trailing slash is not a different page.** The router renders `/precos/` and `/precos` alike,
20
+ * and which one is in the address bar is the host's call: a host serving `precos/index.html`
21
+ * redirects `/precos` to `/precos/` by default (Firebase Hosting does). Compared exactly, that was
22
+ * false on every page of one adopter: every prerendered body was thrown away and drawn again, on a
23
+ * page that looks the same either way.
24
+ *
25
+ * **`null` means nothing the build wrote can be this reader's page.** The case is a site that
26
+ * prerenders one language and detects the reader's at runtime: Portuguese markup hydrated with
27
+ * Spanish strings is the wrong-page mismatch again, spread across every string on the page.
18
28
  */
19
29
  export function hydrateOrMount(rootEl, tree, route) {
20
- if (rootEl.getAttribute(PRERENDERED_ROUTE_ATTR) === route)
30
+ const rendered = rootEl.getAttribute(PRERENDERED_ROUTE_ATTR);
31
+ if (route !== null && rendered !== null && samePath(rendered, route))
21
32
  hydrateRoot(rootEl, tree);
22
33
  else
23
34
  createRoot(rootEl).render(tree);
24
35
  }
36
+ /** `/precos/` and `/precos` are one page; `/` stays `/`. */
37
+ function samePath(a, b) {
38
+ const trim = (path) => path.replace(/(.)\/+$/, "$1");
39
+ return trim(a) === trim(b);
40
+ }
25
41
  //# sourceMappingURL=hydrate.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"hydrate.js","sourceRoot":"","sources":["../src/hydrate.ts"],"names":[],"mappings":"AAiBA,OAAO,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAC3D,OAAO,EAAE,sBAAsB,EAAE,MAAM,yBAAyB,CAAC;AAEjE,OAAO,EAAE,sBAAsB,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAC;AAE9E;;;;;;;;;;;;;;GAcG;AACH,MAAM,UAAU,cAAc,CAAC,MAAe,EAAE,IAAe,EAAE,KAAa;IAC5E,IAAI,MAAM,CAAC,YAAY,CAAC,sBAAsB,CAAC,KAAK,KAAK;QAAE,WAAW,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC;;QAChF,UAAU,CAAC,MAAM,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;AACvC,CAAC"}
1
+ {"version":3,"file":"hydrate.js","sourceRoot":"","sources":["../src/hydrate.ts"],"names":[],"mappings":"AAiBA,OAAO,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAC3D,OAAO,EAAE,sBAAsB,EAAE,MAAM,yBAAyB,CAAC;AAEjE,OAAO,EAAE,sBAAsB,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAC;AAE9E;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,MAAM,UAAU,cAAc,CAAC,MAAe,EAAE,IAAe,EAAE,KAAoB;IACnF,MAAM,QAAQ,GAAG,MAAM,CAAC,YAAY,CAAC,sBAAsB,CAAC,CAAC;IAC7D,IAAI,KAAK,KAAK,IAAI,IAAI,QAAQ,KAAK,IAAI,IAAI,QAAQ,CAAC,QAAQ,EAAE,KAAK,CAAC;QAAE,WAAW,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC;;QAC3F,UAAU,CAAC,MAAM,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;AACvC,CAAC;AAED,4DAA4D;AAC5D,SAAS,QAAQ,CAAC,CAAS,EAAE,CAAS;IACpC,MAAM,IAAI,GAAG,CAAC,IAAY,EAAU,EAAE,CAAC,IAAI,CAAC,OAAO,CAAC,SAAS,EAAE,IAAI,CAAC,CAAC;IACrE,OAAO,IAAI,CAAC,CAAC,CAAC,KAAK,IAAI,CAAC,CAAC,CAAC,CAAC;AAC7B,CAAC"}
package/dist/index.js CHANGED
@@ -13,11 +13,11 @@
13
13
  // @gusnips/react/guards the route guards → react-router-dom
14
14
  // @gusnips/react/hydrate hydrateOrMount → react-dom
15
15
  //
16
- // `react-dom` is the one the second migration added, and it is the sharpest case yet: olhary
17
- // ships a REACT NATIVE app beside its two web apps, and React Native has no react-dom to give.
18
- // A required peer nobody can satisfy is not a strict contract, it is a closed door — the whole
19
- // package was unusable there, for one function no phone would ever call. The rule did not need
20
- // changing to catch it; `react-dom` simply had to stop being required.
16
+ // `react-dom` is the one the second migration added, and it is the sharpest case yet: that
17
+ // adopter ships a REACT NATIVE app beside its two web apps, and React Native has no react-dom to
18
+ // give. A required peer nobody can satisfy is not a strict contract, it is a closed door — the
19
+ // whole package was unusable there, for one function no phone would ever call. The rule did not
20
+ // need changing to catch it; `react-dom` simply had to stop being required.
21
21
  //
22
22
  // So what is left here imports `react` and `@gusnips/http` and nothing else, and an app on
23
23
  // TanStack Router, on Redux, or on a phone takes the fetch client without installing a router, a
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,kEAAkE;AAClE,EAAE;AACF,2CAA2C;AAC3C,EAAE;AACF,+FAA+F;AAC/F,gGAAgG;AAChG,+FAA+F;AAC/F,iGAAiG;AACjG,0BAA0B;AAC1B,EAAE;AACF,oEAAoE;AACpE,6DAA6D;AAC7D,sEAAsE;AACtE,+DAA+D;AAC/D,EAAE;AACF,6FAA6F;AAC7F,+FAA+F;AAC/F,+FAA+F;AAC/F,+FAA+F;AAC/F,uEAAuE;AACvE,EAAE;AACF,2FAA2F;AAC3F,iGAAiG;AACjG,qFAAqF;AACrF,6FAA6F;AAC7F,iGAAiG;AACjG,yFAAyF;AACzF,uBAAuB;AAEvB,OAAO,EAAE,EAAE,EAAE,MAAM,SAAS,CAAC;AAE7B,OAAO,EAAE,QAAQ,EAAE,YAAY,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AACxE,OAAO,EACL,eAAe,GAMhB,MAAM,iBAAiB,CAAC;AACzB,OAAO,EAAE,eAAe,EAAE,aAAa,EAAiB,MAAM,UAAU,CAAC;AAEzE,OAAO,EAAE,aAAa,EAAE,YAAY,EAAE,WAAW,EAA6B,MAAM,YAAY,CAAC;AAEjG,OAAO,EACL,aAAa,GAGd,MAAM,qBAAsB,CAAC;AAC9B,OAAO,EACL,oBAAoB,EACpB,YAAY,GAMb,MAAM,qBAAqB,CAAC;AAE7B,OAAO,EACL,0BAA0B,EAC1B,gBAAgB,EAChB,oBAAoB,EACpB,uBAAuB,GACxB,MAAM,mBAAmB,CAAC;AAE3B,iGAAiG;AACjG,kGAAkG;AAClG,6FAA6F;AAC7F,OAAO,EAAE,sBAAsB,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAC;AAE9E,OAAO,EAAE,cAAc,EAAE,eAAe,EAAwB,MAAM,WAAW,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,kEAAkE;AAClE,EAAE;AACF,2CAA2C;AAC3C,EAAE;AACF,+FAA+F;AAC/F,gGAAgG;AAChG,+FAA+F;AAC/F,iGAAiG;AACjG,0BAA0B;AAC1B,EAAE;AACF,oEAAoE;AACpE,6DAA6D;AAC7D,sEAAsE;AACtE,+DAA+D;AAC/D,EAAE;AACF,2FAA2F;AAC3F,iGAAiG;AACjG,+FAA+F;AAC/F,gGAAgG;AAChG,4EAA4E;AAC5E,EAAE;AACF,2FAA2F;AAC3F,iGAAiG;AACjG,qFAAqF;AACrF,6FAA6F;AAC7F,iGAAiG;AACjG,yFAAyF;AACzF,uBAAuB;AAEvB,OAAO,EAAE,EAAE,EAAE,MAAM,SAAS,CAAC;AAE7B,OAAO,EAAE,QAAQ,EAAE,YAAY,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AACxE,OAAO,EACL,eAAe,GAMhB,MAAM,iBAAiB,CAAC;AACzB,OAAO,EAAE,eAAe,EAAE,aAAa,EAAiB,MAAM,UAAU,CAAC;AAEzE,OAAO,EAAE,aAAa,EAAE,YAAY,EAAE,WAAW,EAA6B,MAAM,YAAY,CAAC;AAEjG,OAAO,EACL,aAAa,GAGd,MAAM,qBAAsB,CAAC;AAC9B,OAAO,EACL,oBAAoB,EACpB,YAAY,GAMb,MAAM,qBAAqB,CAAC;AAE7B,OAAO,EACL,0BAA0B,EAC1B,gBAAgB,EAChB,oBAAoB,EACpB,uBAAuB,GACxB,MAAM,mBAAmB,CAAC;AAE3B,iGAAiG;AACjG,kGAAkG;AAClG,6FAA6F;AAC7F,OAAO,EAAE,sBAAsB,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAC;AAE9E,OAAO,EAAE,cAAc,EAAE,eAAe,EAAwB,MAAM,WAAW,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gusnips/react",
3
- "version": "0.4.2",
3
+ "version": "0.4.4",
4
4
  "description": "The headless runtime under a Vite + React SPA: typed fetch client with single-flight refresh, auth store, query defaults, route guards, error boundary, chunk-reload recovery, i18n bootstrap.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -0,0 +1,65 @@
1
+ import { beforeEach, describe, expect, it, vi } from "vitest";
2
+ import { hydrateOrMount, SHELL_ROUTE } from "./hydrate.ts";
3
+
4
+ // Both roots are mocked: the question is only WHICH one the entry picks, and either choice is
5
+ // silent in a real browser — a wrong mount looks like a hydrate and a wrong hydrate recovers.
6
+ const roots = vi.hoisted(() => ({
7
+ hydrateRoot: vi.fn(),
8
+ createRoot: vi.fn(() => ({ render: vi.fn() })),
9
+ }));
10
+ vi.mock("react-dom/client", () => roots);
11
+
12
+ /** A root whose file names `route` — or names nothing, for `null`. */
13
+ const rootFor = (route: string | null): Element =>
14
+ ({ getAttribute: () => route }) as unknown as Element; // Only `getAttribute` is read.
15
+
16
+ const picked = (): "hydrate" | "mount" => {
17
+ const hydrated = roots.hydrateRoot.mock.calls.length > 0;
18
+ const mounted = roots.createRoot.mock.calls.length > 0;
19
+ expect(hydrated !== mounted).toBe(true);
20
+ return hydrated ? "hydrate" : "mount";
21
+ };
22
+
23
+ beforeEach(() => {
24
+ vi.clearAllMocks();
25
+ });
26
+
27
+ describe("hydrateOrMount", () => {
28
+ it("hydrates the render this file is", () => {
29
+ hydrateOrMount(rootFor("/precos"), null, "/precos");
30
+ expect(picked()).toBe("hydrate");
31
+ });
32
+
33
+ it.each([
34
+ ["the address has one", "/precos", "/precos/"],
35
+ ["the file has one", "/precos/", "/precos"],
36
+ ])("ignores a trailing slash when %s", (_case, rendered, route) => {
37
+ hydrateOrMount(rootFor(rendered), null, route);
38
+ expect(picked()).toBe("hydrate");
39
+ });
40
+
41
+ it("keeps the front page's slash — it is the whole path", () => {
42
+ hydrateOrMount(rootFor("/"), null, "/");
43
+ expect(picked()).toBe("hydrate");
44
+ });
45
+
46
+ it("mounts fresh over a different page", () => {
47
+ hydrateOrMount(rootFor("/precos"), null, "/faq");
48
+ expect(picked()).toBe("mount");
49
+ });
50
+
51
+ it("mounts fresh over a shell, whatever the address", () => {
52
+ hydrateOrMount(rootFor(SHELL_ROUTE), null, "/nao-existe");
53
+ expect(picked()).toBe("mount");
54
+ });
55
+
56
+ it("mounts fresh over a file that names no route", () => {
57
+ hydrateOrMount(rootFor(null), null, "/precos");
58
+ expect(picked()).toBe("mount");
59
+ });
60
+
61
+ it("mounts fresh when the reader cannot be shown this file at all", () => {
62
+ hydrateOrMount(rootFor("/precos"), null, null);
63
+ expect(picked()).toBe("mount");
64
+ });
65
+ });
package/src/hydrate.ts CHANGED
@@ -34,8 +34,25 @@ export { PRERENDERED_ROUTE_ATTR, SHELL_ROUTE } from "./prerender-contract.ts";
34
34
  * serves `/pt/pricing` from a file rendered for `/pricing`, with the locale carried in the
35
35
  * router's `basename` — so the address and the route are different strings, and only the caller
36
36
  * knows which one the build wrote.
37
+ *
38
+ * **A trailing slash is not a different page.** The router renders `/precos/` and `/precos` alike,
39
+ * and which one is in the address bar is the host's call: a host serving `precos/index.html`
40
+ * redirects `/precos` to `/precos/` by default (Firebase Hosting does). Compared exactly, that was
41
+ * false on every page of one adopter: every prerendered body was thrown away and drawn again, on a
42
+ * page that looks the same either way.
43
+ *
44
+ * **`null` means nothing the build wrote can be this reader's page.** The case is a site that
45
+ * prerenders one language and detects the reader's at runtime: Portuguese markup hydrated with
46
+ * Spanish strings is the wrong-page mismatch again, spread across every string on the page.
37
47
  */
38
- export function hydrateOrMount(rootEl: Element, tree: ReactNode, route: string): void {
39
- if (rootEl.getAttribute(PRERENDERED_ROUTE_ATTR) === route) hydrateRoot(rootEl, tree);
48
+ export function hydrateOrMount(rootEl: Element, tree: ReactNode, route: string | null): void {
49
+ const rendered = rootEl.getAttribute(PRERENDERED_ROUTE_ATTR);
50
+ if (route !== null && rendered !== null && samePath(rendered, route)) hydrateRoot(rootEl, tree);
40
51
  else createRoot(rootEl).render(tree);
41
52
  }
53
+
54
+ /** `/precos/` and `/precos` are one page; `/` stays `/`. */
55
+ function samePath(a: string, b: string): boolean {
56
+ const trim = (path: string): string => path.replace(/(.)\/+$/, "$1");
57
+ return trim(a) === trim(b);
58
+ }
package/src/index.ts CHANGED
@@ -13,11 +13,11 @@
13
13
  // @gusnips/react/guards the route guards → react-router-dom
14
14
  // @gusnips/react/hydrate hydrateOrMount → react-dom
15
15
  //
16
- // `react-dom` is the one the second migration added, and it is the sharpest case yet: olhary
17
- // ships a REACT NATIVE app beside its two web apps, and React Native has no react-dom to give.
18
- // A required peer nobody can satisfy is not a strict contract, it is a closed door — the whole
19
- // package was unusable there, for one function no phone would ever call. The rule did not need
20
- // changing to catch it; `react-dom` simply had to stop being required.
16
+ // `react-dom` is the one the second migration added, and it is the sharpest case yet: that
17
+ // adopter ships a REACT NATIVE app beside its two web apps, and React Native has no react-dom to
18
+ // give. A required peer nobody can satisfy is not a strict contract, it is a closed door — the
19
+ // whole package was unusable there, for one function no phone would ever call. The rule did not
20
+ // need changing to catch it; `react-dom` simply had to stop being required.
21
21
  //
22
22
  // So what is left here imports `react` and `@gusnips/http` and nothing else, and an app on
23
23
  // TanStack Router, on Redux, or on a phone takes the fetch client without installing a router, a