UNPKG

@miyagi/core

Version:

miyagi is a component development tool for JavaScript template engines.

1,021 lines (936 loc) 24.3 kB
import fs from "fs"; import path from "path"; import { writeFile, readdir } from "node:fs/promises"; import * as helpers from "../helpers.js"; import render from "../render/index.js"; import appConfig from "../default-config.js"; import { t } from "../i18n/index.js"; import log from "../logger.js"; import { getVariationData } from "../mocks/index.js"; /** * Module for creating a static build * @module build */ export default () => { return new Promise((resolve, reject) => { const config = { ...global.config }; const { build } = config; const buildFolder = build.folder; global.config = config; log("info", null, "Clearing build directory."); fs.rm(path.resolve(buildFolder), { recursive: true }, () => { log("info", null, "Creating build directory."); fs.mkdir(path.resolve(buildFolder), { recursive: true }, () => { const promises = []; promises.push( new Promise((res, rej) => { buildDistDirectory(buildFolder).then(res).catch(rej); }), ); promises.push( new Promise((res, rej) => { buildUserFavicon(buildFolder, global.config.ui.theme.favicon) .then(res) .catch(rej); }), ); promises.push( new Promise((res, rej) => { buildUserAssets(buildFolder, global.config.assets, [ global.config.ui.theme.logo.dark, global.config.ui.theme.logo.light, ]) .then(res) .catch(rej); }), ); promises.push( new Promise((res, rej) => { buildComponentAssets( buildFolder, global.state.components, global.config.components.folder, ) .then(res) .catch(rej); }), ); promises.push( new Promise((res, rej) => { buildIframeIndex(buildFolder).then(res).catch(rej); }), ); promises.push( new Promise((res, rej) => { buildIframeDesignTokens(buildFolder).then(res).catch(rej); }), ); promises.push( new Promise((res, rej) => { buildDesignTokens(buildFolder).then(res).catch(rej); }), ); promises.push( new Promise((res, rej) => { buildIndex(buildFolder).then(res).catch(rej); }), ); global.state.routes.forEach((route) => { if (route.type === "docs") { promises.push( new Promise((res, rej) => { buildDoc(route, buildFolder).then(res).catch(rej); }), ); } }); // const partials = Object.keys(global.state.partials); // const files = partials.map((partial) => { // return { // file: partial, // dir: path.dirname(partial), // }; // }); // Object.keys(global.state.fileContents).forEach((file) => { // if (file.endsWith("/README.md")) { // const filePath = path.dirname( // file.replace( // path.join(process.cwd(), global.config.components.folder, "/"), // "" // ) // ); // if (!files.find((file) => file.dir === filePath)) { // files.push({ // file: null, // dir: filePath, // }); // } // } // }); const paths = []; for (const route of global.state.routes) { if (route.type === "components") { promises.push( new Promise((res, rej) => { if (route.paths.tpl) { buildComponent({ component: route, buildFolder, }) .then((component) => { if (component) { for (const path of getFilePathsForJsonOutput( buildFolder, component, )) { paths.push(path); } } res(); }) .catch((err) => { rej(err); }); } else { buildComponentDocs({ component: route, buildFolder, }) .then((component) => { if (component) { for (const path of getFilePathsForJsonOutput( buildFolder, component, )) { paths.push(path); } } res(); }) .catch((err) => { rej(err); }); } }), ); } } Promise.all(promises) .then(async () => { await createJsonOutputFile(buildFolder, paths); resolve( t("buildDone").replace( "{{count}}", (await readdir(buildFolder, { recursive: true })).length, ), ); }) .catch((err) => { reject(err); }); }); }); }); /** * Creates an "output.json" file with the given array as content * @param {string} buildFolder * @param {Array} paths - all paths → standalone views of component variations */ async function createJsonOutputFile(buildFolder, paths) { log("info", null, `Writing ${path.join(buildFolder, "output.json")}.`); await writeFile( path.join(buildFolder, "output.json"), JSON.stringify( paths.map((path) => { return { path, }; }), null, 2, ), ); } /** * Accepts an array with arrays and returns its values with cwd and buildFolder * @param {string} buildFolder * @param {Array} component - an array containing arrays with file paths * @returns {Array} the flattened arrays */ function getFilePathsForJsonOutput(buildFolder, 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) { log( "info", null, `Copying ${faviconPath}${buildFolder}/favicon.ico.`, ); 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) { log( "info", null, `Copying ${path.join(import.meta.dirname, "../../dist/")}${path.join(buildFolder, "/miyagi")}`, ); return new Promise((resolve) => fs.cp( path.join(import.meta.dirname, "../../dist/"), `${path.join(process.cwd(), buildFolder, "/miyagi")}`, { recursive: true, }, resolve, ), ); } /** * @param {string} buildFolder * @param {Array} components * @param {string} componentsFolder * @returns {Promise} */ async function buildComponentAssets( buildFolder, components, componentsFolder, ) { const promises = []; ["css", "js"].forEach((type) => { components.forEach(({ assets }) => { if (assets[type]) { promises.push( new Promise((resolve) => { log( "info", null, `Copying ${path.join(componentsFolder, assets[type])}${path.join(buildFolder, assets[type])}.`, ); fs.cp( path.join(process.cwd(), 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) => { log( "info", null, `Copying ${path.join(assetsConfig.root, folder)}${path.join(buildFolder, folder)}`, ); 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) => { log( "info", null, `Copying ${path.resolve(logoPath)}${path.join(buildFolder, logoPath)}`, ); 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) => { log( "info", null, `Copying ${file}${path.join(buildFolder, file)}`, ); fs.cp( file, path.join(buildFolder, file), { recursive: true }, resolve, ); }), ); } } return Promise.all(promises); } /** * Rendeers and builds the component overview * @param {string} buildFolder - the build folder from the user configuration * @returns {Promise} gets resolved when the view has been rendered */ function buildIframeIndex(buildFolder) { const promises = []; for (const embedded of [false, true]) { promises.push( new Promise((resolve, reject) => { render .renderIframeIndex({ res: global.app, cb: async (err, response) => { if (err) { if (typeof err === "string") { reject(err); } else if (err.message) { reject(err.message); } } else { const filePath = path.join( buildFolder, embedded ? global.config.indexPath.embedded : global.config.indexPath.default, ); log("info", null, `Writing ${filePath}`); try { await writeFile(filePath, response); resolve(); } catch (err) { reject(err.message); } } }, }) .catch((e) => reject(e)); }), ); } return Promise.all(promises); } /** * @param {string} buildFolder - the build folder from the user configuration * @returns {Promise} gets resolved when the view has been rendered */ function buildIframeDesignTokens(buildFolder) { const promises = []; for (const embedded of [false, true]) { for (const type of ["colors", "sizes", "typography"]) { promises.push( new Promise((resolve, reject) => { render.iframe.designTokens[type]({ res: global.app, cb: async (err, response) => { if (err) { if (typeof err === "string") { reject(err); } else if (err.message) { reject(err.message); } } else { const filePath = path.join( `${buildFolder}/iframe-design-tokens-${type}${ embedded ? "-embedded" : "" }.html`, ); log("info", null, `Writing ${filePath}`); try { await writeFile(filePath, response); resolve(); } catch (err) { reject(err.message); } } }, }).catch((e) => reject(e)); }), ); } } return Promise.all(promises); } /** * @param {string} buildFolder - the build folder from the user configuration * @returns {Promise} gets resolved when the view has been rendered */ function buildDesignTokens(buildFolder) { const promises = []; for (const type of ["colors", "sizes", "typography"]) { promises.push( new Promise((resolve, reject) => { render.renderMainDesignTokens({ res: global.app, type, cb: async (err, response) => { if (err) { if (typeof err === "string") { reject(err); } else if (err.message) { reject(err.message); } } else { const filePath = path.join( `${buildFolder}/design-tokens-${type}.html`, ); log("info", null, `Writing ${filePath}`); try { await writeFile(filePath, response); resolve(); } catch (err) { reject(err.message); } } }, }); }), ); } return Promise.all(promises); } /** * Renders and builds the index view * @param {string} buildFolder - the build folder from the user configuration * @returns {Promise} gets resolved when the view has been rendered */ function buildIndex(buildFolder) { return new Promise((resolve, reject) => { render.renderMainIndex({ res: global.app, cb: async (err, response) => { if (err) { if (typeof err === "string") { reject(err); } else if (err.message) { reject(err.message); } } else { const filePath = path.join(`${buildFolder}/index.html`); log("info", null, `Writing ${filePath}`); try { await writeFile(filePath, response); resolve(); } catch (err) { reject(err.message); } } }, }); }); } /** * Renders and builds a variation * @param {object} object - parameter object * @param {string} object.buildFolder - the build folder from the user configuration * @param {object} object.component * @param {string} object.variation - the variation name * @returns {Promise} gets resolved when all variation views have been rendered */ async function buildVariation({ buildFolder, component, variation }) { const data = (await getVariationData(component, variation)) || {}; return new Promise((res, rej) => { const promises = []; promises.push( new Promise((resolve, reject) => { const filePath = path.join( buildFolder, component.route.embedded.replace( "-embedded.html", `-variation-${helpers.normalizeString(variation)}-embedded.html`, ), ); render.renderIframeVariation({ res: global.app, component, variation, data, cb: async (err, response) => { if (err) { if (typeof err === "string") { reject(err); } else if (err.message) { reject(err.message); } } else { log("info", null, `Writing ${filePath}`); try { await writeFile(filePath, response); resolve(); } catch (err) { reject(err.message); } } }, }); }), ); promises.push( new Promise((resolve, reject) => { const fileName = component.route.default.replace( ".html", `-variation-${helpers.normalizeString(variation)}.html`, ); const filePath = path.join(buildFolder, fileName); render.renderIframeVariationStandalone({ res: global.app, component, componentData: data.resolved, cb: async (err, response) => { if (err) { if (typeof err === "string") { reject(err); } else if (err.message) { reject(err.message); } } else { log("info", null, `Writing ${filePath}`); try { await writeFile(filePath, response); resolve(fileName); } catch (err) { reject(err.message); } } }, }); }), ); promises.push( new Promise((resolve, reject) => { render.renderMainComponent({ res: global.app, component, variation, cb: async (err, response) => { if (err) { if (typeof err === "string") { reject(err); } else if (err.message) { reject(err.message); } } else { const filePath = path.join( buildFolder, component.route.default .replace("component-", "show-") .replace( ".html", `-variation-${helpers.normalizeString(variation)}.html`, ), ); log("info", null, `Writing ${filePath}`); try { await writeFile(filePath, response); resolve(); } catch (err) { reject(err.message); } } }, }); }), ); return Promise.all(promises).then(res).catch(rej); }); } /** * Renders and builds a variation * @param {object} object - parameter object * @param {object} object.component * @param {string} object.buildFolder - the build folder from the user configuration * @returns {Promise} gets resolved when all component views have been rendered */ function buildComponent({ component, buildFolder }) { return new Promise((res, rej) => { const promises = []; const data = global.state.fileContents[ component.paths.mocks.full(global.config.files.mocks.extension[0]) ] || global.state.fileContents[ component.paths.mocks.full(global.config.files.mocks.extension[1]) ]; promises.push( new Promise((resolve, reject) => { render.renderMainComponent({ res: global.app, component, variation: null, cb: async (err, response) => { if (err) { if (typeof err === "string") { reject(err); } else if (err.message) { reject(err.message); } } else { const filePath = path.join( buildFolder, component.route.default.replace("component-", "show-"), ); log("info", null, `Writing ${filePath}`); try { await writeFile(filePath, response); resolve(); } catch (err) { reject(err.message); } } }, }); }), ); for (const embedded of [false, true]) { if (component.paths.tpl) { promises.push( new Promise((resolve, reject) => { render.renderIframeComponent({ res: global.app, component, noCli: embedded, cb: async (err, response) => { if (err) { if (typeof err === "string") { reject(err); } else if (err.message) { reject(err.message); } } else { const filePath = path.join( buildFolder, component.route.default.replace( ".html", embedded ? "-embedded.html" : ".html", ), ); log("info", null, `Writing ${filePath}`); try { await writeFile(filePath, response); resolve(); } catch (err) { reject(err.message); } } }, }); }), ); } } if (component.paths.tpl) { 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, component, variation: name, }) .then((fileName) => { resolve(fileName); }) .catch((err) => { reject(err); }), ), ); } } return Promise.all(promises).then(res).catch(rej); }); } /** * Renders and builds a variation * @param {object} object - parameter object * @param {object} object.component * @param {string} object.buildFolder - the build folder from the user configuration * @returns {Promise} gets resolved when all component views have been rendered */ function buildComponentDocs({ component, buildFolder }) { return new Promise((res, rej) => { const promises = []; promises.push( new Promise((resolve, reject) => { render.renderMainComponentDocs({ res: global.app, component, cb: async (err, response) => { if (err) { if (typeof err === "string") { reject(err); } else if (err.message) { reject(err.message); } } else { const filePath = path.join( buildFolder, component.route.default.replace("component-", "show-"), ); log("info", null, `Writing ${filePath}`); try { await writeFile(filePath, response); resolve(); } catch (err) { reject(err.message); } } }, }); }), ); for (const embedded of [false, true]) { promises.push( new Promise((resolve, reject) => { render.renderIframeComponentDocs({ res: global.app, component, cb: async (err, response) => { if (err) { if (typeof err === "string") { reject(err); } else if (err.message) { reject(err.message); } } else { const filePath = path.join( buildFolder, component.route.default.replace( ".html", embedded ? "-embedded.html" : ".html", ), ); log("info", null, `Writing ${filePath}`); try { await writeFile(filePath, response); resolve(); } catch (err) { reject(err.message); } } }, }); }), ); } return Promise.all(promises).then(res).catch(rej); }); } /** * @param {object} doc * @param {string} buildFolder * @returns {Promise} */ function buildDoc(doc, buildFolder) { const normalizedFileName = helpers.normalizeString(doc.paths.dir.short); const promises = []; promises.push( new Promise((resolve, reject) => { render.renderMainDocs({ res: global.app, doc, cb: async (err, response) => { if (err) { if (typeof err === "string") { reject(err); } else if (err.message) { reject(err.message); } } else { const filePath = path.join( `${buildFolder}/show-${normalizedFileName}.html`, ); log("info", null, `Writing ${filePath}`); try { await writeFile(filePath, response); resolve(); } catch (err) { reject(err.message); } } }, }); }), ); for (const embedded of [false, true]) { promises.push( new Promise((resolve, reject) => { render.renderIframeDocs({ res: global.app, doc, cb: async (err, response) => { if (err) { if (typeof err === "string") { reject(err); } else if (err.message) { reject(err.message); } } else { const filePath = path.join( `${buildFolder}/component-${normalizedFileName}${ -embedded ? "-embedded" : "" }.html`, ); log("info", null, `Writing ${filePath}`); try { await writeFile(filePath, response); resolve(); } catch (err) { reject(err.message); } } }, }); }), ); } return Promise.all(promises); } };