temporal-fmt 0.9.41 → 0.9.70

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (92) hide show
  1. package/MODS.md +148 -0
  2. package/README.md +16 -289
  3. package/dist/calendarUtils.cjs +17 -9
  4. package/dist/calendarUtils.cjs.map +1 -1
  5. package/dist/calendarUtils.js +2 -2
  6. package/dist/{chunk-HJCPNB7J.js → chunk-3FMUGYE2.js} +217 -52
  7. package/dist/chunk-3FMUGYE2.js.map +1 -0
  8. package/dist/{chunk-YXN5RAQ3.js → chunk-64UB3WDT.js} +72 -7
  9. package/dist/chunk-64UB3WDT.js.map +1 -0
  10. package/dist/{chunk-DDE7YTGV.js → chunk-6WKSAP4Y.js} +187 -85
  11. package/dist/chunk-6WKSAP4Y.js.map +1 -0
  12. package/dist/{chunk-M6D57BHI.js → chunk-6ZIYGBFN.js} +3 -3
  13. package/dist/{chunk-LOJO7SGQ.js → chunk-BLZDBPSV.js} +19 -8
  14. package/dist/chunk-BLZDBPSV.js.map +1 -0
  15. package/dist/{chunk-AX3YNHKK.js → chunk-BQDOTFGC.js} +45 -6
  16. package/dist/chunk-BQDOTFGC.js.map +1 -0
  17. package/dist/{chunk-C3QMVR5T.js → chunk-BZPNSQHI.js} +41 -38
  18. package/dist/chunk-BZPNSQHI.js.map +1 -0
  19. package/dist/{chunk-ZSHHTAKC.js → chunk-KEPIXQK7.js} +4 -4
  20. package/dist/chunk-KEPIXQK7.js.map +1 -0
  21. package/dist/{chunk-UF3RVLVL.js → chunk-N4SYRZIP.js} +21 -11
  22. package/dist/chunk-N4SYRZIP.js.map +1 -0
  23. package/dist/{chunk-Z33O2X7I.js → chunk-SC6ENLEB.js} +11 -38
  24. package/dist/chunk-SC6ENLEB.js.map +1 -0
  25. package/dist/{chunk-JI3B7C5L.js → chunk-UU2XN6N2.js} +21 -6
  26. package/dist/chunk-UU2XN6N2.js.map +1 -0
  27. package/dist/{chunk-5553YBSV.js → chunk-VXG3VD3D.js} +3 -3
  28. package/dist/config.d.cts +1 -1
  29. package/dist/config.d.ts +1 -1
  30. package/dist/duration.cjs +32 -23
  31. package/dist/duration.cjs.map +1 -1
  32. package/dist/duration.js +3 -3
  33. package/dist/format.cjs +76 -20
  34. package/dist/format.cjs.map +1 -1
  35. package/dist/format.js +4 -4
  36. package/dist/index.cjs +666 -245
  37. package/dist/index.cjs.map +1 -1
  38. package/dist/index.d.cts +1 -1
  39. package/dist/index.d.ts +1 -1
  40. package/dist/index.js +95 -27
  41. package/dist/index.js.map +1 -1
  42. package/dist/interval.cjs +495 -199
  43. package/dist/interval.cjs.map +1 -1
  44. package/dist/interval.js +11 -11
  45. package/dist/isoWeek.d.cts +2 -0
  46. package/dist/isoWeek.d.ts +2 -0
  47. package/dist/modApi.d.cts +19 -3
  48. package/dist/modApi.d.ts +19 -3
  49. package/dist/parse.cjs +61 -18
  50. package/dist/parse.cjs.map +1 -1
  51. package/dist/parse.js +4 -4
  52. package/dist/recurrence.cjs +222 -62
  53. package/dist/recurrence.cjs.map +1 -1
  54. package/dist/recurrence.d.cts +5 -0
  55. package/dist/recurrence.d.ts +5 -0
  56. package/dist/recurrence.js +4 -4
  57. package/dist/relativeTime.cjs +12 -8
  58. package/dist/relativeTime.cjs.map +1 -1
  59. package/dist/relativeTime.js +5 -5
  60. package/dist/runtime.d.cts +5 -0
  61. package/dist/runtime.d.ts +5 -0
  62. package/dist/timezone.cjs +87 -33
  63. package/dist/timezone.cjs.map +1 -1
  64. package/dist/timezone.js +4 -4
  65. package/dist/tokenize.d.cts +14 -0
  66. package/dist/tokenize.d.ts +14 -0
  67. package/dist/tokens.d.cts +4 -0
  68. package/dist/tokens.d.ts +4 -0
  69. package/package.json +13 -2
  70. package/scripts/cli.mjs +21 -7
  71. package/scripts/loadMods.d.mts +37 -0
  72. package/scripts/loadMods.mjs +801 -0
  73. package/scripts/managePermissions.mjs +126 -0
  74. package/scripts/modConfig.mjs +96 -0
  75. package/scripts/modSandbox.d.mts +13 -0
  76. package/scripts/modSandbox.mjs +855 -0
  77. package/scripts/modWire.mjs +241 -0
  78. package/scripts/modWorker.mjs +574 -0
  79. package/scripts/semverRange.mjs +157 -0
  80. package/scripts/versions.json +37 -0
  81. package/dist/chunk-AX3YNHKK.js.map +0 -1
  82. package/dist/chunk-C3QMVR5T.js.map +0 -1
  83. package/dist/chunk-DDE7YTGV.js.map +0 -1
  84. package/dist/chunk-HJCPNB7J.js.map +0 -1
  85. package/dist/chunk-JI3B7C5L.js.map +0 -1
  86. package/dist/chunk-LOJO7SGQ.js.map +0 -1
  87. package/dist/chunk-UF3RVLVL.js.map +0 -1
  88. package/dist/chunk-YXN5RAQ3.js.map +0 -1
  89. package/dist/chunk-Z33O2X7I.js.map +0 -1
  90. package/dist/chunk-ZSHHTAKC.js.map +0 -1
  91. /package/dist/{chunk-M6D57BHI.js.map → chunk-6ZIYGBFN.js.map} +0 -0
  92. /package/dist/{chunk-5553YBSV.js.map → chunk-VXG3VD3D.js.map} +0 -0
package/MODS.md ADDED
@@ -0,0 +1,148 @@
1
+ # Mods (advanced, optional)
2
+
3
+ Most people don't need this. Mods are a feature for when you want to fix or
4
+ tweak something in this library without forking the whole thing — not
5
+ something you're expected to reach for day to day. If you never touch
6
+ `mods/`, nothing about normal usage changes for you.
7
+
8
+ `registerLocale`, `createHolidayCalendar`, and `createFormatter` are already
9
+ how you extend this library without forking it — the [README](./README.md)
10
+ covers them under [Locales](./README.md#locales), [Business calendars and
11
+ holidays](./README.md#business-calendars-and-holidays), and [Extending with
12
+ custom tokens](./README.md#extending-with-custom-tokens). Mods are a delivery
13
+ mechanism on top of those same functions: drop a file in a `mods/` folder,
14
+ the CLI picks it up on startup and runs it. No publishing to npm, no build
15
+ step in this repo, no manifest to register anywhere. If you've used a
16
+ Minecraft mods folder, it's the same idea — a file the host looks for and
17
+ loads, not a package the host depends on.
18
+
19
+ This exists so bugfixes and locale corrections don't have to wait on a PR
20
+ merging and a release going out. If en-GB's holiday list is wrong for your
21
+ team, a locale you need isn't covered yet, or you want to shave overhead off
22
+ a hot path, someone can write a mod and drop it in. It's not the right tool
23
+ for genuinely new capability — if a fix needs more than the override surface
24
+ can express, that's a sign to open an issue or PR the feature into the
25
+ library itself, not to keep stretching a mod to cover it.
26
+
27
+ **Building a mod isn't covered here.** For the actual API, the sandbox, and
28
+ how to package one, see [API_DOCS/LEVEL_1.md](./API_DOCS/LEVEL_1.md),
29
+ [API_DOCS/LEVEL_2.md](./API_DOCS/LEVEL_2.md), and
30
+ [API_DOCS/LEVEL_3.md](./API_DOCS/LEVEL_3.md). This document is about running
31
+ mods someone else already wrote.
32
+
33
+ ## Mod API levels
34
+
35
+ Tracks the surface a mod talks to — `ModContext`, permissions, the
36
+ subprocess boundary. Bumps independently of the package version; check
37
+ which level a mod was built against before assuming it still works. A
38
+ `.tfmod` can declare the lowest level it needs via `mod.json`'s
39
+ `minApiLevel` — see [API_DOCS/LEVEL_3.md](./API_DOCS/LEVEL_3.md#declaring-the-api-level-your-mod-needs).
40
+
41
+ | Level | Version | What it added |
42
+ |---|---|---|
43
+ | 1 | 0.9.4, 0.9.41, 0.9.5 | `register(ctx, config)`, direct process access, no sandbox |
44
+ | 2 | 0.9.6+ | Subprocess sandbox, permissions, `hasPermission`, `overrideXxx` |
45
+ | 3 | unreleased | `registerFormatToken`, `ctx.log`, `ctx.reportIssue`, `minApiLevel` |
46
+
47
+ Full details for each are in [API_DOCS/LEVEL_1.md](./API_DOCS/LEVEL_1.md),
48
+ [API_DOCS/LEVEL_2.md](./API_DOCS/LEVEL_2.md), and
49
+ [API_DOCS/LEVEL_3.md](./API_DOCS/LEVEL_3.md).
50
+
51
+ ## Installing a mod
52
+
53
+ Two formats show up in `mods/`, side by side:
54
+
55
+ - **A loose `.mjs` file** — one file, no manifest.
56
+ - **A `.tfmod` archive** — a packaged mod with a `mod.json` manifest, an
57
+ entry point, and optionally its own `data/`.
58
+
59
+ Drop either in `mods/` at your project root (not inside this package's own
60
+ checkout) and run any CLI command. The loader reports what it found:
61
+
62
+ ```
63
+ $ temporal-fmt validate "yyyy-MM-dd"
64
+ temporal-fmt mods:
65
+ loaded en-gb-bank-holidays@1.0.0 (en-gb-bank-holidays.tfmod)
66
+ valid
67
+ ```
68
+
69
+ Each mod loads independently — one being broken doesn't stop the rest, or
70
+ the CLI command you actually ran. A missing `mods/` folder isn't a failure
71
+ either; most projects won't have one, and the loader stays quiet about it.
72
+
73
+ ## Reading the load report
74
+
75
+ Four outcomes per mod:
76
+
77
+ - `loaded` — ran clean, got everything it asked for (if anything).
78
+ - `downgraded` — ran, but an optional permission it asked for was denied.
79
+ Still functional, just with less access than it wanted.
80
+ - `failed` — didn't run at all. The report line says why: bad file, a
81
+ required permission denied, a version mismatch, a thrown error, and so on.
82
+ - `conflict` — informational, not a failure. Two mods registered the same
83
+ locale tag, grammar language, or token name; the report says which one
84
+ won.
85
+
86
+ ## Permissions, from the user's side
87
+
88
+ A `.tfmod` mod may ask for filesystem or process access. On first load (and
89
+ after the mod's version changes), you're prompted in the terminal:
90
+
91
+ ```
92
+ temporal-fmt: allow "data-reader" to access fs:read? (y/N)
93
+ ```
94
+
95
+ Your answer is cached in `.temporal-fmt-permissions.json`, next to `mods/`,
96
+ keyed by `name@version` — so you're only asked again if the mod's version
97
+ bumps, or you delete that file. To change an answer without triggering a
98
+ fresh load:
99
+
100
+ ```
101
+ node scripts/managePermissions.mjs list
102
+ node scripts/managePermissions.mjs grant data-reader@1.0.0 fs:read
103
+ node scripts/managePermissions.mjs deny data-reader@1.0.0 fs:write
104
+ node scripts/managePermissions.mjs reset data-reader@1.0.0
105
+ ```
106
+
107
+ What each capability actually grants, and the difference between a required
108
+ and an optional ask, is in
109
+ [API_DOCS/LEVEL_2.md](./API_DOCS/LEVEL_2.md#permissions).
110
+
111
+ ## Giving a mod settings
112
+
113
+ If a mod declares configurable settings, override them by dropping a JSON
114
+ file at `config/<mod-name>.json` — next to `mods/`, not inside it, so
115
+ updating the mod's `.tfmod` never touches your settings:
116
+
117
+ ```
118
+ your-project/
119
+ ├── mods/
120
+ │ └── en-gb-bank-holidays.tfmod
121
+ └── config/
122
+ └── en-gb-bank-holidays.json — { "includeScottish": true, "yearsAhead": 10 }
123
+ ```
124
+
125
+ Only keys the mod actually declares can be set; anything else is reported as
126
+ a mistake, not silently ignored. See the mod's own docs for what it accepts.
127
+
128
+ ## Using mods outside the CLI
129
+
130
+ `loadMods()` lives in `scripts/loadMods.mjs`, shipped with the published
131
+ package but off the `exports` map (it's Node-only ESM, and a loader that
132
+ spawns subprocesses has no honest CommonJS twin):
133
+
134
+ ```js
135
+ import { loadMods, formatModLoadReport } from './node_modules/temporal-fmt/scripts/loadMods.mjs';
136
+
137
+ const report = await loadMods(); // defaults to ./mods
138
+ if (report.loaded.length > 0 || report.downgraded.length > 0 || report.failed.length > 0) {
139
+ console.error('temporal-fmt mods:\n' + formatModLoadReport(report));
140
+ }
141
+ ```
142
+
143
+ If your bundler won't follow that path import, copy the loader out and
144
+ vendor it. For deterministic teardown of any mod subprocesses (a server that
145
+ hot-reloads mods, say), `stopModSubprocesses()` from `scripts/modSandbox.mjs`
146
+ SIGTERMs every live one.
147
+
148
+ Mod support requires `temporal-fmt` 0.9.4 or later.
package/README.md CHANGED
@@ -20,14 +20,20 @@ Locale-aware tokens need Node 20+ regardless of which path you use — native
20
20
  on 26+, or falling back to the Temporal implementation's own
21
21
  `toLocaleString()` otherwise. Untested below Node 20.
22
22
 
23
+ > **Just want `format`/`parse`, nothing else, ever?** Check out
24
+ > [`temporal-fmt-lite`](https://github.com/DirazCoder/temporal-fmt-lite) —
25
+ > a frozen snapshot of this library's original v0.8.2 surface: the same two
26
+ > functions, the same locale support, no CLI/mods/recurrence/business-calendar
27
+ > weight to opt out of via subpaths, and no new tokens or exports getting
28
+ > added later. Security and correctness fixes only, no LTS promise. If you
29
+ > outgrow it, migrating to the full `temporal-fmt` is additive, not a rewrite.
30
+
23
31
  ## Install
24
32
 
25
33
  ```sh
26
34
  npm install temporal-fmt
27
35
  ```
28
36
 
29
- [View on npm](https://www.npmjs.com/package/temporal-fmt)
30
-
31
37
  ## Get started
32
38
 
33
39
  ```js
@@ -70,7 +76,7 @@ The package looks large on npm — locales, recurrence, business calendars, time
70
76
  - [Extending with custom tokens](#extending-with-custom-tokens)
71
77
  - [IDE tooling data](#ide-tooling-data)
72
78
  - [CLI](#cli)
73
- - [Mods](#mods)
79
+ - [Mods (advanced, optional)](#mods-advanced-optional) — full guide in [MODS.md](./MODS.md)
74
80
  - [Subpath imports](#subpath-imports)
75
81
  - [Migrating from Day.js or date-fns](#migrating-from-dayjs-or-date-fns)
76
82
  - [Known limitations](#known-limitations)
@@ -897,288 +903,7 @@ Type a subcommand with all its arguments inline (`validate yyyy-MM-dd`) or just
897
903
 
898
904
  ## Mods (advanced, optional)
899
905
 
900
- Most people don't need this. It's a feature I added just in case you want to fix or tweak something in the library without forking the whole thing — not something you're expected to reach for day to day. If you never touch `mods/`, nothing about normal usage changes for you.
901
-
902
- The mod-related scripts (`loadMods.mjs`, `modConfig.mjs`, `semverRange.mjs`) live in this repo's `scripts/` folder, but they're not part of what actually ships in the npm package — the published package only includes `cli.mjs` from that folder. So if you installed this from npm, those files simply aren't on disk in your `node_modules`; they're repo-only. See [Using mods outside the CLI](#using-mods-outside-the-cli) if you want to copy the loader out of this repo and use it yourself.
903
-
904
- `registerLocale`, `createHolidayCalendar`, and `createFormatter` are already how you extend this library without forking it — [Locales](#locales), [Business calendars and holidays](#business-calendars-and-holidays), and [Extending with custom tokens](#extending-with-custom-tokens) all cover them. Mods are just a delivery mechanism on top of those same functions: drop a file in a `mods/` folder, the CLI picks it up on startup and runs it. No publishing to npm, no build step in this repo, no manifest to register anywhere. If you've used a Minecraft mods folder, it's the same idea — a file the host looks for and loads, not a package the host depends on.
905
-
906
- This exists so bugfixes and locale corrections don't have to wait on a PR merging and a release going out. If en-GB's holiday list is wrong for your team, a locale you need isn't covered yet, or you want to shave overhead off a hot path, write a mod and drop it in. It's not the right tool for genuinely new capability — if you're building something the override surface can't express, that's a sign to open an issue or PR the feature into the library itself, not to keep stretching a mod to cover it. Whether a given fix ever gets upstreamed into this repo is a separate question from whether it works today as a mod.
907
-
908
- ### Writing a mod
909
-
910
- A mod is a `.mjs` file that default-exports an object with a `name` and a `register(ctx, config)` function. `ctx` is the same registration API `index.ts` exports for everyone else — `registerLocale`, `registerLocaleVocab`, `registerRelativeGrammar`, `createFormatter`, `createHolidayCalendar` — nothing beyond that. A mod that needs more than those five functions expose is asking for something this library doesn't support yet, not something to route around by reaching into internals that could shift under it without warning. `config` is `{}` for a loose `.mjs` mod — there's no manifest to declare settings in, so there's nothing to resolve; see [Mod settings and `config/`](#mod-settings-and-config) for mods that need user-adjustable settings, which means packaging as `.tfmod`.
911
-
912
- ```js
913
- // mods/en-gb-bank-holidays.mjs
914
- export default {
915
- name: 'en-gb-bank-holidays',
916
- version: '1.0.0',
917
- register(ctx) {
918
- ctx.createHolidayCalendar([
919
- { month: 1, day: 1, name: "New Year's Day" },
920
- { month: 12, day: 25, name: 'Christmas Day' },
921
- { month: 12, day: 26, name: 'Boxing Day' },
922
- ]);
923
- },
924
- };
925
- ```
926
-
927
- Put that in `mods/` at your project root (the folder the CLI is run from, not inside this package's own checkout) and run any CLI command — the loader reports what it found on stderr:
928
-
929
- ```
930
- $ temporal-fmt validate "yyyy-MM-dd"
931
- temporal-fmt mods:
932
- loaded en-gb-bank-holidays@1.0.0 (en-gb-bank-holidays.mjs)
933
- valid
934
- ```
935
-
936
- `version` is optional and only shows up in that report — it's for your own tracking, not something the loader checks. That's a different field from `temporalFmtVersion`, which *is* checked against the installed `temporal-fmt` version, but only exists on `.tfmod` manifests (see [Pinning a mod to a `temporal-fmt` version](#pinning-a-mod-to-a-temporal-fmt-version)) — a loose `.mjs` mod has no manifest to declare it in.
937
-
938
- ### Packaging a mod as `.tfmod`
939
-
940
- A loose `.mjs` file covers the common case, but it's one file — no bundled data, and the loader has to `import()` it just to find out its `name` before deciding load order. For anything bigger than that, package the mod as a `.tfmod` archive instead: a gzipped tar (same format as `.tgz`, renamed for identity) containing a manifest the loader can read without running any code, plus the mod's actual implementation:
941
-
942
- ```
943
- en-gb-bank-holidays.tfmod
944
- ├── mod.json — name, version, main, requires, priority, temporalFmtVersion, config
945
- ├── main.mjs — the mod's entry point (same shape as a loose .mjs mod's default export, minus `name`/`version`/`requires`/`priority` — mod.json owns those)
946
- └── data/ — optional: JSON files, locale tables, anything main.mjs wants to read at register() time
947
- ```
948
-
949
- ```json
950
- // mod.json
951
- {
952
- "name": "en-gb-bank-holidays",
953
- "version": "1.0.0",
954
- "main": "main.mjs",
955
- "requires": ["some-other-mod"],
956
- "priority": 0,
957
- "temporalFmtVersion": "^0.9.0"
958
- }
959
- ```
960
-
961
- ```js
962
- // main.mjs
963
- export default {
964
- register(ctx) {
965
- ctx.createHolidayCalendar([
966
- { month: 1, day: 1, name: "New Year's Day" },
967
- { month: 12, day: 25, name: 'Christmas Day' },
968
- ]);
969
- },
970
- };
971
- ```
972
-
973
- Build the archive with plain `tar` — no special tooling:
974
-
975
- ```sh
976
- tar -czf en-gb-bank-holidays.tfmod mod.json main.mjs data/
977
- ```
978
-
979
- Drop that in `mods/` alongside any loose `.mjs` mods you have; the loader treats both formats as one pool for load-order and conflict purposes. The report shows `mod.json`'s `name`, not anything from `main.mjs` itself:
980
-
981
- ```
982
- $ temporal-fmt validate "yyyy-MM-dd"
983
- temporal-fmt mods:
984
- loaded en-gb-bank-holidays@1.0.0 (en-gb-bank-holidays.tfmod)
985
- valid
986
- ```
987
-
988
- Why bother with an archive format at all instead of just supporting multi-file `.mjs` mods directly: `mod.json` is metadata the loader can read with zero code execution, which is what makes cross-mod dependency resolution work honestly — with a loose `.mjs` mod, the loader has no choice but to `import()` the file to learn its `name`/`requires`, before it even knows whether that mod should run. A `.tfmod`'s manifest is checked, and the whole dependency graph is resolved, before `main.mjs` is ever imported. At the current few-mods-loaded-once-at-CLI-startup scale that distinction mostly doesn't matter — but it's the honest reason the format exists rather than "loose files but with a folder," and it's what a "list what's installed without running any of it" feature would build on if that ever comes up.
989
-
990
- Failure modes are per-archive, same as loose mods — one bad `.tfmod` doesn't block anything else in `mods/`:
991
-
992
- - `mod.json` missing or malformed (no `name`, no `main`, or `requires`/`priority`/`temporalFmtVersion`/`config` the wrong type) — reported with what was expected, `main.mjs` is never imported.
993
- - `mod.json` names a `main` file that isn't actually in the archive — reported with the missing filename.
994
- - The archive isn't a valid gzip/tar (corrupted, wrong format, a `.tfmod` extension slapped on some other file) — reported with the extraction error.
995
- - `main.mjs`'s default export doesn't have a `register` function — reported, same as a loose mod's malformed export.
996
- - `temporalFmtVersion` doesn't match the installed `temporal-fmt` version — reported with the range and the actual version, `main.mjs` is never imported. See [Pinning a mod to a `temporal-fmt` version](#pinning-a-mod-to-a-temporal-fmt-version).
997
-
998
- Extraction happens to a temporary directory that's cleaned up after the load pass — nothing from a `.tfmod` sticks around on disk after the CLI command finishes. Extraction shells out to the system `tar` binary rather than adding a tar/gzip-parsing dependency, consistent with this package staying dependency-free (see [Providing `Temporal`](#providing-temporal) for the same call made about the polyfill) — if `tar` isn't on the system `PATH`, the archive fails to load with that reason rather than crashing the CLI.
999
-
1000
- ### Pinning a mod to a `temporal-fmt` version
1001
-
1002
- `mod.json` can declare `temporalFmtVersion`, either an exact version (`"0.9.32"`) or a caret range (`"^0.9.0"`, meaning ">=0.9.0, <0.10.0" — same meaning npm gives `^` in `package.json`). If the installed `temporal-fmt` doesn't satisfy it, the mod fails to load with the range and the actual version, before `main.mjs` is ever imported:
1003
-
1004
- ```
1005
- failed holidays.tfmod: "en-gb-bank-holidays" needs temporal-fmt ^2.0.0 (>=2.0.0 <3.0.0), host is 0.9.32
1006
- ```
1007
-
1008
- This exists because nothing else catches the alternative: a mod built against one version's override surface (which functions are zero-fanout and therefore overridable — see [Overriding functions](#overriding-functions)) has no way to know if a future release moved a function it depends on, and would otherwise fail with whatever confusing error `register()` happens to throw, or — worse — silently do nothing if the call it expected to matter just no longer has any effect. A declared range turns that into one clear, pre-`register()` failure instead.
1009
-
1010
- Omitting `temporalFmtVersion` is allowed — the mod loads against whatever version is installed, same as before this field existed. Loose `.mjs` mods have no manifest to put this in at all, so they can't declare a version requirement; that's one real reason to prefer `.tfmod` for anything you plan to distribute rather than just run yourself.
1011
-
1012
- There's no dependency-resolution logic here, unlike `requires`/`priority` — this is a single boolean check (does the host version satisfy the range), not something that affects load order.
1013
-
1014
- ### Mod settings and `config/`
1015
-
1016
- A mod can declare user-adjustable settings in `mod.json`'s `config` array, and `register()` receives the resolved values as its second argument:
1017
-
1018
- ```json
1019
- // mod.json
1020
- {
1021
- "name": "en-gb-bank-holidays",
1022
- "main": "main.mjs",
1023
- "config": [
1024
- { "key": "includeScottish", "type": "boolean", "default": false },
1025
- { "key": "observedRule", "type": "enum", "default": "nearest-weekday", "choices": ["nearest-weekday", "strict-date"] },
1026
- { "key": "yearsAhead", "type": "number", "default": 5, "min": 1, "max": 20 }
1027
- ]
1028
- }
1029
- ```
1030
-
1031
- ```js
1032
- // main.mjs
1033
- export default {
1034
- register(ctx, config) {
1035
- const years = config.yearsAhead; // 5, unless overridden below
1036
- ctx.createHolidayCalendar(buildHolidays({ scottish: config.includeScottish, years }));
1037
- },
1038
- };
1039
- ```
1040
-
1041
- Four setting types are supported: `string`, `number` (with optional `min`/`max`), `boolean`, and `enum` (a string constrained to `choices`). Every entry needs a `key` and a `default` — the default is what `register()` gets if the user hasn't overridden that setting, which also means a mod with no `config/<name>.json` file on disk at all still runs normally, just entirely on defaults.
1042
-
1043
- To override a setting, drop a JSON file at `config/<mod-name>.json` — **next to `mods/`, not inside it** (so re-downloading or updating the `.tfmod` never touches a user's settings, the same reason Forge keeps `config/` and `mods/` as siblings rather than bundling settings into the jar):
1044
-
1045
- ```
1046
- your-project/
1047
- ├── mods/
1048
- │ └── en-gb-bank-holidays.tfmod
1049
- └── config/
1050
- └── en-gb-bank-holidays.json — { "includeScottish": true, "yearsAhead": 10 }
1051
- ```
1052
-
1053
- Only keys the schema actually declares can be set — anything else is a mistake worth surfacing, not a silent no-op:
1054
-
1055
- ```
1056
- temporal-fmt mods:
1057
- loaded en-gb-bank-holidays@1.0.0 (en-gb-bank-holidays.tfmod)
1058
- failed config/en-gb-bank-holidays.json: en-gb-bank-holidays: config key "yearsAhead" must be <= 20, got 50 (using default)
1059
- failed config/en-gb-bank-holidays.json: en-gb-bank-holidays: unknown config key "includeWelsh" (not declared in this mod's schema)
1060
- ```
1061
-
1062
- An invalid value for a declared key falls back to that key's default rather than failing the whole mod — one typo'd number in a config file shouldn't take down a working mod, but it's reported so the mistake doesn't go unnoticed either. This is deliberately not JSON Schema: no nesting, no `$ref`, no conditional rules — just the handful of primitive shapes an actual setting realistically is, kept dependency-free the same way `temporalFmtVersion` checking and `.tfmod` extraction are.
1063
-
1064
- Loose `.mjs` mods have no manifest to declare a schema in, so `register()`'s second argument is always `{}` for them — same as a `.tfmod` mod that didn't declare a `config` field at all.
1065
-
1066
- ### Load order, dependencies, and conflicts
1067
-
1068
- By default mods load in filename order — alphabetical, deterministic, but not something you'd want to rely on once two mods actually need to run in a specific order relative to each other. Two fields on the mod object control that directly:
1069
-
1070
- - `requires: string[]` — other mods' `name` fields that must load (and finish `register()`) before this one. The loader resolves this as a dependency graph, not just "sort requires first" — if A requires B and B requires nothing, B always loads first regardless of filename.
1071
- - `priority: number` — tiebreak for mods with no dependency relationship to each other. Higher loads later. Defaults to `0`.
1072
-
1073
- ```js
1074
- export default {
1075
- name: 'extended-en-gb-holidays',
1076
- requires: ['en-gb-bank-holidays'],
1077
- priority: 10,
1078
- register(ctx) {
1079
- // runs after en-gb-bank-holidays, and after anything else at a lower priority
1080
- },
1081
- };
1082
- ```
1083
-
1084
- Two failure modes come out of this, both reported per-mod without blocking the rest:
1085
-
1086
- - **Missing dependency** — `requires` names a mod that isn't in `mods/`. That mod fails to load; whatever it would've registered doesn't happen, and other mods that don't depend on it load normally.
1087
- - **Circular dependency** — A requires B requires A (or a longer cycle). Every mod in the cycle fails, each reported with what it's still waiting on.
1088
-
1089
- Registration itself is still last-write-wins, same as calling `registerLocale` twice for the same tag outside of mods — that's existing, intentional behavior (see [Locales](#locales)), not something mods change. What mods add is *visibility* into it: if two mods register the same locale tag, the same relative-time-grammar language, or the same custom token name, the load report calls it out as a conflict and says which one won:
1090
-
1091
- ```
1092
- temporal-fmt mods:
1093
- loaded holiday-pack-a (conflict-1.mjs)
1094
- loaded holiday-pack-b (conflict-2.mjs)
1095
- conflict on locale "cv-CV": holiday-pack-a, holiday-pack-b — "holiday-pack-b" wins (loaded last)
1096
- ```
1097
-
1098
- This is informational, not a failure — both mods still loaded, the last one to register just took the key, and now you know it happened instead of silently getting whichever mod's filename sorted last. If that's not what you want, `priority` is the knob: raise the one that should win, or add a `requires` so the loser explicitly runs first and the winner's intent is unambiguous in the mod itself, not just in a startup log line.
1099
-
1100
- Mod names have to be unique across `mods/` — two files claiming the same `name` is ambiguous the moment either one shows up in another mod's `requires`, so the second one to load fails with which file already claimed that name.
1101
-
1102
- ### Overriding functions
1103
-
1104
- The five registration functions above are additive — they add a locale, a holiday set, a token, alongside whatever's already there. `ctx.overrideFormat` and `ctx.overrideParse` work differently: they let a mod replace the actual `format()`/`parse()` implementation everywhere in the library, which is what makes a real bugfix or performance mod possible rather than just new data being registered alongside an unfixed bug.
1105
-
1106
- ```js
1107
- export default {
1108
- name: 'fast-format',
1109
- register(ctx) {
1110
- ctx.overrideFormat((original, value, formatStr, options) => {
1111
- // Handle the one hot-path format string yourself; fall back to the
1112
- // real implementation for everything else.
1113
- if (formatStr === 'yyyy-MM-dd') {
1114
- return `${value.year}-${String(value.month).padStart(2, '0')}-${String(value.day).padStart(2, '0')}`;
1115
- }
1116
- return original(value, formatStr, options);
1117
- });
1118
- },
1119
- };
1120
- ```
1121
-
1122
- `impl` always receives the real built-in as its first argument (`original`), regardless of what else is loaded — call it to keep existing behavior for cases you're not trying to change, or ignore it to replace the behavior outright. The override applies consistently everywhere in the library, not just to whoever imports the function from the package root — `formatRange()`'s internal use of `format()`, for instance, sees it too. Remove the mod and restart, and it's back to the unmodified built-in; nothing about this touches the source file on disk.
1123
-
1124
- **Only one mod may hold each override point.** A second override call for the same function — from any mod, even one that `requires` the first — fails immediately with which mod already owns it:
1125
-
1126
- ```
1127
- temporal-fmt mods:
1128
- loaded override-1 (a-override1.mjs)
1129
- failed b-override2.mjs: temporal-fmt: "format" is already overridden by mod "override-1" — mod "override-2" can't also override it. [...]
1130
- ```
1131
-
1132
- This is a hard failure, not last-write-wins like the registration functions — two mods silently fighting over the same function's behavior is a correctness bug in whatever depends on this library, not a cosmetic surprise. There's no mechanism for two separate mod files to layer through the same override point in sequence; if two mods both need to change a function's behavior, one has to incorporate the other's fix directly rather than composing through the override twice.
1133
-
1134
- **Which functions are overridable.** `format`, `formatToParts`, and `parse` always were. Beyond those, any function in this library that nothing *else* in the library calls internally is also overridable — if a function has zero internal call sites, there's no risk of some other module holding a stale direct reference that a mod's fix would silently fail to reach, so it gets the same `overrideXxx()` treatment. As of this version, that's:
1135
-
1136
- `compileFormat`, `compileParser`, `parseRelative`, `explainFormat`, `tokenizeFormat`, `listTokens`, `tokenInfo`, `isValidFormat`, `validateFormat`, `fieldForToken`, `monthsInYear`, `isLeapYear`, `isLeapMonth`, `weekOfYear`, `weekYear`, `getMonth`, `getWeekday`, `isEqual`, `isBefore`, `isAfter`, `clamp`, `isBetween`, `isToday`, `isTomorrow`, `isYesterday`, `isSameDay`, `isSameWeek`, `isSameMonth`, `isSameQuarter`, `isSameYear`, `isWeekday`, `floor`, `ceil`, `truncate`, `parseRFC3339`, `formatRFC3339`, `parseRFC2822`, `parseHTTPDate`, `fromUnixMicroseconds`, `fromUnixNanoseconds`, `toUnixSeconds`, `toUnixMilliseconds`, `toUnixMicroseconds`, `toUnixNanoseconds`, `parseSQL`, `formatSQL`, `formatDurationToParts`, `parseDuration`, `parseISODuration`, `formatISODuration`, `balanceDuration`, `compareDuration`, `subtractDuration`, `getLocale`, `hasLocale`, `createConfig`, `mergeWithConfig`, `listRegisteredGrammars`, `interval`, `overlaps`, `intersection`, `union`, `mergeIntervals`, `formatRangeToParts`, `between`, `parseRRule`, `formatRRule`, `createBusinessCalendar`, `subtractBusinessDays`, `nextHoliday`, `previousHoliday`, `resolveZoned`, `getNextTransition`, `getPreviousTransition`, `possibleInstantsFor`, `getAutocompleteData`, `getHoverDocs`, `getInlineDiagnostics`, `previewFormat`, `getDocUrl`, `translateDateFnsFormatString`.
1137
-
1138
- Each follows the `ctx.overrideXxx((original, ...args) => ...)` shape shown above for `overrideFormat`. Functions *not* in this list — `round`, `subtract`, `difference`, `formatDistance`, and others that other parts of this library call directly — aren't overridable this way: something else in the codebase holds its own direct reference to them, so a mod's override would silently miss those internal callers, which is worse than not offering the override at all. A function moves onto this list only when an audit confirms nothing internal still calls it directly. If you need to change one of those, that's a real feature request for making it internally indirect first, not something `overrideFormat`-style code can paper over.
1139
-
1140
- ### If you're writing the mod in TypeScript
1141
-
1142
- Compile it and rename the output before it goes in `mods/` — the loader only accepts `.mjs`. It won't run a TS file for you, and it won't skip one quietly either: a `.ts` file sitting in `mods/` shows up in the load report as a failure with the exact compile command to run, because a mod that silently never loads is worse than one that fails loudly.
1143
-
1144
- ```sh
1145
- tsc en-gb-bank-holidays.ts --module esnext --target esnext --outDir mods
1146
- mv mods/en-gb-bank-holidays.js mods/en-gb-bank-holidays.mjs
1147
- ```
1148
-
1149
- If you're importing `ModContext` or `Mod` for the types while you write it, both are exported from `temporal-fmt` itself:
1150
-
1151
- ```ts
1152
- import type { Mod, ModContext } from 'temporal-fmt';
1153
-
1154
- const mod: Mod = {
1155
- name: 'en-gb-bank-holidays',
1156
- register(ctx: ModContext) {
1157
- ctx.createHolidayCalendar([{ month: 1, day: 1, name: "New Year's Day" }]);
1158
- },
1159
- };
1160
-
1161
- export default mod;
1162
- ```
1163
-
1164
- ### What happens when a mod is broken
1165
-
1166
- Each mod loads independently — one throwing doesn't stop the rest from loading, and it doesn't stop the CLI command you actually ran. Every failure mode ends up as one line in the report:
1167
-
1168
- - Wrong file extension (`.ts`, `.js`, anything but `.mjs`) — reported with the compile-and-rename instructions above.
1169
- - Default export isn't shaped right (missing `name`, missing `register`, `register` isn't a function, or `requires`/`priority` are the wrong type) — reported with what was expected.
1170
- - The file fails to import (a syntax error, a bad import path inside the mod) — reported with the underlying error message.
1171
- - Two mods claim the same `name` — reported against whichever file loaded second.
1172
- - A `requires` entry names a mod that isn't present, or is part of a dependency cycle — see [Load order, dependencies, and conflicts](#load-order-dependencies-and-conflicts).
1173
- - `register()` throws — reported with the thrown message, same as any other registration call in this library (see [Typed errors](#typed-errors) for what `registerLocale`/`createHolidayCalendar` themselves throw on bad input).
1174
-
1175
- None of these bring down the CLI. A `mods/` folder that doesn't exist is the common case, not a failure — most runs won't have one, and the loader stays silent about it rather than printing "no mods found" noise on every command.
1176
-
1177
- ### Using mods outside the CLI
1178
-
1179
- `loadMods()` only exists in `scripts/loadMods.mjs`, not in the published library — it needs `fs`/`path`, and this package stays dependency-free and Node-agnostic on its actual import surface (`import { format } from 'temporal-fmt'` shouldn't drag in filesystem code for someone using this in a browser). If you're embedding `temporal-fmt` in your own app rather than using the CLI, copy that loader (or write your own — it's about 400+ lines) and call it at your own startup with `buildModContext()` and `isMod()` from the library, which *are* published.
1180
-
1181
- Mod support (loose `.mjs` mods, `.tfmod` archives, and everything under [Mods](#mods)) requires `temporal-fmt` 0.9.4 or later — that's the version it landed in.
906
+ Mod support (0.9.4+) lets you extend or patch behavior without forking. MODS.md covers installing and running mods someone else wrote — the sandbox from the user side, permissions, load order, and what happens when a mod is broken. Writing your own mod is a separate guide, split by API level: API_DOCS/LEVEL_1.md (0.9.4–0.9.5), API_DOCS/LEVEL_2.md (0.9.6-0.9.61), and API_DOCS/LEVEL_3.md (0.9.70+).
1182
907
 
1183
908
  ## Subpath imports
1184
909
 
@@ -1198,6 +923,8 @@ import { registerLocale } from 'temporal-fmt/locale';
1198
923
 
1199
924
  The rest of the API (arithmetic, comparison, rounding, intervals-adjacent helpers not listed above, business calendars, holidays, serialization, config, type guards, typed errors, the analyzer, and IDE tooling data) is only available from the main `temporal-fmt` entry point — there's no dedicated subpath for those yet.
1200
925
 
926
+ Two Node-only script subpaths exist outside the library API proper: `scripts/loadMods.mjs` (the mod loader, with its sandbox) and `scripts/modSandbox.mjs` (subprocess lifecycle for mods that installed runtime overrides). They ship in the package but aren't in the `exports` map — the loader is ESM-only and has no honest CommonJS twin, so you import them by path rather than by bare specifier. See [MODS.md](./MODS.md); most installs will never touch either.
927
+
1201
928
  `sideEffects: false` is set in `package.json`, so a bundler with tree-shaking enabled genuinely drops what you don't import. Measured with esbuild: `import { format } from 'temporal-fmt/format'` bundles to ~27KB, versus ~68KB for the same single function pulled from the bare `temporal-fmt` entry — the main entry point re-exports everything, so anything imported from it drags the whole graph along regardless of what you actually call. If bundle size matters for your use case, import from the subpath, not the package root.
1202
929
 
1203
930
  ## Migrating from Day.js or date-fns
@@ -1302,14 +1029,14 @@ Migrate file by file, dropping the wrapper once nothing calls the old path anymo
1302
1029
 
1303
1030
  ## Related tools
1304
1031
 
1305
- Neither of these ships as part of this repository — separate packages, install them on their own:
1032
+ Neither of these ships as part of this repository — separate packages. **Both are now deprecated and archived** — single-maintainer bandwidth, no further updates — but the last published versions still work if you want them:
1306
1033
 
1307
- - [`eslint-plugin-temporal-fmt`](https://www.npmjs.com/package/eslint-plugin-temporal-fmt) — lints format strings for common mistakes (e.g. `hh` without `a`). This is what backs the `analyzeFormat(formatStr).warnings` check mentioned in [Introspection and the analyzer](#introspection-and-the-analyzer) — same underlying metadata, surfaced as a lint diagnostic instead of a runtime call.
1308
- - [`temporal-fmt-codemod`](https://github.com/DirazCoder/temporal-fmt-codemod) — a jscodeshift AST codemod that rewrites `dayjs(x).format(...)`/date-fns `format(...)` *call sites* across a codebase, not just format-string literals. A different job from the CLI's `translate` subcommand (see [CLI](#cli)), which only translates a format string you hand it and doesn't touch call sites; use this instead if you're migrating an entire codebase and want the calls themselves rewritten.
1034
+ - [`eslint-plugin-temporal-fmt`](https://www.npmjs.com/package/eslint-plugin-temporal-fmt) *(deprecated, archived)* — lints format strings for common mistakes (e.g. `hh` without `a`). This is what backed the `analyzeFormat(formatStr).warnings` check mentioned in [Introspection and the analyzer](#introspection-and-the-analyzer) — same underlying metadata, surfaced as a lint diagnostic instead of a runtime call.
1035
+ - [`temporal-fmt-codemod`](https://github.com/DirazCoder/temporal-fmt-codemod) *(deprecated, archived)* — a jscodeshift AST codemod that rewrote `dayjs(x).format(...)`/date-fns `format(...)` *call sites* across a codebase, not just format-string literals. A different job from the CLI's `translate` subcommand (see [CLI](#cli)), which only translates a format string you hand it and doesn't touch call sites; this was the option for migrating an entire codebase and rewriting the calls themselves.
1309
1036
 
1310
1037
  ## Testing
1311
1038
 
1312
- This library is heavily tested. The `node:test` suite (`test/*.test.js`) runs 1300+ cases covering hand-picked scenarios, fuzzing, and adversarial input, alongside a separate `vitest/` suite unit-testing internals directly. On top of that there's a dedicated conformance suite, smoke tests that check the package actually resolves correctly under CJS/ESM/bundler/nodenext, and type tests. If it's mentioned in this README, it's backed by a test — not just a docstring.
1039
+ This library is heavily tested. The `node:test` suite (`test/*.test.js`) runs 1400+ cases covering hand-picked scenarios, fuzzing, and adversarial input, alongside a separate `vitest/` suite unit-testing internals directly. On top of that there's a dedicated conformance suite, smoke tests that check the package actually resolves correctly under CJS/ESM/bundler/nodenext, and type tests. If it's mentioned in this README, it's backed by a test — not just a docstring.
1313
1040
 
1314
1041
  # Conformance fixtures
1315
1042
 
@@ -51,14 +51,23 @@ function dayOfYear(year, month, day) {
51
51
  return doy;
52
52
  }
53
53
  var REFERENCE_YEAR = 2e3;
54
+ var REFERENCE_JAN1_DAY_OF_WEEK = 6;
55
+ function daysFromCivil(year, month, day) {
56
+ const y2 = month <= 2 ? year - 1 : year;
57
+ const era = Math.floor(y2 / 400);
58
+ const yoe = y2 - era * 400;
59
+ const m2 = month > 2 ? month - 3 : month + 9;
60
+ const doy = Math.floor((153 * m2 + 2) / 5) + day - 1;
61
+ const doe = yoe * 365 + Math.floor(yoe / 4) - Math.floor(yoe / 100) + doy;
62
+ return era * 146097 + doe - 719468;
63
+ }
64
+ function dayOfWeekFromCivil(year, month, day) {
65
+ const days = daysFromCivil(year, month, day);
66
+ return ((days + 3) % 7 + 7) % 7 + 1;
67
+ }
54
68
  function dayOfWeekOfJan1(year) {
55
- let offset = 0;
56
- if (year >= REFERENCE_YEAR) {
57
- for (let y = REFERENCE_YEAR; y < year; y++) offset += daysInYear(y);
58
- } else {
59
- for (let y = year; y < REFERENCE_YEAR; y++) offset -= daysInYear(y);
60
- }
61
- const zeroIndexed = ((6 - 1 + offset) % 7 + 7) % 7;
69
+ const days = daysFromCivil(year, 1, 1) - daysFromCivil(REFERENCE_YEAR, 1, 1);
70
+ const zeroIndexed = ((REFERENCE_JAN1_DAY_OF_WEEK - 1 + days) % 7 + 7) % 7;
62
71
  return zeroIndexed + 1;
63
72
  }
64
73
  function isoWeekYearAndWeek(year, month, day, dayOfWeek) {
@@ -161,8 +170,7 @@ function recomputeDayOfWeek(view) {
161
170
  * reaches here, year/month/day are guaranteed present too. */
162
171
  if (typeof view.year !== "number" || typeof view.month !== "number" || typeof view.day !== "number") return;
163
172
  /* c8 ignore stop @preserve */
164
- const jsDow = new Date(Date.UTC(view.year, view.month - 1, view.day)).getUTCDay();
165
- view.dayOfWeek = jsDow === 0 ? 7 : jsDow;
173
+ view.dayOfWeek = dayOfWeekFromCivil(view.year, view.month, view.day);
166
174
  }
167
175
  function startOf(value, unit) {
168
176
  const view = asDateFieldView(value);