Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions src/runtime/internal/preview/collection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,9 @@ import { joinURL, withoutLeadingSlash } from 'ufo'
import type { JsonSchema7ObjectType } from 'zod-to-json-schema'
import { hash } from 'ohash'
import { getOrderedSchemaKeys } from '../schema'
import { parseSourceBase } from './utils'
import { formatDate, formatDateTime, parseSourceBase } from './utils'
import { withoutPrefixNumber, withoutRoot } from './files'
import type { CollectionInfo, ResolvedCollectionSource } from '@nuxt/content'
import { formatDate, formatDateTime } from '../../../utils/content/transformers/utils'

export const getCollectionByFilePath = (path: string, collections: Record<string, CollectionInfo>): { collection: CollectionInfo | undefined, matchedSource: ResolvedCollectionSource | undefined } => {
let matchedSource: ResolvedCollectionSource | undefined
Expand Down
42 changes: 42 additions & 0 deletions src/runtime/internal/preview/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,3 +68,45 @@ export function parseSourceBase(source: CollectionSource) {
dynamic: '*' + rest.join('*'),
}
}

/**
* Format a date string as `YYYY-MM-DD` for SQL DATE columns.
*
* Duplicated from `src/utils/content/transformers/utils.ts` because that
* file lives outside the `runtime/` subtree and is not emitted to dist.
* Importing it from the preview runtime causes a broken path in the
* published package.
*
* @see https://github.com/nuxt/content/issues/3742
*/
export const formatDate = (date: string): string => {
const d = new Date(date)
if (Number.isNaN(d.getTime())) {
throw new TypeError(`Invalid date value: "${date}"`)
}

const year = d.getFullYear()
const month = d.getMonth() + 1
const day = d.getDate()

return `${year.toString().padStart(4, '0')}-${month.toString().padStart(2, '0')}-${day.toString().padStart(2, '0')}`
}

/**
* Format a date string as `YYYY-MM-DD HH:mm:ss` for SQL DATETIME columns.
*
* @see {@link formatDate} for why this is duplicated here.
* @see https://github.com/nuxt/content/issues/3742
*/
export const formatDateTime = (datetime: string): string => {
const d = new Date(datetime)
if (Number.isNaN(d.getTime())) {
throw new TypeError(`Invalid datetime value: "${datetime}"`)
}

const hours = d.getHours()
const minutes = d.getMinutes()
const seconds = d.getSeconds()

return `${formatDate(datetime)} ${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`
}
69 changes: 69 additions & 0 deletions test/unit/formatDate.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { describe, expect, it } from 'vitest'
import { formatDate, formatDateTime } from '../../src/runtime/internal/preview/utils'

describe('formatDate', () => {
it('formats a date string as YYYY-MM-DD', () => {
// formatDate uses local time (getFullYear/getMonth/getDate), so we
// construct expected values the same way to stay timezone-agnostic.
const input = '2022-06-15T12:00:00.000Z'
const d = new Date(input)
const expected = `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`
expect(formatDate(input)).toBe(expected)
})

it('pads single-digit month and day', () => {
const input = '2022-01-05T12:00:00.000Z'
const result = formatDate(input)
// Format is always YYYY-MM-DD with zero-padded segments
expect(result).toMatch(/^\d{4}-\d{2}-\d{2}$/)
expect(result).toContain('-05')
})

it('handles end-of-year dates', () => {
const input = '2022-12-31T12:00:00.000Z'
const result = formatDate(input)
expect(result).toMatch(/^\d{4}-12-31$/)
})

it('throws on invalid date', () => {
expect(() => formatDate('not-a-date')).toThrow(TypeError)
expect(() => formatDate('not-a-date')).toThrow('Invalid date value')
})

it('produces same output as the build-time copy', async () => {
// Guard against the two copies drifting apart.
const buildTime = await import('../../src/utils/content/transformers/utils')
const inputs = ['2022-06-15T12:00:00.000Z', '2023-01-01T00:00:00.000Z', '2024-12-31T23:59:59.000Z']
for (const input of inputs) {
expect(formatDate(input)).toBe(buildTime.formatDate(input))
}
})
})

describe('formatDateTime', () => {
it('formats a datetime string as YYYY-MM-DD HH:mm:ss', () => {
const input = '2022-06-15T14:30:45.000Z'
const result = formatDateTime(input)
expect(result).toMatch(/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/)
// The date portion must match formatDate
expect(result.split(' ')[0]).toBe(formatDate(input))
})

it('pads single-digit hours, minutes, and seconds', () => {
const result = formatDateTime('2022-01-01T01:02:03.000Z')
expect(result).toMatch(/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/)
})

it('throws on invalid datetime', () => {
expect(() => formatDateTime('garbage')).toThrow(TypeError)
expect(() => formatDateTime('garbage')).toThrow('Invalid datetime value')
})

it('produces same output as the build-time copy', async () => {
const buildTime = await import('../../src/utils/content/transformers/utils')
const inputs = ['2022-06-15T14:30:45.000Z', '2023-01-01T00:00:00.000Z']
for (const input of inputs) {
expect(formatDateTime(input)).toBe(buildTime.formatDateTime(input))
}
})
})
Loading