Enable Temporal by default - #32978
Conversation
|
Warning Review limit reached
Next review available in: 3 seconds Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: ASSERTIVE Plan: Pro Run ID: 📒 Files selected for processing (4)
Comment |
There was a problem hiding this comment.
I didn't find any issues — the one-line flag flip and the test file both look correct — but enabling a new global by default is a user-visible product decision, so a human should sign off on the ship call.
Extended reasoning...
Overview
This PR flips JSC::Options::useTemporal() to true in the JSC defaults block of ZigGlobalObject.cpp (one line, identical in form to useExplicitResourceManagement and useImportDefer immediately above it) and adds test/js/web/temporal/temporal.test.ts with 10 tests covering the global's presence/attributes, the BUN_JSC_useTemporal=0 escape hatch, the nine namespaces, Temporal.Now, parsing/arithmetic/DST round-trips, Date.prototype.toTemporalInstant, Intl formatting, and structuredClone rejection.
Security risks
None. This exposes a standards-track date/time API that is already compiled into the binary and reachable today via BUN_JSC_useTemporal=1. No auth, crypto, filesystem, network, or permission surface is touched.
Level of scrutiny
The code is trivial and mechanically correct — the flag is set in the same block as the other JSC feature defaults, before the BUN_JSC_* env overrides are applied, so the escape hatch works as described. The new test file follows existing harness conventions (bunEnv, bunExe, await using for spawned procs). The bug-hunting system found nothing.
The decision, however, is a release-level call: shipping a new global to every Bun user by default. The PR description makes a strong case (100% test262 core pass rate, ASAN-clean, no startup cost, Chrome/Firefox/Node parity, documented non-ISO calendar gaps), but whether Bun is ready to commit to Temporal-on-by-default — and accept the listed known gaps as acceptable for GA — is a product judgment a maintainer should make explicitly rather than have auto-approved.
Other factors
No CODEOWNERS entry covers the changed paths. There are no prior human reviews or outstanding comments on the thread. The test's hard-coded DST and TZ-offset expectations (America/New_York, Asia/Tokyo) are stable historical transitions and should not flake.
|
Updated 8:09 PM PT - Jun 27th, 2026
❌ @robobun, your commit 6355a7b has some failures in 🧪 To try this PR locally: bunx bun-pr 32978That installs a local version of the PR into your bun-32978 --bun |
|
Updated 7:39 PM PT - Aug 4th, 2026
❌ @robobun, your commit f2e0cd5 has 1 failures in
🧪 To try this PR locally: bunx bun-pr 32978That installs a local version of the PR into your bun-32978 --bun |
There was a problem hiding this comment.
I didn't find any issues with the code itself — the flag flip, test, and LSan suppression all look correct — but enabling Temporal by default is a user-facing product decision (with documented non-ISO-calendar gaps) that a maintainer should explicitly sign off on.
Extended reasoning...
Overview
This PR flips JSC::Options::useTemporal() to true in the JSC defaults block of ZigGlobalObject.cpp, exposing the Temporal global and Date.prototype.toTemporalInstant to all users by default. It adds test/js/web/temporal/temporal.test.ts covering the global, the BUN_JSC_useTemporal=0 escape hatch, and core Temporal operations, plus an LSan suppression for TemporalCore::withTimeZone's process-lifetime TinyLRUCache of UCalendar handles.
Security risks
None. This toggles an existing upstream JSC feature flag; no new attack surface is introduced beyond what was already reachable via BUN_JSC_useTemporal=1. No auth, crypto, filesystem, or network code is touched.
Level of scrutiny
The mechanical change is a one-liner that follows the exact pattern of useExplicitResourceManagement and useImportDefer directly above it, and the test/suppression additions are straightforward. However, the effect is a user-visible runtime behavior change for every Bun process: a new global is installed, and the PR description itself documents known correctness gaps in non-ISO calendars (buddhist year, dayOfYear, chinese/dangi leap months, legacy TZ IDs). Whether those gaps are acceptable to ship — and whether the test262 data presented justifies flipping the default now — is a product/release judgment call, not a code-correctness question.
Other factors
The CI SIGABRT on x64-asan at 6355a7b was the LSan false positive that the follow-up commit ef49ec7b suppresses (with a well-reasoned comment explaining the bmalloc/LSan root-scanning interaction), so that should be resolved. The PR description is exceptionally thorough. Still, shipping a new standard-library global by default is the kind of decision that warrants an explicit maintainer ack rather than bot approval.
|
CI found a blocker, so I am converting this to a draft.
Temporal.PlainDate.from("2024-06-15").since("2023-01-01", { largestUnit: "month" })
// every other platform, and Node v26: "P17M14D"
// Windows: RangeError: Duration is outside the representable rangeThis is entirely inside JavaScriptCore, not Bun. The throw is the Enabling Temporal by default would ship a core API that throws The other annotation on build 66137, |
Sets JSC::Options::useTemporal() = true alongside the other JSC option defaults in ZigGlobalObject.cpp, exposing the Temporal global and Date.prototype.toTemporalInstant. BUN_JSC_useTemporal=0 still turns it back off because the BUN_JSC_* environment overrides run after the defaults block.
JSC::TemporalCore::withTimeZone (TimeZoneICUBridge.cpp) keeps up to 8 open UCalendars in a process-lifetime LazyNeverDestroyed TinyLRUCache, one per time zone ID; LRU eviction ucal_closes them. The TimeZoneCacheEntry that owns each UCalendar lives in bmalloc memory, which LeakSanitizer does not scan as a root region, so the libc-allocated UCalendar is reported as a direct leak even though it is reachable and bounded. The x64-asan lane hits this on any Temporal test that touches a named time zone.
ef49ec7 to
2f503b5
Compare
|
Un-drafting: the Windows blocker is fixed. The WebKit upgrade in #34373 ( Temporal.PlainDate.from("2024-06-15").since("2023-01-01", { largestUnit: "month" }).toString()
// => "P17M14D" on both, matching Linux and NodeA broader smoke (since/until, Duration.round/total/compare, Instant arithmetic, epoch nanoseconds, ZonedDateTime DST transitions) produces byte-identical output across Windows x64, Windows aarch64, and Linux. Also rebased onto main: #34373 had pinned |
The rebase picked up a bare leak:JSC::TemporalCore::withTimeZone entry that landed on main while this PR was drafted. The commented entry added here matches the same frames by substring, so keep the one with the explanation.
There was a problem hiding this comment.
No issues found — the duplicate LSAN suppression from the earlier pass is resolved (line 88 removed, single commented entry remains). The code change itself is a one-line flag flip with solid test coverage, but flipping a default-on global for all users is a product/API call, so leaving this for a maintainer to sign off.
Checked: BUN_JSC_* env overrides are applied after this defaults block so =0 still works; test/js/node/test/common/globals.js already handles Temporal conditionally; the new test's .since() case exercises the Int128 path that previously broke on Windows; leaksan.supp now has exactly one TemporalCore::withTimeZone entry.
Extended reasoning...
Overview
The PR flips JSC::Options::useTemporal() from false to true in the JSC options defaults block in ZigGlobalObject.cpp, exposing the Temporal global and Date.prototype.toTemporalInstant by default. It updates test/js/bun/jsc/temporal-global.test.ts to assert the new default and the BUN_JSC_useTemporal=0 opt-out, adds test/js/web/temporal/temporal.test.ts with in-process functional coverage (property attributes, all nine namespaces, Now, arithmetic/DST/round-trips, toTemporalInstant, Intl formatting, structuredClone rejection), and consolidates the TemporalCore::withTimeZone LSAN suppression into a single commented entry.
Since my previous inline comment, commit f2e0cd5 removed the pre-existing bare leak:JSC::TemporalCore::withTimeZone at line 88, so the file now has exactly one suppression for that frame with the explanatory comment — the nit is resolved.
Security risks
None identified. The change enables an already-compiled JSC feature via a runtime option; no new parsing of untrusted input is introduced on Bun's side, and Temporal's own ISO 8601 parser is upstream JavaScriptCore code that has been through test262 + ASAN per the PR description.
Level of scrutiny
The mechanical change is trivial and the tests follow harness conventions correctly (bunEnv spread, concurrent pipe drain, test.concurrent for independent subprocesses, exact-value assertions). However, this is a user-facing API surface change: it installs a new global on every Bun process and changes what typeof Temporal returns. Per the repo's guidance on API design, shipping decisions of this kind — even when the implementation is upstream and the case is well-argued — are product calls that a maintainer should ratify, particularly given the documented non-ISO calendar gaps. The PR description makes a thorough, evidence-backed case (test262 parity vs Node, ASAN clean, Windows Int128 fix verified on both arches, startup cost measured), which should make that sign-off quick.
Other factors
- The Windows
Int128Implblocker that drafted this PR is stated resolved by the WebKit upgrade in #34373; the new test includes the exact.since({ largestUnit: "month" })expression that previously threw, so CI on the Windows lanes will re-verify it. - Grepped for other tests asserting
typeof Temporal === "undefined"or Temporal absence — none beyond the file this PR updates;test/js/node/test/common/globals.jsalready guards withif (global.Temporal). - The
Intl.DateTimeFormattest hard-codes"6/15/2024"foren-US; this matches how other Intl tests in the repo pin locale output, so it is consistent with local convention. - Build #89046 for the current head is still in progress per the timeline; the human reviewer will want green Windows lanes before merging.
|
The red |
|
Build 89046 finished. Every lane is green except the one described above: the Notably, all three Windows lanes pass, including the From my side this is ready for sign-off: the diff is a one-line default flip plus tests and one documented LSan suppression, with the supporting data in the PR description. |
### What does this PR do? Enables JavaScriptCore's `Temporal` implementation by default by setting `JSC::Options::useTemporal() = true` alongside the other JSC option defaults in `ZigGlobalObject.cpp`. This exposes the `Temporal` global and `Date.prototype.toTemporalInstant`. `BUN_JSC_useTemporal=0` still turns it back off, because the `BUN_JSC_*` environment overrides are applied after the defaults block. Fixes oven-sh#15853. ### Why now JSC's Temporal implementation has improved a lot since the 40% test262 pass rate reported on oven-sh#15853 in early 2025. Measured with `temporal-test262-runner` 0.4.0 (the same tool that produced that 40% figure) against test262 `de8e621c`, with Node v26.3.0 (which ships Temporal on by default) for comparison: | Suite | Tests | Bun | Node 26.3 | |---|---|---|---| | `built-ins/Temporal` (core spec) | 4603 | 100.0% | 98.7% | | `built-ins/Date/prototype/toTemporalInstant` | 8 | 100% | 100% | | `intl402/DateTimeFormat` + `DurationFormat` on Temporal objects | 259 | 99.2% | 91.9% | | `intl402/Temporal` (non-ISO calendars) | 2029 | 56.2% | 38.3% | Every one of the 136 tests where Bun is behind Node is in `intl402/Temporal`, which is non-ISO calendar behavior. Outside of that there is not a single test262 test where Bun is behind Node. I also byte-diffed the output of 20,500 deterministic Temporal operations (seeded identically in both engines): the iso8601, gregory, and roc calendars, 2000 DST-disambiguation cases, all offset-option handling, Instant rounding, and formatting options produced zero divergences. The full test262 run was repeated on a debug + ASAN build of this branch's parent: zero crashes, zero ASAN reports, zero assertion failures, and a pass/fail set identical to release across all 6634 tests. Chrome 144, Firefox, and Node 26 ship Temporal on by default. TypeScript 6.0 ships the type declarations and its `lib.esnext.d.ts` references `esnext.temporal`, so nothing is needed in `bun-types`. ### The Windows blocker is resolved This PR was converted to a draft when CI showed `Temporal.PlainDate.prototype.since` throwing `RangeError: Duration is outside the representable range` on trivial input, on Windows x64 and Windows aarch64 only (the two platforms where WTF's `Int128` is the emulated `Int128Impl` rather than native `__int128`). The WebKit upgrade in oven-sh#34373 (`2603e9eb41f0`) brought in upstream's Temporal duration-arithmetic and spec-alignment fixes, and the bug no longer reproduces. Verified on current main's canary (`1.4.0-canary.1+6b6fb1a33`) on both Windows 2019 x64 and Windows 11 aarch64 with `BUN_JSC_useTemporal=1`: the previously failing expression returns `P17M14D`, and a 10-expression smoke covering `since`/`until`, `Duration.round`/`total`/`compare`, `Instant` arithmetic, epoch nanoseconds, and ZonedDateTime DST transitions produces byte-identical output to Linux on both. ### Cost The `Temporal` namespace object is installed the same way `Intl` is (one line above it in `JSGlobalObject::init`): a single small object whose nine sub-constructors are `PropertyCallback` lookup-table entries created on first access, with their structures registered via `initLater`. Measured over 420 cold starts of `bun -e ''`: 1887 us/start with `useTemporal` off, 1875 us/start with it on (within measurement noise). RSS is 31.5 MB vs 31.4 MB. The implementation was already compiled into the binary behind the runtime flag, so there is no size change. ### Known gaps, all non-ISO calendars and all upstream JavaScriptCore - `dayOfYear` ignores the calendar and returns the ISO day of year. Every other calendar-dependent field getter has a non-ISO branch through `TemporalCore`; this one does not, and `TemporalZonedDateTimePrototype.cpp` already has a source comment acknowledging it. - The buddhist calendar's `year` returns the ISO year instead of BE, because `TemporalCore::calendarYear()` uses ICU's `UCAL_EXTENDED_YEAR`, which is the Gregorian year for the `GregorianCalendar`-derived calendars. The same function already special-cases `roc` for this exact quirk. - Chinese/dangi leap-month placement disagrees with the reference on 2-3% of dates, and the `ad`/`bc` era aliases are not accepted (`ce`/`bce` are). - Legacy IANA time zone IDs such as `CET` and `EST5EDT` are rejected. This is not Temporal-specific: `new Date()` and `Intl.DateTimeFormat().resolvedOptions().timeZone` already fall back to `UTC` for them today. The first two are being filed upstream on bugs.webkit.org with one-line reproductions. None of these are regressions this PR can introduce; they are pre-existing in the JSC implementation, which is only reachable behind `BUN_JSC_useTemporal=1` today. ### Nothing else needed to change - `test/js/node/test/common/globals.js` already handles the new global conditionally (`if (global.Temporal) intrinsics.add('Temporal')`). - `test/js/bun/jsc/temporal-global.test.ts` (added by oven-sh#34373 to pin the off-by-default state) is updated to assert the new default and the `BUN_JSC_useTemporal=0` opt-out; the subprocess flag tests live there, next to the other JSC option coverage, and the in-process functional coverage lives in `test/js/web/temporal/temporal.test.ts`. - `structuredClone` and `postMessage` correctly reject Temporal objects with a `DataCloneError`, matching Node. - Temporal string parsing uses its own strict ISO 8601 grammar in `ISO8601.cpp`. It never touches the date parser selected by `useV8DateParser`, which has exactly one call site (`DateCache::parseDate`), so there is no interaction with that override. ### LeakSanitizer The x64-asan lane flags JSC's Temporal time zone bridge on any test that touches a named time zone. `JSC::TemporalCore::withTimeZone` keeps a process-lifetime `LazyNeverDestroyed` LRU of at most 8 open `UCalendar`s (evicted entries are `ucal_close`d), and the `TimeZoneCacheEntry` that owns each one lives in bmalloc memory, which LeakSanitizer does not scan as a root region, so the libc-allocated `UCalendar` is misreported as a direct leak. This PR adds the one `test/leaksan.supp` entry for it with a comment. It is bounded and intentional, not a real leak. ### How did you verify your code works? `test/js/bun/jsc/temporal-global.test.ts` covers the global being present by default and the `BUN_JSC_useTemporal=0` escape hatch. `test/js/web/temporal/temporal.test.ts` covers the spec property attributes, all nine namespaces, `Temporal.Now`, parsing/arithmetic/DST-disambiguation round-trips, `Date.prototype.toTemporalInstant`, `Intl.DateTimeFormat` formatting of Temporal objects, and `structuredClone` rejection. ``` $ USE_SYSTEM_BUN=1 bun test test/js/bun/jsc/temporal-global.test.ts test/js/web/temporal/temporal.test.ts 1 pass 9 fail $ bun bd test test/js/bun/jsc/temporal-global.test.ts test/js/web/temporal/temporal.test.ts 10 pass 0 fail ``` `test/js/web/workers/structured-clone.test.ts` (231 tests) also passes with the flag on. <!-- robobun:evidence:begin --> --- **[review]** gate passed · iteration 4 · 4 files touched <details><summary>fails on main (without fix)</summary> ```console ASAN without fix: 9 FAILED $ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/mechgate.xml" test/js/bun/jsc/temporal-global.test.ts test/js/web/temporal/temporal.test.ts bun test v1.4.0 (f2e0cd5) test/js/bun/jsc/temporal-global.test.ts: 9 | cmd: [bunExe(), "-e", `process.stdout.write(typeof Temporal + " " + typeof Temporal.Now.instant)`], 10 | env: { ...bunEnv, BUN_JSC_useTemporal: undefined }, 11 | stderr: "pipe", 12 | }); 13 | const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); 14 | expect({ stdout, stderr, exitCode }).toEqual({ stdout: "object function", stderr: expect.any(String), exitCode: 0 }); ^ error: expect(received).toEqual(expected) { - "exitCode": 0, - "stderr": Any<String>, - "stdout": "object function", + "exitCode": 1, + "stderr": + "1 | process.stdout.write(typeof Temporal + " " + typeof Temporal.Now.instant) + ^ + ReferenceError: Temporal is not defined + at /workspace/bun/[eval]:1:53 + + Bun v1.4.0-debug+f2e0cd5 ... (truncated) release without fix: all passed bun test v1.4.0-canary.1 (2f503b5) test/js/bun/jsc/temporal-global.test.ts: (pass) Temporal is exposed by default [18.29ms] (pass) BUN_JSC_useTemporal=0 disables Temporal [17.95ms] test/js/web/temporal/temporal.test.ts: (pass) Temporal global > is installed with the spec property attributes [0.03ms] (pass) Temporal global > exposes all nine namespaces [0.05ms] (pass) Temporal core operations > Temporal.Now [5.26ms] (pass) Temporal core operations > parsing, arithmetic, and formatting round-trip [0.17ms] (pass) Temporal core operations > DST disambiguation [0.09ms] (pass) Temporal core operations > Date.prototype.toTemporalInstant [0.04ms] (pass) Temporal core operations > Intl.DateTimeFormat formats Temporal objects [2.06ms] (pass) Temporal core operations > structuredClone rejects Temporal objects [0.12ms] 10 pass 0 fail 27 expect() calls Ran 10 tests across 2 files. [175.00ms] __F:0:S:0 ``` </details> <details><summary>passes on PR (with fix)</summary> ```console ASAN with fix: all passed $ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/mechgate.xml" test/js/bun/jsc/temporal-global.test.ts test/js/web/temporal/temporal.test.ts bun test v1.4.0 (f2e0cd5) test/js/bun/jsc/temporal-global.test.ts: (pass) Temporal is exposed by default [1284.32ms] (pass) BUN_JSC_useTemporal=0 disables Temporal [1302.12ms] test/js/web/temporal/temporal.test.ts: (pass) Temporal global > is installed with the spec property attributes [2.15ms] (pass) Temporal global > exposes all nine namespaces [3.39ms] (pass) Temporal core operations > Temporal.Now [168.15ms] (pass) Temporal core operations > parsing, arithmetic, and formatting round-trip [9.26ms] (pass) Temporal core operations > DST disambiguation [6.56ms] (pass) Temporal core operations > Date.prototype.toTemporalInstant [2.74ms] (pass) Temporal core operations > Intl.DateTimeFormat formats Temporal objects [27.37ms] (pass) Temporal core operations > structuredClone rejects Temporal objects [7.59ms] 10 pass 0 fail 27 expect() calls Ran 10 tests across 2 files. [3.65s] __F:0:S:0 release with fix: all passed $ bun scripts/build.ts --profile=release [configured] bun-profile → bun (stripped) in 711ms (unchanged) ninja: Entering directory `/workspace/bun/build/release' [1/7] cxx obj/src/jsc/bindings/ZigGlobalObject.cpp.o [2/7] gen cpp.rs (cppbind) [2/7] cargo bun_bin → libbun_rust.a (--target x86_64-unknown-linux-gnu) nightly-2026-07-20-x86_64-unknown-linux-gnu unchanged - rustc 1.99.0-nightly (9f36de775 2026-07-19) �[1m�[92m Compiling�[0m bun_core v0.0.0 (/workspace/bun/src/bun_core) �[1m�[92m Compiling�[0m bun_errno v0.0.0 (/workspace/bun/src/errno) �[1m�[92m Compiling�[0m bun_ptr v0.0.0 (/workspace/bun/src/ptr) �[1m�[92m Compiling�[0m bun_boringssl_sys v0.0.0 (/workspace/bun/src/boringssl_sys) �[1m�[92m Compiling�[0m bun_safety v0.0.0 (/workspace/bun/src/safety) �[1m�[92m Compiling�[0m bun_zlib_sys v0.0.0 (/workspace/bun/src/zlib_sys) �[1m�[92m Compiling�[0m bun_cares_sys v0.0.0 (/workspace/bun/src/cares_sys) �[1m�[92m Compiling�[0m bun_zstd v0.0.0 (/workspace/bun/src/zstd) �[1m�[92m Compiling�[0m bun_picohttp v0.0.0 (/workspace/bun/src/picohttp) �[1m�[92m Compiling�[0m bun_brotli v0.0.0 (/workspace/bun/src/brotli) �[1m�[92m Compiling�[0m b ... (truncated) ``` </details> <details><summary>diff hotspot</summary> ``` src/jsc/bindings/ZigGlobalObject.cpp | 5 +- test/js/bun/jsc/temporal-global.test.ts | 20 ++++---- test/js/web/temporal/temporal.test.ts | 88 +++++++++++++++++++++++++++++++++ test/leaksan.supp | 9 +++- 4 files changed, 107 insertions(+), 15 deletions(-) ``` </details> **gate history** · 2 passed · 0 rejected · iteration 4 <details><summary>evidence per changed file</summary> ``` file reads edits tests src/jsc/bindings/ZigGlobalObject.cpp 2 1 8 test/js/bun/jsc/temporal-global.test.ts 0 0 0 test/js/web/temporal/temporal.test.ts 0 2 7 test/leaksan.supp 0 0 10 ``` </details> <!-- robobun:evidence:end -->
…oral Bun 1.4.0 (released today) ships the Temporal global by default (oven-sh/bun#32978). zoned-time.ts's Intl.DateTimeFormat-based offset diffing is exactly the kind of manual timezone arithmetic that produced #17 - replaced with Temporal.Instant/PlainDateTime/ZonedDateTime. Bumped typescript to 6.0.3 for its official esnext.temporal lib types (one /// <reference lib="esnext.temporal" /> line) instead of hand-rolling ambient declarations. That surfaced two TS 6.0 breaking changes: baseUrl is now a hard error by default (removed, was unused anyway), and nodenext module resolution needs "types": ["bun-types"] stated explicitly. Build's tsconfig.build.json also needs its own explicit rootDir now (TS5011). Same public API and test suite unchanged in zoned-time.ts; ui's packageManager pin bumped to match Bun. No Dockerfile/CI change needed since both already float to latest 1.x / no bun-version pin. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…l UTC (#19) * Fix calendar write timezone: offset-less times were misread as server-local UTC Closes #17. propose_create_calendar_event/propose_update_calendar_event and list_calendar_events parsed start/end via bare new Date(...), which interprets an offset-less ISO string in the server's local timezone (UTC) rather than the user's configured one. New parseZonedIso() (the inverse of the existing formatZonedIso) treats offset-less input as wall-clock time in the caller's timezone instead. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * Replace hand-rolled offset/DST math in zoned-time.ts with native Temporal Bun 1.4.0 (released today) ships the Temporal global by default (oven-sh/bun#32978). zoned-time.ts's Intl.DateTimeFormat-based offset diffing is exactly the kind of manual timezone arithmetic that produced #17 - replaced with Temporal.Instant/PlainDateTime/ZonedDateTime. Bumped typescript to 6.0.3 for its official esnext.temporal lib types (one /// <reference lib="esnext.temporal" /> line) instead of hand-rolling ambient declarations. That surfaced two TS 6.0 breaking changes: baseUrl is now a hard error by default (removed, was unused anyway), and nodenext module resolution needs "types": ["bun-types"] stated explicitly. Build's tsconfig.build.json also needs its own explicit rootDir now (TS5011). Same public API and test suite unchanged in zoned-time.ts; ui's packageManager pin bumped to match Bun. No Dockerfile/CI change needed since both already float to latest 1.x / no bun-version pin. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * Trim zoned-time.ts comments to WHY only, drop issue/devlog references Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * Validate timezone before parsing in parseZonedIso, not just inside CalendarService parseZonedIso is called from calendar.tools.ts before the service layer's own assertValidTimeZone check runs, so a malformed timezone was wrapped into a misleading "invalid date-time" error blaming the start/end string. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
What does this PR do?
Enables JavaScriptCore's
Temporalimplementation by default by settingJSC::Options::useTemporal() = truealongside the other JSC option defaults inZigGlobalObject.cpp. This exposes theTemporalglobal andDate.prototype.toTemporalInstant.BUN_JSC_useTemporal=0still turns it back off, because theBUN_JSC_*environment overrides are applied after the defaults block.Fixes #15853.
Why now
JSC's Temporal implementation has improved a lot since the 40% test262 pass rate reported on #15853 in early 2025. Measured with
temporal-test262-runner0.4.0 (the same tool that produced that 40% figure) against test262de8e621c, with Node v26.3.0 (which ships Temporal on by default) for comparison:built-ins/Temporal(core spec)built-ins/Date/prototype/toTemporalInstantintl402/DateTimeFormat+DurationFormaton Temporal objectsintl402/Temporal(non-ISO calendars)Every one of the 136 tests where Bun is behind Node is in
intl402/Temporal, which is non-ISO calendar behavior. Outside of that there is not a single test262 test where Bun is behind Node. I also byte-diffed the output of 20,500 deterministic Temporal operations (seeded identically in both engines): the iso8601, gregory, and roc calendars, 2000 DST-disambiguation cases, all offset-option handling, Instant rounding, and formatting options produced zero divergences.The full test262 run was repeated on a debug + ASAN build of this branch's parent: zero crashes, zero ASAN reports, zero assertion failures, and a pass/fail set identical to release across all 6634 tests.
Chrome 144, Firefox, and Node 26 ship Temporal on by default. TypeScript 6.0 ships the type declarations and its
lib.esnext.d.tsreferencesesnext.temporal, so nothing is needed inbun-types.The Windows blocker is resolved
This PR was converted to a draft when CI showed
Temporal.PlainDate.prototype.sincethrowingRangeError: Duration is outside the representable rangeon trivial input, on Windows x64 and Windows aarch64 only (the two platforms where WTF'sInt128is the emulatedInt128Implrather than native__int128).The WebKit upgrade in #34373 (
2603e9eb41f0) brought in upstream's Temporal duration-arithmetic and spec-alignment fixes, and the bug no longer reproduces. Verified on current main's canary (1.4.0-canary.1+6b6fb1a33) on both Windows 2019 x64 and Windows 11 aarch64 withBUN_JSC_useTemporal=1: the previously failing expression returnsP17M14D, and a 10-expression smoke coveringsince/until,Duration.round/total/compare,Instantarithmetic, epoch nanoseconds, and ZonedDateTime DST transitions produces byte-identical output to Linux on both.Cost
The
Temporalnamespace object is installed the same wayIntlis (one line above it inJSGlobalObject::init): a single small object whose nine sub-constructors arePropertyCallbacklookup-table entries created on first access, with their structures registered viainitLater. Measured over 420 cold starts ofbun -e '': 1887 us/start withuseTemporaloff, 1875 us/start with it on (within measurement noise). RSS is 31.5 MB vs 31.4 MB. The implementation was already compiled into the binary behind the runtime flag, so there is no size change.Known gaps, all non-ISO calendars and all upstream JavaScriptCore
dayOfYearignores the calendar and returns the ISO day of year. Every other calendar-dependent field getter has a non-ISO branch throughTemporalCore; this one does not, andTemporalZonedDateTimePrototype.cppalready has a source comment acknowledging it.yearreturns the ISO year instead of BE, becauseTemporalCore::calendarYear()uses ICU'sUCAL_EXTENDED_YEAR, which is the Gregorian year for theGregorianCalendar-derived calendars. The same function already special-casesrocfor this exact quirk.ad/bcera aliases are not accepted (ce/bceare).CETandEST5EDTare rejected. This is not Temporal-specific:new Date()andIntl.DateTimeFormat().resolvedOptions().timeZonealready fall back toUTCfor them today.The first two are being filed upstream on bugs.webkit.org with one-line reproductions. None of these are regressions this PR can introduce; they are pre-existing in the JSC implementation, which is only reachable behind
BUN_JSC_useTemporal=1today.Nothing else needed to change
test/js/node/test/common/globals.jsalready handles the new global conditionally (if (global.Temporal) intrinsics.add('Temporal')).test/js/bun/jsc/temporal-global.test.ts(added by Upgrade WebKit to 2603e9eb41f0 #34373 to pin the off-by-default state) is updated to assert the new default and theBUN_JSC_useTemporal=0opt-out; the subprocess flag tests live there, next to the other JSC option coverage, and the in-process functional coverage lives intest/js/web/temporal/temporal.test.ts.structuredCloneandpostMessagecorrectly reject Temporal objects with aDataCloneError, matching Node.ISO8601.cpp. It never touches the date parser selected byuseV8DateParser, which has exactly one call site (DateCache::parseDate), so there is no interaction with that override.LeakSanitizer
The x64-asan lane flags JSC's Temporal time zone bridge on any test that touches a named time zone.
JSC::TemporalCore::withTimeZonekeeps a process-lifetimeLazyNeverDestroyedLRU of at most 8 openUCalendars (evicted entries areucal_closed), and theTimeZoneCacheEntrythat owns each one lives in bmalloc memory, which LeakSanitizer does not scan as a root region, so the libc-allocatedUCalendaris misreported as a direct leak. This PR adds the onetest/leaksan.suppentry for it with a comment. It is bounded and intentional, not a real leak.How did you verify your code works?
test/js/bun/jsc/temporal-global.test.tscovers the global being present by default and theBUN_JSC_useTemporal=0escape hatch.test/js/web/temporal/temporal.test.tscovers the spec property attributes, all nine namespaces,Temporal.Now, parsing/arithmetic/DST-disambiguation round-trips,Date.prototype.toTemporalInstant,Intl.DateTimeFormatformatting of Temporal objects, andstructuredClonerejection.test/js/web/workers/structured-clone.test.ts(231 tests) also passes with the flag on.[review] gate passed · iteration 4 · 4 files touched
fails on main (without fix)
passes on PR (with fix)
diff hotspot
gate history · 2 passed · 0 rejected · iteration 4
evidence per changed file