UNPKG

@stackbit/sdk

Version:
412 lines 16.6 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.loadContent = void 0; const lodash_1 = __importDefault(require("lodash")); const fs_extra_1 = __importDefault(require("fs-extra")); const path_1 = __importDefault(require("path")); const micromatch_1 = __importDefault(require("micromatch")); const utils_1 = require("@stackbit/utils"); const content_errors_1 = require("./content-errors"); const utils_2 = require("../utils"); const content_validator_1 = require("./content-validator"); const consts_1 = require("../consts"); async function loadContent({ dirPath, config, skipUnmodeledContent }) { const { contentItems: dataItems, errors: dataErrors } = await loadDataFiles({ dirPath, config, skipUnmodeledContent }); const { contentItems: pageItems, errors: pageErrors } = await loadPageFiles({ dirPath, config, skipUnmodeledContent }); const contentItems = lodash_1.default.concat(dataItems, pageItems); const validationResult = (0, content_validator_1.validateContentItems)({ contentItems, config }); const errors = lodash_1.default.concat(dataErrors, pageErrors, validationResult.errors); return { valid: lodash_1.default.isEmpty(errors), contentItems: validationResult.value, errors: errors }; } exports.loadContent = loadContent; async function loadDataFiles({ dirPath, config, skipUnmodeledContent }) { const contentItems = []; const errors = []; // 'config' is a deprecated model type used to describe the models of // ssg-specific configuration files e.g., _config.yml for Jekyll and // config.toml for Hugo. const configModel = config.models.find(utils_2.isConfigModel); let configFilePath; if (configModel) { const result = await loadDataItemForConfigModel(dirPath, configModel, config); if (result.contentItem) { configFilePath = result.contentItem.__metadata.filePath; contentItems.push(result.contentItem); } if (result.error) { errors.push(result.error); } } // if user specifically set dataDir to null, opt-out from loading data files all-together if (config.dataDir === null) { return { contentItems, errors }; } // if dataDir was not set, assume empty string as root folder const dataDir = config.dataDir || ''; const absDataDirPath = path_1.default.join(dirPath, dataDir); const dataDirExists = await fs_extra_1.default.pathExists(absDataDirPath); if (!dataDirExists) { return { contentItems, errors }; } const objectTypeKey = config.objectTypeKey || 'type'; const excludedFiles = [...consts_1.EXCLUDED_COMMON_FILES]; const dataModels = config.models.filter(utils_2.isDataModel); if (dataDir === '') { excludedFiles.push(...consts_1.EXCLUDED_DATA_FILES); if (configFilePath) { excludedFiles.push(configFilePath); } if (config.publishDir) { excludedFiles.push(config.publishDir); } } let filePaths; try { filePaths = await readDirRecursivelyWithFilter(absDataDirPath, excludedFiles, consts_1.DATA_FILE_EXTENSIONS); } catch (error) { return { contentItems, errors: errors.concat(new content_errors_1.FolderReadError({ folderPath: dataDir, error: error })) }; } const result = await loadContentItems({ projectDir: dirPath, contentDir: dataDir, filePaths, models: dataModels, config: config, objectTypeKeyPath: objectTypeKey, modelTypeKeyPath: 'name', skipUnmodeledContent }); contentItems.push(...result.contentItems); errors.push(...result.errors); return { contentItems, errors }; } async function loadPageFiles({ dirPath, config, skipUnmodeledContent }) { const contentItems = []; const errors = []; // if user specifically set pagesDir to null, opt-out from loading page files all-together if (config.pagesDir === null) { return { contentItems, errors }; } // if pagesDir was not set, assume empty string as root folder const pagesDir = config.pagesDir || ''; const absPagesDirPath = path_1.default.join(dirPath, pagesDir); const pageDirExists = await fs_extra_1.default.pathExists(absPagesDirPath); if (!pageDirExists) { return { contentItems, errors }; } const pageLayoutKey = config.pageLayoutKey || 'layout'; const excludedFiles = lodash_1.default.castArray(config.excludePages || []).concat(consts_1.EXCLUDED_COMMON_FILES); const pageModels = config.models.filter(utils_2.isPageModel); if (pagesDir === '') { excludedFiles.push(...consts_1.EXCLUDED_MARKDOWN_FILES); if (config.publishDir) { excludedFiles.push(config.publishDir); } } let filePaths; try { filePaths = await readDirRecursivelyWithFilter(absPagesDirPath, excludedFiles, consts_1.SUPPORTED_FILE_EXTENSIONS); } catch (error) { return { contentItems, errors: errors.concat(new content_errors_1.FolderReadError({ folderPath: pagesDir, error: error })) }; } const result = await loadContentItems({ projectDir: dirPath, contentDir: pagesDir, filePaths, models: pageModels, config: config, objectTypeKeyPath: pageLayoutKey, modelTypeKeyPath: 'layout', skipUnmodeledContent }); contentItems.push(...result.contentItems); errors.push(...result.errors); return { contentItems, errors }; } async function loadDataItemForConfigModel(dirPath, configModel, config) { let filePath; if ('file' in configModel) { filePath = configModel.file; } else { filePath = await inferConfigFileFromSSGName(config, dirPath); } if (!filePath) { return { error: new content_errors_1.FileForModelNotFoundError({ modelName: configModel.name }) }; } const extension = path_1.default.extname(filePath).substring(1); if (!consts_1.DATA_FILE_EXTENSIONS.includes(extension)) { return { error: new content_errors_1.FileReadError({ filePath: filePath, error: new Error(`extension '${extension}' is not supported`) }) }; } const absFilePath = path_1.default.join(dirPath, filePath); const fileExists = await fs_extra_1.default.pathExists(absFilePath); if (!fileExists) { return {}; } try { const data = await loadFile(absFilePath); return { contentItem: modeledDataItem(filePath, data, configModel, config) }; } catch (error) { return { error: new content_errors_1.FileReadError({ filePath: filePath, error: error }) }; } } async function readDirRecursivelyWithFilter(dirPath, excludedFiles, allowedExtensions) { return (0, utils_1.readDirRecursively)(dirPath, { filter: (filePath, stats) => { if (micromatch_1.default.isMatch(filePath, excludedFiles)) { return false; } // return true for all directories to read them recursively if (!stats.isFile()) { return true; } const extension = path_1.default.extname(filePath).substring(1); return allowedExtensions.includes(extension); } }); } /** * Loads files from the provided `filePaths` relative to the directory produced by * joining the `projectDir` and `contentDir`. * * @param options * @param options.projectDir Absolute path of project directory * @param options.contentDir Directory within project directory from where to load files * @param options.filePaths Array of file paths to load, files paths must be relative to contentDir * @param options.models Array of models * @param options.config Config * @param options.objectTypeKeyPath The key path of object field to match a model * @param options.modelTypeKeyPath The key path of model property to match an object * @param options.skipUnmodeledContent Don't return un-modeled data */ async function loadContentItems({ projectDir, contentDir, filePaths, models, config, objectTypeKeyPath, modelTypeKeyPath, skipUnmodeledContent }) { const absContentDir = path_1.default.join(projectDir, contentDir); const contentItems = []; const errors = []; await (0, utils_1.forEachPromise)(filePaths, async (filePath) => { const absFilePath = path_1.default.join(absContentDir, filePath); const filePathRelativeToProject = path_1.default.join(contentDir, filePath); const fileIsInProjectDir = path_1.default.parse(filePathRelativeToProject).dir === ''; let data; try { data = await loadFile(absFilePath, fileIsInProjectDir); } catch (error) { errors.push(new content_errors_1.FileReadError({ filePath: filePathRelativeToProject, error: error })); return; } if (data === null) { return; } const matchedModels = (0, utils_2.getModelsByQuery)({ filePath: filePath, type: objectTypeKeyPath ? lodash_1.default.get(data, objectTypeKeyPath, null) : null, modelTypeKeyPath: modelTypeKeyPath }, models); if (matchedModels.length === 1) { contentItems.push(modeledDataItem(filePathRelativeToProject, data, matchedModels[0], config)); } else { if (matchedModels.length === 0) { errors.push(new content_errors_1.FileNotMatchedModelError({ filePath: filePathRelativeToProject })); } else { errors.push(new content_errors_1.FileMatchedMultipleModelsError({ filePath: filePathRelativeToProject, modelNames: lodash_1.default.map(matchedModels, 'name') })); } if (!skipUnmodeledContent) { contentItems.push(unmodeledDataItem(filePathRelativeToProject, data)); } } }); return { contentItems, errors }; } async function loadFile(filePath, fileIsInProjectDir = false) { let data = await (0, utils_1.parseFile)(filePath); const extension = path_1.default.extname(filePath).substring(1); // transform markdown files by unwrapping 'frontmatter' and renaming 'markdown' to 'markdown_content' // { frontmatter: { ...fields }, markdown: '...md...' } // => // { ...fields, markdown_content: '...md...' } if (consts_1.MARKDOWN_FILE_EXTENSIONS.includes(extension) && lodash_1.default.has(data, 'frontmatter') && lodash_1.default.has(data, 'markdown')) { if (fileIsInProjectDir && lodash_1.default.get(data, 'frontmatter') === null) { return null; } data = lodash_1.default.assign(data.frontmatter, { markdown_content: data.markdown }); } return data; } function modeledDataItem(filePath, data, model, config) { if ((0, utils_2.isPageModel)(model)) { if (model.hideContent) { data = lodash_1.default.omit(data, 'markdown_content'); } } const pageLayoutKey = config.pageLayoutKey || 'layout'; const objectTypeKey = config.objectTypeKey || 'type'; const modelsByName = lodash_1.default.keyBy(config.models, 'name'); data = addMetadataRecursively({ value: data, model, modelsByName, pageLayoutKey, objectTypeKey, valueId: filePath }); if ((0, utils_2.isListDataModel)(model) && lodash_1.default.isArray(data)) { data = { items: data }; } return { __metadata: { filePath, modelName: model.name }, ...data }; } function unmodeledDataItem(filePath, data) { return { __metadata: { filePath, modelName: null }, ...data }; } function addMetadataRecursively({ value, model, modelsByName, pageLayoutKey, objectTypeKey, valueId }) { if (!model) { return value; } function _mapDeep({ value, model, field, fieldListItem, valueKeyPath, modelKeyPath }) { let modelField = null; if (field && (0, utils_2.isModelField)(field)) { modelField = field; } else if (fieldListItem && (0, utils_2.isModelListItems)(fieldListItem)) { modelField = fieldListItem; } if (lodash_1.default.isPlainObject(value) && modelField) { const modelResult = (0, utils_2.getModelOfObject)({ object: value, field: modelField, modelsByName, pageLayoutKey, objectTypeKey, valueKeyPath, modelKeyPath }); if ('error' in modelResult) { return { __metadata: { modelName: null, error: modelResult.error }, ...value }; } model = modelResult.model; field = null; fieldListItem = null; modelKeyPath = [model.name]; value = { __metadata: { modelName: model.name }, ...value }; } // Use lodash methods here, the models and values can be invalid, and therefore not all required properties might exist if (lodash_1.default.isPlainObject(value)) { const modelOrField = model || field || fieldListItem; const fields = lodash_1.default.get(modelOrField, 'fields', []); const fieldsByName = lodash_1.default.keyBy(fields, 'name'); value = lodash_1.default.mapValues(value, (val, key) => { if (key === '__metadata') { return val; } // field might not be defined in the model, for example implicit fields like 'layout' and 'type' // or for nested objects with unmatched models const field = lodash_1.default.get(fieldsByName, key, null); return _mapDeep({ value: val, model: null, field: field, fieldListItem: null, valueKeyPath: lodash_1.default.concat(valueKeyPath, key), modelKeyPath: lodash_1.default.concat(modelKeyPath, ['fields', key]) }); }); } else if (lodash_1.default.isArray(value)) { let fieldListItems; if (field && (0, utils_2.isListField)(field)) { fieldListItems = (0, utils_2.getListFieldItems)(field); } else if (model && (0, utils_2.isListDataModel)(model)) { fieldListItems = model.items; } else { return value; } value = lodash_1.default.map(value, (val, idx) => { return _mapDeep({ value: val, model: null, field: null, fieldListItem: fieldListItems, valueKeyPath: lodash_1.default.concat(valueKeyPath, idx), modelKeyPath: lodash_1.default.concat(modelKeyPath, 'items') }); }); } return value; } return _mapDeep({ value: value, model: model, field: null, fieldListItem: null, valueKeyPath: valueId ? [valueId] : [], modelKeyPath: [model.name] }); } const configFilesSSGMap = { unibit: ['config.yaml', 'config.yml'], jekyll: ['_config.yml', '_config.yaml', '_config.toml'], hugo: ['config.yaml', 'config.yml', 'config.toml', 'config.json'], gatsby: ['site-metadata.json'] }; async function inferConfigFileFromSSGName(config, dirPath) { const ssgName = config.ssgName; if (!ssgName || !(ssgName in configFilesSSGMap)) { return; } const configFiles = configFilesSSGMap[ssgName]; if (!configFiles) { return; } return getFirstExistingFile(configFiles, dirPath); } function getFirstExistingFile(fileNames, inputDir) { return (0, utils_1.findPromise)(fileNames, (fileName) => { const absPath = path_1.default.resolve(inputDir, fileName); return fs_extra_1.default.pathExists(absPath); }); } //# sourceMappingURL=content-loader.js.map