@stackbit/sdk 0.3.5 → 0.3.6

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 (56) hide show
  1. package/dist/config/config-loader-static.js +1 -1
  2. package/dist/config/config-loader-static.js.map +1 -1
  3. package/dist/config/config-loader-utils.d.ts +12 -7
  4. package/dist/config/config-loader-utils.d.ts.map +1 -1
  5. package/dist/config/config-loader-utils.js +104 -68
  6. package/dist/config/config-loader-utils.js.map +1 -1
  7. package/dist/config/config-loader.d.ts +46 -27
  8. package/dist/config/config-loader.d.ts.map +1 -1
  9. package/dist/config/config-loader.js +294 -265
  10. package/dist/config/config-loader.js.map +1 -1
  11. package/dist/config/config-schema.d.ts +2 -2
  12. package/dist/config/config-schema.d.ts.map +1 -1
  13. package/dist/config/config-schema.js +123 -55
  14. package/dist/config/config-schema.js.map +1 -1
  15. package/dist/config/config-types.d.ts +4 -3
  16. package/dist/config/config-types.d.ts.map +1 -1
  17. package/dist/config/config-validator.d.ts +9 -5
  18. package/dist/config/config-validator.d.ts.map +1 -1
  19. package/dist/config/config-validator.js +42 -23
  20. package/dist/config/config-validator.js.map +1 -1
  21. package/dist/config/presets-loader.d.ts +13 -4
  22. package/dist/config/presets-loader.d.ts.map +1 -1
  23. package/dist/config/presets-loader.js +49 -23
  24. package/dist/config/presets-loader.js.map +1 -1
  25. package/dist/content/content-schema.js +1 -1
  26. package/dist/content/content-schema.js.map +1 -1
  27. package/dist/index.d.ts +4 -2
  28. package/dist/index.d.ts.map +1 -1
  29. package/dist/index.js +12 -2
  30. package/dist/index.js.map +1 -1
  31. package/dist/utils/index.d.ts +1 -8
  32. package/dist/utils/index.d.ts.map +1 -1
  33. package/dist/utils/index.js.map +1 -1
  34. package/dist/utils/model-extender.js +1 -1
  35. package/dist/utils/model-extender.js.map +1 -1
  36. package/dist/utils/model-iterators.d.ts +3 -2
  37. package/dist/utils/model-iterators.d.ts.map +1 -1
  38. package/dist/utils/model-iterators.js.map +1 -1
  39. package/dist/utils/model-utils.d.ts +9 -8
  40. package/dist/utils/model-utils.d.ts.map +1 -1
  41. package/dist/utils/model-utils.js +26 -9
  42. package/dist/utils/model-utils.js.map +1 -1
  43. package/package.json +3 -3
  44. package/src/config/config-loader-static.ts +1 -1
  45. package/src/config/config-loader-utils.ts +111 -78
  46. package/src/config/config-loader.ts +457 -394
  47. package/src/config/config-schema.ts +150 -81
  48. package/src/config/config-types.ts +6 -3
  49. package/src/config/config-validator.ts +51 -29
  50. package/src/config/presets-loader.ts +59 -30
  51. package/src/content/content-schema.ts +1 -1
  52. package/src/index.ts +21 -2
  53. package/src/utils/index.ts +1 -13
  54. package/src/utils/model-extender.ts +1 -1
  55. package/src/utils/model-iterators.ts +6 -5
  56. package/src/utils/model-utils.ts +38 -16
@@ -4,33 +4,31 @@ import chokidar from 'chokidar';
4
4
  import semver from 'semver';
5
5
  import _ from 'lodash';
6
6
 
7
- import { ModelsSource, FieldModel } from '@stackbit/types';
8
- import { append, getFirstExistingFile, omitByNil, prepend, rename } from '@stackbit/utils';
7
+ import { ModelsSource, Field } from '@stackbit/types';
8
+ import { append, getFirstExistingFile, prepend, rename } from '@stackbit/utils';
9
9
 
10
10
  import { ConfigValidationResult, validateConfig, validateContentModels } from './config-validator';
11
- import { ConfigError, ConfigLoadError, ConfigPresetsError, ConfigValidationError, STACKBIT_CONFIG_NOT_FOUND } from './config-errors';
11
+ import { ConfigError, ConfigLoadError, ConfigValidationError, STACKBIT_CONFIG_NOT_FOUND } from './config-errors';
12
12
  import { loadStackbitConfigFromJs } from './config-loader-esbuild';
13
13
  import {
14
14
  assignLabelFieldIfNeeded,
15
- extendModelMap,
16
- getListFieldItems,
15
+ getListItemsOrSelf,
17
16
  getModelFieldForModelKeyPath,
18
17
  isCustomModelField,
19
18
  isDataModel,
20
19
  isListDataModel,
21
- isListField,
22
20
  isModelField,
23
21
  isObjectField,
24
22
  isObjectListItems,
25
23
  isPageModel,
26
24
  isReferenceField,
27
- iterateModelFieldsRecursively,
28
25
  mapModelFieldsRecursively,
29
26
  normalizeListFieldInPlace,
30
- Logger
27
+ Logger,
28
+ mapListItemsPropsOrSelfSpecificProps
31
29
  } from '../utils';
32
- import type { Config, Model, SSGRunOptions, YamlModelMap, YamlModel, YamlDataModel, YamlPageModel } from './config-types';
33
- import { loadPresets } from './presets-loader';
30
+ import type { Config, SSGRunOptions, Model, PageModel, DataModel, DataModelSingle } from './config-types';
31
+ import { extendModelsWithPresetsIds, loadPresets } from './presets-loader';
34
32
  import {
35
33
  LoadRawConfigResult,
36
34
  StopConfigWatch,
@@ -38,77 +36,165 @@ import {
38
36
  LATEST_STACKBIT_VERSION,
39
37
  STACKBIT_CONFIG_JS_FILES,
40
38
  STACKBIT_CONFIG_YAML_FILES,
41
- loadConfigWithModelsFromDir
39
+ loadStackbitYamlFromDir,
40
+ loadYamlModelsFromFiles,
41
+ mergeConfigModelsWithModelsFromFiles
42
42
  } from './config-loader-utils';
43
43
 
44
- export interface ConfigLoaderOptions {
44
+ export interface ConfigWithModelsPresetsResult {
45
+ valid: boolean;
46
+ config: Config | null;
47
+ errors: ConfigError[];
48
+ }
49
+
50
+ export async function loadConfigWithModelsPresetsAndValidate({
51
+ dirPath,
52
+ modelsSource,
53
+ stackbitConfigESBuildOutDir,
54
+ watchCallback,
55
+ logger
56
+ }: {
45
57
  dirPath: string;
46
58
  modelsSource?: ModelsSource;
47
59
  stackbitConfigESBuildOutDir?: string;
48
- watchCallback?: (result: ConfigLoaderResult) => void;
60
+ watchCallback?: (result: ConfigWithModelsPresetsResult) => void;
49
61
  logger?: Logger;
62
+ }): Promise<ConfigWithModelsPresetsResult & StopConfigWatch> {
63
+ const configResult = await loadConfigWithModels({
64
+ dirPath,
65
+ stackbitConfigESBuildOutDir,
66
+ watchCallback: watchCallback
67
+ ? async (configResult) => {
68
+ const configLoaderResult = await processConfigLoaderResult({ configResult, dirPath, modelsSource });
69
+ watchCallback(configLoaderResult);
70
+ }
71
+ : undefined,
72
+ logger
73
+ });
74
+
75
+ const configLoaderResult = await processConfigLoaderResult({ configResult, dirPath, modelsSource });
76
+ return {
77
+ ...configLoaderResult,
78
+ stop: configResult.stop,
79
+ reload: configResult.reload
80
+ };
50
81
  }
51
82
 
52
- export interface ConfigLoaderResult {
53
- valid: boolean;
83
+ export type ConfigWithModelsResult = {
54
84
  config: Config | null;
55
- errors: ConfigError[];
56
- }
85
+ errors: (ConfigLoadError | ConfigValidationError)[];
86
+ };
87
+
88
+ export async function loadConfigWithModels({
89
+ dirPath,
90
+ stackbitConfigESBuildOutDir,
91
+ watchCallback,
92
+ logger
93
+ }: {
94
+ dirPath: string;
95
+ stackbitConfigESBuildOutDir?: string;
96
+ watchCallback?: (result: ConfigWithModelsResult) => void;
97
+ logger?: Logger;
98
+ }): Promise<ConfigWithModelsResult & StopConfigWatch> {
99
+ const wrapConfigResult = async (configResult: LoadConfigResult): Promise<ConfigWithModelsResult> => {
100
+ if (!configResult.config) {
101
+ return {
102
+ config: null,
103
+ errors: configResult.errors
104
+ };
105
+ }
106
+ return await loadAndMergeModelsFromFiles(configResult.config);
107
+ };
57
108
 
58
- export type ConfigLoaderResultWithStop = ConfigLoaderResult & StopConfigWatch;
109
+ const rawConfigResult = await loadConfig({
110
+ dirPath,
111
+ stackbitConfigESBuildOutDir,
112
+ logger,
113
+ watchCallback: watchCallback
114
+ ? async (configResult: LoadConfigResult) => {
115
+ const wrappedResult = await wrapConfigResult(configResult);
116
+ watchCallback(wrappedResult);
117
+ }
118
+ : undefined
119
+ });
59
120
 
60
- export interface NormalizedValidationResult {
61
- valid: boolean;
62
- config: Config;
63
- errors: ConfigValidationError[];
64
- }
121
+ const wrappedResult = await wrapConfigResult(rawConfigResult);
65
122
 
66
- export interface RawConfigLoaderResult {
67
- config?: RawConfigWithPaths;
68
- errors: ConfigLoadError[];
123
+ return {
124
+ ...wrappedResult,
125
+ stop: rawConfigResult.stop,
126
+ reload: rawConfigResult.reload
127
+ };
69
128
  }
70
129
 
71
- export type RawConfigLoaderResultWithStop = RawConfigLoaderResult & StopConfigWatch;
130
+ export type LoadConfigResult =
131
+ | {
132
+ config: Config;
133
+ errors: ConfigLoadError[];
134
+ }
135
+ | {
136
+ config: null;
137
+ errors: ConfigLoadError[];
138
+ };
72
139
 
73
140
  export async function loadConfig({
74
141
  dirPath,
75
- modelsSource,
76
142
  stackbitConfigESBuildOutDir,
77
143
  watchCallback,
78
144
  logger
79
- }: ConfigLoaderOptions): Promise<ConfigLoaderResultWithStop> {
80
- let wrappedCallback: ((rawConfigResult: RawConfigLoaderResult) => Promise<void>) | undefined;
81
- if (watchCallback) {
82
- wrappedCallback = async (rawConfigResult: RawConfigLoaderResult) => {
83
- const configLoaderResult = await processConfigLoaderResult({ rawConfigResult, dirPath, modelsSource });
84
- watchCallback(configLoaderResult);
145
+ }: {
146
+ dirPath: string;
147
+ stackbitConfigESBuildOutDir?: string;
148
+ watchCallback?: (result: LoadConfigResult) => void;
149
+ logger?: Logger;
150
+ }): Promise<LoadConfigResult & StopConfigWatch> {
151
+ const normalizeConfigResult = (rawConfigResult: RawConfigLoaderResult): LoadConfigResult => {
152
+ if (!rawConfigResult.config) {
153
+ return {
154
+ config: null,
155
+ errors: [rawConfigResult.error]
156
+ };
157
+ }
158
+
159
+ // TODO: validate config base properties after normalizing and return validation errors
160
+ const config = normalizeConfig(rawConfigResult.config);
161
+ return {
162
+ config: config,
163
+ errors: []
85
164
  };
86
- }
165
+ };
87
166
 
88
167
  const rawConfigResult = await loadConfigFromDir({
89
168
  dirPath,
90
169
  stackbitConfigESBuildOutDir,
91
- watchCallback: wrappedCallback,
170
+ watchCallback: watchCallback
171
+ ? async (rawConfigResult: RawConfigLoaderResult) => {
172
+ const normalizedResult = await normalizeConfigResult(rawConfigResult);
173
+ watchCallback(normalizedResult);
174
+ }
175
+ : undefined,
92
176
  logger
93
177
  });
94
178
 
95
- const configLoaderResult = await processConfigLoaderResult({ rawConfigResult, dirPath, modelsSource });
96
- return Object.assign(configLoaderResult, {
179
+ const normalizedResult = await normalizeConfigResult(rawConfigResult);
180
+
181
+ return {
182
+ ...normalizedResult,
97
183
  stop: rawConfigResult.stop,
98
184
  reload: rawConfigResult.reload
99
- });
185
+ };
100
186
  }
101
187
 
102
188
  async function processConfigLoaderResult({
103
- rawConfigResult,
189
+ configResult,
104
190
  dirPath,
105
191
  modelsSource
106
192
  }: {
107
- rawConfigResult: RawConfigLoaderResult;
193
+ configResult: ConfigWithModelsResult;
108
194
  dirPath: string;
109
195
  modelsSource?: ModelsSource;
110
- }): Promise<ConfigLoaderResult> {
111
- const { config, errors: configLoadErrors } = rawConfigResult;
196
+ }): Promise<ConfigWithModelsPresetsResult> {
197
+ const { config, errors: configLoadErrors } = configResult;
112
198
 
113
199
  if (!config) {
114
200
  return {
@@ -120,67 +206,80 @@ async function processConfigLoaderResult({
120
206
 
121
207
  const { models: externalModels, errors: externalModelsLoadErrors } = await loadModelsFromExternalSource(config, dirPath, modelsSource);
122
208
 
123
- const extendedConfig = await extendConfig({ config, externalModels });
209
+ const mergedModels = mergeConfigModelsWithExternalModels({ configModels: config.models, externalModels });
210
+ const mergedConfig: Config = {
211
+ ...config,
212
+ models: mergedModels
213
+ };
124
214
 
125
- return {
126
- valid: extendedConfig.valid,
127
- config: extendedConfig.config,
128
- errors: [...configLoadErrors, ...externalModelsLoadErrors, ...extendedConfig.errors]
215
+ const normalizedResult = validateAndNormalizeConfig(mergedConfig);
216
+
217
+ const presetsResult = await loadPresets({ config: normalizedResult.config });
218
+
219
+ const modelsWithPresetIds = extendModelsWithPresetsIds({
220
+ models: normalizedResult.config.models,
221
+ presets: presetsResult.presets
222
+ });
223
+
224
+ const configWithPresets = {
225
+ ...normalizedResult.config,
226
+ models: modelsWithPresetIds,
227
+ presets: presetsResult.presets
129
228
  };
130
- }
131
229
 
132
- export async function extendConfig({
133
- config,
134
- externalModels
135
- }: {
136
- config: RawConfigWithPaths;
137
- externalModels?: Model[];
138
- }): Promise<{
139
- valid: boolean;
140
- config: Config;
141
- errors: (ConfigValidationError | ConfigPresetsError)[];
142
- }> {
143
- const normalizedResult = validateAndNormalizeConfig(config, externalModels);
144
- const presetsResult = await loadPresets(config.dirPath, normalizedResult.config);
145
230
  return {
146
231
  valid: normalizedResult.valid,
147
- config: presetsResult.config,
148
- errors: [...normalizedResult.errors, ...presetsResult.errors]
232
+ config: configWithPresets,
233
+ errors: [...configLoadErrors, ...externalModelsLoadErrors, ...normalizedResult.errors, ...presetsResult.errors]
149
234
  };
150
235
  }
151
236
 
152
- export function validateAndNormalizeConfig(config: RawConfigWithPaths, externalModels?: Model[]): NormalizedValidationResult {
153
- // extend config models having the "extends" property
154
- // this must be done before any validation as some properties like
155
- // the labelField will not work when validating models without extending them first
156
- const { models: extendedModels, errors: extendModelErrors } = extendModelMap(config.models);
157
- const extendedConfig: RawConfigWithPaths = {
237
+ export async function loadAndMergeModelsFromFiles(config: Config): Promise<{ config: Config; errors: (ConfigLoadError | ConfigValidationError)[] }> {
238
+ const { models: modelsFromFiles, errors: fileModelsErrors } = await loadYamlModelsFromFiles(config);
239
+ const { models: mergedModels, errors: mergeModelErrors } = mergeConfigModelsWithModelsFromFiles(config.models, modelsFromFiles);
240
+
241
+ const extendedConfig: Config = {
158
242
  ...config,
159
- models: extendedModels
243
+ models: mergedModels
160
244
  };
161
245
 
162
- const { config: mergedConfig, errors: externalModelsMergeErrors } = mergeConfigWithExternalModels(extendedConfig, externalModels);
246
+ return {
247
+ config: extendedConfig,
248
+ errors: [...fileModelsErrors, ...mergeModelErrors]
249
+ };
250
+ }
163
251
 
252
+ export function validateAndNormalizeConfig(config: Config): ConfigValidationResult {
164
253
  // validate the "contentModels" and extend config models with "contentModels"
165
254
  // this must be done before main config validation to make it independent of "contentModels".
166
- const { value: configWithContentModels, errors: contentModelsErrors } = validateAndExtendContentModels(mergedConfig);
255
+ const { config: configWithContentModels, errors: contentModelsErrors } = validateAndExtendContentModels(config);
167
256
 
168
257
  // normalize config - backward compatibility updates, adding extra fields like "markdown_content", "type" and "layout",
169
258
  // and setting other default values.
170
- const normalizedConfig = normalizeConfig(configWithContentModels);
259
+ const configWithNormalizedModels = normalizeModels(configWithContentModels);
171
260
 
172
261
  // validate config
173
- const { value: validatedConfig, errors: validationErrors } = validateConfig(normalizedConfig);
262
+ const { config: validatedConfig, errors: validationErrors } = validateConfig(configWithNormalizedModels);
174
263
 
175
- const errors = [...extendModelErrors, ...externalModelsMergeErrors, ...contentModelsErrors, ...validationErrors];
264
+ const errors = [...contentModelsErrors, ...validationErrors];
176
265
 
177
266
  return normalizeValidationResult({
178
267
  valid: _.isEmpty(errors),
179
- value: validatedConfig,
268
+ config: validatedConfig,
180
269
  errors: errors
181
270
  });
182
271
  }
183
272
 
273
+ export type RawConfigLoaderResult =
274
+ | {
275
+ config: RawConfigWithPaths;
276
+ error: null;
277
+ }
278
+ | {
279
+ config: null;
280
+ error: ConfigLoadError;
281
+ };
282
+
184
283
  export async function loadConfigFromDir({
185
284
  dirPath,
186
285
  stackbitConfigESBuildOutDir,
@@ -189,36 +288,46 @@ export async function loadConfigFromDir({
189
288
  }: {
190
289
  dirPath: string;
191
290
  stackbitConfigESBuildOutDir?: string;
192
- watchCallback?: (rawConfigResult: RawConfigLoaderResult) => void;
291
+ watchCallback?: (result: RawConfigLoaderResult) => void;
193
292
  logger?: Logger;
194
- }): Promise<RawConfigLoaderResultWithStop> {
293
+ }): Promise<RawConfigLoaderResult & StopConfigWatch> {
294
+ function wrapResult(result: LoadRawConfigResult, configFilePath: string): RawConfigLoaderResult {
295
+ if (result.error) {
296
+ return {
297
+ config: null,
298
+ error: result.error
299
+ };
300
+ } else {
301
+ return {
302
+ config: {
303
+ ...result.config,
304
+ dirPath: dirPath,
305
+ filePath: configFilePath
306
+ },
307
+ error: null
308
+ };
309
+ }
310
+ }
311
+
195
312
  // try to load stackbit config from YAML files
196
313
  try {
197
314
  const stackbitYamlPath = await getFirstExistingFile(STACKBIT_CONFIG_YAML_FILES, dirPath);
198
315
  if (stackbitYamlPath) {
199
- const { config, errors } = await loadConfigWithModelsFromDir(dirPath);
316
+ logger?.debug(`loading Stackbit configuration from ${stackbitYamlPath}`);
317
+ const result = await loadStackbitYamlFromDir(dirPath);
200
318
  let close: () => Promise<void> = async () => void 0;
201
319
  let reload: () => void = () => void 0;
202
320
  let stopped = false;
203
321
 
204
322
  if (watchCallback) {
205
- const watcher = chokidar.watch([...STACKBIT_CONFIG_YAML_FILES, '.stackbit'], {
323
+ const watcher = chokidar.watch([...STACKBIT_CONFIG_YAML_FILES], {
206
324
  cwd: dirPath,
207
325
  persistent: true,
208
326
  ignoreInitial: true
209
327
  });
210
328
  const throttledFileChange = _.throttle(async () => {
211
- const { config, errors } = await loadConfigWithModelsFromDir(dirPath);
212
- watchCallback({
213
- config: config
214
- ? {
215
- ...config,
216
- dirPath: dirPath,
217
- filePath: stackbitYamlPath
218
- }
219
- : undefined,
220
- errors
221
- });
329
+ const result = await loadStackbitYamlFromDir(dirPath);
330
+ watchCallback(wrapResult(result, stackbitYamlPath));
222
331
  }, 1000);
223
332
  const handleFileChange = (path: string) => {
224
333
  logger?.debug(`identified change in stackbit config file: ${path}, reloading config...`);
@@ -231,7 +340,8 @@ export async function loadConfigFromDir({
231
340
  watcher.on('unlinkDir', handleFileChange);
232
341
  watcher.on('error', (error) => {
233
342
  watchCallback({
234
- errors: [new ConfigLoadError(`Error loading Stackbit configuration: ${error.message}`, { originalError: error })]
343
+ config: null,
344
+ error: new ConfigLoadError(`Error loading Stackbit configuration: ${error.message}`, { originalError: error })
235
345
  });
236
346
  });
237
347
  close = async () => {
@@ -248,45 +358,23 @@ export async function loadConfigFromDir({
248
358
  }
249
359
 
250
360
  return {
251
- config: config
252
- ? {
253
- ...config,
254
- dirPath: dirPath,
255
- filePath: stackbitYamlPath
256
- }
257
- : undefined,
361
+ ...wrapResult(result, stackbitYamlPath),
258
362
  stop: close,
259
- reload: reload,
260
- errors: errors
363
+ reload: reload
261
364
  };
262
365
  }
263
366
  } catch (error: any) {
264
367
  return {
265
- errors: [new ConfigLoadError(`Error loading Stackbit configuration: ${error.message}`, { originalError: error })]
368
+ config: null,
369
+ error: new ConfigLoadError(`Error loading Stackbit configuration: ${error.message}`, { originalError: error })
266
370
  };
267
371
  }
268
372
 
269
- function wrapResult(result: LoadRawConfigResult, configFilePath: string): RawConfigLoaderResult {
270
- if (result.error) {
271
- return {
272
- errors: [result.error]
273
- };
274
- } else {
275
- return {
276
- config: {
277
- ...result.config,
278
- dirPath: dirPath,
279
- filePath: configFilePath
280
- },
281
- errors: []
282
- };
283
- }
284
- }
285
-
286
373
  // try to load stackbit config from JavaScript files
287
374
  try {
288
375
  const configFilePath = await getFirstExistingFile(STACKBIT_CONFIG_JS_FILES, dirPath);
289
376
  if (configFilePath) {
377
+ logger?.debug(`loading Stackbit configuration from: ${configFilePath}`);
290
378
  const configResult = await loadStackbitConfigFromJs({
291
379
  configPath: configFilePath,
292
380
  outDir: stackbitConfigESBuildOutDir ?? '.stackbit/cache',
@@ -302,17 +390,19 @@ export async function loadConfigFromDir({
302
390
  }
303
391
  } catch (error: any) {
304
392
  return {
305
- errors: [new ConfigLoadError(`Error loading Stackbit configuration: ${error.message}`, { originalError: error })]
393
+ config: null,
394
+ error: new ConfigLoadError(`Error loading Stackbit configuration: ${error.message}`, { originalError: error })
306
395
  };
307
396
  }
308
397
 
309
398
  return {
310
- errors: [new ConfigLoadError(STACKBIT_CONFIG_NOT_FOUND)]
399
+ config: null,
400
+ error: new ConfigLoadError(STACKBIT_CONFIG_NOT_FOUND)
311
401
  };
312
402
  }
313
403
 
314
404
  async function loadModelsFromExternalSource(
315
- config: RawConfigWithPaths,
405
+ config: Config,
316
406
  dirPath: string,
317
407
  modelsSource?: ModelsSource
318
408
  ): Promise<{ models: Model[]; errors: ConfigLoadError[] }> {
@@ -378,201 +468,225 @@ async function loadConfigFromDotStackbit(dirPath: string) {
378
468
  return _.isEmpty(config) ? null : config;
379
469
  }
380
470
 
381
- function mergeConfigWithExternalModels(config: RawConfigWithPaths, externalModels?: Model[]): { config: RawConfigWithPaths; errors: ConfigValidationError[] } {
382
- if (!externalModels || externalModels.length === 0) {
383
- return {
384
- config,
385
- errors: []
386
- };
471
+ export function mergeConfigModelsWithExternalModels({ configModels, externalModels }: { configModels: Model[]; externalModels: Model[] }): Model[] {
472
+ if (externalModels.length === 0) {
473
+ return configModels;
474
+ }
475
+ if (configModels.length === 0) {
476
+ return externalModels;
387
477
  }
388
478
 
389
- const stackbitModels = config?.models ?? {};
390
- const errors: ConfigValidationError[] = [];
479
+ const mergedModelsByName: Record<string, Model> = _.keyBy(externalModels, 'name');
391
480
 
392
- const models = _.reduce(
393
- externalModels,
394
- (modelMap: Record<string, YamlModel>, externalModel) => {
395
- const { name, ...rest } = externalModel;
396
- return Object.assign(modelMap, { [name]: rest });
397
- },
398
- {}
399
- );
400
-
401
- _.forEach(stackbitModels, (stackbitModel: any, modelName: any) => {
402
- let externalModel = models[modelName];
481
+ for (const configModel of configModels) {
482
+ const externalModel = mergedModelsByName[configModel.name];
403
483
  if (!externalModel) {
404
- return;
484
+ continue;
405
485
  }
406
486
 
407
- const modelType = stackbitModel.type ? (stackbitModel.type === 'config' ? 'data' : stackbitModel.type) : externalModel.type ?? 'object';
408
- const urlPath = modelType === 'page' ? stackbitModel?.urlPath ?? '/{slug}' : null;
487
+ const modelType = configModel.type ? (configModel.type === 'config' ? 'data' : configModel.type) : externalModel.type ?? 'object';
409
488
 
410
- externalModel = Object.assign(
489
+ let mergedModel: Model = Object.assign(
411
490
  {},
412
491
  externalModel,
413
- _.pick(stackbitModel, ['__metadata', 'label', 'description', 'thumbnail', 'singleInstance', 'readOnly', 'labelField', 'fieldGroups']),
414
- omitByNil({
415
- type: modelType,
416
- urlPath
417
- })
492
+ _.pick(configModel, ['__metadata', 'urlPath', 'label', 'description', 'thumbnail', 'singleInstance', 'readOnly', 'labelField', 'fieldGroups']),
493
+ { type: modelType }
418
494
  );
419
495
 
420
- externalModel = mapModelFieldsRecursively(externalModel as Model, (externalField, modelKeyPath) => {
421
- const stackbitField = getModelFieldForModelKeyPath(stackbitModel, modelKeyPath);
422
- if (!stackbitField) {
423
- return externalField;
424
- }
496
+ if (mergedModel.type === 'page' && !mergedModel.urlPath) {
497
+ mergedModel.urlPath = '/{slug}';
498
+ }
425
499
 
426
- let override = {};
427
- if (externalField.type === 'json' && stackbitField.type === 'style') {
428
- override = stackbitField;
429
- } else if (externalField.type === 'string' && stackbitField.type === 'color') {
430
- override = { type: 'color' };
431
- } else if (externalField.type === 'enum') {
432
- override = _.pick(stackbitField, ['options']);
433
- } else if (externalField.type === 'number') {
434
- override = _.pick(stackbitField, ['subtype', 'min', 'max', 'step', 'unit']);
435
- } else if (externalField.type === 'object') {
436
- override = _.pick(stackbitField, ['labelField', 'thumbnail', 'fieldGroups']);
500
+ mergedModel = mapModelFieldsRecursively(
501
+ mergedModel,
502
+ (externalField, modelKeyPath): Field => {
503
+ const stackbitField = getModelFieldForModelKeyPath(configModel, modelKeyPath);
504
+ if (!stackbitField) {
505
+ return externalField;
506
+ }
507
+
508
+ let override = {};
509
+ if (externalField.type === 'json' && stackbitField.type === 'style') {
510
+ override = stackbitField;
511
+ } else if (externalField.type === 'string' && stackbitField.type === 'color') {
512
+ override = { type: 'color' };
513
+ } else if (externalField.type === 'enum') {
514
+ override = _.pick(stackbitField, ['options']);
515
+ } else if (externalField.type === 'number') {
516
+ override = _.pick(stackbitField, ['subtype', 'min', 'max', 'step', 'unit']);
517
+ } else if (externalField.type === 'object') {
518
+ override = _.pick(stackbitField, ['labelField', 'thumbnail', 'fieldGroups']);
519
+ } else if (externalField.type === 'reference' || externalField.type === 'model') {
520
+ override = _.pick(stackbitField, ['models']);
521
+ }
522
+
523
+ return Object.assign(
524
+ {},
525
+ externalField,
526
+ _.pick(stackbitField, ['label', 'description', 'required', 'default', 'group', 'const', 'hidden', 'readOnly', 'controlType']),
527
+ override
528
+ );
437
529
  }
530
+ );
438
531
 
439
- return Object.assign(
440
- {},
441
- externalField,
442
- _.pick(stackbitField, ['label', 'description', 'required', 'default', 'group', 'const', 'hidden', 'readOnly', 'controlType']),
443
- override
444
- );
445
- }) as YamlModel;
532
+ mergedModelsByName[configModel.name] = mergedModel;
533
+ }
446
534
 
447
- models[modelName] = externalModel;
448
- });
535
+ return Object.values(mergedModelsByName);
536
+ }
537
+
538
+ function normalizeConfig(rawConfig: RawConfigWithPaths): Config {
539
+ const stackbitVersion = String(_.get(rawConfig, 'stackbitVersion', LATEST_STACKBIT_VERSION));
540
+ const ver = semver.coerce(stackbitVersion);
541
+ const isGTEStackbitYamlV5 = ver ? semver.satisfies(ver, '>=0.5.0') : false;
542
+
543
+ const { logicFields, models: modelMap, ...restConfig } = rawConfig;
544
+
545
+ // in stackbit.yaml 'models' are defined as object where keys are the model names,
546
+ // convert 'models' to array of objects and set their 'name' property to the model name
547
+ const models = _.reduce(modelMap, (accum: Model[], model, modelName) => accum.concat(Object.assign({ name: modelName }, model)), []);
449
548
 
450
549
  return {
451
- config: {
452
- ...config,
453
- models: models
454
- },
455
- errors: errors
550
+ ...restConfig,
551
+ stackbitVersion: stackbitVersion,
552
+ models: models,
553
+ noEncodeFields: logicFields,
554
+ hcrHandled: !stackbitVersion || _.get(rawConfig, 'customContentReload', _.get(rawConfig, 'hcrHandled', !isGTEStackbitYamlV5)),
555
+ internalStackbitRunnerOptions: getInternalStackbitRunnerOptions(rawConfig)
456
556
  };
457
557
  }
458
558
 
459
- function normalizeConfig(config: any): any {
460
- const pageLayoutKey = _.get(config, 'pageLayoutKey', 'layout');
461
- const objectTypeKey = _.get(config, 'objectTypeKey', 'type');
462
- const stackbitYamlVersion = String(_.get(config, 'stackbitVersion', ''));
559
+ function normalizeModels(config: Config): Config {
560
+ const pageLayoutKey = config.pageLayoutKey ?? 'layout';
561
+ const objectTypeKey = config.objectTypeKey ?? 'type';
562
+ const stackbitYamlVersion = String(config.stackbitVersion ?? '');
463
563
  const ver = semver.coerce(stackbitYamlVersion);
464
564
  const isStackbitYamlV2 = ver ? semver.satisfies(ver, '<0.3.0') : false;
465
- const isStackbitYamlV5 = ver ? semver.satisfies(ver, '>=0.5.0') : false;
466
- const models = config?.models || {};
565
+ const models = config.models;
566
+ const modelsByName = _.keyBy(models, 'name');
467
567
  const gitCMS = isGitCMS(config);
468
568
 
469
- rename(config, 'logicFields', 'noEncodeFields');
470
- config.hcrHandled = !stackbitYamlVersion || _.get(config, 'customContentReload', _.get(config, 'hcrHandled', !isStackbitYamlV5));
471
- config.internalStackbitRunnerOptions = getInternalStackbitRunnerOptions(config);
472
-
473
- _.forEach(models, (model, modelName) => {
474
- if (!model) {
475
- return;
476
- }
477
-
478
- if (!_.has(model, 'type')) {
479
- model.type = 'object';
480
- }
481
-
482
- // add model label if not set
483
- if (!_.has(model, 'label')) {
484
- model.label = _.startCase(modelName);
485
- }
569
+ const mappedModels = models.map(
570
+ (model): Model => {
571
+ // create shallow copy of the model to prevent mutation of original models
572
+ model = { ...model };
486
573
 
487
- if (_.has(model, 'fields') && !Array.isArray(model.fields)) {
488
- model.fields = [];
489
- }
490
-
491
- if (isPageModel(model)) {
492
- // rename old 'template' property to 'layout'
493
- rename(model, 'template', 'layout');
574
+ if (!_.has(model, 'type')) {
575
+ model.type = 'object';
576
+ }
494
577
 
495
- updatePageUrlPath(model);
578
+ // add model label if not set
579
+ if (!_.has(model, 'label')) {
580
+ model.label = _.startCase(model.name);
581
+ }
496
582
 
497
- if (gitCMS) {
498
- updatePageFilePath(model, config);
499
- addMarkdownContentField(model);
583
+ if (_.has(model, 'fields') && !Array.isArray(model.fields)) {
584
+ model.fields = [];
500
585
  }
501
- } else if (isDataModel(model) && gitCMS) {
502
- updateDataFilePath(model, config);
503
- }
504
586
 
505
- if (gitCMS) {
506
- // TODO: do not add pageLayoutKey and objectTypeKey fields to models,
507
- // The content validator should always assume these fields.
508
- // And when new objects created from UI, it should add these fields automatically.
509
587
  if (isPageModel(model)) {
510
- addLayoutFieldToPageModel(model, pageLayoutKey, modelName);
511
- } else if (isDataModel(model) && !isListDataModel(model)) {
512
- addObjectTypeKeyField(model, objectTypeKey, modelName);
513
- }
514
- }
588
+ // rename old 'template' property to 'layout'
589
+ rename(model, 'template', 'layout');
515
590
 
516
- if (isListDataModel(model)) {
517
- // 'items.type' of list model defaults to 'string', set it explicitly
518
- if (!_.has(model, 'items.type')) {
519
- _.set(model, 'items.type', 'string');
520
- }
521
- if (isObjectListItems(model.items)) {
522
- assignLabelFieldIfNeeded(model.items);
523
- }
524
- } else if (!_.has(model, 'labelField')) {
525
- assignLabelFieldIfNeeded(model);
526
- }
591
+ updatePageUrlPath(model);
527
592
 
528
- iterateModelFieldsRecursively(model, (field: any) => {
529
- // add field label if label is not set
530
- if (!_.has(field, 'label')) {
531
- field.label = _.startCase(field.name);
593
+ if (gitCMS) {
594
+ updatePageFilePath(model, config);
595
+ addMarkdownContentField(model);
596
+ }
597
+ } else if (isDataModel(model) && gitCMS) {
598
+ updateDataFilePath(model, config);
532
599
  }
533
600
 
534
- if (isListField(field)) {
535
- field = normalizeListFieldInPlace(field);
536
- field = field.items;
601
+ if (gitCMS) {
602
+ // TODO: do not add pageLayoutKey and objectTypeKey fields to models,
603
+ // The content validator should always assume these fields.
604
+ // And when new objects created from UI, it should add these fields automatically.
605
+ if (isPageModel(model)) {
606
+ addLayoutFieldToPageModel(model, pageLayoutKey, model.name);
607
+ } else if (isDataModel(model) && !isListDataModel(model)) {
608
+ addObjectTypeKeyField(model, objectTypeKey, model.name);
609
+ }
537
610
  }
538
611
 
539
- if (isObjectField(field)) {
540
- assignLabelFieldIfNeeded(field);
541
- } else if (isCustomModelField(field, models)) {
542
- // stackbit v0.2.0 compatibility
543
- // convert the old custom model field type: { type: 'action' }
544
- // to the new 'model' field type: { type: 'model', models: ['action'] }
545
- field.models = [field.type];
546
- field.type = 'model';
547
- } else if (field.type === 'models') {
548
- // stackbit v0.2.0 compatibility
549
- // convert the old 'models' field type: { type: 'models', models: ['link', 'button'] }
550
- // to the new 'model' field type: { type: 'model', models: ['link', 'button'] }
551
- field.type = 'model';
552
- field.models = _.get(field, 'models', []);
553
- } else if (field.type === 'model' && _.has(field, 'model')) {
554
- // stackbit v0.2.0 compatibility
555
- // convert the old 'model' field type: { type: 'model', model: 'link' }
556
- // to the new 'model' field type: { type: 'model', models: ['link'] }
557
- field.models = [field.model];
558
- delete field.model;
612
+ if (isListDataModel(model)) {
613
+ // 'items.type' of list model defaults to 'string', set it explicitly
614
+ normalizeListFieldInPlace(model);
615
+ if (isObjectListItems(model.items)) {
616
+ assignLabelFieldIfNeeded(model.items);
617
+ }
618
+ } else if (!_.has(model, 'labelField')) {
619
+ assignLabelFieldIfNeeded(model);
559
620
  }
560
621
 
561
- if (isStackbitYamlV2) {
562
- // in stackbit.yaml v0.2.x, the 'reference' field was what we have today as 'model' field:
563
- if (isReferenceField(field)) {
564
- field = (field as unknown) as FieldModel;
565
- field.type = 'model';
566
- field.models = _.get(field, 'models', []);
622
+ return mapModelFieldsRecursively(
623
+ model,
624
+ (field: Field): Field => {
625
+ // create shallow copy of the field to prevent mutation of original field
626
+ field = { ...field };
627
+
628
+ // add field label if label is not set
629
+ if (!_.has(field, 'label')) {
630
+ field.label = _.startCase(field.name);
631
+ }
632
+
633
+ return mapListItemsPropsOrSelfSpecificProps(field, (fieldSpecificProps) => {
634
+ if (isObjectField(fieldSpecificProps)) {
635
+ assignLabelFieldIfNeeded(fieldSpecificProps);
636
+ } else if (isCustomModelField(fieldSpecificProps, modelsByName)) {
637
+ // stackbit v0.2.0 compatibility
638
+ // convert the old custom model field type: { type: 'action' }
639
+ // to the new 'model' field type: { type: 'model', models: ['action'] }
640
+ fieldSpecificProps = {
641
+ ...fieldSpecificProps,
642
+ type: 'model',
643
+ models: [fieldSpecificProps.type]
644
+ };
645
+ } else if ((fieldSpecificProps as any).type === 'models') {
646
+ // stackbit v0.2.0 compatibility
647
+ // convert the old 'models' field type: { type: 'models', models: ['link', 'button'] }
648
+ // to the new 'model' field type: { type: 'model', models: ['link', 'button'] }
649
+ fieldSpecificProps = {
650
+ ...fieldSpecificProps,
651
+ type: 'model',
652
+ models: _.get(fieldSpecificProps, 'models', [])
653
+ };
654
+ } else if (fieldSpecificProps.type === 'model' && _.has(fieldSpecificProps, 'model')) {
655
+ // stackbit v0.2.0 compatibility
656
+ // convert the old 'model' field type: { type: 'model', model: 'link' }
657
+ // to the new 'model' field type: { type: 'model', models: ['link'] }
658
+ const { model, ...rest } = fieldSpecificProps as any;
659
+ fieldSpecificProps = {
660
+ ...rest,
661
+ models: [model]
662
+ };
663
+ }
664
+
665
+ if (isStackbitYamlV2) {
666
+ // in stackbit.yaml v0.2.x, the 'reference' field was what we have today as 'model' field:
667
+ if (isReferenceField(fieldSpecificProps)) {
668
+ fieldSpecificProps = {
669
+ ...fieldSpecificProps,
670
+ type: 'model',
671
+ models: _.get(fieldSpecificProps, 'models', [])
672
+ };
673
+ }
674
+ }
675
+
676
+ return fieldSpecificProps;
677
+ });
567
678
  }
568
- }
569
- });
570
- });
679
+ );
680
+ }
681
+ );
571
682
 
572
- return config;
683
+ return {
684
+ ...config,
685
+ models: mappedModels
686
+ };
573
687
  }
574
688
 
575
- function updatePageUrlPath(model: YamlPageModel) {
689
+ function updatePageUrlPath(model: PageModel) {
576
690
  // set default urlPath if not set
577
691
  if (!model.urlPath) {
578
692
  model.urlPath = '/{slug}';
@@ -585,7 +699,7 @@ function updatePageUrlPath(model: YamlPageModel) {
585
699
  * If the model has no `filePath` property, then `filePath` is naively inferred by
586
700
  * prefixing `urlPath` with `pagesDir` and appending the `.md` extension.
587
701
  */
588
- function updatePageFilePath(model: YamlPageModel, config: Config) {
702
+ function updatePageFilePath(model: PageModel, config: Config) {
589
703
  let filePath;
590
704
  if (model.filePath) {
591
705
  filePath = model.filePath;
@@ -605,7 +719,7 @@ function updatePageFilePath(model: YamlPageModel, config: Config) {
605
719
  model.filePath = path.join(parentDir, filePath);
606
720
  }
607
721
 
608
- function updateDataFilePath(model: YamlDataModel, config: Config) {
722
+ function updateDataFilePath(model: DataModel, config: Config) {
609
723
  let filePath;
610
724
  if (model.filePath) {
611
725
  filePath = model.filePath;
@@ -619,7 +733,7 @@ function updateDataFilePath(model: YamlDataModel, config: Config) {
619
733
  model.filePath = path.join(parentDir, filePath);
620
734
  }
621
735
 
622
- function addMarkdownContentField(model: YamlPageModel) {
736
+ function addMarkdownContentField(model: PageModel) {
623
737
  if (model.hideContent) {
624
738
  return;
625
739
  }
@@ -635,7 +749,7 @@ function addMarkdownContentField(model: YamlPageModel) {
635
749
  });
636
750
  }
637
751
 
638
- function addLayoutFieldToPageModel(model: any, pageLayoutKey: any, modelName: string) {
752
+ function addLayoutFieldToPageModel(model: PageModel, pageLayoutKey: string, modelName: string) {
639
753
  if (_.intersection(_.keys(model), ['file', 'folder', 'match', 'exclude']).length === 0 && !_.get(model, 'layout')) {
640
754
  model.layout = modelName;
641
755
  }
@@ -656,7 +770,7 @@ function addLayoutFieldToPageModel(model: any, pageLayoutKey: any, modelName: st
656
770
  });
657
771
  }
658
772
 
659
- function addObjectTypeKeyField(model: any, objectTypeKey: string, modelName: string) {
773
+ function addObjectTypeKeyField(model: DataModelSingle, objectTypeKey: string, modelName: string) {
660
774
  const hasObjectTypeField = _.find(_.get(model, 'fields'), { name: objectTypeKey });
661
775
  if (hasObjectTypeField) {
662
776
  return;
@@ -673,40 +787,42 @@ function addObjectTypeKeyField(model: any, objectTypeKey: string, modelName: str
673
787
 
674
788
  /**
675
789
  * Returns model names referenced by polymorphic 'model' and 'reference' fields.
676
- * That is, fields that can hold objects of different types.
677
790
  *
678
791
  * @param field
679
792
  */
680
- function getReferencedModelNames(field: any) {
681
- if (isListField(field)) {
682
- field = getListFieldItems(field);
683
- }
793
+ function getReferencedModelNames(field: Field) {
794
+ const fieldSpecificProps = getListItemsOrSelf(field);
684
795
  // TODO: add type field to model fields inside container update/create object logic rather adding type to schema
685
796
  // 'object' models referenced by 'model' fields should have 'type' field
686
797
  // if these fields have than 1 model.
687
798
  // 'data' models referenced by 'reference' fields should always have 'type' field.
688
799
  let referencedModelNames: string[] = [];
689
- if (isModelField(field) && field.models?.length > 1) {
690
- const modelNames = field.models;
800
+ if (isModelField(fieldSpecificProps) && fieldSpecificProps.models?.length > 1) {
801
+ const modelNames = fieldSpecificProps.models;
691
802
  referencedModelNames = _.union(referencedModelNames, modelNames);
692
- } else if (isReferenceField(field) && field.models?.length > 0) {
693
- const modelNames = field.models;
803
+ } else if (isReferenceField(fieldSpecificProps) && fieldSpecificProps.models?.length > 0) {
804
+ const modelNames = fieldSpecificProps.models;
694
805
  referencedModelNames = _.union(referencedModelNames, modelNames);
695
806
  }
696
807
  return referencedModelNames;
697
808
  }
698
809
 
699
- function validateAndExtendContentModels(config: RawConfigWithPaths): ConfigValidationResult {
810
+ function validateAndExtendContentModels(
811
+ config: Config
812
+ ): {
813
+ config: Config;
814
+ errors: ConfigValidationError[];
815
+ } {
700
816
  const contentModels = config.contentModels ?? {};
701
- const models = config.models ?? {};
817
+ const models = config.models ?? [];
702
818
 
819
+ // external models already merged in mergeConfigModelsWithExternalModels function
703
820
  const externalModels = !isGitCMS(config);
704
821
  const emptyContentModels = _.isEmpty(contentModels);
705
822
 
706
823
  if (externalModels || emptyContentModels) {
707
824
  return {
708
- valid: true,
709
- value: config,
825
+ config: config,
710
826
  errors: []
711
827
  };
712
828
  }
@@ -715,36 +831,36 @@ function validateAndExtendContentModels(config: RawConfigWithPaths): ConfigValid
715
831
 
716
832
  if (_.isEmpty(models)) {
717
833
  return {
718
- valid: validationResult.valid,
719
- value: config,
834
+ config: config,
720
835
  errors: validationResult.errors
721
836
  };
722
837
  }
723
838
 
724
- const extendedModels: YamlModelMap = _.mapValues(
725
- models,
726
- (model, modelName): YamlModel => {
727
- const contentModel = validationResult.value.contentModels![modelName];
839
+ const extendedModels = models.map(
840
+ (model): Model => {
841
+ const contentModel = validationResult.contentModels![model.name];
728
842
  if (!contentModel) {
729
843
  return model;
730
844
  }
731
845
  if (_.get(contentModel, '__metadata.invalid')) {
732
846
  return model;
733
847
  }
734
- if (contentModel.isPage && (!model.type || ['object', 'page'].includes(model.type))) {
848
+ const { isPage, newFilePath, ...restContentModel } = contentModel;
849
+ const { type, ...restModel } = model;
850
+ if (isPage && (!type || ['object', 'page'].includes(type))) {
735
851
  return {
736
852
  type: 'page',
737
- ...(contentModel.newFilePath ? { filePath: contentModel.newFilePath } : {}),
738
- ..._.omit(contentModel, ['isPage', 'newFilePath']),
739
- ..._.omit(model, 'type')
740
- } as YamlModel;
741
- } else if (!contentModel.isPage && (!model.type || ['object', 'data'].includes(model.type))) {
853
+ ...(newFilePath ? { filePath: newFilePath } : {}),
854
+ ...restContentModel,
855
+ ...restModel
856
+ };
857
+ } else if (!isPage && (!type || ['object', 'data'].includes(type))) {
742
858
  return {
743
859
  type: 'data',
744
- ...(contentModel.newFilePath ? { filePath: contentModel.newFilePath } : {}),
745
- ..._.omit(contentModel, ['isPage', 'newFilePath']),
746
- ..._.omit(model, 'type')
747
- } as YamlModel;
860
+ ...(newFilePath ? { filePath: newFilePath } : {}),
861
+ ...restContentModel,
862
+ ...restModel
863
+ };
748
864
  } else {
749
865
  return model;
750
866
  }
@@ -752,8 +868,7 @@ function validateAndExtendContentModels(config: RawConfigWithPaths): ConfigValid
752
868
  );
753
869
 
754
870
  return {
755
- valid: validationResult.valid,
756
- value: {
871
+ config: {
757
872
  ...config,
758
873
  models: extendedModels
759
874
  },
@@ -761,17 +876,16 @@ function validateAndExtendContentModels(config: RawConfigWithPaths): ConfigValid
761
876
  };
762
877
  }
763
878
 
764
- function normalizeValidationResult(validationResult: ConfigValidationResult): NormalizedValidationResult {
879
+ function normalizeValidationResult(validationResult: ConfigValidationResult): ConfigValidationResult {
765
880
  validationResult = filterAndOrderConfigFields(validationResult);
766
- convertModelGroupsToModelList(validationResult);
767
- return convertModelsToArray(validationResult);
881
+ return convertModelGroupsToModelListInPlace(validationResult);
768
882
  }
769
883
 
770
884
  function filterAndOrderConfigFields(validationResult: ConfigValidationResult): ConfigValidationResult {
771
885
  // TODO: check if we can move filtering and sorting to Joi
772
886
  return {
773
887
  ...validationResult,
774
- value: _.pick(validationResult.value, [
888
+ config: _.pick(validationResult.config, [
775
889
  'stackbitVersion',
776
890
  'ssgName',
777
891
  'ssgVersion',
@@ -814,7 +928,7 @@ function filterAndOrderConfigFields(validationResult: ConfigValidationResult): C
814
928
  'encodedFieldTypes', // obsolete, left for backward compatibility
815
929
  'noEncodeFields', // obsolete, left for backward compatibility
816
930
  'omitFields' // obsolete, left for backward compatibility
817
- ]) as RawConfigWithPaths
931
+ ]) as Config
818
932
  };
819
933
  }
820
934
 
@@ -822,18 +936,18 @@ function filterAndOrderConfigFields(validationResult: ConfigValidationResult): C
822
936
  * Collects models groups and injects them into the `models` array of the
823
937
  * `reference` and `model` field types
824
938
  */
825
- function convertModelGroupsToModelList(validationResult: ConfigValidationResult) {
826
- const models = validationResult.value?.models ?? {};
939
+ function convertModelGroupsToModelListInPlace(validationResult: ConfigValidationResult): ConfigValidationResult {
940
+ const models = validationResult.config?.models ?? [];
827
941
 
828
942
  const groupMap = _.reduce(
829
943
  models,
830
- (groupMap, model, modelName) => {
944
+ (groupMap, model) => {
831
945
  if (!model.groups) {
832
946
  return groupMap;
833
947
  }
834
948
  const key = model?.type === 'object' ? 'objectModels' : 'documentModels';
835
949
  _.forEach(model.groups, (groupName) => {
836
- append(groupMap, [groupName, key], modelName);
950
+ append(groupMap, [groupName, key], model.name);
837
951
  });
838
952
  delete model.groups;
839
953
  return groupMap;
@@ -848,92 +962,41 @@ function convertModelGroupsToModelList(validationResult: ConfigValidationResult)
848
962
  });
849
963
  });
850
964
 
851
- _.forEach(models, (model) => {
852
- iterateModelFieldsRecursively(model, (field: any) => {
853
- if (isListField(field)) {
854
- field = field.items;
855
- }
856
- if (field.groups) {
857
- let key: string | null = null;
858
- if (isModelField(field)) {
859
- key = 'objectModels';
860
- } else if (isReferenceField(field)) {
861
- key = 'documentModels';
862
- }
863
- if (key) {
864
- field.models = _.reduce(
865
- field.groups,
965
+ const mappedModels = models.map((model) => {
966
+ return mapModelFieldsRecursively(
967
+ model,
968
+ (field): Field => {
969
+ return mapListItemsPropsOrSelfSpecificProps(field, (fieldSpecificProps) => {
970
+ if (!isModelField(fieldSpecificProps) && !isReferenceField(fieldSpecificProps)) {
971
+ return fieldSpecificProps;
972
+ }
973
+ const { ...cloned } = fieldSpecificProps;
974
+ const key = isModelField(fieldSpecificProps) ? 'objectModels' : 'documentModels';
975
+ const modelNames = _.reduce(
976
+ cloned.groups,
866
977
  (modelNames, groupName) => {
867
978
  const objectModelNames = _.get(groupMap, [groupName, key], []);
868
979
  return _.uniq(modelNames.concat(objectModelNames));
869
980
  },
870
- field.models || []
981
+ fieldSpecificProps.models || []
871
982
  );
872
- }
873
- delete field.groups;
983
+ delete cloned.groups;
984
+ return Object.assign(cloned, { models: modelNames });
985
+ });
874
986
  }
875
- });
876
- });
877
- }
878
-
879
- function convertModelsToArray(validationResult: ConfigValidationResult): NormalizedValidationResult {
880
- const config = validationResult.value;
881
- const { stackbitVersion = LATEST_STACKBIT_VERSION, models, ...rest } = config;
882
-
883
- // in stackbit.yaml 'models' are defined as object where keys are the model names,
884
- // convert 'models' to array of objects and set their 'name' property to the
885
- // model name
886
- const modelMap = models ?? {};
887
- const modelArray: Model[] = (_.map(
888
- modelMap,
889
- (yamlModel: YamlModel, modelName: string): Model => {
890
- return {
891
- name: modelName,
892
- ...yamlModel
893
- };
894
- }
895
- ) as unknown) as Model[];
896
-
897
- if (!isGitCMS(config)) {
898
- addImageModel(modelArray);
899
- }
900
-
901
- const convertedErrors = _.map(validationResult.errors, (error: ConfigValidationError) => {
902
- if (error.fieldPath[0] === 'models' && typeof error.fieldPath[1] == 'string') {
903
- const modelName = error.fieldPath[1];
904
- const modelIndex = _.findIndex(modelArray, { name: modelName });
905
- const normFieldPath = error.fieldPath.slice();
906
- normFieldPath[1] = modelIndex;
907
- error.normFieldPath = normFieldPath;
908
- }
909
- return error;
987
+ );
910
988
  });
911
989
 
912
990
  return {
913
- valid: validationResult.valid,
991
+ ...validationResult,
914
992
  config: {
915
- stackbitVersion,
916
- ...rest,
917
- models: modelArray
918
- },
919
- errors: convertedErrors
993
+ ...validationResult.config,
994
+ models: mappedModels
995
+ }
920
996
  };
921
997
  }
922
998
 
923
- function addImageModel(models: Model[]) {
924
- models.push({
925
- type: 'image',
926
- name: '__image_model',
927
- label: 'Image',
928
- labelField: 'title',
929
- fields: [
930
- { name: 'title', type: 'string' },
931
- { name: 'url', type: 'string' }
932
- ]
933
- });
934
- }
935
-
936
- function isGitCMS(config: RawConfigWithPaths) {
999
+ function isGitCMS(config: Config) {
937
1000
  return !config.contentSources && (!config.cmsName || config.cmsName === 'git');
938
1001
  }
939
1002