temporal-fmt 0.9.61 → 0.9.71
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.
- package/MODS.md +70 -754
- package/README.md +27 -16
- package/dist/{chunk-G6RYR72T.js → chunk-3SC5AVSY.js} +3 -3
- package/dist/{chunk-LPSNAV3S.js → chunk-BQET6RUR.js} +38 -4
- package/dist/chunk-BQET6RUR.js.map +1 -0
- package/dist/{chunk-QIZ2MXS2.js → chunk-CQJYB7E7.js} +11 -5
- package/dist/chunk-CQJYB7E7.js.map +1 -0
- package/dist/{chunk-CGI4FL6H.js → chunk-FLMOMOAQ.js} +2 -2
- package/dist/{chunk-BZPNSQHI.js → chunk-LHR5PCP4.js} +2 -2
- package/dist/{chunk-T7EKGDQT.js → chunk-MXXTJ5CW.js} +4 -4
- package/dist/{chunk-OLNU4LON.js → chunk-QRLQ7RR7.js} +2 -1
- package/dist/chunk-QRLQ7RR7.js.map +1 -0
- package/dist/{chunk-AN4UFCLA.js → chunk-ZT3UXRAA.js} +40 -7
- package/dist/chunk-ZT3UXRAA.js.map +1 -0
- package/dist/{chunk-RIRY5JL2.js → chunk-ZTIF4ZHX.js} +14 -9
- package/dist/chunk-ZTIF4ZHX.js.map +1 -0
- package/dist/config.d.cts +2 -2
- package/dist/config.d.ts +2 -2
- package/dist/duration.cjs.map +1 -1
- package/dist/duration.js +2 -2
- package/dist/format.cjs +88 -1
- package/dist/format.cjs.map +1 -1
- package/dist/format.js +4 -4
- package/dist/index.cjs +110 -3
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +53 -10
- package/dist/index.js.map +1 -1
- package/dist/interval.cjs +60 -2
- package/dist/interval.cjs.map +1 -1
- package/dist/interval.js +8 -8
- package/dist/localeRegistry.cjs.map +1 -1
- package/dist/localeRegistry.js +2 -2
- package/dist/localeVocab.d.cts +4 -3
- package/dist/localeVocab.d.ts +4 -3
- package/dist/modApi.d.cts +18 -4
- package/dist/modApi.d.ts +18 -4
- package/dist/numbering.d.cts +4 -2
- package/dist/numbering.d.ts +4 -2
- package/dist/parse.cjs +55 -1
- package/dist/parse.cjs.map +1 -1
- package/dist/parse.js +4 -4
- package/dist/relativeTime.cjs.map +1 -1
- package/dist/relativeTime.js +3 -3
- package/dist/runtime.d.cts +2 -0
- package/dist/runtime.d.ts +2 -0
- package/dist/tokens.d.cts +4 -0
- package/dist/tokens.d.ts +4 -0
- package/package.json +2 -2
- package/scripts/loadMods.mjs +67 -7
- package/scripts/modWire.mjs +24 -0
- package/scripts/modWorker.mjs +66 -20
- package/scripts/versions.json +10 -0
- package/dist/chunk-AN4UFCLA.js.map +0 -1
- package/dist/chunk-LPSNAV3S.js.map +0 -1
- package/dist/chunk-OLNU4LON.js.map +0 -1
- package/dist/chunk-QIZ2MXS2.js.map +0 -1
- package/dist/chunk-RIRY5JL2.js.map +0 -1
- /package/dist/{chunk-G6RYR72T.js.map → chunk-3SC5AVSY.js.map} +0 -0
- /package/dist/{chunk-CGI4FL6H.js.map → chunk-FLMOMOAQ.js.map} +0 -0
- /package/dist/{chunk-BZPNSQHI.js.map → chunk-LHR5PCP4.js.map} +0 -0
- /package/dist/{chunk-T7EKGDQT.js.map → chunk-MXXTJ5CW.js.map} +0 -0
package/MODS.md
CHANGED
|
@@ -19,388 +19,83 @@ loads, not a package the host depends on.
|
|
|
19
19
|
This exists so bugfixes and locale corrections don't have to wait on a PR
|
|
20
20
|
merging and a release going out. If en-GB's holiday list is wrong for your
|
|
21
21
|
team, a locale you need isn't covered yet, or you want to shave overhead off
|
|
22
|
-
a hot path, write a mod and drop it in. It's not the right tool
|
|
23
|
-
genuinely new capability — if
|
|
24
|
-
can
|
|
25
|
-
library itself, not to keep stretching a mod to cover it.
|
|
26
|
-
fix ever gets upstreamed into this repo is a separate question from whether
|
|
27
|
-
it works today as a mod.
|
|
28
|
-
|
|
29
|
-
## Temporal-Fmt Mod API
|
|
30
|
-
|
|
31
|
-
Tracks changes to the surface a mod talks to — `ModContext`, permissions, the
|
|
32
|
-
subprocess boundary. Bumps independently of the package version; check this
|
|
33
|
-
before assuming a mod built against an older level still works.
|
|
34
|
-
|
|
35
|
-
### Level 2 — 0.9.6
|
|
36
|
-
|
|
37
|
-
- Mods run in their own subprocess under Node's permission model, not the
|
|
38
|
-
host process — no filesystem, child process, or worker access unless
|
|
39
|
-
granted.
|
|
40
|
-
- `.tfmod` mods declare a `permissions` array in `mod.json`; the user is
|
|
41
|
-
prompted once per `name@version` and the answer is cached.
|
|
42
|
-
- `ctx.hasPermission(capability)` for checking a grant and degrading
|
|
43
|
-
gracefully instead of crashing.
|
|
44
|
-
- `register()` is killed after 10s, a runtime override call after 5s, and a
|
|
45
|
-
subprocess is killed if its RSS crosses 512MB.
|
|
46
|
-
- The subprocess doesn't inherit the host's environment variables.
|
|
47
|
-
|
|
48
|
-
### Level 1 — 0.9.4
|
|
49
|
-
|
|
50
|
-
- `register(ctx, config)` running with direct `import()` access to the host
|
|
51
|
-
process. No sandbox, no permissions.
|
|
52
|
-
|
|
53
|
-
## The sandbox
|
|
54
|
-
|
|
55
|
-
A mod's code never runs in your process. Every mod — loose `.mjs` and
|
|
56
|
-
`.tfmod` alike — runs in its own subprocess started with Node's permission
|
|
57
|
-
model ([`--permission`](https://nodejs.org/api/permissions.html) on Node
|
|
58
|
-
22.13+, `--experimental-permission` on Node 20 and early 22, picked per
|
|
59
|
-
running version). With no capabilities granted, Node itself refuses the
|
|
60
|
-
subprocess any filesystem read, filesystem write, child process, or worker
|
|
61
|
-
thread access. The only way a mod affects the host is through the
|
|
62
|
-
`ModContext` API, serialized over an inter-process channel.
|
|
63
|
-
|
|
64
|
-
What this means concretely:
|
|
65
|
-
|
|
66
|
-
- A mod can always read its own files — the `.mjs` file itself, or the
|
|
67
|
-
`.tfmod` archive's extracted contents including its `data/` directory. The
|
|
68
|
-
loader has to be able to load the code, so its own location is readable by
|
|
69
|
-
construction.
|
|
70
|
-
- Everything else needs a granted capability (see
|
|
71
|
-
[Permissions](#permissions)). Denied capabilities aren't a polite request
|
|
72
|
-
the mod can ignore — the subprocess literally cannot open the file,
|
|
73
|
-
because Node blocks the syscall.
|
|
74
|
-
- The subprocess does not inherit your environment variables. `process.env`
|
|
75
|
-
inside a mod contains `PATH`, `TZ`, `LANG`, and a few Windows bootstrap
|
|
76
|
-
variables, nothing else. There is no grant that changes this: environment
|
|
77
|
-
variables are where CI tokens and database URLs live, and the permission
|
|
78
|
-
model has no flag that could gate them, so they don't arrive in the first
|
|
79
|
-
place.
|
|
80
|
-
- Mod output still reaches you: `console.log` from a mod goes to stderr
|
|
81
|
-
(the protocol channel is stdout, or on Windows the reply file).
|
|
82
|
-
|
|
83
|
-
The channel itself is platform-dependent, and the difference is visible
|
|
84
|
-
if you look: on macOS and Linux it's the subprocess's pipes, read directly
|
|
85
|
-
by file descriptor. A Windows named pipe carries no file descriptor for
|
|
86
|
-
the host's end — a fact of the platform, not a Node bug — so there each
|
|
87
|
-
mod's subprocess gets a private scratch directory under the system temp
|
|
88
|
-
folder and the same line protocol rides two plain files. Two consequences
|
|
89
|
-
worth knowing: a runtime-override call costs a few extra milliseconds on
|
|
90
|
-
Windows (the subprocess polls for new requests rather than being woken by
|
|
91
|
-
the kernel), and the subprocess necessarily holds filesystem write access
|
|
92
|
-
to its own scratch directory — the one place its protocol replies live —
|
|
93
|
-
even when `fs:write` was denied. That's the whole carve-out: mod code
|
|
94
|
-
still cannot write anywhere else, so the denial isn't weakened anywhere a
|
|
95
|
-
human keeps files.
|
|
96
|
-
|
|
97
|
-
Four things the sandbox does **not** do, said plainly rather than buried:
|
|
98
|
-
|
|
99
|
-
1. **It does not restrict network access.** Node's permission model cannot
|
|
100
|
-
gate sockets on any supported version, so there is no `net` capability
|
|
101
|
-
to grant or deny. A mod can still import `node:http` and make requests.
|
|
102
|
-
Deleting the global `fetch` removes the most convenient path but not
|
|
103
|
-
`node:http` itself. If your threat model requires no network egress, do
|
|
104
|
-
not run third-party mods.
|
|
105
|
-
2. **It does not sandbox `createFormatter` token handlers at format time.**
|
|
106
|
-
Custom tokens are rebuilt from handler source and run in the host process
|
|
107
|
-
(see [Overriding functions](#overriding-functions) for the why) — treat a
|
|
108
|
-
mod that registers custom tokens as trusted code for that table.
|
|
109
|
-
3. **It is a least-privilege control, not a defense against a determined
|
|
110
|
-
attacker.** Node documents its permission model that way and this sandbox
|
|
111
|
-
inherits the ceiling. One concrete gap: cross-process signaling
|
|
112
|
-
(`process._debugProcess` and friends) isn't gated by the permission model
|
|
113
|
-
on any supported version, so any process owned by the same OS user can
|
|
114
|
-
reach any other. Containing that needs an OS-level sandbox, which is
|
|
115
|
-
explicitly out of scope here.
|
|
116
|
-
4. **It contains runaway mods; it can't prevent them.** A wall-clock
|
|
117
|
-
watchdog (10s for `register()`, 5s per runtime override call) and a
|
|
118
|
-
resident-memory ceiling (512MB per subprocess, polled from the parent)
|
|
119
|
-
kill a mod that hangs or balloons — killed *before* it takes the host
|
|
120
|
-
down, which is containment, not prevention. The memory number is watched
|
|
121
|
-
as actual RSS from the parent, so `ArrayBuffer` allocations count (a V8
|
|
122
|
-
heap limit wouldn't see them) and `--max-old-space-size` set anywhere in
|
|
123
|
-
the process tree can't quietly neuter it.
|
|
124
|
-
|
|
125
|
-
On Node 20 (and Node 22 before 22.13) the permission model is experimental
|
|
126
|
-
and the load report says so; treat sandboxing there as best-effort. On Node
|
|
127
|
-
22.13+ it's stable.
|
|
128
|
-
|
|
129
|
-
## Writing a mod
|
|
130
|
-
|
|
131
|
-
A mod is a `.mjs` file that default-exports an object with a `name` and a
|
|
132
|
-
`register(ctx, config)` function. `ctx` is the same registration API
|
|
133
|
-
`index.ts` exports for everyone else — `registerLocale`,
|
|
134
|
-
`registerLocaleVocab`, `registerRelativeGrammar`, `createFormatter`,
|
|
135
|
-
`createHolidayCalendar` — nothing beyond that. A mod that needs more than
|
|
136
|
-
those functions expose is asking for something this library doesn't support
|
|
137
|
-
yet, not something to route around by reaching into internals that could
|
|
138
|
-
shift under it without warning. `config` is `{}` for a loose `.mjs` mod —
|
|
139
|
-
there's no manifest to declare settings in, so there's nothing to resolve;
|
|
140
|
-
see [Mod settings and `config/`](#mod-settings-and-config) for mods that
|
|
141
|
-
need user-adjustable settings, which means packaging as `.tfmod`.
|
|
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.
|
|
142
26
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
register(ctx) {
|
|
149
|
-
ctx.createHolidayCalendar([
|
|
150
|
-
{ month: 1, day: 1, name: "New Year's Day" },
|
|
151
|
-
{ month: 12, day: 25, name: 'Christmas Day' },
|
|
152
|
-
{ month: 12, day: 26, name: 'Boxing Day' },
|
|
153
|
-
]);
|
|
154
|
-
},
|
|
155
|
-
};
|
|
156
|
-
```
|
|
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.
|
|
157
32
|
|
|
158
|
-
|
|
159
|
-
not inside this package's own checkout) and run any CLI command — the loader
|
|
160
|
-
reports what it found on stderr:
|
|
33
|
+
## Mod API levels
|
|
161
34
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
```
|
|
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).
|
|
168
40
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
`
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
— a loose `.mjs` mod has no manifest to declare it in.
|
|
175
|
-
|
|
176
|
-
A loose `.mjs` mod has no manifest, which also means it has no way to ask
|
|
177
|
-
for capabilities: it runs with **zero** permissions, and a register() that
|
|
178
|
-
touches `node:fs` or spawns anything fails with the access error plus an
|
|
179
|
-
explanation, rather than mysteriously not working. That's the trade for the
|
|
180
|
-
format's simplicity — anything that needs filesystem or process access has
|
|
181
|
-
to be a `.tfmod`.
|
|
182
|
-
|
|
183
|
-
## Packaging a mod as `.tfmod`
|
|
184
|
-
|
|
185
|
-
A loose `.mjs` file covers the common case, but it's one file — no bundled
|
|
186
|
-
data, and the loader has to import it in a sandbox just to find out its
|
|
187
|
-
`name` before deciding load order. For anything bigger than that, package
|
|
188
|
-
the mod as a `.tfmod` archive instead: a gzipped tar (same format as
|
|
189
|
-
`.tgz`, renamed for identity) containing a manifest the loader can read
|
|
190
|
-
without running any code, plus the mod's actual implementation:
|
|
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` |
|
|
191
46
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
├── 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)
|
|
196
|
-
└── data/ — optional: JSON files, locale tables, anything main.mjs wants to read at register() time
|
|
197
|
-
```
|
|
198
|
-
|
|
199
|
-
```json
|
|
200
|
-
// mod.json
|
|
201
|
-
{
|
|
202
|
-
"name": "en-gb-bank-holidays",
|
|
203
|
-
"version": "1.0.0",
|
|
204
|
-
"main": "main.mjs",
|
|
205
|
-
"requires": ["some-other-mod"],
|
|
206
|
-
"priority": 0,
|
|
207
|
-
"permissions": ["fs:read"],
|
|
208
|
-
"temporalFmtVersion": "^0.9.0"
|
|
209
|
-
}
|
|
210
|
-
```
|
|
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).
|
|
211
50
|
|
|
212
|
-
|
|
213
|
-
// main.mjs
|
|
214
|
-
export default {
|
|
215
|
-
register(ctx) {
|
|
216
|
-
ctx.createHolidayCalendar([
|
|
217
|
-
{ month: 1, day: 1, name: "New Year's Day" },
|
|
218
|
-
{ month: 12, day: 25, name: 'Christmas Day' },
|
|
219
|
-
]);
|
|
220
|
-
},
|
|
221
|
-
};
|
|
222
|
-
```
|
|
51
|
+
## Installing a mod
|
|
223
52
|
|
|
224
|
-
|
|
53
|
+
Two formats show up in `mods/`, side by side:
|
|
225
54
|
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
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/`.
|
|
229
58
|
|
|
230
|
-
Drop
|
|
231
|
-
|
|
232
|
-
report shows `mod.json`'s `name`, not anything from `main.mjs` itself:
|
|
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:
|
|
233
61
|
|
|
234
62
|
```
|
|
235
63
|
$ temporal-fmt validate "yyyy-MM-dd"
|
|
236
64
|
temporal-fmt mods:
|
|
237
|
-
loaded en-gb-bank-holidays@1.0.0 (en-gb-bank-holidays.tfmod)
|
|
65
|
+
loaded en-gb-bank-holidays@1.0.0 (en-gb-bank-holidays.tfmod)
|
|
238
66
|
valid
|
|
239
67
|
```
|
|
240
68
|
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
resolution work honestly — with a loose `.mjs` mod, the loader has no choice
|
|
245
|
-
but to import the file (in its sandbox) to learn its `name`/`requires`,
|
|
246
|
-
before it even knows whether that mod should run. A `.tfmod`'s manifest is
|
|
247
|
-
checked, and the whole dependency graph is resolved, before `main.mjs` is
|
|
248
|
-
ever imported. It's also the only shape that can declare `permissions`,
|
|
249
|
-
`config`, or `temporalFmtVersion`, for the same reason: no manifest, no
|
|
250
|
-
declaration.
|
|
251
|
-
|
|
252
|
-
Failure modes are per-archive, same as loose mods — one bad `.tfmod` doesn't
|
|
253
|
-
block anything else in `mods/`:
|
|
254
|
-
|
|
255
|
-
- `mod.json` missing or malformed (no `name`, no `main`, or
|
|
256
|
-
`requires`/`priority`/`temporalFmtVersion`/`config`/`permissions` the
|
|
257
|
-
wrong type) — reported with what was expected, `main.mjs` is never
|
|
258
|
-
imported.
|
|
259
|
-
- `mod.json` names a `main` file that isn't actually in the archive —
|
|
260
|
-
reported with the missing filename.
|
|
261
|
-
- `mod.json` `"main"` (or the mod name) escaping the extraction directory —
|
|
262
|
-
absolute paths, `..` segments, and symlinks that resolve outside are
|
|
263
|
-
rejected before anything is imported. A `.tfmod` runs only files from
|
|
264
|
-
inside its own archive; this is a security boundary, not a nicety.
|
|
265
|
-
- The archive isn't a valid gzip/tar (corrupted, wrong format, a `.tfmod`
|
|
266
|
-
extension slapped on some other file) — reported with the extraction
|
|
267
|
-
error.
|
|
268
|
-
- `main.mjs`'s default export doesn't have a `register` function — reported,
|
|
269
|
-
same as a loose mod's malformed export.
|
|
270
|
-
- `temporalFmtVersion` doesn't match the installed `temporal-fmt` version —
|
|
271
|
-
reported with the range and the actual version, `main.mjs` is never
|
|
272
|
-
imported. See [Pinning a mod to a `temporal-fmt`
|
|
273
|
-
version](#pinning-a-mod-to-a-temporal-fmt-version).
|
|
274
|
-
- `permissions` names something that isn't a capability — reported with the
|
|
275
|
-
supported list. See below.
|
|
276
|
-
|
|
277
|
-
Extraction happens to a temporary directory that's cleaned up after the
|
|
278
|
-
load pass — nothing from a `.tfmod` sticks around on disk after the CLI
|
|
279
|
-
command finishes. Extraction shells out to the system `tar` binary rather
|
|
280
|
-
than adding a tar/gzip-parsing dependency, consistent with this package
|
|
281
|
-
staying dependency-free (see the [README](./README.md#providing-temporal)
|
|
282
|
-
for the same call made about the polyfill) — if `tar` isn't on the system
|
|
283
|
-
`PATH`, the archive fails to load with that reason rather than crashing the
|
|
284
|
-
CLI.
|
|
285
|
-
|
|
286
|
-
## Permissions
|
|
287
|
-
|
|
288
|
-
A `.tfmod` declares what it wants in `mod.json`'s `"permissions"` array,
|
|
289
|
-
marking each entry required or optional:
|
|
290
|
-
|
|
291
|
-
```json
|
|
292
|
-
{
|
|
293
|
-
"name": "data-reader",
|
|
294
|
-
"version": "1.0.0",
|
|
295
|
-
"main": "main.mjs",
|
|
296
|
-
"permissions": [
|
|
297
|
-
{ "capability": "fs:read", "required": true },
|
|
298
|
-
{ "capability": "fs:write", "required": false }
|
|
299
|
-
]
|
|
300
|
-
}
|
|
301
|
-
```
|
|
302
|
-
|
|
303
|
-
The closed list maps one-to-one onto what Node's permission model can
|
|
304
|
-
actually enforce:
|
|
305
|
-
|
|
306
|
-
| Capability | Grants |
|
|
307
|
-
|---|---|
|
|
308
|
-
| `fs:read` | reading files anywhere on the filesystem |
|
|
309
|
-
| `fs:write` | writing files anywhere on the filesystem |
|
|
310
|
-
| `child-process` | spawning child processes |
|
|
311
|
-
| `worker` | starting worker threads |
|
|
312
|
-
|
|
313
|
-
That's the whole list. `fs:read` and `fs:write` are deliberately coarse —
|
|
314
|
-
path scoping ("fs access to only `./data`") isn't something a yes/no
|
|
315
|
-
terminal answer can express honestly, so v1 doesn't pretend to offer it;
|
|
316
|
-
it's a possible follow-up if the permission model's path-scoped flags turn
|
|
317
|
-
out to be worth building on. `net` and `env` are absent because no flag
|
|
318
|
-
backs them: network access can't be restricted by the permission model (see
|
|
319
|
-
[The sandbox](#the-sandbox)), and environment variables are handled by
|
|
320
|
-
never delivering them to the subprocess at all. `addons` is absent because
|
|
321
|
-
native code escapes every other restriction — a mod that needs native
|
|
322
|
-
addons can't be sandboxed, full stop.
|
|
323
|
-
|
|
324
|
-
A bare string array (`"permissions": ["fs:read"]`) still works — it's the
|
|
325
|
-
format from before required/optional existed, and every entry in it is
|
|
326
|
-
treated as required, so nothing that used to fail on denial silently
|
|
327
|
-
downgrades. Leaving `"required"` out of an object entry means the same
|
|
328
|
-
thing: a mod that doesn't say is asking, not wishing.
|
|
329
|
-
|
|
330
|
-
On first load (and after a version bump), the loader asks about each
|
|
331
|
-
requested capability in the terminal:
|
|
332
|
-
|
|
333
|
-
```
|
|
334
|
-
temporal-fmt: allow "data-reader" to access fs:read? (y/N)
|
|
335
|
-
```
|
|
336
|
-
|
|
337
|
-
Empty input is "no". A non-interactive context — CI, piped stdin — can't
|
|
338
|
-
ask anyone, so it denies by default and says so in the report rather than
|
|
339
|
-
quietly granting.
|
|
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.
|
|
340
72
|
|
|
341
|
-
|
|
342
|
-
capability fails the mod's load outright, with the reason in the report:
|
|
73
|
+
## Reading the load report
|
|
343
74
|
|
|
344
|
-
|
|
345
|
-
failed data-reader.tfmod: denied required permission: fs:read — "data-reader" won't load without it. Grant it with "node scripts/managePermissions.mjs grant data-reader@1.0.0 fs:read", or delete .temporal-fmt-permissions.json to re-ask everything.
|
|
346
|
-
```
|
|
75
|
+
Four outcomes per mod:
|
|
347
76
|
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
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.
|
|
353
85
|
|
|
354
|
-
|
|
355
|
-
an optional capability doesn't fail the load — the subprocess starts with
|
|
356
|
-
only what was granted, and the report line reads `downgraded` rather than
|
|
357
|
-
`loaded`, so "this ran, but with less access than it asked for" is visible
|
|
358
|
-
without reading the fine print:
|
|
86
|
+
## Permissions, from the user's side
|
|
359
87
|
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
```
|
|
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:
|
|
363
90
|
|
|
364
|
-
**`ctx.hasPermission(capability)`** tells a mod what it actually got, so
|
|
365
|
-
it can degrade instead of crashing — skip loading supplementary locale
|
|
366
|
-
data from disk when `fs:read` was denied, rather than assuming it's there
|
|
367
|
-
and throwing:
|
|
368
|
-
|
|
369
|
-
```js
|
|
370
|
-
register(ctx) {
|
|
371
|
-
let extra = {};
|
|
372
|
-
if (ctx.hasPermission('fs:read')) {
|
|
373
|
-
extra = JSON.parse(readFileSync('supplementary.json', 'utf8'));
|
|
374
|
-
}
|
|
375
|
-
ctx.registerLocaleVocab('xx-extra', baseVocabMergedWith(extra));
|
|
376
|
-
}
|
|
377
91
|
```
|
|
378
|
-
|
|
379
|
-
Nothing forces this. A mod that doesn't check and touches the missing
|
|
380
|
-
capability anyway fails with the permission model's access error, reported
|
|
381
|
-
like any other `register()` crash — the design offers graceful
|
|
382
|
-
degradation, it can't make an author take it. Two answers are the same
|
|
383
|
-
everywhere: `net` is false (it isn't a capability, see the table above),
|
|
384
|
-
and in a context you built in-process with `buildModContextFor()` — no
|
|
385
|
-
sandbox attached — everything in the table reads as true, because nothing
|
|
386
|
-
is gating it there.
|
|
387
|
-
|
|
388
|
-
Answers are remembered in `.temporal-fmt-permissions.json`, next to
|
|
389
|
-
`mods/` (like the `config/` directory — it's the host project's data about
|
|
390
|
-
what it has agreed to, not part of the mod). The file is keyed by
|
|
391
|
-
`name@version`: bump the version and you're asked again; keep the version
|
|
392
|
-
and the cached answer applies; delete the file and everything is asked
|
|
393
|
-
again. It's plain JSON, safe to edit by hand:
|
|
394
|
-
|
|
395
|
-
```json
|
|
396
|
-
{
|
|
397
|
-
"data-reader@1.0.0": { "fs:read": true },
|
|
398
|
-
"risky@2.3.0": { "fs:read": true, "fs:write": false }
|
|
399
|
-
}
|
|
92
|
+
temporal-fmt: allow "data-reader" to access fs:read? (y/N)
|
|
400
93
|
```
|
|
401
94
|
|
|
402
|
-
|
|
403
|
-
|
|
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:
|
|
404
99
|
|
|
405
100
|
```
|
|
406
101
|
node scripts/managePermissions.mjs list
|
|
@@ -409,86 +104,15 @@ node scripts/managePermissions.mjs deny data-reader@1.0.0 fs:write
|
|
|
409
104
|
node scripts/managePermissions.mjs reset data-reader@1.0.0
|
|
410
105
|
```
|
|
411
106
|
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
prompts use, so there's one cache, not two that can drift apart. Changes
|
|
416
|
-
apply the next time the mod loads; a process already running it is
|
|
417
|
-
unaffected. A mod with no `version` in its `mod.json` is addressed by its
|
|
418
|
-
bare name (`grant data-reader fs:read`), and `list` prints the exact key
|
|
419
|
-
when in doubt.
|
|
420
|
-
|
|
421
|
-
## Pinning a mod to a `temporal-fmt` version
|
|
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).
|
|
422
110
|
|
|
423
|
-
|
|
424
|
-
(`"0.9.32"`) or a caret range (`"^0.9.0"`, meaning ">=0.9.0, <0.10.0" —
|
|
425
|
-
same meaning npm gives `^` in `package.json`). If the installed
|
|
426
|
-
`temporal-fmt` doesn't satisfy it, the mod fails to load with the range and
|
|
427
|
-
the actual version, before `main.mjs` is ever imported:
|
|
111
|
+
## Giving a mod settings
|
|
428
112
|
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
This exists because nothing else catches the alternative: a mod built
|
|
434
|
-
against one version's override surface (which functions are zero-fanout and
|
|
435
|
-
therefore overridable — see [Overriding functions](#overriding-functions))
|
|
436
|
-
has no way to know if a future release moved a function it depends on, and
|
|
437
|
-
would otherwise fail with whatever confusing error `register()` happens to
|
|
438
|
-
throw, or — worse — silently do nothing if the call it expected to matter
|
|
439
|
-
just no longer has any effect. A declared range turns that into one clear,
|
|
440
|
-
pre-`register()` failure instead.
|
|
441
|
-
|
|
442
|
-
Omitting `temporalFmtVersion` is allowed — the mod loads against whatever
|
|
443
|
-
version is installed, same as before this field existed. Loose `.mjs` mods
|
|
444
|
-
have no manifest to put this in at all, so they can't declare a version
|
|
445
|
-
requirement; that's one real reason to prefer `.tfmod` for anything you
|
|
446
|
-
plan to distribute rather than just run yourself.
|
|
447
|
-
|
|
448
|
-
There's no dependency-resolution logic here, unlike `requires`/`priority` —
|
|
449
|
-
this is a single boolean check (does the host version satisfy the range),
|
|
450
|
-
not something that affects load order.
|
|
451
|
-
|
|
452
|
-
## Mod settings and `config/`
|
|
453
|
-
|
|
454
|
-
A mod can declare user-adjustable settings in `mod.json`'s `config` array,
|
|
455
|
-
and `register()` receives the resolved values as its second argument:
|
|
456
|
-
|
|
457
|
-
```json
|
|
458
|
-
// mod.json
|
|
459
|
-
{
|
|
460
|
-
"name": "en-gb-bank-holidays",
|
|
461
|
-
"main": "main.mjs",
|
|
462
|
-
"config": [
|
|
463
|
-
{ "key": "includeScottish", "type": "boolean", "default": false },
|
|
464
|
-
{ "key": "observedRule", "type": "enum", "default": "nearest-weekday", "choices": ["nearest-weekday", "strict-date"] },
|
|
465
|
-
{ "key": "yearsAhead", "type": "number", "default": 5, "min": 1, "max": 20 }
|
|
466
|
-
]
|
|
467
|
-
}
|
|
468
|
-
```
|
|
469
|
-
|
|
470
|
-
```js
|
|
471
|
-
// main.mjs
|
|
472
|
-
export default {
|
|
473
|
-
register(ctx, config) {
|
|
474
|
-
const years = config.yearsAhead; // 5, unless overridden below
|
|
475
|
-
ctx.createHolidayCalendar(buildHolidays({ scottish: config.includeScottish, years }));
|
|
476
|
-
},
|
|
477
|
-
};
|
|
478
|
-
```
|
|
479
|
-
|
|
480
|
-
Four setting types are supported: `string`, `number` (with optional
|
|
481
|
-
`min`/`max`), `boolean`, and `enum` (a string constrained to `choices`).
|
|
482
|
-
Every entry needs a `key` and a `default` — the default is what
|
|
483
|
-
`register()` gets if the user hasn't overridden that setting, which also
|
|
484
|
-
means a mod with no `config/<name>.json` file on disk at all still runs
|
|
485
|
-
normally, just entirely on defaults.
|
|
486
|
-
|
|
487
|
-
To override a setting, drop a JSON file at `config/<mod-name>.json` —
|
|
488
|
-
**next to `mods/`, not inside it** (so re-downloading or updating the
|
|
489
|
-
`.tfmod` never touches a user's settings, the same reason Forge keeps
|
|
490
|
-
`config/` and `mods/` as siblings rather than bundling settings into the
|
|
491
|
-
jar):
|
|
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:
|
|
492
116
|
|
|
493
117
|
```
|
|
494
118
|
your-project/
|
|
@@ -498,311 +122,16 @@ your-project/
|
|
|
498
122
|
└── en-gb-bank-holidays.json — { "includeScottish": true, "yearsAhead": 10 }
|
|
499
123
|
```
|
|
500
124
|
|
|
501
|
-
Only keys the
|
|
502
|
-
mistake
|
|
503
|
-
|
|
504
|
-
```
|
|
505
|
-
temporal-fmt mods:
|
|
506
|
-
loaded en-gb-bank-holidays@1.0.0 (en-gb-bank-holidays.tfmod)
|
|
507
|
-
failed config/en-gb-bank-holidays.json: en-gb-bank-holidays: config key "yearsAhead" must be <= 20, got 50 (using default)
|
|
508
|
-
failed config/en-gb-bank-holidays.json: en-gb-bank-holidays: unknown config key "includeWelsh" (not declared in this mod's schema)
|
|
509
|
-
```
|
|
510
|
-
|
|
511
|
-
An invalid value for a declared key falls back to that key's default rather
|
|
512
|
-
than failing the whole mod — one typo'd number in a config file shouldn't
|
|
513
|
-
take down a working mod, but it's reported so the mistake doesn't go
|
|
514
|
-
unnoticed either. This is deliberately not JSON Schema: no nesting, no
|
|
515
|
-
`$ref`, no conditional rules — just the handful of primitive shapes an
|
|
516
|
-
actual setting realistically is, kept dependency-free the same way
|
|
517
|
-
`temporalFmtVersion` checking and `.tfmod` extraction are.
|
|
518
|
-
|
|
519
|
-
Loose `.mjs` mods have no manifest to declare a schema in, so
|
|
520
|
-
`register()`'s second argument is always `{}` for them — same as a `.tfmod`
|
|
521
|
-
mod that didn't declare a `config` field at all.
|
|
522
|
-
|
|
523
|
-
## Load order, dependencies, and conflicts
|
|
524
|
-
|
|
525
|
-
By default mods load in filename order — alphabetical, deterministic, but
|
|
526
|
-
not something you'd want to rely on once two mods actually need to run in a
|
|
527
|
-
specific order relative to each other. Two fields on the mod object
|
|
528
|
-
control that directly:
|
|
529
|
-
|
|
530
|
-
- `requires: string[]` — other mods' `name` fields that must load (and
|
|
531
|
-
finish `register()`) before this one. The loader resolves this as a
|
|
532
|
-
dependency graph, not just "sort requires first" — if A requires B and B
|
|
533
|
-
requires nothing, B always loads first regardless of filename.
|
|
534
|
-
- `priority: number` — tiebreak for mods with no dependency relationship
|
|
535
|
-
to each other. Higher loads later. Defaults to `0`.
|
|
536
|
-
|
|
537
|
-
```js
|
|
538
|
-
export default {
|
|
539
|
-
name: 'extended-en-gb-holidays',
|
|
540
|
-
requires: ['en-gb-bank-holidays'],
|
|
541
|
-
priority: 10,
|
|
542
|
-
register(ctx) {
|
|
543
|
-
// runs after en-gb-bank-holidays, and after anything else at a lower priority
|
|
544
|
-
},
|
|
545
|
-
};
|
|
546
|
-
```
|
|
547
|
-
|
|
548
|
-
Two failure modes come out of this, both reported per-mod without blocking
|
|
549
|
-
the rest:
|
|
550
|
-
|
|
551
|
-
- **Missing dependency** — `requires` names a mod that isn't in `mods/`.
|
|
552
|
-
That mod fails to load; whatever it would've registered doesn't happen,
|
|
553
|
-
and other mods that don't depend on it load normally.
|
|
554
|
-
- **Circular dependency** — A requires B requires A (or a longer cycle).
|
|
555
|
-
Every mod in the cycle fails, each reported with what it's still waiting
|
|
556
|
-
on.
|
|
557
|
-
|
|
558
|
-
Registration itself is still last-write-wins, same as calling
|
|
559
|
-
`registerLocale` twice for the same tag outside of mods — that's existing,
|
|
560
|
-
intentional behavior (see [Locales](./README.md#locales)), not something
|
|
561
|
-
mods change. What mods add is *visibility* into it: if two mods register
|
|
562
|
-
the same locale tag, the same relative-time-grammar language, or the same
|
|
563
|
-
custom token name, the load report calls it out as a conflict and says
|
|
564
|
-
which one won:
|
|
565
|
-
|
|
566
|
-
```
|
|
567
|
-
temporal-fmt mods:
|
|
568
|
-
loaded holiday-pack-a (conflict-1.mjs)
|
|
569
|
-
loaded holiday-pack-b (conflict-2.mjs)
|
|
570
|
-
conflict on locale "cv-CV": holiday-pack-a, holiday-pack-b — "holiday-pack-b" wins (loaded last)
|
|
571
|
-
```
|
|
572
|
-
|
|
573
|
-
This is informational, not a failure — both mods still loaded, the last one
|
|
574
|
-
to register just took the key, and now you know it happened instead of
|
|
575
|
-
silently getting whichever mod's filename sorted last. If that's not what
|
|
576
|
-
you want, `priority` is the knob: raise the one that should win, or add a
|
|
577
|
-
`requires` so the loser explicitly runs first and the winner's intent is
|
|
578
|
-
unambiguous in the mod itself, not just in a startup log line.
|
|
579
|
-
|
|
580
|
-
Mod names have to be unique across `mods/` — two files claiming the same
|
|
581
|
-
`name` is ambiguous the moment either one shows up in another mod's
|
|
582
|
-
`requires`, so the second one to load fails with which file already claimed
|
|
583
|
-
that name.
|
|
584
|
-
|
|
585
|
-
## Overriding functions
|
|
586
|
-
|
|
587
|
-
The five registration functions above are additive — they add a locale, a
|
|
588
|
-
holiday set, a token, alongside whatever's already there.
|
|
589
|
-
`ctx.overrideFormat` and `ctx.overrideParse` work differently: they let a
|
|
590
|
-
mod replace the actual `format()`/`parse()` implementation everywhere in
|
|
591
|
-
the library, which is what makes a real bugfix or performance mod possible
|
|
592
|
-
rather than just new data being registered alongside an unfixed bug.
|
|
593
|
-
|
|
594
|
-
```js
|
|
595
|
-
export default {
|
|
596
|
-
name: 'fast-format',
|
|
597
|
-
register(ctx) {
|
|
598
|
-
ctx.overrideFormat((original, value, formatStr, options) => {
|
|
599
|
-
// Handle the one hot-path format string yourself; fall back to the
|
|
600
|
-
// real implementation for everything else.
|
|
601
|
-
if (formatStr === 'yyyy-MM-dd') {
|
|
602
|
-
return `${value.year}-${String(value.month).padStart(2, '0')}-${String(value.day).padStart(2, '0')}`;
|
|
603
|
-
}
|
|
604
|
-
return original(value, formatStr, options);
|
|
605
|
-
});
|
|
606
|
-
},
|
|
607
|
-
};
|
|
608
|
-
```
|
|
609
|
-
|
|
610
|
-
`impl` always receives the real built-in as its first argument
|
|
611
|
-
(`original`), regardless of what else is loaded — call it to keep existing
|
|
612
|
-
behavior for cases you're not trying to change, or ignore it to replace the
|
|
613
|
-
behavior outright. The override applies consistently everywhere in the
|
|
614
|
-
library, not just to whoever imports the function from the package root —
|
|
615
|
-
`formatRange()`'s internal use of `format()`, for instance, sees it too.
|
|
616
|
-
Remove the mod and restart, and it's back to the unmodified built-in;
|
|
617
|
-
nothing about this touches the source file on disk.
|
|
618
|
-
|
|
619
|
-
**Only one mod may hold each override point.** A second override call for
|
|
620
|
-
the same function — from any mod, even one that `requires` the first —
|
|
621
|
-
fails immediately with which mod already owns it:
|
|
622
|
-
|
|
623
|
-
```
|
|
624
|
-
temporal-fmt mods:
|
|
625
|
-
loaded override-1 (a-override1.mjs)
|
|
626
|
-
failed b-override2.mjs: temporal-fmt: "format" is already overridden by mod "override-1" — mod "override-2" can't also override it. [...]
|
|
627
|
-
```
|
|
628
|
-
|
|
629
|
-
This is a hard failure, not last-write-wins like the registration
|
|
630
|
-
functions — two mods silently fighting over the same function's behavior
|
|
631
|
-
is a correctness bug in whatever depends on this library, not a cosmetic
|
|
632
|
-
surprise. There's no mechanism for two separate mod files to layer through
|
|
633
|
-
the same override point in sequence; if two mods both need to change a
|
|
634
|
-
function's behavior, one has to incorporate the other's fix directly rather
|
|
635
|
-
than composing through the override twice.
|
|
636
|
-
|
|
637
|
-
**How an override runs under the sandbox.** A closure can't cross a process
|
|
638
|
-
boundary, so a mod that installs one keeps its subprocess alive, and every
|
|
639
|
-
`format()`/`parse()` call in the host forwards to it and waits for the
|
|
640
|
-
answer — synchronously, because those are synchronous APIs and their
|
|
641
|
-
result has to come back inside the caller's stack frame. What that costs
|
|
642
|
-
and what's done about it:
|
|
643
|
-
|
|
644
|
-
- Each overridden call is one round trip to the subprocess — a couple
|
|
645
|
-
of milliseconds, not the nanoseconds of an in-process function call, plus
|
|
646
|
-
a few more on Windows where the channel rides files (see
|
|
647
|
-
[The sandbox](#the-sandbox)). An override in a hot loop is *slower than
|
|
648
|
-
no override at all*, never mind faster. If your mod's whole point is
|
|
649
|
-
performance, it has to save more than the bridge costs.
|
|
650
|
-
- The loader learns, per format string, whether your impl just forwards to
|
|
651
|
-
the built-in. Format strings your mod passes through stop paying the
|
|
652
|
-
round trip entirely after the first call; only the strings you actually
|
|
653
|
-
change keep crossing the process boundary. `formatRange()`'s two
|
|
654
|
-
endpoints batch into a single trip.
|
|
655
|
-
- If the subprocess stops answering — the mod's impl hangs, crashes, or
|
|
656
|
-
hits a permission violation mid-call — the host stops waiting after 5
|
|
657
|
-
seconds (shown in the load report), kills the subprocess, warns on
|
|
658
|
-
stderr, and falls back to the built-in behavior for the rest of the
|
|
659
|
-
process. One hung mod doesn't take your formatting down with it.
|
|
660
|
-
- The value your impl receives is rebuilt from its fields on the other
|
|
661
|
-
side. For real Temporal inputs it's rehydrated as a Temporal instance of
|
|
662
|
-
the same type; if your impl mutates the value (don't), the caller won't
|
|
663
|
-
see it — nothing is shared across the boundary. Values and results that
|
|
664
|
-
can't survive JSON serialization degrade the way JSON does: functions
|
|
665
|
-
vanish, and a `bigint` throws.
|
|
666
|
-
|
|
667
|
-
**Which functions are overridable.** `format`, `formatToParts`, and `parse`
|
|
668
|
-
always were. Beyond those, any function in this library that nothing *else*
|
|
669
|
-
in the library calls internally is also overridable — if a function has
|
|
670
|
-
zero internal call sites, there's no risk of some other module holding a
|
|
671
|
-
stale direct reference that a mod's fix would silently fail to reach, so it
|
|
672
|
-
gets the same `overrideXxx()` treatment. As of this version, that's:
|
|
673
|
-
|
|
674
|
-
`compileFormat`, `compileParser`, `parseRelative`, `explainFormat`,
|
|
675
|
-
`tokenizeFormat`, `listTokens`, `tokenInfo`, `isValidFormat`,
|
|
676
|
-
`validateFormat`, `fieldForToken`, `monthsInYear`, `isLeapYear`,
|
|
677
|
-
`isLeapMonth`, `weekOfYear`, `weekYear`, `getMonth`, `getWeekday`,
|
|
678
|
-
`isEqual`, `isBefore`, `isAfter`, `clamp`, `isBetween`, `isToday`,
|
|
679
|
-
`isTomorrow`, `isYesterday`, `isSameDay`, `isSameWeek`, `isSameMonth`,
|
|
680
|
-
`isSameQuarter`, `isSameYear`, `isWeekday`, `floor`, `ceil`, `truncate`,
|
|
681
|
-
`parseRFC3339`, `formatRFC3339`, `parseRFC2822`, `parseHTTPDate`,
|
|
682
|
-
`fromUnixMicroseconds`, `fromUnixNanoseconds`, `toUnixSeconds`,
|
|
683
|
-
`toUnixMilliseconds`, `toUnixMicroseconds`, `toUnixNanoseconds`,
|
|
684
|
-
`parseSQL`, `formatSQL`, `formatDurationToParts`, `parseDuration`,
|
|
685
|
-
`parseISODuration`, `formatISODuration`, `balanceDuration`,
|
|
686
|
-
`compareDuration`, `subtractDuration`, `getLocale`, `hasLocale`,
|
|
687
|
-
`createConfig`, `mergeWithConfig`, `listRegisteredGrammars`, `interval`,
|
|
688
|
-
`overlaps`, `intersection`, `union`, `mergeIntervals`, `formatRangeToParts`,
|
|
689
|
-
`between`, `parseRRule`, `formatRRule`, `createBusinessCalendar`,
|
|
690
|
-
`subtractBusinessDays`, `nextHoliday`, `previousHoliday`, `resolveZoned`,
|
|
691
|
-
`getNextTransition`, `getPreviousTransition`, `possibleInstantsFor`,
|
|
692
|
-
`getAutocompleteData`, `getHoverDocs`, `getInlineDiagnostics`,
|
|
693
|
-
`previewFormat`, `getDocUrl`, `translateDateFnsFormatString`.
|
|
694
|
-
|
|
695
|
-
Each follows the `ctx.overrideXxx((original, ...args) => ...)` shape shown
|
|
696
|
-
above for `overrideFormat`. Functions *not* in this list — `round`,
|
|
697
|
-
`subtract`, `difference`, `formatDistance`, and others that other parts of
|
|
698
|
-
this library call directly — aren't overridable this way: something else
|
|
699
|
-
in the codebase holds its own direct reference to them, so a mod's
|
|
700
|
-
override would silently miss those internal callers, which is worse than
|
|
701
|
-
not offering the override at all. A function moves onto this list only
|
|
702
|
-
when an audit confirms nothing internal still calls it directly. If you
|
|
703
|
-
need to change one of those, that's a real feature request for making it
|
|
704
|
-
internally indirect first, not something `overrideFormat`-style code can
|
|
705
|
-
paper over.
|
|
706
|
-
|
|
707
|
-
**Custom token handlers are the one thing that runs host-side.** A
|
|
708
|
-
`createFormatter()` call inside register() is setup-time: the token table
|
|
709
|
-
(name, field, and each handler's source text) is shipped back to the host
|
|
710
|
-
and the `Formatter` is rebuilt in-process, because a formatter is a hot
|
|
711
|
-
path and every token lookup can't pay a subprocess round trip. The loader
|
|
712
|
-
verifies each handler is self-contained — it revives the handler from
|
|
713
|
-
source and compares its output against the original's before accepting it,
|
|
714
|
-
and refuses the mod with a clear reason if the revival doesn't behave the
|
|
715
|
-
same. So: a token handler must not close over anything outside its own
|
|
716
|
-
body, and its code runs in the host process at format() time. That last
|
|
717
|
-
part is the real trade — treat formatter-token mods as trusted code, same
|
|
718
|
-
as you'd treat anything you run in-process.
|
|
719
|
-
|
|
720
|
-
## If you're writing the mod in TypeScript
|
|
721
|
-
|
|
722
|
-
Compile it and rename the output before it goes in `mods/` — the loader
|
|
723
|
-
only accepts `.mjs`. It won't run a TS file for you, and it won't skip one
|
|
724
|
-
quietly either: a `.ts` file sitting in `mods/` shows up in the load report
|
|
725
|
-
as a failure with the exact compile command to run, because a mod that
|
|
726
|
-
silently never loads is worse than one that fails loudly.
|
|
727
|
-
|
|
728
|
-
```sh
|
|
729
|
-
tsc en-gb-bank-holidays.ts --module esnext --target esnext --outDir mods
|
|
730
|
-
mv mods/en-gb-bank-holidays.js mods/en-gb-bank-holidays.mjs
|
|
731
|
-
```
|
|
732
|
-
|
|
733
|
-
If you're importing `ModContext` or `Mod` for the types while you write it,
|
|
734
|
-
both are exported from `temporal-fmt` itself:
|
|
735
|
-
|
|
736
|
-
```ts
|
|
737
|
-
import type { Mod, ModContext } from 'temporal-fmt';
|
|
738
|
-
|
|
739
|
-
const mod: Mod = {
|
|
740
|
-
name: 'en-gb-bank-holidays',
|
|
741
|
-
register(ctx: ModContext) {
|
|
742
|
-
ctx.createHolidayCalendar([{ month: 1, day: 1, name: "New Year's Day" }]);
|
|
743
|
-
},
|
|
744
|
-
};
|
|
745
|
-
|
|
746
|
-
export default mod;
|
|
747
|
-
```
|
|
748
|
-
|
|
749
|
-
## What happens when a mod is broken
|
|
750
|
-
|
|
751
|
-
Each mod loads independently — one throwing doesn't stop the rest from
|
|
752
|
-
loading, and it doesn't stop the CLI command you actually ran. Every
|
|
753
|
-
failure mode ends up as one line in the report:
|
|
754
|
-
|
|
755
|
-
- Wrong file extension (`.ts`, `.js`, anything but `.mjs`) — reported with
|
|
756
|
-
the compile-and-rename instructions above.
|
|
757
|
-
- Default export isn't shaped right (missing `name`, missing `register`,
|
|
758
|
-
`register` isn't a function, or `requires`/`priority` are the wrong
|
|
759
|
-
type) — reported with what was expected.
|
|
760
|
-
- The file fails to import (a syntax error, a bad import path inside the
|
|
761
|
-
mod) — reported with the underlying error message.
|
|
762
|
-
- Two mods claim the same `name` — reported against whichever file loaded
|
|
763
|
-
second.
|
|
764
|
-
- A `requires` entry names a mod that isn't present, or is part of a
|
|
765
|
-
dependency cycle — see [Load order, dependencies, and
|
|
766
|
-
conflicts](#load-order-dependencies-and-conflicts).
|
|
767
|
-
- `register()` throws — reported with the thrown message, same as any other
|
|
768
|
-
registration call in this library (see [Typed
|
|
769
|
-
errors](./README.md#typed-errors) for what `registerLocale`/
|
|
770
|
-
`createHolidayCalendar` themselves throw on bad input).
|
|
771
|
-
- A required permission was denied — `denied required permission: <caps>`
|
|
772
|
-
plus how to change the answer, before any of the mod's code runs (see
|
|
773
|
-
[Permissions](#permissions)). An *optional* permission being denied
|
|
774
|
-
isn't a failure: the mod runs with what it got and the report says
|
|
775
|
-
`downgraded`.
|
|
776
|
-
- `register()` touches a capability that wasn't granted — the permission
|
|
777
|
-
model's access error, plus (for a loose `.mjs` mod) the reminder that it
|
|
778
|
-
can't request any. A mod that checks `ctx.hasPermission()` first can
|
|
779
|
-
skip this fate; one that doesn't, hits the wall.
|
|
780
|
-
- `register()` doesn't finish within 10 seconds, a runtime override
|
|
781
|
-
stops answering within 5, or the subprocess grows past the 512MB
|
|
782
|
-
resident-memory ceiling — the subprocess is killed, that mod fails
|
|
783
|
-
(or, at runtime, falls back to the built-in with a stderr warning), and
|
|
784
|
-
the rest of the load pass is unaffected. The memory watch is RSS
|
|
785
|
-
polled from the parent, so `ArrayBuffer` bytes count and
|
|
786
|
-
`--max-old-space-size` set in the process tree doesn't neuter it.
|
|
787
|
-
|
|
788
|
-
None of these bring down the CLI. A `mods/` folder that doesn't exist is
|
|
789
|
-
the common case, not a failure — most runs won't have one, and the loader
|
|
790
|
-
stays silent about it rather than printing "no mods found" noise on every
|
|
791
|
-
command.
|
|
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.
|
|
792
127
|
|
|
793
128
|
## Using mods outside the CLI
|
|
794
129
|
|
|
795
|
-
`loadMods()` lives in `scripts/loadMods.mjs
|
|
796
|
-
package
|
|
797
|
-
|
|
798
|
-
the config resolver (`scripts/modConfig.mjs`), and the permission-cache
|
|
799
|
-
editor (`scripts/managePermissions.mjs`). If you're embedding
|
|
800
|
-
`temporal-fmt` in your own app rather than using the CLI, load it at your
|
|
801
|
-
own startup and the same sandboxing, prompting, and reporting apply:
|
|
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):
|
|
802
133
|
|
|
803
134
|
```js
|
|
804
|
-
// A path into node_modules, not a bare specifier — the loader is
|
|
805
|
-
// Node-only ESM and deliberately isn't in package.json "exports".
|
|
806
135
|
import { loadMods, formatModLoadReport } from './node_modules/temporal-fmt/scripts/loadMods.mjs';
|
|
807
136
|
|
|
808
137
|
const report = await loadMods(); // defaults to ./mods
|
|
@@ -811,22 +140,9 @@ if (report.loaded.length > 0 || report.downgraded.length > 0 || report.failed.le
|
|
|
811
140
|
}
|
|
812
141
|
```
|
|
813
142
|
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
A mod that installs a runtime override keeps its subprocess alive for as
|
|
822
|
-
long as your process might call `format()`/`parse()` — but it won't keep
|
|
823
|
-
your process alive: the loader drops its event-loop references after
|
|
824
|
-
loading, and the subprocess shuts itself down when your process exits. If
|
|
825
|
-
you want deterministic teardown before then (a server that hot-reloads
|
|
826
|
-
mods, say), `stopModSubprocesses()` from `scripts/modSandbox.mjs` SIGTERMs
|
|
827
|
-
every live one.
|
|
828
|
-
|
|
829
|
-
Mod support (loose `.mjs` mods, `.tfmod` archives, and everything in this
|
|
830
|
-
document) requires `temporal-fmt` 0.9.4 or later — that's the version it
|
|
831
|
-
landed in. Subprocess sandboxing arrived after that; the load report tells
|
|
832
|
-
you when you're on a Node version where it's still experimental.
|
|
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.
|