temporal-fmt 0.7.9 → 0.7.96

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/README.md CHANGED
@@ -216,10 +216,16 @@ sitting in your output waiting to confuse someone in three weeks.
216
216
 
217
217
  ## Dev notes
218
218
 
219
- `tsconfig.json` sets `ignoreDeprecations: "6.0"` to work around a tsup bug
220
- ([tsup#1388](https://github.com/egoist/tsup/issues/1388)/[#1389](https://github.com/egoist/tsup/issues/1389)). tsup's dts build step quietly injects a deprecated
221
- `baseUrl`, and TypeScript 6+ hard-errors on it. Workaround, not a fix — drop
222
- it the moment tsup ships a real one upstream.
219
+ Building requires TypeScript 7.0.2+ but `.d.ts` generation runs as a separate
220
+ `tsc` pass, not through tsup. tsup's dts step bundles types via
221
+ `rollup-plugin-dts`, which calls into TypeScript's compiler API the API
222
+ isn't stable yet on 7.x ([targeted for
223
+ 7.1](https://devblogs.microsoft.com/typescript/announcing-typescript-7-0/)),
224
+ so it crashes on 7.0.2. `tsup.config.ts` sets `dts: false` and `build` runs
225
+ `tsup && tsc --declaration --emitDeclarationOnly` instead. One side effect:
226
+ `dist/` now has one `.d.ts` per source file instead of a single rolled-up
227
+ `index.d.ts` — same exported API, different file layout. Revert to `dts:
228
+ true` once tsup/rollup-plugin-dts catch up.
223
229
 
224
230
  Tests pull from `temporal-polyfill/full`, not the slim `temporal-polyfill` —
225
231
  the Hebrew-calendar test needs the full build's calendar data, and the slim
@@ -230,6 +236,19 @@ through `Intl.DateTimeFormat` directly; on the polyfill, it falls back to
230
236
  `parse.test.js` configures Temporal via `setTemporal()` rather than mutating
231
237
  `globalThis.Temporal` directly.
232
238
 
239
+ Run `npm run test:all`, not just `npm test`. `npm test` only runs the
240
+ `node:test` suite in `test/*.test.js` — hand-picked, fuzz, adversarial, and
241
+ perf cases that exercise the public API end to end. It doesn't touch
242
+ `vitest/`, which unit-tests internals like `enumerateValidSplits()`
243
+ directly. That function resolves ambiguous glued numeric runs (does `"112"`
244
+ against `['M', 'd']` mean month 1/day 12, or month 11/day 2?), and a bug
245
+ in its edge cases — an empty token list, a range-boundary off-by-one — can
246
+ easily dodge every `parse()` example in the main suite without ever being
247
+ the specific input one of them happens to use. `test:all` also runs the
248
+ type tests (`test:types`), so it's the only single command that actually
249
+ covers everything. CI runs `test:all` for this reason; running plain `npm
250
+ test` locally will pass even with a broken `vitest/` suite.
251
+
233
252
  ## License
234
253
 
235
- MIT
254
+ MIT
@@ -0,0 +1 @@
1
+ export declare const MAX_FORMAT_LENGTH = 1000;
@@ -0,0 +1,13 @@
1
+ import { type TemporalLike, type FormatOptions } from './tokens.js';
2
+ /**
3
+ * Format a Temporal.PlainDate, PlainTime, PlainDateTime, or ZonedDateTime
4
+ * using a date-fns-style token string.
5
+ *
6
+ * @example
7
+ * format(Temporal.Now.plainDateISO(), 'yyyy-MM-dd') // "2026-08-04"
8
+ * format(zdt, "MMM d, yyyy 'at' h:mm a") // "Aug 4, 2026 at 3:45 PM"
9
+ * format(zdt, 'MMMM d, yyyy', { locale: 'fr-FR' }) // "août 4, 2026"
10
+ *
11
+ * Throws on a token the input type doesn't support (e.g. 'HH' on a PlainDate).
12
+ */
13
+ export declare function format(temporal: TemporalLike, formatStr: string, options?: FormatOptions): string;
package/dist/index.d.ts CHANGED
@@ -1,83 +1,5 @@
1
- interface TemporalLike {
2
- year?: number;
3
- month?: number;
4
- day?: number;
5
- hour?: number;
6
- minute?: number;
7
- second?: number;
8
- millisecond?: number;
9
- timeZoneId?: string;
10
- dayOfWeek?: number;
11
- calendarId?: string;
12
- toInstant?: () => unknown;
13
- toLocaleString?: (locale: string, options: Intl.DateTimeFormatOptions) => string;
14
- }
15
- interface FormatOptions {
16
- /** BCP 47 locale tag, e.g. 'en-US', 'fr-FR', 'ar-EG'. Defaults to 'en-US'. */
17
- locale?: string;
18
- }
19
-
20
- /**
21
- * Format a Temporal.PlainDate, PlainTime, PlainDateTime, or ZonedDateTime
22
- * using a date-fns-style token string.
23
- *
24
- * @example
25
- * format(Temporal.Now.plainDateISO(), 'yyyy-MM-dd') // "2026-08-04"
26
- * format(zdt, "MMM d, yyyy 'at' h:mm a") // "Aug 4, 2026 at 3:45 PM"
27
- * format(zdt, 'MMMM d, yyyy', { locale: 'fr-FR' }) // "août 4, 2026"
28
- *
29
- * Throws on a token the input type doesn't support (e.g. 'HH' on a PlainDate).
30
- */
31
- declare function format(temporal: TemporalLike, formatStr: string, options?: FormatOptions): string;
32
-
33
- /**
34
- * Parses `input` against `formatStr` and builds the real Temporal value it
35
- * describes: a `Temporal.PlainDate`, `PlainTime`, `PlainDateTime`, or
36
- * `ZonedDateTime` depending on which tokens are present.
37
- *
38
- * Returns `unknown` — this package has no ambient `Temporal` types to return
39
- * a real one against.
40
- *
41
- * `options.locale` picks the calendar the result is built in. Pass a locale
42
- * tag with a `-u-ca-` extension (e.g. `'en-u-ca-hebrew'`) to parse into a
43
- * non-Gregorian calendar.
44
- *
45
- * @throws if `input` doesn't match `formatStr`'s shape at all
46
- * @throws if it matches the shape but describes an impossible date (e.g. Feb
47
- * 30) or self-contradictory data (e.g. a weekday name that doesn't match the
48
- * actual date)
49
- *
50
- * @example
51
- * parse('yyyy-MM-dd HH:mm', '2026-08-04 15:45') // Temporal.PlainDateTime
52
- * parse('yyyy-MM', '2026-08-04T15:45:30') // throws — shape doesn't match
53
- * parse('yyyy-MM-dd', '2026-02-30') // throws — not a real date
54
- */
55
- declare function parse(formatStr: string, input: string, options?: FormatOptions): unknown | undefined;
56
-
57
- interface TemporalFactory {
58
- from(fields: Record<string, number | string | undefined>, options?: {
59
- overflow?: 'constrain' | 'reject';
60
- }): unknown;
61
- }
62
- interface TemporalNamespace {
63
- PlainDate: TemporalFactory;
64
- PlainTime: TemporalFactory;
65
- PlainDateTime: TemporalFactory;
66
- ZonedDateTime: TemporalFactory;
67
- }
68
- /**
69
- * Explicitly hand temporal-fmt the Temporal implementation to use, instead
70
- * of relying on a global `Temporal`. Call this once, before your first
71
- * `format()`/`parse()`
72
- *
73
- * Call with no argument (or `undefined`) to clear the override and fall
74
- * back to `globalThis.Temporal` again.
75
- *
76
- * @example
77
- * import { Temporal } from 'temporal-polyfill';
78
- * import { setTemporal } from 'temporal-fmt';
79
- * setTemporal(Temporal);
80
- */
81
- declare function setTemporal(temporal?: TemporalNamespace): void;
82
-
83
- export { type FormatOptions, type TemporalLike, type TemporalNamespace, format, parse, setTemporal };
1
+ export { format } from './format.js';
2
+ export { parse } from './parse.js';
3
+ export { setTemporal } from './temporalProvider.js';
4
+ export type { TemporalLike, FormatOptions } from './tokens.js';
5
+ export type { TemporalNamespace } from './temporalProvider.js';
@@ -0,0 +1,8 @@
1
+ export interface LocaleVocab {
2
+ monthLong: string[];
3
+ monthShort: string[];
4
+ weekdayLong: string[];
5
+ weekdayShort: string[];
6
+ dayPeriod: string[];
7
+ }
8
+ export declare function getLocaleVocab(locale: string): LocaleVocab;
@@ -0,0 +1,24 @@
1
+ import { type FormatOptions } from './tokens.js';
2
+ /**
3
+ * Parses `input` against `formatStr` and builds the real Temporal value it
4
+ * describes: a `Temporal.PlainDate`, `PlainTime`, `PlainDateTime`, or
5
+ * `ZonedDateTime` depending on which tokens are present.
6
+ *
7
+ * Returns `unknown` — this package has no ambient `Temporal` types to return
8
+ * a real one against.
9
+ *
10
+ * `options.locale` picks the calendar the result is built in. Pass a locale
11
+ * tag with a `-u-ca-` extension (e.g. `'en-u-ca-hebrew'`) to parse into a
12
+ * non-Gregorian calendar.
13
+ *
14
+ * @throws if `input` doesn't match `formatStr`'s shape at all
15
+ * @throws if it matches the shape but describes an impossible date (e.g. Feb
16
+ * 30) or self-contradictory data (e.g. a weekday name that doesn't match the
17
+ * actual date)
18
+ *
19
+ * @example
20
+ * parse('yyyy-MM-dd HH:mm', '2026-08-04 15:45') // Temporal.PlainDateTime
21
+ * parse('yyyy-MM', '2026-08-04T15:45:30') // throws — shape doesn't match
22
+ * parse('yyyy-MM-dd', '2026-02-30') // throws — not a real date
23
+ */
24
+ export declare function parse(formatStr: string, input: string, options?: FormatOptions): unknown | undefined;
@@ -0,0 +1,19 @@
1
+ import type { Piece } from './tokenize.js';
2
+ export interface CapturingPattern {
3
+ regex: RegExp;
4
+ groups: Array<{
5
+ name: string;
6
+ token: string;
7
+ }>;
8
+ ambiguousRuns: Array<{
9
+ groupNames: string[];
10
+ tokens: string[];
11
+ }>;
12
+ }
13
+ /**
14
+ * Same walk as buildPatternSource() in pattern.ts, but each token piece
15
+ * gets its own named capture group (positionally named so the same token,
16
+ * e.g. "yyyy", could in theory appear twice) so a caller can pull the
17
+ * matched substring for each token back out after a successful match.
18
+ */
19
+ export declare function buildCapturingPattern(pieces: Piece[], locale: string): CapturingPattern;
@@ -0,0 +1,27 @@
1
+ export declare function tokenFragment(token: string, locale: string): string;
2
+ export declare const UNPADDED_NUMERIC_TOKENS: Set<string>;
3
+ export declare const UNPADDED_NUMERIC_RANGES: Record<string, Array<{
4
+ digits: 1 | 2;
5
+ min: number;
6
+ max: number;
7
+ }>>;
8
+ /**
9
+ * Given the literal digit string a run of N adjacent unpadded-numeric
10
+ * tokens matched as a whole (e.g. "112" for a 2-token run), enumerates
11
+ * every way to split it into N pieces (one per token, each piece 1-2
12
+ * digits per that token's own width rule) and returns every split where
13
+ * every piece is independently valid for its token. Length 0 means the
14
+ * run's regex match shouldn't have been possible in the first place
15
+ * (shouldn't happen — the caller only invokes this after the whole
16
+ * pattern already matched, meaning at least one split exists: the one the
17
+ * regex actually took). Length 1 means the reading is unambiguous.
18
+ * Length 2+ means true ambiguity — the caller should throw rather than
19
+ * pick one.
20
+ *
21
+ * Recursive over token count rather than hardcoded to 2, so a 3+ token
22
+ * unseparated run (e.g. "Hms") is covered by the same logic without a
23
+ * special case — those are rarer in practice but not impossible, and a
24
+ * partial fix that only covered pairs would leave the identical bug for
25
+ * anyone writing a 3-token glued run.
26
+ */
27
+ export declare function enumerateValidSplits(digits: string, tokens: string[]): number[][];
@@ -0,0 +1,27 @@
1
+ interface TemporalFactory {
2
+ from(fields: Record<string, number | string | undefined>, options?: {
3
+ overflow?: 'constrain' | 'reject';
4
+ }): unknown;
5
+ }
6
+ export interface TemporalNamespace {
7
+ PlainDate: TemporalFactory;
8
+ PlainTime: TemporalFactory;
9
+ PlainDateTime: TemporalFactory;
10
+ ZonedDateTime: TemporalFactory;
11
+ }
12
+ /**
13
+ * Explicitly hand temporal-fmt the Temporal implementation to use, instead
14
+ * of relying on a global `Temporal`. Call this once, before your first
15
+ * `format()`/`parse()`
16
+ *
17
+ * Call with no argument (or `undefined`) to clear the override and fall
18
+ * back to `globalThis.Temporal` again.
19
+ *
20
+ * @example
21
+ * import { Temporal } from 'temporal-polyfill';
22
+ * import { setTemporal } from 'temporal-fmt';
23
+ * setTemporal(Temporal);
24
+ */
25
+ export declare function setTemporal(temporal?: TemporalNamespace): void;
26
+ export declare function getTemporal(): TemporalNamespace;
27
+ export {};
@@ -0,0 +1,14 @@
1
+ export type Piece = {
2
+ kind: 'token';
3
+ value: string;
4
+ } | {
5
+ kind: 'literal';
6
+ value: string;
7
+ };
8
+ /**
9
+ * Splits a format string like `"yyyy-MM-dd 'at' HH:mm"` into token/literal
10
+ * pieces. Text in single quotes is always literal (e.g. write 'rd' in
11
+ * "3rd" so it's not read as the day token). A doubled quote ('') means a
12
+ * literal quote character, both inside a quoted span and standalone.
13
+ */
14
+ export declare function tokenize(format: string): Piece[];
@@ -0,0 +1,23 @@
1
+ export declare function pad(n: number, len: number): string;
2
+ export interface TemporalLike {
3
+ year?: number;
4
+ month?: number;
5
+ day?: number;
6
+ hour?: number;
7
+ minute?: number;
8
+ second?: number;
9
+ millisecond?: number;
10
+ timeZoneId?: string;
11
+ dayOfWeek?: number;
12
+ calendarId?: string;
13
+ toInstant?: () => unknown;
14
+ toLocaleString?: (locale: string, options: Intl.DateTimeFormatOptions) => string;
15
+ }
16
+ export interface FormatOptions {
17
+ /** BCP 47 locale tag, e.g. 'en-US', 'fr-FR', 'ar-EG'. Defaults to 'en-US'. */
18
+ locale?: string;
19
+ }
20
+ export declare const DEFAULT_LOCALE = "en-US";
21
+ type TokenHandler = (t: TemporalLike, locale: string) => string;
22
+ export declare const TOKENS: Array<[string, TokenHandler, keyof TemporalLike]>;
23
+ export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "temporal-fmt",
3
- "version": "0.7.9",
3
+ "version": "0.7.96",
4
4
  "description": "Format Temporal.PlainDate/PlainDateTime/PlainTime/ZonedDateTime objects using date-fns-style token strings.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",
@@ -18,9 +18,13 @@
18
18
  ],
19
19
  "sideEffects": false,
20
20
  "scripts": {
21
- "build": "tsup",
21
+ "build": "tsup && tsc --declaration --emitDeclarationOnly",
22
22
  "dev": "tsup --watch",
23
23
  "test": "node --test test/*.test.js",
24
+ "test:unit": "vitest run",
25
+ "test:unit:watch": "vitest",
26
+ "test:types": "vitest run --typecheck",
27
+ "test:all": "npm run build && npm test && npm run test:unit && npm run test:types",
24
28
  "prepublishOnly": "npm run build && npm test"
25
29
  },
26
30
  "keywords": [
@@ -44,9 +48,11 @@
44
48
  },
45
49
  "homepage": "https://github.com/DirazCoder/temporal-fmt#readme",
46
50
  "devDependencies": {
47
- "temporal-polyfill": "^1.0.3",
51
+ "@typescript/native-preview": "^7.0.0-dev.20260707.2",
52
+ "temporal-polyfill": "^1.0.4",
48
53
  "tsup": "^8.5.1",
49
- "typescript": "^6.0.3"
54
+ "typescript": "^7.0.2",
55
+ "vitest": "^4.1.10"
50
56
  },
51
57
  "overrides": {
52
58
  "esbuild": "0.28.1"
@@ -57,4 +63,4 @@
57
63
  "allowScripts": {
58
64
  "esbuild@0.28.1": true
59
65
  }
60
- }
66
+ }
package/dist/index.d.cts DELETED
@@ -1,83 +0,0 @@
1
- interface TemporalLike {
2
- year?: number;
3
- month?: number;
4
- day?: number;
5
- hour?: number;
6
- minute?: number;
7
- second?: number;
8
- millisecond?: number;
9
- timeZoneId?: string;
10
- dayOfWeek?: number;
11
- calendarId?: string;
12
- toInstant?: () => unknown;
13
- toLocaleString?: (locale: string, options: Intl.DateTimeFormatOptions) => string;
14
- }
15
- interface FormatOptions {
16
- /** BCP 47 locale tag, e.g. 'en-US', 'fr-FR', 'ar-EG'. Defaults to 'en-US'. */
17
- locale?: string;
18
- }
19
-
20
- /**
21
- * Format a Temporal.PlainDate, PlainTime, PlainDateTime, or ZonedDateTime
22
- * using a date-fns-style token string.
23
- *
24
- * @example
25
- * format(Temporal.Now.plainDateISO(), 'yyyy-MM-dd') // "2026-08-04"
26
- * format(zdt, "MMM d, yyyy 'at' h:mm a") // "Aug 4, 2026 at 3:45 PM"
27
- * format(zdt, 'MMMM d, yyyy', { locale: 'fr-FR' }) // "août 4, 2026"
28
- *
29
- * Throws on a token the input type doesn't support (e.g. 'HH' on a PlainDate).
30
- */
31
- declare function format(temporal: TemporalLike, formatStr: string, options?: FormatOptions): string;
32
-
33
- /**
34
- * Parses `input` against `formatStr` and builds the real Temporal value it
35
- * describes: a `Temporal.PlainDate`, `PlainTime`, `PlainDateTime`, or
36
- * `ZonedDateTime` depending on which tokens are present.
37
- *
38
- * Returns `unknown` — this package has no ambient `Temporal` types to return
39
- * a real one against.
40
- *
41
- * `options.locale` picks the calendar the result is built in. Pass a locale
42
- * tag with a `-u-ca-` extension (e.g. `'en-u-ca-hebrew'`) to parse into a
43
- * non-Gregorian calendar.
44
- *
45
- * @throws if `input` doesn't match `formatStr`'s shape at all
46
- * @throws if it matches the shape but describes an impossible date (e.g. Feb
47
- * 30) or self-contradictory data (e.g. a weekday name that doesn't match the
48
- * actual date)
49
- *
50
- * @example
51
- * parse('yyyy-MM-dd HH:mm', '2026-08-04 15:45') // Temporal.PlainDateTime
52
- * parse('yyyy-MM', '2026-08-04T15:45:30') // throws — shape doesn't match
53
- * parse('yyyy-MM-dd', '2026-02-30') // throws — not a real date
54
- */
55
- declare function parse(formatStr: string, input: string, options?: FormatOptions): unknown | undefined;
56
-
57
- interface TemporalFactory {
58
- from(fields: Record<string, number | string | undefined>, options?: {
59
- overflow?: 'constrain' | 'reject';
60
- }): unknown;
61
- }
62
- interface TemporalNamespace {
63
- PlainDate: TemporalFactory;
64
- PlainTime: TemporalFactory;
65
- PlainDateTime: TemporalFactory;
66
- ZonedDateTime: TemporalFactory;
67
- }
68
- /**
69
- * Explicitly hand temporal-fmt the Temporal implementation to use, instead
70
- * of relying on a global `Temporal`. Call this once, before your first
71
- * `format()`/`parse()`
72
- *
73
- * Call with no argument (or `undefined`) to clear the override and fall
74
- * back to `globalThis.Temporal` again.
75
- *
76
- * @example
77
- * import { Temporal } from 'temporal-polyfill';
78
- * import { setTemporal } from 'temporal-fmt';
79
- * setTemporal(Temporal);
80
- */
81
- declare function setTemporal(temporal?: TemporalNamespace): void;
82
-
83
- export { type FormatOptions, type TemporalLike, type TemporalNamespace, format, parse, setTemporal };