@stackbit/sdk 0.2.22 → 0.2.23

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.
Files changed (36) hide show
  1. package/dist/config/config-consts.d.ts +1 -1
  2. package/dist/config/config-consts.js +2 -0
  3. package/dist/config/config-consts.js.map +1 -1
  4. package/dist/config/config-loader.d.ts +3 -2
  5. package/dist/config/config-loader.js +248 -71
  6. package/dist/config/config-loader.js.map +1 -1
  7. package/dist/config/config-schema.js +27 -3
  8. package/dist/config/config-schema.js.map +1 -1
  9. package/dist/config/config-types.d.ts +21 -4
  10. package/dist/config/config-writer.js +3 -0
  11. package/dist/config/config-writer.js.map +1 -1
  12. package/dist/config/presets-loader.js +17 -11
  13. package/dist/config/presets-loader.js.map +1 -1
  14. package/dist/content/content-loader.js +1 -1
  15. package/dist/content/content-schema.js +8 -0
  16. package/dist/content/content-schema.js.map +1 -1
  17. package/dist/utils/model-extender.js.map +1 -1
  18. package/dist/utils/model-iterators.d.ts +61 -1
  19. package/dist/utils/model-iterators.js +60 -11
  20. package/dist/utils/model-iterators.js.map +1 -1
  21. package/dist/utils/model-utils.d.ts +44 -3
  22. package/dist/utils/model-utils.js +93 -10
  23. package/dist/utils/model-utils.js.map +1 -1
  24. package/package.json +2 -2
  25. package/src/.DS_Store +0 -0
  26. package/src/config/config-consts.ts +2 -0
  27. package/src/config/config-loader.ts +272 -82
  28. package/src/config/config-schema.ts +34 -4
  29. package/src/config/config-types.ts +25 -4
  30. package/src/config/config-writer.ts +3 -0
  31. package/src/config/presets-loader.ts +18 -15
  32. package/src/content/content-loader.ts +2 -2
  33. package/src/content/content-schema.ts +9 -0
  34. package/src/utils/model-extender.ts +1 -1
  35. package/src/utils/model-iterators.ts +61 -13
  36. package/src/utils/model-utils.ts +91 -8
@@ -9,7 +9,7 @@ import { ConfigError, ConfigLoadError, ConfigValidationError } from './config-er
9
9
  import {
10
10
  assignLabelFieldIfNeeded,
11
11
  extendModelMap,
12
- getListItemsField,
12
+ getListFieldItems,
13
13
  isCustomModelField,
14
14
  isEnumField,
15
15
  isListDataModel,
@@ -20,13 +20,17 @@ import {
20
20
  isPageModel,
21
21
  isDataModel,
22
22
  isReferenceField,
23
- iterateModelFieldsRecursively
23
+ iterateModelFieldsRecursively,
24
+ mapModelFieldsRecursively,
25
+ normalizeListFieldInPlace,
26
+ getModelFieldForModelKeyPath
24
27
  } from '../utils';
25
- import { append, parseFile, readDirRecursively, reducePromise, rename } from '@stackbit/utils';
28
+ import { append, omitByNil, parseFile, readDirRecursively, reducePromise, rename } from '@stackbit/utils';
26
29
  import { Config, DataModel, FieldEnum, FieldModel, FieldObjectProps, Model, PageModel, YamlModel } from './config-types';
27
30
  import { loadPresets } from './presets-loader';
28
31
 
29
32
  export interface ConfigLoaderOptions {
33
+ [option: string]: any;
30
34
  dirPath: string;
31
35
  }
32
36
 
@@ -43,11 +47,11 @@ export interface NormalizedValidationResult {
43
47
  }
44
48
 
45
49
  export interface TempConfigLoaderResult {
46
- config?: any;
50
+ config?: Record<string, unknown>;
47
51
  errors: ConfigLoadError[];
48
52
  }
49
53
 
50
- export async function loadConfig({ dirPath }: ConfigLoaderOptions): Promise<ConfigLoaderResult> {
54
+ export async function loadConfig({ dirPath, ...options }: ConfigLoaderOptions): Promise<ConfigLoaderResult> {
51
55
  let configLoadResult: TempConfigLoaderResult;
52
56
  try {
53
57
  configLoadResult = await loadConfigFromDir(dirPath);
@@ -67,14 +71,18 @@ export async function loadConfig({ dirPath }: ConfigLoaderOptions): Promise<Conf
67
71
  };
68
72
  }
69
73
 
70
- const normalizedResult = validateAndNormalizeConfig(configLoadResult.config);
74
+ const externalModelsResult = await loadModelsFromExternalSource(configLoadResult.config, options);
75
+
76
+ const mergedConfig = mergeConfigWithExternalModels(configLoadResult.config, externalModelsResult.models);
77
+
78
+ const normalizedResult = validateAndNormalizeConfig(mergedConfig);
71
79
 
72
80
  const presetsResult = await loadPresets(dirPath, normalizedResult.config);
73
81
 
74
82
  return {
75
83
  valid: normalizedResult.valid,
76
84
  config: presetsResult.config,
77
- errors: [...configLoadResult.errors, ...normalizedResult.errors, ...presetsResult.errors]
85
+ errors: [...configLoadResult.errors, ...externalModelsResult.errors, ...normalizedResult.errors, ...presetsResult.errors]
78
86
  };
79
87
  }
80
88
 
@@ -110,14 +118,14 @@ async function loadConfigFromDir(dirPath: string): Promise<TempConfigLoaderResul
110
118
  if (error) {
111
119
  return { errors: [error] };
112
120
  }
113
- const externalModelsResult = await loadExternalModels(dirPath, config);
114
- config.models = _.assign(externalModelsResult.models, config.models);
115
- return { config, errors: externalModelsResult.errors };
121
+ const modelsFromFileResult = await loadModelsFromFiles(dirPath, config);
122
+ const mergedConfig = mergeConfigModelsWithModelsFromFiles(config, modelsFromFileResult.models);
123
+ return { config: mergedConfig, errors: modelsFromFileResult.errors };
116
124
  }
117
125
 
118
- type LoadConfigFromStackbitYamlResult = { config: any; error?: undefined } | { config?: undefined; error: ConfigLoadError };
126
+ type StackbitYamlConfigResult = { config: any; error?: undefined } | { config?: undefined; error: ConfigLoadError };
119
127
 
120
- async function loadConfigFromStackbitYaml(dirPath: string): Promise<LoadConfigFromStackbitYamlResult> {
128
+ async function loadConfigFromStackbitYaml(dirPath: string): Promise<StackbitYamlConfigResult> {
121
129
  const stackbitYamlPath = path.join(dirPath, 'stackbit.yaml');
122
130
  const stackbitYamlExists = await fse.pathExists(stackbitYamlPath);
123
131
  if (!stackbitYamlExists) {
@@ -135,54 +143,56 @@ async function loadConfigFromStackbitYaml(dirPath: string): Promise<LoadConfigFr
135
143
  return { config };
136
144
  }
137
145
 
138
- async function loadExternalModels(dirPath: string, config: any) {
146
+ async function loadModelsFromFiles(dirPath: string, config: any): Promise<{ models: Record<string, any>; errors: ConfigLoadError[] }> {
139
147
  const modelsSource = _.get(config, 'modelsSource', {});
140
148
  const sourceType = _.get(modelsSource, 'type', 'files');
141
- if (sourceType === 'files') {
142
- const defaultModelDirs = ['node_modules/@stackbit/components/models', '.stackbit/models'];
143
- const modelDirs = _.castArray(_.get(modelsSource, 'modelDirs', defaultModelDirs)).map((modelDir: string) => _.trim(modelDir, '/'));
144
- const modelFiles = await reducePromise(
145
- modelDirs,
146
- async (modelFiles: string[], modelDir) => {
147
- const absModelsDir = path.join(dirPath, modelDir);
148
- const dirExists = await fse.pathExists(absModelsDir);
149
- if (!dirExists) {
150
- return modelFiles;
151
- }
152
- const files = await readModelFilesFromDir(absModelsDir);
153
- return modelFiles.concat(files.map((filePath) => path.join(modelDir, filePath)));
154
- },
155
- []
156
- );
157
- return reducePromise(
158
- modelFiles,
159
- async (result: { models: any; errors: ConfigLoadError[] }, modelFile) => {
160
- let model;
161
- try {
162
- model = await parseFile(path.join(dirPath, modelFile));
163
- } catch (error) {
164
- return {
165
- models: result.models,
166
- errors: result.errors.concat(new ConfigLoadError(`error parsing model, file: ${modelFile}`))
167
- };
168
- }
169
- const modelName = model?.name;
170
- if (!modelName) {
171
- return {
172
- models: result.models,
173
- errors: result.errors.concat(new ConfigLoadError(`model does not have a name, file: ${modelFile}`))
174
- };
175
- }
176
- result.models[modelName] = _.omit(model, 'name');
177
- result.models[modelName].__metadata = {
178
- filePath: modelFile
149
+ const defaultModelDirs = ['node_modules/@stackbit/components/models', '.stackbit/models'];
150
+ const modelDirs =
151
+ sourceType === 'files'
152
+ ? _.castArray(_.get(modelsSource, 'modelDirs', defaultModelDirs)).map((modelDir: string) => _.trim(modelDir, '/'))
153
+ : defaultModelDirs;
154
+
155
+ const modelFiles = await reducePromise(
156
+ modelDirs,
157
+ async (modelFiles: string[], modelDir) => {
158
+ const absModelsDir = path.join(dirPath, modelDir);
159
+ const dirExists = await fse.pathExists(absModelsDir);
160
+ if (!dirExists) {
161
+ return modelFiles;
162
+ }
163
+ const files = await readModelFilesFromDir(absModelsDir);
164
+ return modelFiles.concat(files.map((filePath) => path.join(modelDir, filePath)));
165
+ },
166
+ []
167
+ );
168
+
169
+ return reducePromise(
170
+ modelFiles,
171
+ async (result: { models: any; errors: ConfigLoadError[] }, modelFile) => {
172
+ let model;
173
+ try {
174
+ model = await parseFile(path.join(dirPath, modelFile));
175
+ } catch (error) {
176
+ return {
177
+ models: result.models,
178
+ errors: result.errors.concat(new ConfigLoadError(`error parsing model, file: ${modelFile}`))
179
179
  };
180
- return result;
181
- },
182
- { models: {}, errors: [] }
183
- );
184
- }
185
- return { models: {}, errors: [] };
180
+ }
181
+ const modelName = model?.name;
182
+ if (!modelName) {
183
+ return {
184
+ models: result.models,
185
+ errors: result.errors.concat(new ConfigLoadError(`model does not have a name, file: ${modelFile}`))
186
+ };
187
+ }
188
+ result.models[modelName] = _.omit(model, 'name');
189
+ result.models[modelName].__metadata = {
190
+ filePath: modelFile
191
+ };
192
+ return result;
193
+ },
194
+ { models: {}, errors: [] }
195
+ );
186
196
  }
187
197
 
188
198
  async function readModelFilesFromDir(modelsDir: string) {
@@ -197,6 +207,33 @@ async function readModelFilesFromDir(modelsDir: string) {
197
207
  });
198
208
  }
199
209
 
210
+ async function loadModelsFromExternalSource(config: any, options: any): Promise<{ models: Model[]; errors: ConfigLoadError[] }> {
211
+ const modelsSource = _.get(config, 'modelsSource', {});
212
+ const sourceType = _.get(modelsSource, 'type', 'files');
213
+ if (sourceType === 'files') {
214
+ return { models: [], errors: [] };
215
+ } else if (sourceType === 'contentful') {
216
+ const contentfulModule = _.get(modelsSource, 'module', '@stackbit/cms-contentful');
217
+ const module = await import(contentfulModule);
218
+ try {
219
+ const { models } = await module.fetchAndConvertSchema(options);
220
+ return {
221
+ models: models,
222
+ errors: []
223
+ };
224
+ } catch (error) {
225
+ return {
226
+ models: [],
227
+ errors: [new ConfigLoadError(`Error fetching and converting Contentful schema, error: ${error.message}`, { originalError: error })]
228
+ };
229
+ }
230
+ }
231
+ return {
232
+ models: [],
233
+ errors: [new ConfigLoadError(`modelsSource ${modelsSource} is unsupported`)]
234
+ };
235
+ }
236
+
200
237
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
201
238
  async function loadConfigFromDotStackbit(dirPath: string) {
202
239
  const stackbitDotPath = path.join(dirPath, '.stackbit');
@@ -231,6 +268,101 @@ async function loadConfigFromDotStackbit(dirPath: string) {
231
268
  return _.isEmpty(config) ? null : config;
232
269
  }
233
270
 
271
+ function mergeConfigModelsWithModelsFromFiles(config: any, modelsFromFiles: Record<string, any>) {
272
+ const configModels = config.models ?? {};
273
+ const mergedModels = _.mapValues(modelsFromFiles, (modelFromFile, modelName) => {
274
+ // resolve thumbnails of models loaded from files
275
+ const modelFilePath = modelFromFile.__metadata?.filePath;
276
+ resolveThumbnailPathForModel(modelFromFile, modelFilePath);
277
+ iterateModelFieldsRecursively(modelFromFile, (field: any) => {
278
+ if (isListField(field)) {
279
+ field = normalizeListFieldInPlace(field);
280
+ field = field.items;
281
+ }
282
+ if (isObjectField(field)) {
283
+ resolveThumbnailPathForModel(field, modelFilePath);
284
+ } else if (isEnumField(field)) {
285
+ resolveThumbnailPathForEnumField(field, modelFilePath);
286
+ }
287
+ });
288
+
289
+ const configModel = configModels[modelName];
290
+ if (!configModel) {
291
+ return modelFromFile;
292
+ }
293
+
294
+ return _.assign({}, modelFromFile, configModel, {
295
+ fields: _.unionBy(configModel?.fields ?? [], modelFromFile?.fields ?? [], 'name')
296
+ });
297
+ });
298
+ return {
299
+ ...config,
300
+ models: Object.assign({}, configModels, mergedModels)
301
+ };
302
+ }
303
+
304
+ function mergeConfigWithExternalModels(config: any, externalModels?: Model[]) {
305
+ if (!externalModels || externalModels.length === 0) {
306
+ return config;
307
+ }
308
+
309
+ const stackbitModels = config?.models ?? {};
310
+
311
+ externalModels = externalModels.map((externalModel) => {
312
+ const stackbitModel = stackbitModels[externalModel.name];
313
+ if (!stackbitModel) {
314
+ return externalModel;
315
+ }
316
+
317
+ const modelType = stackbitModel.type ? (stackbitModel.type === 'config' ? 'data' : stackbitModel.type) : 'object';
318
+ const urlPath = modelType === 'page' ? stackbitModel?.urlPath ?? '/{slug}' : null;
319
+ const fieldGroups = stackbitModel?.fieldGroups;
320
+
321
+ externalModel = Object.assign(
322
+ externalModel,
323
+ omitByNil({
324
+ __metadata: stackbitModel.__metadata,
325
+ type: modelType,
326
+ urlPath,
327
+ fieldGroups
328
+ })
329
+ );
330
+
331
+ return mapModelFieldsRecursively(externalModel, (field, modelKeyPath) => {
332
+ const stackbitField = getModelFieldForModelKeyPath(stackbitModel, modelKeyPath);
333
+ if (!stackbitField) {
334
+ return field;
335
+ }
336
+
337
+ const group = 'group' in stackbitField ? stackbitField.group : null;
338
+ const controlType = 'controlType' in stackbitField ? stackbitField.controlType : null;
339
+ let override = {};
340
+
341
+ if (stackbitField?.type === 'style') {
342
+ override = stackbitField;
343
+ }
344
+
345
+ return Object.assign(
346
+ {},
347
+ field,
348
+ omitByNil({
349
+ group,
350
+ controlType
351
+ }),
352
+ override
353
+ );
354
+ });
355
+ });
356
+
357
+ return {
358
+ ...config,
359
+ models: _.mapValues(_.keyBy(externalModels, 'name'), (model) => {
360
+ _.unset(model, 'name');
361
+ return model;
362
+ })
363
+ };
364
+ }
365
+
234
366
  function normalizeConfig(config: any): any {
235
367
  const pageLayoutKey = _.get(config, 'pageLayoutKey', 'layout');
236
368
  const objectTypeKey = _.get(config, 'objectTypeKey', 'type');
@@ -238,6 +370,7 @@ function normalizeConfig(config: any): any {
238
370
  const ver = semver.coerce(stackbitYamlVersion);
239
371
  const isStackbitYamlV2 = ver ? semver.satisfies(ver, '<0.3.0') : false;
240
372
  const models = config?.models || {};
373
+ const gitCMS = isGitCMS(config);
241
374
  let referencedModelNames: string[] = [];
242
375
 
243
376
  _.forEach(models, (model, modelName) => {
@@ -245,6 +378,10 @@ function normalizeConfig(config: any): any {
245
378
  return;
246
379
  }
247
380
 
381
+ if (!_.has(model, 'type')) {
382
+ model.type = 'object';
383
+ }
384
+
248
385
  // add model label if not set
249
386
  if (!_.has(model, 'label')) {
250
387
  model.label = _.startCase(modelName);
@@ -259,13 +396,15 @@ function normalizeConfig(config: any): any {
259
396
  rename(model, 'template', 'layout');
260
397
 
261
398
  updatePageUrlPath(model);
262
- updatePageFilePath(model, config);
263
399
 
264
- addMarkdownContentField(model);
400
+ if (gitCMS) {
401
+ updatePageFilePath(model, config);
402
+ addMarkdownContentField(model);
265
403
 
266
- // TODO: update schema-editor to not show layout field
267
- addLayoutFieldToPageModel(model, pageLayoutKey);
268
- } else if (isDataModel(model)) {
404
+ // TODO: update schema-editor to not show layout field
405
+ addLayoutFieldToPageModel(model, pageLayoutKey);
406
+ }
407
+ } else if (isDataModel(model) && gitCMS) {
269
408
  updateDataFilePath(model, config);
270
409
  }
271
410
 
@@ -281,8 +420,6 @@ function normalizeConfig(config: any): any {
281
420
  assignLabelFieldIfNeeded(model);
282
421
  }
283
422
 
284
- resolveThumbnailPathForModel(model, model?.__metadata?.filePath);
285
-
286
423
  iterateModelFieldsRecursively(model, (field: any) => {
287
424
  // add field label if label is not set
288
425
  if (!_.has(field, 'label')) {
@@ -290,18 +427,12 @@ function normalizeConfig(config: any): any {
290
427
  }
291
428
 
292
429
  if (isListField(field)) {
293
- // 'items.type' of list field default to 'string', set it explicitly
294
- if (!_.has(field, 'items.type')) {
295
- _.set(field, 'items.type', 'string');
296
- }
297
- field = getListItemsField(field);
430
+ field = normalizeListFieldInPlace(field);
431
+ field = field.items;
298
432
  }
299
433
 
300
434
  if (isObjectField(field)) {
301
435
  assignLabelFieldIfNeeded(field);
302
- resolveThumbnailPathForModel(field, model?.__metadata?.filePath);
303
- } else if (isEnumField(field)) {
304
- resolveThumbnailPathForEnumField(field, model?.__metadata?.filePath);
305
436
  } else if (isCustomModelField(field, models)) {
306
437
  // stackbit v0.2.0 compatibility
307
438
  // convert the old custom model field type: { type: 'action' }
@@ -331,7 +462,9 @@ function normalizeConfig(config: any): any {
331
462
  }
332
463
  }
333
464
 
334
- referencedModelNames = _.union(referencedModelNames, getReferencedModelNames(field));
465
+ if (gitCMS) {
466
+ referencedModelNames = _.union(referencedModelNames, getReferencedModelNames(field));
467
+ }
335
468
  });
336
469
  });
337
470
 
@@ -488,7 +621,7 @@ function resolveThumbnailPath(thumbnail: string, modelDirPath: string) {
488
621
  */
489
622
  function getReferencedModelNames(field: any) {
490
623
  if (isListField(field)) {
491
- field = getListItemsField(field);
624
+ field = getListFieldItems(field);
492
625
  }
493
626
  // TODO: add type field to model fields inside container update/create object logic rather adding type to schema
494
627
  // 'object' models referenced by 'model' fields should have 'type' field
@@ -509,7 +642,10 @@ function validateAndExtendContentModels(config: any): ConfigValidationResult {
509
642
  const contentModels = config.contentModels ?? {};
510
643
  const models = config.models ?? {};
511
644
 
512
- if (_.isEmpty(contentModels)) {
645
+ const externalModels = !isGitCMS(config);
646
+ const emptyContentModels = _.isEmpty(contentModels);
647
+
648
+ if (externalModels || emptyContentModels) {
513
649
  return {
514
650
  valid: true,
515
651
  value: config,
@@ -520,17 +656,17 @@ function validateAndExtendContentModels(config: any): ConfigValidationResult {
520
656
  const validationResult = validateContentModels(contentModels, models);
521
657
 
522
658
  if (_.isEmpty(models)) {
523
- return validationResult;
659
+ return {
660
+ valid: validationResult.valid,
661
+ value: config,
662
+ errors: validationResult.errors
663
+ };
524
664
  }
525
665
 
526
666
  const extendedModels = _.mapValues(models, (model, modelName) => {
527
667
  const contentModel = validationResult.value.contentModels[modelName];
528
668
  if (!contentModel) {
529
- return {
530
- // if a model does not define a type, use the default "object" type
531
- type: model.type || 'object',
532
- ..._.omit(model, 'type')
533
- };
669
+ return model;
534
670
  }
535
671
  if (_.get(contentModel, '__metadata.invalid')) {
536
672
  return model;
@@ -565,10 +701,43 @@ function validateAndExtendContentModels(config: any): ConfigValidationResult {
565
701
  }
566
702
 
567
703
  function normalizeValidationResult(validationResult: ConfigValidationResult): NormalizedValidationResult {
704
+ validationResult = filterAndOrderConfigFields(validationResult);
568
705
  convertModelGroupsToModelList(validationResult);
569
706
  return convertModelsToArray(validationResult);
570
707
  }
571
708
 
709
+ function filterAndOrderConfigFields(validationResult: ConfigValidationResult): ConfigValidationResult {
710
+ // TODO: see if we move filtering and sorting to Joi
711
+ return {
712
+ ...validationResult,
713
+ value: _.pick(validationResult.value, [
714
+ 'stackbitVersion',
715
+ 'ssgName',
716
+ 'ssgVersion',
717
+ 'cmsName',
718
+ 'import',
719
+ 'buildCommand',
720
+ 'publishDir',
721
+ 'nodeVersion',
722
+ 'devCommand',
723
+ 'staticDir',
724
+ 'uploadDir',
725
+ 'assets',
726
+ 'pagesDir',
727
+ 'dataDir',
728
+ 'pageLayoutKey',
729
+ 'objectTypeKey',
730
+ 'styleObjectModelName',
731
+ 'excludePages',
732
+ 'logicFields',
733
+ 'contentModels',
734
+ 'modelsSource',
735
+ 'models',
736
+ 'presets'
737
+ ])
738
+ };
739
+ }
740
+
572
741
  function convertModelGroupsToModelList(validationResult: ConfigValidationResult) {
573
742
  const models = validationResult.value?.models ?? {};
574
743
 
@@ -598,7 +767,7 @@ function convertModelGroupsToModelList(validationResult: ConfigValidationResult)
598
767
  _.forEach(models, (model) => {
599
768
  iterateModelFieldsRecursively(model, (field: any) => {
600
769
  if (isListField(field)) {
601
- field = getListItemsField(field);
770
+ field = field.items;
602
771
  }
603
772
  if (field.groups) {
604
773
  let key: string | null = null;
@@ -640,6 +809,10 @@ function convertModelsToArray(validationResult: ConfigValidationResult): Normali
640
809
  }
641
810
  );
642
811
 
812
+ if (!isGitCMS(config)) {
813
+ addImageModel(modelArray);
814
+ }
815
+
643
816
  const convertedErrors = _.map(validationResult.errors, (error: ConfigValidationError) => {
644
817
  if (error.fieldPath[0] === 'models' && typeof error.fieldPath[1] == 'string') {
645
818
  const modelName = error.fieldPath[1];
@@ -660,3 +833,20 @@ function convertModelsToArray(validationResult: ConfigValidationResult): Normali
660
833
  errors: convertedErrors
661
834
  };
662
835
  }
836
+
837
+ function addImageModel(models: Model[]) {
838
+ models.push({
839
+ type: 'image',
840
+ name: '__image_model',
841
+ label: 'Image',
842
+ labelField: 'title',
843
+ fields: [
844
+ { name: 'title', type: 'string' },
845
+ { name: 'url', type: 'string' }
846
+ ]
847
+ });
848
+ }
849
+
850
+ function isGitCMS(config: any) {
851
+ return !config.cmsName || config.cmsName === 'git';
852
+ }
@@ -20,7 +20,10 @@ import {
20
20
  FieldGroupItem,
21
21
  YamlObjectModel,
22
22
  ContentModelMap,
23
- ContentModel
23
+ ContentModel,
24
+ ModelsSourceFiles,
25
+ ModelsSourceContentful,
26
+ ModelsSourceSanity
24
27
  } from './config-types';
25
28
 
26
29
  function getConfigFromValidationState(state: Joi.State): YamlConfig {
@@ -200,6 +203,11 @@ const styleObjectModelNotObject = 'styleObjectModelName.model.type';
200
203
  const styleObjectModelNameSchema = Joi.string()
201
204
  .allow('', null)
202
205
  .custom((value, { error, state }) => {
206
+ const config = getConfigFromValidationState(state);
207
+ const externalModels = config.cmsName && ['contentful', 'sanity'].includes(config.cmsName);
208
+ if (externalModels) {
209
+ return value;
210
+ }
203
211
  const models = getModelsFromValidationState(state);
204
212
  const modelNames = Object.keys(models);
205
213
  if (!modelNames.includes(value)) {
@@ -245,11 +253,32 @@ const importSchema = Joi.alternatives().conditional('.type', {
245
253
  ]
246
254
  });
247
255
 
248
- const modelsSourceSchema = Joi.object<ModelsSource>({
249
- type: 'files',
256
+ const modelsSourceFilesSchema = Joi.object<ModelsSourceFiles>({
257
+ type: Joi.string().valid(Joi.override, 'files').required(),
250
258
  modelDirs: Joi.array().items(Joi.string()).required()
251
259
  });
252
260
 
261
+ const modelsSourceContentfulSchema = Joi.object<ModelsSourceContentful>({
262
+ type: Joi.string().valid(Joi.override, 'contentful').required(),
263
+ module: Joi.string()
264
+ });
265
+
266
+ const modelsSourceSanitySchema = Joi.object<ModelsSourceSanity>({
267
+ type: Joi.string().valid(Joi.override, 'sanity').required(),
268
+ sanityStudioPath: Joi.string().required(),
269
+ module: Joi.string()
270
+ });
271
+
272
+ const modelsSourceSchema = Joi.object<ModelsSource>({
273
+ type: Joi.string().valid('files', 'contentful', 'sanity').required()
274
+ }).when('.type', {
275
+ switch: [
276
+ { is: 'files', then: modelsSourceFilesSchema },
277
+ { is: 'contentful', then: modelsSourceContentfulSchema },
278
+ { is: 'sanity', then: modelsSourceSanitySchema }
279
+ ]
280
+ });
281
+
253
282
  const assetsSchema = Joi.object<Assets>({
254
283
  referenceType: Joi.string().valid('static', 'relative').required(),
255
284
  assetsDir: Joi.string().allow('').when('referenceType', {
@@ -309,7 +338,8 @@ const fieldCommonPropsSchema = Joi.object({
309
338
  group: inGroups,
310
339
  const: Joi.any(),
311
340
  hidden: Joi.boolean(),
312
- readOnly: Joi.boolean()
341
+ readOnly: Joi.boolean(),
342
+ localized: Joi.boolean()
313
343
  }).oxor('const', 'default');
314
344
 
315
345
  const numberFieldPartialSchema = Joi.object({
@@ -3,7 +3,7 @@ import { CMS_NAMES, FIELD_TYPES, SSG_NAMES, STYLE_PROPS } from './config-consts'
3
3
 
4
4
  export interface Config extends BaseConfig {
5
5
  models: Model[];
6
- presets?: any;
6
+ presets?: Record<string, any>;
7
7
  }
8
8
 
9
9
  export interface YamlConfig extends BaseConfig {
@@ -85,16 +85,29 @@ export interface ContentModel extends BaseMatch {
85
85
  newFilePath?: string;
86
86
  }
87
87
 
88
- export interface ModelsSource {
88
+ export type ModelsSource = ModelsSourceFiles | ModelsSourceContentful | ModelsSourceSanity;
89
+
90
+ export interface ModelsSourceFiles {
89
91
  type: 'files';
90
92
  modelDirs: string[];
91
93
  }
92
94
 
95
+ export interface ModelsSourceContentful {
96
+ type: 'contentful';
97
+ module?: string;
98
+ }
99
+
100
+ export interface ModelsSourceSanity {
101
+ type: 'sanity';
102
+ sanityStudioPath: string;
103
+ module?: string;
104
+ }
105
+
93
106
  /*******************
94
107
  *** Model Types ***
95
108
  *******************/
96
109
 
97
- export type Model = StricterUnion<ObjectModel | DataModel | PageModel | ConfigModel>;
110
+ export type Model = StricterUnion<ObjectModel | DataModel | PageModel | ConfigModel | ImageModel>;
98
111
 
99
112
  export type ObjectModel = YamlObjectModel & BaseModel;
100
113
  export type DataModel = YamlDataModel & BaseModel;
@@ -174,6 +187,14 @@ export interface FieldGroupItem {
174
187
  label: string;
175
188
  }
176
189
 
190
+ export interface ImageModel {
191
+ type: 'image';
192
+ name: '__image_model';
193
+ label?: string;
194
+ labelField?: string;
195
+ fields?: Field[];
196
+ }
197
+
177
198
  /*******************
178
199
  *** Field Types ***
179
200
  *******************/
@@ -209,7 +230,7 @@ export interface FieldCommonProps {
209
230
  export type FieldType = typeof FIELD_TYPES[number];
210
231
 
211
232
  export interface FieldSimpleProps {
212
- type: 'string' | 'url' | 'slug' | 'text' | 'markdown' | 'html' | 'boolean' | 'date' | 'datetime' | 'color' | 'image' | 'file';
233
+ type: 'string' | 'url' | 'slug' | 'text' | 'markdown' | 'html' | 'boolean' | 'date' | 'datetime' | 'color' | 'image' | 'file' | 'json' | 'richText';
213
234
  }
214
235
 
215
236
  export type FieldEnumProps = FieldEnumDropdownProps | FieldEnumThumbnailsProps | FieldEnumPaletteProps;
@@ -29,6 +29,9 @@ export function convertToYamlConfig({ config }: { config: Config }): YamlConfig
29
29
  yamlConfig.models = _.reduce(
30
30
  config.models,
31
31
  (yamlModels: ModelMap, model: Model) => {
32
+ if (model.type === 'image') {
33
+ return yamlModels;
34
+ }
32
35
  const yamlModel = _.omit(model, ['name', '__metadata']) as YamlModel;
33
36
  if (yamlModel.type === 'page' && !yamlModel.hideContent && yamlModel.fields) {
34
37
  _.remove(yamlModel.fields, (field) => field.name === 'markdown_content');