moment-temporal
v0.0.5
Published
Drop-in replacement for moment + moment-timezone reimplemented on the Temporal API. Full API and test-suite compatibility, zero bundled timezone data.
Maintainers
Readme
moment-temporal
A drop-in replacement for moment + moment-timezone, reimplemented on the
Temporal API. Full API and
test-suite compatibility with [email protected] and [email protected] —
with zero bundled timezone data: timezone math comes from the host's IANA
database through Temporal and Intl.
// package.json — drop-in via npm alias
{
"dependencies": {
"moment-timezone": "npm:moment-temporal"
}
}const moment = require('moment-timezone'); // now moment-temporal
moment.tz('2024-07-04 12:00', 'America/New_York').format(); // "2024-07-04T12:00:00-04:00"It also replaces plain moment (the default export is a full moment):
"moment": "npm:moment-temporal".
Why
| consumer bundle (tsdown, minified ESM) | raw | gzip |
| ---------------------------------------------------------- | ------: | ------: |
| moment + moment-timezone (with data) | 781 KB | 58.7 KB |
| moment-temporal | 73 KB | 24.5 KB |
| @js-temporal/polyfill, if your targets need one (opt-in) | +163 KB | +47 KB |
| each additional locale (e.g. de) | +1.6 KB | +0.7 KB |
- No timezone data in your bundle. Offsets, transitions and DST rules come
from the runtime's own IANA database (the same one
Intl.DateTimeFormatuses), viaTemporal.ZonedDateTime. - Real ESM with tree-shakeable, opt-in locales (
import 'moment-temporal/locale/de'), while the CJS entry keeps moment's classic lazymoment.locale('de')auto-loading in Node. - All date/calendar math runs on Temporal — legacy
Dateis kept only as an inert epoch-milliseconds container (_d) for internals compatibility.
Temporal runtime requirement
The library needs globalThis.Temporal:
Available natively: Node ≥ 25 (and recent Deno / Firefox; other engines are shipping it). Nothing to do, nothing extra to ship.
Everywhere else: bring your own polyfill and assign it before loading the library — this package has no polyfill dependency and no opinion about which one you use. For example, with
@js-temporal/polyfill(or the lightertemporal-polyfill):// setup-temporal.js import { Temporal } from '@js-temporal/polyfill'; globalThis.Temporal ??= Temporal;// your entry module import './setup-temporal.js'; import moment from 'moment-timezone'; // aliased to moment-temporal
Without globalThis.Temporal the library throws a descriptive error on first
use. The full bundle-size win arrives with native Temporal (already in
Firefox and Node 25, in progress in Chrome/Safari) — there you ship no
polyfill at all.
Compatibility
The ported upstream test suites are the compatibility contract, running verbatim against this implementation:
| suite | result | | ------------------------------------------- | ------ | | moment core (52 files, ~17k assertions) | 626/626 tests pass | | moment locales (139 files, 138 locales) | 3262/3262 tests pass | | moment-timezone (core + 340 zones + countries) | 112,193 assertions pass | | Temporal-fallback vs real tzdata (2025b) | 239,986 offset checks, 0 mismatches |
Run them with npm test.
moment.version reports 2.30.1 and moment.tz.version reports 0.6.3,
so version sniffing by dependents keeps working.
How zone resolution works
tz.add()/tz.load()packed data, when you load any, always wins — the full moment-timezone data pipeline (unpack,pack,link,filterYears,moment-timezone-utils, …) is implemented and tested.- Otherwise the zone name is resolved case-insensitively against the host's
IANA database and served by a Temporal-backed
Zoneobject with the same interface (utcOffset,abbr,parse,untils,offsets,abbrs,population), including moment-timezone's exactmoveInvalidForward/moveAmbiguousForwardDST disambiguation semantics.
Does it still ship IANA data?
The tz data files are included in the npm package on disk (data/), purely
for compatibility — no entry point ever imports them, so they are never
in your bundle and never parsed at runtime. All timezone math works without
them because the runtime's own IANA database (the one Intl uses, kept
current by OS/browser updates) supplies offsets and transitions through
Temporal.
"Loaded data" means you explicitly opted in, exactly like upstream's data-loading API:
moment.tz.load(require('moment-timezone/data/packed/latest.json'));Doing so restores byte-for-byte upstream behavior where the divergences below matter (name/country enumeration, tzdata abbreviation strings) — at upstream's bundle cost.
Documented divergences (without loaded data)
moment.tz.names()works out of the box: the name/link registries are seeded at import from a small generated table (src/tz/seed-data.js, regenerated byscripts/gen-seed-data.mjs— names only, no offset timelines), while all zone math still comes from the host database.- Country data stays load-only:
moment.tz.countries()returns[]andzonesForCountry()/zone.countries()needtz.load()— zone→country mappings aren't exposed by any web API. - Zone abbreviations (
z/zztokens) are CLDR-derived, using a locale matched to each zone's region (EST,NZDT,AEST,BST,IST,CET, … all match tzdata). Zones for which CLDR defines no alphabetic short name anywhere render in tzdata's numeric convention instead (+04,+0530). Offsets and instants are always correct; load data if you need tzdata's exact abbreviation strings for every zone. zone.untils/offsets/abbrsarrays of fallback zones are materialized lazily over 1800–2040 (parse()/utcOffset()remain correct outside that horizon).- Timezone data reflects the host's tzdb version, which may differ from the latest IANA release (usually newer than what an app was shipping!).
Why locale files instead of Intl?
Intl can render localized month and weekday names, and
Intl.RelativeTimeFormat covers some relative time — but moment's locale
behavior is not CLDR, and a drop-in replacement has to reproduce moment's
exact strings (the 3262 ported locale tests pin them). Locale files carry
what Intl cannot supply:
- moment's community-authored phrasing —
"vor ein paar Sekunden", whereIntlwould say"vor 3 Sekunden"; - calendar phrases (
"Last Monday at 8:30 PM") and per-localeL/LL/LLLlong-format patterns; - ordinal suffix strings (
"31st","1er"—Intl.PluralRulesclassifies ordinals but provides no suffixes); - parsing tables: turning
"3 de enero"or Arabic-digit input back into a date, with moment's exact strict/lenient rules,preparse/postformatdigit maps, meridiem tables, locale week rules (dow/doy), and eras.
They're opt-in and tree-shaken: you pay ~0.7 KB gzipped per locale you
actually import; en is built in.
Architectural notes
src/lib/— the moment engine. The parsing/formatting/locale layer (token tables, regexes, locale configs) follows moment's MIT-licensed implementation closely — it is the API contract the tests encode. Every Date-based time computation (field get/set, wall-clock construction,add/startOf/diffcalendar math, week calendars, offsets) is reimplemented insrc/lib/temporal.js+ the modules that consume it.src/tz/— moment-timezone reimplemented as ESM: the packed-data engine, plus the Temporal-backed fallback zones (temporal-zone.js).src/locale/— moment's 138 locale definitions, vendored as data (MIT, headers preserved).src/test/,test/tz/— upstream test suites, vendored verbatim, running onnode:test(QUnit shim) and a nodeunit-compatible runner.
Development
npm run vendor # re-vendor tests/locales/data from upstream checkouts
npm run build # tsdown (rolldown): root CJS bundles, CJS locales, browser builds, ESM dist
npm test # build + all four suites + tzdata validationLicense
MIT. This project reimplements the APIs of, and vendors locale data and test
suites from, moment and
moment-timezone — both MIT,
(c) JS Foundation and other contributors. See THIRD-PARTY-NOTICES.md.
