diff --git a/src/build.ts b/src/build.ts index 5dc2ea4..55225d7 100644 --- a/src/build.ts +++ b/src/build.ts @@ -1,258 +1,275 @@ import { build, BuildContext, BuildResult, context } from "esbuild"; import { promises as fs } from "fs"; import * as chokidar from "chokidar"; import { default as glob } from "tiny-glob"; import { generate_css_includes } from "./find-css-includes.js"; import { sleep } from "./utils/sleep.js"; import { generateCollections } from "./generate-collections.js"; import { generateRoutes } from "./generate-routes.js"; import { generateComponents } from "./generate-components.js"; import { make_notifier } from "./notifier.js"; import getPort from "get-port"; import { removeNamedImports } from "../remove-named-imports.js"; import md5 from "md5"; import _locreq from "locreq"; import path, { basename, extname } from "path"; import { copyFile, writeFile } from "fs/promises"; const target_locreq = _locreq(process.cwd()); async function get_notifier_port() { return getPort({ port: 4000 }); } async function write_notifier_config(watch: boolean, port?: number) { const config = { watch } as Record<string, boolean | number>; if (port) { config.port = port; } await fs.writeFile( target_locreq.resolve("public/dist/notifier.json"), JSON.stringify(config) ); } function addSuffix(filename: string, suffix: string) { return filename.replace(/(\.[a-zA-Z0-9]+)?$/, suffix + "$1"); } async function makeResourceURL(file_path: string) { const hash_base = file_path + (await fs.stat(file_path)).mtimeMs; const hash = md5(hash_base); const public_url = "dist/" + addSuffix(basename(file_path), "-" + hash.slice(0, 8)); const public_dir = "public"; await copyFile( file_path, target_locreq.resolve(path.resolve(public_dir, public_url)) ); return "/" + public_url; } async function build_ts() { await Promise.all([ generateCollections(), generateRoutes(), generateComponents(), ]); const embeddable_file_extensions = ["svg", "png", "jpg", "jpeg", "webp"]; const entryPoints = ( await glob( `./src/back/**/*.{ts,tsx,${embeddable_file_extensions.join(",")}}` ) ).filter((path) => !path.includes(".#")); try { await build({ entryPoints, sourcemap: true, bundle: false, jsxFactory: "TempstreamJSX.createElement", outdir: "./dist/back", logLevel: "info", platform: "node", target: "es2022", format: "esm", loader: Object.fromEntries( embeddable_file_extensions.map((ext) => ["." + ext, "js"]) ), plugins: [ { name: "sealgen-rewrite-asset-imports", setup(build) { build.onLoad( { filter: /\.tsx?$/ }, async (args: any) => { let contents = await fs.readFile( args.path, "utf8" ); for (const ext of embeddable_file_extensions) { + const regex = new RegExp( + `^import (\\w+|{[^}]+}) from "([^"]+.${ext})";`, + "gm" + ); contents = contents.replaceAll( - new RegExp( - `^import (\w+|{[^}]+}) from "([^"]+.${ext})";`, - "gm" - ), - (line) => - line.replace(`.${ext}"`, '.js"') + regex, + (line) => { + return line.replace( + `.${ext}"`, + '.js"' + ); + } ); } contents = removeNamedImports(contents); return { contents, loader: extname(args.path) === ".tsx" ? "tsx" : "ts", }; } ); }, }, { name: "sealgen-load-assets", setup(build) { build.onLoad( { filter: new RegExp( `\\.(${embeddable_file_extensions.join( "|" )})$` ), }, async (args: any) => { const mode = "url"; let contents = `export const url = "${await makeResourceURL( args.path )}"; import fs from "fs"; -let content_promise = null; +let buffer_promise = null; + +export const getBuffer = function(){ + if(buffer_promise) return buffer_promise; + buffer_promise = fs.promises.readFile("${args.path}"); + return buffer_promise; +} -export const content = function(){ - if(content_promise) return content_promise; - content_promise = fs.promises.readFile("${args.path}", "utf-8"); - return content_promise; +export const getContent = async function(){ + const buffer = await getBuffer(); + return buffer.toString("utf-8"); } + +export const getBase64 = async function(){ + const buffer = await getBuffer(); + return buffer.toString("base64"); +} + +export default { getContent, url, getBase64, getBuffer }; `; return { contents, }; } ); }, }, ], }); } catch (e) { // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access console.error(e.message); } } async function complex_build(watch: boolean): Promise<void> { const loader = Object.fromEntries( (<const>["png", "svg", "jpg", "gif", "jpeg", "otf", "ttf"]).map( (ext) => <const>["." + ext, "file"] ) ); let css_build: BuildContext = await context({ entryPoints: ["./src/main.css"], sourcemap: true, bundle: true, outdir: "./public/dist", logLevel: "info", // plugins: [sassPlugin()], loader, }); let ongoing_ts = false; let ongoing_css = false; let ongoing_ts_promise: Promise<any> | null = null; if (watch) { const watcher = chokidar.watch("src", { ignoreInitial: true }); const port = await get_notifier_port(); const notifier = make_notifier(port); await write_notifier_config(watch, port); watcher.on("all", async (_, path) => { if (!css_build) return; if (path.includes(".#")) return; if (path.endsWith(".css") && !path.endsWith("/includes.css")) { if (ongoing_css) { return; } ongoing_css = true; // refresh the list of all css files in includes.css await generate_css_includes(); try { await css_build?.rebuild?.(); console.log(`Built main.css [on ${path}]`); } catch (e) { // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access console.error(e.message); await sleep(200); css_build ?.rebuild?.() // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access .catch((e) => console.error(e?.message)); } if (ongoing_ts_promise) { await ongoing_ts_promise; } notifier("css"); ongoing_css = false; } if ( (path.endsWith(".ts") || path.endsWith(".tsx")) && !path.endsWith("src/back/collections/collections.ts") && !path.endsWith("src/back/routes/routes.ts") && !path.endsWith("src/back/routes/urls.ts") ) { if (ongoing_ts) { return; } ongoing_ts = true; ongoing_ts_promise = build_ts(); await ongoing_ts_promise; notifier("ts"); ongoing_ts_promise = null; console.log(`Finished TS build [on ${path}]`); ongoing_ts = false; } }); } else { await write_notifier_config(watch); } await generate_css_includes(); await Promise.all([css_build.rebuild(), build_ts()]); if (watch) { css_build?.rebuild?.(); } else { css_build.dispose(); } } export async function buildProject({ watch, }: { watch: boolean; }): Promise<void> { try { await build({ entryPoints: ["./src/front/index.ts"], sourcemap: true, outfile: "./public/dist/bundle.js", logLevel: "info", bundle: true, }); await complex_build(watch); } catch (e) { console.log("CAUGHT!"); // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access console.error(e.message); if (!watch) { process.exit(1); } } }