temporal-fmt 0.8.97 → 0.8.98

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
@@ -834,7 +834,7 @@ A handful of functions exist specifically to feed editor tooling — autocomplet
834
834
 
835
835
  ## CLI
836
836
 
837
- The CLI ships in this package (`scripts/cli.mjs`) and reads/writes stdin/stdout. Run it via `npm run cli` inside a checkout of this repo, or `node scripts/cli.mjs` directly:
837
+ The CLI ships in this package (`scripts/cli.mjs`) and reads/writes stdin/stdout. Run it via `npm run cli` inside a checkout of this repo, or `node scripts/cli.mjs` directly. Called with a subcommand it runs once and exits, same as any Unix tool — fine for scripts and CI:
838
838
 
839
839
  ```sh
840
840
  temporal-fmt format "2026-08-04T15:45:30" "yyyy-MM-dd HH:mm:ss"
@@ -852,7 +852,23 @@ temporal-fmt translate dayjs "YYYY-MM-DD HH:mm:ss"
852
852
  | `validate <format-string>` | Prints `valid` or `invalid`. |
853
853
  | `translate <source-lib> <format-string>` | Translates a Day.js or date-fns format string to `temporal-fmt` tokens. |
854
854
 
855
- The `translate` subcommand imports a separate `temporal-fmt-codemod` package at runtime it isn't bundled in this repo, so `translate` will fail with a module-not-found error unless that package is installed and resolvable. Every other subcommand works standalone.
855
+ `translate` is implemented in-repo (`src/codemod.ts`) against the same token tables the IDE tooling data uses — no external package, no runtime dependency beyond this library itself. It throws on tokens with no `temporal-fmt` equivalent (`Do`/`P`/etc. see the [migration table](#token-mapping)) rather than guessing.
856
+
857
+ ### Interactive mode
858
+
859
+ Run `temporal-fmt` with no arguments to start a REPL:
860
+
861
+ ```
862
+ $ temporal-fmt
863
+ temporal-fmt interactive mode. Type a subcommand, "help", or "exit".
864
+ temporal-fmt> format
865
+ ISO input: 2026-08-04T15:45:30
866
+ Format string: yyyy-MM-dd HH:mm:ss
867
+ 2026-08-04 15:45:30
868
+ temporal-fmt> exit
869
+ ```
870
+
871
+ Type a subcommand with all its arguments inline (`validate yyyy-MM-dd`) or just the subcommand name — the REPL prompts for whatever's missing, one field at a time. Errors print and the session keeps going; `exit`, `quit`, or Ctrl+D ends it. This is the same subcommand logic as one-shot mode, just wrapped in a loop that asks instead of exiting on a missing argument — one-shot stays there for scripting, and doesn't touch the REPL machinery.
856
872
 
857
873
  ## Subpath imports
858
874
 
@@ -977,7 +993,7 @@ Migrate file by file, dropping the wrapper once nothing calls the old path anymo
977
993
  Neither of these ships as part of this repository — separate packages, install them on their own:
978
994
 
979
995
  - [`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.
980
- - [`temporal-fmt-codemod`](https://www.npmjs.com/package/temporal-fmt-codemod) — one-time migration tool that rewrites Day.js/date-fns calls to `temporal-fmt`. The CLI's `translate` subcommand (see [CLI](#cli)) imports this package at runtime, so `translate` needs it installed to work.
996
+ - [`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.
981
997
 
982
998
  ## Testing
983
999
 
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Translates a Day.js format string to the equivalent temporal-fmt
3
+ * token string.
4
+ *
5
+ * ```js
6
+ * translateDayjsFormatString('YYYY-MM-DD HH:mm:ss'); // "yyyy-MM-dd HH:mm:ss"
7
+ * translateDayjsFormatString('[Q]Q YYYY'); // "'Q'Q yyyy" (bracketed text stays literal)
8
+ * ```
9
+ *
10
+ * Throws if the format string uses a Day.js token with no temporal-fmt
11
+ * equivalent (`Do`, `Mo`, `Qo`, `k`, `kk`, `X`, `x`, the localized
12
+ * `L`-family tokens) — these need to be rewritten by hand, since
13
+ * there's no single temporal-fmt token that means the same thing.
14
+ *
15
+ * Note: `Do`/`Mo`/`Qo`/`k`/`kk`/`X`/`x` only have meaning in Day.js
16
+ * once the `AdvancedFormat` (and, for `X`/`x`, base) plugins are
17
+ * loaded — without them Day.js silently glues the parts together or
18
+ * passes the letters through unrendered instead of erroring. This
19
+ * function assumes the plugin is loaded, since that's the common case
20
+ * for anyone who put these tokens in a format string on purpose.
21
+ */
22
+ export declare function translateDayjsFormatString(formatStr: string): string;
23
+ /**
24
+ * Translates a date-fns format string to the equivalent temporal-fmt
25
+ * token string. date-fns already uses Unicode-style tokens close to
26
+ * temporal-fmt's own, so most strings pass through with only a case
27
+ * change on the weekday tokens; the notable divergences are date-fns's
28
+ * `D`/`DD` (day-of-year, matches temporal-fmt already) versus its `d`/
29
+ * `dd` (day-of-month, also matches already) and the locale-dependent
30
+ * `P`/`p` composite tokens, which have no single temporal-fmt token
31
+ * and throw.
32
+ */
33
+ export declare function translateDateFnsFormatString(formatStr: string): string;
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Translates a Day.js format string to the equivalent temporal-fmt
3
+ * token string.
4
+ *
5
+ * ```js
6
+ * translateDayjsFormatString('YYYY-MM-DD HH:mm:ss'); // "yyyy-MM-dd HH:mm:ss"
7
+ * translateDayjsFormatString('[Q]Q YYYY'); // "'Q'Q yyyy" (bracketed text stays literal)
8
+ * ```
9
+ *
10
+ * Throws if the format string uses a Day.js token with no temporal-fmt
11
+ * equivalent (`Do`, `Mo`, `Qo`, `k`, `kk`, `X`, `x`, the localized
12
+ * `L`-family tokens) — these need to be rewritten by hand, since
13
+ * there's no single temporal-fmt token that means the same thing.
14
+ *
15
+ * Note: `Do`/`Mo`/`Qo`/`k`/`kk`/`X`/`x` only have meaning in Day.js
16
+ * once the `AdvancedFormat` (and, for `X`/`x`, base) plugins are
17
+ * loaded — without them Day.js silently glues the parts together or
18
+ * passes the letters through unrendered instead of erroring. This
19
+ * function assumes the plugin is loaded, since that's the common case
20
+ * for anyone who put these tokens in a format string on purpose.
21
+ */
22
+ export declare function translateDayjsFormatString(formatStr: string): string;
23
+ /**
24
+ * Translates a date-fns format string to the equivalent temporal-fmt
25
+ * token string. date-fns already uses Unicode-style tokens close to
26
+ * temporal-fmt's own, so most strings pass through with only a case
27
+ * change on the weekday tokens; the notable divergences are date-fns's
28
+ * `D`/`DD` (day-of-year, matches temporal-fmt already) versus its `d`/
29
+ * `dd` (day-of-month, also matches already) and the locale-dependent
30
+ * `P`/`p` composite tokens, which have no single temporal-fmt token
31
+ * and throw.
32
+ */
33
+ export declare function translateDateFnsFormatString(formatStr: string): string;
@@ -25,7 +25,7 @@ export interface InlineDiagnostic {
25
25
  export declare function getInlineDiagnostics(formatStr: string): InlineDiagnostic[];
26
26
  export interface TokenConversionHint {
27
27
  from: string;
28
- to: string;
28
+ to: string | null;
29
29
  notes?: string;
30
30
  }
31
31
  export declare const DAYJS_TO_TEMPORAL_FMT: TokenConversionHint[];
package/dist/ideData.d.ts CHANGED
@@ -25,7 +25,7 @@ export interface InlineDiagnostic {
25
25
  export declare function getInlineDiagnostics(formatStr: string): InlineDiagnostic[];
26
26
  export interface TokenConversionHint {
27
27
  from: string;
28
- to: string;
28
+ to: string | null;
29
29
  notes?: string;
30
30
  }
31
31
  export declare const DAYJS_TO_TEMPORAL_FMT: TokenConversionHint[];
package/dist/index.cjs CHANGED
@@ -224,6 +224,8 @@ __export(index_exports, {
224
224
  tokenInfo: () => tokenInfo,
225
225
  tokenizeFormat: () => tokenizeFormat,
226
226
  totalDuration: () => totalDuration,
227
+ translateDateFnsFormatString: () => translateDateFnsFormatString,
228
+ translateDayjsFormatString: () => translateDayjsFormatString,
227
229
  truncate: () => truncate,
228
230
  tryParse: () => tryParse,
229
231
  union: () => union,
@@ -6197,19 +6199,102 @@ var DAYJS_TO_TEMPORAL_FMT = [
6197
6199
  { from: "M", to: "M" },
6198
6200
  { from: "DD", to: "dd" },
6199
6201
  { from: "D", to: "d" },
6202
+ { from: "Do", to: null, notes: `Ordinal day (AdvancedFormat) \u2014 use temporal-fmt's own "do" token instead.` },
6200
6203
  { from: "dddd", to: "EEEE" },
6201
6204
  { from: "ddd", to: "EEE" },
6205
+ { from: "dd", to: null, notes: 'Min-name weekday (e.g. "Tu") \u2014 temporal-fmt has no equivalent width.' },
6206
+ { from: "d", to: null, notes: 'Numeric weekday (0-6) \u2014 no temporal-fmt equivalent; not the same as "d" here, which is day-of-month.' },
6202
6207
  { from: "HH", to: "HH" },
6208
+ { from: "H", to: "H" },
6209
+ { from: "hh", to: "hh" },
6210
+ { from: "h", to: "h" },
6211
+ { from: "kk", to: null, notes: "Hour 1-24 (AdvancedFormat) \u2014 no temporal-fmt equivalent." },
6212
+ { from: "k", to: null, notes: "Hour 1-24 (AdvancedFormat) \u2014 no temporal-fmt equivalent." },
6203
6213
  { from: "mm", to: "mm" },
6214
+ { from: "m", to: "m" },
6204
6215
  { from: "ss", to: "ss" },
6205
- { from: "A", to: "a" },
6216
+ { from: "s", to: "s" },
6217
+ { from: "SSS", to: "SSS" },
6218
+ { from: "A", to: "a", notes: "Uppercase AM/PM in Day.js \u2014 temporal-fmt is always lowercase." },
6206
6219
  { from: "a", to: "a" },
6207
- { from: "Z", to: "XXX" }
6220
+ { from: "ZZ", to: "XX", notes: "Numeric UTC offset, no colon." },
6221
+ { from: "Z", to: "XXX", notes: "Numeric UTC offset with colon." },
6222
+ { from: "X", to: null, notes: "Unix timestamp (seconds) \u2014 use fromUnixSeconds() instead." },
6223
+ { from: "x", to: null, notes: "Unix timestamp (ms) \u2014 use fromUnixMilliseconds() instead." },
6224
+ { from: "Qo", to: null, notes: "Ordinal quarter (AdvancedFormat) \u2014 no temporal-fmt equivalent." },
6225
+ { from: "Q", to: "Q" },
6226
+ { from: "Mo", to: null, notes: "Ordinal month (AdvancedFormat) \u2014 no temporal-fmt equivalent." },
6227
+ { from: "ww", to: "ww" },
6228
+ { from: "wo", to: null, notes: "Ordinal ISO week (AdvancedFormat) \u2014 no temporal-fmt equivalent." },
6229
+ { from: "w", to: null, notes: `Unpadded ISO week \u2014 temporal-fmt's "ww" is always 2-digit.` },
6230
+ { from: "gggg", to: "RRRR", notes: "Week-numbering year, not the calendar year \u2014 same caveat as temporal-fmt's RRRR." },
6231
+ { from: "L", to: null, notes: "Localized date format (AdvancedFormat) \u2014 write the format string out explicitly." },
6232
+ { from: "LL", to: null, notes: "Localized date format (AdvancedFormat) \u2014 write the format string out explicitly." },
6233
+ { from: "LLL", to: null, notes: "Localized date format (AdvancedFormat) \u2014 write the format string out explicitly." },
6234
+ { from: "LLLL", to: null, notes: "Localized date format (AdvancedFormat) \u2014 write the format string out explicitly." },
6235
+ { from: "LT", to: null, notes: "Localized time format (AdvancedFormat) \u2014 write the format string out explicitly." },
6236
+ { from: "LTS", to: null, notes: "Localized time format (AdvancedFormat) \u2014 write the format string out explicitly." }
6208
6237
  ];
6209
6238
  var DATE_FNS_TO_TEMPORAL_FMT = [
6210
- ...DAYJS_TO_TEMPORAL_FMT
6211
- // most tokens are identical
6212
- // date-fns-specific differences noted inline.
6239
+ { from: "yyyy", to: "yyyy" },
6240
+ { from: "yy", to: "yy" },
6241
+ { from: "y", to: null, notes: 'Unpadded calendar year, opt-in via useAdditionalWeekYearTokens \u2014 temporal-fmt has no unpadded-year token; use "yyyy".' },
6242
+ { from: "MMMM", to: "MMMM" },
6243
+ { from: "MMM", to: "MMM" },
6244
+ { from: "MM", to: "MM" },
6245
+ { from: "M", to: "M" },
6246
+ { from: "LLLL", to: "LLLL" },
6247
+ { from: "LLL", to: "LLL" },
6248
+ { from: "dd", to: "dd" },
6249
+ { from: "d", to: "d" },
6250
+ { from: "do", to: "do" },
6251
+ { from: "DDD", to: "DDD" },
6252
+ { from: "DD", to: "DD" },
6253
+ { from: "D", to: "D", notes: `Day-of-year, opt-in via useAdditionalDayOfYearTokens \u2014 matches temporal-fmt's own "D" already.` },
6254
+ { from: "EEEE", to: "EEEE" },
6255
+ { from: "EEE", to: "EEE" },
6256
+ { from: "eeee", to: null, notes: "Locale-aware numeric weekday \u2014 no temporal-fmt equivalent." },
6257
+ { from: "cccc", to: "cccc" },
6258
+ { from: "ccc", to: "ccc" },
6259
+ { from: "HH", to: "HH" },
6260
+ { from: "H", to: "H" },
6261
+ { from: "hh", to: "hh" },
6262
+ { from: "h", to: "h" },
6263
+ { from: "mm", to: "mm" },
6264
+ { from: "m", to: "m" },
6265
+ { from: "ss", to: "ss" },
6266
+ { from: "s", to: "s" },
6267
+ { from: "SSS", to: "SSS" },
6268
+ { from: "a", to: "a" },
6269
+ { from: "aaa", to: "a" },
6270
+ { from: "XXX", to: "XXX" },
6271
+ { from: "XX", to: "XX" },
6272
+ { from: "X", to: "X", notes: `Opt-in via useAdditionalDayOfYearTokens-adjacent rules in date-fns v3+; matches temporal-fmt's own "X" already.` },
6273
+ { from: "xxx", to: "xxx" },
6274
+ { from: "xx", to: "xx" },
6275
+ { from: "x", to: "x" },
6276
+ { from: "zzzz", to: "zzzz" },
6277
+ { from: "zzz", to: "zzz" },
6278
+ { from: "z", to: "z" },
6279
+ { from: "QQQ", to: "QQQ" },
6280
+ { from: "Q", to: "Q" },
6281
+ { from: "GGGG", to: "GGGG" },
6282
+ { from: "GGG", to: null, notes: 'Abbreviated era \u2014 temporal-fmt only has "G" (short) and "GGGG" (long).' },
6283
+ { from: "GG", to: null, notes: 'Abbreviated era \u2014 temporal-fmt only has "G" (short) and "GGGG" (long).' },
6284
+ { from: "G", to: "G" },
6285
+ { from: "ww", to: "ww" },
6286
+ { from: "w", to: null, notes: `Unpadded local week number \u2014 temporal-fmt's "ww" is ISO-week and always 2-digit.` },
6287
+ { from: "RRRR", to: "RRRR" },
6288
+ { from: "R", to: null, notes: `Unpadded ISO week-numbering year \u2014 temporal-fmt's "RRRR" is always 4-digit.` },
6289
+ { from: "Y", to: null, notes: 'Week-numbering year (locale week rules), opt-in \u2014 no temporal-fmt equivalent; "RRRR" is ISO week-numbering, a different rule set.' },
6290
+ { from: "PPPP", to: null, notes: "Localized composite format \u2014 write the format string out explicitly." },
6291
+ { from: "PPP", to: null, notes: "Localized composite format \u2014 write the format string out explicitly." },
6292
+ { from: "PP", to: null, notes: "Localized composite format \u2014 write the format string out explicitly." },
6293
+ { from: "P", to: null, notes: "Localized composite format \u2014 write the format string out explicitly." },
6294
+ { from: "pppp", to: null, notes: "Localized composite format \u2014 write the format string out explicitly." },
6295
+ { from: "ppp", to: null, notes: "Localized composite format \u2014 write the format string out explicitly." },
6296
+ { from: "pp", to: null, notes: "Localized composite format \u2014 write the format string out explicitly." },
6297
+ { from: "p", to: null, notes: "Localized composite format \u2014 write the format string out explicitly." }
6213
6298
  ];
6214
6299
  function previewFormat(formatStr, sample) {
6215
6300
  const value = sample ?? {
@@ -6231,6 +6316,83 @@ function getDocUrl(tokenName) {
6231
6316
  void tokenName;
6232
6317
  return "README.md#token-reference";
6233
6318
  }
6319
+
6320
+ // src/codemod.ts
6321
+ function splitOnBrackets(source) {
6322
+ const pieces = [];
6323
+ let i = 0;
6324
+ while (i < source.length) {
6325
+ if (source[i] === "[") {
6326
+ const end = source.indexOf("]", i + 1);
6327
+ if (end === -1) {
6328
+ pieces.push({ kind: "token", value: source.slice(i) });
6329
+ break;
6330
+ }
6331
+ pieces.push({ kind: "literal", value: source.slice(i + 1, end) });
6332
+ i = end + 1;
6333
+ } else {
6334
+ let j = i;
6335
+ while (j < source.length && source[j] !== "[") j += 1;
6336
+ pieces.push({ kind: "token", value: source.slice(i, j) });
6337
+ i = j;
6338
+ }
6339
+ }
6340
+ return pieces;
6341
+ }
6342
+ function splitIntoRuns(span, sortedFrom) {
6343
+ const pieces = [];
6344
+ let i = 0;
6345
+ while (i < span.length) {
6346
+ const match = sortedFrom.find((tok) => span.startsWith(tok, i));
6347
+ if (match) {
6348
+ pieces.push({ kind: "token", value: match });
6349
+ i += match.length;
6350
+ continue;
6351
+ }
6352
+ const last = pieces[pieces.length - 1];
6353
+ if (last && last.kind === "literal") {
6354
+ last.value += span[i];
6355
+ } else {
6356
+ pieces.push({ kind: "literal", value: span[i] });
6357
+ }
6358
+ i += 1;
6359
+ }
6360
+ return pieces;
6361
+ }
6362
+ function translate(source, table, sourceLibLabel) {
6363
+ const byFrom = new Map(table.map((hint) => [hint.from, hint.to]));
6364
+ const sortedFrom = [...byFrom.keys()].sort((a, b) => b.length - a.length);
6365
+ const bracketPieces = splitOnBrackets(source);
6366
+ let out = "";
6367
+ for (const piece of bracketPieces) {
6368
+ if (piece.kind === "literal") {
6369
+ out += `'${piece.value.replace(/'/g, "''")}'`;
6370
+ continue;
6371
+ }
6372
+ for (const run of splitIntoRuns(piece.value, sortedFrom)) {
6373
+ if (run.kind === "token") {
6374
+ const mapped = byFrom.get(run.value);
6375
+ if (mapped === void 0 || mapped === null) {
6376
+ throw new Error(
6377
+ `temporal-fmt: "${run.value}" has no ${sourceLibLabel} -> temporal-fmt mapping (in format string "${source}"). Write the equivalent temporal-fmt token in by hand, or wrap it in brackets if it was meant as literal text.`
6378
+ );
6379
+ }
6380
+ out += mapped;
6381
+ } else if (/[a-zA-Z]/.test(run.value)) {
6382
+ out += `'${run.value.replace(/'/g, "''")}'`;
6383
+ } else {
6384
+ out += run.value;
6385
+ }
6386
+ }
6387
+ }
6388
+ return out;
6389
+ }
6390
+ function translateDayjsFormatString(formatStr) {
6391
+ return translate(formatStr, DAYJS_TO_TEMPORAL_FMT, "Day.js");
6392
+ }
6393
+ function translateDateFnsFormatString(formatStr) {
6394
+ return translate(formatStr, DATE_FNS_TO_TEMPORAL_FMT, "date-fns");
6395
+ }
6234
6396
  // Annotate the CommonJS export names for ESM import in node:
6235
6397
  0 && (module.exports = {
6236
6398
  ALL_TOKEN_NAMES,
@@ -6437,6 +6599,8 @@ function getDocUrl(tokenName) {
6437
6599
  tokenInfo,
6438
6600
  tokenizeFormat,
6439
6601
  totalDuration,
6602
+ translateDateFnsFormatString,
6603
+ translateDayjsFormatString,
6440
6604
  truncate,
6441
6605
  tryParse,
6442
6606
  union,