a reactive (signals based) hypermedia web framework (wip)
stormlightlabs.github.io/volt/
hypermedia
frontend
signals
1import { echo } from "$console/echo.js";
2import { type BuildArtifacts, buildLibrary, copyBuildArtifacts, findBuildArtifacts } from "$utils/build.js";
3import { getExamplesPath } from "$utils/paths.js";
4import { mkdir, readFile, writeFile } from "node:fs/promises";
5import path from "node:path";
6
7export type BuildMode = "markup" | "programmatic";
8
9/**
10 * Create minified build artifacts in examples/dist/
11 */
12async function createMinifiedArtifacts(artifacts: BuildArtifacts, examplesDir: string): Promise<void> {
13 const examplesDistDir = path.join(examplesDir, "dist");
14 await copyBuildArtifacts(artifacts, { outDir: examplesDistDir, minify: true, includeCss: true });
15}
16
17function generateHTML(name: string, mode: BuildMode, standalone: boolean): string {
18 const cssPath = standalone ? "volt.min.css" : "../dist/volt.min.css";
19 const jsPath = standalone ? "volt.min.js" : "../dist/volt.min.js";
20
21 if (mode === "markup") {
22 return `<!DOCTYPE html>
23<html lang="en">
24<head>
25 <meta charset="UTF-8">
26 <meta name="viewport" content="width=device-width, initial-scale=1.0">
27 <title>${name} - Volt.js Example</title>
28 <link rel="stylesheet" href="${cssPath}">
29</head>
30<body>
31 <div data-volt data-volt-state='{"message": "Hello Volt!"}'>
32 <h1 data-volt-text="message">Loading...</h1>
33 <!-- Add your HTML here with data-volt-* attributes -->
34 </div>
35
36 <script type="module">
37 import { charge, registerPlugin, persistPlugin, scrollPlugin, urlPlugin } from './${jsPath}';
38
39 // Register plugins
40 registerPlugin('persist', persistPlugin);
41 registerPlugin('scroll', scrollPlugin);
42 registerPlugin('url', urlPlugin);
43
44 // Initialize Volt roots
45 charge();
46 </script>
47</body>
48</html>
49`;
50 }
51
52 return `<!DOCTYPE html>
53<html lang="en">
54<head>
55 <meta charset="UTF-8">
56 <meta name="viewport" content="width=device-width, initial-scale=1.0">
57 <title>${name} - Volt.js Example</title>
58 <link rel="stylesheet" href="${cssPath}">
59 <link rel="stylesheet" href="app.css">
60</head>
61<body>
62 <div id="app">
63 <h1>${name}</h1>
64 <!-- Add your HTML here with data-volt-* attributes -->
65 </div>
66
67 <script type="module" src="${jsPath}"></script>
68 <script type="module" src="app.js"></script>
69</body>
70</html>
71`;
72}
73
74function generateREADME(name: string): string {
75 return `# ${name}
76
77## Description
78
79Brief description of what this example demonstrates.
80
81## Features
82
83- Feature 1
84- Feature 2
85- Feature 3
86
87## Running the Example
88
891. Make sure the project is built: \`pnpm build\` from the root
902. Open \`index.html\` in a browser
913. Or use a local server: \`python3 -m http.server 8000\`
92
93## Code Highlights
94
95Explain key parts of the implementation here.
96`;
97}
98
99function generateAppJS(): string {
100 return `// Import volt.js functions if needed
101// import { mount, signal, computed, effect } from '../dist/volt.min.js';
102
103// Add your custom JavaScript here
104// Example:
105// const state = {
106// count: signal(0)
107// };
108
109// mount(document.querySelector('#app'), state);
110`;
111}
112
113function generateAppCSS(): string {
114 return "/* Add your custom styles here */\n";
115}
116
117/**
118 * Create example directory with all files
119 */
120async function createExampleFiles(
121 exampleDir: string,
122 name: string,
123 mode: BuildMode,
124 standalone: boolean,
125): Promise<void> {
126 await mkdir(exampleDir, { recursive: true });
127
128 const files = [{ path: "index.html", content: generateHTML(name, mode, standalone) }, {
129 path: "README.md",
130 content: generateREADME(name),
131 }];
132
133 if (mode === "programmatic") {
134 files.push({ path: "app.js", content: generateAppJS() }, { path: "app.css", content: generateAppCSS() });
135 }
136
137 for (const file of files) {
138 const filePath = path.join(exampleDir, file.path);
139 await writeFile(filePath, file.content, "utf8");
140 echo.ok(` Created: examples/${name}/${file.path}`);
141 }
142}
143
144async function copyStandaloneFiles(examplesDir: string, exampleDir: string): Promise<void> {
145 const distDir = path.join(examplesDir, "dist");
146 const jsSource = path.join(distDir, "volt.min.js");
147 const cssSource = path.join(distDir, "volt.min.css");
148
149 const jsDest = path.join(exampleDir, "volt.min.js");
150 const cssDest = path.join(exampleDir, "volt.min.css");
151
152 const jsContent = await readFile(jsSource, "utf8");
153 const cssContent = await readFile(cssSource, "utf8");
154
155 await writeFile(jsDest, jsContent, "utf8");
156 await writeFile(cssDest, cssContent, "utf8");
157
158 echo.ok(` Copied: volt.min.js (${Math.round(jsContent.length / 1024)} KB)`);
159 echo.ok(` Copied: volt.min.css (${Math.round(cssContent.length / 1024)} KB)`);
160}
161
162/**
163 * Example (generator) command implementation
164 *
165 * Creates a new example scaffold with minified volt.js build artifacts
166 */
167export async function exampleCommand(
168 name: string,
169 options: { mode?: BuildMode; standalone?: boolean } = {},
170): Promise<void> {
171 const mode = options.mode || "programmatic";
172 const standalone = options.standalone || false;
173
174 const examplesDir = await getExamplesPath();
175 const exampleDir = path.join(examplesDir, name);
176
177 echo.title(`\nCreating example: ${name}\n`);
178 echo.info(`Mode: ${mode}`);
179 echo.info(`Standalone: ${standalone ? "Yes" : "No (shared)"}\n`);
180
181 echo.info("Building Volt.js library...");
182 await buildLibrary();
183
184 echo.info("Finding build artifacts...");
185 const artifacts = await findBuildArtifacts();
186
187 echo.info("Creating minified build artifacts...");
188 await createMinifiedArtifacts(artifacts, examplesDir);
189
190 echo.info(`\nScaffolding example files...`);
191 await createExampleFiles(exampleDir, name, mode, standalone);
192
193 if (standalone) {
194 echo.info("\nCopying standalone files...");
195 await copyStandaloneFiles(examplesDir, exampleDir);
196 }
197
198 echo.success(`\nExample created successfully!\n`);
199 echo.info(`Location: examples/${name}/`);
200 echo.info(`Next steps:`);
201
202 if (mode === "markup") {
203 echo.text(` 1. Edit examples/${name}/index.html to add your UI with data-volt-* attributes`);
204 echo.text(` 2. Open examples/${name}/index.html in a browser\n`);
205 } else {
206 echo.text(` 1. Edit examples/${name}/index.html to add your UI`);
207 echo.text(` 2. Edit examples/${name}/app.js to add your logic`);
208 echo.text(` 3. Open examples/${name}/index.html in a browser\n`);
209 }
210}