a reactive (signals based) hypermedia web framework (wip)
stormlightlabs.github.io/volt/
hypermedia
frontend
signals
1import { echo } from "$console/echo.js";
2import { findMonorepoRoot, getLibPath } from "$utils/paths.js";
3import { existsSync } from "node:fs";
4import { mkdir, readFile, writeFile } from "node:fs/promises";
5import path from "node:path";
6import { minify as terserMinify } from "terser";
7
8export type BuildArtifacts = { jsPath: string; cssPath: string };
9
10export type BuildOptions = { outDir: string; minify?: boolean; includeCss?: boolean };
11
12/**
13 * Build the library using pnpm workspace commands.
14 *
15 * Runs `pnpm --filter volt build:lib` in the monorepo root to compile the Volt.js library into lib/dist/.
16 */
17export async function buildLibrary(): Promise<void> {
18 const { execSync } = await import("node:child_process");
19 const monorepoRoot = await findMonorepoRoot();
20
21 try {
22 execSync("pnpm --filter volt build:lib", { cwd: monorepoRoot, stdio: "inherit" });
23 } catch {
24 throw new Error("Library build failed. Make sure Vite is configured correctly.");
25 }
26}
27
28/**
29 * Find the library build artifacts in lib/dist/.
30 *
31 * Locates the compiled JavaScript and base CSS files after a successful build.
32 */
33export async function findBuildArtifacts(): Promise<BuildArtifacts> {
34 const libPath = await getLibPath();
35 const distDir = path.join(libPath, "dist");
36 const jsPath = path.join(distDir, "volt.js");
37 const cssPath = path.join(libPath, "src", "styles", "base.css");
38
39 if (!existsSync(jsPath)) {
40 throw new Error(`Library JS not found at ${jsPath}. Build may have failed.`);
41 }
42
43 if (!existsSync(cssPath)) {
44 throw new Error(`Base CSS not found at ${cssPath}.`);
45 }
46
47 return { jsPath, cssPath };
48}
49
50/**
51 * Minify JavaScript code using Terser.
52 *
53 * Applies aggressive compression and mangling to reduce bundle size.
54 */
55export async function minifyJS(code: string): Promise<string> {
56 const result = await terserMinify(code, {
57 compress: {
58 dead_code: true,
59 drop_debugger: true,
60 conditionals: true,
61 evaluate: true,
62 booleans: true,
63 loops: true,
64 unused: true,
65 hoist_funs: true,
66 keep_fargs: false,
67 hoist_vars: false,
68 if_return: true,
69 join_vars: true,
70 side_effects: true,
71 },
72 mangle: { toplevel: true },
73 format: { comments: false },
74 });
75
76 if (!result.code) {
77 throw new Error("Minification failed - no output generated");
78 }
79
80 return result.code;
81}
82
83/**
84 * Minify CSS code using regex-based compression.
85 *
86 * Removes comments, normalizes whitespace, and strips spacing around CSS syntax characters.
87 */
88export function minifyCSS(code: string): string {
89 return code.replaceAll(/\/\*[\s\S]*?\*\//g, "").replaceAll(/\s+/g, " ").replaceAll(/\s*([{}:;,])\s*/g, "$1").trim();
90}
91
92/**
93 * Copy build artifacts to the specified output directory.
94 *
95 * Reads the built JavaScript and CSS files, optionally minifies them, and writes them to the target directory.
96 * Creates the directory if needed.
97 * The output file naming depends on whether minification is enabled.
98 */
99export async function copyBuildArtifacts(artifacts: BuildArtifacts, options: BuildOptions): Promise<void> {
100 const { outDir, minify = true, includeCss = true } = options;
101
102 await mkdir(outDir, { recursive: true });
103
104 const jsContent = await readFile(artifacts.jsPath, "utf8");
105 const jsFilename = minify ? "volt.min.js" : "volt.js";
106 let outputJS = jsContent;
107
108 if (minify) {
109 echo.info(" Minifying JavaScript...");
110 outputJS = await minifyJS(jsContent);
111 }
112
113 const jsOutputPath = path.join(outDir, jsFilename);
114 await writeFile(jsOutputPath, outputJS, "utf8");
115 const jsSize = Math.round(outputJS.length / 1024);
116 echo.ok(` Created: ${jsFilename} (${jsSize} KB)`);
117
118 if (includeCss) {
119 const cssContent = await readFile(artifacts.cssPath, "utf8");
120 const cssFilename = minify ? "volt.min.css" : "volt.css";
121 let outputCSS = cssContent;
122
123 if (minify) {
124 echo.info(" Minifying CSS...");
125 outputCSS = minifyCSS(cssContent);
126 }
127
128 const cssOutputPath = path.join(outDir, cssFilename);
129 await writeFile(cssOutputPath, outputCSS, "utf8");
130 const cssSize = Math.round(outputCSS.length / 1024);
131 echo.ok(` Created: ${cssFilename} (${cssSize} KB)`);
132 }
133}