import fs from "node:fs/promises"; import {constants} from "node:fs"; import path from "path"; import htmlMinifier from "html-minifier-terser"; import esbuild from "esbuild"; const parseComponent = async (file)=>{ const dir = path.dirname(file); let data = {}; if(path.extname(file) === ".neovan"){ data = await getNeovanData(file); }else{ data = await parseHtml(file); } fs.rm(path.join(dir, "tmp/"), {recursive: true, force: true}); return await createBundle(data); } const getNeovanData = async (index)=>{ const neovan = await fs.readFile(index, "utf-8"); const parentPath = path.dirname(index); const html = neovan.slice(neovan.indexOf("") + 10, neovan.indexOf("")); const css = neovan.slice(neovan.indexOf("")); const js = neovan.slice(neovan.indexOf("")); const cssFile = path.join(parentPath, `tmp/${path.basename(index, ".neovan")}.css`); const jsFile = path.join(parentPath, `tmp/${path.basename(index, ".neovan")}.js`); await fs.mkdir(path.join(parentPath, "tmp/")); await Promise.all([ css === "" ? null : fs.writeFile(cssFile, css), js === "" ? null : fs.writeFile(jsFile, js) ]); return { html: html, css: cssFile, js: jsFile, dir: parentPath }; } const parseHtml = async (index)=>{ const parentPath = path.dirname(index); const basename = path.basename(index, ".html"); const cssPath = path.join(parentPath, `${basename}.css`); const jsPath = path.join(parentPath, `${basename}.js`); const proms = [ fs.readFile(index, "utf-8"), fs.access(cssPath, constants.F_OK), fs.access(jsPath, constants.F_OK) ]; let [html, css, js] = await Promise.allSettled(proms); return { html: html.value, css: css.status === "fulfilled" ? cssPath : null, js: js.status === "fulfilled" ? jsPath : null, dir: parentPath }; } const createBundle = async (data)=>{ const entryPoints = []; if(data.css) entryPoints.push(data.css); if(data.js) entryPoints.push(data.js); data.html = await addComponents(data.html, data.dir); const esbuildProm = esbuild.build({ entryPoints: entryPoints, bundle: true, minify: true, write: false, outdir: "/" }); const htmlProm = htmlMinifier.minify(data.html, { collapseBooleanAttributes: true, collapseInlineTagWhitespace: true, collapseWhitespace: true, decodeEntities: true, html5: true, includeAutoGeneratedTags: false, noNewlinesBeforeTagClose: true, removeComments: true, useShortDoctype: true }); const [buildData, html] = await Promise.all([esbuildProm, htmlProm]); const comps = {html: html}; for(let i = 0; i < buildData.outputFiles.length; i++){ const ext = path.extname(buildData.outputFiles[i].path).replace(".", ""); comps[ext] = buildData.outputFiles[i].text; } return mergeFiles(comps); } const mergeFiles = (comps)=>{ let cssIndex = comps.html.indexOf(""); cssIndex = cssIndex < 0 ? 0 : cssIndex; comps.css = comps.css ? `` : ""; const html = `${comps.html.slice(0, cssIndex)}${comps.css}${comps.html.slice(cssIndex)}`; let jsIndex = html.indexOf(""); jsIndex = jsIndex < 0 ? html.length : jsIndex; comps.js = comps.js ? `` : ""; return `${html.slice(0, jsIndex)}${comps.js}${html.slice(jsIndex)}`; } const addComponents = async (html, dir)=>{ let importStart = 0; for(let i = 0; i < html.length; i++){ if(html[i] === "@"){ if(html[i-1] === "<"){ importStart = i + 1; }else if(html[i+1] === ">"){ const importString = html.substring(importStart, i).trim(); const comp = await parseComponent(path.join(dir, importString)) html = html.slice(0, importStart - 2) + comp + html.slice(i+2); } } } return html; } export default parseComponent;