@stackbit/sdk
Version:
1,005 lines • 45.3 kB
JavaScript
;
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