@ultimat3/render 19.2.0 → 19.3.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.
package/CLAUDE.md CHANGED
@@ -69,6 +69,7 @@ for the mirror-image reason, keeping `render → pwa` refused. Never `cli` (upwa
69
69
  | Route filename | `page.tsx` under `site/`/`app/`, `route.ts` under `api/` — `ROUTE_FILENAME`, one per surface. The URL is the directory path. Anything else is `X_ROUTE_FILE_INVALID`; never widen the table to accept a second spelling. |
70
70
  | Registry input | descriptors only. `registerRoute` refuses a raw declaration with `X_ROUTE_UNNORMALIZED` — `defineRoute` is the one normalizer of everything the declaration alone decides, and every reader downstream assumes it ran. The registry fills in exactly one value on top: the island budget, which needs the surface, which is a fact of the file path only the route table reads. |
71
71
  | Descriptors | `describeRoutes()` must stay JSON-safe, sorted by path, deterministic. |
72
+ | Every string order in this package | `byCodeUnit` (`code-unit-order.ts`), never `localeCompare`. With no locale argument `localeCompare` answers from the runtime's ICU **default locale** and **collation version**: `'/A'` sorted after `'/a'` on one machine and before it on the next, and `'/zoo'` before `'/ärzte'` under `sv-SE` but after it under `en-US` — for the same route table. Three sites shipped it: `routeEntries()` (so `describeRoutes()`, and with it `x.manifest.json`, the sitemap and `sw.js`'s rule table), `checkSurfaceBoundary`'s report, and `pageComponentOf`'s fallback, whose own comment promised "the same one on every machine". Package-internal on purpose — a comparator is not public API. Same rule `@ultimat3/pwa`'s `precache.ts` and `service-worker.ts` state for the artifact they emit. |
72
73
  | Boundary | `surfaces.ts` throws; it never warns. Type-only edges are not violations. |
73
74
  | Stream cancellation | the underlying source has a `cancel()`, and `write` is guarded on it. A client that disconnects mid-stream aborts `StreamHole.resolve(signal)` and every later `write`/`close` is a no-op — `settle()` on a cancelled controller threw out of a `void`ed promise, one unhandled rejection per response, while the resolved holes kept doing their database work with nowhere to write. |
74
75
  | ISR detach | `attach()`'s returned function clears the revalidator as well as the dependents — and only if the slot is still its own, tracked in `installedRevalidator` because `@ultimat3/cache` holds ONE and offers no read back. Left installed, a detached controller and its whole store stayed reachable and kept receiving revalidations while the live one's pages never went stale. |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ultimat3/render",
3
- "version": "19.2.0",
3
+ "version": "19.3.1",
4
4
  "description": "The route primitive and the five render modes: static, isr, ssr, stream, spa.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -36,10 +36,10 @@
36
36
  "test": "bun test"
37
37
  },
38
38
  "dependencies": {
39
- "@ultimat3/cache": "19.2.0",
40
- "@ultimat3/core": "19.2.0",
41
- "@ultimat3/i18n": "19.2.0",
42
- "@ultimat3/seo": "19.2.0",
39
+ "@ultimat3/cache": "19.3.1",
40
+ "@ultimat3/core": "19.3.1",
41
+ "@ultimat3/i18n": "19.3.1",
42
+ "@ultimat3/seo": "19.3.1",
43
43
  "sass": "1.102.0"
44
44
  }
45
45
  }
@@ -0,0 +1,18 @@
1
+ /**
2
+ * The one string comparator this package orders derived output with.
3
+ *
4
+ * Package-internal on purpose: it is the ordering rule, not public API. Nothing outside
5
+ * `@ultimat3/render` may need it, and a comparator on the barrel is a shape apps would depend on.
6
+ */
7
+
8
+ /**
9
+ * Compare by UTF-16 code unit — never `localeCompare`, which with no locale argument answers from
10
+ * the runtime's ICU **default locale** and **collation version**: `'/A'` sorts after `'/a'` on one
11
+ * machine and before it on the next, and `'/zoo'` before `'/ärzte'` under `sv-SE` but after it
12
+ * under `en-US`, for the same input. Everything this package orders is either diffed across
13
+ * deploys (`describeRoutes()` feeds `x.manifest.json`, the sitemap and `sw.js`'s rule table) or is
14
+ * a choice a build makes (`pageComponentOf`'s fallback), so a machine-dependent order is a no-op
15
+ * deploy that reads as a change — or a different page rendered per host. Same rule
16
+ * `@ultimat3/pwa`'s `precache.ts` and `service-worker.ts` state for the artifact they emit.
17
+ */
18
+ export const byCodeUnit = (a: string, b: string): number => (a < b ? -1 : a > b ? 1 : 0);
package/src/registry.ts CHANGED
@@ -7,6 +7,7 @@
7
7
 
8
8
  import type { HydrateStrategy, OfflineStrategy, RenderMode } from '@ultimat3/core';
9
9
  import { finiteCount } from '@ultimat3/core';
10
+ import { byCodeUnit } from './code-unit-order';
10
11
  import {
11
12
  RouteDuplicateError,
12
13
  RouteFileInvalidError,
@@ -322,7 +323,9 @@ export function routeCount(): number {
322
323
  }
323
324
 
324
325
  export function routeEntries(): readonly RouteEntry[] {
325
- return [...routes.values()].sort((a, b) => a.path.localeCompare(b.path));
326
+ // Code units, never `localeCompare` — `describeRoutes()` below promises an order "identical for
327
+ // identical input", and `localeCompare` with no locale argument reads the runtime's ICU default.
328
+ return [...routes.values()].sort((a, b) => byCodeUnit(a.path, b.path));
326
329
  }
327
330
 
328
331
  export function routeFor(path: string): RouteEntry | undefined {
@@ -4,6 +4,7 @@
4
4
  * ship today — so the rule is a fixed precedence, evaluated once, here.
5
5
  */
6
6
 
7
+ import { byCodeUnit } from './code-unit-order';
7
8
  import type { JsxComponent } from './jsx';
8
9
 
9
10
  /** The page component of a route module: a function of props, sync or async. */
@@ -15,14 +16,17 @@ const isComponentExport = (name: string, value: unknown): value is RouteComponen
15
16
  /**
16
17
  * `Page` first, because that is the name `examples/dummy` uses and the one the generators should
17
18
  * converge on; then a single `…Page`; then a single capitalised function. Sorted before the last
18
- * fallback so a module with two components resolves to the same one on every machine.
19
+ * fallback so a module with two components resolves to the same one on every machine — by CODE
20
+ * UNIT, because `localeCompare` is what "every machine" fails on: with no locale argument it
21
+ * answers from the runtime's ICU default, which orders `A_Dash` before `ADash` where code units
22
+ * order them the other way.
19
23
  */
20
24
  export function pageComponentOf(
21
25
  module: Readonly<Record<string, unknown>>,
22
26
  ): RouteComponent | undefined {
23
27
  const components = Object.entries(module)
24
28
  .filter(([name, value]) => isComponentExport(name, value))
25
- .sort(([a], [b]) => a.localeCompare(b)) as readonly (readonly [string, RouteComponent])[];
29
+ .sort(([a], [b]) => byCodeUnit(a, b)) as readonly (readonly [string, RouteComponent])[];
26
30
  if (components.length === 0) return undefined;
27
31
 
28
32
  const exact = components.find(([name]) => name === 'Page');
package/src/surfaces.ts CHANGED
@@ -6,6 +6,7 @@
6
6
  */
7
7
 
8
8
  import type { RenderMode } from '@ultimat3/core';
9
+ import { byCodeUnit } from './code-unit-order';
9
10
  import { SurfaceBoundaryError } from './errors';
10
11
 
11
12
  export type Surface = 'site' | 'app' | 'api' | 'shared';
@@ -147,7 +148,10 @@ export function checkSurfaceBoundary(graph: ImportGraph): readonly BoundaryViola
147
148
  walk(graph, entry, entrySurface, found);
148
149
  }
149
150
 
150
- return [...found.values()].sort((a, b) => keyOf(a).localeCompare(keyOf(b)));
151
+ // Code units, never `localeCompare`: two machines running the same check must report the same
152
+ // list in the same order, and `localeCompare` with no locale argument reads the runtime's ICU
153
+ // default locale and collation version.
154
+ return [...found.values()].sort((a, b) => byCodeUnit(keyOf(a), keyOf(b)));
151
155
  }
152
156
 
153
157
  function keyOf(v: BoundaryViolation): string {