UNPKG

@stackbit/sdk

Version:
1,005 lines 45.3 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.mergeConfigModelsWithExternalModels = exports.loadConfigFromDir = exports.validateAndNormalizeConfig = exports.loadAndMergeModelsFromFiles = exports.loadConfig = exports.loadConfigWithModels = exports.loadConfigWithModelsPresetsAndValidate = void 0; const path_1 = __importDefault(require("path")); const fs_extra_1 = __importDefault(require("fs-extra")); const chokidar_1 = __importDefault(require("chokidar")); const semver_1 = __importDefault(require("semver")); const lodash_1 = __importDefault(require("lodash")); const utils_1 = require("@stackbit/utils"); const config_validator_1 = require("./config-validator"); const config_errors_1 = require("./config-errors"); const config_loader_esbuild_1 = require("./config-loader-esbuild"); const utils_2 = require("../utils"); const presets_loader_1 = require("./presets-loader"); const config_loader_utils_1 = require("./config-loader-utils"); const CONTENT_ENGINE_DEFAULT_PORT = 8000; async function loadConfigWithModelsPresetsAndValidate({ dirPath, modelsSource, stackbitConfigESBuildOutDir, watchCallback, logger, isForcedGitCSI }) { const configResult = await loadConfigWithModels({ dirPath, stackbitConfigESBuildOutDir, watchCallback: watchCallback ? async (configResult) => { const configLoaderResult = await processConfigLoaderResult({ configResult, dirPath, modelsSource, logger, isForcedGitCSI }); watchCallback(configLoaderResult); } : undefined, logger }); const configLoaderResult = await processConfigLoaderResult({ configResult, dirPath, modelsSource, logger, isForcedGitCSI }); const reload = configResult.reload; return { ...configLoaderResult, destroy: configResult.destroy, reload: reload ? async () => { const configResult = await reload(); return processConfigLoaderResult({ configResult, dirPath, modelsSource, logger, isForcedGitCSI }); } : undefined }; } exports.loadConfigWithModelsPresetsAndValidate = loadConfigWithModelsPresetsAndValidate; async function loadConfigWithModels({ dirPath, stackbitConfigESBuildOutDir, watchCallback, logger }) { const wrapConfigResult = async (configResult) => { if (!configResult.config) { return { config: null, errors: configResult.errors }; } return await loadAndMergeModelsFromFiles(configResult.config); }; const rawConfigResult = await loadConfig({ dirPath, stackbitConfigESBuildOutDir, logger, watchCallback: watchCallback ? async (configResult) => { const wrappedResult = await wrapConfigResult(configResult); watchCallback(wrappedResult); } : undefined }); const wrappedResult = await wrapConfigResult(rawConfigResult); const reload = rawConfigResult.reload; return { ...wrappedResult, destroy: rawConfigResult.destroy, reload: reload ? async () => { const configResult = await reload(); return wrapConfigResult(configResult); } : undefined }; } exports.loadConfigWithModels = loadConfigWithModels; async function loadConfig({ dirPath, stackbitConfigESBuildOutDir, watchCallback, logger }) { const normalizeConfigResult = (rawConfigResult) => { if (!rawConfigResult.config) { return { config: null, errors: [rawConfigResult.error] }; } const validationResult = (0, config_validator_1.validateBaseConfig)(rawConfigResult.config); const config = normalizeConfig(rawConfigResult.config); return { config: config, errors: validationResult.errors }; }; const rawConfigResult = await loadConfigFromDir({ dirPath, stackbitConfigESBuildOutDir, watchCallback: watchCallback ? async (rawConfigResult) => { const normalizedResult = await normalizeConfigResult(rawConfigResult); watchCallback(normalizedResult); } : undefined, logger }); const normalizedResult = await normalizeConfigResult(rawConfigResult); const reload = rawConfigResult.reload; return { ...normalizedResult, destroy: rawConfigResult.destroy, reload: reload ? async () => { const rawConfigResult = await reload(); return normalizeConfigResult(rawConfigResult); } : undefined }; } exports.loadConfig = loadConfig; async function processConfigLoaderResult({ configResult, dirPath, modelsSource, logger, isForcedGitCSI }) { const { config, errors: configLoadErrors } = configResult; if (!config) { return { valid: false, config: null, errors: configLoadErrors }; } if (isForcedGitCSI) { const normalizedResult = validateAndNormalizeConfig(config, isForcedGitCSI); return { valid: normalizedResult.valid, config: { ...normalizedResult.config, models: normalizedResult.config.models, presets: {} }, errors: [...configLoadErrors, ...normalizedResult.errors] }; } const { models: externalModels, errors: externalModelsLoadErrors } = await loadModelsFromExternalSource(config, dirPath, modelsSource); const mergedModels = externalModels.length === 0 ? config.models : mergeConfigModelsWithExternalModels({ configModels: config.models, externalModels, logger }); const mergedConfig = { ...config, models: mergedModels }; const normalizedResult = validateAndNormalizeConfig(mergedConfig); const presetsResult = await (0, presets_loader_1.loadPresets)({ config: normalizedResult.config }); const modelsWithPresetIds = (0, presets_loader_1.extendModelsWithPresetsIds)({ models: normalizedResult.config.models, presets: presetsResult.presets }); const configWithPresets = { ...normalizedResult.config, models: modelsWithPresetIds, presets: presetsResult.presets }; return { valid: normalizedResult.valid, config: configWithPresets, errors: [...configLoadErrors, ...externalModelsLoadErrors, ...normalizedResult.errors, ...presetsResult.errors] }; } async function loadAndMergeModelsFromFiles(config) { const { models: modelsFromFiles, errors: modelLoadErrors } = await (0, config_loader_utils_1.loadYamlModelsFromFiles)(config); const { models: mergedModels, errors: mergeModelErrors } = (0, config_loader_utils_1.mergeConfigModelsWithModelsFromFiles)(config.models, modelsFromFiles); const extendedConfig = { ...config, models: mergedModels }; return { config: extendedConfig, errors: [...modelLoadErrors, ...mergeModelErrors] }; } exports.loadAndMergeModelsFromFiles = loadAndMergeModelsFromFiles; function validateAndNormalizeConfig(config, isForcedGitCSI) { // validate the "contentModels" and extend config models with "contentModels" // this must be done before main config validation to make it independent of "contentModels". const { config: configWithContentModels, errors: contentModelsErrors } = validateAndExtendContentModels(config); // normalize config - backward compatibility updates, adding extra fields like "markdown_content", "type" and "layout", // and setting other default values. const configWithNormalizedModels = normalizeModels(configWithContentModels, isForcedGitCSI); // validate config const { config: validatedConfig, errors: validationErrors } = (0, config_validator_1.validateConfig)(configWithNormalizedModels); const errors = [...contentModelsErrors, ...validationErrors]; return normalizeValidationResult({ valid: lodash_1.default.isEmpty(errors), config: validatedConfig, errors: errors }); } exports.validateAndNormalizeConfig = validateAndNormalizeConfig; async function loadConfigFromDir({ dirPath, stackbitConfigESBuildOutDir, watchCallback, logger }) { function wrapResult(result, configFilePath) { if (result.error) { return { config: null, error: result.error }; } else { return { config: { ...result.config, dirPath: dirPath, filePath: configFilePath }, error: null }; } } // try to load stackbit config from YAML files try { const stackbitYamlPath = await (0, utils_1.getFirstExistingFile)(config_loader_utils_1.STACKBIT_CONFIG_YAML_FILES, dirPath); if (stackbitYamlPath) { logger?.debug(`loading Stackbit configuration from ${stackbitYamlPath}`); const result = await (0, config_loader_utils_1.loadStackbitYamlFromDir)(dirPath); let close = async () => void 0; let stopped = false; if (watchCallback) { const watcher = chokidar_1.default.watch([...config_loader_utils_1.STACKBIT_CONFIG_YAML_FILES, '.stackbit/models'], { cwd: dirPath, persistent: true, ignoreInitial: true }); const throttledFileChange = lodash_1.default.throttle(async () => { const result = await (0, config_loader_utils_1.loadStackbitYamlFromDir)(dirPath); watchCallback(wrapResult(result, stackbitYamlPath)); }, 1000); const handleFileChange = (path) => { logger?.debug(`identified change in stackbit config file: ${path}, reloading config...`); throttledFileChange(); }; watcher.on('add', handleFileChange); watcher.on('change', handleFileChange); watcher.on('unlink', handleFileChange); watcher.on('addDir', handleFileChange); watcher.on('unlinkDir', handleFileChange); watcher.on('error', (error) => { watchCallback({ config: null, error: new config_errors_1.ConfigLoadError(`Error loading Stackbit configuration: ${error.message}`, { originalError: error }) }); }); close = async () => { if (stopped) { return; } stopped = true; throttledFileChange.cancel(); watcher.close(); }; } return { ...wrapResult(result, stackbitYamlPath), destroy: close, reload: async () => { const result = await (0, config_loader_utils_1.loadStackbitYamlFromDir)(dirPath); return wrapResult(result, stackbitYamlPath); } }; } } catch (error) { return { config: null, error: new config_errors_1.ConfigLoadError(`Error loading Stackbit configuration: ${error.message}`, { originalError: error }) }; } // try to load stackbit config from JavaScript files try { const configFilePath = await (0, utils_1.getFirstExistingFile)(config_loader_utils_1.STACKBIT_CONFIG_JS_FILES, dirPath); if (configFilePath) { logger?.debug(`loading Stackbit configuration from: ${configFilePath}`); const configResult = await (0, config_loader_esbuild_1.loadStackbitConfigFromJs)({ configPath: configFilePath, outDir: stackbitConfigESBuildOutDir ?? '.stackbit/cache', watch: !!watchCallback, logger: logger, callback: watchCallback ? (result) => { watchCallback(wrapResult(result, configFilePath)); } : undefined }); const reload = configResult.reload; return { ...wrapResult(configResult, configFilePath), destroy: configResult.destroy, reload: reload ? async () => { const result = await reload(); return wrapResult(result, configFilePath); } : undefined }; } } catch (error) { return { config: null, error: new config_errors_1.ConfigLoadError(`Error loading Stackbit configuration: ${error.message}`, { originalError: error }) }; } return { config: null, error: new config_errors_1.StackbitConfigNotFoundError() }; } exports.loadConfigFromDir = loadConfigFromDir; async function loadModelsFromExternalSource(config, dirPath, modelsSource) { modelsSource = lodash_1.default.assign({}, modelsSource, config.modelsSource); const sourceType = lodash_1.default.get(modelsSource, 'type', 'files'); if (sourceType === 'files') { // we already loaded models from files inside loadConfigFromDir function return { models: [], errors: [] }; } else if (sourceType === 'contentful') { const contentfulModule = lodash_1.default.get(modelsSource, 'module', '@stackbit/cms-contentful'); const modulePath = path_1.default.resolve(dirPath, 'node_modules', contentfulModule); const module = await import(modulePath); try { const { models } = await module.fetchAndConvertSchema(lodash_1.default.omit(modelsSource, ['type', 'module'])); return { models: models, errors: [] }; } catch (error) { return { models: [], errors: [new config_errors_1.ModelLoadError(`Error fetching and converting Contentful schema, error: ${error.message}`, { originalError: error })] }; } } return { models: [], errors: [new config_errors_1.ModelLoadError(`modelsSource ${modelsSource} is unsupported`)] }; } // eslint-disable-next-line @typescript-eslint/no-unused-vars async function loadConfigFromDotStackbit(dirPath) { const stackbitDotPath = path_1.default.join(dirPath, '.stackbit'); const stackbitDotExists = await fs_extra_1.default.pathExists(stackbitDotPath); if (!stackbitDotExists) { return null; } const config = {}; const themeYaml = path_1.default.join(stackbitDotPath, 'theme.yaml'); const themeYamlExists = await fs_extra_1.default.readFile(themeYaml); if (themeYamlExists) { const themeConfig = await fs_extra_1.default.readFile(themeYaml); lodash_1.default.assign(config, themeConfig); } const studioYaml = path_1.default.join(stackbitDotPath, 'studio.yaml'); const studioYamlExists = await fs_extra_1.default.readFile(themeYaml); if (studioYamlExists) { const studioConfig = await fs_extra_1.default.readFile(studioYaml); lodash_1.default.assign(config, studioConfig); } const schemaYaml = path_1.default.join(stackbitDotPath, 'schema.yaml'); const schemaYamlExists = await fs_extra_1.default.readFile(themeYaml); if (schemaYamlExists) { const schemaConfig = await fs_extra_1.default.readFile(schemaYaml); lodash_1.default.assign(config, schemaConfig); } return lodash_1.default.isEmpty(config) ? null : config; } function mergeConfigModelsWithExternalModels({ configModels, externalModels, logger }) { if (configModels.length === 0) { return externalModels; } const mergedModelsByName = lodash_1.default.keyBy(externalModels, 'name'); for (const configModel of configModels) { const externalModel = mergedModelsByName[configModel.name]; if (!externalModel) { logger?.warn(`Can't remap model '${configModel.name}', the model doesn't exist in content source`); continue; } const modelType = configModel.type ? (configModel.type === 'config' ? 'data' : configModel.type) : externalModel.type ?? 'object'; let mergedModel = Object.assign({}, externalModel, lodash_1.default.pick(configModel, [ '__metadata', 'urlPath', 'label', 'description', 'thumbnail', 'singleInstance', 'readOnly', 'canDelete', 'labelField', 'preview', 'permissions', 'localized' ]), { type: modelType }); if (mergedModel.type === 'page' && !mergedModel.urlPath) { mergedModel.urlPath = '/{slug}'; } const externalFieldGroups = externalModel.fieldGroups && Array.isArray(externalModel.fieldGroups) ? externalModel.fieldGroups : []; const configFieldGroups = configModel.fieldGroups && Array.isArray(configModel.fieldGroups) ? configModel.fieldGroups : []; if (externalFieldGroups.length || configFieldGroups.length) { mergedModel.fieldGroups = lodash_1.default.uniqBy([...externalFieldGroups, ...configFieldGroups], 'name'); } const externalActions = 'actions' in externalModel && Array.isArray(externalModel.actions) ? externalModel.actions : []; const configActions = 'actions' in configModel && Array.isArray(configModel.actions) ? configModel.actions : []; if (externalActions.length || configActions.length) { mergedModel.actions = lodash_1.default.uniqBy([...externalActions, ...configActions], 'name'); } mergedModel = (0, utils_2.mapModelFieldsRecursively)(mergedModel, (externalField, modelKeyPath) => { const stackbitField = (0, utils_2.getModelFieldForModelKeyPath)(configModel, modelKeyPath); if (!stackbitField) { return externalField; } const FieldRemapMatrix = { string: ['text', 'html', 'markdown', 'slug', 'url', 'color', 'date', 'datetime', 'enum', 'image', 'json', 'style', 'cross-reference'], text: ['html', 'markdown', 'image', 'json', 'style', 'cross-reference'], json: ['image', 'style', 'cross-reference'] }; let stackbitFieldProps; if (externalField.type === 'list') { if (!stackbitField.type || stackbitField.type === 'list') { stackbitFieldProps = 'items' in stackbitField ? stackbitField.items ?? {} : {}; } else { stackbitFieldProps = {}; logger?.warn(`Can't remap field of model '${mergedModel.name}' ` + `at path '${(0, utils_1.fieldPathToString)(modelKeyPath)}' ` + `from 'list' type to '${stackbitField.type}' type. ` + 'List fields can only be mapped to list fields'); } } else if (stackbitField.type === 'list' || 'items' in stackbitField) { stackbitFieldProps = {}; logger?.warn(`Can't remap field of model '${mergedModel.name}' ` + `at path '${(0, utils_1.fieldPathToString)(modelKeyPath)}' ` + `from '${externalField.type}' type to 'list' type. ` + 'List fields can only be mapped from list fields'); } else { stackbitFieldProps = stackbitField; } externalField = (0, utils_2.mapListItemsPropsOrSelfSpecificProps)(externalField, (externalFieldProps) => { // override field type if allowed, otherwise show a warning message let fieldType = externalFieldProps.type; const allowedOverrideTypes = FieldRemapMatrix[externalFieldProps.type]; if (stackbitFieldProps.type && stackbitFieldProps.type !== externalFieldProps.type) { if (allowedOverrideTypes && allowedOverrideTypes.includes(stackbitFieldProps.type)) { fieldType = stackbitFieldProps.type; } else { logger?.warn(`Can't remap field of model '${mergedModel.name}' at path '${(0, utils_1.fieldPathToString)(modelKeyPath)}' ` + `from '${externalFieldProps.type}' type to '${stackbitFieldProps.type}' type. ` + `The '${externalFieldProps.type}' fields can be only mapped to ` + (!allowedOverrideTypes ? 'the same type.' : allowedOverrideTypes.length === 1 ? `'${allowedOverrideTypes[0]}' type` : `one of [${allowedOverrideTypes.join(', ')}] types.`)); } } // add field specific properties let fieldSpecificProps = {}; switch (fieldType) { case 'number': fieldSpecificProps = lodash_1.default.pick(stackbitFieldProps, ['subtype', 'min', 'max', 'step', 'unit']); break; case 'enum': fieldSpecificProps = lodash_1.default.pick(stackbitFieldProps, ['options']); break; case 'image': fieldSpecificProps = lodash_1.default.pick(stackbitFieldProps, ['source']); break; case 'style': fieldSpecificProps = lodash_1.default.pick(stackbitFieldProps, ['styles']); break; case 'object': { fieldSpecificProps = lodash_1.default.pick(stackbitFieldProps, ['labelField', 'preview', 'thumbnail']); const externalFieldGroups = 'fieldGroups' in externalFieldProps && Array.isArray(externalFieldProps.fieldGroups) ? externalFieldProps.fieldGroups : []; const configFieldGroups = 'fieldGroups' in stackbitFieldProps && Array.isArray(stackbitFieldProps.fieldGroups) ? stackbitFieldProps.fieldGroups : []; if (externalFieldGroups.length || configFieldGroups.length) { fieldSpecificProps = { ...fieldSpecificProps, fieldGroups: lodash_1.default.uniqBy([...externalFieldGroups, ...configFieldGroups], 'name') }; } break; } case 'model': fieldSpecificProps = lodash_1.default.pick(stackbitFieldProps, ['models']); break; case 'reference': fieldSpecificProps = lodash_1.default.pick(stackbitFieldProps, ['models']); break; case 'cross-reference': fieldSpecificProps = lodash_1.default.pick(stackbitFieldProps, ['models']); break; } const validations = mergeFieldValidations(externalFieldProps, stackbitFieldProps); return Object.assign({}, externalFieldProps, fieldSpecificProps, { type: fieldType }, validations ? { validations } : undefined); }); const mergedField = Object.assign({}, externalField, lodash_1.default.pick(stackbitField, [ 'label', 'description', 'required', 'default', 'group', 'const', 'hidden', 'readOnly', 'controlType', 'controlUrl', 'controlFilePath' ])); // Merge list field specific properties that weren't merged in mapListItemsPropsOrSelfSpecificProps if (externalField.type === 'list') { const validations = mergeFieldValidations(externalField, stackbitField); if (validations) { mergedField.validations = validations; } } const externalActions = 'actions' in externalField && Array.isArray(externalField.actions) ? externalField.actions : []; const configActions = 'actions' in stackbitField && Array.isArray(stackbitField.actions) ? stackbitField.actions : []; if (externalActions.length || configActions.length) { mergedField.actions = lodash_1.default.uniqBy([...externalActions, ...configActions], 'name'); } return mergedField; }); mergedModelsByName[configModel.name] = mergedModel; } return Object.values(mergedModelsByName); } exports.mergeConfigModelsWithExternalModels = mergeConfigModelsWithExternalModels; function mergeFieldValidations(externalFieldProps, stackbitFieldProps) { const externalFieldValidations = externalFieldProps.validations; const configFieldValidation = stackbitFieldProps.validations; if (externalFieldValidations && configFieldValidation) { const extendedValidations = {}; // concatenate custom validation functions if ('validate' in externalFieldValidations && externalFieldValidations.validate && 'validate' in configFieldValidation && configFieldValidation.validate) { const externalFileTypes = Array.isArray(externalFieldValidations.validate) ? externalFieldValidations.validate : [externalFieldValidations.validate]; const stackbitFileTypes = Array.isArray(configFieldValidation.validate) ? configFieldValidation.validate : [configFieldValidation.validate]; extendedValidations.validate = [...externalFileTypes, ...stackbitFileTypes]; } return Object.assign({}, externalFieldValidations, configFieldValidation, extendedValidations); } else if (configFieldValidation) { return configFieldValidation; } else { return externalFieldValidations; } } function normalizeConfig(rawConfig) { const stackbitVersion = String(lodash_1.default.get(rawConfig, 'stackbitVersion', config_loader_utils_1.LATEST_STACKBIT_VERSION)); const ver = semver_1.default.coerce(stackbitVersion); const isGTEStackbitYamlV5 = ver ? semver_1.default.satisfies(ver, '>=0.5.0') : false; const { logicFields, models: modelMap, sitemap, ...restConfig } = rawConfig; // in stackbit.yaml 'models' are defined as object where keys are the model names, // convert 'models' to array of objects and set their 'name' property to the model name const models = lodash_1.default.reduce(modelMap, (accum, model, modelName) => accum.concat(Object.assign({ name: modelName }, model)), []); return { ...restConfig, stackbitVersion: stackbitVersion, models: models, ...(sitemap ? { siteMap: sitemap } : {}), noEncodeFields: logicFields, hcrHandled: !stackbitVersion || lodash_1.default.get(rawConfig, 'customContentReload', lodash_1.default.get(rawConfig, 'hcrHandled', !isGTEStackbitYamlV5)), contentEngine: rawConfig?.contentEngine || { port: CONTENT_ENGINE_DEFAULT_PORT }, internalStackbitRunnerOptions: getInternalStackbitRunnerOptions(rawConfig) }; } function normalizeModels(config, isForcedGitCSI) { const pageLayoutKey = config.pageLayoutKey ?? 'layout'; const objectTypeKey = config.objectTypeKey ?? 'type'; const stackbitYamlVersion = String(config.stackbitVersion ?? ''); const ver = semver_1.default.coerce(stackbitYamlVersion); const isStackbitYamlV2 = ver ? semver_1.default.satisfies(ver, '<0.3.0') : false; const models = config.models; const modelsByName = lodash_1.default.keyBy(models, 'name'); const gitCMS = isGitCMS(config); const mappedModels = models.map((model) => { // create shallow copy of the model to prevent mutation of original models model = { ...model }; if (!lodash_1.default.has(model, 'type')) { model.type = 'object'; } // add model label if not set if (!lodash_1.default.has(model, 'label')) { model.label = lodash_1.default.startCase(model.name); } if (lodash_1.default.has(model, 'fields') && !Array.isArray(model.fields)) { model.fields = []; } if ((0, utils_2.isPageModel)(model)) { // rename old 'template' property to 'layout' (0, utils_1.rename)(model, 'template', 'layout'); updatePageUrlPath(model); if (gitCMS) { updatePageFilePath(model, config); addMarkdownContentField(model); } } else if ((0, utils_2.isDataModel)(model) && gitCMS) { updateDataFilePath(model, config); } if (gitCMS && !isForcedGitCSI) { // TODO: do not add pageLayoutKey and objectTypeKey fields to models, // The content validator should always assume these fields. // And when new objects created from UI, it should add these fields automatically. if ((0, utils_2.isPageModel)(model)) { addLayoutFieldToPageModel(model, pageLayoutKey, model.name); } else if ((0, utils_2.isDataModel)(model) && !(0, utils_2.isListDataModel)(model)) { addObjectTypeKeyField(model, objectTypeKey, model.name); } } if ((0, utils_2.isListDataModel)(model)) { // 'items.type' of list model defaults to 'string', set it explicitly (0, utils_2.normalizeListFieldInPlace)(model); if ((0, utils_2.isObjectListItems)(model.items)) { (0, utils_2.assignLabelFieldIfNeeded)(model.items); } } else if (!lodash_1.default.has(model, 'labelField')) { (0, utils_2.assignLabelFieldIfNeeded)(model); } return (0, utils_2.mapModelFieldsRecursively)(model, (field) => { // create shallow copy of the field to prevent mutation of original field field = { ...field }; // add field label if label is not set if (!lodash_1.default.has(field, 'label')) { field.label = lodash_1.default.startCase(field.name); } return (0, utils_2.mapListItemsPropsOrSelfSpecificProps)(field, (fieldSpecificProps) => { if ((0, utils_2.isObjectField)(fieldSpecificProps)) { (0, utils_2.assignLabelFieldIfNeeded)(fieldSpecificProps); } else if ((0, utils_2.isCustomModelField)(fieldSpecificProps, modelsByName)) { // stackbit v0.2.0 compatibility // convert the old custom model field type: { type: 'action' } // to the new 'model' field type: { type: 'model', models: ['action'] } fieldSpecificProps = { ...fieldSpecificProps, type: 'model', models: [fieldSpecificProps.type] }; } else if (fieldSpecificProps.type === 'models') { // stackbit v0.2.0 compatibility // convert the old 'models' field type: { type: 'models', models: ['link', 'button'] } // to the new 'model' field type: { type: 'model', models: ['link', 'button'] } fieldSpecificProps = { ...fieldSpecificProps, type: 'model', models: lodash_1.default.get(fieldSpecificProps, 'models', []) }; } else if (fieldSpecificProps.type === 'model' && lodash_1.default.has(fieldSpecificProps, 'model')) { // stackbit v0.2.0 compatibility // convert the old 'model' field type: { type: 'model', model: 'link' } // to the new 'model' field type: { type: 'model', models: ['link'] } const { model, ...rest } = fieldSpecificProps; fieldSpecificProps = { ...rest, models: [model] }; } if (isStackbitYamlV2) { // in stackbit.yaml v0.2.x, the 'reference' field was what we have today as 'model' field: if ((0, utils_2.isReferenceField)(fieldSpecificProps)) { fieldSpecificProps = { ...fieldSpecificProps, type: 'model', models: lodash_1.default.get(fieldSpecificProps, 'models', []) }; } } return fieldSpecificProps; }); }); }); return { ...config, models: mappedModels }; } function updatePageUrlPath(model) { // set default urlPath if not set if (!model.urlPath) { model.urlPath = '/{slug}'; } } /** * Sets the page model's filePath pattern. * If the model has `filePath` property, it is prefixed with `pagesDir` and returned. * If the model has no `filePath` property, then `filePath` is naively inferred by * prefixing `urlPath` with `pagesDir` and appending the `.md` extension. */ function updatePageFilePath(model, config) { let filePath; if (model.filePath) { if (typeof model.filePath === 'function') { return; } filePath = model.filePath; } else if (model.file) { filePath = model.file; } else { const urlPath = model.urlPath; if (urlPath === '/') { filePath = 'index.md'; } else if (lodash_1.default.trim(urlPath, '/') === 'posts/{slug}' && config.ssgName === 'jekyll') { filePath = '_posts/{moment_format("YYYY-MM-DD")}-{slug}.md'; } else { filePath = lodash_1.default.trim(urlPath, '/') + '.md'; } } const parentDir = lodash_1.default.trim(config.pagesDir ?? '', '/'); model.filePath = path_1.default.join(parentDir, filePath); } function updateDataFilePath(model, config) { let filePath; if (model.filePath) { if (typeof model.filePath === 'function') { return; } filePath = model.filePath; } else if (model.file) { filePath = model.file; } else { const folder = lodash_1.default.trim(lodash_1.default.get(model, 'folder'), '/'); filePath = lodash_1.default.trim(`${folder}/{slug}.json`, '/'); } const parentDir = lodash_1.default.trim(config.dataDir ?? '', '/'); model.filePath = path_1.default.join(parentDir, filePath); } function addMarkdownContentField(model) { if (model.hideContent) { return; } const hasMarkdownContent = lodash_1.default.find(lodash_1.default.get(model, 'fields'), { name: 'markdown_content' }); if (hasMarkdownContent) { return; } (0, utils_1.append)(model, 'fields', { type: 'markdown', name: 'markdown_content', label: 'Content', description: 'Page content' }); } function addLayoutFieldToPageModel(model, pageLayoutKey, modelName) { if (lodash_1.default.intersection(lodash_1.default.keys(model), ['file', 'folder', 'match', 'exclude']).length === 0 && !lodash_1.default.get(model, 'layout')) { model.layout = modelName; } const modelLayout = lodash_1.default.get(model, 'layout'); if (!modelLayout) { return; } const hasLayoutField = lodash_1.default.find(lodash_1.default.get(model, 'fields'), { name: pageLayoutKey }); if (hasLayoutField) { return; } (0, utils_1.prepend)(model, 'fields', { type: 'string', name: pageLayoutKey, label: lodash_1.default.startCase(pageLayoutKey), const: modelLayout, hidden: true }); } function addObjectTypeKeyField(model, objectTypeKey, modelName) { const hasObjectTypeField = lodash_1.default.find(lodash_1.default.get(model, 'fields'), { name: objectTypeKey }); if (hasObjectTypeField) { return; } (0, utils_1.prepend)(model, 'fields', { type: 'string', name: objectTypeKey, label: 'Object Type', description: 'The type of the object', const: modelName, hidden: true }); } /** * Returns model names referenced by polymorphic 'model' and 'reference' fields. * * @param field */ function getReferencedModelNames(field) { const fieldSpecificProps = (0, utils_2.getListItemsOrSelf)(field); // TODO: add type field to model fields inside container update/create object logic rather adding type to schema // 'object' models referenced by 'model' fields should have 'type' field // if these fields have than 1 model. // 'data' models referenced by 'reference' fields should always have 'type' field. let referencedModelNames = []; if ((0, utils_2.isModelField)(fieldSpecificProps) && fieldSpecificProps.models?.length > 1) { const modelNames = fieldSpecificProps.models; referencedModelNames = lodash_1.default.union(referencedModelNames, modelNames); } else if ((0, utils_2.isReferenceField)(fieldSpecificProps) && fieldSpecificProps.models?.length > 0) { const modelNames = fieldSpecificProps.models; referencedModelNames = lodash_1.default.union(referencedModelNames, modelNames); } return referencedModelNames; } function validateAndExtendContentModels(config) { const contentModels = config.contentModels ?? {}; const models = config.models ?? []; // external models already merged in mergeConfigModelsWithExternalModels function const externalModels = !isGitCMS(config); const emptyContentModels = lodash_1.default.isEmpty(contentModels); if (externalModels || emptyContentModels) { return { config: config, errors: [] }; } const validationResult = (0, config_validator_1.validateContentModels)(contentModels, models); if (lodash_1.default.isEmpty(models)) { return { config: config, errors: validationResult.errors }; } const extendedModels = models.map((model) => { const contentModel = validationResult.contentModels[model.name]; if (!contentModel) { return model; } if (lodash_1.default.get(contentModel, '__metadata.invalid')) { return model; } const { isPage, newFilePath, ...restContentModel } = contentModel; const { type, ...restModel } = model; if (isPage && (!type || ['object', 'page'].includes(type))) { return { type: 'page', ...(newFilePath ? { filePath: newFilePath } : {}), ...restContentModel, ...restModel }; } else if (!isPage && (!type || ['object', 'data'].includes(type))) { return { type: 'data', ...(newFilePath ? { filePath: newFilePath } : {}), ...restContentModel, ...restModel }; } else { return model; } }); return { config: { ...config, models: extendedModels }, errors: validationResult.errors }; } function normalizeValidationResult(validationResult) { validationResult = filterAndOrderConfigFields(validationResult); return convertModelGroupsToModelListInPlace(validationResult); } function filterAndOrderConfigFields(validationResult) { // TODO: check if we can move filtering and sorting to Joi return { ...validationResult, config: lodash_1.default.pick(validationResult.config, [ 'stackbitVersion', 'ssgName', 'ssgVersion', 'cmsName', 'import', 'buildCommand', 'publishDir', 'nodeVersion', 'postGitCloneCommand', 'preInstallCommand', 'postInstallCommand', 'installCommand', 'devCommand', 'cacheDir', 'staticDir', 'uploadDir', 'assets', 'pagesDir', 'dataDir', 'pageLayoutKey', 'objectTypeKey', 'styleObjectModelName', 'excludePages', 'logicFields', 'contentModels', 'presetSource', 'modelsSource', 'mapModels', 'presetReferenceBehavior', 'nonDuplicatableModels', 'duplicatableModels', 'contentSources', 'hcrHandled', 'internalStackbitRunnerOptions', 'dirPath', 'filePath', 'models', 'presets', 'sidebarButtons', 'pageData', 'pageModels', 'encodedFieldTypes', 'noEncodeFields', 'omitFields' // obsolete, left for backward compatibility ]) }; } /** * Collects models groups and injects them into the `models` array of the * `reference` and `model` field types */ function convertModelGroupsToModelListInPlace(validationResult) { const models = validationResult.config?.models ?? []; const groupMap = lodash_1.default.reduce(models, (groupMap, model) => { if (!model.groups) { return groupMap; } const key = model?.type === 'object' ? 'objectModels' : 'documentModels'; lodash_1.default.forEach(model.groups, (groupName) => { (0, utils_1.append)(groupMap, [groupName, key], model.name); }); delete model.groups; return groupMap; }, {}); // update groups to have unique model names lodash_1.default.forEach(groupMap, (group) => { lodash_1.default.forEach(group, (modelGroup, key) => { lodash_1.default.set(group, key, lodash_1.default.uniq(modelGroup)); }); }); const mappedModels = models.map((model) => { return (0, utils_2.mapModelFieldsRecursively)(model, (field) => { return (0, utils_2.mapListItemsPropsOrSelfSpecificProps)(field, (fieldSpecificProps) => { if (!(0, utils_2.isModelField)(fieldSpecificProps) && !(0, utils_2.isReferenceField)(fieldSpecificProps)) { return fieldSpecificProps; } const { ...cloned } = fieldSpecificProps; const key = (0, utils_2.isModelField)(fieldSpecificProps) ? 'objectModels' : 'documentModels'; const modelNames = lodash_1.default.reduce(cloned.groups, (modelNames, groupName) => { const objectModelNames = lodash_1.default.get(groupMap, [groupName, key], []); return lodash_1.default.uniq(modelNames.concat(objectModelNames)); }, fieldSpecificProps.models || []); delete cloned.groups; return Object.assign(cloned, { models: modelNames }); }); }); }); return { ...validationResult, config: { ...validationResult.config, models: mappedModels } }; } function isGitCMS(config) { return !config.contentSources && (!config.cmsName || config.cmsName === 'git'); } function getInternalStackbitRunnerOptions(config) { const experimentalSsgData = config?.experimental?.ssg || config.__unsafe_internal_stackbitRunnerOptions; if (!experimentalSsgData) { return; } let doneStart = experimentalSsgData.logPatterns?.up; if (typeof doneStart === 'string') { doneStart = [doneStart]; } const ssgRunOptions = (0, utils_1.omitByNil)({ displayName: experimentalSsgData.name, triggerInstallFiles: experimentalSsgData.watch?.reinstallPackages, directPaths: experimentalSsgData.passthrough, directRoutes: experimentalSsgData.directRoutes, proxyWebsockets: experimentalSsgData.proxyWebsockets, ...(doneStart ? { patterns: { doneStart } } : undefined) }); return lodash_1.default.isEmpty(ssgRunOptions) ? undefined : ssgRunOptions; } //# sourceMappingURL=config-loader.js.map