@abreen/tada 1.7.0 → 1.7.1

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/bin/tada.ts CHANGED
@@ -607,21 +607,14 @@ try {
607
607
  await initCommand(process.argv.slice(3));
608
608
  break;
609
609
 
610
- case 'dev': {
611
- requireSiteConfig('dev');
612
- const { runPipeline } = await import(
613
- path.join(packageDir, 'build/pipeline.ts')
614
- );
615
- await runPipeline('development');
616
- break;
617
- }
618
-
610
+ case 'dev':
619
611
  case 'prod': {
620
- requireSiteConfig('prod');
612
+ const mode = command === 'dev' ? 'development' : 'production';
613
+ requireSiteConfig(command);
621
614
  const { runPipeline } = await import(
622
615
  path.join(packageDir, 'build/pipeline.ts')
623
616
  );
624
- await runPipeline('production');
617
+ await runPipeline(mode);
625
618
  break;
626
619
  }
627
620
 
@@ -3,6 +3,7 @@ import type MarkdownIt from 'markdown-it';
3
3
  import type Token from 'markdown-it/lib/token.mjs';
4
4
  import { createApplyBasePath } from './utils/paths';
5
5
  import { isFeatureEnabled } from './features';
6
+ import { isInternalLink } from './utils/link';
6
7
  import { makeLogger } from './log';
7
8
  import type { SiteVariables } from './types';
8
9
 
@@ -26,6 +27,11 @@ export default function applyBasePathPlugin(
26
27
  const validTargets = pluginOptions.validTargets;
27
28
 
28
29
  function rewriteInternalHref(href: string): string {
30
+ const resolveAgainstSource = (p: string): string =>
31
+ p.startsWith('/')
32
+ ? p
33
+ : path.posix.join(path.posix.dirname(sourceUrlPath!), p);
34
+
29
35
  const match = href.match(/^([^?#]*)(.*)$/);
30
36
  const pathname = match ? match[1] : href;
31
37
  const suffix = match ? match[2] : '';
@@ -35,12 +41,7 @@ export default function applyBasePathPlugin(
35
41
  for (const ext of Object.keys(siteVariables.codeLanguages ?? {})) {
36
42
  if (modifiedPath.endsWith(`.${ext}`)) {
37
43
  if (literateJavaOutputPaths && sourceUrlPath) {
38
- const resolved = modifiedPath.startsWith('/')
39
- ? modifiedPath
40
- : path.posix.join(
41
- path.posix.dirname(sourceUrlPath),
42
- modifiedPath,
43
- );
44
+ const resolved = resolveAgainstSource(modifiedPath);
44
45
  if (literateJavaOutputPaths.has(resolved)) {
45
46
  break;
46
47
  }
@@ -48,12 +49,7 @@ export default function applyBasePathPlugin(
48
49
  // Only rewrite if the .html version exists. Public files with
49
50
  // code extensions are copied as-is and have no .html page.
50
51
  if (validTargets && sourceUrlPath) {
51
- const resolved = modifiedPath.startsWith('/')
52
- ? modifiedPath
53
- : path.posix.join(
54
- path.posix.dirname(sourceUrlPath),
55
- modifiedPath,
56
- );
52
+ const resolved = resolveAgainstSource(modifiedPath);
57
53
  if (!validTargets.has(`${resolved}.html`)) {
58
54
  break;
59
55
  }
@@ -79,11 +75,7 @@ export default function applyBasePathPlugin(
79
75
  const afterApply = applyBasePath(modifiedHref);
80
76
  log.debug`Applying base path: ${href} -> ${afterApply}`;
81
77
  token.attrSet('href', afterApply);
82
- } else if (
83
- !href.startsWith('#') &&
84
- !href.startsWith('//') &&
85
- !/^[a-zA-Z][a-zA-Z\d+.-]*:/.test(href)
86
- ) {
78
+ } else if (isInternalLink(href)) {
87
79
  // Relative link: only rewrite code extensions, don't apply base path
88
80
  const modifiedHref = rewriteInternalHref(href);
89
81
  if (modifiedHref !== href) {
@@ -120,10 +112,10 @@ export default function applyBasePathPlugin(
120
112
  );
121
113
  }
122
114
 
123
- token.children?.map(checkAndApplyBasePath);
115
+ token.children?.forEach(checkAndApplyBasePath);
124
116
  }
125
117
 
126
118
  md.core.ruler.push('apply_base_path', state => {
127
- state.tokens.map(checkAndApplyBasePath);
119
+ state.tokens.forEach(checkAndApplyBasePath);
128
120
  });
129
121
  }
package/build/colors.ts CHANGED
@@ -1,25 +1,22 @@
1
1
  import util from 'util';
2
2
 
3
- export const R = (str: TemplateStringsArray, ...args: unknown[]): string =>
4
- util.styleText(['red'], String.raw(str, ...args));
5
- export const G = (str: TemplateStringsArray, ...args: unknown[]): string =>
6
- util.styleText(['green'], String.raw(str, ...args));
7
- export const B = (str: TemplateStringsArray, ...args: unknown[]): string =>
8
- util.styleText(['blue'], String.raw(str, ...args));
9
- export const Y = (str: TemplateStringsArray, ...args: unknown[]): string =>
10
- util.styleText(['yellow'], String.raw(str, ...args));
11
- export const L = (str: TemplateStringsArray, ...args: unknown[]): string =>
12
- util.styleText(['blackBright'], String.raw(str, ...args));
13
- export const P = (str: TemplateStringsArray, ...args: unknown[]): string =>
14
- util.styleText(['magenta'], String.raw(str, ...args));
15
- export const I = (str: TemplateStringsArray, ...args: unknown[]): string =>
16
- util.styleText(['italic', 'bold'], String.raw(str, ...args));
3
+ type StyleFormat = Parameters<typeof util.styleText>[0];
4
+ type TagFn = (str: TemplateStringsArray, ...args: unknown[]) => string;
17
5
 
18
- export const Ri = (str: TemplateStringsArray, ...args: unknown[]): string =>
19
- util.styleText(['inverse', 'red'], String.raw(str, ...args));
20
- export const Gi = (str: TemplateStringsArray, ...args: unknown[]): string =>
21
- util.styleText(['inverse', 'green'], String.raw(str, ...args));
22
- export const Yi = (str: TemplateStringsArray, ...args: unknown[]): string =>
23
- util.styleText(['inverse', 'yellow'], String.raw(str, ...args));
24
- export const Li = (str: TemplateStringsArray, ...args: unknown[]): string =>
25
- util.styleText(['inverse', 'blackBright'], String.raw(str, ...args));
6
+ const style =
7
+ (styles: StyleFormat): TagFn =>
8
+ (str, ...args) =>
9
+ util.styleText(styles, String.raw(str, ...args));
10
+
11
+ export const R = style(['red']);
12
+ export const G = style(['green']);
13
+ export const B = style(['blue']);
14
+ export const Y = style(['yellow']);
15
+ export const L = style(['blackBright']);
16
+ export const P = style(['magenta']);
17
+ export const I = style(['italic', 'bold']);
18
+
19
+ export const Ri = style(['inverse', 'red']);
20
+ export const Gi = style(['inverse', 'green']);
21
+ export const Yi = style(['inverse', 'yellow']);
22
+ export const Li = style(['inverse', 'blackBright']);
@@ -2,7 +2,6 @@ import path from 'path';
2
2
  import type { SiteVariables } from './types';
3
3
  import { getContentDir, getBuildContentFiles, isPartial } from './util';
4
4
  import { compileTemplates, getJsonDataDir, JSON_DATA_FILES } from './templates';
5
- import { getProjectDir } from './utils/paths';
6
5
  import { B } from './colors';
7
6
  import { makeLogger } from './log';
8
7
 
@@ -18,18 +17,16 @@ interface ChangeDetectionResult {
18
17
 
19
18
  export class ContentChangeDetector {
20
19
  private siteVariables: SiteVariables;
21
- private siteConfigPath: string;
22
20
  private lastSig: string | null;
23
21
 
24
22
  constructor(siteVariables: SiteVariables) {
25
23
  this.siteVariables = siteVariables;
26
- this.siteConfigPath = path.resolve(getProjectDir(), 'site.dev.json');
27
24
  this.lastSig = null;
28
25
  }
29
26
 
30
27
  detectChanges(modifiedFiles: Iterable<string>): ChangeDetectionResult {
31
28
  const resolvedFiles = new Set(
32
- [...(modifiedFiles || [])].map(filePath => path.resolve(filePath)),
29
+ [...modifiedFiles].map(filePath => path.resolve(filePath)),
33
30
  );
34
31
 
35
32
  const jsonDataDir = getJsonDataDir();
@@ -75,16 +72,14 @@ export class ContentChangeDetector {
75
72
  jsonDataPaths.includes(filePath),
76
73
  );
77
74
  const jsonDataChanged = changedJsonDataPaths.length > 0;
78
- const partialsChanged = [...changedContentFiles].some(f => isPartial(f));
75
+ const changedPartials = [...changedContentFiles].filter(f => isPartial(f));
76
+ const partialsChanged = changedPartials.length > 0;
79
77
 
80
78
  for (const filePath of changedJsonDataPaths) {
81
79
  log.event`${B`${path.basename(filePath)}`} changed, rebuilding`;
82
80
  }
83
81
 
84
82
  if (partialsChanged) {
85
- const changedPartials = [...changedContentFiles].filter(f =>
86
- isPartial(f),
87
- );
88
83
  for (const filePath of changedPartials) {
89
84
  log.event`Partial ${B`${path.basename(filePath)}`} changed, rebuilding`;
90
85
  }
@@ -97,14 +92,6 @@ export class ContentChangeDetector {
97
92
  log.event`${B`${path.basename(filePath)}`} changed, re-running traces`;
98
93
  }
99
94
 
100
- // Check if site config changed
101
- const siteConfigChanged = resolvedFiles.has(
102
- path.resolve(this.siteConfigPath),
103
- );
104
- if (siteConfigChanged) {
105
- needsRestart = true;
106
- }
107
-
108
95
  return {
109
96
  templateError: null,
110
97
  needsRestart,
package/build/copy.ts CHANGED
@@ -15,8 +15,11 @@ function collectFiles(dir: string): CollectedFile[] {
15
15
  let entries: fs.Dirent[];
16
16
  try {
17
17
  entries = fs.readdirSync(dir, { withFileTypes: true, recursive: true });
18
- } catch {
19
- return [];
18
+ } catch (err: unknown) {
19
+ if ((err as NodeJS.ErrnoException).code === 'ENOENT') {
20
+ return [];
21
+ }
22
+ throw err;
20
23
  }
21
24
  return entries
22
25
  .filter(entry => entry.isFile())
@@ -59,7 +62,7 @@ export function copyContentAssets(
59
62
  continue;
60
63
  }
61
64
  contentAssetRelPaths.add(rel);
62
- if (publicRelPaths && publicRelPaths.has(rel)) {
65
+ if (publicRelPaths.has(rel)) {
63
66
  conflicts.push(rel);
64
67
  }
65
68
  const dest = path.join(distDir, rel);
@@ -78,21 +81,41 @@ export function copyContentAssets(
78
81
  return contentAssetRelPaths;
79
82
  }
80
83
 
81
- export function copyPublicFile(
82
- publicDir: string,
84
+ function copySingleFile(
85
+ sourceDir: string,
83
86
  distDir: string,
84
87
  filePath: string,
85
- contentAssetRelPaths?: Set<string>,
88
+ sourceLabel: string,
89
+ conflictLabel: string,
90
+ conflictSet?: Set<string>,
86
91
  ): void {
87
- const rel = toPosix(path.relative(publicDir, filePath));
88
- if (contentAssetRelPaths && contentAssetRelPaths.has(rel)) {
89
- log.error`public/${B`${rel}`} conflicts with content/${B`${rel}`}`;
90
- throw new Error(`public/${rel} and content/${rel} have the same path`);
92
+ const rel = toPosix(path.relative(sourceDir, filePath));
93
+ if (conflictSet?.has(rel)) {
94
+ log.error`${sourceLabel}/${B`${rel}`} conflicts with ${conflictLabel}/${B`${rel}`}`;
95
+ throw new Error(
96
+ `${sourceLabel}/${rel} and ${conflictLabel}/${rel} have the same path`,
97
+ );
91
98
  }
92
99
  const dest = path.join(distDir, rel);
93
100
  fs.mkdirSync(path.dirname(dest), { recursive: true });
94
101
  fs.copyFileSync(filePath, dest);
95
- log.info`Copying public file ${B`${rel}`}`;
102
+ log.info`Copying ${sourceLabel} file ${B`${rel}`}`;
103
+ }
104
+
105
+ export function copyPublicFile(
106
+ publicDir: string,
107
+ distDir: string,
108
+ filePath: string,
109
+ contentAssetRelPaths?: Set<string>,
110
+ ): void {
111
+ copySingleFile(
112
+ publicDir,
113
+ distDir,
114
+ filePath,
115
+ 'public',
116
+ 'content',
117
+ contentAssetRelPaths,
118
+ );
96
119
  }
97
120
 
98
121
  export function copyContentFile(
@@ -101,13 +124,12 @@ export function copyContentFile(
101
124
  filePath: string,
102
125
  publicRelPaths?: Set<string>,
103
126
  ): void {
104
- const rel = toPosix(path.relative(contentDir, filePath));
105
- if (publicRelPaths && publicRelPaths.has(rel)) {
106
- log.error`content/${B`${rel}`} conflicts with public/${B`${rel}`}`;
107
- throw new Error(`content/${rel} and public/${rel} have the same path`);
108
- }
109
- const dest = path.join(distDir, rel);
110
- fs.mkdirSync(path.dirname(dest), { recursive: true });
111
- fs.copyFileSync(filePath, dest);
112
- log.info`Copying content file ${B`${rel}`}`;
127
+ copySingleFile(
128
+ contentDir,
129
+ distDir,
130
+ filePath,
131
+ 'content',
132
+ 'public',
133
+ publicRelPaths,
134
+ );
113
135
  }
@@ -1,5 +1,5 @@
1
1
  import type MarkdownIt from 'markdown-it';
2
- import textToId from './text-to-id';
2
+ import textToId, { deduplicateId } from './text-to-id';
3
3
 
4
4
  export default function deflistIdPlugin(md: MarkdownIt): void {
5
5
  md.core.ruler.push('deflist_id_injector', function (state) {
@@ -7,21 +7,8 @@ export default function deflistIdPlugin(md: MarkdownIt): void {
7
7
  const used = new Map<string, number>();
8
8
 
9
9
  function slugify(str: string): string {
10
- let slug = textToId(str);
11
-
12
- if (!slug) {
13
- slug = 'term';
14
- }
15
-
16
- if (used.has(slug)) {
17
- const n = (used.get(slug) as number) + 1;
18
- used.set(slug, n);
19
- slug = `${slug}-${n}`;
20
- } else {
21
- used.set(slug, 1);
22
- }
23
-
24
- return slug;
10
+ const base = textToId(str) || 'term';
11
+ return deduplicateId(used, base);
25
12
  }
26
13
 
27
14
  for (let i = 0; i < tokens.length; i++) {
package/build/features.ts CHANGED
@@ -1,11 +1,8 @@
1
- import type { SiteVariables } from './types';
1
+ import type { FeatureConfig, SiteVariables } from './types';
2
2
 
3
3
  export function isFeatureEnabled(
4
4
  siteVariables: SiteVariables,
5
- featureName: string,
5
+ featureName: keyof FeatureConfig,
6
6
  ): boolean {
7
- return (
8
- siteVariables.features?.[featureName as keyof SiteVariables['features']] !==
9
- false
10
- );
7
+ return siteVariables.features[featureName] !== false;
11
8
  }
@@ -44,12 +44,16 @@ export class ContentRenderer {
44
44
  > = new Map();
45
45
 
46
46
  constructor(siteVariables: SiteVariables) {
47
- this.siteVariables = siteVariables || {};
47
+ this.siteVariables = siteVariables;
48
48
  this.loggedCodeDisabled = false;
49
49
  this.sourceFileCache = new Map();
50
50
  this.lastBuildFiles = new Set();
51
51
  }
52
52
 
53
+ private getCachedAssets(filePath: string): Asset[] {
54
+ return this.sourceFileCache.get(filePath) ?? [];
55
+ }
56
+
53
57
  async initHighlighter(): Promise<void> {
54
58
  const langs = [
55
59
  ...new Set([
@@ -210,7 +214,7 @@ export class ContentRenderer {
210
214
  changedHtmlAssetPaths: Set<string>,
211
215
  removedHtmlAssetPaths: Set<string>,
212
216
  ): void {
213
- const previousAssets = this.sourceFileCache.get(filePath) || [];
217
+ const previousAssets = this.getCachedAssets(filePath);
214
218
  const nextAssetPaths = new Set(assets.map(asset => asset.assetPath));
215
219
 
216
220
  for (const asset of previousAssets) {
@@ -233,7 +237,7 @@ export class ContentRenderer {
233
237
 
234
238
  writeCachedAssets(distDir: string, buildContentFiles: string[]): void {
235
239
  for (const filePath of buildContentFiles) {
236
- const assets = this.sourceFileCache.get(filePath) || [];
240
+ const assets = this.getCachedAssets(filePath);
237
241
  for (const asset of assets) {
238
242
  const outPath = path.join(distDir, asset.assetPath);
239
243
  fs.mkdirSync(path.dirname(outPath), { recursive: true });
@@ -251,7 +255,7 @@ export class ContentRenderer {
251
255
  continue;
252
256
  }
253
257
 
254
- const previousAssets = this.sourceFileCache.get(filePath) || [];
258
+ const previousAssets = this.getCachedAssets(filePath);
255
259
  for (const asset of previousAssets) {
256
260
  if (asset.assetPath.endsWith('.html')) {
257
261
  removedHtmlAssetPaths.add(asset.assetPath);
@@ -352,7 +356,7 @@ export class ContentRenderer {
352
356
  // Collect HTML asset content for Pagefind
353
357
  const htmlAssetsByPath = new Map<string, string>();
354
358
  for (const filePath of buildContentFiles) {
355
- const assets = this.sourceFileCache.get(filePath) || [];
359
+ const assets = this.getCachedAssets(filePath);
356
360
  for (const asset of assets) {
357
361
  if (asset.assetPath.endsWith('.html')) {
358
362
  htmlAssetsByPath.set(asset.assetPath, asset.content as string);
@@ -10,6 +10,8 @@ import type { SiteVariables } from './types';
10
10
 
11
11
  const log = makeLogger(import.meta.url);
12
12
 
13
+ export const FAVICON_SIZES = [16, 32, 48, 64, 128, 192, 256, 512, 1024];
14
+
13
15
  const FONT_PATH = path.join(
14
16
  getPackageDir(),
15
17
  'fonts',
@@ -110,24 +112,12 @@ export async function generateFavicons(
110
112
  siteVariables: SiteVariables,
111
113
  distDir: string,
112
114
  ): Promise<void> {
113
- const color = siteVariables.faviconColor;
114
- const symbol = siteVariables.faviconSymbol;
115
+ const color = siteVariables.faviconColor!;
116
+ const symbol = siteVariables.faviconSymbol!;
115
117
  const fontWeight = siteVariables.faviconFontWeight || 700;
116
- const sizes = [16, 32, 48, 64, 128, 192, 256, 512, 1024];
117
118
  const svgSize = 512;
118
119
  const filenameBase = 'favicon';
119
120
 
120
- if (!symbol) {
121
- throw new Error(
122
- 'Favicon generation requires "symbol" in site config (or "faviconSymbol")',
123
- );
124
- }
125
- if (!color) {
126
- throw new Error(
127
- 'Favicon generation requires "themeColor" in site config (or "faviconColor")',
128
- );
129
- }
130
-
131
121
  const { themeColorLight, textOnThemeLight } = deriveTheme(color);
132
122
 
133
123
  log.info`Generating favicons`;
@@ -145,7 +135,7 @@ export async function generateFavicons(
145
135
  fs.writeFileSync(path.join(distDir, `${filenameBase}.svg`), svgMarkup);
146
136
 
147
137
  const pngBuffers = await Promise.all(
148
- sizes.map(async size => {
138
+ FAVICON_SIZES.map(async size => {
149
139
  const svgForSize = createFaviconSvg(
150
140
  size,
151
141
  themeColorLight,
@@ -1,6 +1,7 @@
1
1
  import fs from 'fs';
2
2
  import path from 'path';
3
3
  import { createApplyBasePath } from './util';
4
+ import { FAVICON_SIZES } from './generate-favicon';
4
5
  import type { SiteVariables } from './types';
5
6
 
6
7
  interface ManifestIcon {
@@ -19,6 +20,20 @@ interface WebAppManifest {
19
20
 
20
21
  function createManifest(siteVariables: SiteVariables): WebAppManifest {
21
22
  const applyBasePath = createApplyBasePath(siteVariables);
23
+ const filenameBase = 'favicon';
24
+
25
+ const pngIcons: ManifestIcon[] = [...FAVICON_SIZES]
26
+ .sort((a, b) => b - a)
27
+ .map(size => ({
28
+ src: `${filenameBase}-${size}.png`,
29
+ sizes: `${size}x${size}`,
30
+ type: 'image/png',
31
+ purpose: size >= 128 ? 'any maskable' : 'any',
32
+ }));
33
+
34
+ const icoSizes = FAVICON_SIZES.filter(s => s <= 256)
35
+ .map(s => `${s}x${s}`)
36
+ .join(' ');
22
37
 
23
38
  return {
24
39
  name: siteVariables.title,
@@ -26,68 +41,15 @@ function createManifest(siteVariables: SiteVariables): WebAppManifest {
26
41
  display: 'minimal-ui',
27
42
  icons: [
28
43
  {
29
- src: 'favicon.svg',
44
+ src: `${filenameBase}.svg`,
30
45
  sizes: 'any',
31
46
  type: 'image/svg+xml',
32
47
  purpose: 'any maskable',
33
48
  },
49
+ ...pngIcons,
34
50
  {
35
- src: 'favicon-1024.png',
36
- sizes: '1024x1024',
37
- type: 'image/png',
38
- purpose: 'any maskable',
39
- },
40
- {
41
- src: 'favicon-512.png',
42
- sizes: '512x512',
43
- type: 'image/png',
44
- purpose: 'any maskable',
45
- },
46
- {
47
- src: 'favicon-256.png',
48
- sizes: '256x256',
49
- type: 'image/png',
50
- purpose: 'any maskable',
51
- },
52
- {
53
- src: 'favicon-192.png',
54
- sizes: '192x192',
55
- type: 'image/png',
56
- purpose: 'any maskable',
57
- },
58
- {
59
- src: 'favicon-128.png',
60
- sizes: '128x128',
61
- type: 'image/png',
62
- purpose: 'any maskable',
63
- },
64
- {
65
- src: 'favicon-64.png',
66
- sizes: '64x64',
67
- type: 'image/png',
68
- purpose: 'any',
69
- },
70
- {
71
- src: 'favicon-48.png',
72
- sizes: '48x48',
73
- type: 'image/png',
74
- purpose: 'any',
75
- },
76
- {
77
- src: 'favicon-32.png',
78
- sizes: '32x32',
79
- type: 'image/png',
80
- purpose: 'any',
81
- },
82
- {
83
- src: 'favicon-16.png',
84
- sizes: '16x16',
85
- type: 'image/png',
86
- purpose: 'any',
87
- },
88
- {
89
- src: 'favicon.ico',
90
- sizes: '16x16 32x32 48x48 64x64 128x128 192x192 256x256',
51
+ src: `${filenameBase}.ico`,
52
+ sizes: icoSizes,
91
53
  type: 'image/x-icon',
92
54
  purpose: 'any maskable',
93
55
  },
package/build/globals.ts CHANGED
@@ -1,6 +1,22 @@
1
1
  import timezones from '../src/timezone/timezones.json' with { type: 'json' };
2
2
  import type { SiteVariables } from './types';
3
3
 
4
+ const MONTHS = [
5
+ '',
6
+ 'January',
7
+ 'February',
8
+ 'March',
9
+ 'April',
10
+ 'May',
11
+ 'June',
12
+ 'July',
13
+ 'August',
14
+ 'September',
15
+ 'October',
16
+ 'November',
17
+ 'December',
18
+ ];
19
+
4
20
  interface Globals {
5
21
  isHomePage: boolean;
6
22
  isoDate: (str: string | null | undefined) => string | null;
@@ -58,31 +74,10 @@ function readableDate(date: string | Date | null | undefined): string {
58
74
 
59
75
  const str = date.toISOString();
60
76
  const year = str.slice(0, 4);
61
- let month: string = str.slice(5, 7);
62
- if (month[0] === '0') {
63
- month = month[1];
64
- }
65
- let day: string = str.slice(8, 10);
66
- if (day[0] === '0') {
67
- day = day[1];
68
- }
69
-
70
- const months: Record<string, string> = {
71
- 1: 'January',
72
- 2: 'February',
73
- 3: 'March',
74
- 4: 'April',
75
- 5: 'May',
76
- 6: 'June',
77
- 7: 'July',
78
- 8: 'August',
79
- 9: 'September',
80
- 10: 'October',
81
- 11: 'November',
82
- 12: 'December',
83
- };
77
+ const month = Number(str.slice(5, 7));
78
+ const day = Number(str.slice(8, 10));
84
79
 
85
- return `${months[month]} ${day}, ${year}`;
80
+ return `${MONTHS[month]} ${day}, ${year}`;
86
81
  }
87
82
 
88
83
  export function classNames(obj: Record<string, unknown>): string {