Skip to content

Commit 01ba1fc

Browse files
committed
Support for more field types in list elements & improved field validation
1 parent e80d78f commit 01ba1fc

File tree

24 files changed

+779
-481
lines changed

24 files changed

+779
-481
lines changed

.prettierignore

+1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
packages/_archive/*
22
**/.contentlayer/*
3+
**/fixtures/**/*.yaml
34
**/dist/*
45
**/.nyc_output/*

examples/node-script/contentlayer.config.ts

+1
Original file line numberDiff line numberDiff line change
@@ -26,4 +26,5 @@ const Post = defineDocumentType(() => ({
2626
export default makeSource({
2727
contentDirPath: 'posts',
2828
documentTypes: [Post],
29+
disableImportAliasWarning: true,
2930
})

packages/@contentlayer/core/src/data-types.ts

+24
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,16 @@ export type MDX = {
2727
code: string
2828
}
2929

30+
/**
31+
* ISO 8601 Date string
32+
*
33+
* @example '2021-01-01T00:00:00.000Z'
34+
*
35+
* See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toISOString
36+
*
37+
*/
38+
export type IsoDateTimeString = string
39+
3040
export type ImageFieldData = {
3141
/** Image file path relative to `contentDirPath` */
3242
filePath: string
@@ -36,4 +46,18 @@ export type ImageFieldData = {
3646
height: number
3747
format: string
3848
blurhashDataUrl: string
49+
/**
50+
* Alt text for the image
51+
*
52+
* Can be provided via ...
53+
* - JSON: `"myImageField": { "alt": "My alt text", "src": "my-image.jpg" }`
54+
* - YAML / Frontmatter:
55+
* ```yaml
56+
* # ...
57+
* myImageField:
58+
* alt: My alt text
59+
* src: my-image.jpg
60+
* ```
61+
*/
62+
alt?: string
3963
}

packages/@contentlayer/core/src/generation/generate-types.ts

+14-4
Original file line numberDiff line numberDiff line change
@@ -50,13 +50,12 @@ export const renderTypes = ({
5050
return `\
5151
// ${autogeneratedNote}
5252
53-
import type { Markdown, MDX, ImageFieldData } from 'contentlayer/core'
53+
import type { Markdown, MDX, ImageFieldData, IsoDateTimeString } from 'contentlayer/core'
5454
${importsForRawTypes}
5555
5656
export { isType } from 'contentlayer/client'
5757
58-
// export type Image = string
59-
export type { Markdown, MDX }
58+
export type { Markdown, MDX, ImageFieldData, IsoDateTimeString }
6059
6160
/** Document types */
6261
${documentTypes.map(prop('typeDef')).join('\n\n')}
@@ -177,7 +176,7 @@ const renderFieldType = (field: FieldDef): string => {
177176
case 'json':
178177
return 'any'
179178
case 'date':
180-
return 'string'
179+
return 'IsoDateTimeString'
181180
// TODO but requires schema knowledge in the client
182181
// return 'Date'
183182
case 'markdown':
@@ -225,7 +224,18 @@ const renderListItemFieldType = (item: ListFieldDefItem.Item): string => {
225224
switch (item.type) {
226225
case 'boolean':
227226
case 'string':
227+
case 'number':
228228
return item.type
229+
case 'json':
230+
return 'any'
231+
case 'date':
232+
return 'string'
233+
case 'markdown':
234+
return 'Markdown'
235+
case 'mdx':
236+
return 'MDX'
237+
case 'image':
238+
return 'ImageFieldData'
229239
case 'nested':
230240
return item.nestedTypeName
231241
case 'enum':

packages/@contentlayer/core/src/schema/field.ts

+22-4
Original file line numberDiff line numberDiff line change
@@ -53,15 +53,33 @@ export interface ListPolymorphicFieldDef extends FieldDefBase {
5353
}
5454

5555
export namespace ListFieldDefItem {
56-
export type Item = ItemString | ItemEnum | ItemBoolean | ItemNested | ItemNestedUnnamed | ItemReference
56+
export type Item =
57+
| ItemString
58+
| ItemNumber
59+
| ItemBoolean
60+
| ItemJSON
61+
| ItemDate
62+
| ItemMarkdown
63+
| ItemMDX
64+
| ItemImage
65+
| ItemEnum
66+
| ItemNested
67+
| ItemNestedUnnamed
68+
| ItemDocumentReference
5769

5870
type BaseItem = {
5971
// labelField: string | undefined
6072
}
6173

6274
export type ItemString = BaseItem & { type: 'string' }
63-
export type ItemEnum = BaseItem & { type: 'enum'; options: readonly string[] }
75+
export type ItemNumber = BaseItem & { type: 'number' }
6476
export type ItemBoolean = BaseItem & { type: 'boolean' }
77+
export type ItemJSON = BaseItem & { type: 'json' }
78+
export type ItemDate = BaseItem & { type: 'date' }
79+
export type ItemMarkdown = BaseItem & { type: 'markdown' }
80+
export type ItemMDX = BaseItem & { type: 'mdx' }
81+
export type ItemImage = BaseItem & { type: 'image' }
82+
export type ItemEnum = BaseItem & { type: 'enum'; options: readonly string[] }
6583
export type ItemNested = BaseItem & {
6684
type: 'nested'
6785
nestedTypeName: string
@@ -73,7 +91,7 @@ export namespace ListFieldDefItem {
7391

7492
export const isDefItemNested = (_: Item): _ is ItemNested => _.type === 'nested'
7593

76-
export type ItemReference = BaseItem & {
94+
export type ItemDocumentReference = BaseItem & {
7795
type: 'reference'
7896
documentTypeName: string
7997

@@ -86,7 +104,7 @@ export namespace ListFieldDefItem {
86104
embedDocument: boolean
87105
}
88106

89-
export const isDefItemReference = (_: Item): _ is ItemReference => _.type === 'reference'
107+
export const isDefItemReference = (_: Item): _ is ItemDocumentReference => _.type === 'reference'
90108
}
91109

92110
export type StringFieldDef = FieldDefBase & {

packages/@contentlayer/experimental-source-files-stackbit/src/__test__/__snapshots__/stackbit.spec.ts.snap

+8-8
Original file line numberDiff line numberDiff line change
@@ -285,7 +285,7 @@ exports[`small-business 1`] = `
285285
"isRequired": false,
286286
"isSystemField": false,
287287
"name": "favicon",
288-
"type": "string",
288+
"type": "image",
289289
},
290290
{
291291
"default": undefined,
@@ -319,7 +319,7 @@ exports[`small-business 1`] = `
319319
"isRequired": false,
320320
"isSystemField": false,
321321
"name": "defaultSocialImage",
322-
"type": "string",
322+
"type": "image",
323323
},
324324
{
325325
"default": undefined,
@@ -389,7 +389,7 @@ exports[`small-business 1`] = `
389389
"isRequired": false,
390390
"isSystemField": false,
391391
"name": "socialImage",
392-
"type": "string",
392+
"type": "image",
393393
},
394394
{
395395
"default": undefined,
@@ -665,7 +665,7 @@ exports[`small-business 1`] = `
665665
"isRequired": false,
666666
"isSystemField": false,
667667
"name": "socialImage",
668-
"type": "string",
668+
"type": "image",
669669
},
670670
{
671671
"default": undefined,
@@ -933,7 +933,7 @@ exports[`small-business 1`] = `
933933
"isRequired": true,
934934
"isSystemField": false,
935935
"name": "date",
936-
"type": "string",
936+
"type": "date",
937937
},
938938
{
939939
"default": undefined,
@@ -1061,7 +1061,7 @@ exports[`small-business 1`] = `
10611061
"isRequired": false,
10621062
"isSystemField": false,
10631063
"name": "socialImage",
1064-
"type": "string",
1064+
"type": "image",
10651065
},
10661066
{
10671067
"default": undefined,
@@ -2061,7 +2061,7 @@ exports[`small-business 1`] = `
20612061
"isRequired": true,
20622062
"isSystemField": false,
20632063
"name": "url",
2064-
"type": "string",
2064+
"type": "image",
20652065
},
20662066
{
20672067
"default": "cover",
@@ -4172,7 +4172,7 @@ vitae interdum. Ut nec massa eget lorem blandit condimentum et at risus.",
41724172
"isRequired": true,
41734173
"isSystemField": false,
41744174
"name": "url",
4175-
"type": "string",
4175+
"type": "image",
41764176
},
41774177
{
41784178
"default": "altText of the image",

packages/@contentlayer/experimental-source-files-stackbit/src/index.ts

+6-4
Original file line numberDiff line numberDiff line change
@@ -181,19 +181,21 @@ const stackbitFieldToField =
181181
return identity<WithName<SourceFiles.MarkdownFieldDef>>({ ...commonFields, type: 'markdown' })
182182
case 'json':
183183
return identity<WithName<SourceFiles.JSONFieldDef>>({ ...commonFields, type: 'json' })
184+
case 'image':
185+
return identity<WithName<SourceFiles.ImageFieldDef>>({ ...commonFields, type: 'image' })
186+
case 'datetime':
187+
case 'date':
188+
return identity<WithName<SourceFiles.DateFieldDef>>({ ...commonFields, type: 'date' })
184189
case 'string':
185190
case 'url':
186191
case 'text':
187-
case 'image': // TODO altText
188192
case 'color':
189193
case 'slug':
190194
case 'html':
191-
case 'date':
192195
case 'file':
193-
case 'datetime':
194196
return identity<WithName<SourceFiles.StringFieldDef>>({ ...commonFields, type: 'string' })
195197
case 'richText':
196-
notImplemented(`richText doesn't exist in files content source`)
198+
notImplemented(`richText doesn't exist in the "files" content source`)
197199
default:
198200
casesHandled(stackbitField)
199201
}

packages/@contentlayer/source-files/package.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,8 @@
3737
"micromatch": "^4.0.5",
3838
"ts-pattern": "^4.0.5",
3939
"unified": "^10.1.2",
40-
"yaml": "^1.10.2"
40+
"yaml": "^1.10.2",
41+
"zod": "^3.18.0"
4142
},
4243
"devDependencies": {
4344
"@types/faker": "^5.5.8",

packages/@contentlayer/source-files/src/__test__/mapping.spec.ts

+5-5
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import { describe, expect, test } from 'vitest'
88

99
import type { HasDocumentContext } from '../fetchData/DocumentContext.js'
1010
import { provideDocumentContext } from '../fetchData/DocumentContext.js'
11-
import { getFlattenedPath, testOnly_getDataForFieldDef as getDataForFieldDef } from '../fetchData/mapping.js'
11+
import { getFlattenedPath, testOnly_getDataForFieldDef as getDataForFieldDef } from '../fetchData/mapping/index.js'
1212

1313
test('getFlattenedPath', () => {
1414
expect(getFlattenedPath('some/path/doc.md')).toBe('some/path/doc')
@@ -35,7 +35,7 @@ describe('getDataForFieldDef', () => {
3535
const transformedData = await pipe(
3636
getDataForFieldDef({
3737
rawFieldData,
38-
typeName: __unusedValue,
38+
documentTypeName: __unusedValue,
3939
coreSchemaDef: { hash: '', documentTypeDefMap: {}, nestedTypeDefMap: {} },
4040
contentDirPath: __unusedValue,
4141
fieldDef: {
@@ -46,7 +46,7 @@ describe('getDataForFieldDef', () => {
4646
default: undefined,
4747
description: undefined,
4848
},
49-
relativeFilePath: __unusedValue,
49+
documentFilePath: __unusedValue,
5050
options: {
5151
fieldOptions: core.defaultFieldOptions,
5252
markdown: undefined,
@@ -111,7 +111,7 @@ test('getDataForFieldDef error', async () => {
111111
pipe(
112112
getDataForFieldDef({
113113
rawFieldData,
114-
typeName: 'Post',
114+
documentTypeName: 'Post',
115115
coreSchemaDef: { hash: '', documentTypeDefMap: {}, nestedTypeDefMap: {} },
116116
contentDirPath: __unusedValue,
117117
fieldDef: {
@@ -122,7 +122,7 @@ test('getDataForFieldDef error', async () => {
122122
default: undefined,
123123
description: undefined,
124124
},
125-
relativeFilePath: unknownToRelativePosixFilePath('some/path/doc.md'),
125+
documentFilePath: unknownToRelativePosixFilePath('some/path/doc.md'),
126126
options: {
127127
fieldOptions: core.defaultFieldOptions,
128128
markdown: undefined,

packages/@contentlayer/source-files/src/__test__/type-generation/__snapshots__/basic.spec.ts.snap

+8-11
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,12 @@
33
exports[`generate-types: references with embedded schema 1`] = `
44
"// NOTE This file is auto-generated by Contentlayer
55
6-
import type { Markdown, MDX, ImageFieldData } from 'contentlayer/core'
6+
import type { Markdown, MDX, ImageFieldData, IsoDateTimeString } from 'contentlayer/core'
77
import * as Local from 'contentlayer/source-files'
88
99
export { isType } from 'contentlayer/client'
1010
11-
// export type Image = string
12-
export type { Markdown, MDX }
11+
export type { Markdown, MDX, ImageFieldData, IsoDateTimeString }
1312
1413
/** Document types */
1514
export type Person = {
@@ -79,13 +78,12 @@ export type NestedTypeMap = {
7978
exports[`generate-types: simple schema 1`] = `
8079
"// NOTE This file is auto-generated by Contentlayer
8180
82-
import type { Markdown, MDX, ImageFieldData } from 'contentlayer/core'
81+
import type { Markdown, MDX, ImageFieldData, IsoDateTimeString } from 'contentlayer/core'
8382
import * as Local from 'contentlayer/source-files'
8483
8584
export { isType } from 'contentlayer/client'
8685
87-
// export type Image = string
88-
export type { Markdown, MDX }
86+
export type { Markdown, MDX, ImageFieldData, IsoDateTimeString }
8987
9088
/** Document types */
9189
export type TestPost = {
@@ -96,7 +94,7 @@ export type TestPost = {
9694
/** The title of the post */
9795
title: string
9896
/** The date of the post */
99-
date: string
97+
date: IsoDateTimeString
10098
/** Markdown file body */
10199
body: Markdown
102100
slug: string
@@ -145,13 +143,12 @@ export type NestedTypeMap = {
145143
exports[`generate-types: simple schema with optional fields 1`] = `
146144
"// NOTE This file is auto-generated by Contentlayer
147145
148-
import type { Markdown, MDX, ImageFieldData } from 'contentlayer/core'
146+
import type { Markdown, MDX, ImageFieldData, IsoDateTimeString } from 'contentlayer/core'
149147
import * as Local from 'contentlayer/source-files'
150148
151149
export { isType } from 'contentlayer/client'
152150
153-
// export type Image = string
154-
export type { Markdown, MDX }
151+
export type { Markdown, MDX, ImageFieldData, IsoDateTimeString }
155152
156153
/** Document types */
157154
export type TestPost = {
@@ -162,7 +159,7 @@ export type TestPost = {
162159
/** The title of the post */
163160
title: string
164161
/** The date of the post */
165-
date?: string | undefined
162+
date?: IsoDateTimeString | undefined
166163
/** Markdown file body */
167164
body: Markdown
168165
slug: string

packages/@contentlayer/source-files/src/fetchData/DocumentContext.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { T, tag } from '@contentlayer/utils/effect'
77

88
import type { DocumentContentType } from '../schema/defs/index.js'
99
import type { RawDocumentData } from '../types.js'
10-
import { getFlattenedPath } from './mapping.js'
10+
import { getFlattenedPath } from './mapping/index.js'
1111
import type { RawContent } from './types.js'
1212

1313
/** `DocumentContext` is meant as a "container object" that provides useful information when processing a document */

0 commit comments

Comments
 (0)