Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 62 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,68 @@ Semver applies from 1.0.0. A breaking change to a documented API needs a major

## [Unreleased]

Nothing yet.
### Changed

- **BREAKING —** `isValidTimeZone`, `assertTimeZone` and `canonicalTimeZone` refuse every single-label
timezone name except `UTC`. A zone is `Area/Location`; `Intl` accepting a string is not evidence it is
one, and at ICU 78 the two stopped agreeing — `CET`, `EST`, `GMT` and `MST` resolve there where ICU 75
threw, so a Bun upgrade alone reopened the "no date without an explicit IANA zone" rule, silently and in
the direction that fails dangerous. The judgement is now structural. 43 names change answer (#251):

| Refused | Replace with |
|---|---|
| `Japan` `GB` `GB-Eire` `Eire` `W-SU` `PRC` `ROK` `ROC` `Singapore` `Israel` `Iran` `Egypt` `Cuba` `Jamaica` `Poland` `Portugal` `Turkey` `Hongkong` `Iceland` `Libya` `Navajo` `Kwajalein` `NZ` `NZ-CHAT` | the `Area/Location` spelling: `Asia/Tokyo`, `Europe/London`, `Europe/London`, `Europe/Dublin`, `Europe/Moscow`, `Asia/Shanghai`, `Asia/Seoul`, `Asia/Taipei`, `Asia/Singapore`, `Asia/Jerusalem`, `Asia/Tehran`, `Africa/Cairo`, `America/Havana`, `America/Jamaica`, `Europe/Warsaw`, `Europe/Lisbon`, `Europe/Istanbul`, `Asia/Hong_Kong`, `Atlantic/Reykjavik`, `Africa/Tripoli`, `America/Denver`, `Pacific/Kwajalein`, `Pacific/Auckland`, `Pacific/Chatham` |
| `UCT` `Universal` `Zulu` | `UTC` |
| `GMT` `GMT0` `GMT+0` `GMT-0` `Greenwich` | `Etc/GMT`, which keeps the `GMT` label; `UTC` renders `UTC` |
| `CET` `EET` `MET` `WET` `EST` `MST` `HST` `EST5EDT` `CST6CDT` `MST7MDT` `PST8PDT` | no mechanical replacement, and that is the defect: an abbreviation names no jurisdiction and carries no DST rule. Name the city — `Europe/Paris` for `CET`, `America/New_York` for `EST5EDT`, `America/Phoenix` for `MST` |

The first three rows are textual: each replacement renders the identical wall clock and offset, verified
against seven probe instants including both 2026 DST transitions. `Etc/GMT+2` is unaffected — only a
LEADING sign is a bare offset. No denylist: a list of refused abbreviations grows with every tzdata
release, and there is no structural rule that keeps `CET` out and lets `Japan` in.

- **BREAKING —** the image pipeline is `Bun.Image`, and its terminals are async. `transformImageBytes()`
and `blurDataUrl()` return a `Promise`; add `await`. Every consumer inside the framework
(`storage.transformImage`, `seo`'s `builtinImageDriver`, `pwa`'s `BuiltinImagePipeline`) was already
`async` and its signature is unchanged.

The hand-rolled JPEG and PNG codecs and the Lanczos resampler are deleted — **1,679 lines** — and
replaced by `Bun.Image`'s statically-linked libjpeg-turbo / libspng / libwebp with SIMD resize
kernels. Still zero dependencies: no `sharp`, no native build step (#252).

- **WebP encodes now.** `ENCODABLE_FORMATS` is `png | jpeg | webp`; `DECODABLE_FORMATS` is
`png | jpeg | webp | gif`. `@ultimat3/storage`'s default variant format and its default `.webp`
key extension finally agree — a `srcset` entry can be served, not only named.
- **AVIF and HEIC are refused on EVERY platform**, deliberately. `Bun.Image.backend` is pinned to
`'bun'` on every call, so the static codecs and Highway geometry produce identical bytes on a
laptop and on a node — which `variantKey`'s content-addressed cache requires. A platform-dependent
variant is a cache that never hits. Producing AVIF means a CDN or a custom `ImageTransformDriver`.
- **BREAKING — `blurDataUrl(bytes, width)` lost its `width`.** It is `Bun.Image.placeholder()`: a
deterministic ThumbHash `data:image/png;base64,…`, at most 32px on its long edge.
`BLUR_PLACEHOLDER_WIDTH` and `@ultimat3/storage`'s re-export of it are deleted — there is no width
to name. ThumbHash quantises the aspect ratio; `responsiveImage()` paints the LQIP as
`background-size: cover` inside a box sized from the real dimensions, so nothing shifts.
- **BREAKING — with no `format`, the SOURCE format is kept** (PNG if the source is one the pipeline
cannot write). The old rule guessed from the pixels and turned an opaque PNG into a JPEG. A
heuristic that *can* flatten a logo eventually does.
- **BREAKING — deleted exports:** `decodeJpeg`, `encodeJpeg`, `decodePng`, `encodePng`,
`resizeRaster`, `defaultFormatFor`, `DEFAULT_JPEG_QUALITY` (now `DEFAULT_IMAGE_QUALITY`),
`BLUR_PLACEHOLDER_WIDTH`. `encodeImage` / `decodeImage` survive as the raw-pixel seam and are now
**8-bit RGBA PNG only** — `Bun.Image` has no compositor and no raw-pixel terminal, and the PWA
maskable safe zone needs both.
- EXIF `Orientation` is now applied to JPEG sources, which the old decoder ignored.
- `Bun.Image`'s `ERR_IMAGE_*` rejections map onto the existing `X_IMAGE_UNSUPPORTED` /
`X_IMAGE_TOO_LARGE` / `X_IMAGE_DECODE_FAILED`. No new code.

### Fixed

- island JSX compiles to real SolidJS reactivity. `Bun.build` was called with no `plugins`, so island
JSX became `React.createElement` against a `React` that is never imported and every island
containing JSX threw `ReferenceError` on first interaction, while the build reported success and
`x verify` stayed green. Islands are now compiled with `babel-preset-solid` (#243)
- a browser bundle can load `@ultimat3/core`: three module-scope `AsyncLocalStorage` constructions moved
onto one lazy seam, so `@ultimat3/ui` no longer throws `TypeError: undefined is not a constructor` at
module evaluation (#244)

## 5.0.1

Expand Down
16 changes: 11 additions & 5 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -166,11 +166,11 @@ Milestone detail: [`docs/idea/14-roadmap.md`](docs/idea/14-roadmap.md).
| Task | Command |
|---|---|
| install | `bun install` |
| **the gate** | `bun run verify` — `x verify` at the repo root, 17 steps: typecheck, lint, boundaries, filesize, package-shape, errors, unit, contract, live, job, e2e, eval, drift, contract-diff, budgets, manifest, roadmap. Green = shippable. |
| **the gate** | `bun run verify` — `x verify` at the repo root, 19 steps: typecheck, lint, boundaries, filesize, package-shape, errors, unit, contract, live, job, e2e, eval, drift, contract-diff, budgets, seo, i18n, manifest, roadmap. Green = shippable. The list is `VERIFY_STEP_NAMES` in `packages/cli/src/verify-step.ts`; `bun run scripts/gate-steps.ts` fails when a page states another number. |
| typecheck | `bun run typecheck` |
| lint | `bun run lint` · fix: `bun run lint:fix` |
| test (all) | `bun run test` — every framework suite, opt-in ones included. The reference app is gated separately: `cd examples/dummy && bun run ../../packages/cli/src/bin.ts verify` |
| **the app gate** | `bun run scripts/reference-app-gate.ts` — both tracked apps' own 17 steps (`examples/dummy`, `dummy/social-media-clone`), blocking on a ratchet: a step passing today must keep passing, a step pinned in that app's `expectedRed` (`scripts/lib/gated-apps.ts`) must still be failing, and a `typecheck` that goes green must join the root `tsconfig.json` references |
| **the app gate** | `bun run scripts/reference-app-gate.ts` — both tracked apps' own 19 steps (`examples/dummy`, `dummy/social-media-clone`), blocking on a ratchet: a step passing today must keep passing, a step pinned in that app's `expectedRed` (`scripts/lib/gated-apps.ts`) must still be failing, and a `typecheck` that goes green must join the root `tsconfig.json` references |
| shrink the ratchet | `bun run scripts/reference-app-gate.ts --unpin <app>:<step>[,<step>]` — the edit `X_REFERENCE_APP_PIN_STALE` names, performed |
| test (one file) | `bun test packages/core/src/errors.test.ts` |
| test (one name) | `bun test -t 'formats the fix line'` |
Expand Down Expand Up @@ -279,9 +279,15 @@ Everything in the framework is one of these. **If a feature doesn't fit one of t
`packages/i18n/catalogs/<locale>.json` (`CATALOG_ROOT`, what `x g route` / `x g resource` merge
keys into, and what both tracked apps have on disk); the **framework's own** catalog is
`packages/i18n/src/catalogs/en.json`, imported by `framework.ts`. `x i18n check` audits the first;
the `boundaries` step audits the second (`X_CATALOG_KEY_UNREACHABLE`), because pointing an app
check at this repo silently answers `ok` — `CATALOG_ROOT` does not exist here, so it loads zero
locales and passes.
the `boundaries` step audits the second (`X_CATALOG_KEY_UNREACHABLE`), because the framework repo
is not an app and the app check cannot read it.
**Pointing `x i18n check` at this repo does not answer `ok`** — this file said so until 2026-08-20
and `packages/cli/src/cmd-i18n.ts:227` calls `requireAppRoot('i18n', ctx.cwd)`, which refuses with
`X_NOT_IN_APP` before a catalog is loaded. The vacuous green was one step further in: an app WITH
an `app.config.ts` and no catalogs at all loaded zero locales and passed, and a catalog on disk
that no module registered passed with it. Both are now refused — `X_CATALOG_UNREGISTERED` — and
`i18n` is a step of the gate in its own right, so an app can no longer ship every user-facing
string as `⟦key⟧` under a green `x verify`.
- Docs style: lead with the rule, fragments over sentences, tables for any ≥3-row structure, no meta-framing, no trailing summary. Date load-bearing claims `As of 2026-07`.

## Where things live
Expand Down
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ that already works will be declined — that ambiguity is the tax agents pay.

```sh
bin/setup # bun version check, install, .env.development.local, git hooks
bin/check # the gate: typecheck, lint, boundaries, errors, tests, drift, budgets, manifest, roadmap
bin/check # the gate: 19 steps, one command, no --only and no --skip. Green = shippable
```

Requirements: **Bun >= 1.3**. Nothing else. No Node, no Docker, no database — `x dev` runs
Expand Down
5 changes: 5 additions & 0 deletions PUBLISHING.md
Original file line number Diff line number Diff line change
Expand Up @@ -289,6 +289,11 @@ whether or not anyone updates this table.

## Ongoing releases (automated)

0. **On a major only:** `wiki/Upgrading.md` already carries a `## <previous major>.x → <new major>`
section, written when the first breaking change landed — releasing it is deleting `unreleased`
from its heading and updating the summary table's counts. If the section does not exist, stop and
write it before tagging; the procedure is
[`docs/architecture/19-cutting-a-major.md`](docs/architecture/19-cutting-a-major.md).
1. `bun run scripts/release.ts --bump patch|minor|major` — bumps every package in lockstep and
appends the changelog entry.
2. Commit, tag `vX.Y.Z`, push.
Expand Down
Loading