@miyagi/core
Version:
miyagi is a component development tool for JavaScript template engines.
763 lines (712 loc) • 23.1 kB
JavaScript
const fs = require("fs");
const path = require("path");
const helpers = require("../helpers.js");
const render = require("../render/index.js");
const log = require("../logger.js");
const appConfig = require("../config.json");
/**
* Module for creating a static build
*
* @module build
* @param {object} app - the express instance
*/
module.exports = (app) => {
return new Promise((resolve, reject) => {
const config = { ...app.get("config") };
const { build } = config;
const buildFolder = build.folder;
const date = new Date();
const utcDate = new Date(date.getTime() + date.getTimezoneOffset() * 60000);
const buildDate = `${utcDate.getFullYear()}-${(utcDate.getMonth() + 1)
.toString()
.padStart(
2,
"0"
)}-${utcDate.getDate()} ${utcDate.getHours()}:${utcDate.getMinutes()}:${utcDate.getSeconds()}Z`;
const formattedBuildDate = `${utcDate.getFullYear()}/${(
utcDate.getMonth() + 1
)
.toString()
.padStart(
2,
"0"
)}/${utcDate.getDate()} ${utcDate.getHours()}:${utcDate.getMinutes()}:${utcDate.getSeconds()} UTC`;
config.ui.validations.accessibility = false;
config.ui.validations.html = false;
app.set("config", config);
fs.rm(path.resolve(buildFolder), { recursive: true }, () => {
fs.mkdir(path.resolve(buildFolder), { recursive: true }, async () => {
const promises = [];
promises.push(
new Promise((resolve, reject) => {
buildDistDirectory(buildFolder).then(resolve).catch(reject);
})
);
promises.push(
new Promise((resolve, reject) => {
buildUserFavicon(buildFolder, app.get("config").ui.theme.favicon)
.then(resolve)
.catch(reject);
})
);
promises.push(
new Promise((resolve, reject) => {
buildUserAssets(buildFolder, app.get("config").assets, [
app.get("config").ui.theme.dark.logo,
app.get("config").ui.theme.light.logo,
])
.then(resolve)
.catch(reject);
})
);
promises.push(
new Promise((resolve, reject) => {
buildComponentAssets(
app.get("state").components,
app.get("config").components.folder
)
.then(resolve)
.catch(reject);
})
);
promises.push(
new Promise((resolve, reject) => {
buildIframeIndex(buildFolder, app).then(resolve).catch(reject);
})
);
promises.push(
new Promise((resolve, reject) => {
buildIndex(buildFolder, app, buildDate, formattedBuildDate)
.then(resolve)
.catch(reject);
})
);
const partials = Object.keys(app.get("state").partials);
const readMes = Object.keys(app.get("state").fileContents)
.filter((file) => file.endsWith("/README.md"))
.map((file) =>
file
.replace(
path.join(
process.cwd(),
app.get("config").components.folder,
"/"
),
""
)
.replace("/README.md", "")
)
.filter((file) => file !== "README.md");
const files = partials.map((partial) => {
return {
file: partial,
dir: path.dirname(partial),
};
});
readMes.forEach((readMe) => {
if (!files.find((file) => file.dir === readMe)) {
files.push({
file: null,
dir: readMe,
});
}
});
const paths = [];
for (const { file, dir } of files) {
const component = await buildComponent({
file,
dir,
buildFolder,
app,
buildDate,
formattedBuildDate,
});
if (component) {
for (const path of getFilePathsForJsonOutput(component)) {
paths.push(path);
}
}
}
Promise.all(promises)
.then(async () => {
if (app.get("config").build.outputFile) {
await createJsonOutputFile(paths, () => {
log("success", appConfig.messages.buildDone);
resolve();
});
} else {
log("success", appConfig.messages.buildDone);
resolve();
}
})
.catch((err) => {
console.log(err);
log("error", appConfig.messages.buildFailed);
reject();
});
});
});
/**
* Creates an "output.json" file with the given array as content
*
* @param {Array} paths - all paths to standalone views of component variations
* @param {Function} cb
*/
function createJsonOutputFile(paths, cb) {
fs.writeFile(
path.join(buildFolder, "output.json"),
JSON.stringify(
paths.map((path) => {
return {
path,
};
}),
null,
2
),
cb
);
}
/**
* Accepts an array with arrays and returns its values with cwd and buildFolder
*
* @param {Array} component - an array containing arrays with file paths
* @returns {Array} the flattened arrays
*/
function getFilePathsForJsonOutput(component) {
const paths = [];
for (const entries of component) {
if (entries) {
for (const file of entries) {
if (file) {
paths.push(
file.replace(path.join(process.cwd(), buildFolder, "/"), "")
);
}
}
}
}
return paths;
}
/**
* Copies the user favicon
*
* @param {string} buildFolder - the build folder from the user configuration
* @param {string} faviconPath - the favicon path from the user configuration
* @returns {Promise} gets resolved after the favicon has been copied
*/
function buildUserFavicon(buildFolder, faviconPath) {
return new Promise((resolve) => {
if (faviconPath) {
fs.cp(
path.join(process.cwd(), faviconPath),
`${buildFolder}/favicon.ico`,
{ recursive: true },
resolve
);
} else {
resolve();
}
});
}
/**
* Copies the dist directory
*
* @param {string} buildFolder - the build folder from the user configuration
* @returns {Promise} gets resolved when the dist directory has been copied
*/
function buildDistDirectory(buildFolder) {
return new Promise((resolve) =>
fs.cp(
path.join(__dirname, "../../dist/"),
`${buildFolder}/miyagi/`,
{
recursive: true,
},
resolve
)
);
}
async function buildComponentAssets(components, componentsFolder) {
const promises = [];
["css", "js"].forEach((type) => {
components.forEach(({ assets }) => {
if (assets[type]) {
promises.push(
new Promise((resolve) => {
fs.cp(
path.join(process.cwd(), componentsFolder, assets[type]),
path.join(process.cwd(), buildFolder, assets[type]),
{
recursive: true,
},
resolve
);
})
);
}
});
});
return Promise.all(promises);
}
/**
* Copies the user assets
*
* @param {string} buildFolder - the build folder from the user configuration
* @param {object} assetsConfig - the assets object from the user configuration
* @param {Array} logoPaths - the logo paths from the user configuration
* @returns {Promise} gets resolved when all assets have been copied
*/
async function buildUserAssets(buildFolder, assetsConfig, logoPaths) {
const promises = [];
for (const folder of assetsConfig.folder) {
promises.push(
new Promise((resolve) => {
fs.cp(
path.resolve(path.join(assetsConfig.root, folder)),
path.join(process.cwd(), buildFolder, folder),
{
recursive: true,
},
resolve
);
})
);
}
if (logoPaths) {
logoPaths.forEach((logoPath) => {
if (
logoPath &&
assetsConfig.folder.filter((entry) => logoPath.startsWith(entry))
.length === 0
) {
promises.push(
new Promise((resolve) =>
fs.cp(
path.resolve(logoPath),
path.join(buildFolder, logoPath),
{ recursive: true },
resolve
)
)
);
}
});
}
const cssJsFiles = [
...new Set([
...assetsConfig.css.map((file) =>
path.join(
assetsConfig.root,
typeof file === "string" ? file : file.src
)
),
...assetsConfig.js.map((file) =>
path.join(
assetsConfig.root,
typeof file === "string" ? file : file.src
)
),
...assetsConfig.customProperties.files.map((file) =>
path.join(assetsConfig.root, file)
),
]),
];
for (const file of cssJsFiles) {
if (
assetsConfig.folder.filter((entry) => file.startsWith(entry))
.length === 0
) {
promises.push(
new Promise((resolve) =>
fs.cp(
file,
path.join(buildFolder, path.relative(assetsConfig.root, file)),
{ recursive: true },
resolve
)
)
);
}
}
return Promise.all(promises);
}
/**
* Rendeers and builds the component overview
*
* @param {string} buildFolder - the build folder from the user configuration
* @param {object} app - the express instance
* @returns {Promise} gets resolved when the view has been rendered
*/
function buildIframeIndex(buildFolder, app) {
const promises = [];
for (const embedded of [false, true]) {
promises.push(
new Promise((resolve, reject) => {
render
.renderIframeIndex({
app,
res: app,
cb: (err, response) => {
if (err) {
if (typeof err === "string") {
log("error", err);
} else if (err.message) {
log("error", err.message);
}
reject();
} else {
fs.writeFile(
path.resolve(
`${buildFolder}/component-all${
embedded ? "-embedded" : ""
}.html`
),
response,
(err) => {
if (err) {
if (typeof err === "string") {
log("error", err);
} else if (err.message) {
log("error", err.message);
}
reject();
} else {
resolve();
}
}
);
}
},
})
.catch(reject);
})
);
}
return Promise.all(promises);
}
/**
* Renders and builds the index view
*
* @param {string} buildFolder - the build folder from the user configuration
* @param {object} app - the express instance
* @param {string} buildDate - a machine readable date time string of the current build
* @param {string} formattedBuildDate - a human readable date time string of the current build
* @returns {Promise} gets resolved when the view has been rendered
*/
function buildIndex(buildFolder, app, buildDate, formattedBuildDate) {
return new Promise((resolve, reject) => {
render.renderMainIndex({
app,
res: app,
buildDate,
formattedBuildDate,
cb: (err, response) => {
if (err) {
if (typeof err === "string") {
log("error", err);
} else if (err.message) {
log("error", err.message);
}
reject();
} else {
fs.writeFile(
path.resolve(`${buildFolder}/index.html`),
response,
(err) => {
if (err) {
if (typeof err === "string") {
log("error", err);
} else if (err.message) {
log("error", err.message);
}
reject();
} else {
resolve();
}
}
);
}
},
});
});
}
/**
* Renders and builds a variation
*
* @param {object} object - parameter object
* @param {string} object.buildFolder - the build folder from the user configuration
* @param {object} object.app - the express instance
* @param {string} object.file - the template file path
* @param {string} object.normalizedFileName - the normalized template file path
* @param {string} object.variation - the variation name
* @param {string} object.buildDate - a date time string of the current build
* @param {string} object.formattedBuildDate - a formatted date time string of the current build
* @returns {Promise} gets resolved when all variation views have been rendered
*/
function buildVariation({
buildFolder,
app,
file,
normalizedFileName,
variation,
buildDate,
formattedBuildDate,
}) {
return new Promise((res, rej) => {
const promises = [];
for (const embedded of [false, true]) {
promises.push(
new Promise((resolve, reject) => {
const fileName = path.resolve(
`${buildFolder}/component-${normalizedFileName}-variation-${helpers.normalizeString(
variation
)}${embedded ? "-embedded" : ""}.html`
);
render.renderIframeVariation({
app,
res: app,
file: path.dirname(file),
variation,
embedded,
cb: (err, response) => {
if (err) {
if (typeof err === "string") {
log("error", err);
} else if (err.message) {
log("error", err.message);
}
reject();
} else {
fs.writeFile(fileName, response, (err) => {
if (err) {
if (typeof err === "string") {
log("error", err);
} else if (err.message) {
log("error", err.message);
}
reject();
} else {
if (embedded) {
resolve(null);
} else {
resolve(fileName);
}
}
});
}
},
});
})
);
}
promises.push(
new Promise((resolve, reject) => {
render.renderMainComponent({
app,
res: app,
file: path.dirname(file),
variation,
buildDate,
formattedBuildDate,
cb: (err, response) => {
if (err) {
if (typeof err === "string") {
log("error", err);
} else if (err.message) {
log("error", err.message);
}
reject();
} else {
fs.writeFile(
path.resolve(
`${buildFolder}/show-${normalizedFileName}-variation-${helpers.normalizeString(
variation
)}.html`
),
response,
(err) => {
if (err) {
if (typeof err === "string") {
log("error", err);
} else if (err.message) {
log("error", err.message);
}
reject();
} else {
resolve();
}
}
);
}
},
});
})
);
return Promise.all(promises).then(res).catch(rej);
});
}
/**
* Renders and builds a variation
*
* @param {object} object - parameter object
* @param {string} object.file - the template file path
* @param {string} object.dir - the directory of the component
* @param {string} object.buildFolder - the build folder from the user configuration
* @param {object} object.app - the express instance
* @param {string} object.buildDate - a date time string of the current build
* @param {string} object.formattedBuildDate - a formatted date time string of the current build
* @returns {Promise} gets resolved when all component views have been rendered
*/
function buildComponent({
file,
dir,
buildFolder,
app,
buildDate,
formattedBuildDate,
}) {
return new Promise((res, rej) => {
const promises = [];
const normalizedFileName = helpers.normalizeString(dir);
const data =
app.get("state").fileContents[
helpers.getDataPathFromTemplatePath(
app,
helpers.getFullPathFromShortPath(app, file)
)
];
promises.push(
new Promise((resolve, reject) => {
render.renderMainComponent({
app,
res: app,
file: dir,
variation: null,
buildDate,
formattedBuildDate,
cb: (err, response) => {
if (err) {
if (typeof err === "string") {
log("error", err);
} else if (err.message) {
log("error", err.message);
}
reject();
} else {
fs.writeFile(
path.resolve(
`${buildFolder}/show-${normalizedFileName}.html`
),
response,
(err) => {
if (err) {
if (typeof err === "string") {
log("error", err);
} else if (err.message) {
log("error", err.message);
}
reject();
} else {
resolve();
}
}
);
}
},
});
})
);
for (const embedded of [false, true]) {
promises.push(
new Promise((resolve, reject) => {
render.renderIframeComponent({
app,
res: app,
file: dir,
noCli: embedded,
cb: (err, response) => {
if (err) {
if (typeof err === "string") {
log("error", err);
} else if (err.message) {
log("error", err.message);
}
reject();
} else {
fs.writeFile(
path.resolve(
`${buildFolder}/component-${normalizedFileName}${
-embedded ? "-embedded" : ""
}.html`
),
response,
(err) => {
if (err) {
if (typeof err === "string") {
log("error", err);
} else if (err.message) {
log("error", err.message);
}
reject();
} else {
resolve();
}
}
);
}
},
});
})
);
}
if (file) {
let variations = [];
if (data) {
const dataWithoutInternalKeys = helpers.removeInternalKeys(data);
if (
!data.$hidden &&
Object.keys(dataWithoutInternalKeys).length > 0
) {
variations.push({
$name: data.$name || appConfig.defaultVariationName,
...dataWithoutInternalKeys,
});
}
if (data.$variants) {
variations = [...variations, ...data.$variants];
}
} else {
variations.push({
$name: appConfig.defaultVariationName,
});
}
for (const variation of variations) {
const name = variation.$name;
if (!name) break;
promises.push(
new Promise((resolve, reject) =>
buildVariation({
buildFolder,
app,
file,
normalizedFileName,
variation: name,
buildDate,
formattedBuildDate,
})
.then((fileName) => {
resolve(fileName);
})
.catch(() => {
reject();
})
)
);
}
}
return Promise.all(promises).then(res).catch(rej);
});
}
});
};