Skip to content

Commit 4a76b2a

Browse files
fix(preview): move formatDate/formatDateTime into runtime subtree (#3749)
1 parent 0ccf94f commit 4a76b2a

File tree

3 files changed

+112
-2
lines changed

3 files changed

+112
-2
lines changed

src/runtime/internal/preview/collection.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,9 @@ import { joinURL, withoutLeadingSlash } from 'ufo'
33
import type { JsonSchema7ObjectType } from 'zod-to-json-schema'
44
import { hash } from 'ohash'
55
import { getOrderedSchemaKeys } from '../schema'
6-
import { parseSourceBase } from './utils'
6+
import { formatDate, formatDateTime, parseSourceBase } from './utils'
77
import { withoutPrefixNumber, withoutRoot } from './files'
88
import type { CollectionInfo, ResolvedCollectionSource } from '@nuxt/content'
9-
import { formatDate, formatDateTime } from '../../../utils/content/transformers/utils'
109

1110
export const getCollectionByFilePath = (path: string, collections: Record<string, CollectionInfo>): { collection: CollectionInfo | undefined, matchedSource: ResolvedCollectionSource | undefined } => {
1211
let matchedSource: ResolvedCollectionSource | undefined

src/runtime/internal/preview/utils.ts

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,3 +68,45 @@ export function parseSourceBase(source: CollectionSource) {
6868
dynamic: '*' + rest.join('*'),
6969
}
7070
}
71+
72+
/**
73+
* Format a date string as `YYYY-MM-DD` for SQL DATE columns.
74+
*
75+
* Duplicated from `src/utils/content/transformers/utils.ts` because that
76+
* file lives outside the `runtime/` subtree and is not emitted to dist.
77+
* Importing it from the preview runtime causes a broken path in the
78+
* published package.
79+
*
80+
* @see https://github.com/nuxt/content/issues/3742
81+
*/
82+
export const formatDate = (date: string): string => {
83+
const d = new Date(date)
84+
if (Number.isNaN(d.getTime())) {
85+
throw new TypeError(`Invalid date value: "${date}"`)
86+
}
87+
88+
const year = d.getFullYear()
89+
const month = d.getMonth() + 1
90+
const day = d.getDate()
91+
92+
return `${year.toString().padStart(4, '0')}-${month.toString().padStart(2, '0')}-${day.toString().padStart(2, '0')}`
93+
}
94+
95+
/**
96+
* Format a date string as `YYYY-MM-DD HH:mm:ss` for SQL DATETIME columns.
97+
*
98+
* @see {@link formatDate} for why this is duplicated here.
99+
* @see https://github.com/nuxt/content/issues/3742
100+
*/
101+
export const formatDateTime = (datetime: string): string => {
102+
const d = new Date(datetime)
103+
if (Number.isNaN(d.getTime())) {
104+
throw new TypeError(`Invalid datetime value: "${datetime}"`)
105+
}
106+
107+
const hours = d.getHours()
108+
const minutes = d.getMinutes()
109+
const seconds = d.getSeconds()
110+
111+
return `${formatDate(datetime)} ${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`
112+
}

test/unit/formatDate.test.ts

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import { describe, expect, it } from 'vitest'
2+
import { formatDate, formatDateTime } from '../../src/runtime/internal/preview/utils'
3+
4+
describe('formatDate', () => {
5+
it('formats a date string as YYYY-MM-DD', () => {
6+
// formatDate uses local time (getFullYear/getMonth/getDate), so we
7+
// construct expected values the same way to stay timezone-agnostic.
8+
const input = '2022-06-15T12:00:00.000Z'
9+
const d = new Date(input)
10+
const expected = `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`
11+
expect(formatDate(input)).toBe(expected)
12+
})
13+
14+
it('pads single-digit month and day', () => {
15+
const input = '2022-01-05T12:00:00.000Z'
16+
const result = formatDate(input)
17+
// Format is always YYYY-MM-DD with zero-padded segments
18+
expect(result).toMatch(/^\d{4}-\d{2}-\d{2}$/)
19+
expect(result).toContain('-05')
20+
})
21+
22+
it('handles end-of-year dates', () => {
23+
const input = '2022-12-31T12:00:00.000Z'
24+
const result = formatDate(input)
25+
expect(result).toMatch(/^\d{4}-12-31$/)
26+
})
27+
28+
it('throws on invalid date', () => {
29+
expect(() => formatDate('not-a-date')).toThrow(TypeError)
30+
expect(() => formatDate('not-a-date')).toThrow('Invalid date value')
31+
})
32+
33+
it('produces same output as the build-time copy', async () => {
34+
// Guard against the two copies drifting apart.
35+
const buildTime = await import('../../src/utils/content/transformers/utils')
36+
const inputs = ['2022-06-15T12:00:00.000Z', '2023-01-01T00:00:00.000Z', '2024-12-31T23:59:59.000Z']
37+
for (const input of inputs) {
38+
expect(formatDate(input)).toBe(buildTime.formatDate(input))
39+
}
40+
})
41+
})
42+
43+
describe('formatDateTime', () => {
44+
it('formats a datetime string as YYYY-MM-DD HH:mm:ss', () => {
45+
const input = '2022-06-15T14:30:45.000Z'
46+
const result = formatDateTime(input)
47+
expect(result).toMatch(/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/)
48+
// The date portion must match formatDate
49+
expect(result.split(' ')[0]).toBe(formatDate(input))
50+
})
51+
52+
it('pads single-digit hours, minutes, and seconds', () => {
53+
const result = formatDateTime('2022-01-01T01:02:03.000Z')
54+
expect(result).toMatch(/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/)
55+
})
56+
57+
it('throws on invalid datetime', () => {
58+
expect(() => formatDateTime('garbage')).toThrow(TypeError)
59+
expect(() => formatDateTime('garbage')).toThrow('Invalid datetime value')
60+
})
61+
62+
it('produces same output as the build-time copy', async () => {
63+
const buildTime = await import('../../src/utils/content/transformers/utils')
64+
const inputs = ['2022-06-15T14:30:45.000Z', '2023-01-01T00:00:00.000Z']
65+
for (const input of inputs) {
66+
expect(formatDateTime(input)).toBe(buildTime.formatDateTime(input))
67+
}
68+
})
69+
})

0 commit comments

Comments
 (0)