+8
ROADMAP.md
+8
ROADMAP.md
···
161
161
- Web
162
162
- Tauri
163
163
- Wails
164
+
165
+
## Docs
166
+
167
+
- [ ] Document `charge()` bootstrap flow and declarative state/computed attributes (`data-volt-state`, `data-volt-computed:*`).
168
+
- [ ] Add async effect guide covering abort signals, debounce/throttle, retries, and `onError` handling.
169
+
- [ ] Write lifecycle instrumentation docs for `registerGlobalHook`, `registerElementHook`, `getElementBindings`, and plugin `context.lifecycle` callbacks.
170
+
- [ ] Explain `data-volt-bind:*` semantics, especially boolean attribute handling and dependency subscription behavior.
171
+
- [ ] Refresh README and overview content to use `data-volt-*` naming and reflect the current module layout.
+1
-1
cli/eslint.config.js
+1
-1
cli/eslint.config.js
+42
cli/src/commands/build.ts
+42
cli/src/commands/build.ts
···
1
+
import { echo } from "$console/echo.js";
2
+
import { buildLibrary, copyBuildArtifacts, findBuildArtifacts } from "$utils/build.js";
3
+
import path from "node:path";
4
+
5
+
export type BuildCommandOptions = { minify?: boolean; css?: boolean };
6
+
7
+
/**
8
+
* Build command implementation.
9
+
*
10
+
* Builds the Volt.js library and copies the build artifacts to the specified
11
+
* output directory. Supports optional minification and CSS inclusion.
12
+
*/
13
+
export async function buildCommand(outDir: string = ".", options: BuildCommandOptions = {}): Promise<void> {
14
+
const minify = options.minify !== false;
15
+
const includeCss = options.css !== false;
16
+
17
+
const resolvedOutDir = path.resolve(process.cwd(), outDir);
18
+
19
+
echo.title("\nBuilding Volt.js library...\n");
20
+
echo.info("Building library...");
21
+
await buildLibrary();
22
+
23
+
echo.info("Finding build artifacts...");
24
+
const artifacts = await findBuildArtifacts();
25
+
26
+
echo.info(`Copying to ${resolvedOutDir}...`);
27
+
await copyBuildArtifacts(artifacts, { outDir: resolvedOutDir, minify, includeCss });
28
+
29
+
echo.success("\nBuild completed successfully!\n");
30
+
31
+
if (minify) {
32
+
echo.info(`Output: ${outDir}${outDir.endsWith("/") ? "" : "/"}volt.min.js`);
33
+
if (includeCss) {
34
+
echo.info(` ${outDir}${outDir.endsWith("/") ? "" : "/"}volt.min.css\n`);
35
+
}
36
+
} else {
37
+
echo.info(`Output: ${outDir}${outDir.endsWith("/") ? "" : "/"}volt.js`);
38
+
if (includeCss) {
39
+
echo.info(` ${outDir}${outDir.endsWith("/") ? "" : "/"}volt.css\n`);
40
+
}
41
+
}
42
+
}
+4
-5
cli/src/commands/css-docs.ts
+4
-5
cli/src/commands/css-docs.ts
···
1
+
import { echo } from "$console/echo.js";
2
+
import { getDocsPath, getLibSrcPath } from "$utils/paths.js";
3
+
import { trackVersion } from "$versioning/tracker.js";
1
4
import { mkdir, readFile, writeFile } from "node:fs/promises";
2
5
import path from "node:path";
3
-
import { echo } from "../console/echo.js";
4
-
import { trackVersion } from "../versioning/tracker.js";
5
-
import { getLibSrcPath, getDocsPath } from "../utils/paths.js";
6
6
7
7
type CSSComment = { selector: string; comment: string };
8
8
···
265
265
continue;
266
266
}
267
267
268
-
lines.push(`### \`${comment.selector}\``, "");
269
-
lines.push(comment.comment, "");
268
+
lines.push(`### \`${comment.selector}\``, "", comment.comment, "");
270
269
}
271
270
272
271
return lines.join("\n");
+5
-5
cli/src/commands/docs.ts
+5
-5
cli/src/commands/docs.ts
···
1
+
import { echo } from "$console/echo.js";
2
+
import { getDocsPath, getLibSrcPath } from "$utils/paths.js";
3
+
import { trackVersion } from "$versioning/tracker.js";
1
4
import { mkdir, readdir, readFile, writeFile } from "node:fs/promises";
2
5
import path from "node:path";
3
6
import ts from "typescript";
4
-
import { echo } from "../console/echo.js";
5
-
import { trackVersion } from "../versioning/tracker.js";
6
-
import { getLibSrcPath, getDocsPath } from "../utils/paths.js";
7
7
8
8
type Member = { name: string; type: string; docs?: string };
9
9
···
31
31
return { description: "", examples: [] };
32
32
}
33
33
34
-
const comments = ranges.map((range) => fullText.substring(range.pos, range.end));
34
+
const comments = ranges.map((range) => fullText.slice(range.pos, range.end));
35
35
const jsdocComments = comments.filter((c) => c.trim().startsWith("/**"));
36
36
37
37
if (jsdocComments.length === 0) {
···
85
85
function extractFnSig(node: ts.FunctionDeclaration, sourceFile: ts.SourceFile): string {
86
86
const start = node.getStart(sourceFile);
87
87
const end = node.body ? node.body.getStart(sourceFile) : node.getEnd();
88
-
return sourceFile.text.substring(start, end).trim().replaceAll(/\s+/g, " ");
88
+
return sourceFile.text.slice(start, end).trim().replaceAll(/\s+/g, " ");
89
89
}
90
90
91
91
/**
+8
-97
cli/src/commands/example.ts
+8
-97
cli/src/commands/example.ts
···
1
-
import { existsSync } from "node:fs";
1
+
import { echo } from "$console/echo.js";
2
+
import { type BuildArtifacts, buildLibrary, copyBuildArtifacts, findBuildArtifacts } from "$utils/build.js";
3
+
import { getExamplesPath } from "$utils/paths.js";
2
4
import { mkdir, readFile, writeFile } from "node:fs/promises";
3
5
import path from "node:path";
4
-
import { minify as terserMinify } from "terser";
5
-
import { echo } from "../console/echo.js";
6
-
import { findMonorepoRoot, getLibPath, getExamplesPath } from "../utils/paths.js";
7
6
8
-
type BuildArtifacts = { jsPath: string; cssPath: string };
9
-
10
-
/**
11
-
* Build the library using pnpm workspace commands
12
-
*/
13
-
async function buildLibrary(): Promise<void> {
14
-
const { execSync } = await import("node:child_process");
15
-
const monorepoRoot = await findMonorepoRoot();
16
-
17
-
try {
18
-
execSync("pnpm --filter volt build:lib", { cwd: monorepoRoot, stdio: "inherit" });
19
-
} catch {
20
-
throw new Error("Library build failed. Make sure Vite is configured correctly.");
21
-
}
22
-
}
23
-
24
-
/**
25
-
* Find the library build artifacts in dist/
26
-
*/
27
-
async function findBuildArtifacts(): Promise<BuildArtifacts> {
28
-
const libPath = await getLibPath();
29
-
const distDir = path.join(libPath, "dist");
30
-
const jsPath = path.join(distDir, "volt.js");
31
-
const cssPath = path.join(libPath, "src", "styles", "base.css");
32
-
33
-
if (!existsSync(jsPath)) {
34
-
throw new Error(`Library JS not found at ${jsPath}. Build may have failed.`);
35
-
}
36
-
37
-
if (!existsSync(cssPath)) {
38
-
throw new Error(`Base CSS not found at ${cssPath}.`);
39
-
}
40
-
41
-
return { jsPath, cssPath };
42
-
}
43
-
44
-
async function minifyJS(code: string): Promise<string> {
45
-
const result = await terserMinify(code, {
46
-
compress: {
47
-
dead_code: true,
48
-
drop_debugger: true,
49
-
conditionals: true,
50
-
evaluate: true,
51
-
booleans: true,
52
-
loops: true,
53
-
unused: true,
54
-
hoist_funs: true,
55
-
keep_fargs: false,
56
-
hoist_vars: false,
57
-
if_return: true,
58
-
join_vars: true,
59
-
side_effects: true,
60
-
},
61
-
mangle: { toplevel: true },
62
-
format: { comments: false },
63
-
});
64
-
65
-
if (!result.code) {
66
-
throw new Error("Minification failed - no output generated");
67
-
}
68
-
69
-
return result.code;
70
-
}
71
-
72
-
// TODO: use terser
73
-
function minifyCSS(code: string): string {
74
-
return code.replaceAll(/\/\*[\s\S]*?\*\//g, "").replaceAll(/\s+/g, " ").replaceAll(/\s*([{}:;,])\s*/g, "$1").trim();
75
-
}
7
+
export type BuildMode = "markup" | "programmatic";
76
8
77
9
/**
78
10
* Create minified build artifacts in examples/dist/
79
11
*/
80
12
async function createMinifiedArtifacts(artifacts: BuildArtifacts, examplesDir: string): Promise<void> {
81
13
const examplesDistDir = path.join(examplesDir, "dist");
82
-
await mkdir(examplesDistDir, { recursive: true });
83
-
84
-
const jsContent = await readFile(artifacts.jsPath, "utf8");
85
-
const cssContent = await readFile(artifacts.cssPath, "utf8");
86
-
87
-
echo.info(" Minifying JavaScript...");
88
-
const minifiedJS = await minifyJS(jsContent);
89
-
90
-
echo.info(" Minifying CSS...");
91
-
const minifiedCSS = minifyCSS(cssContent);
92
-
93
-
const voltJSPath = path.join(examplesDistDir, "volt.min.js");
94
-
const voltCSSPath = path.join(examplesDistDir, "volt.min.css");
95
-
96
-
await writeFile(voltJSPath, minifiedJS, "utf8");
97
-
await writeFile(voltCSSPath, minifiedCSS, "utf8");
98
-
99
-
echo.ok(` Created: examples/dist/volt.min.js (${Math.round(minifiedJS.length / 1024)} KB)`);
100
-
echo.ok(` Created: examples/dist/volt.min.css (${Math.round(minifiedCSS.length / 1024)} KB)`);
14
+
await copyBuildArtifacts(artifacts, { outDir: examplesDistDir, minify: true, includeCss: true });
101
15
}
102
16
103
-
function generateHTML(name: string, mode: "markup" | "programmatic", standalone: boolean): string {
17
+
function generateHTML(name: string, mode: BuildMode, standalone: boolean): string {
104
18
const cssPath = standalone ? "volt.min.css" : "../dist/volt.min.css";
105
19
const jsPath = standalone ? "volt.min.js" : "../dist/volt.min.js";
106
20
···
182
96
`;
183
97
}
184
98
185
-
/**
186
-
* Generate app.js template
187
-
*/
188
99
function generateAppJS(): string {
189
100
return `// Import volt.js functions if needed
190
101
// import { mount, signal, computed, effect } from '../dist/volt.min.js';
···
209
120
async function createExampleFiles(
210
121
exampleDir: string,
211
122
name: string,
212
-
mode: "markup" | "programmatic",
123
+
mode: BuildMode,
213
124
standalone: boolean,
214
125
): Promise<void> {
215
126
await mkdir(exampleDir, { recursive: true });
···
255
166
*/
256
167
export async function exampleCommand(
257
168
name: string,
258
-
options: { mode?: "markup" | "programmatic"; standalone?: boolean } = {},
169
+
options: { mode?: BuildMode; standalone?: boolean } = {},
259
170
): Promise<void> {
260
171
const mode = options.mode || "programmatic";
261
172
const standalone = options.standalone || false;
+3
-4
cli/src/commands/stats.ts
+3
-4
cli/src/commands/stats.ts
···
1
+
import { echo } from "$console/echo.js";
2
+
import { findMonorepoRoot, getLibSrcPath, getLibTestPath } from "$utils/paths.js";
1
3
import { readdir, readFile, stat } from "node:fs/promises";
2
4
import path from "node:path";
3
-
import { echo } from "../console/echo.js";
4
-
import { getLibSrcPath, getLibTestPath, findMonorepoRoot } from "../utils/paths.js";
5
5
6
6
type FileStats = { path: string; lines: number; totalLines: number };
7
7
type DirectoryStats = { totalLines: number; codeLines: number; files: FileStats[] };
···
92
92
export async function statsCommand(includeFull: boolean): Promise<void> {
93
93
const monorepoRoot = await findMonorepoRoot();
94
94
const srcDir = await getLibSrcPath();
95
+
const srcStats = await collectStats(srcDir, monorepoRoot);
95
96
96
97
echo.title("\nVolt.js Code Statistics\n");
97
-
98
-
const srcStats = await collectStats(srcDir, monorepoRoot);
99
98
100
99
echo.label("Source Code (src/):");
101
100
echo.text(` Files: ${srcStats.files.length}`);
+21
-6
cli/src/index.ts
+21
-6
cli/src/index.ts
···
1
1
/* eslint-disable unicorn/no-process-exit */
2
+
import { buildCommand } from "$commands/build.js";
3
+
import { cssDocsCommand } from "$commands/css-docs.js";
4
+
import { docsCommand } from "$commands/docs.js";
5
+
import { type BuildMode, exampleCommand } from "$commands/example.js";
6
+
import { statsCommand } from "$commands/stats.js";
7
+
import { echo } from "$console/echo.js";
2
8
import { Command } from "commander";
3
-
import { cssDocsCommand } from "./commands/css-docs.js";
4
-
import { docsCommand } from "./commands/docs.js";
5
-
import { exampleCommand } from "./commands/example.js";
6
-
import { statsCommand } from "./commands/stats.js";
7
-
import { echo } from "./console/echo.js";
8
9
9
10
const program = new Command();
10
11
···
31
32
}
32
33
});
33
34
35
+
program.command("build [outDir]").description("Build the library and output to a directory").option(
36
+
"--no-minify",
37
+
"Skip minification (outputs volt.js instead of volt.min.js)",
38
+
).option("--no-css", "Skip CSS output").action(
39
+
async (outDir: string | undefined, options: { minify: boolean; css: boolean }) => {
40
+
try {
41
+
await buildCommand(outDir, options);
42
+
} catch (error) {
43
+
echo.err("Error building library:", error);
44
+
process.exit(1);
45
+
}
46
+
},
47
+
);
48
+
34
49
program.command("css-docs").description("Generate CSS documentation from base.css comments and variables").action(
35
50
async () => {
36
51
try {
···
49
64
"Example mode: markup (declarative) or programmatic (imperative)",
50
65
"programmatic",
51
66
).option("--standalone", "Create standalone example with local copies of volt.min.js and volt.min.css", false).action(
52
-
async (name: string, options: { mode: "markup" | "programmatic"; standalone: boolean }) => {
67
+
async (name: string, options: { mode: BuildMode; standalone: boolean }) => {
53
68
try {
54
69
await exampleCommand(name, options);
55
70
} catch (error) {
+133
cli/src/utils/build.ts
+133
cli/src/utils/build.ts
···
1
+
import { echo } from "$console/echo.js";
2
+
import { findMonorepoRoot, getLibPath } from "$utils/paths.js";
3
+
import { existsSync } from "node:fs";
4
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
5
+
import path from "node:path";
6
+
import { minify as terserMinify } from "terser";
7
+
8
+
export type BuildArtifacts = { jsPath: string; cssPath: string };
9
+
10
+
export 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
+
*/
17
+
export 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
+
*/
33
+
export 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
+
*/
55
+
export 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
+
*/
88
+
export 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
+
*/
99
+
export 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
+
}
+8
-1
cli/tsconfig.json
+8
-1
cli/tsconfig.json
···
14
14
"esModuleInterop": true,
15
15
"isolatedModules": true,
16
16
"verbatimModuleSyntax": true,
17
-
"skipLibCheck": true
17
+
"skipLibCheck": true,
18
+
"baseUrl": ".",
19
+
"paths": {
20
+
"$commands/*": ["./src/commands/*"],
21
+
"$utils/*": ["./src/utils/*"],
22
+
"$versioning/*": ["./src/versioning/*"],
23
+
"$console/*": ["./src/console/*"]
24
+
}
18
25
},
19
26
"include": ["src"]
20
27
}
+6
cli/tsdown.config.ts
+6
cli/tsdown.config.ts
+7
-1
dprint.json
+7
-1
dprint.json
···
1
1
{
2
2
"typescript": { "preferSingleLine": true, "jsx.bracketPosition": "sameLine" },
3
3
"json": { "preferSingleLine": true, "lineWidth": 121, "indentWidth": 2 },
4
-
"markup": { "preferAttrsSingleLine": true, "printWidth": 121, "styleIndent": true, "scriptIndent": true },
4
+
"markup": {
5
+
"preferAttrsSingleLine": true,
6
+
"printWidth": 121,
7
+
"styleIndent": true,
8
+
"scriptIndent": true,
9
+
"closingBracketSameLine": true
10
+
},
5
11
"excludes": ["**/node_modules"],
6
12
"plugins": [
7
13
"https://plugins.dprint.dev/typescript-0.95.8.wasm",