Skip to content

Commit 71aca59

Browse files
authored
feat: implement CLI utilities and file watcher with debouncing functionality (#15)
1 parent ca5183c commit 71aca59

File tree

4 files changed

+618
-339
lines changed

4 files changed

+618
-339
lines changed

bin/cli-utils.ts

Lines changed: 353 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,353 @@
1+
import * as fs from "fs/promises";
2+
import * as path from "path";
3+
import {
4+
WgslTemplateError,
5+
WgslTemplateLoadError,
6+
WgslTemplateParseError,
7+
WgslTemplateGenerateError,
8+
WgslTemplateBuildError,
9+
} from "../src/index.js";
10+
import type { TemplateRepository, TemplatePass0, TemplatePass1, TemplatePass2 } from "../src/types.js";
11+
12+
/**
13+
* Common CLI options interface
14+
*/
15+
export interface CliOptions {
16+
help?: boolean;
17+
version?: boolean;
18+
input?: string;
19+
output?: string;
20+
generator?: string;
21+
ext?: string;
22+
includePrefix?: string;
23+
preserveCodeRef?: boolean;
24+
clean?: boolean;
25+
watch?: boolean;
26+
debounce?: number;
27+
verbose?: boolean;
28+
}
29+
30+
/**
31+
* Display source code context around an error line using the appropriate pipeline result
32+
*/
33+
export async function displaySourceContext(
34+
filePath: string,
35+
lineNumber: number,
36+
contextLines = 3,
37+
result?: {
38+
pass0?: TemplateRepository<TemplatePass0>;
39+
pass1?: TemplateRepository<TemplatePass1>;
40+
pass2?: TemplateRepository<TemplatePass2>;
41+
files?: TemplateRepository<string>;
42+
},
43+
errorType?: string,
44+
verbose = false
45+
): Promise<void> {
46+
try {
47+
let content: string | undefined;
48+
49+
// Determine which result to use based on error type
50+
if (errorType === "load" && result?.pass0) {
51+
// For loading errors, use pass0 (raw file content)
52+
const template = result.pass0.templates.get(filePath);
53+
content = template?.raw.join("\n");
54+
} else if (errorType === "parse" && result?.pass0) {
55+
// For parser errors, use pass0 (original content before parsing)
56+
const template = result.pass0.templates.get(filePath);
57+
content = template?.raw.join("\n");
58+
} else if (errorType === "generate" && result?.pass1) {
59+
// For generator errors, use pass1 (parsed content before generation)
60+
const template = result.pass1.templates.get(filePath);
61+
content = template?.pass1.join("\n");
62+
} else if (errorType === "build" && result?.pass2) {
63+
// For build errors, use pass2 (generated content before build)
64+
const template = result.pass2.templates.get(filePath);
65+
content = template?.generateResult.code;
66+
}
67+
68+
// If we don't have content from the pipeline, skip showing context
69+
if (!content) {
70+
console.error(` (Source content not available in pipeline results)`);
71+
return;
72+
}
73+
74+
const lines = content.split(/\r?\n/); // In verbose mode, show the entire file; otherwise show limited context
75+
const startLine = verbose ? 0 : Math.max(0, lineNumber - contextLines - 1);
76+
const endLine = verbose ? lines.length - 1 : Math.min(lines.length - 1, lineNumber + contextLines - 1);
77+
78+
console.error(verbose ? "\n📍 Complete Source Content:" : "\n📍 Source Context:");
79+
80+
for (let i = startLine; i <= endLine; i++) {
81+
const currentLineNumber = i + 1;
82+
const isErrorLine = currentLineNumber === lineNumber;
83+
const lineNumberStr = currentLineNumber.toString().padStart(4, " ");
84+
85+
if (isErrorLine) {
86+
console.error(`❌ ${lineNumberStr} | ${lines[i]}`);
87+
} else {
88+
console.error(` ${lineNumberStr} | ${lines[i]}`);
89+
}
90+
}
91+
} catch (error) {
92+
console.error(` (Unable to display source context: ${error instanceof Error ? error.message : String(error)})`);
93+
}
94+
}
95+
96+
/**
97+
* Display enhanced error information with source context
98+
*/
99+
export async function displayError(
100+
error: Error,
101+
verbose: boolean,
102+
result?: {
103+
pass0?: TemplateRepository<TemplatePass0>;
104+
pass1?: TemplateRepository<TemplatePass1>;
105+
pass2?: TemplateRepository<TemplatePass2>;
106+
files?: TemplateRepository<string>;
107+
}
108+
): Promise<void> {
109+
console.error("❌ Build failed:");
110+
// Verbose: Show error type and properties
111+
if (verbose) {
112+
console.error(`\n🐛 Verbose Information:`);
113+
console.error(` Error type: ${error.constructor.name}`);
114+
console.error(` Error name: ${error.name}`);
115+
if (error.stack) {
116+
console.error(` Stack trace:\n${error.stack}`);
117+
}
118+
console.error(` Error properties:`);
119+
for (const [key, value] of Object.entries(error)) {
120+
if (key !== "name" && key !== "message" && key !== "stack") {
121+
console.error(` ${key}: ${JSON.stringify(value)}`);
122+
}
123+
}
124+
}
125+
126+
// Show specific error information based on error type
127+
if (error instanceof WgslTemplateLoadError) {
128+
console.error(`🗂️ Loading Error: ${error.message}`);
129+
if (error.filePath) {
130+
console.error(` File: ${error.filePath}`);
131+
}
132+
if (error.lineNumber) {
133+
console.error(` Line: ${error.lineNumber}`);
134+
} // Show source context if we have file path and line number
135+
if (error.filePath && error.lineNumber) {
136+
await displaySourceContext(error.filePath, error.lineNumber, 3, result, "load", verbose);
137+
}
138+
} else if (error instanceof WgslTemplateParseError) {
139+
console.error(`📝 Parse Error: ${error.message}`);
140+
if (error.filePath) {
141+
console.error(` File: ${error.filePath}`);
142+
}
143+
if (error.lineNumber) {
144+
console.error(` Line: ${error.lineNumber}`);
145+
} // Show source context if we have file path and line number
146+
if (error.filePath && error.lineNumber) {
147+
await displaySourceContext(error.filePath, error.lineNumber, 3, result, "parse", verbose);
148+
}
149+
} else if (error instanceof WgslTemplateGenerateError) {
150+
console.error(`⚙️ Generation Error: ${error.message}`);
151+
if (error.filePath) {
152+
console.error(` File: ${error.filePath}`);
153+
}
154+
if (error.lineNumber) {
155+
console.error(` Line: ${error.lineNumber}`);
156+
} // Show source context if we have file path and line number
157+
if (error.filePath && error.lineNumber) {
158+
await displaySourceContext(error.filePath, error.lineNumber, 3, result, "generate", verbose);
159+
}
160+
} else if (error instanceof WgslTemplateBuildError) {
161+
console.error(`🏗️ Build Error: ${error.message}`);
162+
if (error.filePath) {
163+
console.error(` File: ${error.filePath}`);
164+
}
165+
} else if (error instanceof WgslTemplateError) {
166+
// Generic WGSL template error
167+
console.error(`🔧 Template Error: ${error.message}`);
168+
if (error.filePath) {
169+
console.error(` File: ${error.filePath}`);
170+
}
171+
if (error.lineNumber) {
172+
console.error(` Line: ${error.lineNumber}`);
173+
}
174+
} else {
175+
// Generic error
176+
console.error(`💥 Unexpected Error: ${error.message}`);
177+
}
178+
}
179+
180+
/**
181+
* Format file size for display
182+
*/
183+
export function formatFileSize(bytes: number): string {
184+
const units = ["B", "KB", "MB", "GB"];
185+
let size = bytes;
186+
let unitIndex = 0;
187+
188+
while (size >= 1024 && unitIndex < units.length - 1) {
189+
size /= 1024;
190+
unitIndex++;
191+
}
192+
193+
return unitIndex === 0 ? `${size} ${units[unitIndex]}` : `${size.toFixed(1)} ${units[unitIndex]}`;
194+
}
195+
196+
/**
197+
* Tree node structure for file display
198+
*/
199+
type TreeNode =
200+
| {
201+
type: "file";
202+
size: number;
203+
error?: string;
204+
}
205+
| {
206+
type: "directory";
207+
children: Record<string, TreeNode>;
208+
};
209+
210+
/**
211+
* Build a file tree structure from a flat map of files
212+
*/
213+
export function buildFileTree(files: Map<string, { error?: string; size: number }>): Record<string, TreeNode> {
214+
const tree: Record<string, TreeNode> = {};
215+
216+
for (const [filePath, fileInfo] of files) {
217+
const parts = filePath.split(/[/\\]/);
218+
let current = tree;
219+
220+
// Create directories
221+
for (let i = 0; i < parts.length - 1; i++) {
222+
const part = parts[i];
223+
if (!current[part]) {
224+
current[part] = { type: "directory", children: {} };
225+
}
226+
if (current[part].type === "directory") {
227+
current = current[part].children;
228+
}
229+
}
230+
231+
// Add file
232+
const fileName = parts[parts.length - 1];
233+
current[fileName] = {
234+
type: "file",
235+
size: fileInfo.size,
236+
error: fileInfo.error,
237+
};
238+
}
239+
240+
return tree;
241+
}
242+
243+
/**
244+
* Display a file tree with sizes and errors
245+
*/
246+
export function displayFileTree(files: Map<string, { error?: string; size: number }>): void {
247+
const tree = buildFileTree(files);
248+
249+
function printTree(node: TreeNode | Record<string, TreeNode>, prefix = "", isLast = true, name = ""): void {
250+
if (typeof node === "object" && "type" in node) {
251+
if (node.type === "file") {
252+
const sizeStr = formatFileSize(node.size);
253+
const errorStr = node.error ? ` ❌ ${node.error}` : "";
254+
console.log(`${prefix}${isLast ? "└── " : "├── "}📄 ${name} (${sizeStr})${errorStr}`);
255+
return;
256+
} else if (node.type === "directory") {
257+
if (name) {
258+
console.log(`${prefix}${isLast ? "└── " : "├── "}📁 ${name}/`);
259+
}
260+
const entries = Object.entries(node.children);
261+
entries.forEach(([key, value], index) => {
262+
const isLastEntry = index === entries.length - 1;
263+
const newPrefix = name ? prefix + (isLast ? " " : "│ ") : prefix;
264+
printTree(value, newPrefix, isLastEntry, key);
265+
});
266+
return;
267+
}
268+
}
269+
270+
// Handle root level
271+
const entries = Object.entries(node as Record<string, TreeNode>);
272+
entries.forEach(([key, value], index) => {
273+
const isLastEntry = index === entries.length - 1;
274+
printTree(value, prefix, isLastEntry, key);
275+
});
276+
}
277+
278+
printTree(tree);
279+
}
280+
281+
/**
282+
* Show help message
283+
*/
284+
export function showHelp(): void {
285+
console.log(`
286+
WGSL Template Generator CLI
287+
288+
Usage:
289+
npx wgsl-gen [options]
290+
291+
Options:
292+
--input, -i <dir> Source directory containing WGSL template files (required)
293+
--output, -o <dir> Output directory for generated files (required)
294+
--generator <n> Code generator to use (default: "static-cpp-literal")
295+
Available: "dynamic", "static-cpp", "static-cpp-literal"
296+
--ext <extension> Template file extension (default: ".wgsl.template")
297+
--include-prefix, -I <prefix> Include path prefix for generated headers
298+
--preserve-code-ref Preserve code references in generated output
299+
--clean, -c Clean output directory before building
300+
--watch, -w Watch for file changes and rebuild automatically
301+
--debounce <ms> Debounce delay for watch mode in milliseconds (default: 300)
302+
--verbose, -v Enable verbose output for detailed error information and file changes
303+
--help, -h Show this help message
304+
--version Show version information
305+
306+
Examples:
307+
npx wgsl-gen --input ./templates --output ./generated
308+
npx wgsl-gen --input ./shaders --output ./cpp --generator static-cpp
309+
npx wgsl-gen -i ./src -o ./build --include-prefix "myproject/"
310+
npx wgsl-gen -i ./templates -o ./generated --clean --verbose
311+
npx wgsl-gen -i ./templates -o ./generated --watch --debounce 500
312+
`);
313+
}
314+
315+
/**
316+
* Show version information
317+
*/
318+
export async function showVersion(): Promise<void> {
319+
try {
320+
// Read version from package.json
321+
const packageJsonPath = path.join(import.meta.dirname, "..", "package.json");
322+
const content = await fs.readFile(packageJsonPath, "utf8");
323+
const packageJson = JSON.parse(content);
324+
console.log(`WGSL Template Generator v${packageJson.version}`);
325+
} catch {
326+
console.log("WGSL Template Generator (version unknown)");
327+
}
328+
}
329+
330+
/**
331+
* Validate CLI options
332+
*/
333+
export function validateOptions(options: CliOptions): string[] {
334+
const errors: string[] = [];
335+
336+
if (!options.input) {
337+
errors.push("--input option is required");
338+
}
339+
340+
if (!options.output) {
341+
errors.push("--output option is required");
342+
}
343+
// Validate watch-specific options
344+
if (options.debounce !== undefined && !options.watch) {
345+
errors.push("--debounce option is only valid in watch mode (--watch)");
346+
}
347+
348+
if (options.debounce !== undefined && (options.debounce < 0 || !Number.isInteger(options.debounce))) {
349+
errors.push("--debounce must be a non-negative integer");
350+
}
351+
352+
return errors;
353+
}

0 commit comments

Comments
 (0)