@stackbit/sdk 0.2.39 → 0.3.0
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/analyzer/site-analyzer.d.ts.map +1 -1
- package/dist/analyzer/site-analyzer.js +3 -1
- package/dist/analyzer/site-analyzer.js.map +1 -1
- package/dist/config/config-errors.d.ts +3 -0
- package/dist/config/config-errors.d.ts.map +1 -1
- package/dist/config/config-errors.js +7 -2
- package/dist/config/config-errors.js.map +1 -1
- package/dist/config/config-loader-esbuild.d.ts +3 -14
- package/dist/config/config-loader-esbuild.d.ts.map +1 -1
- package/dist/config/config-loader-esbuild.js +19 -2
- package/dist/config/config-loader-esbuild.js.map +1 -1
- package/dist/config/config-loader-static.d.ts +14 -0
- package/dist/config/config-loader-static.d.ts.map +1 -0
- package/dist/config/config-loader-static.js +170 -0
- package/dist/config/config-loader-static.js.map +1 -0
- package/dist/config/config-loader-utils.d.ts +34 -0
- package/dist/config/config-loader-utils.d.ts.map +1 -0
- package/dist/config/config-loader-utils.js +211 -0
- package/dist/config/config-loader-utils.js.map +1 -0
- package/dist/config/config-loader.d.ts +13 -9
- package/dist/config/config-loader.d.ts.map +1 -1
- package/dist/config/config-loader.js +174 -198
- package/dist/config/config-loader.js.map +1 -1
- package/dist/config/config-schema/style-field-schema.d.ts.map +1 -1
- package/dist/config/config-schema/style-field-schema.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 +5 -6
- package/dist/config/config-schema.js.map +1 -1
- package/dist/config/config-types.d.ts +32 -2
- package/dist/config/config-types.d.ts.map +1 -1
- package/dist/config/config-validator.d.ts +5 -3
- package/dist/config/config-validator.d.ts.map +1 -1
- package/dist/config/config-validator.js.map +1 -1
- package/dist/config/config-writer.d.ts +1 -4
- package/dist/config/config-writer.d.ts.map +1 -1
- package/dist/config/config-writer.js +3 -27
- package/dist/config/config-writer.js.map +1 -1
- package/dist/content/content-schema.js +5 -6
- 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 +6 -2
- package/dist/index.js.map +1 -1
- package/dist/utils/index.d.ts +1 -1
- package/dist/utils/index.d.ts.map +1 -1
- package/dist/utils/model-extender.d.ts +1 -1
- package/dist/utils/model-extender.d.ts.map +1 -1
- package/dist/utils/model-extender.js.map +1 -1
- package/package.json +4 -3
- package/src/analyzer/site-analyzer.ts +3 -1
- package/src/config/config-errors.ts +7 -1
- package/src/config/config-loader-esbuild.ts +24 -19
- package/src/config/config-loader-static.ts +210 -0
- package/src/config/config-loader-utils.ts +253 -0
- package/src/config/config-loader.ts +209 -236
- package/src/config/config-schema/style-field-schema.ts +1 -4
- package/src/config/config-schema.ts +17 -18
- package/src/config/config-types.ts +31 -2
- package/src/config/config-validator.ts +5 -3
- package/src/config/config-writer.ts +2 -30
- package/src/content/content-schema.ts +17 -18
- package/src/index.ts +4 -2
- package/src/utils/index.ts +1 -1
- package/src/utils/model-extender.ts +2 -2
|
@@ -1,45 +1,51 @@
|
|
|
1
1
|
import path from 'path';
|
|
2
2
|
import fse from 'fs-extra';
|
|
3
|
-
import
|
|
3
|
+
import chokidar from 'chokidar';
|
|
4
4
|
import semver from 'semver';
|
|
5
5
|
import _ from 'lodash';
|
|
6
6
|
|
|
7
|
+
import { append, getFirstExistingFile, omitByNil, prepend, rename } from '@stackbit/utils';
|
|
8
|
+
|
|
7
9
|
import { ConfigValidationResult, validateConfig, validateContentModels } from './config-validator';
|
|
8
|
-
import {
|
|
9
|
-
|
|
10
|
-
ConfigLoadError,
|
|
11
|
-
ConfigPresetsError,
|
|
12
|
-
ConfigValidationError
|
|
13
|
-
} from './config-errors';
|
|
14
|
-
import { loadStackbitConfigFromJs, LoadStackbitConfigResult, StopConfigWatch } from './config-loader-esbuild';
|
|
10
|
+
import { ConfigError, ConfigLoadError, ConfigPresetsError, ConfigValidationError, STACKBIT_CONFIG_NOT_FOUND } from './config-errors';
|
|
11
|
+
import { loadStackbitConfigFromJs } from './config-loader-esbuild';
|
|
15
12
|
import {
|
|
16
13
|
assignLabelFieldIfNeeded,
|
|
17
14
|
extendModelMap,
|
|
18
15
|
getListFieldItems,
|
|
16
|
+
getModelFieldForModelKeyPath,
|
|
19
17
|
isCustomModelField,
|
|
20
|
-
|
|
18
|
+
isDataModel,
|
|
21
19
|
isListDataModel,
|
|
22
20
|
isListField,
|
|
23
21
|
isModelField,
|
|
24
22
|
isObjectField,
|
|
25
23
|
isObjectListItems,
|
|
26
24
|
isPageModel,
|
|
27
|
-
isDataModel,
|
|
28
25
|
isReferenceField,
|
|
29
26
|
iterateModelFieldsRecursively,
|
|
30
27
|
mapModelFieldsRecursively,
|
|
31
28
|
normalizeListFieldInPlace,
|
|
32
|
-
|
|
29
|
+
Logger
|
|
33
30
|
} from '../utils';
|
|
34
|
-
import {
|
|
35
|
-
import { Config, DataModel, FieldEnum, FieldModel, FieldObjectProps, Model, ModelsSource, PageModel, YamlModel } from './config-types';
|
|
31
|
+
import { Config, DataModel, FieldModel, Model, ModelMap, ModelsSource, PageModel, YamlModel } from './config-types';
|
|
36
32
|
import { loadPresets } from './presets-loader';
|
|
33
|
+
import {
|
|
34
|
+
LoadRawConfigResult,
|
|
35
|
+
StopConfigWatch,
|
|
36
|
+
RawConfigWithPaths,
|
|
37
|
+
LATEST_STACKBIT_VERSION,
|
|
38
|
+
STACKBIT_CONFIG_JS_FILES,
|
|
39
|
+
STACKBIT_CONFIG_YAML_FILES,
|
|
40
|
+
loadConfigWithModelsFromDir
|
|
41
|
+
} from './config-loader-utils';
|
|
37
42
|
|
|
38
43
|
export interface ConfigLoaderOptions {
|
|
39
44
|
dirPath: string;
|
|
40
45
|
modelsSource?: ModelsSource;
|
|
41
46
|
stackbitConfigESBuildOutDir?: string;
|
|
42
47
|
watchCallback?: (result: ConfigLoaderResult) => void;
|
|
48
|
+
logger?: Logger;
|
|
43
49
|
}
|
|
44
50
|
|
|
45
51
|
export interface ConfigLoaderResult {
|
|
@@ -48,6 +54,8 @@ export interface ConfigLoaderResult {
|
|
|
48
54
|
errors: ConfigError[];
|
|
49
55
|
}
|
|
50
56
|
|
|
57
|
+
export type ConfigLoaderResultWithStop = ConfigLoaderResult & StopConfigWatch;
|
|
58
|
+
|
|
51
59
|
export interface NormalizedValidationResult {
|
|
52
60
|
valid: boolean;
|
|
53
61
|
config: Config;
|
|
@@ -55,29 +63,39 @@ export interface NormalizedValidationResult {
|
|
|
55
63
|
}
|
|
56
64
|
|
|
57
65
|
export interface RawConfigLoaderResult {
|
|
58
|
-
config?:
|
|
66
|
+
config?: RawConfigWithPaths;
|
|
59
67
|
errors: ConfigLoadError[];
|
|
60
68
|
}
|
|
61
69
|
|
|
70
|
+
export type RawConfigLoaderResultWithStop = RawConfigLoaderResult & StopConfigWatch;
|
|
71
|
+
|
|
62
72
|
export async function loadConfig({
|
|
63
73
|
dirPath,
|
|
64
74
|
modelsSource,
|
|
65
75
|
stackbitConfigESBuildOutDir,
|
|
66
|
-
watchCallback
|
|
67
|
-
|
|
76
|
+
watchCallback,
|
|
77
|
+
logger
|
|
78
|
+
}: ConfigLoaderOptions): Promise<ConfigLoaderResultWithStop> {
|
|
79
|
+
let wrappedCallback: ((rawConfigResult: RawConfigLoaderResult) => Promise<void>) | undefined;
|
|
80
|
+
if (watchCallback) {
|
|
81
|
+
wrappedCallback = async (rawConfigResult: RawConfigLoaderResult) => {
|
|
82
|
+
const configLoaderResult = await processConfigLoaderResult({ rawConfigResult, dirPath, modelsSource });
|
|
83
|
+
watchCallback(configLoaderResult);
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
68
87
|
const rawConfigResult = await loadConfigFromDir({
|
|
69
88
|
dirPath,
|
|
70
89
|
stackbitConfigESBuildOutDir,
|
|
71
|
-
watchCallback:
|
|
72
|
-
|
|
73
|
-
const configLoaderResult = await processConfigLoaderResult({ rawConfigResult, dirPath, modelsSource });
|
|
74
|
-
watchCallback(configLoaderResult);
|
|
75
|
-
}
|
|
76
|
-
: undefined
|
|
90
|
+
watchCallback: wrappedCallback,
|
|
91
|
+
logger
|
|
77
92
|
});
|
|
78
93
|
|
|
79
94
|
const configLoaderResult = await processConfigLoaderResult({ rawConfigResult, dirPath, modelsSource });
|
|
80
|
-
return Object.assign(configLoaderResult, {
|
|
95
|
+
return Object.assign(configLoaderResult, {
|
|
96
|
+
stop: rawConfigResult.stop,
|
|
97
|
+
reload: rawConfigResult.reload
|
|
98
|
+
});
|
|
81
99
|
}
|
|
82
100
|
|
|
83
101
|
async function processConfigLoaderResult({
|
|
@@ -101,7 +119,7 @@ async function processConfigLoaderResult({
|
|
|
101
119
|
|
|
102
120
|
const { models: externalModels, errors: externalModelsLoadErrors } = await loadModelsFromExternalSource(config, dirPath, modelsSource);
|
|
103
121
|
|
|
104
|
-
const extendedConfig = await extendConfig({
|
|
122
|
+
const extendedConfig = await extendConfig({ config, externalModels });
|
|
105
123
|
|
|
106
124
|
return {
|
|
107
125
|
valid: extendedConfig.valid,
|
|
@@ -111,12 +129,10 @@ async function processConfigLoaderResult({
|
|
|
111
129
|
}
|
|
112
130
|
|
|
113
131
|
export async function extendConfig({
|
|
114
|
-
dirPath,
|
|
115
132
|
config,
|
|
116
133
|
externalModels
|
|
117
134
|
}: {
|
|
118
|
-
|
|
119
|
-
config: Record<string, any>;
|
|
135
|
+
config: RawConfigWithPaths;
|
|
120
136
|
externalModels?: Model[];
|
|
121
137
|
}): Promise<{
|
|
122
138
|
valid: boolean;
|
|
@@ -124,7 +140,7 @@ export async function extendConfig({
|
|
|
124
140
|
errors: (ConfigValidationError | ConfigPresetsError)[];
|
|
125
141
|
}> {
|
|
126
142
|
const normalizedResult = validateAndNormalizeConfig(config, externalModels);
|
|
127
|
-
const presetsResult = await loadPresets(dirPath, normalizedResult.config);
|
|
143
|
+
const presetsResult = await loadPresets(config.dirPath, normalizedResult.config);
|
|
128
144
|
return {
|
|
129
145
|
valid: normalizedResult.valid,
|
|
130
146
|
config: presetsResult.config,
|
|
@@ -132,12 +148,12 @@ export async function extendConfig({
|
|
|
132
148
|
};
|
|
133
149
|
}
|
|
134
150
|
|
|
135
|
-
export function validateAndNormalizeConfig(config:
|
|
151
|
+
export function validateAndNormalizeConfig(config: RawConfigWithPaths, externalModels?: Model[]): NormalizedValidationResult {
|
|
136
152
|
// extend config models having the "extends" property
|
|
137
153
|
// this must be done before any validation as some properties like
|
|
138
154
|
// the labelField will not work when validating models without extending them first
|
|
139
|
-
const { models: extendedModels, errors: extendModelErrors } = extendModelMap(config.models
|
|
140
|
-
const extendedConfig = {
|
|
155
|
+
const { models: extendedModels, errors: extendModelErrors } = extendModelMap(config.models);
|
|
156
|
+
const extendedConfig: RawConfigWithPaths = {
|
|
141
157
|
...config,
|
|
142
158
|
models: extendedModels
|
|
143
159
|
};
|
|
@@ -167,32 +183,80 @@ export function validateAndNormalizeConfig(config: Record<string, any>, external
|
|
|
167
183
|
export async function loadConfigFromDir({
|
|
168
184
|
dirPath,
|
|
169
185
|
stackbitConfigESBuildOutDir,
|
|
170
|
-
watchCallback
|
|
186
|
+
watchCallback,
|
|
187
|
+
logger
|
|
171
188
|
}: {
|
|
172
189
|
dirPath: string;
|
|
173
190
|
stackbitConfigESBuildOutDir?: string;
|
|
174
191
|
watchCallback?: (rawConfigResult: RawConfigLoaderResult) => void;
|
|
175
|
-
|
|
192
|
+
logger?: Logger
|
|
193
|
+
}): Promise<RawConfigLoaderResultWithStop> {
|
|
176
194
|
// try to load stackbit config from YAML files
|
|
177
195
|
try {
|
|
178
|
-
const stackbitYamlPath =
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
196
|
+
const stackbitYamlPath = await getFirstExistingFile(STACKBIT_CONFIG_YAML_FILES, dirPath);
|
|
197
|
+
if (stackbitYamlPath) {
|
|
198
|
+
const { config, errors } = await loadConfigWithModelsFromDir(dirPath);
|
|
199
|
+
let close: () => Promise<void> = async () => void 0;
|
|
200
|
+
let reload: () => void = () => void 0;
|
|
201
|
+
let stopped = false;
|
|
202
|
+
|
|
203
|
+
if (watchCallback) {
|
|
204
|
+
const watcher = chokidar.watch([...STACKBIT_CONFIG_YAML_FILES, '.stackbit'], {
|
|
205
|
+
cwd: dirPath,
|
|
206
|
+
persistent: true,
|
|
207
|
+
ignoreInitial: true
|
|
208
|
+
});
|
|
209
|
+
const throttledFileChange = _.throttle(async () => {
|
|
210
|
+
const { config, errors } = await loadConfigWithModelsFromDir(dirPath);
|
|
211
|
+
watchCallback({
|
|
212
|
+
config: config
|
|
213
|
+
? {
|
|
214
|
+
...config,
|
|
215
|
+
dirPath: dirPath,
|
|
216
|
+
filePath: stackbitYamlPath
|
|
217
|
+
}
|
|
218
|
+
: undefined,
|
|
219
|
+
errors
|
|
220
|
+
});
|
|
221
|
+
}, 1000);
|
|
222
|
+
const handleFileChange = (path: string) => {
|
|
223
|
+
logger?.debug(`identified change in stackbit config file: ${path}, reloading config...`);
|
|
224
|
+
throttledFileChange();
|
|
225
|
+
};
|
|
226
|
+
watcher.on('add', handleFileChange);
|
|
227
|
+
watcher.on('change', handleFileChange);
|
|
228
|
+
watcher.on('unlink', handleFileChange);
|
|
229
|
+
watcher.on('addDir', handleFileChange);
|
|
230
|
+
watcher.on('unlinkDir', handleFileChange);
|
|
231
|
+
watcher.on('error', (error) => {
|
|
232
|
+
watchCallback({
|
|
233
|
+
errors: [new ConfigLoadError(`Error loading Stackbit configuration: ${error.message}`, { originalError: error })]
|
|
234
|
+
});
|
|
235
|
+
});
|
|
236
|
+
close = async () => {
|
|
237
|
+
if (stopped) {
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
stopped = true;
|
|
241
|
+
throttledFileChange.cancel();
|
|
242
|
+
watcher.close();
|
|
243
|
+
};
|
|
244
|
+
reload = () => {
|
|
245
|
+
throttledFileChange();
|
|
246
|
+
};
|
|
184
247
|
}
|
|
185
248
|
|
|
186
|
-
const { models: modelsFromFiles, errors: fileModelsErrors } = await loadModelsFromFiles(dirPath, stackbitYamlResult.config);
|
|
187
|
-
|
|
188
|
-
const mergedModels = mergeConfigModelsWithModelsFromFiles(stackbitYamlResult.config.models ?? {}, modelsFromFiles);
|
|
189
|
-
|
|
190
249
|
return {
|
|
191
|
-
config:
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
250
|
+
config: config
|
|
251
|
+
? {
|
|
252
|
+
...config,
|
|
253
|
+
dirPath: dirPath,
|
|
254
|
+
filePath: stackbitYamlPath
|
|
255
|
+
}
|
|
256
|
+
: undefined,
|
|
257
|
+
stop: close,
|
|
258
|
+
reload: reload,
|
|
259
|
+
errors: errors
|
|
196
260
|
};
|
|
197
261
|
}
|
|
198
262
|
} catch (error: any) {
|
|
@@ -201,14 +265,18 @@ export async function loadConfigFromDir({
|
|
|
201
265
|
};
|
|
202
266
|
}
|
|
203
267
|
|
|
204
|
-
function wrapResult(result:
|
|
268
|
+
function wrapResult(result: LoadRawConfigResult, configFilePath: string): RawConfigLoaderResult {
|
|
205
269
|
if (result.error) {
|
|
206
270
|
return {
|
|
207
271
|
errors: [result.error]
|
|
208
272
|
};
|
|
209
273
|
} else {
|
|
210
274
|
return {
|
|
211
|
-
config:
|
|
275
|
+
config: {
|
|
276
|
+
...result.config,
|
|
277
|
+
dirPath: dirPath,
|
|
278
|
+
filePath: configFilePath
|
|
279
|
+
},
|
|
212
280
|
errors: []
|
|
213
281
|
};
|
|
214
282
|
}
|
|
@@ -216,19 +284,20 @@ export async function loadConfigFromDir({
|
|
|
216
284
|
|
|
217
285
|
// try to load stackbit config from JavaScript files
|
|
218
286
|
try {
|
|
219
|
-
const configFilePath = await getFirstExistingFile(
|
|
287
|
+
const configFilePath = await getFirstExistingFile(STACKBIT_CONFIG_JS_FILES, dirPath);
|
|
220
288
|
if (configFilePath) {
|
|
221
289
|
const configResult = await loadStackbitConfigFromJs({
|
|
222
290
|
configPath: configFilePath,
|
|
223
|
-
outDir: stackbitConfigESBuildOutDir,
|
|
291
|
+
outDir: stackbitConfigESBuildOutDir ?? '.stackbit/cache',
|
|
224
292
|
watch: !!watchCallback,
|
|
293
|
+
logger: logger,
|
|
225
294
|
callback: watchCallback
|
|
226
|
-
? (result:
|
|
227
|
-
watchCallback(wrapResult(result));
|
|
295
|
+
? (result: LoadRawConfigResult) => {
|
|
296
|
+
watchCallback(wrapResult(result, configFilePath));
|
|
228
297
|
}
|
|
229
298
|
: undefined
|
|
230
299
|
});
|
|
231
|
-
return Object.assign(wrapResult(configResult), { stop: configResult.stop });
|
|
300
|
+
return Object.assign(wrapResult(configResult, configFilePath), { stop: configResult.stop });
|
|
232
301
|
}
|
|
233
302
|
} catch (error: any) {
|
|
234
303
|
return {
|
|
@@ -237,100 +306,19 @@ export async function loadConfigFromDir({
|
|
|
237
306
|
}
|
|
238
307
|
|
|
239
308
|
return {
|
|
240
|
-
errors: [
|
|
241
|
-
new ConfigLoadError(
|
|
242
|
-
'stackbit.yaml or stackbit.config.js was not found, please refer Stackbit documentation: https://www.stackbit.com/docs/stackbit-yaml/'
|
|
243
|
-
)
|
|
244
|
-
]
|
|
309
|
+
errors: [new ConfigLoadError(STACKBIT_CONFIG_NOT_FOUND)]
|
|
245
310
|
};
|
|
246
311
|
}
|
|
247
312
|
|
|
248
|
-
type StackbitYamlConfigResult = { config: Record<string, any>; error?: never } | { config?: never; error: ConfigLoadError };
|
|
249
|
-
|
|
250
|
-
async function loadConfigFromStackbitYaml(stackbitYamlPath: string): Promise<StackbitYamlConfigResult> {
|
|
251
|
-
const stackbitYaml = await fse.readFile(stackbitYamlPath);
|
|
252
|
-
const config = yaml.load(stackbitYaml.toString('utf8'), { schema: yaml.JSON_SCHEMA });
|
|
253
|
-
if (!config || typeof config !== 'object') {
|
|
254
|
-
return {
|
|
255
|
-
error: new ConfigLoadError('error parsing stackbit.yaml, please refer Stackbit documentation: https://www.stackbit.com/docs/stackbit-yaml/')
|
|
256
|
-
};
|
|
257
|
-
}
|
|
258
|
-
return { config };
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
async function loadModelsFromFiles(dirPath: string, config: Record<string, any>): Promise<{ models: Record<string, any>; errors: ConfigLoadError[] }> {
|
|
262
|
-
const modelsSource = _.get(config, 'modelsSource', {});
|
|
263
|
-
const sourceType = _.get(modelsSource, 'type', 'files');
|
|
264
|
-
const defaultModelDirs = ['node_modules/@stackbit/components/models', '.stackbit/models'];
|
|
265
|
-
const modelDirs =
|
|
266
|
-
sourceType === 'files'
|
|
267
|
-
? _.castArray(_.get(modelsSource, 'modelDirs', defaultModelDirs)).map((modelDir: string) => _.trim(modelDir, '/'))
|
|
268
|
-
: defaultModelDirs;
|
|
269
|
-
|
|
270
|
-
const modelFiles = await reducePromise(
|
|
271
|
-
modelDirs,
|
|
272
|
-
async (modelFiles: string[], modelDir) => {
|
|
273
|
-
const absModelsDir = path.join(dirPath, modelDir);
|
|
274
|
-
const dirExists = await fse.pathExists(absModelsDir);
|
|
275
|
-
if (!dirExists) {
|
|
276
|
-
return modelFiles;
|
|
277
|
-
}
|
|
278
|
-
const files = await readModelFilesFromDir(absModelsDir);
|
|
279
|
-
return modelFiles.concat(files.map((filePath) => path.join(modelDir, filePath)));
|
|
280
|
-
},
|
|
281
|
-
[]
|
|
282
|
-
);
|
|
283
|
-
|
|
284
|
-
return reducePromise(
|
|
285
|
-
modelFiles,
|
|
286
|
-
async (result: { models: any; errors: ConfigLoadError[] }, modelFile) => {
|
|
287
|
-
let model;
|
|
288
|
-
try {
|
|
289
|
-
model = await parseFile(path.join(dirPath, modelFile));
|
|
290
|
-
} catch (error) {
|
|
291
|
-
return {
|
|
292
|
-
models: result.models,
|
|
293
|
-
errors: result.errors.concat(new ConfigLoadError(`error parsing model, file: ${modelFile}`))
|
|
294
|
-
};
|
|
295
|
-
}
|
|
296
|
-
const modelName = model?.name;
|
|
297
|
-
if (!modelName) {
|
|
298
|
-
return {
|
|
299
|
-
models: result.models,
|
|
300
|
-
errors: result.errors.concat(new ConfigLoadError(`model does not have a name, file: ${modelFile}`))
|
|
301
|
-
};
|
|
302
|
-
}
|
|
303
|
-
result.models[modelName] = _.omit(model, 'name');
|
|
304
|
-
result.models[modelName].__metadata = {
|
|
305
|
-
filePath: modelFile
|
|
306
|
-
};
|
|
307
|
-
return result;
|
|
308
|
-
},
|
|
309
|
-
{ models: {}, errors: [] }
|
|
310
|
-
);
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
async function readModelFilesFromDir(modelsDir: string) {
|
|
314
|
-
return await readDirRecursively(modelsDir, {
|
|
315
|
-
filter: (filePath, stats) => {
|
|
316
|
-
if (stats.isDirectory()) {
|
|
317
|
-
return true;
|
|
318
|
-
}
|
|
319
|
-
const extension = path.extname(filePath).substring(1);
|
|
320
|
-
return stats.isFile() && ['yaml', 'yml'].includes(extension);
|
|
321
|
-
}
|
|
322
|
-
});
|
|
323
|
-
}
|
|
324
|
-
|
|
325
313
|
async function loadModelsFromExternalSource(
|
|
326
|
-
config:
|
|
314
|
+
config: RawConfigWithPaths,
|
|
327
315
|
dirPath: string,
|
|
328
316
|
modelsSource?: ModelsSource
|
|
329
317
|
): Promise<{ models: Model[]; errors: ConfigLoadError[] }> {
|
|
330
|
-
modelsSource = _.assign({}, modelsSource, config.
|
|
318
|
+
modelsSource = _.assign({}, modelsSource, config.modelsSource);
|
|
331
319
|
const sourceType = _.get(modelsSource, 'type', 'files');
|
|
332
320
|
if (sourceType === 'files') {
|
|
333
|
-
// we already loaded models from files inside
|
|
321
|
+
// we already loaded models from files inside loadConfigFromDir function
|
|
334
322
|
return { models: [], errors: [] };
|
|
335
323
|
} else if (sourceType === 'contentful') {
|
|
336
324
|
const contentfulModule = _.get(modelsSource, 'module', '@stackbit/cms-contentful');
|
|
@@ -389,36 +377,7 @@ async function loadConfigFromDotStackbit(dirPath: string) {
|
|
|
389
377
|
return _.isEmpty(config) ? null : config;
|
|
390
378
|
}
|
|
391
379
|
|
|
392
|
-
function
|
|
393
|
-
const mergedModels = _.mapValues(modelsFromFiles, (modelFromFile, modelName) => {
|
|
394
|
-
// resolve thumbnails of models loaded from files
|
|
395
|
-
const modelFilePath = modelFromFile.__metadata?.filePath;
|
|
396
|
-
resolveThumbnailPathForModel(modelFromFile, modelFilePath);
|
|
397
|
-
iterateModelFieldsRecursively(modelFromFile, (field: any) => {
|
|
398
|
-
if (isListField(field)) {
|
|
399
|
-
field = normalizeListFieldInPlace(field);
|
|
400
|
-
field = field.items;
|
|
401
|
-
}
|
|
402
|
-
if (isObjectField(field)) {
|
|
403
|
-
resolveThumbnailPathForModel(field, modelFilePath);
|
|
404
|
-
} else if (isEnumField(field)) {
|
|
405
|
-
resolveThumbnailPathForEnumField(field, modelFilePath);
|
|
406
|
-
}
|
|
407
|
-
});
|
|
408
|
-
|
|
409
|
-
const configModel = _.get(configModels, modelName);
|
|
410
|
-
if (!configModel) {
|
|
411
|
-
return modelFromFile;
|
|
412
|
-
}
|
|
413
|
-
|
|
414
|
-
return _.assign({}, modelFromFile, configModel, {
|
|
415
|
-
fields: _.unionBy(configModel?.fields ?? [], modelFromFile?.fields ?? [], 'name')
|
|
416
|
-
});
|
|
417
|
-
});
|
|
418
|
-
return Object.assign({}, configModels, mergedModels);
|
|
419
|
-
}
|
|
420
|
-
|
|
421
|
-
function mergeConfigWithExternalModels(config: any, externalModels?: Model[]): { config: any; errors: ConfigValidationError[] } {
|
|
380
|
+
function mergeConfigWithExternalModels(config: RawConfigWithPaths, externalModels?: Model[]): { config: RawConfigWithPaths; errors: ConfigValidationError[] } {
|
|
422
381
|
if (!externalModels || externalModels.length === 0) {
|
|
423
382
|
return {
|
|
424
383
|
config,
|
|
@@ -502,9 +461,14 @@ function normalizeConfig(config: any): any {
|
|
|
502
461
|
const stackbitYamlVersion = String(_.get(config, 'stackbitVersion', ''));
|
|
503
462
|
const ver = semver.coerce(stackbitYamlVersion);
|
|
504
463
|
const isStackbitYamlV2 = ver ? semver.satisfies(ver, '<0.3.0') : false;
|
|
464
|
+
const isStackbitYamlV5 = ver ? semver.satisfies(ver, '>=0.5.0') : false;
|
|
505
465
|
const models = config?.models || {};
|
|
506
466
|
const gitCMS = isGitCMS(config);
|
|
507
467
|
|
|
468
|
+
rename(config, 'logicFields', 'noEncodeFields');
|
|
469
|
+
config.hcrHandled = !stackbitYamlVersion || _.get(config, 'customContentReload', _.get(config, 'hcrHandled', !isStackbitYamlV5));
|
|
470
|
+
config.internalStackbitRunnerOptions = getInternalStackbitRunnerOptions(config);
|
|
471
|
+
|
|
508
472
|
_.forEach(models, (model, modelName) => {
|
|
509
473
|
if (!model) {
|
|
510
474
|
return;
|
|
@@ -706,39 +670,6 @@ function addObjectTypeKeyField(model: any, objectTypeKey: string, modelName: str
|
|
|
706
670
|
});
|
|
707
671
|
}
|
|
708
672
|
|
|
709
|
-
function resolveThumbnailPathForModel(modelOrField: Model | FieldObjectProps, modelFilePath: string | undefined) {
|
|
710
|
-
if (modelOrField.thumbnail && modelFilePath) {
|
|
711
|
-
const modelDirPath = path.dirname(modelFilePath);
|
|
712
|
-
modelOrField.thumbnail = resolveThumbnailPath(modelOrField.thumbnail, modelDirPath);
|
|
713
|
-
}
|
|
714
|
-
}
|
|
715
|
-
|
|
716
|
-
function resolveThumbnailPathForEnumField(enumField: FieldEnum, modelFilePath: string | undefined) {
|
|
717
|
-
if (enumField.controlType === 'thumbnails' && modelFilePath) {
|
|
718
|
-
const modelDirPath = path.dirname(modelFilePath);
|
|
719
|
-
_.forEach(enumField.options, (option) => {
|
|
720
|
-
if (option.thumbnail) {
|
|
721
|
-
option.thumbnail = resolveThumbnailPath(option.thumbnail, modelDirPath);
|
|
722
|
-
}
|
|
723
|
-
});
|
|
724
|
-
}
|
|
725
|
-
}
|
|
726
|
-
|
|
727
|
-
function resolveThumbnailPath(thumbnail: string, modelDirPath: string) {
|
|
728
|
-
if (thumbnail.startsWith('//') || /https?:\/\//.test(thumbnail)) {
|
|
729
|
-
return thumbnail;
|
|
730
|
-
}
|
|
731
|
-
if (thumbnail.startsWith('/')) {
|
|
732
|
-
if (modelDirPath.endsWith('@stackbit/components/models')) {
|
|
733
|
-
modelDirPath = modelDirPath.replace(/\/models$/, '');
|
|
734
|
-
} else {
|
|
735
|
-
modelDirPath = '';
|
|
736
|
-
}
|
|
737
|
-
thumbnail = thumbnail.replace(/^\//, '');
|
|
738
|
-
}
|
|
739
|
-
return path.join(modelDirPath, thumbnail);
|
|
740
|
-
}
|
|
741
|
-
|
|
742
673
|
/**
|
|
743
674
|
* Returns model names referenced by polymorphic 'model' and 'reference' fields.
|
|
744
675
|
* That is, fields that can hold objects of different types.
|
|
@@ -764,7 +695,7 @@ function getReferencedModelNames(field: any) {
|
|
|
764
695
|
return referencedModelNames;
|
|
765
696
|
}
|
|
766
697
|
|
|
767
|
-
function validateAndExtendContentModels(config:
|
|
698
|
+
function validateAndExtendContentModels(config: RawConfigWithPaths): ConfigValidationResult {
|
|
768
699
|
const contentModels = config.contentModels ?? {};
|
|
769
700
|
const models = config.models ?? {};
|
|
770
701
|
|
|
@@ -789,32 +720,35 @@ function validateAndExtendContentModels(config: any): ConfigValidationResult {
|
|
|
789
720
|
};
|
|
790
721
|
}
|
|
791
722
|
|
|
792
|
-
const extendedModels = _.mapValues(
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
723
|
+
const extendedModels: ModelMap = _.mapValues(
|
|
724
|
+
models,
|
|
725
|
+
(model, modelName): YamlModel => {
|
|
726
|
+
const contentModel = validationResult.value.contentModels![modelName];
|
|
727
|
+
if (!contentModel) {
|
|
728
|
+
return model;
|
|
729
|
+
}
|
|
730
|
+
if (_.get(contentModel, '__metadata.invalid')) {
|
|
731
|
+
return model;
|
|
732
|
+
}
|
|
733
|
+
if (contentModel.isPage && (!model.type || ['object', 'page'].includes(model.type))) {
|
|
734
|
+
return {
|
|
735
|
+
type: 'page',
|
|
736
|
+
...(contentModel.newFilePath ? { filePath: contentModel.newFilePath } : {}),
|
|
737
|
+
..._.omit(contentModel, ['isPage', 'newFilePath']),
|
|
738
|
+
..._.omit(model, 'type')
|
|
739
|
+
} as YamlModel;
|
|
740
|
+
} else if (!contentModel.isPage && (!model.type || ['object', 'data'].includes(model.type))) {
|
|
741
|
+
return {
|
|
742
|
+
type: 'data',
|
|
743
|
+
...(contentModel.newFilePath ? { filePath: contentModel.newFilePath } : {}),
|
|
744
|
+
..._.omit(contentModel, ['isPage', 'newFilePath']),
|
|
745
|
+
..._.omit(model, 'type')
|
|
746
|
+
} as YamlModel;
|
|
747
|
+
} else {
|
|
748
|
+
return model;
|
|
749
|
+
}
|
|
816
750
|
}
|
|
817
|
-
|
|
751
|
+
);
|
|
818
752
|
|
|
819
753
|
return {
|
|
820
754
|
valid: validationResult.valid,
|
|
@@ -833,7 +767,7 @@ function normalizeValidationResult(validationResult: ConfigValidationResult): No
|
|
|
833
767
|
}
|
|
834
768
|
|
|
835
769
|
function filterAndOrderConfigFields(validationResult: ConfigValidationResult): ConfigValidationResult {
|
|
836
|
-
// TODO:
|
|
770
|
+
// TODO: check if we can move filtering and sorting to Joi
|
|
837
771
|
return {
|
|
838
772
|
...validationResult,
|
|
839
773
|
value: _.pick(validationResult.value, [
|
|
@@ -845,6 +779,9 @@ function filterAndOrderConfigFields(validationResult: ConfigValidationResult): C
|
|
|
845
779
|
'buildCommand',
|
|
846
780
|
'publishDir',
|
|
847
781
|
'nodeVersion',
|
|
782
|
+
'postGitCloneCommand',
|
|
783
|
+
'preInstallCommand',
|
|
784
|
+
'postInstallCommand',
|
|
848
785
|
'devCommand',
|
|
849
786
|
'staticDir',
|
|
850
787
|
'uploadDir',
|
|
@@ -859,12 +796,30 @@ function filterAndOrderConfigFields(validationResult: ConfigValidationResult): C
|
|
|
859
796
|
'contentModels',
|
|
860
797
|
'presetSource',
|
|
861
798
|
'modelsSource',
|
|
799
|
+
'mapModels',
|
|
800
|
+
'presetReferenceBehavior',
|
|
801
|
+
'nonDuplicatableModels',
|
|
802
|
+
'duplicatableModels',
|
|
803
|
+
'contentSources',
|
|
804
|
+
'hcrHandled',
|
|
805
|
+
'internalStackbitRunnerOptions',
|
|
806
|
+
'dirPath',
|
|
807
|
+
'filePath',
|
|
862
808
|
'models',
|
|
863
|
-
'presets'
|
|
864
|
-
|
|
809
|
+
'presets',
|
|
810
|
+
'pageData', // obsolete, left for backward compatibility
|
|
811
|
+
'pageModels', // obsolete, left for backward compatibility
|
|
812
|
+
'encodedFieldTypes', // obsolete, left for backward compatibility
|
|
813
|
+
'noEncodeFields', // obsolete, left for backward compatibility
|
|
814
|
+
'omitFields' // obsolete, left for backward compatibility
|
|
815
|
+
]) as RawConfigWithPaths
|
|
865
816
|
};
|
|
866
817
|
}
|
|
867
818
|
|
|
819
|
+
/**
|
|
820
|
+
* Collects models groups and injects them into the `models` array of the
|
|
821
|
+
* `reference` and `model` field types
|
|
822
|
+
*/
|
|
868
823
|
function convertModelGroupsToModelList(validationResult: ConfigValidationResult) {
|
|
869
824
|
const models = validationResult.value?.models ?? {};
|
|
870
825
|
|
|
@@ -892,7 +847,7 @@ function convertModelGroupsToModelList(validationResult: ConfigValidationResult)
|
|
|
892
847
|
});
|
|
893
848
|
|
|
894
849
|
_.forEach(models, (model) => {
|
|
895
|
-
iterateModelFieldsRecursively(model, (field: any) => {
|
|
850
|
+
iterateModelFieldsRecursively(model as Model, (field: any) => {
|
|
896
851
|
if (isListField(field)) {
|
|
897
852
|
field = field.items;
|
|
898
853
|
}
|
|
@@ -921,11 +876,12 @@ function convertModelGroupsToModelList(validationResult: ConfigValidationResult)
|
|
|
921
876
|
|
|
922
877
|
function convertModelsToArray(validationResult: ConfigValidationResult): NormalizedValidationResult {
|
|
923
878
|
const config = validationResult.value;
|
|
879
|
+
const { stackbitVersion = LATEST_STACKBIT_VERSION, models, ...rest } = config;
|
|
924
880
|
|
|
925
881
|
// in stackbit.yaml 'models' are defined as object where keys are the model names,
|
|
926
882
|
// convert 'models' to array of objects and set their 'name' property to the
|
|
927
883
|
// model name
|
|
928
|
-
const modelMap =
|
|
884
|
+
const modelMap = models ?? {};
|
|
929
885
|
const modelArray: Model[] = _.map(
|
|
930
886
|
modelMap,
|
|
931
887
|
(yamlModel: YamlModel, modelName: string): Model => {
|
|
@@ -954,7 +910,8 @@ function convertModelsToArray(validationResult: ConfigValidationResult): Normali
|
|
|
954
910
|
return {
|
|
955
911
|
valid: validationResult.valid,
|
|
956
912
|
config: {
|
|
957
|
-
|
|
913
|
+
stackbitVersion,
|
|
914
|
+
...rest,
|
|
958
915
|
models: modelArray
|
|
959
916
|
},
|
|
960
917
|
errors: convertedErrors
|
|
@@ -974,6 +931,22 @@ function addImageModel(models: Model[]) {
|
|
|
974
931
|
});
|
|
975
932
|
}
|
|
976
933
|
|
|
977
|
-
function isGitCMS(config:
|
|
934
|
+
function isGitCMS(config: RawConfigWithPaths) {
|
|
978
935
|
return !config.cmsName || config.cmsName === 'git';
|
|
979
936
|
}
|
|
937
|
+
|
|
938
|
+
function getInternalStackbitRunnerOptions(config: any) {
|
|
939
|
+
const experimentalSsgData = _.get(config, 'experimental.ssg');
|
|
940
|
+
if (!experimentalSsgData) {
|
|
941
|
+
return _.get(config, '__unsafe_internal_stackbitRunnerOptions');
|
|
942
|
+
}
|
|
943
|
+
const doneStart = _.get(experimentalSsgData, 'logPatterns.up');
|
|
944
|
+
return {
|
|
945
|
+
displayName: experimentalSsgData.name || 'ssg',
|
|
946
|
+
triggerInstallFiles: _.get(experimentalSsgData, 'watch.reinstallPackages', ['package.json', 'package-lock.json']),
|
|
947
|
+
directPaths: experimentalSsgData.passthrough || [],
|
|
948
|
+
patterns: {
|
|
949
|
+
doneStart: _.isEmpty(doneStart) || _.isArray(doneStart) ? doneStart : [doneStart]
|
|
950
|
+
}
|
|
951
|
+
};
|
|
952
|
+
}
|