@stackbit/sdk 0.2.22 → 0.2.23
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.
- package/dist/config/config-consts.d.ts +1 -1
- package/dist/config/config-consts.js +2 -0
- package/dist/config/config-consts.js.map +1 -1
- package/dist/config/config-loader.d.ts +3 -2
- package/dist/config/config-loader.js +248 -71
- package/dist/config/config-loader.js.map +1 -1
- package/dist/config/config-schema.js +27 -3
- package/dist/config/config-schema.js.map +1 -1
- package/dist/config/config-types.d.ts +21 -4
- package/dist/config/config-writer.js +3 -0
- package/dist/config/config-writer.js.map +1 -1
- package/dist/config/presets-loader.js +17 -11
- package/dist/config/presets-loader.js.map +1 -1
- package/dist/content/content-loader.js +1 -1
- package/dist/content/content-schema.js +8 -0
- package/dist/content/content-schema.js.map +1 -1
- package/dist/utils/model-extender.js.map +1 -1
- package/dist/utils/model-iterators.d.ts +61 -1
- package/dist/utils/model-iterators.js +60 -11
- package/dist/utils/model-iterators.js.map +1 -1
- package/dist/utils/model-utils.d.ts +44 -3
- package/dist/utils/model-utils.js +93 -10
- package/dist/utils/model-utils.js.map +1 -1
- package/package.json +2 -2
- package/src/.DS_Store +0 -0
- package/src/config/config-consts.ts +2 -0
- package/src/config/config-loader.ts +272 -82
- package/src/config/config-schema.ts +34 -4
- package/src/config/config-types.ts +25 -4
- package/src/config/config-writer.ts +3 -0
- package/src/config/presets-loader.ts +18 -15
- package/src/content/content-loader.ts +2 -2
- package/src/content/content-schema.ts +9 -0
- package/src/utils/model-extender.ts +1 -1
- package/src/utils/model-iterators.ts +61 -13
- package/src/utils/model-utils.ts +91 -8
|
@@ -9,7 +9,7 @@ import { ConfigError, ConfigLoadError, ConfigValidationError } from './config-er
|
|
|
9
9
|
import {
|
|
10
10
|
assignLabelFieldIfNeeded,
|
|
11
11
|
extendModelMap,
|
|
12
|
-
|
|
12
|
+
getListFieldItems,
|
|
13
13
|
isCustomModelField,
|
|
14
14
|
isEnumField,
|
|
15
15
|
isListDataModel,
|
|
@@ -20,13 +20,17 @@ import {
|
|
|
20
20
|
isPageModel,
|
|
21
21
|
isDataModel,
|
|
22
22
|
isReferenceField,
|
|
23
|
-
iterateModelFieldsRecursively
|
|
23
|
+
iterateModelFieldsRecursively,
|
|
24
|
+
mapModelFieldsRecursively,
|
|
25
|
+
normalizeListFieldInPlace,
|
|
26
|
+
getModelFieldForModelKeyPath
|
|
24
27
|
} from '../utils';
|
|
25
|
-
import { append, parseFile, readDirRecursively, reducePromise, rename } from '@stackbit/utils';
|
|
28
|
+
import { append, omitByNil, parseFile, readDirRecursively, reducePromise, rename } from '@stackbit/utils';
|
|
26
29
|
import { Config, DataModel, FieldEnum, FieldModel, FieldObjectProps, Model, PageModel, YamlModel } from './config-types';
|
|
27
30
|
import { loadPresets } from './presets-loader';
|
|
28
31
|
|
|
29
32
|
export interface ConfigLoaderOptions {
|
|
33
|
+
[option: string]: any;
|
|
30
34
|
dirPath: string;
|
|
31
35
|
}
|
|
32
36
|
|
|
@@ -43,11 +47,11 @@ export interface NormalizedValidationResult {
|
|
|
43
47
|
}
|
|
44
48
|
|
|
45
49
|
export interface TempConfigLoaderResult {
|
|
46
|
-
config?:
|
|
50
|
+
config?: Record<string, unknown>;
|
|
47
51
|
errors: ConfigLoadError[];
|
|
48
52
|
}
|
|
49
53
|
|
|
50
|
-
export async function loadConfig({ dirPath }: ConfigLoaderOptions): Promise<ConfigLoaderResult> {
|
|
54
|
+
export async function loadConfig({ dirPath, ...options }: ConfigLoaderOptions): Promise<ConfigLoaderResult> {
|
|
51
55
|
let configLoadResult: TempConfigLoaderResult;
|
|
52
56
|
try {
|
|
53
57
|
configLoadResult = await loadConfigFromDir(dirPath);
|
|
@@ -67,14 +71,18 @@ export async function loadConfig({ dirPath }: ConfigLoaderOptions): Promise<Conf
|
|
|
67
71
|
};
|
|
68
72
|
}
|
|
69
73
|
|
|
70
|
-
const
|
|
74
|
+
const externalModelsResult = await loadModelsFromExternalSource(configLoadResult.config, options);
|
|
75
|
+
|
|
76
|
+
const mergedConfig = mergeConfigWithExternalModels(configLoadResult.config, externalModelsResult.models);
|
|
77
|
+
|
|
78
|
+
const normalizedResult = validateAndNormalizeConfig(mergedConfig);
|
|
71
79
|
|
|
72
80
|
const presetsResult = await loadPresets(dirPath, normalizedResult.config);
|
|
73
81
|
|
|
74
82
|
return {
|
|
75
83
|
valid: normalizedResult.valid,
|
|
76
84
|
config: presetsResult.config,
|
|
77
|
-
errors: [...configLoadResult.errors, ...normalizedResult.errors, ...presetsResult.errors]
|
|
85
|
+
errors: [...configLoadResult.errors, ...externalModelsResult.errors, ...normalizedResult.errors, ...presetsResult.errors]
|
|
78
86
|
};
|
|
79
87
|
}
|
|
80
88
|
|
|
@@ -110,14 +118,14 @@ async function loadConfigFromDir(dirPath: string): Promise<TempConfigLoaderResul
|
|
|
110
118
|
if (error) {
|
|
111
119
|
return { errors: [error] };
|
|
112
120
|
}
|
|
113
|
-
const
|
|
114
|
-
|
|
115
|
-
return { config, errors:
|
|
121
|
+
const modelsFromFileResult = await loadModelsFromFiles(dirPath, config);
|
|
122
|
+
const mergedConfig = mergeConfigModelsWithModelsFromFiles(config, modelsFromFileResult.models);
|
|
123
|
+
return { config: mergedConfig, errors: modelsFromFileResult.errors };
|
|
116
124
|
}
|
|
117
125
|
|
|
118
|
-
type
|
|
126
|
+
type StackbitYamlConfigResult = { config: any; error?: undefined } | { config?: undefined; error: ConfigLoadError };
|
|
119
127
|
|
|
120
|
-
async function loadConfigFromStackbitYaml(dirPath: string): Promise<
|
|
128
|
+
async function loadConfigFromStackbitYaml(dirPath: string): Promise<StackbitYamlConfigResult> {
|
|
121
129
|
const stackbitYamlPath = path.join(dirPath, 'stackbit.yaml');
|
|
122
130
|
const stackbitYamlExists = await fse.pathExists(stackbitYamlPath);
|
|
123
131
|
if (!stackbitYamlExists) {
|
|
@@ -135,54 +143,56 @@ async function loadConfigFromStackbitYaml(dirPath: string): Promise<LoadConfigFr
|
|
|
135
143
|
return { config };
|
|
136
144
|
}
|
|
137
145
|
|
|
138
|
-
async function
|
|
146
|
+
async function loadModelsFromFiles(dirPath: string, config: any): Promise<{ models: Record<string, any>; errors: ConfigLoadError[] }> {
|
|
139
147
|
const modelsSource = _.get(config, 'modelsSource', {});
|
|
140
148
|
const sourceType = _.get(modelsSource, 'type', 'files');
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
return modelFiles
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
return {
|
|
172
|
-
models: result.models,
|
|
173
|
-
errors: result.errors.concat(new ConfigLoadError(`model does not have a name, file: ${modelFile}`))
|
|
174
|
-
};
|
|
175
|
-
}
|
|
176
|
-
result.models[modelName] = _.omit(model, 'name');
|
|
177
|
-
result.models[modelName].__metadata = {
|
|
178
|
-
filePath: modelFile
|
|
149
|
+
const defaultModelDirs = ['node_modules/@stackbit/components/models', '.stackbit/models'];
|
|
150
|
+
const modelDirs =
|
|
151
|
+
sourceType === 'files'
|
|
152
|
+
? _.castArray(_.get(modelsSource, 'modelDirs', defaultModelDirs)).map((modelDir: string) => _.trim(modelDir, '/'))
|
|
153
|
+
: defaultModelDirs;
|
|
154
|
+
|
|
155
|
+
const modelFiles = await reducePromise(
|
|
156
|
+
modelDirs,
|
|
157
|
+
async (modelFiles: string[], modelDir) => {
|
|
158
|
+
const absModelsDir = path.join(dirPath, modelDir);
|
|
159
|
+
const dirExists = await fse.pathExists(absModelsDir);
|
|
160
|
+
if (!dirExists) {
|
|
161
|
+
return modelFiles;
|
|
162
|
+
}
|
|
163
|
+
const files = await readModelFilesFromDir(absModelsDir);
|
|
164
|
+
return modelFiles.concat(files.map((filePath) => path.join(modelDir, filePath)));
|
|
165
|
+
},
|
|
166
|
+
[]
|
|
167
|
+
);
|
|
168
|
+
|
|
169
|
+
return reducePromise(
|
|
170
|
+
modelFiles,
|
|
171
|
+
async (result: { models: any; errors: ConfigLoadError[] }, modelFile) => {
|
|
172
|
+
let model;
|
|
173
|
+
try {
|
|
174
|
+
model = await parseFile(path.join(dirPath, modelFile));
|
|
175
|
+
} catch (error) {
|
|
176
|
+
return {
|
|
177
|
+
models: result.models,
|
|
178
|
+
errors: result.errors.concat(new ConfigLoadError(`error parsing model, file: ${modelFile}`))
|
|
179
179
|
};
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
180
|
+
}
|
|
181
|
+
const modelName = model?.name;
|
|
182
|
+
if (!modelName) {
|
|
183
|
+
return {
|
|
184
|
+
models: result.models,
|
|
185
|
+
errors: result.errors.concat(new ConfigLoadError(`model does not have a name, file: ${modelFile}`))
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
result.models[modelName] = _.omit(model, 'name');
|
|
189
|
+
result.models[modelName].__metadata = {
|
|
190
|
+
filePath: modelFile
|
|
191
|
+
};
|
|
192
|
+
return result;
|
|
193
|
+
},
|
|
194
|
+
{ models: {}, errors: [] }
|
|
195
|
+
);
|
|
186
196
|
}
|
|
187
197
|
|
|
188
198
|
async function readModelFilesFromDir(modelsDir: string) {
|
|
@@ -197,6 +207,33 @@ async function readModelFilesFromDir(modelsDir: string) {
|
|
|
197
207
|
});
|
|
198
208
|
}
|
|
199
209
|
|
|
210
|
+
async function loadModelsFromExternalSource(config: any, options: any): Promise<{ models: Model[]; errors: ConfigLoadError[] }> {
|
|
211
|
+
const modelsSource = _.get(config, 'modelsSource', {});
|
|
212
|
+
const sourceType = _.get(modelsSource, 'type', 'files');
|
|
213
|
+
if (sourceType === 'files') {
|
|
214
|
+
return { models: [], errors: [] };
|
|
215
|
+
} else if (sourceType === 'contentful') {
|
|
216
|
+
const contentfulModule = _.get(modelsSource, 'module', '@stackbit/cms-contentful');
|
|
217
|
+
const module = await import(contentfulModule);
|
|
218
|
+
try {
|
|
219
|
+
const { models } = await module.fetchAndConvertSchema(options);
|
|
220
|
+
return {
|
|
221
|
+
models: models,
|
|
222
|
+
errors: []
|
|
223
|
+
};
|
|
224
|
+
} catch (error) {
|
|
225
|
+
return {
|
|
226
|
+
models: [],
|
|
227
|
+
errors: [new ConfigLoadError(`Error fetching and converting Contentful schema, error: ${error.message}`, { originalError: error })]
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
return {
|
|
232
|
+
models: [],
|
|
233
|
+
errors: [new ConfigLoadError(`modelsSource ${modelsSource} is unsupported`)]
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
|
|
200
237
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
201
238
|
async function loadConfigFromDotStackbit(dirPath: string) {
|
|
202
239
|
const stackbitDotPath = path.join(dirPath, '.stackbit');
|
|
@@ -231,6 +268,101 @@ async function loadConfigFromDotStackbit(dirPath: string) {
|
|
|
231
268
|
return _.isEmpty(config) ? null : config;
|
|
232
269
|
}
|
|
233
270
|
|
|
271
|
+
function mergeConfigModelsWithModelsFromFiles(config: any, modelsFromFiles: Record<string, any>) {
|
|
272
|
+
const configModels = config.models ?? {};
|
|
273
|
+
const mergedModels = _.mapValues(modelsFromFiles, (modelFromFile, modelName) => {
|
|
274
|
+
// resolve thumbnails of models loaded from files
|
|
275
|
+
const modelFilePath = modelFromFile.__metadata?.filePath;
|
|
276
|
+
resolveThumbnailPathForModel(modelFromFile, modelFilePath);
|
|
277
|
+
iterateModelFieldsRecursively(modelFromFile, (field: any) => {
|
|
278
|
+
if (isListField(field)) {
|
|
279
|
+
field = normalizeListFieldInPlace(field);
|
|
280
|
+
field = field.items;
|
|
281
|
+
}
|
|
282
|
+
if (isObjectField(field)) {
|
|
283
|
+
resolveThumbnailPathForModel(field, modelFilePath);
|
|
284
|
+
} else if (isEnumField(field)) {
|
|
285
|
+
resolveThumbnailPathForEnumField(field, modelFilePath);
|
|
286
|
+
}
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
const configModel = configModels[modelName];
|
|
290
|
+
if (!configModel) {
|
|
291
|
+
return modelFromFile;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
return _.assign({}, modelFromFile, configModel, {
|
|
295
|
+
fields: _.unionBy(configModel?.fields ?? [], modelFromFile?.fields ?? [], 'name')
|
|
296
|
+
});
|
|
297
|
+
});
|
|
298
|
+
return {
|
|
299
|
+
...config,
|
|
300
|
+
models: Object.assign({}, configModels, mergedModels)
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
function mergeConfigWithExternalModels(config: any, externalModels?: Model[]) {
|
|
305
|
+
if (!externalModels || externalModels.length === 0) {
|
|
306
|
+
return config;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
const stackbitModels = config?.models ?? {};
|
|
310
|
+
|
|
311
|
+
externalModels = externalModels.map((externalModel) => {
|
|
312
|
+
const stackbitModel = stackbitModels[externalModel.name];
|
|
313
|
+
if (!stackbitModel) {
|
|
314
|
+
return externalModel;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
const modelType = stackbitModel.type ? (stackbitModel.type === 'config' ? 'data' : stackbitModel.type) : 'object';
|
|
318
|
+
const urlPath = modelType === 'page' ? stackbitModel?.urlPath ?? '/{slug}' : null;
|
|
319
|
+
const fieldGroups = stackbitModel?.fieldGroups;
|
|
320
|
+
|
|
321
|
+
externalModel = Object.assign(
|
|
322
|
+
externalModel,
|
|
323
|
+
omitByNil({
|
|
324
|
+
__metadata: stackbitModel.__metadata,
|
|
325
|
+
type: modelType,
|
|
326
|
+
urlPath,
|
|
327
|
+
fieldGroups
|
|
328
|
+
})
|
|
329
|
+
);
|
|
330
|
+
|
|
331
|
+
return mapModelFieldsRecursively(externalModel, (field, modelKeyPath) => {
|
|
332
|
+
const stackbitField = getModelFieldForModelKeyPath(stackbitModel, modelKeyPath);
|
|
333
|
+
if (!stackbitField) {
|
|
334
|
+
return field;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
const group = 'group' in stackbitField ? stackbitField.group : null;
|
|
338
|
+
const controlType = 'controlType' in stackbitField ? stackbitField.controlType : null;
|
|
339
|
+
let override = {};
|
|
340
|
+
|
|
341
|
+
if (stackbitField?.type === 'style') {
|
|
342
|
+
override = stackbitField;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
return Object.assign(
|
|
346
|
+
{},
|
|
347
|
+
field,
|
|
348
|
+
omitByNil({
|
|
349
|
+
group,
|
|
350
|
+
controlType
|
|
351
|
+
}),
|
|
352
|
+
override
|
|
353
|
+
);
|
|
354
|
+
});
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
return {
|
|
358
|
+
...config,
|
|
359
|
+
models: _.mapValues(_.keyBy(externalModels, 'name'), (model) => {
|
|
360
|
+
_.unset(model, 'name');
|
|
361
|
+
return model;
|
|
362
|
+
})
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
|
|
234
366
|
function normalizeConfig(config: any): any {
|
|
235
367
|
const pageLayoutKey = _.get(config, 'pageLayoutKey', 'layout');
|
|
236
368
|
const objectTypeKey = _.get(config, 'objectTypeKey', 'type');
|
|
@@ -238,6 +370,7 @@ function normalizeConfig(config: any): any {
|
|
|
238
370
|
const ver = semver.coerce(stackbitYamlVersion);
|
|
239
371
|
const isStackbitYamlV2 = ver ? semver.satisfies(ver, '<0.3.0') : false;
|
|
240
372
|
const models = config?.models || {};
|
|
373
|
+
const gitCMS = isGitCMS(config);
|
|
241
374
|
let referencedModelNames: string[] = [];
|
|
242
375
|
|
|
243
376
|
_.forEach(models, (model, modelName) => {
|
|
@@ -245,6 +378,10 @@ function normalizeConfig(config: any): any {
|
|
|
245
378
|
return;
|
|
246
379
|
}
|
|
247
380
|
|
|
381
|
+
if (!_.has(model, 'type')) {
|
|
382
|
+
model.type = 'object';
|
|
383
|
+
}
|
|
384
|
+
|
|
248
385
|
// add model label if not set
|
|
249
386
|
if (!_.has(model, 'label')) {
|
|
250
387
|
model.label = _.startCase(modelName);
|
|
@@ -259,13 +396,15 @@ function normalizeConfig(config: any): any {
|
|
|
259
396
|
rename(model, 'template', 'layout');
|
|
260
397
|
|
|
261
398
|
updatePageUrlPath(model);
|
|
262
|
-
updatePageFilePath(model, config);
|
|
263
399
|
|
|
264
|
-
|
|
400
|
+
if (gitCMS) {
|
|
401
|
+
updatePageFilePath(model, config);
|
|
402
|
+
addMarkdownContentField(model);
|
|
265
403
|
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
404
|
+
// TODO: update schema-editor to not show layout field
|
|
405
|
+
addLayoutFieldToPageModel(model, pageLayoutKey);
|
|
406
|
+
}
|
|
407
|
+
} else if (isDataModel(model) && gitCMS) {
|
|
269
408
|
updateDataFilePath(model, config);
|
|
270
409
|
}
|
|
271
410
|
|
|
@@ -281,8 +420,6 @@ function normalizeConfig(config: any): any {
|
|
|
281
420
|
assignLabelFieldIfNeeded(model);
|
|
282
421
|
}
|
|
283
422
|
|
|
284
|
-
resolveThumbnailPathForModel(model, model?.__metadata?.filePath);
|
|
285
|
-
|
|
286
423
|
iterateModelFieldsRecursively(model, (field: any) => {
|
|
287
424
|
// add field label if label is not set
|
|
288
425
|
if (!_.has(field, 'label')) {
|
|
@@ -290,18 +427,12 @@ function normalizeConfig(config: any): any {
|
|
|
290
427
|
}
|
|
291
428
|
|
|
292
429
|
if (isListField(field)) {
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
_.set(field, 'items.type', 'string');
|
|
296
|
-
}
|
|
297
|
-
field = getListItemsField(field);
|
|
430
|
+
field = normalizeListFieldInPlace(field);
|
|
431
|
+
field = field.items;
|
|
298
432
|
}
|
|
299
433
|
|
|
300
434
|
if (isObjectField(field)) {
|
|
301
435
|
assignLabelFieldIfNeeded(field);
|
|
302
|
-
resolveThumbnailPathForModel(field, model?.__metadata?.filePath);
|
|
303
|
-
} else if (isEnumField(field)) {
|
|
304
|
-
resolveThumbnailPathForEnumField(field, model?.__metadata?.filePath);
|
|
305
436
|
} else if (isCustomModelField(field, models)) {
|
|
306
437
|
// stackbit v0.2.0 compatibility
|
|
307
438
|
// convert the old custom model field type: { type: 'action' }
|
|
@@ -331,7 +462,9 @@ function normalizeConfig(config: any): any {
|
|
|
331
462
|
}
|
|
332
463
|
}
|
|
333
464
|
|
|
334
|
-
|
|
465
|
+
if (gitCMS) {
|
|
466
|
+
referencedModelNames = _.union(referencedModelNames, getReferencedModelNames(field));
|
|
467
|
+
}
|
|
335
468
|
});
|
|
336
469
|
});
|
|
337
470
|
|
|
@@ -488,7 +621,7 @@ function resolveThumbnailPath(thumbnail: string, modelDirPath: string) {
|
|
|
488
621
|
*/
|
|
489
622
|
function getReferencedModelNames(field: any) {
|
|
490
623
|
if (isListField(field)) {
|
|
491
|
-
field =
|
|
624
|
+
field = getListFieldItems(field);
|
|
492
625
|
}
|
|
493
626
|
// TODO: add type field to model fields inside container update/create object logic rather adding type to schema
|
|
494
627
|
// 'object' models referenced by 'model' fields should have 'type' field
|
|
@@ -509,7 +642,10 @@ function validateAndExtendContentModels(config: any): ConfigValidationResult {
|
|
|
509
642
|
const contentModels = config.contentModels ?? {};
|
|
510
643
|
const models = config.models ?? {};
|
|
511
644
|
|
|
512
|
-
|
|
645
|
+
const externalModels = !isGitCMS(config);
|
|
646
|
+
const emptyContentModels = _.isEmpty(contentModels);
|
|
647
|
+
|
|
648
|
+
if (externalModels || emptyContentModels) {
|
|
513
649
|
return {
|
|
514
650
|
valid: true,
|
|
515
651
|
value: config,
|
|
@@ -520,17 +656,17 @@ function validateAndExtendContentModels(config: any): ConfigValidationResult {
|
|
|
520
656
|
const validationResult = validateContentModels(contentModels, models);
|
|
521
657
|
|
|
522
658
|
if (_.isEmpty(models)) {
|
|
523
|
-
return
|
|
659
|
+
return {
|
|
660
|
+
valid: validationResult.valid,
|
|
661
|
+
value: config,
|
|
662
|
+
errors: validationResult.errors
|
|
663
|
+
};
|
|
524
664
|
}
|
|
525
665
|
|
|
526
666
|
const extendedModels = _.mapValues(models, (model, modelName) => {
|
|
527
667
|
const contentModel = validationResult.value.contentModels[modelName];
|
|
528
668
|
if (!contentModel) {
|
|
529
|
-
return
|
|
530
|
-
// if a model does not define a type, use the default "object" type
|
|
531
|
-
type: model.type || 'object',
|
|
532
|
-
..._.omit(model, 'type')
|
|
533
|
-
};
|
|
669
|
+
return model;
|
|
534
670
|
}
|
|
535
671
|
if (_.get(contentModel, '__metadata.invalid')) {
|
|
536
672
|
return model;
|
|
@@ -565,10 +701,43 @@ function validateAndExtendContentModels(config: any): ConfigValidationResult {
|
|
|
565
701
|
}
|
|
566
702
|
|
|
567
703
|
function normalizeValidationResult(validationResult: ConfigValidationResult): NormalizedValidationResult {
|
|
704
|
+
validationResult = filterAndOrderConfigFields(validationResult);
|
|
568
705
|
convertModelGroupsToModelList(validationResult);
|
|
569
706
|
return convertModelsToArray(validationResult);
|
|
570
707
|
}
|
|
571
708
|
|
|
709
|
+
function filterAndOrderConfigFields(validationResult: ConfigValidationResult): ConfigValidationResult {
|
|
710
|
+
// TODO: see if we move filtering and sorting to Joi
|
|
711
|
+
return {
|
|
712
|
+
...validationResult,
|
|
713
|
+
value: _.pick(validationResult.value, [
|
|
714
|
+
'stackbitVersion',
|
|
715
|
+
'ssgName',
|
|
716
|
+
'ssgVersion',
|
|
717
|
+
'cmsName',
|
|
718
|
+
'import',
|
|
719
|
+
'buildCommand',
|
|
720
|
+
'publishDir',
|
|
721
|
+
'nodeVersion',
|
|
722
|
+
'devCommand',
|
|
723
|
+
'staticDir',
|
|
724
|
+
'uploadDir',
|
|
725
|
+
'assets',
|
|
726
|
+
'pagesDir',
|
|
727
|
+
'dataDir',
|
|
728
|
+
'pageLayoutKey',
|
|
729
|
+
'objectTypeKey',
|
|
730
|
+
'styleObjectModelName',
|
|
731
|
+
'excludePages',
|
|
732
|
+
'logicFields',
|
|
733
|
+
'contentModels',
|
|
734
|
+
'modelsSource',
|
|
735
|
+
'models',
|
|
736
|
+
'presets'
|
|
737
|
+
])
|
|
738
|
+
};
|
|
739
|
+
}
|
|
740
|
+
|
|
572
741
|
function convertModelGroupsToModelList(validationResult: ConfigValidationResult) {
|
|
573
742
|
const models = validationResult.value?.models ?? {};
|
|
574
743
|
|
|
@@ -598,7 +767,7 @@ function convertModelGroupsToModelList(validationResult: ConfigValidationResult)
|
|
|
598
767
|
_.forEach(models, (model) => {
|
|
599
768
|
iterateModelFieldsRecursively(model, (field: any) => {
|
|
600
769
|
if (isListField(field)) {
|
|
601
|
-
field =
|
|
770
|
+
field = field.items;
|
|
602
771
|
}
|
|
603
772
|
if (field.groups) {
|
|
604
773
|
let key: string | null = null;
|
|
@@ -640,6 +809,10 @@ function convertModelsToArray(validationResult: ConfigValidationResult): Normali
|
|
|
640
809
|
}
|
|
641
810
|
);
|
|
642
811
|
|
|
812
|
+
if (!isGitCMS(config)) {
|
|
813
|
+
addImageModel(modelArray);
|
|
814
|
+
}
|
|
815
|
+
|
|
643
816
|
const convertedErrors = _.map(validationResult.errors, (error: ConfigValidationError) => {
|
|
644
817
|
if (error.fieldPath[0] === 'models' && typeof error.fieldPath[1] == 'string') {
|
|
645
818
|
const modelName = error.fieldPath[1];
|
|
@@ -660,3 +833,20 @@ function convertModelsToArray(validationResult: ConfigValidationResult): Normali
|
|
|
660
833
|
errors: convertedErrors
|
|
661
834
|
};
|
|
662
835
|
}
|
|
836
|
+
|
|
837
|
+
function addImageModel(models: Model[]) {
|
|
838
|
+
models.push({
|
|
839
|
+
type: 'image',
|
|
840
|
+
name: '__image_model',
|
|
841
|
+
label: 'Image',
|
|
842
|
+
labelField: 'title',
|
|
843
|
+
fields: [
|
|
844
|
+
{ name: 'title', type: 'string' },
|
|
845
|
+
{ name: 'url', type: 'string' }
|
|
846
|
+
]
|
|
847
|
+
});
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
function isGitCMS(config: any) {
|
|
851
|
+
return !config.cmsName || config.cmsName === 'git';
|
|
852
|
+
}
|
|
@@ -20,7 +20,10 @@ import {
|
|
|
20
20
|
FieldGroupItem,
|
|
21
21
|
YamlObjectModel,
|
|
22
22
|
ContentModelMap,
|
|
23
|
-
ContentModel
|
|
23
|
+
ContentModel,
|
|
24
|
+
ModelsSourceFiles,
|
|
25
|
+
ModelsSourceContentful,
|
|
26
|
+
ModelsSourceSanity
|
|
24
27
|
} from './config-types';
|
|
25
28
|
|
|
26
29
|
function getConfigFromValidationState(state: Joi.State): YamlConfig {
|
|
@@ -200,6 +203,11 @@ const styleObjectModelNotObject = 'styleObjectModelName.model.type';
|
|
|
200
203
|
const styleObjectModelNameSchema = Joi.string()
|
|
201
204
|
.allow('', null)
|
|
202
205
|
.custom((value, { error, state }) => {
|
|
206
|
+
const config = getConfigFromValidationState(state);
|
|
207
|
+
const externalModels = config.cmsName && ['contentful', 'sanity'].includes(config.cmsName);
|
|
208
|
+
if (externalModels) {
|
|
209
|
+
return value;
|
|
210
|
+
}
|
|
203
211
|
const models = getModelsFromValidationState(state);
|
|
204
212
|
const modelNames = Object.keys(models);
|
|
205
213
|
if (!modelNames.includes(value)) {
|
|
@@ -245,11 +253,32 @@ const importSchema = Joi.alternatives().conditional('.type', {
|
|
|
245
253
|
]
|
|
246
254
|
});
|
|
247
255
|
|
|
248
|
-
const
|
|
249
|
-
type: 'files',
|
|
256
|
+
const modelsSourceFilesSchema = Joi.object<ModelsSourceFiles>({
|
|
257
|
+
type: Joi.string().valid(Joi.override, 'files').required(),
|
|
250
258
|
modelDirs: Joi.array().items(Joi.string()).required()
|
|
251
259
|
});
|
|
252
260
|
|
|
261
|
+
const modelsSourceContentfulSchema = Joi.object<ModelsSourceContentful>({
|
|
262
|
+
type: Joi.string().valid(Joi.override, 'contentful').required(),
|
|
263
|
+
module: Joi.string()
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
const modelsSourceSanitySchema = Joi.object<ModelsSourceSanity>({
|
|
267
|
+
type: Joi.string().valid(Joi.override, 'sanity').required(),
|
|
268
|
+
sanityStudioPath: Joi.string().required(),
|
|
269
|
+
module: Joi.string()
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
const modelsSourceSchema = Joi.object<ModelsSource>({
|
|
273
|
+
type: Joi.string().valid('files', 'contentful', 'sanity').required()
|
|
274
|
+
}).when('.type', {
|
|
275
|
+
switch: [
|
|
276
|
+
{ is: 'files', then: modelsSourceFilesSchema },
|
|
277
|
+
{ is: 'contentful', then: modelsSourceContentfulSchema },
|
|
278
|
+
{ is: 'sanity', then: modelsSourceSanitySchema }
|
|
279
|
+
]
|
|
280
|
+
});
|
|
281
|
+
|
|
253
282
|
const assetsSchema = Joi.object<Assets>({
|
|
254
283
|
referenceType: Joi.string().valid('static', 'relative').required(),
|
|
255
284
|
assetsDir: Joi.string().allow('').when('referenceType', {
|
|
@@ -309,7 +338,8 @@ const fieldCommonPropsSchema = Joi.object({
|
|
|
309
338
|
group: inGroups,
|
|
310
339
|
const: Joi.any(),
|
|
311
340
|
hidden: Joi.boolean(),
|
|
312
|
-
readOnly: Joi.boolean()
|
|
341
|
+
readOnly: Joi.boolean(),
|
|
342
|
+
localized: Joi.boolean()
|
|
313
343
|
}).oxor('const', 'default');
|
|
314
344
|
|
|
315
345
|
const numberFieldPartialSchema = Joi.object({
|
|
@@ -3,7 +3,7 @@ import { CMS_NAMES, FIELD_TYPES, SSG_NAMES, STYLE_PROPS } from './config-consts'
|
|
|
3
3
|
|
|
4
4
|
export interface Config extends BaseConfig {
|
|
5
5
|
models: Model[];
|
|
6
|
-
presets?: any
|
|
6
|
+
presets?: Record<string, any>;
|
|
7
7
|
}
|
|
8
8
|
|
|
9
9
|
export interface YamlConfig extends BaseConfig {
|
|
@@ -85,16 +85,29 @@ export interface ContentModel extends BaseMatch {
|
|
|
85
85
|
newFilePath?: string;
|
|
86
86
|
}
|
|
87
87
|
|
|
88
|
-
export
|
|
88
|
+
export type ModelsSource = ModelsSourceFiles | ModelsSourceContentful | ModelsSourceSanity;
|
|
89
|
+
|
|
90
|
+
export interface ModelsSourceFiles {
|
|
89
91
|
type: 'files';
|
|
90
92
|
modelDirs: string[];
|
|
91
93
|
}
|
|
92
94
|
|
|
95
|
+
export interface ModelsSourceContentful {
|
|
96
|
+
type: 'contentful';
|
|
97
|
+
module?: string;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export interface ModelsSourceSanity {
|
|
101
|
+
type: 'sanity';
|
|
102
|
+
sanityStudioPath: string;
|
|
103
|
+
module?: string;
|
|
104
|
+
}
|
|
105
|
+
|
|
93
106
|
/*******************
|
|
94
107
|
*** Model Types ***
|
|
95
108
|
*******************/
|
|
96
109
|
|
|
97
|
-
export type Model = StricterUnion<ObjectModel | DataModel | PageModel | ConfigModel>;
|
|
110
|
+
export type Model = StricterUnion<ObjectModel | DataModel | PageModel | ConfigModel | ImageModel>;
|
|
98
111
|
|
|
99
112
|
export type ObjectModel = YamlObjectModel & BaseModel;
|
|
100
113
|
export type DataModel = YamlDataModel & BaseModel;
|
|
@@ -174,6 +187,14 @@ export interface FieldGroupItem {
|
|
|
174
187
|
label: string;
|
|
175
188
|
}
|
|
176
189
|
|
|
190
|
+
export interface ImageModel {
|
|
191
|
+
type: 'image';
|
|
192
|
+
name: '__image_model';
|
|
193
|
+
label?: string;
|
|
194
|
+
labelField?: string;
|
|
195
|
+
fields?: Field[];
|
|
196
|
+
}
|
|
197
|
+
|
|
177
198
|
/*******************
|
|
178
199
|
*** Field Types ***
|
|
179
200
|
*******************/
|
|
@@ -209,7 +230,7 @@ export interface FieldCommonProps {
|
|
|
209
230
|
export type FieldType = typeof FIELD_TYPES[number];
|
|
210
231
|
|
|
211
232
|
export interface FieldSimpleProps {
|
|
212
|
-
type: 'string' | 'url' | 'slug' | 'text' | 'markdown' | 'html' | 'boolean' | 'date' | 'datetime' | 'color' | 'image' | 'file';
|
|
233
|
+
type: 'string' | 'url' | 'slug' | 'text' | 'markdown' | 'html' | 'boolean' | 'date' | 'datetime' | 'color' | 'image' | 'file' | 'json' | 'richText';
|
|
213
234
|
}
|
|
214
235
|
|
|
215
236
|
export type FieldEnumProps = FieldEnumDropdownProps | FieldEnumThumbnailsProps | FieldEnumPaletteProps;
|
|
@@ -29,6 +29,9 @@ export function convertToYamlConfig({ config }: { config: Config }): YamlConfig
|
|
|
29
29
|
yamlConfig.models = _.reduce(
|
|
30
30
|
config.models,
|
|
31
31
|
(yamlModels: ModelMap, model: Model) => {
|
|
32
|
+
if (model.type === 'image') {
|
|
33
|
+
return yamlModels;
|
|
34
|
+
}
|
|
32
35
|
const yamlModel = _.omit(model, ['name', '__metadata']) as YamlModel;
|
|
33
36
|
if (yamlModel.type === 'page' && !yamlModel.hideContent && yamlModel.fields) {
|
|
34
37
|
_.remove(yamlModel.fields, (field) => field.name === 'markdown_content');
|