@stackbit/sdk 0.2.22 → 0.2.26
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 +5 -4
- package/dist/config/config-loader.js +291 -99
- 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 +23 -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 +327 -111
- package/src/config/config-schema.ts +34 -4
- package/src/config/config-types.ts +27 -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,14 +20,18 @@ 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';
|
|
26
|
-
import { Config, DataModel, FieldEnum, FieldModel, FieldObjectProps, Model, PageModel, YamlModel } from './config-types';
|
|
28
|
+
import { append, omitByNil, parseFile, readDirRecursively, reducePromise, rename } from '@stackbit/utils';
|
|
29
|
+
import { Config, DataModel, FieldEnum, FieldModel, FieldObjectProps, Model, ModelsSource, PageModel, YamlModel } from './config-types';
|
|
27
30
|
import { loadPresets } from './presets-loader';
|
|
28
31
|
|
|
29
32
|
export interface ConfigLoaderOptions {
|
|
30
33
|
dirPath: string;
|
|
34
|
+
modelsSource?: ModelsSource;
|
|
31
35
|
}
|
|
32
36
|
|
|
33
37
|
export interface ConfigLoaderResult {
|
|
@@ -43,81 +47,94 @@ 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> {
|
|
51
|
-
|
|
52
|
-
try {
|
|
53
|
-
configLoadResult = await loadConfigFromDir(dirPath);
|
|
54
|
-
} catch (error) {
|
|
55
|
-
return {
|
|
56
|
-
valid: false,
|
|
57
|
-
config: null,
|
|
58
|
-
errors: [new ConfigLoadError(`Error loading Stackbit configuration: ${error.message}`, { originalError: error })]
|
|
59
|
-
};
|
|
60
|
-
}
|
|
54
|
+
export async function loadConfig({ dirPath, modelsSource }: ConfigLoaderOptions): Promise<ConfigLoaderResult> {
|
|
55
|
+
const { config, errors: configLoadErrors } = await loadConfigFromDir(dirPath);
|
|
61
56
|
|
|
62
|
-
if (!
|
|
57
|
+
if (!config) {
|
|
63
58
|
return {
|
|
64
59
|
valid: false,
|
|
65
60
|
config: null,
|
|
66
|
-
errors:
|
|
61
|
+
errors: configLoadErrors
|
|
67
62
|
};
|
|
68
63
|
}
|
|
69
64
|
|
|
70
|
-
const
|
|
65
|
+
const { models: externalModels, errors: externalModelsLoadErrors } = await loadModelsFromExternalSource(config, dirPath, modelsSource);
|
|
66
|
+
|
|
67
|
+
const normalizedResult = validateAndNormalizeConfig(config, externalModels);
|
|
71
68
|
|
|
72
69
|
const presetsResult = await loadPresets(dirPath, normalizedResult.config);
|
|
73
70
|
|
|
74
71
|
return {
|
|
75
72
|
valid: normalizedResult.valid,
|
|
76
73
|
config: presetsResult.config,
|
|
77
|
-
errors: [...
|
|
74
|
+
errors: [...configLoadErrors, ...externalModelsLoadErrors, ...normalizedResult.errors, ...presetsResult.errors]
|
|
78
75
|
};
|
|
79
76
|
}
|
|
80
77
|
|
|
81
|
-
export function validateAndNormalizeConfig(config: any): NormalizedValidationResult {
|
|
82
|
-
// validate the "contentModels" and extend config models with "contentModels"
|
|
83
|
-
// this must be done before main config validation to make it independent of "contentModels".
|
|
84
|
-
const contentModelsValidationResult = validateAndExtendContentModels(config);
|
|
85
|
-
config = contentModelsValidationResult.value;
|
|
86
|
-
|
|
78
|
+
export function validateAndNormalizeConfig(config: any, externalModels?: Model[]): NormalizedValidationResult {
|
|
87
79
|
// extend config models having the "extends" property
|
|
88
|
-
// this must be done before
|
|
80
|
+
// this must be done before any validation as some properties like
|
|
89
81
|
// the labelField will not work when validating models without extending them first
|
|
90
|
-
const { models, errors: extendModelErrors } = extendModelMap(config
|
|
91
|
-
|
|
82
|
+
const { models: extendedModels, errors: extendModelErrors } = extendModelMap(config.models as any);
|
|
83
|
+
const extendedConfig = {
|
|
84
|
+
...config,
|
|
85
|
+
models: extendedModels
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
const { config: mergedConfig, errors: externalModelsMergeErrors } = mergeConfigWithExternalModels(extendedConfig, externalModels);
|
|
89
|
+
|
|
90
|
+
// validate the "contentModels" and extend config models with "contentModels"
|
|
91
|
+
// this must be done before main config validation to make it independent of "contentModels".
|
|
92
|
+
const { value: configWithContentModels, errors: contentModelsErrors } = validateAndExtendContentModels(mergedConfig);
|
|
92
93
|
|
|
93
94
|
// normalize config - backward compatibility updates, adding extra fields like "markdown_content", "type" and "layout",
|
|
94
95
|
// and setting other default values.
|
|
95
|
-
|
|
96
|
+
const normalizedConfig = normalizeConfig(configWithContentModels);
|
|
96
97
|
|
|
97
98
|
// validate config
|
|
98
|
-
const
|
|
99
|
+
const { value: validatedConfig, errors: validationErrors } = validateConfig(normalizedConfig);
|
|
100
|
+
|
|
101
|
+
const errors = [...extendModelErrors, ...externalModelsMergeErrors, ...contentModelsErrors, ...validationErrors];
|
|
99
102
|
|
|
100
|
-
const errors = [...contentModelsValidationResult.errors, ...extendModelErrors, ...configValidationResult.errors];
|
|
101
103
|
return normalizeValidationResult({
|
|
102
104
|
valid: _.isEmpty(errors),
|
|
103
|
-
value:
|
|
105
|
+
value: validatedConfig,
|
|
104
106
|
errors: errors
|
|
105
107
|
});
|
|
106
108
|
}
|
|
107
109
|
|
|
108
110
|
async function loadConfigFromDir(dirPath: string): Promise<TempConfigLoaderResult> {
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
111
|
+
try {
|
|
112
|
+
const { config, error } = await loadConfigFromStackbitYaml(dirPath);
|
|
113
|
+
if (error) {
|
|
114
|
+
return { errors: [error] };
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const { models: modelsFromFiles, errors: fileModelsErrors } = await loadModelsFromFiles(dirPath, config);
|
|
118
|
+
|
|
119
|
+
const mergedModels = mergeConfigModelsWithModelsFromFiles(config.models ?? {}, modelsFromFiles);
|
|
120
|
+
|
|
121
|
+
return {
|
|
122
|
+
config: {
|
|
123
|
+
...config,
|
|
124
|
+
models: mergedModels
|
|
125
|
+
},
|
|
126
|
+
errors: fileModelsErrors
|
|
127
|
+
};
|
|
128
|
+
} catch (error) {
|
|
129
|
+
return {
|
|
130
|
+
errors: [new ConfigLoadError(`Error loading Stackbit configuration: ${error.message}`, { originalError: error })]
|
|
131
|
+
};
|
|
112
132
|
}
|
|
113
|
-
const externalModelsResult = await loadExternalModels(dirPath, config);
|
|
114
|
-
config.models = _.assign(externalModelsResult.models, config.models);
|
|
115
|
-
return { config, errors: externalModelsResult.errors };
|
|
116
133
|
}
|
|
117
134
|
|
|
118
|
-
type
|
|
135
|
+
type StackbitYamlConfigResult = { config: any; error?: undefined } | { config?: undefined; error: ConfigLoadError };
|
|
119
136
|
|
|
120
|
-
async function loadConfigFromStackbitYaml(dirPath: string): Promise<
|
|
137
|
+
async function loadConfigFromStackbitYaml(dirPath: string): Promise<StackbitYamlConfigResult> {
|
|
121
138
|
const stackbitYamlPath = path.join(dirPath, 'stackbit.yaml');
|
|
122
139
|
const stackbitYamlExists = await fse.pathExists(stackbitYamlPath);
|
|
123
140
|
if (!stackbitYamlExists) {
|
|
@@ -135,54 +152,56 @@ async function loadConfigFromStackbitYaml(dirPath: string): Promise<LoadConfigFr
|
|
|
135
152
|
return { config };
|
|
136
153
|
}
|
|
137
154
|
|
|
138
|
-
async function
|
|
155
|
+
async function loadModelsFromFiles(dirPath: string, config: any): Promise<{ models: Record<string, any>; errors: ConfigLoadError[] }> {
|
|
139
156
|
const modelsSource = _.get(config, 'modelsSource', {});
|
|
140
157
|
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
|
|
158
|
+
const defaultModelDirs = ['node_modules/@stackbit/components/models', '.stackbit/models'];
|
|
159
|
+
const modelDirs =
|
|
160
|
+
sourceType === 'files'
|
|
161
|
+
? _.castArray(_.get(modelsSource, 'modelDirs', defaultModelDirs)).map((modelDir: string) => _.trim(modelDir, '/'))
|
|
162
|
+
: defaultModelDirs;
|
|
163
|
+
|
|
164
|
+
const modelFiles = await reducePromise(
|
|
165
|
+
modelDirs,
|
|
166
|
+
async (modelFiles: string[], modelDir) => {
|
|
167
|
+
const absModelsDir = path.join(dirPath, modelDir);
|
|
168
|
+
const dirExists = await fse.pathExists(absModelsDir);
|
|
169
|
+
if (!dirExists) {
|
|
170
|
+
return modelFiles;
|
|
171
|
+
}
|
|
172
|
+
const files = await readModelFilesFromDir(absModelsDir);
|
|
173
|
+
return modelFiles.concat(files.map((filePath) => path.join(modelDir, filePath)));
|
|
174
|
+
},
|
|
175
|
+
[]
|
|
176
|
+
);
|
|
177
|
+
|
|
178
|
+
return reducePromise(
|
|
179
|
+
modelFiles,
|
|
180
|
+
async (result: { models: any; errors: ConfigLoadError[] }, modelFile) => {
|
|
181
|
+
let model;
|
|
182
|
+
try {
|
|
183
|
+
model = await parseFile(path.join(dirPath, modelFile));
|
|
184
|
+
} catch (error) {
|
|
185
|
+
return {
|
|
186
|
+
models: result.models,
|
|
187
|
+
errors: result.errors.concat(new ConfigLoadError(`error parsing model, file: ${modelFile}`))
|
|
179
188
|
};
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
189
|
+
}
|
|
190
|
+
const modelName = model?.name;
|
|
191
|
+
if (!modelName) {
|
|
192
|
+
return {
|
|
193
|
+
models: result.models,
|
|
194
|
+
errors: result.errors.concat(new ConfigLoadError(`model does not have a name, file: ${modelFile}`))
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
result.models[modelName] = _.omit(model, 'name');
|
|
198
|
+
result.models[modelName].__metadata = {
|
|
199
|
+
filePath: modelFile
|
|
200
|
+
};
|
|
201
|
+
return result;
|
|
202
|
+
},
|
|
203
|
+
{ models: {}, errors: [] }
|
|
204
|
+
);
|
|
186
205
|
}
|
|
187
206
|
|
|
188
207
|
async function readModelFilesFromDir(modelsDir: string) {
|
|
@@ -197,6 +216,38 @@ async function readModelFilesFromDir(modelsDir: string) {
|
|
|
197
216
|
});
|
|
198
217
|
}
|
|
199
218
|
|
|
219
|
+
async function loadModelsFromExternalSource(
|
|
220
|
+
config: any,
|
|
221
|
+
dirPath: string,
|
|
222
|
+
modelsSource?: ModelsSource
|
|
223
|
+
): Promise<{ models: Model[]; errors: ConfigLoadError[] }> {
|
|
224
|
+
modelsSource = _.get(config, 'modelsSource', modelsSource);
|
|
225
|
+
const sourceType = _.get(modelsSource, 'type', 'files');
|
|
226
|
+
if (sourceType === 'files') {
|
|
227
|
+
return { models: [], errors: [] };
|
|
228
|
+
} else if (sourceType === 'contentful') {
|
|
229
|
+
const contentfulModule = _.get(modelsSource, 'module', '@stackbit/cms-contentful');
|
|
230
|
+
const modulePath = path.resolve(dirPath, 'node_modules', contentfulModule);
|
|
231
|
+
const module = await import(modulePath);
|
|
232
|
+
try {
|
|
233
|
+
const { models } = await module.fetchAndConvertSchema(_.omit(modelsSource, ['type', 'module']));
|
|
234
|
+
return {
|
|
235
|
+
models: models,
|
|
236
|
+
errors: []
|
|
237
|
+
};
|
|
238
|
+
} catch (error) {
|
|
239
|
+
return {
|
|
240
|
+
models: [],
|
|
241
|
+
errors: [new ConfigLoadError(`Error fetching and converting Contentful schema, error: ${error.message}`, { originalError: error })]
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
return {
|
|
246
|
+
models: [],
|
|
247
|
+
errors: [new ConfigLoadError(`modelsSource ${modelsSource} is unsupported`)]
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
|
|
200
251
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
201
252
|
async function loadConfigFromDotStackbit(dirPath: string) {
|
|
202
253
|
const stackbitDotPath = path.join(dirPath, '.stackbit');
|
|
@@ -231,6 +282,113 @@ async function loadConfigFromDotStackbit(dirPath: string) {
|
|
|
231
282
|
return _.isEmpty(config) ? null : config;
|
|
232
283
|
}
|
|
233
284
|
|
|
285
|
+
function mergeConfigModelsWithModelsFromFiles(configModels: any, modelsFromFiles: Record<string, any>) {
|
|
286
|
+
const mergedModels = _.mapValues(modelsFromFiles, (modelFromFile, modelName) => {
|
|
287
|
+
// resolve thumbnails of models loaded from files
|
|
288
|
+
const modelFilePath = modelFromFile.__metadata?.filePath;
|
|
289
|
+
resolveThumbnailPathForModel(modelFromFile, modelFilePath);
|
|
290
|
+
iterateModelFieldsRecursively(modelFromFile, (field: any) => {
|
|
291
|
+
if (isListField(field)) {
|
|
292
|
+
field = normalizeListFieldInPlace(field);
|
|
293
|
+
field = field.items;
|
|
294
|
+
}
|
|
295
|
+
if (isObjectField(field)) {
|
|
296
|
+
resolveThumbnailPathForModel(field, modelFilePath);
|
|
297
|
+
} else if (isEnumField(field)) {
|
|
298
|
+
resolveThumbnailPathForEnumField(field, modelFilePath);
|
|
299
|
+
}
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
const configModel = _.get(configModels, modelName);
|
|
303
|
+
if (!configModel) {
|
|
304
|
+
return modelFromFile;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
return _.assign({}, modelFromFile, configModel, {
|
|
308
|
+
fields: _.unionBy(configModel?.fields ?? [], modelFromFile?.fields ?? [], 'name')
|
|
309
|
+
});
|
|
310
|
+
});
|
|
311
|
+
return Object.assign({}, configModels, mergedModels);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function mergeConfigWithExternalModels(config: any, externalModels?: Model[]): { config: any; errors: ConfigValidationError[] } {
|
|
315
|
+
if (!externalModels || externalModels.length === 0) {
|
|
316
|
+
return {
|
|
317
|
+
config,
|
|
318
|
+
errors: []
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
const stackbitModels = config?.models ?? {};
|
|
323
|
+
const errors: ConfigValidationError[] = [];
|
|
324
|
+
|
|
325
|
+
const models = _.reduce(
|
|
326
|
+
externalModels,
|
|
327
|
+
(modelMap: Record<string, YamlModel>, externalModel) => {
|
|
328
|
+
const { name, ...rest } = externalModel;
|
|
329
|
+
return Object.assign(modelMap, { [name]: rest });
|
|
330
|
+
},
|
|
331
|
+
{}
|
|
332
|
+
);
|
|
333
|
+
|
|
334
|
+
_.forEach(stackbitModels, (stackbitModel: any, modelName: any) => {
|
|
335
|
+
let externalModel = models[modelName];
|
|
336
|
+
if (!externalModel) {
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
const modelType = stackbitModel.type ? (stackbitModel.type === 'config' ? 'data' : stackbitModel.type) : externalModel.type ?? 'object';
|
|
341
|
+
const urlPath = modelType === 'page' ? stackbitModel?.urlPath ?? '/{slug}' : null;
|
|
342
|
+
|
|
343
|
+
externalModel = Object.assign(
|
|
344
|
+
{},
|
|
345
|
+
externalModel,
|
|
346
|
+
_.pick(stackbitModel, ['__metadata', 'label', 'description', 'thumbnail', 'singleInstance', 'readOnly', 'labelField', 'fieldGroups']),
|
|
347
|
+
omitByNil({
|
|
348
|
+
type: modelType,
|
|
349
|
+
urlPath
|
|
350
|
+
})
|
|
351
|
+
);
|
|
352
|
+
|
|
353
|
+
externalModel = mapModelFieldsRecursively(externalModel as Model, (field, modelKeyPath) => {
|
|
354
|
+
const stackbitField = getModelFieldForModelKeyPath(stackbitModel, modelKeyPath);
|
|
355
|
+
if (!stackbitField) {
|
|
356
|
+
return field;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
let override = {};
|
|
360
|
+
if (stackbitField.type === 'style') {
|
|
361
|
+
override = stackbitField;
|
|
362
|
+
} else if (field.type === 'enum') {
|
|
363
|
+
override = _.pick(stackbitField, ['options']);
|
|
364
|
+
} else if (field.type === 'color') {
|
|
365
|
+
override = { type: 'color' };
|
|
366
|
+
} else if (field.type === 'number') {
|
|
367
|
+
override = _.pick(stackbitField, ['subtype', 'min', 'max', 'step', 'unit']);
|
|
368
|
+
} else if (field.type === 'object') {
|
|
369
|
+
override = _.pick(stackbitField, ['labelField', 'thumbnail', 'fieldGroups']);
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
return Object.assign(
|
|
373
|
+
{},
|
|
374
|
+
field,
|
|
375
|
+
_.pick(stackbitField, ['label', 'description', 'required', 'default', 'group', 'const', 'hidden', 'readOnly', 'controlType']),
|
|
376
|
+
override
|
|
377
|
+
);
|
|
378
|
+
}) as YamlModel;
|
|
379
|
+
|
|
380
|
+
models[modelName] = externalModel;
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
return {
|
|
384
|
+
config: {
|
|
385
|
+
...config,
|
|
386
|
+
models: models
|
|
387
|
+
},
|
|
388
|
+
errors: errors
|
|
389
|
+
};
|
|
390
|
+
}
|
|
391
|
+
|
|
234
392
|
function normalizeConfig(config: any): any {
|
|
235
393
|
const pageLayoutKey = _.get(config, 'pageLayoutKey', 'layout');
|
|
236
394
|
const objectTypeKey = _.get(config, 'objectTypeKey', 'type');
|
|
@@ -238,6 +396,7 @@ function normalizeConfig(config: any): any {
|
|
|
238
396
|
const ver = semver.coerce(stackbitYamlVersion);
|
|
239
397
|
const isStackbitYamlV2 = ver ? semver.satisfies(ver, '<0.3.0') : false;
|
|
240
398
|
const models = config?.models || {};
|
|
399
|
+
const gitCMS = isGitCMS(config);
|
|
241
400
|
let referencedModelNames: string[] = [];
|
|
242
401
|
|
|
243
402
|
_.forEach(models, (model, modelName) => {
|
|
@@ -245,6 +404,10 @@ function normalizeConfig(config: any): any {
|
|
|
245
404
|
return;
|
|
246
405
|
}
|
|
247
406
|
|
|
407
|
+
if (!_.has(model, 'type')) {
|
|
408
|
+
model.type = 'object';
|
|
409
|
+
}
|
|
410
|
+
|
|
248
411
|
// add model label if not set
|
|
249
412
|
if (!_.has(model, 'label')) {
|
|
250
413
|
model.label = _.startCase(modelName);
|
|
@@ -259,13 +422,15 @@ function normalizeConfig(config: any): any {
|
|
|
259
422
|
rename(model, 'template', 'layout');
|
|
260
423
|
|
|
261
424
|
updatePageUrlPath(model);
|
|
262
|
-
updatePageFilePath(model, config);
|
|
263
425
|
|
|
264
|
-
|
|
426
|
+
if (gitCMS) {
|
|
427
|
+
updatePageFilePath(model, config);
|
|
428
|
+
addMarkdownContentField(model);
|
|
265
429
|
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
430
|
+
// TODO: update schema-editor to not show layout field
|
|
431
|
+
addLayoutFieldToPageModel(model, pageLayoutKey);
|
|
432
|
+
}
|
|
433
|
+
} else if (isDataModel(model) && gitCMS) {
|
|
269
434
|
updateDataFilePath(model, config);
|
|
270
435
|
}
|
|
271
436
|
|
|
@@ -281,8 +446,6 @@ function normalizeConfig(config: any): any {
|
|
|
281
446
|
assignLabelFieldIfNeeded(model);
|
|
282
447
|
}
|
|
283
448
|
|
|
284
|
-
resolveThumbnailPathForModel(model, model?.__metadata?.filePath);
|
|
285
|
-
|
|
286
449
|
iterateModelFieldsRecursively(model, (field: any) => {
|
|
287
450
|
// add field label if label is not set
|
|
288
451
|
if (!_.has(field, 'label')) {
|
|
@@ -290,18 +453,12 @@ function normalizeConfig(config: any): any {
|
|
|
290
453
|
}
|
|
291
454
|
|
|
292
455
|
if (isListField(field)) {
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
_.set(field, 'items.type', 'string');
|
|
296
|
-
}
|
|
297
|
-
field = getListItemsField(field);
|
|
456
|
+
field = normalizeListFieldInPlace(field);
|
|
457
|
+
field = field.items;
|
|
298
458
|
}
|
|
299
459
|
|
|
300
460
|
if (isObjectField(field)) {
|
|
301
461
|
assignLabelFieldIfNeeded(field);
|
|
302
|
-
resolveThumbnailPathForModel(field, model?.__metadata?.filePath);
|
|
303
|
-
} else if (isEnumField(field)) {
|
|
304
|
-
resolveThumbnailPathForEnumField(field, model?.__metadata?.filePath);
|
|
305
462
|
} else if (isCustomModelField(field, models)) {
|
|
306
463
|
// stackbit v0.2.0 compatibility
|
|
307
464
|
// convert the old custom model field type: { type: 'action' }
|
|
@@ -331,7 +488,9 @@ function normalizeConfig(config: any): any {
|
|
|
331
488
|
}
|
|
332
489
|
}
|
|
333
490
|
|
|
334
|
-
|
|
491
|
+
if (gitCMS) {
|
|
492
|
+
referencedModelNames = _.union(referencedModelNames, getReferencedModelNames(field));
|
|
493
|
+
}
|
|
335
494
|
});
|
|
336
495
|
});
|
|
337
496
|
|
|
@@ -488,7 +647,7 @@ function resolveThumbnailPath(thumbnail: string, modelDirPath: string) {
|
|
|
488
647
|
*/
|
|
489
648
|
function getReferencedModelNames(field: any) {
|
|
490
649
|
if (isListField(field)) {
|
|
491
|
-
field =
|
|
650
|
+
field = getListFieldItems(field);
|
|
492
651
|
}
|
|
493
652
|
// TODO: add type field to model fields inside container update/create object logic rather adding type to schema
|
|
494
653
|
// 'object' models referenced by 'model' fields should have 'type' field
|
|
@@ -509,7 +668,10 @@ function validateAndExtendContentModels(config: any): ConfigValidationResult {
|
|
|
509
668
|
const contentModels = config.contentModels ?? {};
|
|
510
669
|
const models = config.models ?? {};
|
|
511
670
|
|
|
512
|
-
|
|
671
|
+
const externalModels = !isGitCMS(config);
|
|
672
|
+
const emptyContentModels = _.isEmpty(contentModels);
|
|
673
|
+
|
|
674
|
+
if (externalModels || emptyContentModels) {
|
|
513
675
|
return {
|
|
514
676
|
valid: true,
|
|
515
677
|
value: config,
|
|
@@ -520,17 +682,17 @@ function validateAndExtendContentModels(config: any): ConfigValidationResult {
|
|
|
520
682
|
const validationResult = validateContentModels(contentModels, models);
|
|
521
683
|
|
|
522
684
|
if (_.isEmpty(models)) {
|
|
523
|
-
return
|
|
685
|
+
return {
|
|
686
|
+
valid: validationResult.valid,
|
|
687
|
+
value: config,
|
|
688
|
+
errors: validationResult.errors
|
|
689
|
+
};
|
|
524
690
|
}
|
|
525
691
|
|
|
526
692
|
const extendedModels = _.mapValues(models, (model, modelName) => {
|
|
527
693
|
const contentModel = validationResult.value.contentModels[modelName];
|
|
528
694
|
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
|
-
};
|
|
695
|
+
return model;
|
|
534
696
|
}
|
|
535
697
|
if (_.get(contentModel, '__metadata.invalid')) {
|
|
536
698
|
return model;
|
|
@@ -565,10 +727,43 @@ function validateAndExtendContentModels(config: any): ConfigValidationResult {
|
|
|
565
727
|
}
|
|
566
728
|
|
|
567
729
|
function normalizeValidationResult(validationResult: ConfigValidationResult): NormalizedValidationResult {
|
|
730
|
+
validationResult = filterAndOrderConfigFields(validationResult);
|
|
568
731
|
convertModelGroupsToModelList(validationResult);
|
|
569
732
|
return convertModelsToArray(validationResult);
|
|
570
733
|
}
|
|
571
734
|
|
|
735
|
+
function filterAndOrderConfigFields(validationResult: ConfigValidationResult): ConfigValidationResult {
|
|
736
|
+
// TODO: see if we move filtering and sorting to Joi
|
|
737
|
+
return {
|
|
738
|
+
...validationResult,
|
|
739
|
+
value: _.pick(validationResult.value, [
|
|
740
|
+
'stackbitVersion',
|
|
741
|
+
'ssgName',
|
|
742
|
+
'ssgVersion',
|
|
743
|
+
'cmsName',
|
|
744
|
+
'import',
|
|
745
|
+
'buildCommand',
|
|
746
|
+
'publishDir',
|
|
747
|
+
'nodeVersion',
|
|
748
|
+
'devCommand',
|
|
749
|
+
'staticDir',
|
|
750
|
+
'uploadDir',
|
|
751
|
+
'assets',
|
|
752
|
+
'pagesDir',
|
|
753
|
+
'dataDir',
|
|
754
|
+
'pageLayoutKey',
|
|
755
|
+
'objectTypeKey',
|
|
756
|
+
'styleObjectModelName',
|
|
757
|
+
'excludePages',
|
|
758
|
+
'logicFields',
|
|
759
|
+
'contentModels',
|
|
760
|
+
'modelsSource',
|
|
761
|
+
'models',
|
|
762
|
+
'presets'
|
|
763
|
+
])
|
|
764
|
+
};
|
|
765
|
+
}
|
|
766
|
+
|
|
572
767
|
function convertModelGroupsToModelList(validationResult: ConfigValidationResult) {
|
|
573
768
|
const models = validationResult.value?.models ?? {};
|
|
574
769
|
|
|
@@ -598,7 +793,7 @@ function convertModelGroupsToModelList(validationResult: ConfigValidationResult)
|
|
|
598
793
|
_.forEach(models, (model) => {
|
|
599
794
|
iterateModelFieldsRecursively(model, (field: any) => {
|
|
600
795
|
if (isListField(field)) {
|
|
601
|
-
field =
|
|
796
|
+
field = field.items;
|
|
602
797
|
}
|
|
603
798
|
if (field.groups) {
|
|
604
799
|
let key: string | null = null;
|
|
@@ -640,6 +835,10 @@ function convertModelsToArray(validationResult: ConfigValidationResult): Normali
|
|
|
640
835
|
}
|
|
641
836
|
);
|
|
642
837
|
|
|
838
|
+
if (!isGitCMS(config)) {
|
|
839
|
+
addImageModel(modelArray);
|
|
840
|
+
}
|
|
841
|
+
|
|
643
842
|
const convertedErrors = _.map(validationResult.errors, (error: ConfigValidationError) => {
|
|
644
843
|
if (error.fieldPath[0] === 'models' && typeof error.fieldPath[1] == 'string') {
|
|
645
844
|
const modelName = error.fieldPath[1];
|
|
@@ -660,3 +859,20 @@ function convertModelsToArray(validationResult: ConfigValidationResult): Normali
|
|
|
660
859
|
errors: convertedErrors
|
|
661
860
|
};
|
|
662
861
|
}
|
|
862
|
+
|
|
863
|
+
function addImageModel(models: Model[]) {
|
|
864
|
+
models.push({
|
|
865
|
+
type: 'image',
|
|
866
|
+
name: '__image_model',
|
|
867
|
+
label: 'Image',
|
|
868
|
+
labelField: 'title',
|
|
869
|
+
fields: [
|
|
870
|
+
{ name: 'title', type: 'string' },
|
|
871
|
+
{ name: 'url', type: 'string' }
|
|
872
|
+
]
|
|
873
|
+
});
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
function isGitCMS(config: any) {
|
|
877
|
+
return !config.cmsName || config.cmsName === 'git';
|
|
878
|
+
}
|