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