temporal_rs
v0.1.9
Published
Native Node.js and WASM bindings for [temporal_rs](https://github.com/boa-dev/temporal) — the Rust implementation of the [TC39 Temporal proposal](https://tc39.es/proposal-temporal/).
Readme
temporal_rs
Native Node.js and WASM bindings for temporal_rs — the Rust implementation of the TC39 Temporal proposal.
Install
npm install temporal_rsPrebuilt native binaries are available for:
| Platform | Architecture | |-----------------|-------------| | macOS | arm64, x64 | | Linux (glibc) | x64, arm64 | | Linux (musl) | x64 | | Windows (MSVC) | x64, arm64 |
Quick Start
import { Temporal } from 'temporal_rs'
// Current time
const now = Temporal.Now.instant()
const today = Temporal.Now.plainDateISO()
const zonedNow = Temporal.Now.zonedDateTimeISO()
// Dates
const date = new Temporal.PlainDate(2024, 3, 15)
const parsed = Temporal.PlainDate.from('2024-03-15')
console.log(date.dayOfWeek) // 5 (Friday)
console.log(date.inLeapYear) // true
// Times
const time = new Temporal.PlainTime(13, 30, 45)
const morning = Temporal.PlainTime.from('09:00:00')
// Date + Time
const dt = new Temporal.PlainDateTime(2024, 12, 31, 23, 59)
// Zoned (timezone-aware)
const zdt = Temporal.ZonedDateTime.from('2024-03-15T12:00:00-04:00[America/New_York]')
console.log(zdt.offset) // '-04:00'
console.log(zdt.hoursInDay) // 24
// Instants (exact time)
const inst = Temporal.Instant.fromEpochMilliseconds(Date.now())
// Durations
const dur = Temporal.Duration.from('P1Y2M3DT4H5M6S')
console.log(dur.years) // 1
console.log(dur.months) // 2Arithmetic
const { PlainDate, Duration } = Temporal
const date = PlainDate.from('2024-01-31')
const oneMonth = Duration.from('P1M')
const next = date.add(oneMonth)
console.log(next.toString()) // '2024-02-29' (constrained to valid date)
const diff = PlainDate.from('2024-01-01').until(PlainDate.from('2024-12-31'))
console.log(diff.days) // 365
// Property bag arguments
const result = date.add({ months: 1, days: 5 })Timezone-aware Operations
const zdt = Temporal.ZonedDateTime.from('2024-03-09T12:00:00-05:00[America/New_York]')
// Adding 1 day across DST spring-forward
const nextDay = zdt.add({ days: 1 })
console.log(nextDay.hour) // 12 (wall time preserved)
// Convert between timezones
const utc = zdt.withTimeZone('UTC')
console.log(utc.epochMilliseconds === zdt.epochMilliseconds) // true (same instant)
// Start of day
const sod = zdt.startOfDay()
console.log(sod.hour) // 0
// Timezone transitions
const transition = zdt.getTimeZoneTransition('next')Calendars
16 calendar systems are supported:
// ISO 8601 (default), Gregorian, Japanese, Buddhist, Chinese, Coptic,
// Dangi, Ethiopian, Ethiopic (Amete Alem), Hebrew, Indian,
// Islamic (civil, tabular, Umm al-Qura), Persian, ROC
const date = Temporal.PlainDate.from({
year: 5784, monthCode: 'M01', day: 1, calendar: 'hebrew'
})
console.log(date.calendarId) // 'hebrew'
console.log(date.era) // 'am'
console.log(date.eraYear) // 5784Rounding and Comparison
// Round time
const time = new Temporal.PlainTime(13, 45, 30)
const rounded = time.round({ smallestUnit: 'hour', roundingMode: 'halfExpand' })
console.log(rounded.hour) // 14
console.log(rounded.minute) // 0
// Compare dates
const d1 = new Temporal.PlainDate(2024, 1, 1)
const d2 = new Temporal.PlainDate(2024, 12, 31)
console.log(Temporal.PlainDate.compare(d1, d2)) // -1
// Compute difference with options
const diff = d1.until(d2, { largestUnit: 'month' })
console.log(diff.months) // 11Type Conversions
// PlainDateTime -> PlainDate + PlainTime
const dt = Temporal.PlainDateTime.from('2024-06-15T10:30:00')
const date = dt.toPlainDate()
const time = dt.toPlainTime()
// ZonedDateTime -> Instant, PlainDate, PlainTime, PlainDateTime
const zdt = Temporal.ZonedDateTime.from('2024-06-15T10:30:00+02:00[Europe/Berlin]')
const instant = zdt.toInstant()
const plainDate = zdt.toPlainDate()
const plainTime = zdt.toPlainTime()
const plainDateTime = zdt.toPlainDateTime()
// PlainDate -> PlainDateTime (with optional time)
const dateOnly = new Temporal.PlainDate(2024, 6, 15)
const withTime = dateOnly.toPlainDateTime(new Temporal.PlainTime(10, 30))Now Functions
const instant = Temporal.Now.instant() // Current Instant
const tz = Temporal.Now.timeZoneId() // System timezone ID
const date = Temporal.Now.plainDateISO() // Current date (local tz)
const time = Temporal.Now.plainTimeISO() // Current time (local tz)
const dateTime = Temporal.Now.plainDateTimeISO() // Current date+time (local tz)
const zoned = Temporal.Now.zonedDateTimeISO() // Current ZonedDateTime (local tz)
// With explicit timezone
const utcDate = Temporal.Now.plainDateISO('UTC')API Reference
Types
| Class | Description |
|-------|-------------|
| Temporal.PlainDate | Calendar date (no time, no timezone) |
| Temporal.PlainTime | Wall-clock time (no date, no timezone) |
| Temporal.PlainDateTime | Calendar date + wall-clock time (no timezone) |
| Temporal.ZonedDateTime | Date + time with timezone (DST-aware) |
| Temporal.Instant | Exact point in time (epoch nanoseconds) |
| Temporal.Duration | ISO 8601 duration with arithmetic |
| Temporal.PlainYearMonth | Calendar year + month |
| Temporal.PlainMonthDay | Calendar month + day |
| Temporal.Now | Current time access |
Imports
// ESM — spec-conforming Temporal namespace (recommended)
import { Temporal } from 'temporal_rs'
// ESM — individual named exports
import { PlainDate, Duration, Instant } from 'temporal_rs'
// CJS — spec-conforming Temporal namespace
const { Temporal } = require('temporal_rs')
// ESM — raw NAPI bindings (no spec conformance layer)
import { PlainDate } from 'temporal_rs/native'
// CJS — raw NAPI bindings
const { PlainDate } = require('temporal_rs/native')
// WASM — for browser/edge runtimes (ESM only)
import { PlainDate } from 'temporal_rs/wasm'WASM (Browser)
A WASM build is also available for browser use:
npm run build:wasmThis produces a wasm-pkg/ directory with ES module + TypeScript definitions usable via bundlers (webpack, vite, etc).
WASM Limitations
The WASM bindings have some differences from the native Node.js (NAPI) bindings due to wasm-bindgen constraints:
- Epoch nanosecond precision:
InstantandZonedDateTimeconstructors acceptf64(JSnumber) for epoch nanoseconds, which loses precision beyond ±2^53 (~104 days from Unix epoch in nanoseconds). For full nanosecond precision, useInstant.from()orZonedDateTime.from()with ISO string arguments. The NAPI bindings useBigIntand have no precision loss. epochNanosecondsgetter: Returnsnumber(f64) in WASM vsbigintin NAPI. UseepochMillisecondsfor a safe numeric value, ortoString()for full nanosecond fidelity.toString()display options:ZonedDateTime.toString()andInstant.toString()in WASM do not accept display/rounding options (calendarName, timeZoneName, fractionalSecondDigits, etc.). The NAPI bindings support these options forPlainDateTime.toString()but not yet forZonedDateTime/Instant.
The pre-built wasm-pkg/ is included in the npm package for convenience. If you only use the Node.js API (import from 'temporal_rs'), the WASM artifacts are unused.
Development
# Install dependencies
npm install
# Build everything (NAPI + TypeScript conformance layer)
npm run build
# Build individual targets
npm run build:napi # native addon only
npm run build:ts # TypeScript conformance layer only
npm run build:wasm # WASM target
# Run unit tests
npm test
# Lint and format
npm run lint # ESLint (strictTypeChecked)
npm run lint:fix # auto-fix
npm run format # Prettier
npm run format:check # check only
# Run TC39 Test262 Temporal compliance tests (requires test262 submodule)
git submodule update --init
npm run test262
# Run Test262 with filter
npm run test262 -- PlainDate
# Run Test262 verbose (show each test result)
npm run test262:verbose
# Run Test262 and write failure list
npm run test262 -- --write-failures
# Bump version across all packages and lockfiles
./scripts/bump-version.sh patch # 0.1.1 → 0.1.2
./scripts/bump-version.sh minor # 0.1.1 → 0.2.0
./scripts/bump-version.sh major # 0.1.1 → 1.0.0
./scripts/bump-version.sh 2.0.0 # explicit versionPrerequisites
Test262 Compliance
This package includes a runner for the TC39 Test262 Temporal test suite (6,661 tests). The runner executes tests in Node.js vm contexts with the spec conformance layer injected as globalThis.Temporal.
Current results: 6,659 / 6,661 pass (99.97%) — 0 skipped.
The 2 remaining failures are caused by inconsistencies between Node.js's ICU4C and temporal_rs's ICU4X implementations, not by the bindings themselves:
| Test | Root cause |
|------|-----------|
| format/temporal-objects-resolved-time-zone | Node.js ICU4C format() uses U+0020 before AM/PM but formatToParts() uses U+202F (narrow no-break space). The test compares them. Present in Node.js v22–v25 (ICU 77–78). |
| formatToParts/compare-to-temporal-lunisolar | ICU4C and ICU4X disagree on Chinese calendar 2030/M01 start date by 1 day. The new moon falls 7 minutes past midnight Beijing time — ICU4X (temporal_rs) correctly places it on Feb 3, ICU4C on Feb 2. Both are algorithmic ICU bugs that cannot be fixed via ICU data updates. |
How It Works
This package wraps temporal_rs (the Rust implementation used by Boa, Kiesel, and V8) via two binding layers:
- NAPI-RS for native Node.js addons with hand-crafted TypeScript definitions (aligned with
esnext.temporal.d.ts) - wasm-bindgen for browser-compatible WASM builds
A modular TypeScript spec conformance layer (lib/) bridges the gap between the NAPI binding surface and the TC39 Temporal specification. It is compiled via tsup to both ESM (.mjs) and CJS (.js) with sourcemaps; hand-crafted type declarations (.d.ts, .d.mts) aligned with TypeScript's esnext.temporal.d.ts provide full type coverage. The conformance layer provides:
Temporalnamespace with all types andTemporal.Now- Property bag arguments for
from(),with(),add(),subtract() calendarId/timeZoneIdstring getters- Calendar year-to-ISO conversion for 16 calendar systems
- Era/eraYear resolution with calendar-specific epoch offsets
- Proper
TypeError/RangeErrorerror types Symbol.toStringTag,Symbol.hasInstance,valueOf()per specIntl.DateTimeFormatandIntl.DurationFormatintegration
Timezone data is embedded in the binary via the zoneinfo64 provider, so no external timezone database is needed.
License
MIT
