@stackbit/sdk 0.2.30 → 0.2.33

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.
@@ -6,6 +6,7 @@ import _ from 'lodash';
6
6
 
7
7
  import { ConfigValidationResult, validateConfig, validateContentModels } from './config-validator';
8
8
  import { ConfigError, ConfigLoadError, ConfigValidationError } from './config-errors';
9
+ import { loadStackbitConfigFromJs, LoadStackbitConfigResult, StopConfigWatch } from './config-loader-esbuild';
9
10
  import {
10
11
  assignLabelFieldIfNeeded,
11
12
  extendModelMap,
@@ -25,13 +26,15 @@ import {
25
26
  normalizeListFieldInPlace,
26
27
  getModelFieldForModelKeyPath
27
28
  } from '../utils';
28
- import { append, prepend, omitByNil, parseFile, readDirRecursively, reducePromise, rename } from '@stackbit/utils';
29
+ import { append, prepend, omitByNil, parseFile, readDirRecursively, reducePromise, rename, getFirstExistingFile } from '@stackbit/utils';
29
30
  import { Config, DataModel, FieldEnum, FieldModel, FieldObjectProps, Model, ModelsSource, PageModel, YamlModel } from './config-types';
30
31
  import { loadPresets } from './presets-loader';
31
32
 
32
33
  export interface ConfigLoaderOptions {
33
34
  dirPath: string;
34
35
  modelsSource?: ModelsSource;
36
+ stackbitConfigESBuildOutDir?: string;
37
+ watchCallback?: (result: ConfigLoaderResult) => void;
35
38
  }
36
39
 
37
40
  export interface ConfigLoaderResult {
@@ -46,13 +49,42 @@ export interface NormalizedValidationResult {
46
49
  errors: ConfigValidationError[];
47
50
  }
48
51
 
49
- export interface TempConfigLoaderResult {
52
+ export interface RawConfigLoaderResult {
50
53
  config?: Record<string, any>;
51
54
  errors: ConfigLoadError[];
52
55
  }
53
56
 
54
- export async function loadConfig({ dirPath, modelsSource }: ConfigLoaderOptions): Promise<ConfigLoaderResult> {
55
- const { config, errors: configLoadErrors } = await loadConfigFromDir(dirPath);
57
+ export async function loadConfig({
58
+ dirPath,
59
+ modelsSource,
60
+ stackbitConfigESBuildOutDir,
61
+ watchCallback
62
+ }: ConfigLoaderOptions): Promise<ConfigLoaderResult & StopConfigWatch> {
63
+ const rawConfigResult = await loadStackbitConfigFromDir({
64
+ dirPath,
65
+ stackbitConfigESBuildOutDir,
66
+ watchCallback: watchCallback
67
+ ? async (rawConfigResult: RawConfigLoaderResult) => {
68
+ const configLoaderResult = await processConfigLoaderResult({ rawConfigResult, dirPath, modelsSource });
69
+ watchCallback(configLoaderResult);
70
+ }
71
+ : undefined
72
+ });
73
+
74
+ const configLoaderResult = await processConfigLoaderResult({ rawConfigResult, dirPath, modelsSource });
75
+ return Object.assign(configLoaderResult, { stop: rawConfigResult.stop });
76
+ }
77
+
78
+ async function processConfigLoaderResult({
79
+ rawConfigResult,
80
+ dirPath,
81
+ modelsSource
82
+ }: {
83
+ rawConfigResult: RawConfigLoaderResult;
84
+ dirPath: string;
85
+ modelsSource?: ModelsSource;
86
+ }): Promise<ConfigLoaderResult> {
87
+ const { config, errors: configLoadErrors } = rawConfigResult;
56
88
 
57
89
  if (!config) {
58
90
  return {
@@ -107,41 +139,90 @@ export function validateAndNormalizeConfig(config: Record<string, any>, external
107
139
  });
108
140
  }
109
141
 
110
- async function loadConfigFromDir(dirPath: string): Promise<TempConfigLoaderResult> {
142
+ async function loadStackbitConfigFromDir({
143
+ dirPath,
144
+ stackbitConfigESBuildOutDir,
145
+ watchCallback
146
+ }: {
147
+ dirPath: string;
148
+ stackbitConfigESBuildOutDir?: string;
149
+ watchCallback?: (rawConfigResult: RawConfigLoaderResult) => void;
150
+ }): Promise<RawConfigLoaderResult & StopConfigWatch> {
151
+ // try to load stackbit config from YAML files
111
152
  try {
112
- const stackbitYamlResult = await loadConfigFromStackbitYaml(dirPath);
113
- if (stackbitYamlResult.error) {
114
- return { errors: [stackbitYamlResult.error] };
115
- }
153
+ const stackbitYamlPath = path.join(dirPath, 'stackbit.yaml');
154
+ const stackbitYamlExists = await fse.pathExists(stackbitYamlPath);
155
+ if (stackbitYamlExists) {
156
+ const stackbitYamlResult = await loadConfigFromStackbitYaml(stackbitYamlPath);
157
+ if (stackbitYamlResult.error) {
158
+ return { errors: [stackbitYamlResult.error] };
159
+ }
116
160
 
117
- const { models: modelsFromFiles, errors: fileModelsErrors } = await loadModelsFromFiles(dirPath, stackbitYamlResult.config);
161
+ const { models: modelsFromFiles, errors: fileModelsErrors } = await loadModelsFromFiles(dirPath, stackbitYamlResult.config);
118
162
 
119
- const mergedModels = mergeConfigModelsWithModelsFromFiles(stackbitYamlResult.config.models ?? {}, modelsFromFiles);
163
+ const mergedModels = mergeConfigModelsWithModelsFromFiles(stackbitYamlResult.config.models ?? {}, modelsFromFiles);
120
164
 
165
+ return {
166
+ config: {
167
+ ...stackbitYamlResult.config,
168
+ models: mergedModels
169
+ },
170
+ errors: fileModelsErrors
171
+ };
172
+ }
173
+ } catch (error: any) {
121
174
  return {
122
- config: {
123
- ...stackbitYamlResult.config,
124
- models: mergedModels
125
- },
126
- errors: fileModelsErrors
175
+ errors: [new ConfigLoadError(`Error loading Stackbit configuration: ${error.message}`, { originalError: error })]
127
176
  };
177
+ }
178
+
179
+ function wrapResult(result: LoadStackbitConfigResult) {
180
+ if (result.error) {
181
+ return {
182
+ errors: [result.error]
183
+ };
184
+ } else {
185
+ return {
186
+ config: result.config,
187
+ errors: []
188
+ };
189
+ }
190
+ }
191
+
192
+ // try to load stackbit config from JavaScript files
193
+ try {
194
+ const configFilePath = await getFirstExistingFile(['stackbit.config.js', 'stackbit.config.cjs', 'stackbit.config.mjs', 'stackbit.config.ts'], dirPath);
195
+ if (configFilePath) {
196
+ const configResult = await loadStackbitConfigFromJs({
197
+ configPath: configFilePath,
198
+ outDir: stackbitConfigESBuildOutDir,
199
+ watch: !!watchCallback,
200
+ callback: watchCallback
201
+ ? (result: LoadStackbitConfigResult) => {
202
+ watchCallback(wrapResult(result));
203
+ }
204
+ : undefined
205
+ });
206
+ return Object.assign(wrapResult(configResult), { stop: configResult.stop });
207
+ }
128
208
  } catch (error: any) {
129
209
  return {
130
210
  errors: [new ConfigLoadError(`Error loading Stackbit configuration: ${error.message}`, { originalError: error })]
131
211
  };
132
212
  }
213
+
214
+ return {
215
+ errors: [
216
+ new ConfigLoadError(
217
+ 'stackbit.yaml or stackbit.config.js was not found, please refer Stackbit documentation: https://www.stackbit.com/docs/stackbit-yaml/'
218
+ )
219
+ ]
220
+ };
133
221
  }
134
222
 
135
223
  type StackbitYamlConfigResult = { config: Record<string, any>; error?: never } | { config?: never; error: ConfigLoadError };
136
224
 
137
- async function loadConfigFromStackbitYaml(dirPath: string): Promise<StackbitYamlConfigResult> {
138
- const stackbitYamlPath = path.join(dirPath, 'stackbit.yaml');
139
- const stackbitYamlExists = await fse.pathExists(stackbitYamlPath);
140
- if (!stackbitYamlExists) {
141
- return {
142
- error: new ConfigLoadError('stackbit.yaml was not found, please refer Stackbit documentation: https://www.stackbit.com/docs/stackbit-yaml/')
143
- };
144
- }
225
+ async function loadConfigFromStackbitYaml(stackbitYamlPath: string): Promise<StackbitYamlConfigResult> {
145
226
  const stackbitYaml = await fse.readFile(stackbitYamlPath);
146
227
  const config = yaml.load(stackbitYaml.toString('utf8'), { schema: yaml.JSON_SCHEMA });
147
228
  if (!config || typeof config !== 'object') {
@@ -300,7 +300,8 @@ const fieldGroupsSchema = Joi.array()
300
300
  .items(
301
301
  Joi.object<FieldGroupItem>({
302
302
  name: Joi.string().required(),
303
- label: Joi.string().required()
303
+ label: Joi.string().required(),
304
+ icon: Joi.string().optional()
304
305
  })
305
306
  )
306
307
  .unique('name')
@@ -357,6 +358,10 @@ const enumFieldBaseOptionSchema = Joi.object({
357
358
  value: Joi.alternatives().try(Joi.string(), Joi.number()).required()
358
359
  });
359
360
 
361
+ const imageFieldPartialSchema = Joi.object({
362
+ source: Joi.string()
363
+ });
364
+
360
365
  const enumFieldPartialSchema = Joi.object({
361
366
  type: Joi.string().valid('enum').required(),
362
367
  controlType: Joi.string().valid('dropdown', 'button-group', 'thumbnails', 'palette'),
@@ -430,6 +435,7 @@ const listItemsSchema = Joi.object({
430
435
  switch: [
431
436
  { is: 'number', then: numberFieldPartialSchema },
432
437
  { is: 'enum', then: enumFieldPartialSchema },
438
+ { is: 'image', then: imageFieldPartialSchema },
433
439
  { is: 'object', then: objectFieldPartialSchema },
434
440
  { is: 'model', then: modelFieldPartialSchema },
435
441
  { is: 'reference', then: referenceFieldPartialSchema }
@@ -445,6 +451,7 @@ const fieldSchema: Joi.ObjectSchema<Field> = fieldCommonPropsSchema.when('.type'
445
451
  switch: [
446
452
  { is: 'number', then: numberFieldPartialSchema },
447
453
  { is: 'enum', then: enumFieldPartialSchema },
454
+ { is: 'image', then: imageFieldPartialSchema },
448
455
  { is: 'object', then: objectFieldPartialSchema },
449
456
  { is: 'model', then: modelFieldPartialSchema },
450
457
  { is: 'reference', then: referenceFieldPartialSchema },
@@ -187,6 +187,7 @@ export interface BaseDataModeList extends Omit<BaseDataModel, 'fields'> {
187
187
  export interface FieldGroupItem {
188
188
  name: string;
189
189
  label: string;
190
+ icon?: string;
190
191
  }
191
192
 
192
193
  export interface ImageModel {
@@ -195,16 +196,18 @@ export interface ImageModel {
195
196
  label?: string;
196
197
  labelField?: string;
197
198
  fields?: Field[];
199
+ source?: string;
198
200
  }
199
201
 
200
202
  /*******************
201
203
  *** Field Types ***
202
204
  *******************/
203
205
 
204
- export type Field = FieldSimple | FieldEnum | FieldNumber | FieldObject | FieldModel | FieldReference | FieldStyle | FieldList;
206
+ export type Field = FieldSimple | FieldEnum | FieldImage | FieldNumber | FieldObject | FieldModel | FieldReference | FieldStyle | FieldList;
205
207
 
206
208
  export type FieldSimple = FieldCommonProps & FieldSimpleProps;
207
209
  export type FieldEnum = FieldCommonProps & FieldEnumProps;
210
+ export type FieldImage = FieldCommonProps & FieldImageProps;
208
211
  export type FieldNumber = FieldCommonProps & FieldNumberProps;
209
212
  export type FieldObject = FieldCommonProps & FieldObjectProps;
210
213
  export type FieldModel = FieldCommonProps & FieldModelProps;
@@ -232,7 +235,7 @@ export interface FieldCommonProps {
232
235
  export type FieldType = typeof FIELD_TYPES[number];
233
236
 
234
237
  export interface FieldSimpleProps {
235
- type: 'string' | 'url' | 'slug' | 'text' | 'markdown' | 'html' | 'boolean' | 'date' | 'datetime' | 'color' | 'image' | 'file' | 'json' | 'richText';
238
+ type: 'string' | 'url' | 'slug' | 'text' | 'markdown' | 'html' | 'boolean' | 'date' | 'datetime' | 'color' | 'file' | 'json' | 'richText';
236
239
  }
237
240
 
238
241
  export type FieldEnumProps = FieldEnumDropdownProps | FieldEnumThumbnailsProps | FieldEnumPaletteProps;
@@ -272,6 +275,11 @@ export interface FieldEnumOptionPalette extends FieldEnumOptionObject {
272
275
  borderColor?: string;
273
276
  }
274
277
 
278
+ export interface FieldImageProps {
279
+ type: 'image';
280
+ source?: string;
281
+ }
282
+
275
283
  export interface FieldNumberProps {
276
284
  type: 'number';
277
285
  controlType?: 'slider';
@@ -315,4 +323,4 @@ export interface FieldListProps {
315
323
  items?: FieldListItems;
316
324
  }
317
325
 
318
- export type FieldListItems = FieldSimpleProps | FieldEnumProps | FieldNumberProps | FieldObjectProps | FieldModelProps | FieldReferenceProps;
326
+ export type FieldListItems = FieldSimpleProps | FieldEnumProps | FieldImageProps | FieldNumberProps | FieldObjectProps | FieldModelProps | FieldReferenceProps;