@ultimat3/render 22.3.2 → 22.3.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ultimat3/render",
3
- "version": "22.3.2",
3
+ "version": "22.3.4",
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,11 +36,11 @@
36
36
  "test": "bun test"
37
37
  },
38
38
  "dependencies": {
39
- "@ultimat3/cache": "22.3.2",
40
- "@ultimat3/core": "22.3.2",
41
- "@ultimat3/http": "22.3.2",
42
- "@ultimat3/i18n": "22.3.2",
43
- "@ultimat3/seo": "22.3.2",
39
+ "@ultimat3/cache": "22.3.4",
40
+ "@ultimat3/core": "22.3.4",
41
+ "@ultimat3/http": "22.3.4",
42
+ "@ultimat3/i18n": "22.3.4",
43
+ "@ultimat3/seo": "22.3.4",
44
44
  "sass": "1.104.0"
45
45
  }
46
46
  }
@@ -78,6 +78,14 @@ export interface Stylesheet {
78
78
  readonly surface: Surface | null;
79
79
  /** A plain (non-module) stylesheet: the tokens and the reset, which the cascade needs first. */
80
80
  readonly global: boolean;
81
+ /**
82
+ * Registered only by an ISLAND build, never by the server's module graph. Such a sheet is ordered
83
+ * by path, not by arrival: `Bun.build` loads an island graph in parallel, so its arrival order
84
+ * differs between processes, and the surface stylesheet — its URL a hash of the joined bytes —
85
+ * differed between two pods of one image (notificado.co, 22.3.2: one pod's `/styles/<hash>.css`
86
+ * was the other's 404, and each `sw.js` precached a sheet the other did not serve).
87
+ */
88
+ readonly island: boolean;
81
89
  readonly css: string;
82
90
  }
83
91
 
@@ -158,9 +166,17 @@ export function stylesFor(surface: Surface | null): string {
158
166
  );
159
167
  // `stripCharset` on every sheet, not only the first: a `@charset` or a BOM is legal at byte 0 of
160
168
  // a FILE and nowhere else, and this join is what turns seven files into one.
161
- return [...carried.filter((sheet) => sheet.global), ...carried.filter((sheet) => !sheet.global)]
162
- .map((sheet) => stripCharset(sheet.css))
163
- .join('');
169
+ //
170
+ // Within each group, the server graph's sheets keep load order — deterministic, because `loadApp`
171
+ // imports in sorted order, and meaningful, because a page's module loads after the component
172
+ // module it overrides — and the island-only sheets follow, by path.
173
+ const byPath = (a: Stylesheet, b: Stylesheet): number =>
174
+ a.file < b.file ? -1 : a.file > b.file ? 1 : 0;
175
+ const group = (global: boolean): Stylesheet[] => [
176
+ ...carried.filter((sheet) => sheet.global === global && !sheet.island),
177
+ ...carried.filter((sheet) => sheet.global === global && sheet.island).sort(byPath),
178
+ ];
179
+ return [...group(true), ...group(false)].map((sheet) => stripCharset(sheet.css)).join('');
164
180
  }
165
181
 
166
182
  /**
@@ -181,17 +197,26 @@ export function transformTsx(source: string): string {
181
197
  * compilation — so a token file `@use`d by twenty `page.module.scss` would inline its `:root` block
182
198
  * twenty times. One file, imported for its side effect, is the shape that cannot do that.
183
199
  */
184
- export function loadStylesheet(path: string, source: string): string {
200
+ export function loadStylesheet(
201
+ path: string,
202
+ source: string,
203
+ origin: 'module' | 'island' = 'module',
204
+ ): string {
185
205
  const compiled = compileStylesheet(path, source);
186
206
  // An EMPTY compile unregisters: under `x dev` an edit that deleted every rule left the old entry
187
207
  // in place, serving rules the file no longer had until the process restarted.
188
208
  if (compiled.css.length === 0 && stylesheets.delete(path)) revision += 1;
189
209
  if (compiled.css.length > 0) {
190
- if (stylesheets.get(path)?.css !== compiled.css) revision += 1;
210
+ const held = stylesheets.get(path);
211
+ // Island-only until the server graph loads it too; then it takes its place in load order.
212
+ const island = origin === 'island' && (held === undefined || held.island);
213
+ if (held?.css !== compiled.css || held.island !== island) revision += 1;
214
+ if (held?.island === true && !island) stylesheets.delete(path);
191
215
  stylesheets.set(path, {
192
216
  file: path,
193
217
  surface: surfaceOfSheet(path),
194
218
  global: isGlobalStylesheet(path),
219
+ island,
195
220
  css: compiled.css,
196
221
  });
197
222
  }
package/src/registry.ts CHANGED
@@ -68,7 +68,19 @@ export interface RouteDescriptor {
68
68
  readonly prerenderable: boolean;
69
69
  readonly dynamic: boolean;
70
70
  readonly hasPolicy: boolean;
71
+ /**
72
+ * The document is rendered FOR someone: a `policy`, a `stream` (always `private, no-store`), or a
73
+ * declared `cache` of `no-store` / `private`. `sw.js` never caches one (`@ultimat3/pwa`'s
74
+ * `strategyFor`): a per-member page kept for offline answered the previous member's data on a
75
+ * shared device after sign-out.
76
+ */
77
+ readonly personal: boolean;
71
78
  readonly islands: readonly string[];
79
+ /**
80
+ * Each island's `src`, relative to `file`, in `islands` order — what the island bundle resolves
81
+ * to a chunk URL, so `sw.js` precaches only the chunks a precached page boots.
82
+ */
83
+ readonly islandSources: readonly string[];
72
84
  readonly budgetJs: string | null;
73
85
  readonly budgetLcp: number | null;
74
86
  }
@@ -361,12 +373,22 @@ function buildDescriptors(): RouteDescriptor[] {
361
373
  prerenderable: entry.config.prerender !== undefined,
362
374
  dynamic: entry.pattern.keys.length > 0,
363
375
  hasPolicy: entry.config.policy !== undefined,
376
+ personal: isPersonal(entry.config),
364
377
  islands: entry.islands,
378
+ islandSources: entry.config.islands.map((spec) => spec.src),
365
379
  budgetJs: entry.config.budget.js ?? null,
366
380
  budgetLcp: entry.config.budget.lcp ?? null,
367
381
  }));
368
382
  }
369
383
 
384
+ /** See `RouteDescriptor.personal`. */
385
+ function isPersonal(config: RouteConfig): boolean {
386
+ if (config.policy !== undefined || config.render === 'stream') return true;
387
+ const cache = config.cache;
388
+ if (cache === undefined) return false;
389
+ return cache === 'no-store' || cache.mode === 'no-store' || cache.mode === 'private';
390
+ }
391
+
370
392
  /**
371
393
  * `undefined` for a malformed percent-escape. A pathname is whatever the client typed, and
372
394
  * `decodeURIComponent('%zz')` throws a bare `URIError` — no code, no fix line — where a router