@stackbit/sdk 0.2.20 → 0.2.24

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