@stackbit/sdk 0.2.39 → 0.3.1

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 (65) hide show
  1. package/dist/analyzer/site-analyzer.d.ts.map +1 -1
  2. package/dist/analyzer/site-analyzer.js +3 -1
  3. package/dist/analyzer/site-analyzer.js.map +1 -1
  4. package/dist/config/config-errors.d.ts +3 -0
  5. package/dist/config/config-errors.d.ts.map +1 -1
  6. package/dist/config/config-errors.js +7 -2
  7. package/dist/config/config-errors.js.map +1 -1
  8. package/dist/config/config-loader-esbuild.d.ts +3 -14
  9. package/dist/config/config-loader-esbuild.d.ts.map +1 -1
  10. package/dist/config/config-loader-esbuild.js +19 -2
  11. package/dist/config/config-loader-esbuild.js.map +1 -1
  12. package/dist/config/config-loader-static.d.ts +14 -0
  13. package/dist/config/config-loader-static.d.ts.map +1 -0
  14. package/dist/config/config-loader-static.js +170 -0
  15. package/dist/config/config-loader-static.js.map +1 -0
  16. package/dist/config/config-loader-utils.d.ts +34 -0
  17. package/dist/config/config-loader-utils.d.ts.map +1 -0
  18. package/dist/config/config-loader-utils.js +211 -0
  19. package/dist/config/config-loader-utils.js.map +1 -0
  20. package/dist/config/config-loader.d.ts +13 -9
  21. package/dist/config/config-loader.d.ts.map +1 -1
  22. package/dist/config/config-loader.js +175 -199
  23. package/dist/config/config-loader.js.map +1 -1
  24. package/dist/config/config-schema/style-field-schema.d.ts.map +1 -1
  25. package/dist/config/config-schema/style-field-schema.js.map +1 -1
  26. package/dist/config/config-schema.d.ts +2 -2
  27. package/dist/config/config-schema.d.ts.map +1 -1
  28. package/dist/config/config-schema.js +5 -6
  29. package/dist/config/config-schema.js.map +1 -1
  30. package/dist/config/config-types.d.ts +32 -2
  31. package/dist/config/config-types.d.ts.map +1 -1
  32. package/dist/config/config-validator.d.ts +5 -3
  33. package/dist/config/config-validator.d.ts.map +1 -1
  34. package/dist/config/config-validator.js.map +1 -1
  35. package/dist/config/config-writer.d.ts +1 -4
  36. package/dist/config/config-writer.d.ts.map +1 -1
  37. package/dist/config/config-writer.js +3 -27
  38. package/dist/config/config-writer.js.map +1 -1
  39. package/dist/content/content-schema.js +5 -6
  40. package/dist/content/content-schema.js.map +1 -1
  41. package/dist/index.d.ts +4 -2
  42. package/dist/index.d.ts.map +1 -1
  43. package/dist/index.js +6 -2
  44. package/dist/index.js.map +1 -1
  45. package/dist/utils/index.d.ts +1 -1
  46. package/dist/utils/index.d.ts.map +1 -1
  47. package/dist/utils/model-extender.d.ts +1 -1
  48. package/dist/utils/model-extender.d.ts.map +1 -1
  49. package/dist/utils/model-extender.js.map +1 -1
  50. package/package.json +4 -3
  51. package/src/analyzer/site-analyzer.ts +3 -1
  52. package/src/config/config-errors.ts +7 -1
  53. package/src/config/config-loader-esbuild.ts +24 -19
  54. package/src/config/config-loader-static.ts +210 -0
  55. package/src/config/config-loader-utils.ts +253 -0
  56. package/src/config/config-loader.ts +210 -237
  57. package/src/config/config-schema/style-field-schema.ts +1 -4
  58. package/src/config/config-schema.ts +17 -18
  59. package/src/config/config-types.ts +31 -2
  60. package/src/config/config-validator.ts +5 -3
  61. package/src/config/config-writer.ts +2 -30
  62. package/src/content/content-schema.ts +17 -18
  63. package/src/index.ts +4 -2
  64. package/src/utils/index.ts +1 -1
  65. package/src/utils/model-extender.ts +2 -2
@@ -1,45 +1,51 @@
1
1
  import path from 'path';
2
2
  import fse from 'fs-extra';
3
- import yaml from 'js-yaml';
3
+ import chokidar from 'chokidar';
4
4
  import semver from 'semver';
5
5
  import _ from 'lodash';
6
6
 
7
+ import { append, getFirstExistingFile, omitByNil, prepend, rename } from '@stackbit/utils';
8
+
7
9
  import { ConfigValidationResult, validateConfig, validateContentModels } from './config-validator';
8
- import {
9
- ConfigError,
10
- ConfigLoadError,
11
- ConfigPresetsError,
12
- ConfigValidationError
13
- } from './config-errors';
14
- import { loadStackbitConfigFromJs, LoadStackbitConfigResult, StopConfigWatch } from './config-loader-esbuild';
10
+ import { ConfigError, ConfigLoadError, ConfigPresetsError, ConfigValidationError, STACKBIT_CONFIG_NOT_FOUND } from './config-errors';
11
+ import { loadStackbitConfigFromJs } from './config-loader-esbuild';
15
12
  import {
16
13
  assignLabelFieldIfNeeded,
17
14
  extendModelMap,
18
15
  getListFieldItems,
16
+ getModelFieldForModelKeyPath,
19
17
  isCustomModelField,
20
- isEnumField,
18
+ isDataModel,
21
19
  isListDataModel,
22
20
  isListField,
23
21
  isModelField,
24
22
  isObjectField,
25
23
  isObjectListItems,
26
24
  isPageModel,
27
- isDataModel,
28
25
  isReferenceField,
29
26
  iterateModelFieldsRecursively,
30
27
  mapModelFieldsRecursively,
31
28
  normalizeListFieldInPlace,
32
- getModelFieldForModelKeyPath
29
+ Logger
33
30
  } from '../utils';
34
- import { append, prepend, omitByNil, parseFile, readDirRecursively, reducePromise, rename, getFirstExistingFile } from '@stackbit/utils';
35
- import { Config, DataModel, FieldEnum, FieldModel, FieldObjectProps, Model, ModelsSource, PageModel, YamlModel } from './config-types';
31
+ import { Config, DataModel, FieldModel, Model, ModelMap, ModelsSource, PageModel, YamlModel } from './config-types';
36
32
  import { loadPresets } from './presets-loader';
33
+ import {
34
+ LoadRawConfigResult,
35
+ StopConfigWatch,
36
+ RawConfigWithPaths,
37
+ LATEST_STACKBIT_VERSION,
38
+ STACKBIT_CONFIG_JS_FILES,
39
+ STACKBIT_CONFIG_YAML_FILES,
40
+ loadConfigWithModelsFromDir
41
+ } from './config-loader-utils';
37
42
 
38
43
  export interface ConfigLoaderOptions {
39
44
  dirPath: string;
40
45
  modelsSource?: ModelsSource;
41
46
  stackbitConfigESBuildOutDir?: string;
42
47
  watchCallback?: (result: ConfigLoaderResult) => void;
48
+ logger?: Logger;
43
49
  }
44
50
 
45
51
  export interface ConfigLoaderResult {
@@ -48,6 +54,8 @@ export interface ConfigLoaderResult {
48
54
  errors: ConfigError[];
49
55
  }
50
56
 
57
+ export type ConfigLoaderResultWithStop = ConfigLoaderResult & StopConfigWatch;
58
+
51
59
  export interface NormalizedValidationResult {
52
60
  valid: boolean;
53
61
  config: Config;
@@ -55,29 +63,39 @@ export interface NormalizedValidationResult {
55
63
  }
56
64
 
57
65
  export interface RawConfigLoaderResult {
58
- config?: Record<string, any>;
66
+ config?: RawConfigWithPaths;
59
67
  errors: ConfigLoadError[];
60
68
  }
61
69
 
70
+ export type RawConfigLoaderResultWithStop = RawConfigLoaderResult & StopConfigWatch;
71
+
62
72
  export async function loadConfig({
63
73
  dirPath,
64
74
  modelsSource,
65
75
  stackbitConfigESBuildOutDir,
66
- watchCallback
67
- }: ConfigLoaderOptions): Promise<ConfigLoaderResult & StopConfigWatch> {
76
+ watchCallback,
77
+ logger
78
+ }: ConfigLoaderOptions): Promise<ConfigLoaderResultWithStop> {
79
+ let wrappedCallback: ((rawConfigResult: RawConfigLoaderResult) => Promise<void>) | undefined;
80
+ if (watchCallback) {
81
+ wrappedCallback = async (rawConfigResult: RawConfigLoaderResult) => {
82
+ const configLoaderResult = await processConfigLoaderResult({ rawConfigResult, dirPath, modelsSource });
83
+ watchCallback(configLoaderResult);
84
+ };
85
+ }
86
+
68
87
  const rawConfigResult = await loadConfigFromDir({
69
88
  dirPath,
70
89
  stackbitConfigESBuildOutDir,
71
- watchCallback: watchCallback
72
- ? async (rawConfigResult: RawConfigLoaderResult) => {
73
- const configLoaderResult = await processConfigLoaderResult({ rawConfigResult, dirPath, modelsSource });
74
- watchCallback(configLoaderResult);
75
- }
76
- : undefined
90
+ watchCallback: wrappedCallback,
91
+ logger
77
92
  });
78
93
 
79
94
  const configLoaderResult = await processConfigLoaderResult({ rawConfigResult, dirPath, modelsSource });
80
- return Object.assign(configLoaderResult, { stop: rawConfigResult.stop });
95
+ return Object.assign(configLoaderResult, {
96
+ stop: rawConfigResult.stop,
97
+ reload: rawConfigResult.reload
98
+ });
81
99
  }
82
100
 
83
101
  async function processConfigLoaderResult({
@@ -101,7 +119,7 @@ async function processConfigLoaderResult({
101
119
 
102
120
  const { models: externalModels, errors: externalModelsLoadErrors } = await loadModelsFromExternalSource(config, dirPath, modelsSource);
103
121
 
104
- const extendedConfig = await extendConfig({ dirPath, config, externalModels });
122
+ const extendedConfig = await extendConfig({ config, externalModels });
105
123
 
106
124
  return {
107
125
  valid: extendedConfig.valid,
@@ -111,12 +129,10 @@ async function processConfigLoaderResult({
111
129
  }
112
130
 
113
131
  export async function extendConfig({
114
- dirPath,
115
132
  config,
116
133
  externalModels
117
134
  }: {
118
- dirPath: string;
119
- config: Record<string, any>;
135
+ config: RawConfigWithPaths;
120
136
  externalModels?: Model[];
121
137
  }): Promise<{
122
138
  valid: boolean;
@@ -124,7 +140,7 @@ export async function extendConfig({
124
140
  errors: (ConfigValidationError | ConfigPresetsError)[];
125
141
  }> {
126
142
  const normalizedResult = validateAndNormalizeConfig(config, externalModels);
127
- const presetsResult = await loadPresets(dirPath, normalizedResult.config);
143
+ const presetsResult = await loadPresets(config.dirPath, normalizedResult.config);
128
144
  return {
129
145
  valid: normalizedResult.valid,
130
146
  config: presetsResult.config,
@@ -132,12 +148,12 @@ export async function extendConfig({
132
148
  };
133
149
  }
134
150
 
135
- export function validateAndNormalizeConfig(config: Record<string, any>, externalModels?: Model[]): NormalizedValidationResult {
151
+ export function validateAndNormalizeConfig(config: RawConfigWithPaths, externalModels?: Model[]): NormalizedValidationResult {
136
152
  // extend config models having the "extends" property
137
153
  // this must be done before any validation as some properties like
138
154
  // the labelField will not work when validating models without extending them first
139
- const { models: extendedModels, errors: extendModelErrors } = extendModelMap(config.models as any);
140
- const extendedConfig = {
155
+ const { models: extendedModels, errors: extendModelErrors } = extendModelMap(config.models);
156
+ const extendedConfig: RawConfigWithPaths = {
141
157
  ...config,
142
158
  models: extendedModels
143
159
  };
@@ -167,32 +183,80 @@ export function validateAndNormalizeConfig(config: Record<string, any>, external
167
183
  export async function loadConfigFromDir({
168
184
  dirPath,
169
185
  stackbitConfigESBuildOutDir,
170
- watchCallback
186
+ watchCallback,
187
+ logger
171
188
  }: {
172
189
  dirPath: string;
173
190
  stackbitConfigESBuildOutDir?: string;
174
191
  watchCallback?: (rawConfigResult: RawConfigLoaderResult) => void;
175
- }): Promise<RawConfigLoaderResult & StopConfigWatch> {
192
+ logger?: Logger
193
+ }): Promise<RawConfigLoaderResultWithStop> {
176
194
  // try to load stackbit config from YAML files
177
195
  try {
178
- const stackbitYamlPath = path.join(dirPath, 'stackbit.yaml');
179
- const stackbitYamlExists = await fse.pathExists(stackbitYamlPath);
180
- if (stackbitYamlExists) {
181
- const stackbitYamlResult = await loadConfigFromStackbitYaml(stackbitYamlPath);
182
- if (stackbitYamlResult.error) {
183
- return { errors: [stackbitYamlResult.error] };
196
+ const stackbitYamlPath = await getFirstExistingFile(STACKBIT_CONFIG_YAML_FILES, dirPath);
197
+ if (stackbitYamlPath) {
198
+ const { config, errors } = await loadConfigWithModelsFromDir(dirPath);
199
+ let close: () => Promise<void> = async () => void 0;
200
+ let reload: () => void = () => void 0;
201
+ let stopped = false;
202
+
203
+ if (watchCallback) {
204
+ const watcher = chokidar.watch([...STACKBIT_CONFIG_YAML_FILES, '.stackbit'], {
205
+ cwd: dirPath,
206
+ persistent: true,
207
+ ignoreInitial: true
208
+ });
209
+ const throttledFileChange = _.throttle(async () => {
210
+ const { config, errors } = await loadConfigWithModelsFromDir(dirPath);
211
+ watchCallback({
212
+ config: config
213
+ ? {
214
+ ...config,
215
+ dirPath: dirPath,
216
+ filePath: stackbitYamlPath
217
+ }
218
+ : undefined,
219
+ errors
220
+ });
221
+ }, 1000);
222
+ const handleFileChange = (path: string) => {
223
+ logger?.debug(`identified change in stackbit config file: ${path}, reloading config...`);
224
+ throttledFileChange();
225
+ };
226
+ watcher.on('add', handleFileChange);
227
+ watcher.on('change', handleFileChange);
228
+ watcher.on('unlink', handleFileChange);
229
+ watcher.on('addDir', handleFileChange);
230
+ watcher.on('unlinkDir', handleFileChange);
231
+ watcher.on('error', (error) => {
232
+ watchCallback({
233
+ errors: [new ConfigLoadError(`Error loading Stackbit configuration: ${error.message}`, { originalError: error })]
234
+ });
235
+ });
236
+ close = async () => {
237
+ if (stopped) {
238
+ return;
239
+ }
240
+ stopped = true;
241
+ throttledFileChange.cancel();
242
+ watcher.close();
243
+ };
244
+ reload = () => {
245
+ throttledFileChange();
246
+ };
184
247
  }
185
248
 
186
- const { models: modelsFromFiles, errors: fileModelsErrors } = await loadModelsFromFiles(dirPath, stackbitYamlResult.config);
187
-
188
- const mergedModels = mergeConfigModelsWithModelsFromFiles(stackbitYamlResult.config.models ?? {}, modelsFromFiles);
189
-
190
249
  return {
191
- config: {
192
- ...stackbitYamlResult.config,
193
- models: mergedModels
194
- },
195
- errors: fileModelsErrors
250
+ config: config
251
+ ? {
252
+ ...config,
253
+ dirPath: dirPath,
254
+ filePath: stackbitYamlPath
255
+ }
256
+ : undefined,
257
+ stop: close,
258
+ reload: reload,
259
+ errors: errors
196
260
  };
197
261
  }
198
262
  } catch (error: any) {
@@ -201,14 +265,18 @@ export async function loadConfigFromDir({
201
265
  };
202
266
  }
203
267
 
204
- function wrapResult(result: LoadStackbitConfigResult) {
268
+ function wrapResult(result: LoadRawConfigResult, configFilePath: string): RawConfigLoaderResult {
205
269
  if (result.error) {
206
270
  return {
207
271
  errors: [result.error]
208
272
  };
209
273
  } else {
210
274
  return {
211
- config: result.config,
275
+ config: {
276
+ ...result.config,
277
+ dirPath: dirPath,
278
+ filePath: configFilePath
279
+ },
212
280
  errors: []
213
281
  };
214
282
  }
@@ -216,19 +284,20 @@ export async function loadConfigFromDir({
216
284
 
217
285
  // try to load stackbit config from JavaScript files
218
286
  try {
219
- const configFilePath = await getFirstExistingFile(['stackbit.config.js', 'stackbit.config.cjs', 'stackbit.config.mjs', 'stackbit.config.ts'], dirPath);
287
+ const configFilePath = await getFirstExistingFile(STACKBIT_CONFIG_JS_FILES, dirPath);
220
288
  if (configFilePath) {
221
289
  const configResult = await loadStackbitConfigFromJs({
222
290
  configPath: configFilePath,
223
- outDir: stackbitConfigESBuildOutDir,
291
+ outDir: stackbitConfigESBuildOutDir ?? '.stackbit/cache',
224
292
  watch: !!watchCallback,
293
+ logger: logger,
225
294
  callback: watchCallback
226
- ? (result: LoadStackbitConfigResult) => {
227
- watchCallback(wrapResult(result));
295
+ ? (result: LoadRawConfigResult) => {
296
+ watchCallback(wrapResult(result, configFilePath));
228
297
  }
229
298
  : undefined
230
299
  });
231
- return Object.assign(wrapResult(configResult), { stop: configResult.stop });
300
+ return Object.assign(wrapResult(configResult, configFilePath), { stop: configResult.stop });
232
301
  }
233
302
  } catch (error: any) {
234
303
  return {
@@ -237,100 +306,19 @@ export async function loadConfigFromDir({
237
306
  }
238
307
 
239
308
  return {
240
- errors: [
241
- new ConfigLoadError(
242
- 'stackbit.yaml or stackbit.config.js was not found, please refer Stackbit documentation: https://www.stackbit.com/docs/stackbit-yaml/'
243
- )
244
- ]
309
+ errors: [new ConfigLoadError(STACKBIT_CONFIG_NOT_FOUND)]
245
310
  };
246
311
  }
247
312
 
248
- type StackbitYamlConfigResult = { config: Record<string, any>; error?: never } | { config?: never; error: ConfigLoadError };
249
-
250
- async function loadConfigFromStackbitYaml(stackbitYamlPath: string): Promise<StackbitYamlConfigResult> {
251
- const stackbitYaml = await fse.readFile(stackbitYamlPath);
252
- const config = yaml.load(stackbitYaml.toString('utf8'), { schema: yaml.JSON_SCHEMA });
253
- if (!config || typeof config !== 'object') {
254
- return {
255
- error: new ConfigLoadError('error parsing stackbit.yaml, please refer Stackbit documentation: https://www.stackbit.com/docs/stackbit-yaml/')
256
- };
257
- }
258
- return { config };
259
- }
260
-
261
- async function loadModelsFromFiles(dirPath: string, config: Record<string, any>): Promise<{ models: Record<string, any>; errors: ConfigLoadError[] }> {
262
- const modelsSource = _.get(config, 'modelsSource', {});
263
- const sourceType = _.get(modelsSource, 'type', 'files');
264
- const defaultModelDirs = ['node_modules/@stackbit/components/models', '.stackbit/models'];
265
- const modelDirs =
266
- sourceType === 'files'
267
- ? _.castArray(_.get(modelsSource, 'modelDirs', defaultModelDirs)).map((modelDir: string) => _.trim(modelDir, '/'))
268
- : defaultModelDirs;
269
-
270
- const modelFiles = await reducePromise(
271
- modelDirs,
272
- async (modelFiles: string[], modelDir) => {
273
- const absModelsDir = path.join(dirPath, modelDir);
274
- const dirExists = await fse.pathExists(absModelsDir);
275
- if (!dirExists) {
276
- return modelFiles;
277
- }
278
- const files = await readModelFilesFromDir(absModelsDir);
279
- return modelFiles.concat(files.map((filePath) => path.join(modelDir, filePath)));
280
- },
281
- []
282
- );
283
-
284
- return reducePromise(
285
- modelFiles,
286
- async (result: { models: any; errors: ConfigLoadError[] }, modelFile) => {
287
- let model;
288
- try {
289
- model = await parseFile(path.join(dirPath, modelFile));
290
- } catch (error) {
291
- return {
292
- models: result.models,
293
- errors: result.errors.concat(new ConfigLoadError(`error parsing model, file: ${modelFile}`))
294
- };
295
- }
296
- const modelName = model?.name;
297
- if (!modelName) {
298
- return {
299
- models: result.models,
300
- errors: result.errors.concat(new ConfigLoadError(`model does not have a name, file: ${modelFile}`))
301
- };
302
- }
303
- result.models[modelName] = _.omit(model, 'name');
304
- result.models[modelName].__metadata = {
305
- filePath: modelFile
306
- };
307
- return result;
308
- },
309
- { models: {}, errors: [] }
310
- );
311
- }
312
-
313
- async function readModelFilesFromDir(modelsDir: string) {
314
- return await readDirRecursively(modelsDir, {
315
- filter: (filePath, stats) => {
316
- if (stats.isDirectory()) {
317
- return true;
318
- }
319
- const extension = path.extname(filePath).substring(1);
320
- return stats.isFile() && ['yaml', 'yml'].includes(extension);
321
- }
322
- });
323
- }
324
-
325
313
  async function loadModelsFromExternalSource(
326
- config: Record<string, any>,
314
+ config: RawConfigWithPaths,
327
315
  dirPath: string,
328
316
  modelsSource?: ModelsSource
329
317
  ): Promise<{ models: Model[]; errors: ConfigLoadError[] }> {
330
- modelsSource = _.assign({}, modelsSource, config.modelSource);
318
+ modelsSource = _.assign({}, modelsSource, config.modelsSource);
331
319
  const sourceType = _.get(modelsSource, 'type', 'files');
332
320
  if (sourceType === 'files') {
333
- // we already loaded models from files inside loadModelsFromFiles function
321
+ // we already loaded models from files inside loadConfigFromDir function
334
322
  return { models: [], errors: [] };
335
323
  } else if (sourceType === 'contentful') {
336
324
  const contentfulModule = _.get(modelsSource, 'module', '@stackbit/cms-contentful');
@@ -389,36 +377,7 @@ async function loadConfigFromDotStackbit(dirPath: string) {
389
377
  return _.isEmpty(config) ? null : config;
390
378
  }
391
379
 
392
- function mergeConfigModelsWithModelsFromFiles(configModels: any, modelsFromFiles: Record<string, any>) {
393
- const mergedModels = _.mapValues(modelsFromFiles, (modelFromFile, modelName) => {
394
- // resolve thumbnails of models loaded from files
395
- const modelFilePath = modelFromFile.__metadata?.filePath;
396
- resolveThumbnailPathForModel(modelFromFile, modelFilePath);
397
- iterateModelFieldsRecursively(modelFromFile, (field: any) => {
398
- if (isListField(field)) {
399
- field = normalizeListFieldInPlace(field);
400
- field = field.items;
401
- }
402
- if (isObjectField(field)) {
403
- resolveThumbnailPathForModel(field, modelFilePath);
404
- } else if (isEnumField(field)) {
405
- resolveThumbnailPathForEnumField(field, modelFilePath);
406
- }
407
- });
408
-
409
- const configModel = _.get(configModels, modelName);
410
- if (!configModel) {
411
- return modelFromFile;
412
- }
413
-
414
- return _.assign({}, modelFromFile, configModel, {
415
- fields: _.unionBy(configModel?.fields ?? [], modelFromFile?.fields ?? [], 'name')
416
- });
417
- });
418
- return Object.assign({}, configModels, mergedModels);
419
- }
420
-
421
- function mergeConfigWithExternalModels(config: any, externalModels?: Model[]): { config: any; errors: ConfigValidationError[] } {
380
+ function mergeConfigWithExternalModels(config: RawConfigWithPaths, externalModels?: Model[]): { config: RawConfigWithPaths; errors: ConfigValidationError[] } {
422
381
  if (!externalModels || externalModels.length === 0) {
423
382
  return {
424
383
  config,
@@ -502,9 +461,14 @@ function normalizeConfig(config: any): any {
502
461
  const stackbitYamlVersion = String(_.get(config, 'stackbitVersion', ''));
503
462
  const ver = semver.coerce(stackbitYamlVersion);
504
463
  const isStackbitYamlV2 = ver ? semver.satisfies(ver, '<0.3.0') : false;
464
+ const isStackbitYamlV5 = ver ? semver.satisfies(ver, '>=0.5.0') : false;
505
465
  const models = config?.models || {};
506
466
  const gitCMS = isGitCMS(config);
507
467
 
468
+ rename(config, 'logicFields', 'noEncodeFields');
469
+ config.hcrHandled = !stackbitYamlVersion || _.get(config, 'customContentReload', _.get(config, 'hcrHandled', !isStackbitYamlV5));
470
+ config.internalStackbitRunnerOptions = getInternalStackbitRunnerOptions(config);
471
+
508
472
  _.forEach(models, (model, modelName) => {
509
473
  if (!model) {
510
474
  return;
@@ -706,39 +670,6 @@ function addObjectTypeKeyField(model: any, objectTypeKey: string, modelName: str
706
670
  });
707
671
  }
708
672
 
709
- function resolveThumbnailPathForModel(modelOrField: Model | FieldObjectProps, modelFilePath: string | undefined) {
710
- if (modelOrField.thumbnail && modelFilePath) {
711
- const modelDirPath = path.dirname(modelFilePath);
712
- modelOrField.thumbnail = resolveThumbnailPath(modelOrField.thumbnail, modelDirPath);
713
- }
714
- }
715
-
716
- function resolveThumbnailPathForEnumField(enumField: FieldEnum, modelFilePath: string | undefined) {
717
- if (enumField.controlType === 'thumbnails' && modelFilePath) {
718
- const modelDirPath = path.dirname(modelFilePath);
719
- _.forEach(enumField.options, (option) => {
720
- if (option.thumbnail) {
721
- option.thumbnail = resolveThumbnailPath(option.thumbnail, modelDirPath);
722
- }
723
- });
724
- }
725
- }
726
-
727
- function resolveThumbnailPath(thumbnail: string, modelDirPath: string) {
728
- if (thumbnail.startsWith('//') || /https?:\/\//.test(thumbnail)) {
729
- return thumbnail;
730
- }
731
- if (thumbnail.startsWith('/')) {
732
- if (modelDirPath.endsWith('@stackbit/components/models')) {
733
- modelDirPath = modelDirPath.replace(/\/models$/, '');
734
- } else {
735
- modelDirPath = '';
736
- }
737
- thumbnail = thumbnail.replace(/^\//, '');
738
- }
739
- return path.join(modelDirPath, thumbnail);
740
- }
741
-
742
673
  /**
743
674
  * Returns model names referenced by polymorphic 'model' and 'reference' fields.
744
675
  * That is, fields that can hold objects of different types.
@@ -764,7 +695,7 @@ function getReferencedModelNames(field: any) {
764
695
  return referencedModelNames;
765
696
  }
766
697
 
767
- function validateAndExtendContentModels(config: any): ConfigValidationResult {
698
+ function validateAndExtendContentModels(config: RawConfigWithPaths): ConfigValidationResult {
768
699
  const contentModels = config.contentModels ?? {};
769
700
  const models = config.models ?? {};
770
701
 
@@ -789,32 +720,35 @@ function validateAndExtendContentModels(config: any): ConfigValidationResult {
789
720
  };
790
721
  }
791
722
 
792
- const extendedModels = _.mapValues(models, (model, modelName) => {
793
- const contentModel = validationResult.value.contentModels[modelName];
794
- if (!contentModel) {
795
- return model;
796
- }
797
- if (_.get(contentModel, '__metadata.invalid')) {
798
- return model;
799
- }
800
- if (contentModel.isPage && (!model.type || ['object', 'page'].includes(model.type))) {
801
- return {
802
- type: 'page',
803
- ...(contentModel.newFilePath ? { filePath: contentModel.newFilePath } : {}),
804
- ..._.omit(contentModel, ['isPage', 'newFilePath']),
805
- ..._.omit(model, 'type')
806
- };
807
- } else if (!contentModel.isPage && (!model.type || ['object', 'data'].includes(model.type))) {
808
- return {
809
- type: 'data',
810
- ...(contentModel.newFilePath ? { filePath: contentModel.newFilePath } : {}),
811
- ..._.omit(contentModel, ['isPage', 'newFilePath']),
812
- ..._.omit(model, 'type')
813
- };
814
- } else {
815
- return model;
723
+ const extendedModels: ModelMap = _.mapValues(
724
+ models,
725
+ (model, modelName): YamlModel => {
726
+ const contentModel = validationResult.value.contentModels![modelName];
727
+ if (!contentModel) {
728
+ return model;
729
+ }
730
+ if (_.get(contentModel, '__metadata.invalid')) {
731
+ return model;
732
+ }
733
+ if (contentModel.isPage && (!model.type || ['object', 'page'].includes(model.type))) {
734
+ return {
735
+ type: 'page',
736
+ ...(contentModel.newFilePath ? { filePath: contentModel.newFilePath } : {}),
737
+ ..._.omit(contentModel, ['isPage', 'newFilePath']),
738
+ ..._.omit(model, 'type')
739
+ } as YamlModel;
740
+ } else if (!contentModel.isPage && (!model.type || ['object', 'data'].includes(model.type))) {
741
+ return {
742
+ type: 'data',
743
+ ...(contentModel.newFilePath ? { filePath: contentModel.newFilePath } : {}),
744
+ ..._.omit(contentModel, ['isPage', 'newFilePath']),
745
+ ..._.omit(model, 'type')
746
+ } as YamlModel;
747
+ } else {
748
+ return model;
749
+ }
816
750
  }
817
- });
751
+ );
818
752
 
819
753
  return {
820
754
  valid: validationResult.valid,
@@ -833,7 +767,7 @@ function normalizeValidationResult(validationResult: ConfigValidationResult): No
833
767
  }
834
768
 
835
769
  function filterAndOrderConfigFields(validationResult: ConfigValidationResult): ConfigValidationResult {
836
- // TODO: see if we move filtering and sorting to Joi
770
+ // TODO: check if we can move filtering and sorting to Joi
837
771
  return {
838
772
  ...validationResult,
839
773
  value: _.pick(validationResult.value, [
@@ -845,6 +779,9 @@ function filterAndOrderConfigFields(validationResult: ConfigValidationResult): C
845
779
  'buildCommand',
846
780
  'publishDir',
847
781
  'nodeVersion',
782
+ 'postGitCloneCommand',
783
+ 'preInstallCommand',
784
+ 'postInstallCommand',
848
785
  'devCommand',
849
786
  'staticDir',
850
787
  'uploadDir',
@@ -859,12 +796,30 @@ function filterAndOrderConfigFields(validationResult: ConfigValidationResult): C
859
796
  'contentModels',
860
797
  'presetSource',
861
798
  'modelsSource',
799
+ 'mapModels',
800
+ 'presetReferenceBehavior',
801
+ 'nonDuplicatableModels',
802
+ 'duplicatableModels',
803
+ 'contentSources',
804
+ 'hcrHandled',
805
+ 'internalStackbitRunnerOptions',
806
+ 'dirPath',
807
+ 'filePath',
862
808
  'models',
863
- 'presets'
864
- ])
809
+ 'presets',
810
+ 'pageData', // obsolete, left for backward compatibility
811
+ 'pageModels', // obsolete, left for backward compatibility
812
+ 'encodedFieldTypes', // obsolete, left for backward compatibility
813
+ 'noEncodeFields', // obsolete, left for backward compatibility
814
+ 'omitFields' // obsolete, left for backward compatibility
815
+ ]) as RawConfigWithPaths
865
816
  };
866
817
  }
867
818
 
819
+ /**
820
+ * Collects models groups and injects them into the `models` array of the
821
+ * `reference` and `model` field types
822
+ */
868
823
  function convertModelGroupsToModelList(validationResult: ConfigValidationResult) {
869
824
  const models = validationResult.value?.models ?? {};
870
825
 
@@ -892,7 +847,7 @@ function convertModelGroupsToModelList(validationResult: ConfigValidationResult)
892
847
  });
893
848
 
894
849
  _.forEach(models, (model) => {
895
- iterateModelFieldsRecursively(model, (field: any) => {
850
+ iterateModelFieldsRecursively(model as Model, (field: any) => {
896
851
  if (isListField(field)) {
897
852
  field = field.items;
898
853
  }
@@ -921,11 +876,12 @@ function convertModelGroupsToModelList(validationResult: ConfigValidationResult)
921
876
 
922
877
  function convertModelsToArray(validationResult: ConfigValidationResult): NormalizedValidationResult {
923
878
  const config = validationResult.value;
879
+ const { stackbitVersion = LATEST_STACKBIT_VERSION, models, ...rest } = config;
924
880
 
925
881
  // in stackbit.yaml 'models' are defined as object where keys are the model names,
926
882
  // convert 'models' to array of objects and set their 'name' property to the
927
883
  // model name
928
- const modelMap = config.models ?? {};
884
+ const modelMap = models ?? {};
929
885
  const modelArray: Model[] = _.map(
930
886
  modelMap,
931
887
  (yamlModel: YamlModel, modelName: string): Model => {
@@ -954,7 +910,8 @@ function convertModelsToArray(validationResult: ConfigValidationResult): Normali
954
910
  return {
955
911
  valid: validationResult.valid,
956
912
  config: {
957
- ...config,
913
+ stackbitVersion,
914
+ ...rest,
958
915
  models: modelArray
959
916
  },
960
917
  errors: convertedErrors
@@ -974,6 +931,22 @@ function addImageModel(models: Model[]) {
974
931
  });
975
932
  }
976
933
 
977
- function isGitCMS(config: any) {
978
- return !config.cmsName || config.cmsName === 'git';
934
+ function isGitCMS(config: RawConfigWithPaths) {
935
+ return !config.contentSources && (!config.cmsName || config.cmsName === 'git');
936
+ }
937
+
938
+ function getInternalStackbitRunnerOptions(config: any) {
939
+ const experimentalSsgData = _.get(config, 'experimental.ssg');
940
+ if (!experimentalSsgData) {
941
+ return _.get(config, '__unsafe_internal_stackbitRunnerOptions');
942
+ }
943
+ const doneStart = _.get(experimentalSsgData, 'logPatterns.up');
944
+ return {
945
+ displayName: experimentalSsgData.name || 'ssg',
946
+ triggerInstallFiles: _.get(experimentalSsgData, 'watch.reinstallPackages', ['package.json', 'package-lock.json']),
947
+ directPaths: experimentalSsgData.passthrough || [],
948
+ patterns: {
949
+ doneStart: _.isEmpty(doneStart) || _.isArray(doneStart) ? doneStart : [doneStart]
950
+ }
951
+ };
979
952
  }