@stackbit/sdk 0.3.5 → 0.3.7
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-loader-static.js +1 -1
- package/dist/config/config-loader-static.js.map +1 -1
- package/dist/config/config-loader-utils.d.ts +12 -7
- package/dist/config/config-loader-utils.d.ts.map +1 -1
- package/dist/config/config-loader-utils.js +104 -68
- package/dist/config/config-loader-utils.js.map +1 -1
- package/dist/config/config-loader.d.ts +46 -27
- package/dist/config/config-loader.d.ts.map +1 -1
- package/dist/config/config-loader.js +294 -265
- package/dist/config/config-loader.js.map +1 -1
- package/dist/config/config-schema.d.ts +2 -2
- package/dist/config/config-schema.d.ts.map +1 -1
- package/dist/config/config-schema.js +123 -55
- package/dist/config/config-schema.js.map +1 -1
- package/dist/config/config-types.d.ts +4 -3
- package/dist/config/config-types.d.ts.map +1 -1
- package/dist/config/config-validator.d.ts +9 -5
- package/dist/config/config-validator.d.ts.map +1 -1
- package/dist/config/config-validator.js +42 -23
- package/dist/config/config-validator.js.map +1 -1
- package/dist/config/presets-loader.d.ts +13 -4
- package/dist/config/presets-loader.d.ts.map +1 -1
- package/dist/config/presets-loader.js +49 -23
- package/dist/config/presets-loader.js.map +1 -1
- package/dist/content/content-schema.js +1 -1
- package/dist/content/content-schema.js.map +1 -1
- package/dist/index.d.ts +4 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +12 -2
- package/dist/index.js.map +1 -1
- package/dist/utils/index.d.ts +1 -8
- package/dist/utils/index.d.ts.map +1 -1
- package/dist/utils/index.js.map +1 -1
- package/dist/utils/model-extender.js +1 -1
- package/dist/utils/model-extender.js.map +1 -1
- package/dist/utils/model-iterators.d.ts +3 -2
- package/dist/utils/model-iterators.d.ts.map +1 -1
- package/dist/utils/model-iterators.js.map +1 -1
- package/dist/utils/model-utils.d.ts +9 -8
- package/dist/utils/model-utils.d.ts.map +1 -1
- package/dist/utils/model-utils.js +26 -9
- package/dist/utils/model-utils.js.map +1 -1
- package/package.json +4 -4
- package/src/config/config-loader-static.ts +1 -1
- package/src/config/config-loader-utils.ts +111 -78
- package/src/config/config-loader.ts +457 -394
- package/src/config/config-schema.ts +150 -81
- package/src/config/config-types.ts +6 -3
- package/src/config/config-validator.ts +51 -29
- package/src/config/presets-loader.ts +59 -30
- package/src/content/content-schema.ts +1 -1
- package/src/index.ts +21 -2
- package/src/utils/index.ts +1 -13
- package/src/utils/model-extender.ts +1 -1
- package/src/utils/model-iterators.ts +6 -5
- package/src/utils/model-utils.ts +38 -16
|
@@ -4,33 +4,31 @@ import chokidar from 'chokidar';
|
|
|
4
4
|
import semver from 'semver';
|
|
5
5
|
import _ from 'lodash';
|
|
6
6
|
|
|
7
|
-
import { ModelsSource,
|
|
8
|
-
import { append, getFirstExistingFile,
|
|
7
|
+
import { ModelsSource, Field } from '@stackbit/types';
|
|
8
|
+
import { append, getFirstExistingFile, prepend, rename } from '@stackbit/utils';
|
|
9
9
|
|
|
10
10
|
import { ConfigValidationResult, validateConfig, validateContentModels } from './config-validator';
|
|
11
|
-
import { ConfigError, ConfigLoadError,
|
|
11
|
+
import { ConfigError, ConfigLoadError, ConfigValidationError, STACKBIT_CONFIG_NOT_FOUND } from './config-errors';
|
|
12
12
|
import { loadStackbitConfigFromJs } from './config-loader-esbuild';
|
|
13
13
|
import {
|
|
14
14
|
assignLabelFieldIfNeeded,
|
|
15
|
-
|
|
16
|
-
getListFieldItems,
|
|
15
|
+
getListItemsOrSelf,
|
|
17
16
|
getModelFieldForModelKeyPath,
|
|
18
17
|
isCustomModelField,
|
|
19
18
|
isDataModel,
|
|
20
19
|
isListDataModel,
|
|
21
|
-
isListField,
|
|
22
20
|
isModelField,
|
|
23
21
|
isObjectField,
|
|
24
22
|
isObjectListItems,
|
|
25
23
|
isPageModel,
|
|
26
24
|
isReferenceField,
|
|
27
|
-
iterateModelFieldsRecursively,
|
|
28
25
|
mapModelFieldsRecursively,
|
|
29
26
|
normalizeListFieldInPlace,
|
|
30
|
-
Logger
|
|
27
|
+
Logger,
|
|
28
|
+
mapListItemsPropsOrSelfSpecificProps
|
|
31
29
|
} from '../utils';
|
|
32
|
-
import type { Config,
|
|
33
|
-
import { loadPresets } from './presets-loader';
|
|
30
|
+
import type { Config, SSGRunOptions, Model, PageModel, DataModel, DataModelSingle } from './config-types';
|
|
31
|
+
import { extendModelsWithPresetsIds, loadPresets } from './presets-loader';
|
|
34
32
|
import {
|
|
35
33
|
LoadRawConfigResult,
|
|
36
34
|
StopConfigWatch,
|
|
@@ -38,77 +36,165 @@ import {
|
|
|
38
36
|
LATEST_STACKBIT_VERSION,
|
|
39
37
|
STACKBIT_CONFIG_JS_FILES,
|
|
40
38
|
STACKBIT_CONFIG_YAML_FILES,
|
|
41
|
-
|
|
39
|
+
loadStackbitYamlFromDir,
|
|
40
|
+
loadYamlModelsFromFiles,
|
|
41
|
+
mergeConfigModelsWithModelsFromFiles
|
|
42
42
|
} from './config-loader-utils';
|
|
43
43
|
|
|
44
|
-
export interface
|
|
44
|
+
export interface ConfigWithModelsPresetsResult {
|
|
45
|
+
valid: boolean;
|
|
46
|
+
config: Config | null;
|
|
47
|
+
errors: ConfigError[];
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export async function loadConfigWithModelsPresetsAndValidate({
|
|
51
|
+
dirPath,
|
|
52
|
+
modelsSource,
|
|
53
|
+
stackbitConfigESBuildOutDir,
|
|
54
|
+
watchCallback,
|
|
55
|
+
logger
|
|
56
|
+
}: {
|
|
45
57
|
dirPath: string;
|
|
46
58
|
modelsSource?: ModelsSource;
|
|
47
59
|
stackbitConfigESBuildOutDir?: string;
|
|
48
|
-
watchCallback?: (result:
|
|
60
|
+
watchCallback?: (result: ConfigWithModelsPresetsResult) => void;
|
|
49
61
|
logger?: Logger;
|
|
62
|
+
}): Promise<ConfigWithModelsPresetsResult & StopConfigWatch> {
|
|
63
|
+
const configResult = await loadConfigWithModels({
|
|
64
|
+
dirPath,
|
|
65
|
+
stackbitConfigESBuildOutDir,
|
|
66
|
+
watchCallback: watchCallback
|
|
67
|
+
? async (configResult) => {
|
|
68
|
+
const configLoaderResult = await processConfigLoaderResult({ configResult, dirPath, modelsSource });
|
|
69
|
+
watchCallback(configLoaderResult);
|
|
70
|
+
}
|
|
71
|
+
: undefined,
|
|
72
|
+
logger
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
const configLoaderResult = await processConfigLoaderResult({ configResult, dirPath, modelsSource });
|
|
76
|
+
return {
|
|
77
|
+
...configLoaderResult,
|
|
78
|
+
stop: configResult.stop,
|
|
79
|
+
reload: configResult.reload
|
|
80
|
+
};
|
|
50
81
|
}
|
|
51
82
|
|
|
52
|
-
export
|
|
53
|
-
valid: boolean;
|
|
83
|
+
export type ConfigWithModelsResult = {
|
|
54
84
|
config: Config | null;
|
|
55
|
-
errors:
|
|
56
|
-
}
|
|
85
|
+
errors: (ConfigLoadError | ConfigValidationError)[];
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
export async function loadConfigWithModels({
|
|
89
|
+
dirPath,
|
|
90
|
+
stackbitConfigESBuildOutDir,
|
|
91
|
+
watchCallback,
|
|
92
|
+
logger
|
|
93
|
+
}: {
|
|
94
|
+
dirPath: string;
|
|
95
|
+
stackbitConfigESBuildOutDir?: string;
|
|
96
|
+
watchCallback?: (result: ConfigWithModelsResult) => void;
|
|
97
|
+
logger?: Logger;
|
|
98
|
+
}): Promise<ConfigWithModelsResult & StopConfigWatch> {
|
|
99
|
+
const wrapConfigResult = async (configResult: LoadConfigResult): Promise<ConfigWithModelsResult> => {
|
|
100
|
+
if (!configResult.config) {
|
|
101
|
+
return {
|
|
102
|
+
config: null,
|
|
103
|
+
errors: configResult.errors
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
return await loadAndMergeModelsFromFiles(configResult.config);
|
|
107
|
+
};
|
|
57
108
|
|
|
58
|
-
|
|
109
|
+
const rawConfigResult = await loadConfig({
|
|
110
|
+
dirPath,
|
|
111
|
+
stackbitConfigESBuildOutDir,
|
|
112
|
+
logger,
|
|
113
|
+
watchCallback: watchCallback
|
|
114
|
+
? async (configResult: LoadConfigResult) => {
|
|
115
|
+
const wrappedResult = await wrapConfigResult(configResult);
|
|
116
|
+
watchCallback(wrappedResult);
|
|
117
|
+
}
|
|
118
|
+
: undefined
|
|
119
|
+
});
|
|
59
120
|
|
|
60
|
-
|
|
61
|
-
valid: boolean;
|
|
62
|
-
config: Config;
|
|
63
|
-
errors: ConfigValidationError[];
|
|
64
|
-
}
|
|
121
|
+
const wrappedResult = await wrapConfigResult(rawConfigResult);
|
|
65
122
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
123
|
+
return {
|
|
124
|
+
...wrappedResult,
|
|
125
|
+
stop: rawConfigResult.stop,
|
|
126
|
+
reload: rawConfigResult.reload
|
|
127
|
+
};
|
|
69
128
|
}
|
|
70
129
|
|
|
71
|
-
export type
|
|
130
|
+
export type LoadConfigResult =
|
|
131
|
+
| {
|
|
132
|
+
config: Config;
|
|
133
|
+
errors: ConfigLoadError[];
|
|
134
|
+
}
|
|
135
|
+
| {
|
|
136
|
+
config: null;
|
|
137
|
+
errors: ConfigLoadError[];
|
|
138
|
+
};
|
|
72
139
|
|
|
73
140
|
export async function loadConfig({
|
|
74
141
|
dirPath,
|
|
75
|
-
modelsSource,
|
|
76
142
|
stackbitConfigESBuildOutDir,
|
|
77
143
|
watchCallback,
|
|
78
144
|
logger
|
|
79
|
-
}:
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
145
|
+
}: {
|
|
146
|
+
dirPath: string;
|
|
147
|
+
stackbitConfigESBuildOutDir?: string;
|
|
148
|
+
watchCallback?: (result: LoadConfigResult) => void;
|
|
149
|
+
logger?: Logger;
|
|
150
|
+
}): Promise<LoadConfigResult & StopConfigWatch> {
|
|
151
|
+
const normalizeConfigResult = (rawConfigResult: RawConfigLoaderResult): LoadConfigResult => {
|
|
152
|
+
if (!rawConfigResult.config) {
|
|
153
|
+
return {
|
|
154
|
+
config: null,
|
|
155
|
+
errors: [rawConfigResult.error]
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// TODO: validate config base properties after normalizing and return validation errors
|
|
160
|
+
const config = normalizeConfig(rawConfigResult.config);
|
|
161
|
+
return {
|
|
162
|
+
config: config,
|
|
163
|
+
errors: []
|
|
85
164
|
};
|
|
86
|
-
}
|
|
165
|
+
};
|
|
87
166
|
|
|
88
167
|
const rawConfigResult = await loadConfigFromDir({
|
|
89
168
|
dirPath,
|
|
90
169
|
stackbitConfigESBuildOutDir,
|
|
91
|
-
watchCallback:
|
|
170
|
+
watchCallback: watchCallback
|
|
171
|
+
? async (rawConfigResult: RawConfigLoaderResult) => {
|
|
172
|
+
const normalizedResult = await normalizeConfigResult(rawConfigResult);
|
|
173
|
+
watchCallback(normalizedResult);
|
|
174
|
+
}
|
|
175
|
+
: undefined,
|
|
92
176
|
logger
|
|
93
177
|
});
|
|
94
178
|
|
|
95
|
-
const
|
|
96
|
-
|
|
179
|
+
const normalizedResult = await normalizeConfigResult(rawConfigResult);
|
|
180
|
+
|
|
181
|
+
return {
|
|
182
|
+
...normalizedResult,
|
|
97
183
|
stop: rawConfigResult.stop,
|
|
98
184
|
reload: rawConfigResult.reload
|
|
99
|
-
}
|
|
185
|
+
};
|
|
100
186
|
}
|
|
101
187
|
|
|
102
188
|
async function processConfigLoaderResult({
|
|
103
|
-
|
|
189
|
+
configResult,
|
|
104
190
|
dirPath,
|
|
105
191
|
modelsSource
|
|
106
192
|
}: {
|
|
107
|
-
|
|
193
|
+
configResult: ConfigWithModelsResult;
|
|
108
194
|
dirPath: string;
|
|
109
195
|
modelsSource?: ModelsSource;
|
|
110
|
-
}): Promise<
|
|
111
|
-
const { config, errors: configLoadErrors } =
|
|
196
|
+
}): Promise<ConfigWithModelsPresetsResult> {
|
|
197
|
+
const { config, errors: configLoadErrors } = configResult;
|
|
112
198
|
|
|
113
199
|
if (!config) {
|
|
114
200
|
return {
|
|
@@ -120,67 +206,80 @@ async function processConfigLoaderResult({
|
|
|
120
206
|
|
|
121
207
|
const { models: externalModels, errors: externalModelsLoadErrors } = await loadModelsFromExternalSource(config, dirPath, modelsSource);
|
|
122
208
|
|
|
123
|
-
const
|
|
209
|
+
const mergedModels = mergeConfigModelsWithExternalModels({ configModels: config.models, externalModels });
|
|
210
|
+
const mergedConfig: Config = {
|
|
211
|
+
...config,
|
|
212
|
+
models: mergedModels
|
|
213
|
+
};
|
|
124
214
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
215
|
+
const normalizedResult = validateAndNormalizeConfig(mergedConfig);
|
|
216
|
+
|
|
217
|
+
const presetsResult = await loadPresets({ config: normalizedResult.config });
|
|
218
|
+
|
|
219
|
+
const modelsWithPresetIds = extendModelsWithPresetsIds({
|
|
220
|
+
models: normalizedResult.config.models,
|
|
221
|
+
presets: presetsResult.presets
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
const configWithPresets = {
|
|
225
|
+
...normalizedResult.config,
|
|
226
|
+
models: modelsWithPresetIds,
|
|
227
|
+
presets: presetsResult.presets
|
|
129
228
|
};
|
|
130
|
-
}
|
|
131
229
|
|
|
132
|
-
export async function extendConfig({
|
|
133
|
-
config,
|
|
134
|
-
externalModels
|
|
135
|
-
}: {
|
|
136
|
-
config: RawConfigWithPaths;
|
|
137
|
-
externalModels?: Model[];
|
|
138
|
-
}): Promise<{
|
|
139
|
-
valid: boolean;
|
|
140
|
-
config: Config;
|
|
141
|
-
errors: (ConfigValidationError | ConfigPresetsError)[];
|
|
142
|
-
}> {
|
|
143
|
-
const normalizedResult = validateAndNormalizeConfig(config, externalModels);
|
|
144
|
-
const presetsResult = await loadPresets(config.dirPath, normalizedResult.config);
|
|
145
230
|
return {
|
|
146
231
|
valid: normalizedResult.valid,
|
|
147
|
-
config:
|
|
148
|
-
errors: [...normalizedResult.errors, ...presetsResult.errors]
|
|
232
|
+
config: configWithPresets,
|
|
233
|
+
errors: [...configLoadErrors, ...externalModelsLoadErrors, ...normalizedResult.errors, ...presetsResult.errors]
|
|
149
234
|
};
|
|
150
235
|
}
|
|
151
236
|
|
|
152
|
-
export function
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
const
|
|
157
|
-
const extendedConfig: RawConfigWithPaths = {
|
|
237
|
+
export async function loadAndMergeModelsFromFiles(config: Config): Promise<{ config: Config; errors: (ConfigLoadError | ConfigValidationError)[] }> {
|
|
238
|
+
const { models: modelsFromFiles, errors: fileModelsErrors } = await loadYamlModelsFromFiles(config);
|
|
239
|
+
const { models: mergedModels, errors: mergeModelErrors } = mergeConfigModelsWithModelsFromFiles(config.models, modelsFromFiles);
|
|
240
|
+
|
|
241
|
+
const extendedConfig: Config = {
|
|
158
242
|
...config,
|
|
159
|
-
models:
|
|
243
|
+
models: mergedModels
|
|
160
244
|
};
|
|
161
245
|
|
|
162
|
-
|
|
246
|
+
return {
|
|
247
|
+
config: extendedConfig,
|
|
248
|
+
errors: [...fileModelsErrors, ...mergeModelErrors]
|
|
249
|
+
};
|
|
250
|
+
}
|
|
163
251
|
|
|
252
|
+
export function validateAndNormalizeConfig(config: Config): ConfigValidationResult {
|
|
164
253
|
// validate the "contentModels" and extend config models with "contentModels"
|
|
165
254
|
// this must be done before main config validation to make it independent of "contentModels".
|
|
166
|
-
const {
|
|
255
|
+
const { config: configWithContentModels, errors: contentModelsErrors } = validateAndExtendContentModels(config);
|
|
167
256
|
|
|
168
257
|
// normalize config - backward compatibility updates, adding extra fields like "markdown_content", "type" and "layout",
|
|
169
258
|
// and setting other default values.
|
|
170
|
-
const
|
|
259
|
+
const configWithNormalizedModels = normalizeModels(configWithContentModels);
|
|
171
260
|
|
|
172
261
|
// validate config
|
|
173
|
-
const {
|
|
262
|
+
const { config: validatedConfig, errors: validationErrors } = validateConfig(configWithNormalizedModels);
|
|
174
263
|
|
|
175
|
-
const errors = [...
|
|
264
|
+
const errors = [...contentModelsErrors, ...validationErrors];
|
|
176
265
|
|
|
177
266
|
return normalizeValidationResult({
|
|
178
267
|
valid: _.isEmpty(errors),
|
|
179
|
-
|
|
268
|
+
config: validatedConfig,
|
|
180
269
|
errors: errors
|
|
181
270
|
});
|
|
182
271
|
}
|
|
183
272
|
|
|
273
|
+
export type RawConfigLoaderResult =
|
|
274
|
+
| {
|
|
275
|
+
config: RawConfigWithPaths;
|
|
276
|
+
error: null;
|
|
277
|
+
}
|
|
278
|
+
| {
|
|
279
|
+
config: null;
|
|
280
|
+
error: ConfigLoadError;
|
|
281
|
+
};
|
|
282
|
+
|
|
184
283
|
export async function loadConfigFromDir({
|
|
185
284
|
dirPath,
|
|
186
285
|
stackbitConfigESBuildOutDir,
|
|
@@ -189,36 +288,46 @@ export async function loadConfigFromDir({
|
|
|
189
288
|
}: {
|
|
190
289
|
dirPath: string;
|
|
191
290
|
stackbitConfigESBuildOutDir?: string;
|
|
192
|
-
watchCallback?: (
|
|
291
|
+
watchCallback?: (result: RawConfigLoaderResult) => void;
|
|
193
292
|
logger?: Logger;
|
|
194
|
-
}): Promise<
|
|
293
|
+
}): Promise<RawConfigLoaderResult & StopConfigWatch> {
|
|
294
|
+
function wrapResult(result: LoadRawConfigResult, configFilePath: string): RawConfigLoaderResult {
|
|
295
|
+
if (result.error) {
|
|
296
|
+
return {
|
|
297
|
+
config: null,
|
|
298
|
+
error: result.error
|
|
299
|
+
};
|
|
300
|
+
} else {
|
|
301
|
+
return {
|
|
302
|
+
config: {
|
|
303
|
+
...result.config,
|
|
304
|
+
dirPath: dirPath,
|
|
305
|
+
filePath: configFilePath
|
|
306
|
+
},
|
|
307
|
+
error: null
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
195
312
|
// try to load stackbit config from YAML files
|
|
196
313
|
try {
|
|
197
314
|
const stackbitYamlPath = await getFirstExistingFile(STACKBIT_CONFIG_YAML_FILES, dirPath);
|
|
198
315
|
if (stackbitYamlPath) {
|
|
199
|
-
|
|
316
|
+
logger?.debug(`loading Stackbit configuration from ${stackbitYamlPath}`);
|
|
317
|
+
const result = await loadStackbitYamlFromDir(dirPath);
|
|
200
318
|
let close: () => Promise<void> = async () => void 0;
|
|
201
319
|
let reload: () => void = () => void 0;
|
|
202
320
|
let stopped = false;
|
|
203
321
|
|
|
204
322
|
if (watchCallback) {
|
|
205
|
-
const watcher = chokidar.watch([...STACKBIT_CONFIG_YAML_FILES
|
|
323
|
+
const watcher = chokidar.watch([...STACKBIT_CONFIG_YAML_FILES], {
|
|
206
324
|
cwd: dirPath,
|
|
207
325
|
persistent: true,
|
|
208
326
|
ignoreInitial: true
|
|
209
327
|
});
|
|
210
328
|
const throttledFileChange = _.throttle(async () => {
|
|
211
|
-
const
|
|
212
|
-
watchCallback(
|
|
213
|
-
config: config
|
|
214
|
-
? {
|
|
215
|
-
...config,
|
|
216
|
-
dirPath: dirPath,
|
|
217
|
-
filePath: stackbitYamlPath
|
|
218
|
-
}
|
|
219
|
-
: undefined,
|
|
220
|
-
errors
|
|
221
|
-
});
|
|
329
|
+
const result = await loadStackbitYamlFromDir(dirPath);
|
|
330
|
+
watchCallback(wrapResult(result, stackbitYamlPath));
|
|
222
331
|
}, 1000);
|
|
223
332
|
const handleFileChange = (path: string) => {
|
|
224
333
|
logger?.debug(`identified change in stackbit config file: ${path}, reloading config...`);
|
|
@@ -231,7 +340,8 @@ export async function loadConfigFromDir({
|
|
|
231
340
|
watcher.on('unlinkDir', handleFileChange);
|
|
232
341
|
watcher.on('error', (error) => {
|
|
233
342
|
watchCallback({
|
|
234
|
-
|
|
343
|
+
config: null,
|
|
344
|
+
error: new ConfigLoadError(`Error loading Stackbit configuration: ${error.message}`, { originalError: error })
|
|
235
345
|
});
|
|
236
346
|
});
|
|
237
347
|
close = async () => {
|
|
@@ -248,45 +358,23 @@ export async function loadConfigFromDir({
|
|
|
248
358
|
}
|
|
249
359
|
|
|
250
360
|
return {
|
|
251
|
-
|
|
252
|
-
? {
|
|
253
|
-
...config,
|
|
254
|
-
dirPath: dirPath,
|
|
255
|
-
filePath: stackbitYamlPath
|
|
256
|
-
}
|
|
257
|
-
: undefined,
|
|
361
|
+
...wrapResult(result, stackbitYamlPath),
|
|
258
362
|
stop: close,
|
|
259
|
-
reload: reload
|
|
260
|
-
errors: errors
|
|
363
|
+
reload: reload
|
|
261
364
|
};
|
|
262
365
|
}
|
|
263
366
|
} catch (error: any) {
|
|
264
367
|
return {
|
|
265
|
-
|
|
368
|
+
config: null,
|
|
369
|
+
error: new ConfigLoadError(`Error loading Stackbit configuration: ${error.message}`, { originalError: error })
|
|
266
370
|
};
|
|
267
371
|
}
|
|
268
372
|
|
|
269
|
-
function wrapResult(result: LoadRawConfigResult, configFilePath: string): RawConfigLoaderResult {
|
|
270
|
-
if (result.error) {
|
|
271
|
-
return {
|
|
272
|
-
errors: [result.error]
|
|
273
|
-
};
|
|
274
|
-
} else {
|
|
275
|
-
return {
|
|
276
|
-
config: {
|
|
277
|
-
...result.config,
|
|
278
|
-
dirPath: dirPath,
|
|
279
|
-
filePath: configFilePath
|
|
280
|
-
},
|
|
281
|
-
errors: []
|
|
282
|
-
};
|
|
283
|
-
}
|
|
284
|
-
}
|
|
285
|
-
|
|
286
373
|
// try to load stackbit config from JavaScript files
|
|
287
374
|
try {
|
|
288
375
|
const configFilePath = await getFirstExistingFile(STACKBIT_CONFIG_JS_FILES, dirPath);
|
|
289
376
|
if (configFilePath) {
|
|
377
|
+
logger?.debug(`loading Stackbit configuration from: ${configFilePath}`);
|
|
290
378
|
const configResult = await loadStackbitConfigFromJs({
|
|
291
379
|
configPath: configFilePath,
|
|
292
380
|
outDir: stackbitConfigESBuildOutDir ?? '.stackbit/cache',
|
|
@@ -302,17 +390,19 @@ export async function loadConfigFromDir({
|
|
|
302
390
|
}
|
|
303
391
|
} catch (error: any) {
|
|
304
392
|
return {
|
|
305
|
-
|
|
393
|
+
config: null,
|
|
394
|
+
error: new ConfigLoadError(`Error loading Stackbit configuration: ${error.message}`, { originalError: error })
|
|
306
395
|
};
|
|
307
396
|
}
|
|
308
397
|
|
|
309
398
|
return {
|
|
310
|
-
|
|
399
|
+
config: null,
|
|
400
|
+
error: new ConfigLoadError(STACKBIT_CONFIG_NOT_FOUND)
|
|
311
401
|
};
|
|
312
402
|
}
|
|
313
403
|
|
|
314
404
|
async function loadModelsFromExternalSource(
|
|
315
|
-
config:
|
|
405
|
+
config: Config,
|
|
316
406
|
dirPath: string,
|
|
317
407
|
modelsSource?: ModelsSource
|
|
318
408
|
): Promise<{ models: Model[]; errors: ConfigLoadError[] }> {
|
|
@@ -378,201 +468,225 @@ async function loadConfigFromDotStackbit(dirPath: string) {
|
|
|
378
468
|
return _.isEmpty(config) ? null : config;
|
|
379
469
|
}
|
|
380
470
|
|
|
381
|
-
function
|
|
382
|
-
if (
|
|
383
|
-
return
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
471
|
+
export function mergeConfigModelsWithExternalModels({ configModels, externalModels }: { configModels: Model[]; externalModels: Model[] }): Model[] {
|
|
472
|
+
if (externalModels.length === 0) {
|
|
473
|
+
return configModels;
|
|
474
|
+
}
|
|
475
|
+
if (configModels.length === 0) {
|
|
476
|
+
return externalModels;
|
|
387
477
|
}
|
|
388
478
|
|
|
389
|
-
const
|
|
390
|
-
const errors: ConfigValidationError[] = [];
|
|
479
|
+
const mergedModelsByName: Record<string, Model> = _.keyBy(externalModels, 'name');
|
|
391
480
|
|
|
392
|
-
const
|
|
393
|
-
|
|
394
|
-
(modelMap: Record<string, YamlModel>, externalModel) => {
|
|
395
|
-
const { name, ...rest } = externalModel;
|
|
396
|
-
return Object.assign(modelMap, { [name]: rest });
|
|
397
|
-
},
|
|
398
|
-
{}
|
|
399
|
-
);
|
|
400
|
-
|
|
401
|
-
_.forEach(stackbitModels, (stackbitModel: any, modelName: any) => {
|
|
402
|
-
let externalModel = models[modelName];
|
|
481
|
+
for (const configModel of configModels) {
|
|
482
|
+
const externalModel = mergedModelsByName[configModel.name];
|
|
403
483
|
if (!externalModel) {
|
|
404
|
-
|
|
484
|
+
continue;
|
|
405
485
|
}
|
|
406
486
|
|
|
407
|
-
const modelType =
|
|
408
|
-
const urlPath = modelType === 'page' ? stackbitModel?.urlPath ?? '/{slug}' : null;
|
|
487
|
+
const modelType = configModel.type ? (configModel.type === 'config' ? 'data' : configModel.type) : externalModel.type ?? 'object';
|
|
409
488
|
|
|
410
|
-
|
|
489
|
+
let mergedModel: Model = Object.assign(
|
|
411
490
|
{},
|
|
412
491
|
externalModel,
|
|
413
|
-
_.pick(
|
|
414
|
-
|
|
415
|
-
type: modelType,
|
|
416
|
-
urlPath
|
|
417
|
-
})
|
|
492
|
+
_.pick(configModel, ['__metadata', 'urlPath', 'label', 'description', 'thumbnail', 'singleInstance', 'readOnly', 'labelField', 'fieldGroups']),
|
|
493
|
+
{ type: modelType }
|
|
418
494
|
);
|
|
419
495
|
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
return externalField;
|
|
424
|
-
}
|
|
496
|
+
if (mergedModel.type === 'page' && !mergedModel.urlPath) {
|
|
497
|
+
mergedModel.urlPath = '/{slug}';
|
|
498
|
+
}
|
|
425
499
|
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
override =
|
|
435
|
-
|
|
436
|
-
|
|
500
|
+
mergedModel = mapModelFieldsRecursively(
|
|
501
|
+
mergedModel,
|
|
502
|
+
(externalField, modelKeyPath): Field => {
|
|
503
|
+
const stackbitField = getModelFieldForModelKeyPath(configModel, modelKeyPath);
|
|
504
|
+
if (!stackbitField) {
|
|
505
|
+
return externalField;
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
let override = {};
|
|
509
|
+
if (externalField.type === 'json' && stackbitField.type === 'style') {
|
|
510
|
+
override = stackbitField;
|
|
511
|
+
} else if (externalField.type === 'string' && stackbitField.type === 'color') {
|
|
512
|
+
override = { type: 'color' };
|
|
513
|
+
} else if (externalField.type === 'enum') {
|
|
514
|
+
override = _.pick(stackbitField, ['options']);
|
|
515
|
+
} else if (externalField.type === 'number') {
|
|
516
|
+
override = _.pick(stackbitField, ['subtype', 'min', 'max', 'step', 'unit']);
|
|
517
|
+
} else if (externalField.type === 'object') {
|
|
518
|
+
override = _.pick(stackbitField, ['labelField', 'thumbnail', 'fieldGroups']);
|
|
519
|
+
} else if (externalField.type === 'reference' || externalField.type === 'model') {
|
|
520
|
+
override = _.pick(stackbitField, ['models']);
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
return Object.assign(
|
|
524
|
+
{},
|
|
525
|
+
externalField,
|
|
526
|
+
_.pick(stackbitField, ['label', 'description', 'required', 'default', 'group', 'const', 'hidden', 'readOnly', 'controlType']),
|
|
527
|
+
override
|
|
528
|
+
);
|
|
437
529
|
}
|
|
530
|
+
);
|
|
438
531
|
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
externalField,
|
|
442
|
-
_.pick(stackbitField, ['label', 'description', 'required', 'default', 'group', 'const', 'hidden', 'readOnly', 'controlType']),
|
|
443
|
-
override
|
|
444
|
-
);
|
|
445
|
-
}) as YamlModel;
|
|
532
|
+
mergedModelsByName[configModel.name] = mergedModel;
|
|
533
|
+
}
|
|
446
534
|
|
|
447
|
-
|
|
448
|
-
|
|
535
|
+
return Object.values(mergedModelsByName);
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
function normalizeConfig(rawConfig: RawConfigWithPaths): Config {
|
|
539
|
+
const stackbitVersion = String(_.get(rawConfig, 'stackbitVersion', LATEST_STACKBIT_VERSION));
|
|
540
|
+
const ver = semver.coerce(stackbitVersion);
|
|
541
|
+
const isGTEStackbitYamlV5 = ver ? semver.satisfies(ver, '>=0.5.0') : false;
|
|
542
|
+
|
|
543
|
+
const { logicFields, models: modelMap, ...restConfig } = rawConfig;
|
|
544
|
+
|
|
545
|
+
// in stackbit.yaml 'models' are defined as object where keys are the model names,
|
|
546
|
+
// convert 'models' to array of objects and set their 'name' property to the model name
|
|
547
|
+
const models = _.reduce(modelMap, (accum: Model[], model, modelName) => accum.concat(Object.assign({ name: modelName }, model)), []);
|
|
449
548
|
|
|
450
549
|
return {
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
550
|
+
...restConfig,
|
|
551
|
+
stackbitVersion: stackbitVersion,
|
|
552
|
+
models: models,
|
|
553
|
+
noEncodeFields: logicFields,
|
|
554
|
+
hcrHandled: !stackbitVersion || _.get(rawConfig, 'customContentReload', _.get(rawConfig, 'hcrHandled', !isGTEStackbitYamlV5)),
|
|
555
|
+
internalStackbitRunnerOptions: getInternalStackbitRunnerOptions(rawConfig)
|
|
456
556
|
};
|
|
457
557
|
}
|
|
458
558
|
|
|
459
|
-
function
|
|
460
|
-
const pageLayoutKey =
|
|
461
|
-
const objectTypeKey =
|
|
462
|
-
const stackbitYamlVersion = String(
|
|
559
|
+
function normalizeModels(config: Config): Config {
|
|
560
|
+
const pageLayoutKey = config.pageLayoutKey ?? 'layout';
|
|
561
|
+
const objectTypeKey = config.objectTypeKey ?? 'type';
|
|
562
|
+
const stackbitYamlVersion = String(config.stackbitVersion ?? '');
|
|
463
563
|
const ver = semver.coerce(stackbitYamlVersion);
|
|
464
564
|
const isStackbitYamlV2 = ver ? semver.satisfies(ver, '<0.3.0') : false;
|
|
465
|
-
const
|
|
466
|
-
const
|
|
565
|
+
const models = config.models;
|
|
566
|
+
const modelsByName = _.keyBy(models, 'name');
|
|
467
567
|
const gitCMS = isGitCMS(config);
|
|
468
568
|
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
_.forEach(models, (model, modelName) => {
|
|
474
|
-
if (!model) {
|
|
475
|
-
return;
|
|
476
|
-
}
|
|
477
|
-
|
|
478
|
-
if (!_.has(model, 'type')) {
|
|
479
|
-
model.type = 'object';
|
|
480
|
-
}
|
|
481
|
-
|
|
482
|
-
// add model label if not set
|
|
483
|
-
if (!_.has(model, 'label')) {
|
|
484
|
-
model.label = _.startCase(modelName);
|
|
485
|
-
}
|
|
569
|
+
const mappedModels = models.map(
|
|
570
|
+
(model): Model => {
|
|
571
|
+
// create shallow copy of the model to prevent mutation of original models
|
|
572
|
+
model = { ...model };
|
|
486
573
|
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
if (isPageModel(model)) {
|
|
492
|
-
// rename old 'template' property to 'layout'
|
|
493
|
-
rename(model, 'template', 'layout');
|
|
574
|
+
if (!_.has(model, 'type')) {
|
|
575
|
+
model.type = 'object';
|
|
576
|
+
}
|
|
494
577
|
|
|
495
|
-
|
|
578
|
+
// add model label if not set
|
|
579
|
+
if (!_.has(model, 'label')) {
|
|
580
|
+
model.label = _.startCase(model.name);
|
|
581
|
+
}
|
|
496
582
|
|
|
497
|
-
if (
|
|
498
|
-
|
|
499
|
-
addMarkdownContentField(model);
|
|
583
|
+
if (_.has(model, 'fields') && !Array.isArray(model.fields)) {
|
|
584
|
+
model.fields = [];
|
|
500
585
|
}
|
|
501
|
-
} else if (isDataModel(model) && gitCMS) {
|
|
502
|
-
updateDataFilePath(model, config);
|
|
503
|
-
}
|
|
504
586
|
|
|
505
|
-
if (gitCMS) {
|
|
506
|
-
// TODO: do not add pageLayoutKey and objectTypeKey fields to models,
|
|
507
|
-
// The content validator should always assume these fields.
|
|
508
|
-
// And when new objects created from UI, it should add these fields automatically.
|
|
509
587
|
if (isPageModel(model)) {
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
addObjectTypeKeyField(model, objectTypeKey, modelName);
|
|
513
|
-
}
|
|
514
|
-
}
|
|
588
|
+
// rename old 'template' property to 'layout'
|
|
589
|
+
rename(model, 'template', 'layout');
|
|
515
590
|
|
|
516
|
-
|
|
517
|
-
// 'items.type' of list model defaults to 'string', set it explicitly
|
|
518
|
-
if (!_.has(model, 'items.type')) {
|
|
519
|
-
_.set(model, 'items.type', 'string');
|
|
520
|
-
}
|
|
521
|
-
if (isObjectListItems(model.items)) {
|
|
522
|
-
assignLabelFieldIfNeeded(model.items);
|
|
523
|
-
}
|
|
524
|
-
} else if (!_.has(model, 'labelField')) {
|
|
525
|
-
assignLabelFieldIfNeeded(model);
|
|
526
|
-
}
|
|
591
|
+
updatePageUrlPath(model);
|
|
527
592
|
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
593
|
+
if (gitCMS) {
|
|
594
|
+
updatePageFilePath(model, config);
|
|
595
|
+
addMarkdownContentField(model);
|
|
596
|
+
}
|
|
597
|
+
} else if (isDataModel(model) && gitCMS) {
|
|
598
|
+
updateDataFilePath(model, config);
|
|
532
599
|
}
|
|
533
600
|
|
|
534
|
-
if (
|
|
535
|
-
|
|
536
|
-
|
|
601
|
+
if (gitCMS) {
|
|
602
|
+
// TODO: do not add pageLayoutKey and objectTypeKey fields to models,
|
|
603
|
+
// The content validator should always assume these fields.
|
|
604
|
+
// And when new objects created from UI, it should add these fields automatically.
|
|
605
|
+
if (isPageModel(model)) {
|
|
606
|
+
addLayoutFieldToPageModel(model, pageLayoutKey, model.name);
|
|
607
|
+
} else if (isDataModel(model) && !isListDataModel(model)) {
|
|
608
|
+
addObjectTypeKeyField(model, objectTypeKey, model.name);
|
|
609
|
+
}
|
|
537
610
|
}
|
|
538
611
|
|
|
539
|
-
if (
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
} else if (field.type === 'models') {
|
|
548
|
-
// stackbit v0.2.0 compatibility
|
|
549
|
-
// convert the old 'models' field type: { type: 'models', models: ['link', 'button'] }
|
|
550
|
-
// to the new 'model' field type: { type: 'model', models: ['link', 'button'] }
|
|
551
|
-
field.type = 'model';
|
|
552
|
-
field.models = _.get(field, 'models', []);
|
|
553
|
-
} else if (field.type === 'model' && _.has(field, 'model')) {
|
|
554
|
-
// stackbit v0.2.0 compatibility
|
|
555
|
-
// convert the old 'model' field type: { type: 'model', model: 'link' }
|
|
556
|
-
// to the new 'model' field type: { type: 'model', models: ['link'] }
|
|
557
|
-
field.models = [field.model];
|
|
558
|
-
delete field.model;
|
|
612
|
+
if (isListDataModel(model)) {
|
|
613
|
+
// 'items.type' of list model defaults to 'string', set it explicitly
|
|
614
|
+
normalizeListFieldInPlace(model);
|
|
615
|
+
if (isObjectListItems(model.items)) {
|
|
616
|
+
assignLabelFieldIfNeeded(model.items);
|
|
617
|
+
}
|
|
618
|
+
} else if (!_.has(model, 'labelField')) {
|
|
619
|
+
assignLabelFieldIfNeeded(model);
|
|
559
620
|
}
|
|
560
621
|
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
field
|
|
566
|
-
|
|
622
|
+
return mapModelFieldsRecursively(
|
|
623
|
+
model,
|
|
624
|
+
(field: Field): Field => {
|
|
625
|
+
// create shallow copy of the field to prevent mutation of original field
|
|
626
|
+
field = { ...field };
|
|
627
|
+
|
|
628
|
+
// add field label if label is not set
|
|
629
|
+
if (!_.has(field, 'label')) {
|
|
630
|
+
field.label = _.startCase(field.name);
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
return mapListItemsPropsOrSelfSpecificProps(field, (fieldSpecificProps) => {
|
|
634
|
+
if (isObjectField(fieldSpecificProps)) {
|
|
635
|
+
assignLabelFieldIfNeeded(fieldSpecificProps);
|
|
636
|
+
} else if (isCustomModelField(fieldSpecificProps, modelsByName)) {
|
|
637
|
+
// stackbit v0.2.0 compatibility
|
|
638
|
+
// convert the old custom model field type: { type: 'action' }
|
|
639
|
+
// to the new 'model' field type: { type: 'model', models: ['action'] }
|
|
640
|
+
fieldSpecificProps = {
|
|
641
|
+
...fieldSpecificProps,
|
|
642
|
+
type: 'model',
|
|
643
|
+
models: [fieldSpecificProps.type]
|
|
644
|
+
};
|
|
645
|
+
} else if ((fieldSpecificProps as any).type === 'models') {
|
|
646
|
+
// stackbit v0.2.0 compatibility
|
|
647
|
+
// convert the old 'models' field type: { type: 'models', models: ['link', 'button'] }
|
|
648
|
+
// to the new 'model' field type: { type: 'model', models: ['link', 'button'] }
|
|
649
|
+
fieldSpecificProps = {
|
|
650
|
+
...fieldSpecificProps,
|
|
651
|
+
type: 'model',
|
|
652
|
+
models: _.get(fieldSpecificProps, 'models', [])
|
|
653
|
+
};
|
|
654
|
+
} else if (fieldSpecificProps.type === 'model' && _.has(fieldSpecificProps, 'model')) {
|
|
655
|
+
// stackbit v0.2.0 compatibility
|
|
656
|
+
// convert the old 'model' field type: { type: 'model', model: 'link' }
|
|
657
|
+
// to the new 'model' field type: { type: 'model', models: ['link'] }
|
|
658
|
+
const { model, ...rest } = fieldSpecificProps as any;
|
|
659
|
+
fieldSpecificProps = {
|
|
660
|
+
...rest,
|
|
661
|
+
models: [model]
|
|
662
|
+
};
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
if (isStackbitYamlV2) {
|
|
666
|
+
// in stackbit.yaml v0.2.x, the 'reference' field was what we have today as 'model' field:
|
|
667
|
+
if (isReferenceField(fieldSpecificProps)) {
|
|
668
|
+
fieldSpecificProps = {
|
|
669
|
+
...fieldSpecificProps,
|
|
670
|
+
type: 'model',
|
|
671
|
+
models: _.get(fieldSpecificProps, 'models', [])
|
|
672
|
+
};
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
return fieldSpecificProps;
|
|
677
|
+
});
|
|
567
678
|
}
|
|
568
|
-
|
|
569
|
-
}
|
|
570
|
-
|
|
679
|
+
);
|
|
680
|
+
}
|
|
681
|
+
);
|
|
571
682
|
|
|
572
|
-
return
|
|
683
|
+
return {
|
|
684
|
+
...config,
|
|
685
|
+
models: mappedModels
|
|
686
|
+
};
|
|
573
687
|
}
|
|
574
688
|
|
|
575
|
-
function updatePageUrlPath(model:
|
|
689
|
+
function updatePageUrlPath(model: PageModel) {
|
|
576
690
|
// set default urlPath if not set
|
|
577
691
|
if (!model.urlPath) {
|
|
578
692
|
model.urlPath = '/{slug}';
|
|
@@ -585,7 +699,7 @@ function updatePageUrlPath(model: YamlPageModel) {
|
|
|
585
699
|
* If the model has no `filePath` property, then `filePath` is naively inferred by
|
|
586
700
|
* prefixing `urlPath` with `pagesDir` and appending the `.md` extension.
|
|
587
701
|
*/
|
|
588
|
-
function updatePageFilePath(model:
|
|
702
|
+
function updatePageFilePath(model: PageModel, config: Config) {
|
|
589
703
|
let filePath;
|
|
590
704
|
if (model.filePath) {
|
|
591
705
|
filePath = model.filePath;
|
|
@@ -605,7 +719,7 @@ function updatePageFilePath(model: YamlPageModel, config: Config) {
|
|
|
605
719
|
model.filePath = path.join(parentDir, filePath);
|
|
606
720
|
}
|
|
607
721
|
|
|
608
|
-
function updateDataFilePath(model:
|
|
722
|
+
function updateDataFilePath(model: DataModel, config: Config) {
|
|
609
723
|
let filePath;
|
|
610
724
|
if (model.filePath) {
|
|
611
725
|
filePath = model.filePath;
|
|
@@ -619,7 +733,7 @@ function updateDataFilePath(model: YamlDataModel, config: Config) {
|
|
|
619
733
|
model.filePath = path.join(parentDir, filePath);
|
|
620
734
|
}
|
|
621
735
|
|
|
622
|
-
function addMarkdownContentField(model:
|
|
736
|
+
function addMarkdownContentField(model: PageModel) {
|
|
623
737
|
if (model.hideContent) {
|
|
624
738
|
return;
|
|
625
739
|
}
|
|
@@ -635,7 +749,7 @@ function addMarkdownContentField(model: YamlPageModel) {
|
|
|
635
749
|
});
|
|
636
750
|
}
|
|
637
751
|
|
|
638
|
-
function addLayoutFieldToPageModel(model:
|
|
752
|
+
function addLayoutFieldToPageModel(model: PageModel, pageLayoutKey: string, modelName: string) {
|
|
639
753
|
if (_.intersection(_.keys(model), ['file', 'folder', 'match', 'exclude']).length === 0 && !_.get(model, 'layout')) {
|
|
640
754
|
model.layout = modelName;
|
|
641
755
|
}
|
|
@@ -656,7 +770,7 @@ function addLayoutFieldToPageModel(model: any, pageLayoutKey: any, modelName: st
|
|
|
656
770
|
});
|
|
657
771
|
}
|
|
658
772
|
|
|
659
|
-
function addObjectTypeKeyField(model:
|
|
773
|
+
function addObjectTypeKeyField(model: DataModelSingle, objectTypeKey: string, modelName: string) {
|
|
660
774
|
const hasObjectTypeField = _.find(_.get(model, 'fields'), { name: objectTypeKey });
|
|
661
775
|
if (hasObjectTypeField) {
|
|
662
776
|
return;
|
|
@@ -673,40 +787,42 @@ function addObjectTypeKeyField(model: any, objectTypeKey: string, modelName: str
|
|
|
673
787
|
|
|
674
788
|
/**
|
|
675
789
|
* Returns model names referenced by polymorphic 'model' and 'reference' fields.
|
|
676
|
-
* That is, fields that can hold objects of different types.
|
|
677
790
|
*
|
|
678
791
|
* @param field
|
|
679
792
|
*/
|
|
680
|
-
function getReferencedModelNames(field:
|
|
681
|
-
|
|
682
|
-
field = getListFieldItems(field);
|
|
683
|
-
}
|
|
793
|
+
function getReferencedModelNames(field: Field) {
|
|
794
|
+
const fieldSpecificProps = getListItemsOrSelf(field);
|
|
684
795
|
// TODO: add type field to model fields inside container update/create object logic rather adding type to schema
|
|
685
796
|
// 'object' models referenced by 'model' fields should have 'type' field
|
|
686
797
|
// if these fields have than 1 model.
|
|
687
798
|
// 'data' models referenced by 'reference' fields should always have 'type' field.
|
|
688
799
|
let referencedModelNames: string[] = [];
|
|
689
|
-
if (isModelField(
|
|
690
|
-
const modelNames =
|
|
800
|
+
if (isModelField(fieldSpecificProps) && fieldSpecificProps.models?.length > 1) {
|
|
801
|
+
const modelNames = fieldSpecificProps.models;
|
|
691
802
|
referencedModelNames = _.union(referencedModelNames, modelNames);
|
|
692
|
-
} else if (isReferenceField(
|
|
693
|
-
const modelNames =
|
|
803
|
+
} else if (isReferenceField(fieldSpecificProps) && fieldSpecificProps.models?.length > 0) {
|
|
804
|
+
const modelNames = fieldSpecificProps.models;
|
|
694
805
|
referencedModelNames = _.union(referencedModelNames, modelNames);
|
|
695
806
|
}
|
|
696
807
|
return referencedModelNames;
|
|
697
808
|
}
|
|
698
809
|
|
|
699
|
-
function validateAndExtendContentModels(
|
|
810
|
+
function validateAndExtendContentModels(
|
|
811
|
+
config: Config
|
|
812
|
+
): {
|
|
813
|
+
config: Config;
|
|
814
|
+
errors: ConfigValidationError[];
|
|
815
|
+
} {
|
|
700
816
|
const contentModels = config.contentModels ?? {};
|
|
701
|
-
const models = config.models ??
|
|
817
|
+
const models = config.models ?? [];
|
|
702
818
|
|
|
819
|
+
// external models already merged in mergeConfigModelsWithExternalModels function
|
|
703
820
|
const externalModels = !isGitCMS(config);
|
|
704
821
|
const emptyContentModels = _.isEmpty(contentModels);
|
|
705
822
|
|
|
706
823
|
if (externalModels || emptyContentModels) {
|
|
707
824
|
return {
|
|
708
|
-
|
|
709
|
-
value: config,
|
|
825
|
+
config: config,
|
|
710
826
|
errors: []
|
|
711
827
|
};
|
|
712
828
|
}
|
|
@@ -715,36 +831,36 @@ function validateAndExtendContentModels(config: RawConfigWithPaths): ConfigValid
|
|
|
715
831
|
|
|
716
832
|
if (_.isEmpty(models)) {
|
|
717
833
|
return {
|
|
718
|
-
|
|
719
|
-
value: config,
|
|
834
|
+
config: config,
|
|
720
835
|
errors: validationResult.errors
|
|
721
836
|
};
|
|
722
837
|
}
|
|
723
838
|
|
|
724
|
-
const extendedModels
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
const contentModel = validationResult.value.contentModels![modelName];
|
|
839
|
+
const extendedModels = models.map(
|
|
840
|
+
(model): Model => {
|
|
841
|
+
const contentModel = validationResult.contentModels![model.name];
|
|
728
842
|
if (!contentModel) {
|
|
729
843
|
return model;
|
|
730
844
|
}
|
|
731
845
|
if (_.get(contentModel, '__metadata.invalid')) {
|
|
732
846
|
return model;
|
|
733
847
|
}
|
|
734
|
-
|
|
848
|
+
const { isPage, newFilePath, ...restContentModel } = contentModel;
|
|
849
|
+
const { type, ...restModel } = model;
|
|
850
|
+
if (isPage && (!type || ['object', 'page'].includes(type))) {
|
|
735
851
|
return {
|
|
736
852
|
type: 'page',
|
|
737
|
-
...(
|
|
738
|
-
...
|
|
739
|
-
...
|
|
740
|
-
}
|
|
741
|
-
} else if (!
|
|
853
|
+
...(newFilePath ? { filePath: newFilePath } : {}),
|
|
854
|
+
...restContentModel,
|
|
855
|
+
...restModel
|
|
856
|
+
};
|
|
857
|
+
} else if (!isPage && (!type || ['object', 'data'].includes(type))) {
|
|
742
858
|
return {
|
|
743
859
|
type: 'data',
|
|
744
|
-
...(
|
|
745
|
-
...
|
|
746
|
-
...
|
|
747
|
-
}
|
|
860
|
+
...(newFilePath ? { filePath: newFilePath } : {}),
|
|
861
|
+
...restContentModel,
|
|
862
|
+
...restModel
|
|
863
|
+
};
|
|
748
864
|
} else {
|
|
749
865
|
return model;
|
|
750
866
|
}
|
|
@@ -752,8 +868,7 @@ function validateAndExtendContentModels(config: RawConfigWithPaths): ConfigValid
|
|
|
752
868
|
);
|
|
753
869
|
|
|
754
870
|
return {
|
|
755
|
-
|
|
756
|
-
value: {
|
|
871
|
+
config: {
|
|
757
872
|
...config,
|
|
758
873
|
models: extendedModels
|
|
759
874
|
},
|
|
@@ -761,17 +876,16 @@ function validateAndExtendContentModels(config: RawConfigWithPaths): ConfigValid
|
|
|
761
876
|
};
|
|
762
877
|
}
|
|
763
878
|
|
|
764
|
-
function normalizeValidationResult(validationResult: ConfigValidationResult):
|
|
879
|
+
function normalizeValidationResult(validationResult: ConfigValidationResult): ConfigValidationResult {
|
|
765
880
|
validationResult = filterAndOrderConfigFields(validationResult);
|
|
766
|
-
|
|
767
|
-
return convertModelsToArray(validationResult);
|
|
881
|
+
return convertModelGroupsToModelListInPlace(validationResult);
|
|
768
882
|
}
|
|
769
883
|
|
|
770
884
|
function filterAndOrderConfigFields(validationResult: ConfigValidationResult): ConfigValidationResult {
|
|
771
885
|
// TODO: check if we can move filtering and sorting to Joi
|
|
772
886
|
return {
|
|
773
887
|
...validationResult,
|
|
774
|
-
|
|
888
|
+
config: _.pick(validationResult.config, [
|
|
775
889
|
'stackbitVersion',
|
|
776
890
|
'ssgName',
|
|
777
891
|
'ssgVersion',
|
|
@@ -814,7 +928,7 @@ function filterAndOrderConfigFields(validationResult: ConfigValidationResult): C
|
|
|
814
928
|
'encodedFieldTypes', // obsolete, left for backward compatibility
|
|
815
929
|
'noEncodeFields', // obsolete, left for backward compatibility
|
|
816
930
|
'omitFields' // obsolete, left for backward compatibility
|
|
817
|
-
]) as
|
|
931
|
+
]) as Config
|
|
818
932
|
};
|
|
819
933
|
}
|
|
820
934
|
|
|
@@ -822,18 +936,18 @@ function filterAndOrderConfigFields(validationResult: ConfigValidationResult): C
|
|
|
822
936
|
* Collects models groups and injects them into the `models` array of the
|
|
823
937
|
* `reference` and `model` field types
|
|
824
938
|
*/
|
|
825
|
-
function
|
|
826
|
-
const models = validationResult.
|
|
939
|
+
function convertModelGroupsToModelListInPlace(validationResult: ConfigValidationResult): ConfigValidationResult {
|
|
940
|
+
const models = validationResult.config?.models ?? [];
|
|
827
941
|
|
|
828
942
|
const groupMap = _.reduce(
|
|
829
943
|
models,
|
|
830
|
-
(groupMap, model
|
|
944
|
+
(groupMap, model) => {
|
|
831
945
|
if (!model.groups) {
|
|
832
946
|
return groupMap;
|
|
833
947
|
}
|
|
834
948
|
const key = model?.type === 'object' ? 'objectModels' : 'documentModels';
|
|
835
949
|
_.forEach(model.groups, (groupName) => {
|
|
836
|
-
append(groupMap, [groupName, key],
|
|
950
|
+
append(groupMap, [groupName, key], model.name);
|
|
837
951
|
});
|
|
838
952
|
delete model.groups;
|
|
839
953
|
return groupMap;
|
|
@@ -848,92 +962,41 @@ function convertModelGroupsToModelList(validationResult: ConfigValidationResult)
|
|
|
848
962
|
});
|
|
849
963
|
});
|
|
850
964
|
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
if (key) {
|
|
864
|
-
field.models = _.reduce(
|
|
865
|
-
field.groups,
|
|
965
|
+
const mappedModels = models.map((model) => {
|
|
966
|
+
return mapModelFieldsRecursively(
|
|
967
|
+
model,
|
|
968
|
+
(field): Field => {
|
|
969
|
+
return mapListItemsPropsOrSelfSpecificProps(field, (fieldSpecificProps) => {
|
|
970
|
+
if (!isModelField(fieldSpecificProps) && !isReferenceField(fieldSpecificProps)) {
|
|
971
|
+
return fieldSpecificProps;
|
|
972
|
+
}
|
|
973
|
+
const { ...cloned } = fieldSpecificProps;
|
|
974
|
+
const key = isModelField(fieldSpecificProps) ? 'objectModels' : 'documentModels';
|
|
975
|
+
const modelNames = _.reduce(
|
|
976
|
+
cloned.groups,
|
|
866
977
|
(modelNames, groupName) => {
|
|
867
978
|
const objectModelNames = _.get(groupMap, [groupName, key], []);
|
|
868
979
|
return _.uniq(modelNames.concat(objectModelNames));
|
|
869
980
|
},
|
|
870
|
-
|
|
981
|
+
fieldSpecificProps.models || []
|
|
871
982
|
);
|
|
872
|
-
|
|
873
|
-
|
|
983
|
+
delete cloned.groups;
|
|
984
|
+
return Object.assign(cloned, { models: modelNames });
|
|
985
|
+
});
|
|
874
986
|
}
|
|
875
|
-
|
|
876
|
-
});
|
|
877
|
-
}
|
|
878
|
-
|
|
879
|
-
function convertModelsToArray(validationResult: ConfigValidationResult): NormalizedValidationResult {
|
|
880
|
-
const config = validationResult.value;
|
|
881
|
-
const { stackbitVersion = LATEST_STACKBIT_VERSION, models, ...rest } = config;
|
|
882
|
-
|
|
883
|
-
// in stackbit.yaml 'models' are defined as object where keys are the model names,
|
|
884
|
-
// convert 'models' to array of objects and set their 'name' property to the
|
|
885
|
-
// model name
|
|
886
|
-
const modelMap = models ?? {};
|
|
887
|
-
const modelArray: Model[] = (_.map(
|
|
888
|
-
modelMap,
|
|
889
|
-
(yamlModel: YamlModel, modelName: string): Model => {
|
|
890
|
-
return {
|
|
891
|
-
name: modelName,
|
|
892
|
-
...yamlModel
|
|
893
|
-
};
|
|
894
|
-
}
|
|
895
|
-
) as unknown) as Model[];
|
|
896
|
-
|
|
897
|
-
if (!isGitCMS(config)) {
|
|
898
|
-
addImageModel(modelArray);
|
|
899
|
-
}
|
|
900
|
-
|
|
901
|
-
const convertedErrors = _.map(validationResult.errors, (error: ConfigValidationError) => {
|
|
902
|
-
if (error.fieldPath[0] === 'models' && typeof error.fieldPath[1] == 'string') {
|
|
903
|
-
const modelName = error.fieldPath[1];
|
|
904
|
-
const modelIndex = _.findIndex(modelArray, { name: modelName });
|
|
905
|
-
const normFieldPath = error.fieldPath.slice();
|
|
906
|
-
normFieldPath[1] = modelIndex;
|
|
907
|
-
error.normFieldPath = normFieldPath;
|
|
908
|
-
}
|
|
909
|
-
return error;
|
|
987
|
+
);
|
|
910
988
|
});
|
|
911
989
|
|
|
912
990
|
return {
|
|
913
|
-
|
|
991
|
+
...validationResult,
|
|
914
992
|
config: {
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
},
|
|
919
|
-
errors: convertedErrors
|
|
993
|
+
...validationResult.config,
|
|
994
|
+
models: mappedModels
|
|
995
|
+
}
|
|
920
996
|
};
|
|
921
997
|
}
|
|
922
998
|
|
|
923
|
-
function
|
|
924
|
-
models.push({
|
|
925
|
-
type: 'image',
|
|
926
|
-
name: '__image_model',
|
|
927
|
-
label: 'Image',
|
|
928
|
-
labelField: 'title',
|
|
929
|
-
fields: [
|
|
930
|
-
{ name: 'title', type: 'string' },
|
|
931
|
-
{ name: 'url', type: 'string' }
|
|
932
|
-
]
|
|
933
|
-
});
|
|
934
|
-
}
|
|
935
|
-
|
|
936
|
-
function isGitCMS(config: RawConfigWithPaths) {
|
|
999
|
+
function isGitCMS(config: Config) {
|
|
937
1000
|
return !config.contentSources && (!config.cmsName || config.cmsName === 'git');
|
|
938
1001
|
}
|
|
939
1002
|
|