diff --git a/.gitignore b/.gitignore index 05a3a49..cbcae4c 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ zig-out/ zig-cache/ *.core static/ +.jetzig diff --git a/build.zig b/build.zig index 82532bc..48a40c5 100644 --- a/build.zig +++ b/build.zig @@ -86,6 +86,14 @@ pub fn jetzigInit(b: *std.Build, exe: *std.Build.Step.Compile, options: JetzigIn exe.root_module.addImport("jetzig", jetzig_module); exe.root_module.addImport("zmpl", zmpl_module); + if (b.option(bool, "jetzig_runner", "Used internally by `jetzig server` command.")) |jetzig_runner| { + if (jetzig_runner) { + const file = try std.fs.cwd().createFile(".jetzig", .{ .truncate = true }); + defer file.close(); + try file.writeAll(exe.name); + } + } + var generate_routes = try GenerateRoutes.init(b.allocator, "src/app/views"); try generate_routes.generateRoutes(); const write_files = b.addWriteFiles(); diff --git a/cli/cli.zig b/cli/cli.zig index e6853b0..ca62b78 100644 --- a/cli/cli.zig +++ b/cli/cli.zig @@ -3,6 +3,7 @@ const args = @import("args"); const init = @import("commands/init.zig"); const update = @import("commands/update.zig"); const generate = @import("commands/generate.zig"); +const server = @import("commands/server.zig"); const Options = struct { help: bool = false, @@ -17,6 +18,7 @@ const Options = struct { .init = "Initialize a new project", .update = "Update current project to latest version of Jetzig", .generate = "Generate scaffolding", + .server = "Run a development server", .help = "Print help and exit", }, }; @@ -26,7 +28,9 @@ const Verb = union(enum) { init: init.Options, update: update.Options, generate: generate.Options, + server: server.Options, g: generate.Options, + s: server.Options, }; /// Main entrypoint for `jetzig` executable. Parses command line args and generates a new @@ -57,6 +61,7 @@ pub fn main() !void { \\ init Initialize a new project. \\ update Update current project to latest version of Jetzig. \\ generate Generate scaffolding. + \\ server Run a development server. \\ \\ Pass --help to any command for more information, e.g. `jetzig init --help` \\ @@ -88,6 +93,13 @@ fn run(allocator: std.mem.Allocator, options: args.ParseArgsResult(Options, Verb options.positionals, .{ .help = options.options.help }, ), + .s, .server => |opts| server.run( + allocator, + opts, + writer, + options.positionals, + .{ .help = options.options.help }, + ), }; } } diff --git a/cli/commands/server.zig b/cli/commands/server.zig new file mode 100644 index 0000000..8395c3d --- /dev/null +++ b/cli/commands/server.zig @@ -0,0 +1,166 @@ +const std = @import("std"); +const args = @import("args"); +const util = @import("../util.zig"); + +pub const watch_changes_pause_duration = 1 * 1000 * 1000 * 1000; + +/// Command line options for the `update` command. +pub const Options = struct { + reload: bool = true, + + pub const meta = .{ + .full_text = + \\Launches a development server. + \\ + \\The development server reloads when files in `src/` are updated. + \\ + \\To disable this behaviour, pass `--reload=false` + \\ + \\Example: + \\ + \\ jetzig server + \\ jetzig server --reload=false + , + .option_docs = .{ + .reload = "Enable or disable automatic reload on update (default: true)", + }, + }; +}; + +/// Run the `jetzig server` command. +pub fn run( + allocator: std.mem.Allocator, + options: Options, + writer: anytype, + positionals: [][]const u8, + other_options: struct { help: bool }, +) !void { + if (other_options.help) { + try args.printHelp(Options, "jetzig server", writer); + return; + } + + if (positionals.len > 0) { + std.debug.print("The `server` command does not accept positional arguments.", .{}); + return error.JetzigCommandError; + } + + var cwd = try util.detectJetzigProjectDir(); + defer cwd.close(); + + const realpath = try std.fs.realpathAlloc(allocator, "."); + defer allocator.free(realpath); + + var mtime = try totalMtime(allocator, cwd, "src"); + + std.debug.print( + "Launching development server. [reload:{s}]\n", + .{ + if (options.reload) "enabled" else "disabled", + }, + ); + + while (true) { + try util.runCommand( + allocator, + realpath, + &[_][]const u8{ "zig", "build", "-Djetzig_runner=true", "install" }, + ); + + const exe_path = try locateExecutable(allocator, cwd); + if (exe_path == null) { + std.debug.print("Unable to locate compiled executable. Exiting.\n", .{}); + std.os.exit(1); + } + + const argv = &[_][]const u8{exe_path.?}; + defer allocator.free(exe_path.?); + + var process = std.process.Child.init(argv, allocator); + process.stdin_behavior = .Inherit; + process.stdout_behavior = .Inherit; + process.stderr_behavior = .Inherit; + process.cwd = realpath; + + var stdout_buf = std.ArrayList(u8).init(allocator); + defer stdout_buf.deinit(); + + var stderr_buf = std.ArrayList(u8).init(allocator); + defer stderr_buf.deinit(); + + try process.spawn(); + + if (!options.reload) { + const term = try process.wait(); + std.os.exit(term.Exited); + } + + while (true) { + if (process.term) |_| { + _ = try process.wait(); + std.debug.print("Server exited, restarting...\n", .{}); + } + + std.time.sleep(watch_changes_pause_duration); + + const new_mtime = try totalMtime(allocator, cwd, "src"); + + if (new_mtime > mtime) { + std.debug.print("Changes detected, restarting server...\n", .{}); + _ = try process.kill(); + mtime = new_mtime; + break; + } + } + } +} + +fn totalMtime(allocator: std.mem.Allocator, cwd: std.fs.Dir, sub_path: []const u8) !i128 { + var dir = try cwd.openDir(sub_path, .{ .iterate = true }); + defer dir.close(); + + var walker = try dir.walk(allocator); + defer walker.deinit(); + + var sum: i128 = 0; + + while (try walker.next()) |entry| { + if (entry.kind != .file) continue; + const extension = std.fs.path.extension(entry.path); + + if (std.mem.eql(u8, extension, ".zig") or std.mem.eql(u8, extension, ".zmpl")) { + const stat = try dir.statFile(entry.path); + sum += stat.mtime; + } + } + + return sum; +} + +fn locateExecutable(allocator: std.mem.Allocator, dir: std.fs.Dir) !?[]const u8 { + const file = dir.openFile(".jetzig", .{}) catch |err| { + switch (err) { + error.FileNotFound => return null, + else => return err, + } + }; + const content = try file.readToEndAlloc(allocator, 1024); + defer allocator.free(content); + + const exe_name = util.strip(content); + + // XXX: Will fail if user sets a custom install path. + var bin_dir = try dir.openDir("zig-out/bin", .{ .iterate = true }); + defer bin_dir.close(); + + var walker = try bin_dir.walk(allocator); + defer walker.deinit(); + + while (try walker.next()) |entry| { + if (entry.kind == .file and std.mem.eql(u8, entry.path, exe_name)) { + return try bin_dir.realpathAlloc(allocator, entry.path); + } + } + + return null; +}