@miyagi/core
Version:
miyagi is a component development tool for JavaScript template engines.
581 lines (505 loc) • 15.5 kB
JavaScript
const fs = require("fs");
const path = require("path");
const { promisify } = require("util");
const deepMerge = require("deepmerge");
const fileStat = promisify(fs.stat);
const config = require("../config.json");
const helpers = require("../helpers.js");
const log = require("../logger.js");
const {
extendTemplateData,
getDataForRenderFunction,
} = require("../render/helpers");
function getMergeMethod(method) {
const methods = {
combine: (target, source, options) => {
const destination = target.slice();
source.forEach((item, index) => {
if (options.isMergeableObject(item)) {
if (typeof destination[index] === "undefined") {
destination[index] = options.cloneUnlessOtherwiseSpecified(
item,
options
);
} else {
destination[index] = deepMerge(target[index], item, options);
}
} else {
destination[index] = options.cloneUnlessOtherwiseSpecified(
item,
options
);
}
});
return destination;
},
overwrite: (destinationArray, sourceArray) => sourceArray,
};
return methods[method];
}
module.exports =
/**
* @param {object} app - the express instance
* @param {object} data - the mock data object that will be passed into the component
* @param {object} [rootData] - the root mock data object
* @returns {Promise<object>} the resolved data object
*/
async function resolveData(app, data, rootData) {
let merged = rootData
? mergeRootDataWithVariationData(rootData, data)
: data;
let resolved;
merged = mergeWithGlobalData(app, merged);
resolved = await overwriteJsonLinksWithJsonData(app, merged);
resolved = await overwriteTplLinksWithTplContent(app, resolved);
resolved = await overwriteRenderKey(app, resolved);
return { merged, resolved };
};
/**
* @param {object} app - the express instance
* @param {object} data - the mock data object that will be passed into the component
* @returns {Promise} gets resolved with resolved data object
*/
async function overwriteJsonLinksWithJsonData(app, data) {
return new Promise((resolve) => iterateOverJsonData(app, data).then(resolve));
}
/**
* @param {object} app - the express instance
* @param {object} data - the mock data object that will be passed into the component
* @returns {Promise} gets resolved with resolved data object
*/
async function overwriteTplLinksWithTplContent(app, data) {
return new Promise((resolve) => iterateOverTplData(app, data).then(resolve));
}
/**
* @param {object} app - the express instance
* @param {object|Array|string|boolean} entry - a value from the mock data object
* @returns {Promise<object|Array|string|boolean>} the resolved value from the mock data object
*/
async function iterateOverTplData(app, entry) {
if (entry) {
if (
typeof entry === "string" ||
typeof entry === "number" ||
typeof entry === "boolean" ||
entry === null
) {
return entry;
}
if (entry instanceof Array) {
const o = [];
const promises = [];
entry.forEach((entry, i) => {
promises.push(
new Promise((resolve) => {
resolveTpl(app, entry)
.then((result) => iterateOverTplData(app, result))
.then((result) => {
o[i] = result;
resolve();
});
})
);
});
return Promise.all(promises).then(() => {
return o;
});
}
const o = { ...entry };
await Promise.all(
Object.keys(o).map(async (key) => {
o[key] = await resolveTpl(app, o[key]);
o[key] = await iterateOverTplData(app, o[key]);
return o[key];
})
);
return o;
}
return entry;
}
/**
* @param {object} app - the express instance
* @param {object|Array|string|boolean} entry - a value from the mock data object
* @returns {Promise<object|Array|string|boolean>} the resolved value from the mock data object
*/
async function iterateOverJsonData(app, entry) {
if (entry) {
if (
typeof entry === "string" ||
typeof entry === "number" ||
typeof entry === "boolean" ||
entry === null
) {
return entry;
}
if (entry instanceof Array) {
const o = [];
const promises = [];
entry.forEach((ent, i) => {
promises.push(
new Promise((resolve) => {
resolveJson(app, ent)
.then((result) => iterateOverJsonData(app, result))
.then((result) => {
o[i] = result;
resolve();
});
})
);
});
return Promise.all(promises).then(() => {
return o;
});
}
let o = {};
await Promise.all(
Object.entries({ ...entry }).map(async ([key, value]) => {
if (key === "$ref") {
let resolvedValue = await getRootOrVariantDataOfReference(app, value);
resolvedValue = await iterateOverJsonData(
app,
await resolveJson(app, resolvedValue)
);
o = { ...o, ...resolvedValue };
} else {
const resolvedValue = await iterateOverJsonData(
app,
await resolveJson(app, value)
);
o[key] = resolvedValue;
}
return true;
})
);
return o;
}
return entry;
}
/**
* @param {object} app - the express instance
* @param {object|Array|string|boolean} entry - a value from the mock data object
* @returns {Promise} gets resolved with the resolved value from the mock data object
*/
function resolveTpl(app, entry) {
return new Promise((resolve1) => {
if (entry) {
if (Array.isArray(entry)) {
const promises = [];
const arr = [...entry];
arr.forEach((o, i) => {
promises.push(
new Promise((resolve) => {
resolveTpl(app, o).then((res) => {
arr[i] = res;
resolve();
});
})
);
});
return Promise.all(promises).then(() => {
resolve1(arr);
});
}
if (
typeof entry === "string" ||
typeof entry === "number" ||
typeof entry === "boolean" ||
entry === null
) {
return resolve1(entry);
}
const promises = [];
let entries = { ...entry };
Object.entries(entries).forEach(async ([key, val]) => {
if (key !== "$tpl") {
promises.push(
new Promise((resolve) => {
resolveTpl(app, val).then((result) => {
entries[key] = result;
resolve();
});
})
);
}
});
return Promise.all(promises).then(async () => {
if (entries.$tpl) {
let data = { ...entries };
delete data.$tpl;
let filePath;
let fullFilePath;
if (entries.$tpl.startsWith("@")) {
const namespace = entries.$tpl.split("/")[0];
const resolvedNamespace =
app.get("config").engine.options.namespaces[namespace.slice(1)];
const stat = await fileStat(path.resolve(resolvedNamespace));
if (stat.isSymbolicLink()) {
filePath = `${entries.$tpl.replace(
namespace,
resolvedNamespace.replace(
path.join(app.get("config").components.folder, "/"),
""
),
""
)}/${helpers.getResolvedFileName(
app.get("config").files.templates.name,
path.basename(entries.$tpl)
)}.${app.get("config").files.templates.extension}`;
fullFilePath = helpers.getFullPathFromShortPath(app, filePath);
} else {
filePath = `${entries.$tpl.replace(
namespace,
resolvedNamespace.replace(
app.get("config").components.folder,
""
),
""
)}/${helpers.getResolvedFileName(
app.get("config").files.templates.name,
path.basename(entries.$tpl)
)}.${app.get("config").files.templates.extension}`.slice(1);
}
fullFilePath = helpers.getFullPathFromShortPath(app, filePath);
} else {
filePath = `${entries.$tpl}/${helpers.getResolvedFileName(
app.get("config").files.templates.name,
path.basename(entries.$tpl)
)}.${app.get("config").files.templates.extension}`;
fullFilePath = helpers.getFullPathFromShortPath(app, filePath);
}
fs.stat(fullFilePath, async function (err) {
if (err == null) {
data = await extendTemplateData(
app.get("config"),
data,
filePath
);
await app.render(
fullFilePath,
getDataForRenderFunction(app, data),
(err, html) => {
if (err)
log(
"warn",
config.messages.renderingTemplateFailed
.replace("{{filePath}}", filePath)
.replace("{{engine}}", app.get("config").engine.name)
);
resolve1(html);
}
);
} else if (err.code === "ENOENT") {
const msg = config.messages.templateDoesNotExist.replace(
"{{template}}",
filePath
);
log("error", msg);
resolve1(msg);
}
});
} else {
entries = await overwriteRenderKey(app, entries);
resolve1(entries);
}
});
}
return resolve1(entry);
});
}
/**
* @param {object} app - the express instance
* @param {object|Array|string|boolean} entry - a value from the mock data object
* @returns {Promise<object|Array|string|boolean>} the resolved value from the mock data object
*/
async function resolveJson(app, entry) {
if (entry !== null) {
if (Array.isArray(entry)) {
return entry;
}
if (
typeof entry === "string" ||
typeof entry === "number" ||
typeof entry === "boolean" ||
entry === null
) {
return entry;
}
if (entry === undefined) {
log("warn", config.messages.referencedMockFileNotFound);
return entry;
}
if (entry.$ref) {
const customData = helpers.cloneDeep(entry);
delete customData.$ref;
const resolvedJson = await getRootOrVariantDataOfReference(
app,
entry.$ref
);
return deepMerge(resolvedJson, customData);
}
}
return entry;
}
/**
* @param {object} app - the express instance
* @param {string} ref - the reference to another mock data
* @returns {object} the resolved data object
*/
async function getRootOrVariantDataOfReference(app, ref) {
let [shortVal, variation] = ref.split("#");
let val;
if (shortVal.startsWith("@")) {
const namespace = shortVal.split("/")[0];
const resolvedNamespace =
app.get("config").engine.options.namespaces[namespace.slice(1)];
const stat = await fileStat(path.resolve(resolvedNamespace));
if (stat.isSymbolicLink()) {
shortVal = shortVal.replace(
namespace,
resolvedNamespace.replace(
path.join(app.get("config").components.folder, "/"),
""
),
""
);
} else {
shortVal = shortVal.replace(
namespace,
resolvedNamespace.replace(app.get("config").components.folder, ""),
""
);
}
}
val = `${shortVal}/${app.get("config").files.mocks.name}.${
app.get("config").files.mocks.extension
}`;
const jsonFromData =
app.get("state").fileContents[helpers.getFullPathFromShortPath(app, val)];
if (jsonFromData) {
const embeddedJson = jsonFromData;
let variantJson = {};
const rootJson = helpers.removeInternalKeys(embeddedJson);
if (variation && embeddedJson.$variants && embeddedJson.$variants.length) {
const variant = embeddedJson.$variants.find((vari) => {
if (vari.$name) {
return (
helpers.normalizeString(vari.$name) ===
helpers.normalizeString(variation)
);
}
return false;
});
if (variant) {
variantJson = helpers.removeInternalKeys(variant);
} else {
log(
"warn",
config.messages.variationNotFound
.replace("{{variation}}", variation)
.replace("{{fileName}}", val)
);
}
}
return mergeRootDataWithVariationData(helpers.cloneDeep(rootJson), variantJson);
}
log(
"warn",
config.messages.fileNotFoundLinkIncorrect.replace("{{filePath}}", val)
);
return {};
}
/**
* @param {object} app - the express instance
* @param {object} data - the mock data object that will be passed into the component
* @returns {object} the resolved data object
*/
function overwriteRenderKey(app, data) {
let o;
if (data) {
if (Array.isArray(data)) {
o = [...data];
} else {
o = { ...data };
}
const entries = Object.entries(o);
for (const [key, val] of entries) {
if (key === "$render") {
let str = "";
for (const html of val) {
str += html;
}
o = str;
} else {
if (
typeof val == "string" ||
typeof val === "number" ||
typeof val === "boolean" ||
val === null
) {
o[key] = val;
} else if (Array.isArray(val)) {
val.forEach((v, i) => {
if (
typeof v == "string" ||
typeof v === "number" ||
typeof v === "boolean" ||
val === null
) {
o[key][i] = v;
} else {
o[key][i] = overwriteRenderKey(app, v);
}
});
} else {
o[key] = overwriteRenderKey(app, val);
}
}
}
}
return o;
}
/**
* @param {object} rootData - the root mock data of a component
* @param {object} variationData - a variation mock data of a component
* @returns {object} the merged data
*/
function mergeRootDataWithVariationData(rootData, variationData) {
if (!rootData) {
return variationData;
}
if (!variationData) {
return rootData;
}
const merged = deepMerge(rootData, variationData, {
customMerge: (key) => {
const options = variationData.$opts || rootData.$opts;
if (options) {
const option = options[key];
if (option) {
return getMergeMethod(option);
}
}
return undefined;
},
});
if (merged.$opts) {
delete merged.$opts;
}
return merged;
}
/**
* @param {object} app - the express instance
* @param {object} data - the mock data object that will be passed into the component
* @returns {object} the merged data object
*/
function mergeWithGlobalData(app, data) {
return {
...app.get("state").fileContents[
helpers.getFullPathFromShortPath(
app,
`data.${app.get("config").files.mocks.extension}`
)
],
...data,
};
}