@stackbit/sdk 0.2.39 → 0.3.1

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.
Files changed (65) hide show
  1. package/dist/analyzer/site-analyzer.d.ts.map +1 -1
  2. package/dist/analyzer/site-analyzer.js +3 -1
  3. package/dist/analyzer/site-analyzer.js.map +1 -1
  4. package/dist/config/config-errors.d.ts +3 -0
  5. package/dist/config/config-errors.d.ts.map +1 -1
  6. package/dist/config/config-errors.js +7 -2
  7. package/dist/config/config-errors.js.map +1 -1
  8. package/dist/config/config-loader-esbuild.d.ts +3 -14
  9. package/dist/config/config-loader-esbuild.d.ts.map +1 -1
  10. package/dist/config/config-loader-esbuild.js +19 -2
  11. package/dist/config/config-loader-esbuild.js.map +1 -1
  12. package/dist/config/config-loader-static.d.ts +14 -0
  13. package/dist/config/config-loader-static.d.ts.map +1 -0
  14. package/dist/config/config-loader-static.js +170 -0
  15. package/dist/config/config-loader-static.js.map +1 -0
  16. package/dist/config/config-loader-utils.d.ts +34 -0
  17. package/dist/config/config-loader-utils.d.ts.map +1 -0
  18. package/dist/config/config-loader-utils.js +211 -0
  19. package/dist/config/config-loader-utils.js.map +1 -0
  20. package/dist/config/config-loader.d.ts +13 -9
  21. package/dist/config/config-loader.d.ts.map +1 -1
  22. package/dist/config/config-loader.js +175 -199
  23. package/dist/config/config-loader.js.map +1 -1
  24. package/dist/config/config-schema/style-field-schema.d.ts.map +1 -1
  25. package/dist/config/config-schema/style-field-schema.js.map +1 -1
  26. package/dist/config/config-schema.d.ts +2 -2
  27. package/dist/config/config-schema.d.ts.map +1 -1
  28. package/dist/config/config-schema.js +5 -6
  29. package/dist/config/config-schema.js.map +1 -1
  30. package/dist/config/config-types.d.ts +32 -2
  31. package/dist/config/config-types.d.ts.map +1 -1
  32. package/dist/config/config-validator.d.ts +5 -3
  33. package/dist/config/config-validator.d.ts.map +1 -1
  34. package/dist/config/config-validator.js.map +1 -1
  35. package/dist/config/config-writer.d.ts +1 -4
  36. package/dist/config/config-writer.d.ts.map +1 -1
  37. package/dist/config/config-writer.js +3 -27
  38. package/dist/config/config-writer.js.map +1 -1
  39. package/dist/content/content-schema.js +5 -6
  40. package/dist/content/content-schema.js.map +1 -1
  41. package/dist/index.d.ts +4 -2
  42. package/dist/index.d.ts.map +1 -1
  43. package/dist/index.js +6 -2
  44. package/dist/index.js.map +1 -1
  45. package/dist/utils/index.d.ts +1 -1
  46. package/dist/utils/index.d.ts.map +1 -1
  47. package/dist/utils/model-extender.d.ts +1 -1
  48. package/dist/utils/model-extender.d.ts.map +1 -1
  49. package/dist/utils/model-extender.js.map +1 -1
  50. package/package.json +4 -3
  51. package/src/analyzer/site-analyzer.ts +3 -1
  52. package/src/config/config-errors.ts +7 -1
  53. package/src/config/config-loader-esbuild.ts +24 -19
  54. package/src/config/config-loader-static.ts +210 -0
  55. package/src/config/config-loader-utils.ts +253 -0
  56. package/src/config/config-loader.ts +210 -237
  57. package/src/config/config-schema/style-field-schema.ts +1 -4
  58. package/src/config/config-schema.ts +17 -18
  59. package/src/config/config-types.ts +31 -2
  60. package/src/config/config-validator.ts +5 -3
  61. package/src/config/config-writer.ts +2 -30
  62. package/src/content/content-schema.ts +17 -18
  63. package/src/index.ts +4 -2
  64. package/src/utils/index.ts +1 -1
  65. package/src/utils/model-extender.ts +2 -2
@@ -0,0 +1,253 @@
1
+ import path from 'path';
2
+ import fse from 'fs-extra';
3
+ import yaml from 'js-yaml';
4
+ import _ from 'lodash';
5
+
6
+ import { parseFile, readDirRecursively, reducePromise, getFirstExistingFile } from '@stackbit/utils';
7
+
8
+ import { ConfigLoadError, REFER_TO_STACKBIT_CONFIG_DOCS, STACKBIT_CONFIG_NOT_FOUND } from './config-errors';
9
+ import { Config, FieldEnum, FieldObjectProps, Model, ModelMap, RawConfig, YamlModel } from './config-types';
10
+ import { isEnumField, isListField, isObjectField, iterateModelFieldsRecursively, normalizeListFieldInPlace } from '../utils';
11
+
12
+ export const STACKBIT_CONFIG_YAML_FILES = ['stackbit.yaml', 'stackbit.yml'];
13
+ export const STACKBIT_CONFIG_JS_FILES = ['stackbit.config.js', 'stackbit.config.cjs', 'stackbit.config.mjs', 'stackbit.config.ts'];
14
+ export const STACKBIT_CONFIG_FILES = [...STACKBIT_CONFIG_YAML_FILES, ...STACKBIT_CONFIG_JS_FILES];
15
+ export const LATEST_STACKBIT_VERSION = '0.5.0';
16
+
17
+ export type RawConfigWithPaths = RawConfig & {
18
+ dirPath: string;
19
+ filePath: string;
20
+ };
21
+
22
+ export type LoadRawConfigResult =
23
+ | {
24
+ config: RawConfig;
25
+ error: null;
26
+ }
27
+ | {
28
+ config: null;
29
+ error: ConfigLoadError;
30
+ };
31
+
32
+ export type StopConfigWatch = {
33
+ stop?: () => void;
34
+ reload?: () => void;
35
+ };
36
+
37
+ export type LoadRawConfigResultWithStop = LoadRawConfigResult & StopConfigWatch;
38
+
39
+ export type LoadRawConfigWithModelsResult = {
40
+ config?: RawConfig;
41
+ errors: ConfigLoadError[];
42
+ };
43
+
44
+ export async function loadConfigWithModelsFromDir(dirPath: string): Promise<LoadRawConfigWithModelsResult> {
45
+ const stackbitYamlPath = await getFirstExistingFile(STACKBIT_CONFIG_YAML_FILES, dirPath);
46
+ if (!stackbitYamlPath) {
47
+ return {
48
+ errors: [new ConfigLoadError(STACKBIT_CONFIG_NOT_FOUND)]
49
+ };
50
+ }
51
+ const stackbitYamlResult = await loadConfigFromStackbitYaml(stackbitYamlPath);
52
+ if (stackbitYamlResult.error) {
53
+ return {
54
+ errors: [stackbitYamlResult.error]
55
+ };
56
+ }
57
+ const { models: modelsFromFiles, errors: fileModelsErrors } = await loadYamlModelsFromFiles(dirPath, stackbitYamlResult.config);
58
+ const mergedModels = mergeConfigModelsWithModelsFromFiles(stackbitYamlResult.config.models ?? {}, modelsFromFiles);
59
+ return {
60
+ config: {
61
+ ...stackbitYamlResult.config,
62
+ models: mergedModels
63
+ },
64
+ errors: fileModelsErrors
65
+ };
66
+ }
67
+
68
+ export async function loadConfigFromStackbitYaml(stackbitYamlPath: string): Promise<LoadRawConfigResult> {
69
+ const stackbitYaml = await fse.readFile(stackbitYamlPath, 'utf8');
70
+ const config = yaml.load(stackbitYaml, { schema: yaml.JSON_SCHEMA });
71
+ if (!config || typeof config !== 'object') {
72
+ const fileName = path.basename(stackbitYamlPath);
73
+ return {
74
+ config: null,
75
+ error: new ConfigLoadError(`error parsing ${fileName}, ${REFER_TO_STACKBIT_CONFIG_DOCS}`)
76
+ };
77
+ }
78
+ return {
79
+ config,
80
+ error: null
81
+ };
82
+ }
83
+
84
+ export async function findStackbitConfigFile(dirs: string[]): Promise<string | null> {
85
+ for (const dir of dirs) {
86
+ for (const fileName of STACKBIT_CONFIG_FILES) {
87
+ const filePath = path.resolve(dir, fileName);
88
+ if (await fse.pathExists(filePath)) {
89
+ return filePath;
90
+ }
91
+ }
92
+ }
93
+ return null;
94
+ }
95
+
96
+ export function isStackbitYamlFile(filePath: string) {
97
+ const pathObject = path.parse(filePath);
98
+ return pathObject.base === 'stackbit.yaml' || pathObject.dir.split(path.sep).includes('.stackbit');
99
+ }
100
+
101
+ export function convertToYamlConfig({ config }: { config: Config }): RawConfig {
102
+ const yamlConfig: RawConfig = _.cloneDeep(_.omit(config, ['models', 'dirPath', 'filePath']));
103
+ if (!_.isEmpty(config.models)) {
104
+ yamlConfig.models = _.reduce(
105
+ config.models,
106
+ (yamlModels: ModelMap, model: Model) => {
107
+ if (model.type === 'image') {
108
+ return yamlModels;
109
+ }
110
+ const yamlModel = _.omit(model, ['name', '__metadata']) as YamlModel;
111
+ if (yamlModel.type === 'page' && !yamlModel.hideContent && yamlModel.fields) {
112
+ _.remove(yamlModel.fields, (field) => field.name === 'markdown_content');
113
+ }
114
+ if (yamlModel.type === 'page' && yamlModel.fields) {
115
+ _.remove(yamlModel.fields, (field) => field.name === (config.pageLayoutKey || 'layout'));
116
+ }
117
+ if (yamlModel.type === 'data' && yamlModel.fields) {
118
+ _.remove(yamlModel.fields, (field) => field.name === (config.objectTypeKey || 'type'));
119
+ }
120
+ yamlModels[model.name] = yamlModel;
121
+ return yamlModels;
122
+ },
123
+ {}
124
+ );
125
+ }
126
+ return yamlConfig;
127
+ }
128
+
129
+ async function loadYamlModelsFromFiles(dirPath: string, config: Partial<RawConfig>): Promise<{ models: Record<string, any>; errors: ConfigLoadError[] }> {
130
+ const modelsSource = _.get(config, 'modelsSource', {});
131
+ const sourceType = _.get(modelsSource, 'type', 'files');
132
+ const defaultModelDirs = ['node_modules/@stackbit/components/models', '.stackbit/models'];
133
+ const modelDirs =
134
+ sourceType === 'files'
135
+ ? _.castArray(_.get(modelsSource, 'modelDirs', defaultModelDirs)).map((modelDir: string) => _.trim(modelDir, '/'))
136
+ : defaultModelDirs;
137
+
138
+ const modelFiles = await reducePromise(
139
+ modelDirs,
140
+ async (modelFiles: string[], modelDir) => {
141
+ const absModelsDir = path.join(dirPath, modelDir);
142
+ const dirExists = await fse.pathExists(absModelsDir);
143
+ if (!dirExists) {
144
+ return modelFiles;
145
+ }
146
+ const files = await readYamlModelFilesFromDir(absModelsDir);
147
+ return modelFiles.concat(files.map((filePath) => path.join(modelDir, filePath)));
148
+ },
149
+ []
150
+ );
151
+
152
+ return reducePromise(
153
+ modelFiles,
154
+ async (result: { models: any; errors: ConfigLoadError[] }, modelFile) => {
155
+ let model;
156
+ try {
157
+ model = await parseFile(path.join(dirPath, modelFile));
158
+ } catch (error) {
159
+ return {
160
+ models: result.models,
161
+ errors: result.errors.concat(new ConfigLoadError(`error parsing model, file: ${modelFile}`))
162
+ };
163
+ }
164
+ const modelName = model?.name;
165
+ if (!modelName) {
166
+ return {
167
+ models: result.models,
168
+ errors: result.errors.concat(new ConfigLoadError(`model does not have a name, file: ${modelFile}`))
169
+ };
170
+ }
171
+ result.models[modelName] = _.omit(model, 'name');
172
+ result.models[modelName].__metadata = {
173
+ filePath: modelFile
174
+ };
175
+ return result;
176
+ },
177
+ { models: {}, errors: [] }
178
+ );
179
+ }
180
+
181
+ async function readYamlModelFilesFromDir(modelsDir: string) {
182
+ return await readDirRecursively(modelsDir, {
183
+ filter: (filePath, stats) => {
184
+ if (stats.isDirectory()) {
185
+ return true;
186
+ }
187
+ const extension = path.extname(filePath).substring(1);
188
+ return stats.isFile() && ['yaml', 'yml'].includes(extension);
189
+ }
190
+ });
191
+ }
192
+
193
+ function mergeConfigModelsWithModelsFromFiles(configModels: any, modelsFromFiles: Record<string, any>) {
194
+ const mergedModels = _.mapValues(modelsFromFiles, (modelFromFile, modelName) => {
195
+ // resolve thumbnails of models loaded from files
196
+ const modelFilePath = modelFromFile.__metadata?.filePath;
197
+ resolveThumbnailPathForModel(modelFromFile, modelFilePath);
198
+ iterateModelFieldsRecursively(modelFromFile, (field: any) => {
199
+ if (isListField(field)) {
200
+ field = normalizeListFieldInPlace(field);
201
+ field = field.items;
202
+ }
203
+ if (isObjectField(field)) {
204
+ resolveThumbnailPathForModel(field, modelFilePath);
205
+ } else if (isEnumField(field)) {
206
+ resolveThumbnailPathForEnumField(field, modelFilePath);
207
+ }
208
+ });
209
+
210
+ const configModel = _.get(configModels, modelName);
211
+ if (!configModel) {
212
+ return modelFromFile;
213
+ }
214
+
215
+ return _.assign({}, modelFromFile, configModel, {
216
+ fields: _.unionBy(configModel?.fields ?? [], modelFromFile?.fields ?? [], 'name')
217
+ });
218
+ });
219
+ return Object.assign({}, configModels, mergedModels);
220
+ }
221
+
222
+ function resolveThumbnailPathForModel(modelOrField: Model | FieldObjectProps, modelFilePath: string | undefined) {
223
+ if (modelOrField.thumbnail && modelFilePath) {
224
+ const modelDirPath = path.dirname(modelFilePath);
225
+ modelOrField.thumbnail = resolveThumbnailPath(modelOrField.thumbnail, modelDirPath);
226
+ }
227
+ }
228
+
229
+ function resolveThumbnailPathForEnumField(enumField: FieldEnum, modelFilePath: string | undefined) {
230
+ if (enumField.controlType === 'thumbnails' && modelFilePath) {
231
+ const modelDirPath = path.dirname(modelFilePath);
232
+ _.forEach(enumField.options, (option) => {
233
+ if (option.thumbnail) {
234
+ option.thumbnail = resolveThumbnailPath(option.thumbnail, modelDirPath);
235
+ }
236
+ });
237
+ }
238
+ }
239
+
240
+ function resolveThumbnailPath(thumbnail: string, modelDirPath: string) {
241
+ if (thumbnail.startsWith('//') || /https?:\/\//.test(thumbnail)) {
242
+ return thumbnail;
243
+ }
244
+ if (thumbnail.startsWith('/')) {
245
+ if (modelDirPath.endsWith('@stackbit/components/models')) {
246
+ modelDirPath = modelDirPath.replace(/\/models$/, '');
247
+ } else {
248
+ modelDirPath = '';
249
+ }
250
+ thumbnail = thumbnail.replace(/^\//, '');
251
+ }
252
+ return path.join(modelDirPath, thumbnail);
253
+ }