@stackbit/sdk 0.3.12 → 0.3.13-alpha.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 (31) hide show
  1. package/dist/config/config-errors.d.ts +12 -1
  2. package/dist/config/config-errors.d.ts.map +1 -1
  3. package/dist/config/config-errors.js +23 -1
  4. package/dist/config/config-errors.js.map +1 -1
  5. package/dist/config/config-loader-static.d.ts +2 -2
  6. package/dist/config/config-loader-static.d.ts.map +1 -1
  7. package/dist/config/config-loader-static.js +1 -1
  8. package/dist/config/config-loader-static.js.map +1 -1
  9. package/dist/config/config-loader-utils.d.ts +3 -3
  10. package/dist/config/config-loader-utils.d.ts.map +1 -1
  11. package/dist/config/config-loader-utils.js +4 -6
  12. package/dist/config/config-loader-utils.js.map +1 -1
  13. package/dist/config/config-loader.d.ts +10 -9
  14. package/dist/config/config-loader.d.ts.map +1 -1
  15. package/dist/config/config-loader.js +70 -34
  16. package/dist/config/config-loader.js.map +1 -1
  17. package/dist/config/config-schema.d.ts +3 -2
  18. package/dist/config/config-schema.d.ts.map +1 -1
  19. package/dist/config/config-schema.js +40 -13
  20. package/dist/config/config-schema.js.map +1 -1
  21. package/dist/config/config-validator.d.ts +6 -1
  22. package/dist/config/config-validator.d.ts.map +1 -1
  23. package/dist/config/config-validator.js +13 -2
  24. package/dist/config/config-validator.js.map +1 -1
  25. package/package.json +4 -4
  26. package/src/config/config-errors.ts +26 -1
  27. package/src/config/config-loader-static.ts +4 -4
  28. package/src/config/config-loader-utils.ts +8 -10
  29. package/src/config/config-loader.ts +97 -40
  30. package/src/config/config-schema.ts +44 -14
  31. package/src/config/config-validator.ts +25 -11
@@ -4,11 +4,11 @@ import chokidar from 'chokidar';
4
4
  import semver from 'semver';
5
5
  import _ from 'lodash';
6
6
 
7
- import { ModelsSource, Field } from '@stackbit/types';
7
+ import { ModelsSource, Field, ModelExtension, FieldExtension, FieldType } from '@stackbit/types';
8
8
  import { append, getFirstExistingFile, prepend, rename } from '@stackbit/utils';
9
9
 
10
- import { ConfigValidationResult, validateConfig, validateContentModels } from './config-validator';
11
- import { ConfigError, ConfigLoadError, ConfigValidationError, STACKBIT_CONFIG_NOT_FOUND } from './config-errors';
10
+ import { ConfigValidationResult, validateBaseConfig, validateConfig, validateContentModels } from './config-validator';
11
+ import { ConfigError, ConfigLoadError, ModelLoadError, ConfigValidationError, StackbitConfigNotFoundError } from './config-errors';
12
12
  import { loadStackbitConfigFromJs } from './config-loader-esbuild';
13
13
  import {
14
14
  assignLabelFieldIfNeeded,
@@ -65,14 +65,14 @@ export async function loadConfigWithModelsPresetsAndValidate({
65
65
  stackbitConfigESBuildOutDir,
66
66
  watchCallback: watchCallback
67
67
  ? async (configResult) => {
68
- const configLoaderResult = await processConfigLoaderResult({ configResult, dirPath, modelsSource });
68
+ const configLoaderResult = await processConfigLoaderResult({ configResult, dirPath, modelsSource, logger });
69
69
  watchCallback(configLoaderResult);
70
70
  }
71
71
  : undefined,
72
72
  logger
73
73
  });
74
74
 
75
- const configLoaderResult = await processConfigLoaderResult({ configResult, dirPath, modelsSource });
75
+ const configLoaderResult = await processConfigLoaderResult({ configResult, dirPath, modelsSource, logger });
76
76
  return {
77
77
  ...configLoaderResult,
78
78
  stop: configResult.stop,
@@ -82,7 +82,7 @@ export async function loadConfigWithModelsPresetsAndValidate({
82
82
 
83
83
  export type ConfigWithModelsResult = {
84
84
  config: Config | null;
85
- errors: (ConfigLoadError | ConfigValidationError)[];
85
+ errors: (ConfigLoadError | StackbitConfigNotFoundError | ModelLoadError | ConfigValidationError)[];
86
86
  };
87
87
 
88
88
  export async function loadConfigWithModels({
@@ -130,11 +130,11 @@ export async function loadConfigWithModels({
130
130
  export type LoadConfigResult =
131
131
  | {
132
132
  config: Config;
133
- errors: ConfigLoadError[];
133
+ errors: ConfigValidationError[];
134
134
  }
135
135
  | {
136
136
  config: null;
137
- errors: ConfigLoadError[];
137
+ errors: (ConfigLoadError | StackbitConfigNotFoundError)[];
138
138
  };
139
139
 
140
140
  export async function loadConfig({
@@ -156,11 +156,12 @@ export async function loadConfig({
156
156
  };
157
157
  }
158
158
 
159
- // TODO: validate config base properties after normalizing and return validation errors
159
+ const validationResult = validateBaseConfig(rawConfigResult.config);
160
160
  const config = normalizeConfig(rawConfigResult.config);
161
+
161
162
  return {
162
163
  config: config,
163
- errors: []
164
+ errors: validationResult.errors
164
165
  };
165
166
  };
166
167
 
@@ -188,11 +189,13 @@ export async function loadConfig({
188
189
  async function processConfigLoaderResult({
189
190
  configResult,
190
191
  dirPath,
191
- modelsSource
192
+ modelsSource,
193
+ logger
192
194
  }: {
193
195
  configResult: ConfigWithModelsResult;
194
196
  dirPath: string;
195
197
  modelsSource?: ModelsSource;
198
+ logger?: Logger;
196
199
  }): Promise<ConfigWithModelsPresetsResult> {
197
200
  const { config, errors: configLoadErrors } = configResult;
198
201
 
@@ -206,7 +209,10 @@ async function processConfigLoaderResult({
206
209
 
207
210
  const { models: externalModels, errors: externalModelsLoadErrors } = await loadModelsFromExternalSource(config, dirPath, modelsSource);
208
211
 
209
- const mergedModels = mergeConfigModelsWithExternalModels({ configModels: config.models, externalModels });
212
+ const mergedModels =
213
+ externalModels.length === 0
214
+ ? config.models
215
+ : mergeConfigModelsWithExternalModels({ configModels: config.models as ModelExtension[], externalModels, logger });
210
216
  const mergedConfig: Config = {
211
217
  ...config,
212
218
  models: mergedModels
@@ -234,8 +240,8 @@ async function processConfigLoaderResult({
234
240
  };
235
241
  }
236
242
 
237
- export async function loadAndMergeModelsFromFiles(config: Config): Promise<{ config: Config; errors: (ConfigLoadError | ConfigValidationError)[] }> {
238
- const { models: modelsFromFiles, errors: fileModelsErrors } = await loadYamlModelsFromFiles(config);
243
+ export async function loadAndMergeModelsFromFiles(config: Config): Promise<{ config: Config; errors: (ModelLoadError | ConfigValidationError)[] }> {
244
+ const { models: modelsFromFiles, errors: modelLoadErrors } = await loadYamlModelsFromFiles(config);
239
245
  const { models: mergedModels, errors: mergeModelErrors } = mergeConfigModelsWithModelsFromFiles(config.models, modelsFromFiles);
240
246
 
241
247
  const extendedConfig: Config = {
@@ -245,7 +251,7 @@ export async function loadAndMergeModelsFromFiles(config: Config): Promise<{ con
245
251
 
246
252
  return {
247
253
  config: extendedConfig,
248
- errors: [...fileModelsErrors, ...mergeModelErrors]
254
+ errors: [...modelLoadErrors, ...mergeModelErrors]
249
255
  };
250
256
  }
251
257
 
@@ -277,7 +283,7 @@ export type RawConfigLoaderResult =
277
283
  }
278
284
  | {
279
285
  config: null;
280
- error: ConfigLoadError;
286
+ error: ConfigLoadError | StackbitConfigNotFoundError;
281
287
  };
282
288
 
283
289
  export async function loadConfigFromDir({
@@ -397,7 +403,7 @@ export async function loadConfigFromDir({
397
403
 
398
404
  return {
399
405
  config: null,
400
- error: new ConfigLoadError(STACKBIT_CONFIG_NOT_FOUND)
406
+ error: new StackbitConfigNotFoundError()
401
407
  };
402
408
  }
403
409
 
@@ -405,7 +411,7 @@ async function loadModelsFromExternalSource(
405
411
  config: Config,
406
412
  dirPath: string,
407
413
  modelsSource?: ModelsSource
408
- ): Promise<{ models: Model[]; errors: ConfigLoadError[] }> {
414
+ ): Promise<{ models: Model[]; errors: ModelLoadError[] }> {
409
415
  modelsSource = _.assign({}, modelsSource, config.modelsSource);
410
416
  const sourceType = _.get(modelsSource, 'type', 'files');
411
417
  if (sourceType === 'files') {
@@ -424,13 +430,13 @@ async function loadModelsFromExternalSource(
424
430
  } catch (error: any) {
425
431
  return {
426
432
  models: [],
427
- errors: [new ConfigLoadError(`Error fetching and converting Contentful schema, error: ${error.message}`, { originalError: error })]
433
+ errors: [new ModelLoadError(`Error fetching and converting Contentful schema, error: ${error.message}`, { originalError: error })]
428
434
  };
429
435
  }
430
436
  }
431
437
  return {
432
438
  models: [],
433
- errors: [new ConfigLoadError(`modelsSource ${modelsSource} is unsupported`)]
439
+ errors: [new ModelLoadError(`modelsSource ${modelsSource} is unsupported`)]
434
440
  };
435
441
  }
436
442
 
@@ -468,10 +474,15 @@ async function loadConfigFromDotStackbit(dirPath: string) {
468
474
  return _.isEmpty(config) ? null : config;
469
475
  }
470
476
 
471
- export function mergeConfigModelsWithExternalModels({ configModels, externalModels }: { configModels: Model[]; externalModels: Model[] }): Model[] {
472
- if (externalModels.length === 0) {
473
- return configModels;
474
- }
477
+ export function mergeConfigModelsWithExternalModels({
478
+ configModels,
479
+ externalModels,
480
+ logger
481
+ }: {
482
+ configModels: ModelExtension[];
483
+ externalModels: Model[];
484
+ logger?: Logger;
485
+ }): Model[] {
475
486
  if (configModels.length === 0) {
476
487
  return externalModels;
477
488
  }
@@ -489,7 +500,18 @@ export function mergeConfigModelsWithExternalModels({ configModels, externalMode
489
500
  let mergedModel: Model = Object.assign(
490
501
  {},
491
502
  externalModel,
492
- _.pick(configModel, ['__metadata', 'urlPath', 'label', 'description', 'thumbnail', 'singleInstance', 'readOnly', 'labelField', 'fieldGroups']),
503
+ _.pick(configModel, [
504
+ '__metadata',
505
+ 'urlPath',
506
+ 'label',
507
+ 'description',
508
+ 'thumbnail',
509
+ 'singleInstance',
510
+ 'readOnly',
511
+ 'labelField',
512
+ 'fieldGroups',
513
+ 'localized'
514
+ ]),
493
515
  { type: modelType }
494
516
  );
495
517
 
@@ -500,31 +522,66 @@ export function mergeConfigModelsWithExternalModels({ configModels, externalMode
500
522
  mergedModel = mapModelFieldsRecursively(
501
523
  mergedModel,
502
524
  (externalField, modelKeyPath): Field => {
503
- const stackbitField = getModelFieldForModelKeyPath(configModel, modelKeyPath);
525
+ const stackbitField = getModelFieldForModelKeyPath(configModel as Model, modelKeyPath) as FieldExtension;
504
526
  if (!stackbitField) {
505
527
  return externalField;
506
528
  }
507
529
 
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']);
530
+ const FieldRemapMatrix: Record<string, FieldType[]> = {
531
+ string: ['text', 'html', 'markdown', 'slug', 'url', 'color', 'date', 'datetime', 'enum', 'json', 'style'],
532
+ text: ['html', 'markdown', 'json', 'style'],
533
+ json: ['style']
534
+ };
535
+
536
+ // override field type if allowed, otherwise show a warning message
537
+ let fieldType: FieldType = externalField.type;
538
+ const allowedOverrideTypes = FieldRemapMatrix[externalField.type];
539
+ if (stackbitField.type && stackbitField.type !== externalField.type) {
540
+ if (allowedOverrideTypes && allowedOverrideTypes.includes(stackbitField.type)) {
541
+ fieldType = stackbitField.type;
542
+ } else {
543
+ logger?.warn(
544
+ `Can't remap field of model '${mergedModel.name}' at path ${modelKeyPath}' ` +
545
+ `from ${externalField.type}' type to '${stackbitField.type}' type. ` +
546
+ `'${externalField.type}' fields can be only mapped to ` +
547
+ (!allowedOverrideTypes
548
+ ? 'the same type.'
549
+ : allowedOverrideTypes.length === 1
550
+ ? `'${allowedOverrideTypes[0]}' type`
551
+ : `one of [${allowedOverrideTypes.join(', ')}] types.`)
552
+ );
553
+ }
554
+ }
555
+
556
+ // add field specific properties
557
+ let fieldSpecificProps = {};
558
+ switch (fieldType) {
559
+ case 'number':
560
+ fieldSpecificProps = _.pick(stackbitField, ['subtype', 'min', 'max', 'step', 'unit']);
561
+ break;
562
+ case 'enum':
563
+ fieldSpecificProps = _.pick(stackbitField, ['options']);
564
+ break;
565
+ case 'style':
566
+ fieldSpecificProps = _.pick(stackbitField, ['styles']);
567
+ break;
568
+ case 'object':
569
+ fieldSpecificProps = _.pick(stackbitField, ['labelField', 'thumbnail', 'fieldGroups']);
570
+ break;
571
+ case 'model':
572
+ fieldSpecificProps = _.pick(stackbitField, ['models']);
573
+ break;
574
+ case 'reference':
575
+ fieldSpecificProps = _.pick(stackbitField, ['models']);
576
+ break;
521
577
  }
522
578
 
523
579
  return Object.assign(
524
580
  {},
525
581
  externalField,
526
582
  _.pick(stackbitField, ['label', 'description', 'required', 'default', 'group', 'const', 'hidden', 'readOnly', 'controlType']),
527
- override
583
+ fieldSpecificProps,
584
+ { type: fieldType }
528
585
  );
529
586
  }
530
587
  );
@@ -22,6 +22,7 @@ import {
22
22
  import { CMS_NAMES, FIELD_TYPES, SSG_NAMES } from './config-consts';
23
23
  import { styleFieldPartialSchema } from './config-schema/style-field-schema';
24
24
  import { Config, Model, PageModel, ObjectModel, DataModel, ConfigModel } from './config-types';
25
+ import { RawConfigWithPaths } from './config-loader-utils';
25
26
 
26
27
  function getConfigFromValidationState(state: Joi.State): Config {
27
28
  return _.last(state.ancestors)!;
@@ -139,12 +140,6 @@ function getModelNamesForGroup(group: string, config: Config) {
139
140
  );
140
141
  }
141
142
 
142
- const logicField = Joi.string();
143
- // TODO: validate that all logicFields reference existing fields
144
- // const logicField = Joi.custom((value) => {
145
- // return value;
146
- // });
147
-
148
143
  const labelFieldNotFoundError = 'labelField.not.found';
149
144
  const labelFieldNotSimple = 'labelField.not.simple';
150
145
  const labelFieldSchema = Joi.custom((value, { error, state }) => {
@@ -645,7 +640,7 @@ const fieldNameUnique = 'field.name.unique';
645
640
  function errorLabelFromModelAndFieldPath(model: Model, modelIndex: number, fieldPath: (string | number | object)[]): string {
646
641
  let fieldSpecificProps: Model | FieldSpecificProps | undefined = model;
647
642
  let fields: Field[] | undefined;
648
- let label = `models[${ model.name ? `name='${model.name}'` : modelIndex }]`;
643
+ let label = `models[${model.name ? `name='${model.name}'` : modelIndex}]`;
649
644
 
650
645
  for (const pathPart of fieldPath) {
651
646
  if (typeof pathPart === 'string') {
@@ -815,11 +810,14 @@ const sidebarButtonSchema = Joi.object({
815
810
  ]
816
811
  });
817
812
 
818
- export const stackbitConfigSchema = Joi.object<Config>({
813
+ export const stackbitConfigBaseSchema = Joi.object<RawConfigWithPaths>({
819
814
  stackbitVersion: Joi.string().required(),
820
815
  ssgName: Joi.string().valid(...SSG_NAMES),
821
816
  ssgVersion: Joi.string(),
822
817
  nodeVersion: Joi.string(),
818
+ postGitCloneCommand: Joi.string(),
819
+ preInstallCommand: Joi.string(),
820
+ postInstallCommand: Joi.string(),
823
821
  devCommand: Joi.string(),
824
822
  cmsName: Joi.string().valid(...CMS_NAMES),
825
823
  import: importSchema,
@@ -832,17 +830,41 @@ export const stackbitConfigSchema = Joi.object<Config>({
832
830
  dataDir: Joi.string().allow('', null),
833
831
  pageLayoutKey: Joi.string().allow(null),
834
832
  objectTypeKey: Joi.string(),
835
- styleObjectModelName: styleObjectModelNameSchema,
836
833
  excludePages: Joi.array().items(Joi.string()).single(),
837
- logicFields: Joi.array().items(logicField),
834
+ styleObjectModelName: Joi.string(),
835
+ logicFields: Joi.array().items(Joi.string()),
838
836
  contentModels: Joi.any(), // contentModels should have been already validated by now
839
- sidebarButtons: Joi.array().items(sidebarButtonSchema),
840
837
  presetSource: presetSourceSchema,
841
838
  modelsSource: modelsSourceSchema,
842
- models: modelsSchema
839
+ sidebarButtons: Joi.array().items(sidebarButtonSchema),
840
+ models: Joi.any(),
841
+ modelExtensions: Joi.array().items(Joi.any()),
842
+ presetReferenceBehavior: Joi.string().valid('copyReference', 'duplicateContents'),
843
+ nonDuplicatableModels: Joi.array().items(Joi.string()).when('presetReferenceBehavior', {
844
+ is: 'copyReference',
845
+ then: Joi.forbidden()
846
+ }),
847
+ duplicatableModels: Joi.array().items(Joi.string()).when('presetReferenceBehavior', {
848
+ is: 'duplicateContents',
849
+ then: Joi.forbidden()
850
+ }),
851
+ customContentReload: Joi.boolean(),
852
+ experimental: Joi.any(),
853
+ contentSources: Joi.array().items(Joi.any()),
854
+ siteMap: Joi.function(),
855
+ mapModels: Joi.function(),
856
+ // internal properties added by load
857
+ dirPath: Joi.string(),
858
+ filePath: Joi.string()
843
859
  })
844
- .unknown(true)
845
860
  .without('assets', ['staticDir', 'uploadDir'])
861
+ .without('contentSources', ['cmsName', 'assets', 'contentModels', 'modelsSource'])
862
+ .when('.modelExtensions', {
863
+ is: Joi.exist(),
864
+ then: Joi.object({
865
+ models: Joi.forbidden()
866
+ })
867
+ })
846
868
  .when('.cmsName', {
847
869
  is: ['contentful', 'sanity'],
848
870
  then: Joi.object({
@@ -853,5 +875,13 @@ export const stackbitConfigSchema = Joi.object<Config>({
853
875
  dataDir: Joi.forbidden(),
854
876
  excludePages: Joi.forbidden()
855
877
  })
878
+ });
879
+
880
+ export const stackbitConfigFullSchema = stackbitConfigBaseSchema.concat(
881
+ Joi.object<Config>({
882
+ styleObjectModelName: styleObjectModelNameSchema,
883
+ models: modelsSchema
856
884
  })
857
- .shared(fieldsSchema);
885
+ .unknown(true)
886
+ .shared(fieldsSchema)
887
+ );
@@ -1,8 +1,8 @@
1
1
  import _ from 'lodash';
2
2
  import Joi from 'joi';
3
- import { ContentModelMap } from '@stackbit/types';
3
+ import { ContentModelMap, StackbitConfig } from '@stackbit/types';
4
4
 
5
- import { contentModelsSchema, stackbitConfigSchema } from './config-schema';
5
+ import { contentModelsSchema, stackbitConfigBaseSchema, stackbitConfigFullSchema } from './config-schema';
6
6
  import { ConfigValidationError } from './config-errors';
7
7
  import { Config, Model } from './config-types';
8
8
 
@@ -12,9 +12,26 @@ export interface ConfigValidationResult {
12
12
  errors: ConfigValidationError[];
13
13
  }
14
14
 
15
+ export function validateBaseConfig(
16
+ config: StackbitConfig
17
+ ): {
18
+ config: StackbitConfig;
19
+ valid: boolean;
20
+ errors: ConfigValidationError[];
21
+ } {
22
+ const validationOptions = { abortEarly: false, context: { config: config } };
23
+ const validationResult = stackbitConfigBaseSchema.validate(config, validationOptions);
24
+ const errors = mapJoiErrorsToConfigValidationErrors(validationResult);
25
+ return {
26
+ config: validationResult.value,
27
+ valid: _.isEmpty(errors),
28
+ errors
29
+ };
30
+ }
31
+
15
32
  export function validateConfig(config: Config): ConfigValidationResult {
16
33
  const validationOptions = { abortEarly: false, context: { config: config } };
17
- const validationResult = stackbitConfigSchema.validate(config, validationOptions);
34
+ const validationResult = stackbitConfigFullSchema.validate(config, validationOptions);
18
35
  const validatedConfig: Config = validationResult.value;
19
36
  const errors = mapJoiErrorsToConfigValidationErrors(validationResult);
20
37
  const valid = _.isEmpty(errors);
@@ -41,15 +58,12 @@ export interface ContentModelsValidationResult {
41
58
  export function validateContentModels(contentModels: ContentModelMap, models: Model[]): ContentModelsValidationResult {
42
59
  const modelMap: Record<string, Model> = _.keyBy(models, 'name');
43
60
  const config = { contentModels: contentModels };
44
- const validationResult = contentModelsSchema.validate(
45
- config,
46
- {
47
- abortEarly: false,
48
- context: {
49
- modelMap: modelMap
50
- }
61
+ const validationResult = contentModelsSchema.validate(config, {
62
+ abortEarly: false,
63
+ context: {
64
+ modelMap: modelMap
51
65
  }
52
- );
66
+ });
53
67
  const validatedConfig: Config = validationResult.value;
54
68
  const errors = mapJoiErrorsToConfigValidationErrors(validationResult);
55
69
  const valid = _.isEmpty(errors);