Ultimate

6.x → 7.0.0, entry by entry

Four breaking entries, and only one of them can reach you at runtime. Three are compile errors the moment you upgrade; the fourth is a type you may never have named. None ships a codemod.

#SurfaceCosts you an edit if
1ScrapeTarget.pageErrorsyou implement ScrapeDriver/ScrapeTarget yourself
2PwaRenderModeyou import that type name
3PwaOfflineStrategyyou import that type name
4PrerenderReport.skippedyou read x build --target static --json, or the report in code

1. ScrapeTarget gains a required pageErrors: PageErrorRing

Only a third-party driver author pays this, and nothing in an ordinary app implements ScrapeTarget. If you build one — the shape packages/scraping/README.md's driver-author example builds — construct the ring and, if your transport can observe uncaught page exceptions, push to it.

+ import { createRing, type PageErrorRing } from '@ultimat3/scraping';

  const target: ScrapeTarget = {
    // …
+   pageErrors: createRing(200),
  };

A driver that cannot observe them builds the ring and never pushes — which is exactly what the offline targets do. That is the whole migration.

Required rather than optional, deliberately. An optional ring lets a driver stay silent about errors it can see, which is the gap this closed: nothing in @ultimat3/scraping subscribed to pageerror at all, so an island that threw was invisible. A throw calls no console method, so console() answered [] and a page whose script had died read as clean.

New on ScrapePage, and additive — no edit needed to consume them: pageErrors() and pageErrorsDropped(). The dropped count makes the list a floor, not a total.

2 and 3. PwaRenderMode and PwaOfflineStrategy are deleted from @ultimat3/pwa

Two type-only renames. No member changed — only the name the type is declared under.

WasIsMembers, unchanged
PwaRenderModeRenderMode'static' | 'isr' | 'ssr' | 'stream'
PwaOfflineStrategyOfflineStrategy'precache' | 'runtime' | 'network-only'
- import type { PwaRenderMode, PwaOfflineStrategy } from '@ultimat3/pwa';
+ import type { RenderMode, OfflineStrategy } from '@ultimat3/pwa';

@ultimat3/pwa re-exports both under the canonical name, so the import path does not have to move — @ultimat3/core is where they are declared and is equally correct.

Why the alias existed and why it could not stay. Tier 4 may not import tier 4, so @ultimat3/pwa wrote its own copy of a set @ultimat3/render already had. That copy is what kept spa mapped to cache-first after spa was deleted in 6.0.0 — the one strategy that gives an app/ route a shared cache entry, i.e. one signed-in member's HTML served to the next. The vocabulary is now declared once at tier 0, and bun run scripts/render-modes.ts --json refuses a second declaration anywhere in packages/*/src.

4. PrerenderReport.skipped carries the reason, not just the path

readonly string[] → readonly SkippedRoute[], where a SkippedRoute is { route, surface, render, reason, why }. PrerenderedPage also gains route, the declared path a concrete URL came from.

- for (const path of report.skipped) console.log(`skipped ${path}`);
+ for (const skipped of report.skipped) console.log(`skipped ${skipped.route}: ${skipped.why}`);

x build --target static --json now returns emitted and skipped, and the human path prints the same rows.

Why it changed. .x/static/ held a partial site and said nothing about the difference: app/ routes exist only through the server, so a tool pointed at the directory filed "the island did not mount" against a route that was never emitted. A list of paths cannot distinguish "not emitted because it needs a server" from "not emitted because it is broken", and those are opposite facts.

Edit this page on GitHubThis page as markdown