import { build } from "esbuild"; import { resolve, relative, basename, dirname } from "node:path"; import glob from "tiny-glob"; import { promises as fs } from "fs"; import { walkDir } from "../utils/walk.js"; import { Builder } from "./builder.js"; export class CSSClump { public files: string[] = []; constructor( public project_dir: string, public name: string ) {} addFile(full_path: string) { this.files.push(full_path); } async build() { await this.makeEntrypoint(); const loader = Object.fromEntries( ([ "png", "svg", "jpg", "gif", "jpeg", "otf", "ttf", "woff", "woff2", ]).map((ext) => ["." + ext, "file"]) ); const result = await build({ entryPoints: [relative(this.project_dir, this.getEntrypointPath())], sourcemap: true, bundle: true, outdir: "./public/dist", logLevel: "info", loader, minify: true, target: ["safari16.5"], // to make css nesting work without manually adding `&` metafile: true, }); await fs.writeFile( relative(this.project_dir, "public/dist") + "/" + this.name + ".meta.json", JSON.stringify(result.metafile) ); } getEntrypointPath(): string { return resolve( this.project_dir, `src/style-entrypoints/${this.name}.entrypoint.css` ); } async makeEntrypoint() { const entrypoint_path = this.getEntrypointPath(); await fs.mkdir(dirname(entrypoint_path), { recursive: true }); await fs.writeFile( entrypoint_path, [ `/* DO NOT EDIT! Automatically generated by sealgen */`, ...this.files .sort() .map( (fullpath) => `@import "${relative( dirname(entrypoint_path), fullpath )}";` ), ].join("\n") ); } static assignFileToClumps(file_path: string): string[] { const segments = basename(file_path).split("."); segments.shift(); // filename segments.pop(); // extension if (!segments.length) { if (file_path.includes("/jdd-components/")) { const component_name = file_path.match( /jdd-components\/([^/]+)\// )?.[1]; if (!component_name) { return []; } return ["jdd-components", "jdd-component__" + component_name]; } else { // no segments, default clump return ["default"]; } } if (segments.length == 1 && segments[0] == "entrypoint") { return []; } return segments; } static async findCSSClumps( project_dir: string, style_dirs: string[] ): Promise { const clumps: Record = {}; const dirs_to_scan = [resolve(project_dir, "src"), ...style_dirs]; const all_files = (await Promise.all(dirs_to_scan.map(walkDir))).flat(); for (const file of all_files) { if (!file.endsWith(".css")) continue; const clump_names = CSSClump.assignFileToClumps(file); for (const clump_name of clump_names) { if (!clump_name) continue; if (!clumps[clump_name]) { clumps[clump_name] = new CSSClump(project_dir, clump_name); } clumps[clump_name]?.addFile(file); } } return Object.values(clumps); } } export class CSSBuilder extends Builder { getName(): string { return "css"; } ownsFile(file_path: string) { return ( file_path.endsWith(".css") && !file_path.endsWith("/includes.css") ); } async dispose(): Promise { //noop } async _build() { try { const entrypoints = await glob(`./src/*.entrypoint.css`); await Promise.allSettled( entrypoints.map((rel_path) => fs.unlink(resolve(this.project_dir, rel_path)) ) ); const clumps = await CSSClump.findCSSClumps( this.project_dir, this.style_dirs ); await Promise.all(clumps.map((clump) => clump.build())); } catch (e) { console.error(e); } } }