Genearte .zig verions of templates to use in production.
This commit is contained in:
369
src/compile_tpls.zig
Normal file
369
src/compile_tpls.zig
Normal file
@@ -0,0 +1,369 @@
|
||||
// Build step for compiling Pug templates at build time
|
||||
//
|
||||
// Usage in build.zig:
|
||||
// const pugz = @import("pugz");
|
||||
// const compile_step = pugz.addCompileStep(b, .{
|
||||
// .name = "compile-templates",
|
||||
// .source_dirs = &.{"src/views", "src/pages"},
|
||||
// .output_dir = "generated",
|
||||
// });
|
||||
// exe.step.dependOn(&compile_step.step);
|
||||
|
||||
const std = @import("std");
|
||||
const fs = std.fs;
|
||||
const mem = std.mem;
|
||||
const Build = std.Build;
|
||||
const Step = Build.Step;
|
||||
const GeneratedFile = Build.GeneratedFile;
|
||||
|
||||
const zig_codegen = @import("tpl_compiler/zig_codegen.zig");
|
||||
const view_engine = @import("view_engine.zig");
|
||||
const mixin = @import("mixin.zig");
|
||||
|
||||
pub const CompileOptions = struct {
|
||||
/// Name for the compile step
|
||||
name: []const u8 = "compile-pug-templates",
|
||||
|
||||
/// Source directories containing .pug files (can be multiple)
|
||||
source_dirs: []const []const u8,
|
||||
|
||||
/// Output directory for generated .zig files
|
||||
output_dir: []const u8,
|
||||
|
||||
/// Base directory for resolving includes/extends
|
||||
/// If not specified, automatically inferred as the common parent of all source_dirs
|
||||
/// e.g., ["views/pages", "views/partials"] -> "views"
|
||||
views_root: ?[]const u8 = null,
|
||||
};
|
||||
|
||||
pub const CompileStep = struct {
|
||||
step: Step,
|
||||
options: CompileOptions,
|
||||
output_file: GeneratedFile,
|
||||
|
||||
pub fn create(owner: *Build, options: CompileOptions) *CompileStep {
|
||||
const self = owner.allocator.create(CompileStep) catch @panic("OOM");
|
||||
|
||||
self.* = .{
|
||||
.step = Step.init(.{
|
||||
.id = .custom,
|
||||
.name = options.name,
|
||||
.owner = owner,
|
||||
.makeFn = make,
|
||||
}),
|
||||
.options = options,
|
||||
.output_file = .{ .step = &self.step },
|
||||
};
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
fn make(step: *Step, options: Step.MakeOptions) !void {
|
||||
_ = options;
|
||||
const self: *CompileStep = @fieldParentPtr("step", step);
|
||||
const b = step.owner;
|
||||
const allocator = b.allocator;
|
||||
|
||||
// Use output_dir relative to project root (not zig-out/)
|
||||
const output_path = b.pathFromRoot(self.options.output_dir);
|
||||
try fs.cwd().makePath(output_path);
|
||||
|
||||
var arena = std.heap.ArenaAllocator.init(allocator);
|
||||
defer arena.deinit();
|
||||
const arena_allocator = arena.allocator();
|
||||
|
||||
// Track all compiled templates
|
||||
var all_templates = std.StringHashMap([]const u8).init(allocator);
|
||||
defer {
|
||||
var iter = all_templates.iterator();
|
||||
while (iter.next()) |entry| {
|
||||
allocator.free(entry.key_ptr.*);
|
||||
allocator.free(entry.value_ptr.*);
|
||||
}
|
||||
all_templates.deinit();
|
||||
}
|
||||
|
||||
// Determine views_root (common parent directory for all templates)
|
||||
const views_root = if (self.options.views_root) |root|
|
||||
b.pathFromRoot(root)
|
||||
else if (self.options.source_dirs.len > 0) blk: {
|
||||
// Infer common parent from all source_dirs
|
||||
// e.g., ["views/pages", "views/partials"] -> "views"
|
||||
const first_dir = b.pathFromRoot(self.options.source_dirs[0]);
|
||||
const common_parent = fs.path.dirname(first_dir) orelse first_dir;
|
||||
|
||||
// Verify all source_dirs share this parent
|
||||
for (self.options.source_dirs) |dir| {
|
||||
const abs_dir = b.pathFromRoot(dir);
|
||||
if (!mem.startsWith(u8, abs_dir, common_parent)) {
|
||||
// Dirs don't share common parent, use first dir's parent
|
||||
break :blk common_parent;
|
||||
}
|
||||
}
|
||||
|
||||
break :blk common_parent;
|
||||
} else b.pathFromRoot(".");
|
||||
|
||||
// Compile each source directory
|
||||
for (self.options.source_dirs) |source_dir| {
|
||||
const abs_source_dir = b.pathFromRoot(source_dir);
|
||||
|
||||
std.debug.print("Compiling templates from {s}...\n", .{source_dir});
|
||||
|
||||
try compileDirectory(
|
||||
allocator,
|
||||
arena_allocator,
|
||||
abs_source_dir,
|
||||
views_root,
|
||||
output_path,
|
||||
&all_templates,
|
||||
);
|
||||
}
|
||||
|
||||
// Generate root.zig
|
||||
try generateRootZig(allocator, output_path, &all_templates);
|
||||
|
||||
// Copy helpers.zig
|
||||
try copyHelpersZig(allocator, output_path);
|
||||
|
||||
std.debug.print("Compiled {d} templates to {s}/root.zig\n", .{ all_templates.count(), output_path });
|
||||
|
||||
// Set the output file path
|
||||
self.output_file.path = try fs.path.join(allocator, &.{ output_path, "root.zig" });
|
||||
}
|
||||
|
||||
pub fn getOutput(self: *CompileStep) Build.LazyPath {
|
||||
return .{ .generated = .{ .file = &self.output_file } };
|
||||
}
|
||||
};
|
||||
|
||||
fn compileDirectory(
|
||||
allocator: mem.Allocator,
|
||||
arena_allocator: mem.Allocator,
|
||||
input_dir: []const u8,
|
||||
views_root: []const u8,
|
||||
output_dir: []const u8,
|
||||
template_map: *std.StringHashMap([]const u8),
|
||||
) !void {
|
||||
// Find all .pug files recursively
|
||||
const pug_files = try findPugFiles(arena_allocator, input_dir);
|
||||
|
||||
// Initialize ViewEngine with views_root for resolving includes/extends
|
||||
var engine = view_engine.ViewEngine.init(.{
|
||||
.views_dir = views_root,
|
||||
});
|
||||
defer engine.deinit();
|
||||
|
||||
// Initialize mixin registry
|
||||
var registry = mixin.MixinRegistry.init(arena_allocator);
|
||||
defer registry.deinit();
|
||||
|
||||
// Compile each file
|
||||
for (pug_files) |pug_file| {
|
||||
compileSingleFile(
|
||||
allocator,
|
||||
arena_allocator,
|
||||
&engine,
|
||||
®istry,
|
||||
pug_file,
|
||||
views_root,
|
||||
output_dir,
|
||||
template_map,
|
||||
) catch |err| {
|
||||
std.debug.print(" ERROR: Failed to compile {s}: {}\n", .{ pug_file, err });
|
||||
continue;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
fn compileSingleFile(
|
||||
allocator: mem.Allocator,
|
||||
arena_allocator: mem.Allocator,
|
||||
engine: *view_engine.ViewEngine,
|
||||
registry: *mixin.MixinRegistry,
|
||||
pug_file: []const u8,
|
||||
views_root: []const u8,
|
||||
output_dir: []const u8,
|
||||
template_map: *std.StringHashMap([]const u8),
|
||||
) !void {
|
||||
// Get relative path from views_root (for template resolution)
|
||||
const views_rel = if (mem.startsWith(u8, pug_file, views_root))
|
||||
pug_file[views_root.len..]
|
||||
else
|
||||
pug_file;
|
||||
|
||||
// Skip leading slash
|
||||
const trimmed_views = if (views_rel.len > 0 and views_rel[0] == '/')
|
||||
views_rel[1..]
|
||||
else
|
||||
views_rel;
|
||||
|
||||
// Remove .pug extension for template name (used by ViewEngine)
|
||||
const template_name = if (mem.endsWith(u8, trimmed_views, ".pug"))
|
||||
trimmed_views[0 .. trimmed_views.len - 4]
|
||||
else
|
||||
trimmed_views;
|
||||
|
||||
// Parse template with full resolution
|
||||
const final_ast = try engine.parseWithIncludes(arena_allocator, template_name, registry);
|
||||
|
||||
// Extract field names
|
||||
const fields = try zig_codegen.extractFieldNames(arena_allocator, final_ast);
|
||||
|
||||
// Generate Zig code
|
||||
var codegen = zig_codegen.Codegen.init(arena_allocator);
|
||||
defer codegen.deinit();
|
||||
|
||||
const zig_code = try codegen.generate(final_ast, "render", fields);
|
||||
|
||||
// Create flat filename from views-relative path to avoid collisions
|
||||
// e.g., "pages/404.pug" → "pages_404.zig"
|
||||
const flat_name = try makeFlatFileName(allocator, trimmed_views);
|
||||
defer allocator.free(flat_name);
|
||||
|
||||
const output_path = try fs.path.join(allocator, &.{ output_dir, flat_name });
|
||||
defer allocator.free(output_path);
|
||||
|
||||
try fs.cwd().writeFile(.{ .sub_path = output_path, .data = zig_code });
|
||||
|
||||
// Track for root.zig (use same naming convention for both)
|
||||
const name = try makeTemplateName(allocator, trimmed_views);
|
||||
const output_copy = try allocator.dupe(u8, flat_name);
|
||||
try template_map.put(name, output_copy);
|
||||
}
|
||||
|
||||
fn findPugFiles(allocator: mem.Allocator, dir_path: []const u8) ![][]const u8 {
|
||||
var results: std.ArrayListUnmanaged([]const u8) = .{};
|
||||
errdefer {
|
||||
for (results.items) |item| allocator.free(item);
|
||||
results.deinit(allocator);
|
||||
}
|
||||
|
||||
try findPugFilesRecursive(allocator, dir_path, &results);
|
||||
return results.toOwnedSlice(allocator);
|
||||
}
|
||||
|
||||
fn findPugFilesRecursive(allocator: mem.Allocator, dir_path: []const u8, results: *std.ArrayListUnmanaged([]const u8)) !void {
|
||||
var dir = try fs.cwd().openDir(dir_path, .{ .iterate = true });
|
||||
defer dir.close();
|
||||
|
||||
var iter = dir.iterate();
|
||||
while (try iter.next()) |entry| {
|
||||
const full_path = try fs.path.join(allocator, &.{ dir_path, entry.name });
|
||||
errdefer allocator.free(full_path);
|
||||
|
||||
switch (entry.kind) {
|
||||
.file => {
|
||||
if (mem.endsWith(u8, entry.name, ".pug")) {
|
||||
try results.append(allocator, full_path);
|
||||
} else {
|
||||
allocator.free(full_path);
|
||||
}
|
||||
},
|
||||
.directory => {
|
||||
try findPugFilesRecursive(allocator, full_path, results);
|
||||
allocator.free(full_path);
|
||||
},
|
||||
else => {
|
||||
allocator.free(full_path);
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn makeTemplateName(allocator: mem.Allocator, path: []const u8) ![]const u8 {
|
||||
const without_ext = if (mem.endsWith(u8, path, ".pug"))
|
||||
path[0 .. path.len - 4]
|
||||
else
|
||||
path;
|
||||
|
||||
var result: std.ArrayListUnmanaged(u8) = .{};
|
||||
defer result.deinit(allocator);
|
||||
|
||||
for (without_ext) |c| {
|
||||
if (c == '/' or c == '-' or c == '.') {
|
||||
try result.append(allocator, '_');
|
||||
} else {
|
||||
try result.append(allocator, c);
|
||||
}
|
||||
}
|
||||
|
||||
return result.toOwnedSlice(allocator);
|
||||
}
|
||||
|
||||
fn makeFlatFileName(allocator: mem.Allocator, path: []const u8) ![]const u8 {
|
||||
// Convert "pages/404.pug" → "pages_404.zig"
|
||||
const without_ext = if (mem.endsWith(u8, path, ".pug"))
|
||||
path[0 .. path.len - 4]
|
||||
else
|
||||
path;
|
||||
|
||||
var result: std.ArrayListUnmanaged(u8) = .{};
|
||||
defer result.deinit(allocator);
|
||||
|
||||
for (without_ext) |c| {
|
||||
if (c == '/' or c == '-') {
|
||||
try result.append(allocator, '_');
|
||||
} else {
|
||||
try result.append(allocator, c);
|
||||
}
|
||||
}
|
||||
|
||||
try result.appendSlice(allocator, ".zig");
|
||||
|
||||
return result.toOwnedSlice(allocator);
|
||||
}
|
||||
|
||||
fn generateRootZig(allocator: mem.Allocator, output_dir: []const u8, template_map: *std.StringHashMap([]const u8)) !void {
|
||||
var output: std.ArrayListUnmanaged(u8) = .{};
|
||||
defer output.deinit(allocator);
|
||||
|
||||
try output.appendSlice(allocator, "// Auto-generated by Pugz build step\n");
|
||||
try output.appendSlice(allocator, "// This file exports all compiled templates\n\n");
|
||||
|
||||
// Sort template names
|
||||
var names: std.ArrayListUnmanaged([]const u8) = .{};
|
||||
defer names.deinit(allocator);
|
||||
|
||||
var iter = template_map.keyIterator();
|
||||
while (iter.next()) |key| {
|
||||
try names.append(allocator, key.*);
|
||||
}
|
||||
|
||||
std.mem.sort([]const u8, names.items, {}, struct {
|
||||
fn lessThan(_: void, a: []const u8, b: []const u8) bool {
|
||||
return std.mem.lessThan(u8, a, b);
|
||||
}
|
||||
}.lessThan);
|
||||
|
||||
// Generate exports
|
||||
for (names.items) |name| {
|
||||
const file_path = template_map.get(name).?;
|
||||
// file_path is already the flat filename like "pages_404.zig"
|
||||
const import_path = file_path[0 .. file_path.len - 4]; // Remove .zig to get "pages_404"
|
||||
|
||||
try output.appendSlice(allocator, "pub const ");
|
||||
try output.appendSlice(allocator, name);
|
||||
try output.appendSlice(allocator, " = @import(\"");
|
||||
try output.appendSlice(allocator, import_path);
|
||||
try output.appendSlice(allocator, ".zig\");\n");
|
||||
}
|
||||
|
||||
const root_path = try fs.path.join(allocator, &.{ output_dir, "root.zig" });
|
||||
defer allocator.free(root_path);
|
||||
|
||||
try fs.cwd().writeFile(.{ .sub_path = root_path, .data = output.items });
|
||||
}
|
||||
|
||||
fn copyHelpersZig(allocator: mem.Allocator, output_dir: []const u8) !void {
|
||||
const helpers_source = @embedFile("tpl_compiler/helpers_template.zig");
|
||||
const output_path = try fs.path.join(allocator, &.{ output_dir, "helpers.zig" });
|
||||
defer allocator.free(output_path);
|
||||
|
||||
try fs.cwd().writeFile(.{ .sub_path = output_path, .data = helpers_source });
|
||||
}
|
||||
|
||||
/// Convenience function to add a compile step to the build
|
||||
pub fn addCompileStep(b: *Build, options: CompileOptions) *CompileStep {
|
||||
return CompileStep.create(b, options);
|
||||
}
|
||||
Reference in New Issue
Block a user