@fs-eire/wgsl-template
Version:
A powerful template system for generating WGSL (WebGPU Shading Language) code with support for parameters, conditionals, and multiple output formats including C++ code generation.
140 lines • 6.62 kB
JavaScript
import { readdir, readFile, stat } from "node:fs/promises";
import * as path from "node:path";
import { WgslTemplateLoadError } from "./errors.js";
/**
* Recursively scans a directory and its subdirectories for template files with alias support.
*
* @param directory The directory to scan
* @param basePath The base directory path for calculating relative paths
* @param ext The file extension to look for
* @param templates The map to store loaded templates
* @param alias Optional alias to prepend to template names
*/
async function loadTemplatesRecursivelyWithAlias(directory, basePath, ext, templates, alias) {
try {
const entries = await readdir(directory);
for (const entry of entries) {
const fullPath = path.join(directory, entry);
const resolvedPath = path.resolve(fullPath);
// Security check: ensure the resolved path is within the base directory
const resolvedBasePath = path.resolve(basePath);
if (!resolvedPath.startsWith(resolvedBasePath + path.sep) && resolvedPath !== resolvedBasePath) {
console.warn(`Skipping file outside base directory: ${fullPath}`);
continue;
}
const entryStat = await stat(fullPath);
if (entryStat.isDirectory()) {
// Recursively process subdirectories
await loadTemplatesRecursivelyWithAlias(fullPath, basePath, ext, templates, alias);
}
else if (entryStat.isFile() && entry.endsWith(ext)) {
// Load template file with alias
await loadTemplateFileWithAlias(fullPath, basePath, templates, alias);
}
// Skip symbolic links and other special file types for security
}
}
catch (error) {
throw new WgslTemplateLoadError(`Error scanning directory ${directory}: ${error.message}`, "scan-directory", { cause: error });
}
}
/**
* Loads a single template file with alias support and adds it to the templates map.
*
* @param filePath The path to the template file
* @param basePath The base directory path for calculating relative paths
* @param templates The map to store the loaded template
* @param alias Optional alias to prepend to template name
*/
async function loadTemplateFileWithAlias(filePath, basePath, templates, alias) {
try {
const resolvedFilePath = path.resolve(filePath);
const resolvedBasePath = path.resolve(basePath);
// Security check: ensure the file is within the base directory
if (!resolvedFilePath.startsWith(resolvedBasePath + path.sep) && resolvedFilePath !== resolvedBasePath) {
throw new WgslTemplateLoadError(`Security violation: attempted to read file outside base directory: ${filePath}`, "read-file", { filePath });
}
const content = await readFile(filePath, "utf8");
const lines = content.split(/\r?\n/);
// Calculate relative path from base directory
const relativePath = path.relative(basePath, filePath);
// always use UNIX-style paths for consistency
let templateName = relativePath.replace(/\\/g, "/");
// Prepend alias if provided
if (alias) {
templateName = `${alias}/${templateName}`;
}
// Check for filename conflicts
if (templates.has(templateName)) {
throw new WgslTemplateLoadError(`Template name conflict: ${templateName} already exists`, "template-conflict", {
filePath,
});
}
const template = {
filePath: resolvedFilePath,
raw: lines,
};
templates.set(templateName, template);
}
catch (error) {
if (error instanceof WgslTemplateLoadError) {
throw error;
}
throw new WgslTemplateLoadError(`Error loading template file ${filePath}: ${error.message}`, "read-file", { filePath, cause: error });
}
}
/**
* NodeLoader is an implementation of the Loader interface that uses Node.js APIs
* to load template files from the filesystem. It provides methods to scan directories
* for template files and load their contents into memory.
*/
export const loader = {
/**
* Loads template files from a directory using Node.js filesystem APIs.
*
* @param directory The directory path to scan for template files
* @param options Optional configuration for loading templates
* @returns A promise that resolves to a TemplateRepository containing loaded templates
*/
async loadFromDirectory(directory, options) {
// Use loadFromDirectories as it handles the same functionality
return this.loadFromDirectories([directory], options);
},
/**
* Loads template files from multiple directories using Node.js filesystem APIs.
* Supports optional aliases to prevent filename conflicts.
*
* @param directories Array of directory paths or objects with path and alias
* @param options Optional configuration for loading templates
* @returns A promise that resolves to a TemplateRepository containing all loaded templates
*/
async loadFromDirectories(directories, options) {
const ext = options?.ext ?? ".wgsl.template";
const templates = new Map();
const resolvedBasePaths = [];
for (const dir of directories) {
const dirPath = typeof dir === "string" ? dir : dir.path;
const alias = typeof dir === "string" ? undefined : dir.alias;
// Ensure the directory exists
try {
const dirStat = await stat(dirPath);
if (!dirStat.isDirectory()) {
throw new WgslTemplateLoadError(`Path ${dirPath} is not a directory`, "scan-directory");
}
}
catch (error) {
throw new WgslTemplateLoadError(`Cannot access directory ${dirPath}: ${error.message}`, "scan-directory", { cause: error });
}
// Recursively load template files with alias support
await loadTemplatesRecursivelyWithAlias(dirPath, dirPath, ext, templates, alias);
resolvedBasePaths.push(path.resolve(dirPath));
}
// Use the first directory as the base path for simplicity
const basePath = resolvedBasePaths.length === 1 ? resolvedBasePaths[0] : resolvedBasePaths[0]; // For multiple directories, use the first one as base
return {
basePath,
templates: templates,
};
},
};
//# sourceMappingURL=loader-impl.js.map