feat!: parse dates to native Date instead of Moment - #275
Merged
Conversation
|
PR checklist
|
joris974
marked this pull request as draft
August 25, 2026 17:13
joris974
marked this pull request as ready for review
August 25, 2026 17:17
`date()` was the only consumer of moment-timezone in this repo, and it used none of the timezone functionality — just `moment(x)` and `isValid()`. That pulled the entire IANA timezone database into every consumer's bundle: 764KB minified, against 3KB for date-fns' `parseISO` + `isValid`. Parsing now returns a native `Date`, so the library's public API carries no date-library opinion and consumers can pick whatever they use downstream. date-fns is preferred over bare `new Date()` because `new Date()` is unsafe for validation — it silently rolls `'2016-02-30'` over to March 1, and resolves date-only strings to UTC rather than local midnight. `parseISO` matches the previous moment behavior on both. Only ISO-8601 is accepted now. Locale-dependent formats that moment took via its deprecated `Date` fallback (`'03/12/2016'`, `'March 12, 2016'`) are rejected, since `'03/12/2016'` is ambiguous between March 12 and December 3. BREAKING CHANGE: `date()` returns `ParserT<Date>` instead of `ParserT<Moment>`, and rejects non-ISO-8601 strings that were previously accepted. To keep Moment values, convert at the boundary with `mapStatic(date(), d => moment(d))`. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
joris974
force-pushed
the
drop-moment-timezone
branch
from
August 25, 2026 17:18
0976ee5 to
fe61bda
Compare
cdmren
approved these changes
Aug 25, 2026
This was referenced Aug 25, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
date()was the only thing in this repo importingmoment-timezone, and it used none of the timezone functionality — justmoment(x)followed byisValid(). That pulled the whole IANA timezone database into every consumer's bundle to do a string parse.moment-timezonedate-fns(parseISO+isValid)date()now returns a nativeDate. The library's public API carries no date-library opinion, so consumers pick whatever they use downstream.momentis now entirely absent frompnpm-lock.yaml— it was the only path to it.Upstream status
Moment is also a dead end for us going forward. Its own README says so:
Moment core's last release was 2.30.1, in December 2023 — over two and a half years ago.
moment-timezonestill publishes (0.6.3 landed 2026-07-19), but those releases are IANA timezone database refreshes rather than library work, which is exactly what maintenance mode looks like. The upstream project also recommends migrating to a modern alternative,date-fnsamong them.So the 764KB we were shipping bought us a dependency that is explicitly not being developed.
Why date-fns rather than bare
new Date()new Date()is unsafe for a validation library. Measured against the current behavior:moment(before)new Date()parseISO(after)'2016-03-12 13:00:00''2016-03-12'(date-only)'2016-02-30''2015-02-29'parseISOmatches the previous moment behavior on every case that matters.date-fnsis also already used in megarepo, so this doesn't introduce a new library to the stack.Temporalwould be the longer-term answer, but it's unavailable on Node 22 (typeof Temporal === 'undefined') and would need a polyfill.Release
This is a major version bump:
3.0.1->4.0.0.The commit is
feat!:with aBREAKING CHANGE:footer, so semantic-release cuts the major automatically on merge. Note thatpackage.jsonin the repo reads"version": "2.0.0"— that value is stale and unmanaged, because@semantic-release/npmsets the version at publish time and does not commit it back. The publishedlatestis3.0.1(tagv3.0.1), so don't read the 2.0.0 in the diff as the current version, and there's no manual version edit in this PR.Worth knowing for anyone tracking the
v3references below: v3.0.0 was the semantic-release adoption, not an API change.date()still returnsParserT<Moment>in the published v3.0.1, so the console comment predictingnativeDate()"in v3" was overtaken by events — that change is landing here, in v4.Breaking changes
date()returnsParserT<Date>, notParserT<Moment>.'03/12/2016'and'March 12, 2016'previously worked via moment's deprecatedDatefallback (it logged a deprecation warning) and are now rejected.'03/12/2016'is genuinely ambiguous between March 12 and December 3.date()returned aMomentthrough v3. To keepMomentvalues, convert at the boundary:Downstream impact
megarepo is the only consumer.
frontend/platform/consolealready has a Moment→Date adapter used across ~15 model files, carrying this note:That adapter collapses to
date().frontend/educator(PremiumCutoff = Moment) andfrontend/student's test mock need the inverse wrapper above; megarepo has 409 files importingmoment-timezone, so it stays moment-based regardless. A companion megarepo PR is required before this is consumed — not included here.Notes for the reviewer
parseISOaccepts the ISO century form, so'12'parses as year 1200 (moment gave 2001-12-01). Both are nonsense; I keptparseISO's standard-conformant behavior rather than adding custom length rules. Flagging in case you'd rather guard it.for...ofrather than lodashforEachto avoid conflicting with refactor: remove lodash dependency #273, which removes lodash from this file.TZ=UTC,Asia/Tokyo,America/Los_Angeles,Pacific/Kiritimati— the date assertions are timezone-independent.Test plan
pnpm test— 82 tests pass (76 before, +6 pinning the new contract)pnpm build— typechecks clean;dist/index.d.tsemitsdate(): ParserT<Date>; noMomentreference left indist/pnpm check-git-clean— committeddist/in syncprettier --check 'src/**/*.ts'— cleangrep -c moment pnpm-lock.yaml→0🤖 Generated with Claude Code