Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 34 additions & 2 deletions bin/cli-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import type { TemplateRepository, TemplatePass0, TemplatePass1, TemplatePass2 }
export interface CliOptions {
help?: boolean;
version?: boolean;
input?: string;
input?: string | string[]; // Now supports multiple inputs
output?: string;
generator?: string;
ext?: string;
Expand Down Expand Up @@ -289,7 +289,9 @@ Usage:
npx wgsl-gen [options]

Options:
--input, -i <dir> Source directory containing WGSL template files (required)
--input, -i <dir>[:@<alias>] Source directory containing WGSL template files (required)
Can be specified multiple times for multiple directories
Optional alias: "dir:@alias" to prefix template names
--output, -o <dir> Output directory for generated files (required)
--generator <n> Code generator to use (default: "static-cpp-literal")
Available: "dynamic", "static-cpp", "static-cpp-literal"
Expand All @@ -309,6 +311,8 @@ Examples:
npx wgsl-gen -i ./src -o ./build --include-prefix "myproject/"
npx wgsl-gen -i ./templates -o ./generated --clean --verbose
npx wgsl-gen -i ./templates -o ./generated --watch --debounce 500
npx wgsl-gen -i ./common -i ./effects:@fx --output ./generated
npx wgsl-gen -i ./base -i ./shaders:@shaders -i ./utils:@utils -o ./cpp
`);
}

Expand Down Expand Up @@ -351,3 +355,31 @@ export function validateOptions(options: CliOptions): string[] {

return errors;
}

/**
* Parse input directories with optional aliases from CLI arguments
* Format: "dir1" or "dir1:@alias1" or ["dir1", "dir2:@alias2"]
*/
export function parseInputDirectories(input: string | string[]): ({ path: string; alias?: string } | string)[] {
const inputs = Array.isArray(input) ? input : [input];

return inputs.map((inputStr) => {
const parts = inputStr.split(":");
if (parts.length === 1) {
// No alias specified
return parts[0];
} else if (parts.length === 2) {
// Alias specified
const [dirPath, alias] = parts;
if (!alias.startsWith("@")) {
throw new Error(`Invalid alias format: ${alias}. Aliases must start with '@'. Example: "dir:@alias"`);
}
return {
path: dirPath,
alias: alias,
};
} else {
throw new Error(`Invalid input format: ${inputStr}. Expected "dir" or "dir:@alias"`);
}
});
}
55 changes: 42 additions & 13 deletions bin/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,15 @@

import minimist from "minimist";
import { build } from "../src/index.js";
import { displayError, displayFileTree, showHelp, showVersion, validateOptions, type CliOptions } from "./cli-utils.js";
import {
displayError,
displayFileTree,
showHelp,
showVersion,
validateOptions,
parseInputDirectories,
type CliOptions,
} from "./cli-utils.js";
import { TemplateWatcher } from "./watcher.js";
import * as fs from "fs/promises";
import * as path from "path";
Expand All @@ -20,14 +28,16 @@ async function main() {
w: "watch",
v: "verbose",
},
string: ["input", "output", "generator", "ext", "include-prefix", "debounce"],
string: ["output", "generator", "ext", "include-prefix", "debounce"],
boolean: ["help", "version", "clean", "preserve-code-ref", "watch", "verbose"],
// Allow multiple values for input
default: { input: [] },
});

const options: CliOptions = {
help: argv.help,
version: argv.version,
input: argv.input,
input: Array.isArray(argv.input) ? argv.input : argv.input ? [argv.input] : undefined,
output: argv.output,
generator: argv.generator,
ext: argv.ext,
Expand Down Expand Up @@ -65,17 +75,34 @@ async function main() {
const debounce = options.debounce || 300;

try {
// Check if source directory exists
const srcPath = path.resolve(options.input!);
// Parse input directories with aliases
const sourceDirs = parseInputDirectories(options.input!);
const outPath = path.resolve(options.output!);

try {
await fs.access(srcPath);
} catch {
console.error(`Error: Source directory "${srcPath}" does not exist`);
process.exit(1);
// Check if all source directories exist
for (const dir of sourceDirs) {
const dirPath = typeof dir === "string" ? dir : dir.path;
const resolvedPath = path.resolve(dirPath);
try {
await fs.access(resolvedPath);
} catch {
console.error(`Error: Source directory "${resolvedPath}" does not exist`);
process.exit(1);
}
}

// Convert to absolute paths for the build function
const absoluteSourceDirs = sourceDirs.map((dir) => {
if (typeof dir === "string") {
return path.resolve(dir);
} else {
return {
path: path.resolve(dir.path),
alias: dir.alias,
};
}
});

// Check if output path is an existing file (not allowed regardless of clean flag)
try {
const outStat = await fs.stat(outPath);
Expand All @@ -91,7 +118,7 @@ async function main() {
// Handle watch mode
if (options.watch) {
const watcher = new TemplateWatcher({
sourceDir: srcPath,
sourceDirs: absoluteSourceDirs,
outDir: outPath,
templateExt,
generator,
Expand Down Expand Up @@ -132,7 +159,9 @@ async function main() {
}

console.log(`Building WGSL templates...`);
console.log(` Source: ${srcPath}`);
console.log(
` Sources: ${absoluteSourceDirs.map((d) => (typeof d === "string" ? d : `${d.path} (${d.alias})`)).join(", ")}`
);
console.log(` Output: ${outPath}`);
console.log(` Generator: ${generator}`);
console.log(` Template extension: ${templateExt}`);
Expand All @@ -151,7 +180,7 @@ async function main() {

// Run the build
const result = await build({
sourceDir: srcPath,
sourceDirs: absoluteSourceDirs,
outDir: outPath,
templateExt,
generator,
Expand Down
44 changes: 31 additions & 13 deletions bin/watcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,20 +34,24 @@ export class TemplateWatcher {
}

/**
* Start watching the source directory for changes
* Start watching the source directories for changes
*/
async start(): Promise<void> {
const srcPath = path.resolve(this.options.sourceDir);

// Verify source directory exists
try {
await fs.access(srcPath);
} catch {
throw new Error(`Source directory "${srcPath}" does not exist`);
// Verify all source directories exist and get their absolute paths
const absoluteSourceDirs: string[] = [];
for (const dir of this.options.sourceDirs) {
const dirPath = typeof dir === "string" ? dir : dir.path;
const resolvedPath = path.resolve(dirPath);
try {
await fs.access(resolvedPath);
absoluteSourceDirs.push(resolvedPath);
} catch {
throw new Error(`Source directory "${resolvedPath}" does not exist`);
}
}

console.log(`🔍 Starting watch mode...`);
console.log(` Source: ${srcPath}`);
console.log(` Sources: ${absoluteSourceDirs.join(", ")}`);
console.log(` Output: ${path.resolve(this.options.outDir)}`);
console.log(` Template extension: ${this.options.templateExt}`);
console.log(` Debounce delay: ${this.options.debounce}ms`);
Expand All @@ -56,8 +60,10 @@ export class TemplateWatcher {
console.log(`\n⚡ Running initial build...`);
await this.runBuild();

// Start watching
await this.setupWatchers(srcPath);
// Start watching all source directories
for (const srcPath of absoluteSourceDirs) {
await this.setupWatchers(srcPath);
}

console.log(`\n👀 Watching for changes... (Press Ctrl+C to stop)`);
}
Expand Down Expand Up @@ -113,7 +119,19 @@ export class TemplateWatcher {
return;
}

const relativePath = path.relative(this.options.sourceDir, filePath);
// Find which source directory this file belongs to
let relativePath = filePath;
for (const dir of this.options.sourceDirs) {
const dirPath = typeof dir === "string" ? dir : dir.path;
const resolvedDirPath = path.resolve(dirPath);
if (filePath.startsWith(resolvedDirPath)) {
relativePath = path.relative(resolvedDirPath, filePath);
if (typeof dir !== "string" && dir.alias) {
relativePath = `${dir.alias}/${relativePath}`;
}
break;
}
}

if (this.options.verbose) {
const timestamp = new Date().toLocaleTimeString();
Expand All @@ -140,7 +158,7 @@ export class TemplateWatcher {
const startTime = Date.now();

const result = await build({
sourceDir: this.options.sourceDir,
sourceDirs: this.options.sourceDirs,
outDir: this.options.outDir,
templateExt: this.options.templateExt,
generator: this.options.generator,
Expand Down
11 changes: 9 additions & 2 deletions src/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,13 @@ export class WgslTemplateLoadError extends WgslTemplateError {
/**
* The type of load operation that failed.
*/
readonly operation: "read-file" | "scan-directory" | "resolve-path" | "file-not-found" | "permission-denied";
readonly operation:
| "read-file"
| "scan-directory"
| "resolve-path"
| "file-not-found"
| "permission-denied"
| "template-conflict";

constructor(
message: string,
Expand Down Expand Up @@ -180,7 +186,8 @@ export class WgslTemplateBuildError extends WgslTemplateError {
| "permission-denied"
| "disk-full"
| "path-security-violation"
| "output-validation-failed";
| "output-validation-failed"
| "invalid-options";

/**
* The target output path that caused the error, if applicable.
Expand Down
28 changes: 25 additions & 3 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,29 @@ import type { TemplateRepository, TemplatePass0, TemplatePass1, TemplatePass2 }
// Export error types
export * from "./errors.js";

// Export types
export type {
TemplateRepository,
TemplatePass0,
TemplatePass1,
TemplatePass2,
LoadFromDirectoryOptions,
} from "./types.js";

// Export loader functions
export { loader } from "./loader-impl.js";

/**
* Options for the build process.
*/
export interface BuildOptions {
/**
* The source root directory containing the source templates.
* Multiple source directories containing the source templates.
* Can be strings (directory paths) or objects with path and optional alias.
* When using aliases, template names will be prefixed with the alias.
*/
sourceDir: string;
sourceDirs: ({ path: string; alias?: string } | string)[];

/**
* The output directory where the generated files will be written.
*/
Expand Down Expand Up @@ -86,7 +101,14 @@ export const build = async (options: BuildOptions): Promise<BuildResult> => {
let files: TemplateRepository<string> | undefined;

try {
pass0 = await loader.loadFromDirectory(options.sourceDir, { ext: options.templateExt });
// Validate that sourceDirs is provided
if (!options.sourceDirs || options.sourceDirs.length === 0) {
throw new WgslTemplateBuildError("sourceDirs must be provided and cannot be empty", "invalid-options", {});
}

// Load templates using the multi-directory loader
pass0 = await loader.loadFromDirectories(options.sourceDirs, { ext: options.templateExt });

pass1 = parser.parse(pass0);

const codeGenerator = resolveCodeGenerator(options.generator);
Expand Down
Loading