@stackbit/sdk
Version:
412 lines • 16.6 kB
JavaScript
"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