1
- import * as SourceFiles from '@contentlayer/source-files'
2
- import { casesHandled , isReadonlyArray , not , notImplemented , partition , pick } from '@contentlayer/utils'
3
- import { identity } from '@contentlayer/utils/effect'
1
+ import type { GetDocumentTypeNamesGen } from '@contentlayer/core'
2
+ import type * as SourceFiles from '@contentlayer/source-files'
3
+ import { defineDocumentType } from '@contentlayer/source-files'
4
+ import { not , partition } from '@contentlayer/utils'
4
5
import * as Stackbit from '@stackbit/sdk'
5
6
import { validateAndNormalizeConfig } from '@stackbit/sdk/dist/config/config-loader.js'
6
7
7
- export { addComputedFields } from './addComputedFields.js'
8
+ import type { SharedCtx } from './mapping.js'
9
+ import { stackbitDocumentLikeModelToDocumentType , stackbitObjectModelToDocumentType } from './mapping.js'
8
10
9
- type DocumentTypeMap = Record < string , SourceFiles . DocumentType >
10
- type NestedTypeMap = Record < string , SourceFiles . NestedType >
11
- type SharedCtx = {
12
- documentTypeMap : DocumentTypeMap
13
- nestedTypeMap : NestedTypeMap
11
+ export type ContentlayerOverrideArgs < TDocumentTypeNames extends string > = {
12
+ documentTypes : Partial < {
13
+ [ TDocumentTypeName in TDocumentTypeNames ] : ContentlayerOverrideDocumentType < TDocumentTypeName >
14
+ } >
15
+ }
16
+
17
+ export type ContentlayerOverrideDocumentType < TDocumentTypeName extends string > = {
18
+ filePathPattern ?: string
19
+ computedFields ?: SourceFiles . ComputedFields < TDocumentTypeName >
14
20
}
15
21
16
22
/**
17
23
* @example
18
24
* ```ts
25
+ * // contentlayer.config.ts
19
26
* import { makeSource } from 'contentlayer/source-files'
20
27
* import { loadStackbitConfigAsDocumentTypes } from '@contentlayer/experimental-source-files-stackbit'
21
28
*
29
+ * // Looks for `stackbit.yaml` in the current directory
22
30
* export default loadStackbitConfigAsDocumentTypes().then((documentTypes) => {
23
31
* return makeSource({ contentDirPath: 'content', documentTypes })
24
32
* })
25
33
* ```
26
34
*/
27
- export const loadStackbitConfigAsDocumentTypes = (
35
+ export const loadStackbitConfigAsDocumentTypes = < TDocumentTypeNames extends GetDocumentTypeNamesGen > (
28
36
options : Stackbit . ConfigLoaderOptions = { dirPath : '' } ,
37
+ overrideArgs : ContentlayerOverrideArgs < TDocumentTypeNames > = { documentTypes : { } } ,
29
38
) : Promise < SourceFiles . DocumentType [ ] > =>
30
39
Stackbit . loadConfig ( options ) . then ( ( configResult ) => {
31
40
if ( configResult . errors . length > 0 ) {
32
41
throw new Error ( configResult . errors . join ( '\n' ) )
33
42
}
34
43
35
- return stackbitConfigToDocumentTypes ( configResult . config ! )
44
+ return stackbitConfigToDocumentTypes ( configResult . config ! , overrideArgs )
36
45
} )
37
46
38
47
/**
39
48
*
40
49
* @example
41
50
* ```ts
51
+ * // contentlayer.config.ts
42
52
* import { makeSource } from 'contentlayer/source-files'
43
53
* import { stackbitConfigToDocumentTypes } from '@contentlayer/source-files-stackbit'
44
54
* import stackbitConfig from './stackbit.config.js'
@@ -48,8 +58,9 @@ export const loadStackbitConfigAsDocumentTypes = (
48
58
* export default makeSource({ contentDirPath: 'content', documentTypes })
49
59
* ```
50
60
*/
51
- export const stackbitConfigToDocumentTypes = (
61
+ export const stackbitConfigToDocumentTypes = < TDocumentTypeNames extends GetDocumentTypeNamesGen > (
52
62
stackbitConfig : Stackbit . Config | Stackbit . YamlConfig ,
63
+ overrideArgs : ContentlayerOverrideArgs < TDocumentTypeNames > = { documentTypes : { } } ,
53
64
) : SourceFiles . DocumentType [ ] => {
54
65
const validatedStackbitConfig = validateStackbitConfig ( stackbitConfig )
55
66
@@ -68,12 +79,26 @@ export const stackbitConfigToDocumentTypes = (
68
79
} )
69
80
70
81
documentTypes . forEach ( ( documentType ) => {
71
- ctx . documentTypeMap [ documentType . def ( ) . name ] = documentType
82
+ const documentTypeName = documentType . def ( ) . name
83
+ ctx . documentTypeMap [ documentTypeName ] = documentType
84
+
85
+ const documentOverride = ( overrideArgs . documentTypes as any ) [ documentTypeName ]
86
+ if ( documentOverride ) {
87
+ patchDocumentType ( documentType , documentOverride )
88
+ }
72
89
} )
73
90
74
91
return documentTypes
75
92
}
76
93
94
+ const patchDocumentType = (
95
+ documentType : SourceFiles . DocumentType ,
96
+ patch : Partial < SourceFiles . DocumentTypeDef > ,
97
+ ) : void => {
98
+ const previousDef = documentType . def ( )
99
+ documentType . def = defineDocumentType ( ( ) => ( { ...previousDef , ...patch } ) ) . def
100
+ }
101
+
77
102
const validateStackbitConfig = ( stackbitConfig : Stackbit . Config | Stackbit . YamlConfig ) : Stackbit . Config => {
78
103
if ( Array . isArray ( stackbitConfig . models ) ) {
79
104
return stackbitConfig as Stackbit . Config
@@ -88,190 +113,9 @@ const validateStackbitConfig = (stackbitConfig: Stackbit.Config | Stackbit.YamlC
88
113
return stackbitConfigResult . config
89
114
}
90
115
91
- const stackbitDocumentLikeModelToDocumentType =
92
- ( ctx : SharedCtx ) =>
93
- ( stackbitModel : Stackbit . PageModel | Stackbit . DataModel | Stackbit . ConfigModel ) : SourceFiles . DocumentType => {
94
- return SourceFiles . defineDocumentType ( ( ) => ( {
95
- name : stackbitModel . name ,
96
- description : stackbitModel . description ,
97
- fields : ( stackbitModel . fields ?? [ ] ) . map ( stackbitFieldToField ( ctx ) ) ,
98
- isSingleton : stackbitModel . type === 'config' || stackbitModel . singleInstance === true ,
99
- } ) )
100
- }
101
-
102
- const stackbitObjectModelToDocumentType =
103
- ( ctx : SharedCtx ) =>
104
- ( stackbitModel : Stackbit . ObjectModel ) : SourceFiles . NestedType => {
105
- return SourceFiles . defineNestedType ( ( ) => ( {
106
- name : stackbitModel . name ,
107
- description : stackbitModel . description ,
108
- fields : ( stackbitModel . fields ?? [ ] ) . map ( stackbitFieldToField ( ctx ) ) ,
109
- } ) )
110
- }
111
-
112
- const stackbitFieldToField =
113
- ( ctx : SharedCtx ) =>
114
- ( stackbitField : Stackbit . Field ) : SourceFiles . FieldDefWithName => {
115
- const commonFields = {
116
- ...pick ( stackbitField , [ 'name' , 'description' , 'required' ] ) ,
117
- default : stackbitField . default as any ,
118
- }
119
-
120
- type WithName < T > = T & { name : string }
121
-
122
- switch ( stackbitField . type ) {
123
- case 'boolean' :
124
- case 'number' :
125
- type FieldDef = SourceFiles . BooleanFieldDef | SourceFiles . NumberFieldDef
126
- return identity < WithName < FieldDef > > ( { ...commonFields , type : stackbitField . type } )
127
- case 'enum' :
128
- return identity < WithName < SourceFiles . EnumFieldDef > > ( {
129
- ...commonFields ,
130
- type : 'enum' ,
131
- options : stackbitField . options . map ( mapStackbitEnumOption ) ,
132
- } )
133
- case 'style' :
134
- return identity < WithName < SourceFiles . JSONFieldDef > > ( { ...commonFields , type : 'json' } )
135
- case 'list' : {
136
- const of = stackbitListItemToListFieldDef ( ctx ) ( stackbitField . items ! )
137
- return isReadonlyArray ( of )
138
- ? identity < WithName < SourceFiles . ListPolymorphicFieldDef > > ( {
139
- ...commonFields ,
140
- type : 'list' ,
141
- of,
142
- typeField : 'type' ,
143
- } )
144
- : identity < WithName < SourceFiles . ListFieldDef > > ( { ...commonFields , type : 'list' , of } )
145
- }
146
- case 'reference' : {
147
- const of = stackbitField . models . map ( ( modelName ) => ctx . documentTypeMap [ modelName ] ! )
148
- if ( of . length === 1 ) {
149
- return identity < WithName < SourceFiles . ReferenceFieldDef > > ( { ...commonFields , type : 'reference' , of : of [ 0 ] ! } )
150
- }
151
- return identity < WithName < SourceFiles . ReferencePolymorphicFieldDef > > ( {
152
- ...commonFields ,
153
- type : 'reference' ,
154
- of,
155
- typeField : 'type' ,
156
- } )
157
- }
158
- case 'model' : {
159
- const of = stackbitField . models . map ( ( modelName ) => ctx . nestedTypeMap [ modelName ] ! )
160
- if ( of . length === 1 ) {
161
- return identity < WithName < SourceFiles . NestedFieldDef > > ( { ...commonFields , type : 'nested' , of : of [ 0 ] ! } )
162
- }
163
- return identity < WithName < SourceFiles . NestedPolymorphicFieldDef > > ( {
164
- ...commonFields ,
165
- type : 'nested' ,
166
- of,
167
- typeField : 'type' ,
168
- } )
169
- }
170
- case 'object' : {
171
- const unnamedNestedTypeDef = identity < SourceFiles . NestedUnnamedTypeDef > ( {
172
- fields : stackbitField . fields . map ( stackbitFieldToField ( ctx ) ) ,
173
- } )
174
- return identity < WithName < SourceFiles . NestedFieldDef > > ( {
175
- ...commonFields ,
176
- type : 'nested' ,
177
- of : { type : 'nested' , def : ( ) => unnamedNestedTypeDef } ,
178
- } )
179
- }
180
- case 'markdown' :
181
- return identity < WithName < SourceFiles . MarkdownFieldDef > > ( { ...commonFields , type : 'markdown' } )
182
- case 'json' :
183
- 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' } )
189
- case 'string' :
190
- case 'url' :
191
- case 'text' :
192
- case 'color' :
193
- case 'slug' :
194
- case 'html' :
195
- case 'file' :
196
- return identity < WithName < SourceFiles . StringFieldDef > > ( { ...commonFields , type : 'string' } )
197
- case 'richText' :
198
- notImplemented ( `richText doesn't exist in the "files" content source` )
199
- default :
200
- casesHandled ( stackbitField )
201
- }
202
- }
203
-
204
- const stackbitListItemToListFieldDef =
205
- ( ctx : SharedCtx ) =>
206
- (
207
- stackbitListItem : Stackbit . FieldListItems ,
208
- ) : SourceFiles . ListFieldDefItem . Item | readonly SourceFiles . ListFieldDefItem . Item [ ] => {
209
- switch ( stackbitListItem . type ) {
210
- case 'boolean' :
211
- case 'string' :
212
- case 'number' :
213
- type Item =
214
- | SourceFiles . ListFieldDefItem . ItemString
215
- | SourceFiles . ListFieldDefItem . ItemBoolean
216
- | SourceFiles . ListFieldDefItem . ItemNumber
217
- return identity < Item > ( { type : stackbitListItem . type } )
218
- case 'enum' :
219
- return identity < SourceFiles . ListFieldDefItem . ItemEnum > ( {
220
- type : 'enum' ,
221
- options : stackbitListItem . options . map ( mapStackbitEnumOption ) ,
222
- } )
223
- case 'reference' :
224
- return firstArrayItemIfOne (
225
- stackbitListItem . models . map ( ( modelName ) =>
226
- identity < SourceFiles . ListFieldDefItem . ItemDocumentReference > ( ctx . documentTypeMap [ modelName ] ! ) ,
227
- ) ,
228
- )
229
- case 'model' :
230
- return firstArrayItemIfOne (
231
- stackbitListItem . models . map ( ( modelName ) =>
232
- identity < SourceFiles . ListFieldDefItem . ItemNestedType > ( ctx . nestedTypeMap [ modelName ] ! ) ,
233
- ) ,
234
- )
235
- case 'object' :
236
- return identity < SourceFiles . ListFieldDefItem . ItemNestedType > ( {
237
- type : 'nested' ,
238
- def : ( ) => ( { fields : stackbitListItem . fields . map ( stackbitFieldToField ( ctx ) ) } ) ,
239
- } )
240
- case 'date' :
241
- case 'datetime' :
242
- return identity < SourceFiles . ListFieldDefItem . ItemDate > ( { type : 'date' } )
243
- case 'json' :
244
- return identity < SourceFiles . ListFieldDefItem . ItemJSON > ( { type : 'json' } )
245
- case 'markdown' :
246
- return identity < SourceFiles . ListFieldDefItem . ItemMarkdown > ( { type : 'markdown' } )
247
- case 'image' :
248
- return identity < SourceFiles . ListFieldDefItem . ItemImage > ( { type : 'image' } )
249
- case 'url' :
250
- case 'text' :
251
- case 'color' :
252
- case 'slug' :
253
- case 'html' :
254
- case 'file' :
255
- return identity < SourceFiles . ListFieldDefItem . ItemString > ( { type : 'string' } )
256
- case 'richText' :
257
- notImplemented ( `richText doesn't exist in the "files" content source` )
258
- default :
259
- casesHandled ( stackbitListItem )
260
- }
261
- }
262
-
263
- const mapStackbitEnumOption = ( option : Stackbit . FieldEnumOptionValue | Stackbit . FieldEnumOptionObject ) : string => {
264
- if ( typeof option === 'string' || typeof option === 'number' ) {
265
- return option . toString ( )
266
- }
267
- return option . value . toString ( )
268
- }
269
-
270
116
const isDocumentLikeModel = (
271
117
model : Stackbit . Model ,
272
118
) : model is Stackbit . PageModel | Stackbit . DataModel | Stackbit . ConfigModel =>
273
119
model . type === 'data' || model . type === 'page' || model . type === 'config'
274
120
275
121
const isImageModel = ( model : Stackbit . Model ) : model is Stackbit . ImageModel => model . type === 'image'
276
-
277
- const firstArrayItemIfOne = < T > ( array : readonly T [ ] ) : T | readonly T [ ] => ( array . length === 1 ? array [ 0 ] ! : array )
0 commit comments