@stackbit/sdk 0.2.22 → 0.2.26

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 +5 -4
  5. package/dist/config/config-loader.js +291 -99
  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 +23 -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 +327 -111
  28. package/src/config/config-schema.ts +34 -4
  29. package/src/config/config-types.ts +27 -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,14 +20,18 @@ 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';
26
- import { Config, DataModel, FieldEnum, FieldModel, FieldObjectProps, Model, PageModel, YamlModel } from './config-types';
28
+ import { append, omitByNil, parseFile, readDirRecursively, reducePromise, rename } from '@stackbit/utils';
29
+ import { Config, DataModel, FieldEnum, FieldModel, FieldObjectProps, Model, ModelsSource, PageModel, YamlModel } from './config-types';
27
30
  import { loadPresets } from './presets-loader';
28
31
 
29
32
  export interface ConfigLoaderOptions {
30
33
  dirPath: string;
34
+ modelsSource?: ModelsSource;
31
35
  }
32
36
 
33
37
  export interface ConfigLoaderResult {
@@ -43,81 +47,94 @@ 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> {
51
- let configLoadResult: TempConfigLoaderResult;
52
- try {
53
- configLoadResult = await loadConfigFromDir(dirPath);
54
- } catch (error) {
55
- return {
56
- valid: false,
57
- config: null,
58
- errors: [new ConfigLoadError(`Error loading Stackbit configuration: ${error.message}`, { originalError: error })]
59
- };
60
- }
54
+ export async function loadConfig({ dirPath, modelsSource }: ConfigLoaderOptions): Promise<ConfigLoaderResult> {
55
+ const { config, errors: configLoadErrors } = await loadConfigFromDir(dirPath);
61
56
 
62
- if (!configLoadResult.config) {
57
+ if (!config) {
63
58
  return {
64
59
  valid: false,
65
60
  config: null,
66
- errors: configLoadResult.errors
61
+ errors: configLoadErrors
67
62
  };
68
63
  }
69
64
 
70
- const normalizedResult = validateAndNormalizeConfig(configLoadResult.config);
65
+ const { models: externalModels, errors: externalModelsLoadErrors } = await loadModelsFromExternalSource(config, dirPath, modelsSource);
66
+
67
+ const normalizedResult = validateAndNormalizeConfig(config, externalModels);
71
68
 
72
69
  const presetsResult = await loadPresets(dirPath, normalizedResult.config);
73
70
 
74
71
  return {
75
72
  valid: normalizedResult.valid,
76
73
  config: presetsResult.config,
77
- errors: [...configLoadResult.errors, ...normalizedResult.errors, ...presetsResult.errors]
74
+ errors: [...configLoadErrors, ...externalModelsLoadErrors, ...normalizedResult.errors, ...presetsResult.errors]
78
75
  };
79
76
  }
80
77
 
81
- export function validateAndNormalizeConfig(config: any): NormalizedValidationResult {
82
- // validate the "contentModels" and extend config models with "contentModels"
83
- // this must be done before main config validation to make it independent of "contentModels".
84
- const contentModelsValidationResult = validateAndExtendContentModels(config);
85
- config = contentModelsValidationResult.value;
86
-
78
+ export function validateAndNormalizeConfig(config: any, externalModels?: Model[]): NormalizedValidationResult {
87
79
  // extend config models having the "extends" property
88
- // this must be done before main config validation as some properties like
80
+ // this must be done before any validation as some properties like
89
81
  // the labelField will not work when validating models without extending them first
90
- const { models, errors: extendModelErrors } = extendModelMap(config?.models || {});
91
- config.models = models;
82
+ const { models: extendedModels, errors: extendModelErrors } = extendModelMap(config.models as any);
83
+ const extendedConfig = {
84
+ ...config,
85
+ models: extendedModels
86
+ };
87
+
88
+ const { config: mergedConfig, errors: externalModelsMergeErrors } = mergeConfigWithExternalModels(extendedConfig, externalModels);
89
+
90
+ // validate the "contentModels" and extend config models with "contentModels"
91
+ // this must be done before main config validation to make it independent of "contentModels".
92
+ const { value: configWithContentModels, errors: contentModelsErrors } = validateAndExtendContentModels(mergedConfig);
92
93
 
93
94
  // normalize config - backward compatibility updates, adding extra fields like "markdown_content", "type" and "layout",
94
95
  // and setting other default values.
95
- config = normalizeConfig(config);
96
+ const normalizedConfig = normalizeConfig(configWithContentModels);
96
97
 
97
98
  // validate config
98
- const configValidationResult = validateConfig(config);
99
+ const { value: validatedConfig, errors: validationErrors } = validateConfig(normalizedConfig);
100
+
101
+ const errors = [...extendModelErrors, ...externalModelsMergeErrors, ...contentModelsErrors, ...validationErrors];
99
102
 
100
- const errors = [...contentModelsValidationResult.errors, ...extendModelErrors, ...configValidationResult.errors];
101
103
  return normalizeValidationResult({
102
104
  valid: _.isEmpty(errors),
103
- value: configValidationResult.value,
105
+ value: validatedConfig,
104
106
  errors: errors
105
107
  });
106
108
  }
107
109
 
108
110
  async function loadConfigFromDir(dirPath: string): Promise<TempConfigLoaderResult> {
109
- const { config, error } = await loadConfigFromStackbitYaml(dirPath);
110
- if (error) {
111
- return { errors: [error] };
111
+ try {
112
+ const { config, error } = await loadConfigFromStackbitYaml(dirPath);
113
+ if (error) {
114
+ return { errors: [error] };
115
+ }
116
+
117
+ const { models: modelsFromFiles, errors: fileModelsErrors } = await loadModelsFromFiles(dirPath, config);
118
+
119
+ const mergedModels = mergeConfigModelsWithModelsFromFiles(config.models ?? {}, modelsFromFiles);
120
+
121
+ return {
122
+ config: {
123
+ ...config,
124
+ models: mergedModels
125
+ },
126
+ errors: fileModelsErrors
127
+ };
128
+ } catch (error) {
129
+ return {
130
+ errors: [new ConfigLoadError(`Error loading Stackbit configuration: ${error.message}`, { originalError: error })]
131
+ };
112
132
  }
113
- const externalModelsResult = await loadExternalModels(dirPath, config);
114
- config.models = _.assign(externalModelsResult.models, config.models);
115
- return { config, errors: externalModelsResult.errors };
116
133
  }
117
134
 
118
- type LoadConfigFromStackbitYamlResult = { config: any; error?: undefined } | { config?: undefined; error: ConfigLoadError };
135
+ type StackbitYamlConfigResult = { config: any; error?: undefined } | { config?: undefined; error: ConfigLoadError };
119
136
 
120
- async function loadConfigFromStackbitYaml(dirPath: string): Promise<LoadConfigFromStackbitYamlResult> {
137
+ async function loadConfigFromStackbitYaml(dirPath: string): Promise<StackbitYamlConfigResult> {
121
138
  const stackbitYamlPath = path.join(dirPath, 'stackbit.yaml');
122
139
  const stackbitYamlExists = await fse.pathExists(stackbitYamlPath);
123
140
  if (!stackbitYamlExists) {
@@ -135,54 +152,56 @@ async function loadConfigFromStackbitYaml(dirPath: string): Promise<LoadConfigFr
135
152
  return { config };
136
153
  }
137
154
 
138
- async function loadExternalModels(dirPath: string, config: any) {
155
+ async function loadModelsFromFiles(dirPath: string, config: any): Promise<{ models: Record<string, any>; errors: ConfigLoadError[] }> {
139
156
  const modelsSource = _.get(config, 'modelsSource', {});
140
157
  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
158
+ const defaultModelDirs = ['node_modules/@stackbit/components/models', '.stackbit/models'];
159
+ const modelDirs =
160
+ sourceType === 'files'
161
+ ? _.castArray(_.get(modelsSource, 'modelDirs', defaultModelDirs)).map((modelDir: string) => _.trim(modelDir, '/'))
162
+ : defaultModelDirs;
163
+
164
+ const modelFiles = await reducePromise(
165
+ modelDirs,
166
+ async (modelFiles: string[], modelDir) => {
167
+ const absModelsDir = path.join(dirPath, modelDir);
168
+ const dirExists = await fse.pathExists(absModelsDir);
169
+ if (!dirExists) {
170
+ return modelFiles;
171
+ }
172
+ const files = await readModelFilesFromDir(absModelsDir);
173
+ return modelFiles.concat(files.map((filePath) => path.join(modelDir, filePath)));
174
+ },
175
+ []
176
+ );
177
+
178
+ return reducePromise(
179
+ modelFiles,
180
+ async (result: { models: any; errors: ConfigLoadError[] }, modelFile) => {
181
+ let model;
182
+ try {
183
+ model = await parseFile(path.join(dirPath, modelFile));
184
+ } catch (error) {
185
+ return {
186
+ models: result.models,
187
+ errors: result.errors.concat(new ConfigLoadError(`error parsing model, file: ${modelFile}`))
179
188
  };
180
- return result;
181
- },
182
- { models: {}, errors: [] }
183
- );
184
- }
185
- return { models: {}, errors: [] };
189
+ }
190
+ const modelName = model?.name;
191
+ if (!modelName) {
192
+ return {
193
+ models: result.models,
194
+ errors: result.errors.concat(new ConfigLoadError(`model does not have a name, file: ${modelFile}`))
195
+ };
196
+ }
197
+ result.models[modelName] = _.omit(model, 'name');
198
+ result.models[modelName].__metadata = {
199
+ filePath: modelFile
200
+ };
201
+ return result;
202
+ },
203
+ { models: {}, errors: [] }
204
+ );
186
205
  }
187
206
 
188
207
  async function readModelFilesFromDir(modelsDir: string) {
@@ -197,6 +216,38 @@ async function readModelFilesFromDir(modelsDir: string) {
197
216
  });
198
217
  }
199
218
 
219
+ async function loadModelsFromExternalSource(
220
+ config: any,
221
+ dirPath: string,
222
+ modelsSource?: ModelsSource
223
+ ): Promise<{ models: Model[]; errors: ConfigLoadError[] }> {
224
+ modelsSource = _.get(config, 'modelsSource', modelsSource);
225
+ const sourceType = _.get(modelsSource, 'type', 'files');
226
+ if (sourceType === 'files') {
227
+ return { models: [], errors: [] };
228
+ } else if (sourceType === 'contentful') {
229
+ const contentfulModule = _.get(modelsSource, 'module', '@stackbit/cms-contentful');
230
+ const modulePath = path.resolve(dirPath, 'node_modules', contentfulModule);
231
+ const module = await import(modulePath);
232
+ try {
233
+ const { models } = await module.fetchAndConvertSchema(_.omit(modelsSource, ['type', 'module']));
234
+ return {
235
+ models: models,
236
+ errors: []
237
+ };
238
+ } catch (error) {
239
+ return {
240
+ models: [],
241
+ errors: [new ConfigLoadError(`Error fetching and converting Contentful schema, error: ${error.message}`, { originalError: error })]
242
+ };
243
+ }
244
+ }
245
+ return {
246
+ models: [],
247
+ errors: [new ConfigLoadError(`modelsSource ${modelsSource} is unsupported`)]
248
+ };
249
+ }
250
+
200
251
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
201
252
  async function loadConfigFromDotStackbit(dirPath: string) {
202
253
  const stackbitDotPath = path.join(dirPath, '.stackbit');
@@ -231,6 +282,113 @@ async function loadConfigFromDotStackbit(dirPath: string) {
231
282
  return _.isEmpty(config) ? null : config;
232
283
  }
233
284
 
285
+ function mergeConfigModelsWithModelsFromFiles(configModels: any, modelsFromFiles: Record<string, any>) {
286
+ const mergedModels = _.mapValues(modelsFromFiles, (modelFromFile, modelName) => {
287
+ // resolve thumbnails of models loaded from files
288
+ const modelFilePath = modelFromFile.__metadata?.filePath;
289
+ resolveThumbnailPathForModel(modelFromFile, modelFilePath);
290
+ iterateModelFieldsRecursively(modelFromFile, (field: any) => {
291
+ if (isListField(field)) {
292
+ field = normalizeListFieldInPlace(field);
293
+ field = field.items;
294
+ }
295
+ if (isObjectField(field)) {
296
+ resolveThumbnailPathForModel(field, modelFilePath);
297
+ } else if (isEnumField(field)) {
298
+ resolveThumbnailPathForEnumField(field, modelFilePath);
299
+ }
300
+ });
301
+
302
+ const configModel = _.get(configModels, modelName);
303
+ if (!configModel) {
304
+ return modelFromFile;
305
+ }
306
+
307
+ return _.assign({}, modelFromFile, configModel, {
308
+ fields: _.unionBy(configModel?.fields ?? [], modelFromFile?.fields ?? [], 'name')
309
+ });
310
+ });
311
+ return Object.assign({}, configModels, mergedModels);
312
+ }
313
+
314
+ function mergeConfigWithExternalModels(config: any, externalModels?: Model[]): { config: any; errors: ConfigValidationError[] } {
315
+ if (!externalModels || externalModels.length === 0) {
316
+ return {
317
+ config,
318
+ errors: []
319
+ };
320
+ }
321
+
322
+ const stackbitModels = config?.models ?? {};
323
+ const errors: ConfigValidationError[] = [];
324
+
325
+ const models = _.reduce(
326
+ externalModels,
327
+ (modelMap: Record<string, YamlModel>, externalModel) => {
328
+ const { name, ...rest } = externalModel;
329
+ return Object.assign(modelMap, { [name]: rest });
330
+ },
331
+ {}
332
+ );
333
+
334
+ _.forEach(stackbitModels, (stackbitModel: any, modelName: any) => {
335
+ let externalModel = models[modelName];
336
+ if (!externalModel) {
337
+ return;
338
+ }
339
+
340
+ const modelType = stackbitModel.type ? (stackbitModel.type === 'config' ? 'data' : stackbitModel.type) : externalModel.type ?? 'object';
341
+ const urlPath = modelType === 'page' ? stackbitModel?.urlPath ?? '/{slug}' : null;
342
+
343
+ externalModel = Object.assign(
344
+ {},
345
+ externalModel,
346
+ _.pick(stackbitModel, ['__metadata', 'label', 'description', 'thumbnail', 'singleInstance', 'readOnly', 'labelField', 'fieldGroups']),
347
+ omitByNil({
348
+ type: modelType,
349
+ urlPath
350
+ })
351
+ );
352
+
353
+ externalModel = mapModelFieldsRecursively(externalModel as Model, (field, modelKeyPath) => {
354
+ const stackbitField = getModelFieldForModelKeyPath(stackbitModel, modelKeyPath);
355
+ if (!stackbitField) {
356
+ return field;
357
+ }
358
+
359
+ let override = {};
360
+ if (stackbitField.type === 'style') {
361
+ override = stackbitField;
362
+ } else if (field.type === 'enum') {
363
+ override = _.pick(stackbitField, ['options']);
364
+ } else if (field.type === 'color') {
365
+ override = { type: 'color' };
366
+ } else if (field.type === 'number') {
367
+ override = _.pick(stackbitField, ['subtype', 'min', 'max', 'step', 'unit']);
368
+ } else if (field.type === 'object') {
369
+ override = _.pick(stackbitField, ['labelField', 'thumbnail', 'fieldGroups']);
370
+ }
371
+
372
+ return Object.assign(
373
+ {},
374
+ field,
375
+ _.pick(stackbitField, ['label', 'description', 'required', 'default', 'group', 'const', 'hidden', 'readOnly', 'controlType']),
376
+ override
377
+ );
378
+ }) as YamlModel;
379
+
380
+ models[modelName] = externalModel;
381
+ });
382
+
383
+ return {
384
+ config: {
385
+ ...config,
386
+ models: models
387
+ },
388
+ errors: errors
389
+ };
390
+ }
391
+
234
392
  function normalizeConfig(config: any): any {
235
393
  const pageLayoutKey = _.get(config, 'pageLayoutKey', 'layout');
236
394
  const objectTypeKey = _.get(config, 'objectTypeKey', 'type');
@@ -238,6 +396,7 @@ function normalizeConfig(config: any): any {
238
396
  const ver = semver.coerce(stackbitYamlVersion);
239
397
  const isStackbitYamlV2 = ver ? semver.satisfies(ver, '<0.3.0') : false;
240
398
  const models = config?.models || {};
399
+ const gitCMS = isGitCMS(config);
241
400
  let referencedModelNames: string[] = [];
242
401
 
243
402
  _.forEach(models, (model, modelName) => {
@@ -245,6 +404,10 @@ function normalizeConfig(config: any): any {
245
404
  return;
246
405
  }
247
406
 
407
+ if (!_.has(model, 'type')) {
408
+ model.type = 'object';
409
+ }
410
+
248
411
  // add model label if not set
249
412
  if (!_.has(model, 'label')) {
250
413
  model.label = _.startCase(modelName);
@@ -259,13 +422,15 @@ function normalizeConfig(config: any): any {
259
422
  rename(model, 'template', 'layout');
260
423
 
261
424
  updatePageUrlPath(model);
262
- updatePageFilePath(model, config);
263
425
 
264
- addMarkdownContentField(model);
426
+ if (gitCMS) {
427
+ updatePageFilePath(model, config);
428
+ addMarkdownContentField(model);
265
429
 
266
- // TODO: update schema-editor to not show layout field
267
- addLayoutFieldToPageModel(model, pageLayoutKey);
268
- } else if (isDataModel(model)) {
430
+ // TODO: update schema-editor to not show layout field
431
+ addLayoutFieldToPageModel(model, pageLayoutKey);
432
+ }
433
+ } else if (isDataModel(model) && gitCMS) {
269
434
  updateDataFilePath(model, config);
270
435
  }
271
436
 
@@ -281,8 +446,6 @@ function normalizeConfig(config: any): any {
281
446
  assignLabelFieldIfNeeded(model);
282
447
  }
283
448
 
284
- resolveThumbnailPathForModel(model, model?.__metadata?.filePath);
285
-
286
449
  iterateModelFieldsRecursively(model, (field: any) => {
287
450
  // add field label if label is not set
288
451
  if (!_.has(field, 'label')) {
@@ -290,18 +453,12 @@ function normalizeConfig(config: any): any {
290
453
  }
291
454
 
292
455
  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);
456
+ field = normalizeListFieldInPlace(field);
457
+ field = field.items;
298
458
  }
299
459
 
300
460
  if (isObjectField(field)) {
301
461
  assignLabelFieldIfNeeded(field);
302
- resolveThumbnailPathForModel(field, model?.__metadata?.filePath);
303
- } else if (isEnumField(field)) {
304
- resolveThumbnailPathForEnumField(field, model?.__metadata?.filePath);
305
462
  } else if (isCustomModelField(field, models)) {
306
463
  // stackbit v0.2.0 compatibility
307
464
  // convert the old custom model field type: { type: 'action' }
@@ -331,7 +488,9 @@ function normalizeConfig(config: any): any {
331
488
  }
332
489
  }
333
490
 
334
- referencedModelNames = _.union(referencedModelNames, getReferencedModelNames(field));
491
+ if (gitCMS) {
492
+ referencedModelNames = _.union(referencedModelNames, getReferencedModelNames(field));
493
+ }
335
494
  });
336
495
  });
337
496
 
@@ -488,7 +647,7 @@ function resolveThumbnailPath(thumbnail: string, modelDirPath: string) {
488
647
  */
489
648
  function getReferencedModelNames(field: any) {
490
649
  if (isListField(field)) {
491
- field = getListItemsField(field);
650
+ field = getListFieldItems(field);
492
651
  }
493
652
  // TODO: add type field to model fields inside container update/create object logic rather adding type to schema
494
653
  // 'object' models referenced by 'model' fields should have 'type' field
@@ -509,7 +668,10 @@ function validateAndExtendContentModels(config: any): ConfigValidationResult {
509
668
  const contentModels = config.contentModels ?? {};
510
669
  const models = config.models ?? {};
511
670
 
512
- if (_.isEmpty(contentModels)) {
671
+ const externalModels = !isGitCMS(config);
672
+ const emptyContentModels = _.isEmpty(contentModels);
673
+
674
+ if (externalModels || emptyContentModels) {
513
675
  return {
514
676
  valid: true,
515
677
  value: config,
@@ -520,17 +682,17 @@ function validateAndExtendContentModels(config: any): ConfigValidationResult {
520
682
  const validationResult = validateContentModels(contentModels, models);
521
683
 
522
684
  if (_.isEmpty(models)) {
523
- return validationResult;
685
+ return {
686
+ valid: validationResult.valid,
687
+ value: config,
688
+ errors: validationResult.errors
689
+ };
524
690
  }
525
691
 
526
692
  const extendedModels = _.mapValues(models, (model, modelName) => {
527
693
  const contentModel = validationResult.value.contentModels[modelName];
528
694
  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
- };
695
+ return model;
534
696
  }
535
697
  if (_.get(contentModel, '__metadata.invalid')) {
536
698
  return model;
@@ -565,10 +727,43 @@ function validateAndExtendContentModels(config: any): ConfigValidationResult {
565
727
  }
566
728
 
567
729
  function normalizeValidationResult(validationResult: ConfigValidationResult): NormalizedValidationResult {
730
+ validationResult = filterAndOrderConfigFields(validationResult);
568
731
  convertModelGroupsToModelList(validationResult);
569
732
  return convertModelsToArray(validationResult);
570
733
  }
571
734
 
735
+ function filterAndOrderConfigFields(validationResult: ConfigValidationResult): ConfigValidationResult {
736
+ // TODO: see if we move filtering and sorting to Joi
737
+ return {
738
+ ...validationResult,
739
+ value: _.pick(validationResult.value, [
740
+ 'stackbitVersion',
741
+ 'ssgName',
742
+ 'ssgVersion',
743
+ 'cmsName',
744
+ 'import',
745
+ 'buildCommand',
746
+ 'publishDir',
747
+ 'nodeVersion',
748
+ 'devCommand',
749
+ 'staticDir',
750
+ 'uploadDir',
751
+ 'assets',
752
+ 'pagesDir',
753
+ 'dataDir',
754
+ 'pageLayoutKey',
755
+ 'objectTypeKey',
756
+ 'styleObjectModelName',
757
+ 'excludePages',
758
+ 'logicFields',
759
+ 'contentModels',
760
+ 'modelsSource',
761
+ 'models',
762
+ 'presets'
763
+ ])
764
+ };
765
+ }
766
+
572
767
  function convertModelGroupsToModelList(validationResult: ConfigValidationResult) {
573
768
  const models = validationResult.value?.models ?? {};
574
769
 
@@ -598,7 +793,7 @@ function convertModelGroupsToModelList(validationResult: ConfigValidationResult)
598
793
  _.forEach(models, (model) => {
599
794
  iterateModelFieldsRecursively(model, (field: any) => {
600
795
  if (isListField(field)) {
601
- field = getListItemsField(field);
796
+ field = field.items;
602
797
  }
603
798
  if (field.groups) {
604
799
  let key: string | null = null;
@@ -640,6 +835,10 @@ function convertModelsToArray(validationResult: ConfigValidationResult): Normali
640
835
  }
641
836
  );
642
837
 
838
+ if (!isGitCMS(config)) {
839
+ addImageModel(modelArray);
840
+ }
841
+
643
842
  const convertedErrors = _.map(validationResult.errors, (error: ConfigValidationError) => {
644
843
  if (error.fieldPath[0] === 'models' && typeof error.fieldPath[1] == 'string') {
645
844
  const modelName = error.fieldPath[1];
@@ -660,3 +859,20 @@ function convertModelsToArray(validationResult: ConfigValidationResult): Normali
660
859
  errors: convertedErrors
661
860
  };
662
861
  }
862
+
863
+ function addImageModel(models: Model[]) {
864
+ models.push({
865
+ type: 'image',
866
+ name: '__image_model',
867
+ label: 'Image',
868
+ labelField: 'title',
869
+ fields: [
870
+ { name: 'title', type: 'string' },
871
+ { name: 'url', type: 'string' }
872
+ ]
873
+ });
874
+ }
875
+
876
+ function isGitCMS(config: any) {
877
+ return !config.cmsName || config.cmsName === 'git';
878
+ }