@uniflowed/router 0.16.0 → 0.18.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.
@@ -36,13 +36,35 @@ export type RoutingSettings = {|
36
36
  let installedBase: string = "";
37
37
  let installedSlash: TrailingSlash = "ignore";
38
38
 
39
+ /**
40
+ * The string `@uniflowed/web`'s vitals beacon reads. Duplicated there on
41
+ * purpose: that package does not depend on this one, and the symbol is not
42
+ * part of the public API. The two have to move together. ubugeeei-prod/uf#1701
43
+ */
44
+ const ROUTING_BASE: symbol = Symbol.for("@uniflowed/router.basePath");
45
+
46
+ function publishBase(base: string): void {
47
+ // `globalThis[symbol]` is a computed property on a namespace, which Flow
48
+ // rejects. `Object.defineProperty` is the write the rest of the repo uses.
49
+ Object.defineProperty(globalThis, ROUTING_BASE, {
50
+ value: base,
51
+ writable: true,
52
+ configurable: true,
53
+ });
54
+ }
55
+
39
56
  /**
40
57
  * Say where this application is served and how its paths are spelled. Called
41
58
  * once, by the entry that starts it.
59
+ *
60
+ * This is what publishes the base `vitalsBeacon` reads. The module does not
61
+ * publish when it loads: nothing installed is the root, and a shipped module
62
+ * cannot run a call at import time. ubugeeei-prod/uf#1701
42
63
  */
43
64
  export function installRouting(settings: RoutingSettings): void {
44
65
  installedBase = normalizeBase(settings.basePath ?? "");
45
66
  installedSlash = settings.trailingSlash ?? "ignore";
67
+ publishBase(installedBase);
46
68
  }
47
69
 
48
70
  /**
@@ -143,10 +165,18 @@ export function spellPath(path: string, policy: TrailingSlash, underBase: boolea
143
165
  return policy === "always" ? `${trimmed}/` : trimmed;
144
166
  }
145
167
 
146
- /** Whether a path's last segment has an extension. */
168
+ /**
169
+ * Whether a path's last segment is a file: it has an extension, or it is
170
+ * Apple's association file.
171
+ *
172
+ * That file has no dot, and it is served only without a trailing slash.
173
+ * `trailingSlash: "always"` must not add one. The server's copy is
174
+ * `@uniflowed/server`'s `internal/routing.js`; `routing.test.js` holds the
175
+ * two to one answer. ubugeeei-prod/uf#1704
176
+ */
147
177
  function looksLikeAFile(path: string): boolean {
148
178
  const last = path.slice(path.lastIndexOf("/") + 1);
149
- return last.includes(".");
179
+ return last.includes(".") || last.toLowerCase() === "apple-app-site-association";
150
180
  }
151
181
 
152
182
  function splitPath(to: string): {| readonly path: string, readonly rest: string |} {
package/internal/head.js CHANGED
@@ -10,6 +10,7 @@
10
10
 
11
11
  import * as React from "react";
12
12
 
13
+ import { addressOf } from "./base-path.js";
13
14
  import type { JsonLd, Metadata, Robots } from "./resolve.js";
14
15
 
15
16
  /**
@@ -22,18 +23,26 @@ import type { JsonLd, Metadata, Robots } from "./resolve.js";
22
23
  * Three things it deliberately does not do. It does not resolve against the
23
24
  * *page's* URL: `Head` renders inside the route and does not know it, and a
24
25
  * `metadataBase` is a site-wide fact rather than a per-page one. It does not
25
- * invent a base: with none declared the value is emitted exactly as written,
26
- * which is what every page that predates this field already gets. And it does
27
- * not throw — a `metadataBase` that is not a URL is a mistake in one field,
28
- * and turning it into a blank page would be a worse answer than an unresolved
29
- * `og:image`.
26
+ * invent a `metadataBase`: with none declared, a root-relative value is the
27
+ * address the router writes — the installed base in front of an application
28
+ * path — and every other value is emitted as written. The routing base is not
29
+ * a host, and a page that predates `metadataBase` still gets no origin invented
30
+ * for it. And it does not throw — a `metadataBase` that is not a URL is a
31
+ * mistake in one field, and turning it into a blank page would be a worse
32
+ * answer than an unresolved `og:image`.
30
33
  */
31
34
  function absoluteUrl(value: string, base: void | string): string {
32
- if (base == null) return value;
35
+ // `addressOf` leaves an absolute URL, a protocol-relative URL, `mailto:`,
36
+ // a query and a fragment as written, and it does not add a slash to a file.
37
+ // A path-absolute URL replaces `metadataBase`'s path, so the base has to be
38
+ // in the path before `URL` sees it or `og:image` drops `app.router.basePath`.
39
+ // ubugeeei-prod/uf#1705
40
+ const addressed = addressOf(value);
41
+ if (base == null) return addressed;
33
42
  try {
34
- return new URL(value, base).href;
43
+ return new URL(addressed, base).href;
35
44
  } catch {
36
- return value;
45
+ return addressed;
37
46
  }
38
47
  }
39
48
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@uniflowed/router",
3
- "version": "0.16.0",
3
+ "version": "0.18.0",
4
4
  "description": "The file-system router for Flow React applications: matching, layouts, loaders, navigation, server rendering and hydration.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -73,8 +73,8 @@
73
73
  }
74
74
  },
75
75
  "dependencies": {
76
- "@uniflowed/hooks": "0.16.0",
77
- "@uniflowed/server": "0.16.0",
78
- "@uniflowed/validator": "0.16.0"
76
+ "@uniflowed/hooks": "0.18.0",
77
+ "@uniflowed/server": "0.18.0",
78
+ "@uniflowed/validator": "0.18.0"
79
79
  }
80
80
  }