diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml
index 68d7e8f..1f6df02 100644
--- a/.github/workflows/CI.yml
+++ b/.github/workflows/CI.yml
@@ -22,7 +22,7 @@ jobs:
- uses: actions/checkout@v3
with:
submodules: true
-
+
- name: Setup Zig
# You may pin to the exact commit or the version.
# uses: goto-bus-stop/setup-zig@41ae19e72e21b9a1380e86ff9f058db709fc8fc6
@@ -30,10 +30,10 @@ jobs:
with:
version: master
cache: true # Let's see how this behaves
-
+
- run: zig version
- run: zig env
-
+
- name: Build
run: zig build --verbose
@@ -49,13 +49,13 @@ jobs:
cd cli
for target in "${targets[@]}"; do
mkdir -p $root/artifacts/$target
- echo "Building target ${target}..."
+ echo "Building target ${target}..."
zig build -Dtarget=${target} -Doptimize=ReleaseSafe --prefix $root/artifacts/${target}/ &
sed -e '1,5d' < $root/README.md > $root/artifacts/${target}/README.md
cp $root/LICENSE $root/artifacts/${target}/
done
wait
-
+
- name: Upload artifacts Target Windows
if: ${{ matrix.os == 'ubuntu-latest' }}
uses: actions/upload-artifact@v2
@@ -72,7 +72,7 @@ jobs:
if: ${{ matrix.os == 'ubuntu-latest' }}
uses: actions/upload-artifact@v2
with:
- name: builds-macos-x86
+ name: build-macos-x86
path: artifacts/x86_64-macos
- name: Upload artifacts Target MacOS 2
if: ${{ matrix.os == 'ubuntu-latest' }}
diff --git a/cli/cli.zig b/cli/cli.zig
index 65ef87d..61b9384 100644
--- a/cli/cli.zig
+++ b/cli/cli.zig
@@ -1,6 +1,7 @@
const std = @import("std");
const args = @import("args");
-const init = @import("init.zig");
+const init = @import("commands/init.zig");
+const generate = @import("commands/generate.zig");
const Options = struct {
help: bool = false,
@@ -10,8 +11,10 @@ const Options = struct {
};
pub const meta = .{
+ .usage_summary = "[COMMAND]",
.option_docs = .{
.init = "Initialize a new project",
+ .generate = "Generate scaffolding",
.help = "Print help and exit",
},
};
@@ -19,6 +22,8 @@ const Options = struct {
const Verb = union(enum) {
init: init.Options,
+ generate: generate.Options,
+ g: generate.Options,
};
/// Main entrypoint for `jetzig` executable. Parses command line args and generates a new
@@ -33,28 +38,45 @@ pub fn main() !void {
const writer = std.io.getStdErr().writer();
- if (options.verb) |verb| {
- switch (verb) {
- .init => |opts| return init.run(
- allocator,
- opts,
- writer,
- options.positionals,
- .{ .help = options.options.help },
- ),
+ run(allocator, options, writer) catch |err| {
+ switch (err) {
+ error.JetzigCommandError => std.os.exit(1),
+ else => return err,
}
- }
+ };
- if (options.options.help) {
+ if (options.options.help or options.verb == null) {
try args.printHelp(Options, "jetzig", writer);
try writer.writeAll(
\\
\\Commands:
\\
\\ init Initialize a new project.
+ \\ generate Generate scaffolding.
\\
\\ Pass --help to any command for more information, e.g. `jetzig init --help`
\\
);
}
}
+
+fn run(allocator: std.mem.Allocator, options: args.ParseArgsResult(Options, Verb), writer: anytype) !void {
+ if (options.verb) |verb| {
+ return switch (verb) {
+ .init => |opts| init.run(
+ allocator,
+ opts,
+ writer,
+ options.positionals,
+ .{ .help = options.options.help },
+ ),
+ .g, .generate => |opts| generate.run(
+ allocator,
+ opts,
+ writer,
+ options.positionals,
+ .{ .help = options.options.help },
+ ),
+ };
+ }
+}
diff --git a/cli/commands/generate.zig b/cli/commands/generate.zig
new file mode 100644
index 0000000..7898aa5
--- /dev/null
+++ b/cli/commands/generate.zig
@@ -0,0 +1,81 @@
+const std = @import("std");
+const args = @import("args");
+const view = @import("generate/view.zig");
+const partial = @import("generate/partial.zig");
+const middleware = @import("generate/middleware.zig");
+const util = @import("../util.zig");
+
+/// Command line options for the `generate` command.
+pub const Options = struct {
+ path: ?[]const u8 = null,
+
+ pub const shorthands = .{
+ .p = "path",
+ };
+
+ pub const meta = .{
+ .usage_summary = "[view|middleware] [options]",
+ .full_text =
+ \\Generates scaffolding for views, middleware, and other objects in future.
+ \\
+ \\When generating a view, by default all actions will be included.
+ \\Optionally pass one or more of the following arguments to specify desired actions:
+ \\
+ \\ index, get, post, patch, put, delete
+ \\
+ \\Each view action can be qualified with a `:static` option to mark the view content
+ \\as statically generated at build time.
+ \\
+ \\e.g. generate a view named `iguanas` with a static `index` action:
+ \\
+ \\ jetzig generate view iguanas index:static get post delete
+ ,
+ };
+};
+
+/// Run the `jetzig init` command.
+pub fn run(
+ allocator: std.mem.Allocator,
+ options: Options,
+ writer: anytype,
+ positionals: [][]const u8,
+ other_options: struct { help: bool },
+) !void {
+ var cwd = try util.detectJetzigProjectDir();
+ defer cwd.close();
+
+ _ = options;
+ if (other_options.help) {
+ try args.printHelp(Options, "jetzig generate", writer);
+ return;
+ }
+ var generate_type: ?enum { view, partial, middleware } = null;
+ var sub_args = std.ArrayList([]const u8).init(allocator);
+ defer sub_args.deinit();
+
+ for (positionals) |arg| {
+ if (generate_type == null and std.mem.eql(u8, arg, "view")) {
+ generate_type = .view;
+ } else if (generate_type == null and std.mem.eql(u8, arg, "partial")) {
+ generate_type = .partial;
+ } else if (generate_type == null and std.mem.eql(u8, arg, "middleware")) {
+ generate_type = .middleware;
+ } else if (generate_type == null) {
+ std.debug.print("Unknown generator command: {s}\n", .{arg});
+ return error.JetzigCommandError;
+ } else {
+ try sub_args.append(arg);
+ }
+ }
+
+ if (generate_type) |capture| {
+ return switch (capture) {
+ .view => view.run(allocator, cwd, sub_args.items),
+ .partial => partial.run(allocator, cwd, sub_args.items),
+ .middleware => middleware.run(allocator, cwd, sub_args.items),
+ };
+ } else {
+ std.debug.print("Missing sub-command. Expected: [view|middleware]\n", .{});
+ return error.JetzigCommandError;
+ }
+}
diff --git a/cli/commands/generate/middleware.zig b/cli/commands/generate/middleware.zig
new file mode 100644
index 0000000..ff9aca3
--- /dev/null
+++ b/cli/commands/generate/middleware.zig
@@ -0,0 +1,101 @@
+const std = @import("std");
+const util = @import("../../util.zig");
+
+/// Run the middleware generator. Create a middleware file in `src/app/middleware/`
+pub fn run(allocator: std.mem.Allocator, cwd: std.fs.Dir, args: [][]const u8) !void {
+ if (args.len != 1 or !util.isCamelCase(args[0])) {
+ std.debug.print(
+ \\Expected a middleware name in CamelCase.
+ \\
+ \\Example:
+ \\
+ \\ jetzig generate middleware IguanaBrain
+ \\
+ , .{});
+ return error.JetzigCommandError;
+ }
+
+ const dir_path = try std.fs.path.join(allocator, &[_][]const u8{ "src", "app", "middleware" });
+ defer allocator.free(dir_path);
+
+ var dir = try cwd.makeOpenPath(dir_path, .{});
+ defer dir.close();
+
+ const filename = try std.mem.concat(allocator, u8, &[_][]const u8{ args[0], ".zig" });
+ defer allocator.free(filename);
+
+ const file = dir.createFile(filename, .{ .exclusive = true }) catch |err| {
+ switch (err) {
+ error.PathAlreadyExists => {
+ std.debug.print("Middleware already exists: {s}\n", .{filename});
+ return error.JetzigCommandError;
+ },
+ else => return err,
+ }
+ };
+
+ try file.writeAll(middleware_content);
+
+ file.close();
+
+ const realpath = try dir.realpathAlloc(allocator, filename);
+ defer allocator.free(realpath);
+ std.debug.print(
+ \\Generated middleware: {s}
+ \\
+ \\Edit `src/main.zig` and add the new middleware to the `jetzig_options.middleware` declaration:
+ \\
+ \\ pub const jetzig_options = struct {{
+ \\ pub const middleware: []const type = &.{{
+ \\ @import("app/middleware/{s}.zig"),
+ \\ }};
+ \\ }};
+ \\
+ \\Middleware are invoked in the order they appear in `jetzig_options.middleware`.
+ \\
+ \\
+ , .{ realpath, args[0] });
+}
+
+const middleware_content =
+ \\const std = @import("std");
+ \\const jetzig = @import("jetzig");
+ \\
+ \\/// Define any custom data fields you want to store here. Assigning to these fields in the `init`
+ \\/// function allows you to access them in the `beforeRequest` and `afterRequest` functions, where
+ \\/// they can also be modified.
+ \\my_custom_value: []const u8,
+ \\
+ \\const Self = @This();
+ \\
+ \\/// Initialize middleware.
+ \\pub fn init(request: *jetzig.http.Request) !*Self {
+ \\ var middleware = try request.allocator.create(Self);
+ \\ middleware.my_custom_value = "initial value";
+ \\ return middleware;
+ \\}
+ \\
+ \\/// Invoked immediately after the request head has been processed, before relevant view function
+ \\/// is processed. This gives you access to request headers but not the request body.
+ \\pub fn beforeRequest(self: *Self, request: *jetzig.http.Request) !void {
+ \\ request.server.logger.debug("[middleware] my_custom_value: {s}", .{self.my_custom_value});
+ \\ self.my_custom_value = @tagName(request.method);
+ \\}
+ \\
+ \\/// Invoked immediately after the request has finished responding. Provides full access to the
+ \\/// response as well as the request.
+ \\pub fn afterRequest(self: *Self, request: *jetzig.http.Request, response: *jetzig.http.Response) !void {
+ \\ request.server.logger.debug(
+ \\ "[middleware] my_custom_value: {s}, response status: {s}",
+ \\ .{ self.my_custom_value, @tagName(response.status_code) },
+ \\ );
+ \\}
+ \\
+ \\/// Invoked after `afterRequest` is called, use this function to do any clean-up.
+ \\/// Note that `request.allocator` is an arena allocator, so any allocations are automatically
+ \\/// done before the next request starts processing.
+ \\pub fn deinit(self: *Self, request: *jetzig.http.Request) void {
+ \\ request.allocator.destroy(self);
+ \\}
+ \\
+;
diff --git a/cli/commands/generate/partial.zig b/cli/commands/generate/partial.zig
new file mode 100644
index 0000000..dfc9983
--- /dev/null
+++ b/cli/commands/generate/partial.zig
@@ -0,0 +1,46 @@
+const std = @import("std");
+
+/// Run the partial generator. Create a partial template in `src/app/views/`
+pub fn run(allocator: std.mem.Allocator, cwd: std.fs.Dir, args: [][]const u8) !void {
+ if (args.len != 2) {
+ std.debug.print(
+ \\Expected a view name and a name for a partial.
+ \\
+ \\Example:
+ \\
+ \\ jetzig generate partial iguanas ziglet
+ \\
+ , .{});
+ return error.JetzigCommandError;
+ }
+
+ const dir_path = try std.fs.path.join(allocator, &[_][]const u8{ "src", "app", "views", args[0] });
+ defer allocator.free(dir_path);
+
+ var dir = try cwd.makeOpenPath(dir_path, .{});
+ defer dir.close();
+
+ const filename = try std.mem.concat(allocator, u8, &[_][]const u8{ "_", args[1], ".zmpl" });
+ defer allocator.free(filename);
+
+ const file = dir.createFile(filename, .{ .exclusive = true }) catch |err| {
+ switch (err) {
+ error.PathAlreadyExists => {
+ std.debug.print("Partial already exists: {s}\n", .{filename});
+ return error.JetzigCommandError;
+ },
+ else => return err,
+ }
+ };
+
+ try file.writeAll(
+ \\
Partial content goes here.
+ \\
+ );
+
+ file.close();
+
+ const realpath = try dir.realpathAlloc(allocator, filename);
+ defer allocator.free(realpath);
+ std.debug.print("Generated partial template: {s}\n", .{realpath});
+}
diff --git a/cli/commands/generate/view.zig b/cli/commands/generate/view.zig
new file mode 100644
index 0000000..e9d7c55
--- /dev/null
+++ b/cli/commands/generate/view.zig
@@ -0,0 +1,220 @@
+const std = @import("std");
+const util = @import("../../util.zig");
+
+/// Run the view generator. Create a view in `src/app/views/`
+pub fn run(allocator: std.mem.Allocator, cwd: std.fs.Dir, args: [][]const u8) !void {
+ if (args.len == 0) {
+ std.debug.print(".\n", .{});
+ std.debug.print(
+ \\Expected view name followed by optional actions.
+ \\
+ \\Example:
+ \\
+ \\ jetzig generate view iguanas index:static get post delete
+ \\
+ , .{});
+ return error.JetzigCommandError;
+ }
+
+ var buf = std.ArrayList(u8).init(allocator);
+ defer buf.deinit();
+
+ const writer = buf.writer();
+
+ try writer.writeAll(
+ \\const std = @import("std");
+ \\const jetzig = @import("jetzig");
+ \\
+ \\
+ );
+
+ const filename = try std.mem.concat(allocator, u8, &[_][]const u8{ args[0], ".zig" });
+ defer allocator.free(filename);
+ const action_args = if (args.len > 1)
+ args[1..]
+ else
+ &[_][]const u8{ "index", "get", "post", "put", "patch", "delete" };
+
+ var actions = std.ArrayList(Action).init(allocator);
+ defer actions.deinit();
+
+ var static_actions = std.ArrayList(Action).init(allocator);
+ defer static_actions.deinit();
+
+ for (action_args) |arg| {
+ if (parseAction(arg)) |action| {
+ try actions.append(action);
+ if (action.static) try static_actions.append(action);
+ } else {
+ std.debug.print("Unexpected argument: {s}\n", .{arg});
+ return error.JetzigCommandError;
+ }
+ }
+
+ if (static_actions.items.len > 0) try writeStaticParams(allocator, static_actions.items, writer);
+
+ for (actions.items) |action| {
+ try writeAction(allocator, writer, action);
+ try writeTemplate(allocator, cwd, args[0], action);
+ }
+
+ var dir = try cwd.openDir("src/app/views", .{});
+ defer dir.close();
+
+ const file = dir.createFile(filename, .{ .exclusive = true }) catch |err| {
+ switch (err) {
+ error.PathAlreadyExists => {
+ std.debug.print("Path already exists, skipping view creation: {s}\n", .{filename});
+ return error.JetzigCommandError;
+ },
+ else => return err,
+ }
+ };
+ try file.writeAll(util.strip(buf.items));
+ try file.writeAll("\n");
+ file.close();
+ const realpath = try dir.realpathAlloc(allocator, filename);
+ defer allocator.free(realpath);
+ std.debug.print("Generated view: {s}\n", .{realpath});
+}
+
+const Method = enum { index, get, post, put, patch, delete };
+const Action = struct {
+ method: Method,
+ static: bool,
+};
+
+// Parse a view arg. Grammar:
+// [index[:static]|get[:static]|post[:static]|put[:static]|patch[:static]|delete[:static]]
+fn parseAction(arg: []const u8) ?Action {
+ inline for (@typeInfo(Method).Enum.fields) |tag| {
+ const with_static = tag.name ++ ":static";
+ const method: Method = @enumFromInt(tag.value);
+
+ if (std.mem.eql(u8, tag.name, arg)) return .{ .method = method, .static = false };
+ if (std.mem.eql(u8, with_static, arg)) return .{ .method = method, .static = true };
+ }
+
+ return null;
+}
+
+// Write a view function to the output buffer.
+fn writeAction(allocator: std.mem.Allocator, writer: anytype, action: Action) !void {
+ const function = try std.fmt.allocPrint(
+ allocator,
+ \\pub fn {s}({s}request: *jetzig.{s}, data: *jetzig.Data) !jetzig.View {{
+ \\ _ = data;{s}
+ \\ return request.render({s});
+ \\}}
+ \\
+ \\
+ ,
+ .{
+ @tagName(action.method),
+ switch (action.method) {
+ .index, .post => "",
+ .get, .put, .patch, .delete => "id: []const u8, ",
+ },
+ if (action.static) "StaticRequest" else "Request",
+ switch (action.method) {
+ .index, .post => "",
+ .get, .put, .patch, .delete => "\n _ = id;",
+ },
+ switch (action.method) {
+ .index, .get => ".ok",
+ .post => ".created",
+ .put, .patch, .delete => ".ok",
+ },
+ },
+ );
+ defer allocator.free(function);
+ try writer.writeAll(function);
+}
+
+// Output static params example. Only invoked if one or more static routes are created.
+fn writeStaticParams(allocator: std.mem.Allocator, actions: []Action, writer: anytype) !void {
+ try writer.writeAll(
+ \\// Define an array of params for each static view function.
+ \\// At build time, static outputs are generated for each set of params.
+ \\// At run time, requests matching the provided params will render the pre-rendered content.
+ \\pub const static_params = .{
+ \\
+ );
+
+ for (actions) |action| {
+ switch (action.method) {
+ .index, .post => {
+ const output = try std.fmt.allocPrint(
+ allocator,
+ \\ .{s} = .{{
+ \\ .{{ .params = .{{ .foo = "bar", .baz = "qux" }} }},
+ \\ }},
+ \\
+ ,
+ .{@tagName(action.method)},
+ );
+ defer allocator.free(output);
+ try writer.writeAll(output);
+ },
+ .get, .put, .patch, .delete => {
+ const output = try std.fmt.allocPrint(
+ allocator,
+ \\ .{s} = .{{
+ \\ .{{ .id = "1", .params = .{{ .foo = "bar", .baz = "qux" }} }},
+ \\ }},
+ \\
+ ,
+ .{@tagName(action.method)},
+ );
+ defer allocator.free(output);
+ try writer.writeAll(output);
+ },
+ }
+ }
+
+ try writer.writeAll(
+ \\};
+ \\
+ \\
+ );
+}
+
+// Generates a Zmpl template for a corresponding view + action.
+fn writeTemplate(allocator: std.mem.Allocator, cwd: std.fs.Dir, name: []const u8, action: Action) !void {
+ const path = try std.fs.path.join(allocator, &[_][]const u8{
+ "src",
+ "app",
+ "views",
+ name,
+ });
+ defer allocator.free(path);
+
+ var view_dir = try cwd.makeOpenPath(path, .{});
+ defer view_dir.close();
+
+ const filename = try std.mem.concat(allocator, u8, &[_][]const u8{ @tagName(action.method), ".zmpl" });
+ defer allocator.free(filename);
+
+ const file = view_dir.createFile(filename, .{ .exclusive = true }) catch |err| {
+ switch (err) {
+ error.PathAlreadyExists => {
+ std.debug.print("Path already exists, skipping template creation: {s}\n", .{filename});
+ return;
+ },
+ else => return err,
+ }
+ };
+
+ try file.writeAll(
+ \\
+ \\ Content goes here
+ \\
+ \\
+ );
+
+ file.close();
+
+ const realpath = try view_dir.realpathAlloc(allocator, filename);
+ defer allocator.free(realpath);
+ std.debug.print("Generated template: {s}\n", .{realpath});
+}
diff --git a/cli/init.zig b/cli/commands/init.zig
similarity index 89%
rename from cli/init.zig
rename to cli/commands/init.zig
index c718948..4244088 100644
--- a/cli/init.zig
+++ b/cli/commands/init.zig
@@ -1,18 +1,10 @@
const std = @import("std");
const args = @import("args");
+const util = @import("../util.zig");
+
const init_data = @import("init_data").init_data;
-fn base64Decode(allocator: std.mem.Allocator, input: []const u8) ![]const u8 {
- const decoder = std.base64.Base64Decoder.init(
- std.base64.url_safe_no_pad.alphabet_chars,
- std.base64.url_safe_no_pad.pad_char,
- );
- const size = try decoder.calcSizeForSlice(input);
- const ptr = try allocator.alloc(u8, size);
- try decoder.decode(ptr, input);
- return ptr;
-}
-
+/// Command line options for the `init` command.
pub const Options = struct {
path: ?[]const u8 = null,
@@ -50,7 +42,7 @@ pub fn run(
for (positionals) |arg| {
if (install_path != null) {
std.debug.print("Unexpected positional argument: {s}\n", .{arg});
- return error.JetzigUnexpectedPositionalArgumentsError;
+ return error.JetzigCommandError;
}
install_path = arg;
}
@@ -230,7 +222,7 @@ fn runCommand(allocator: std.mem.Allocator, install_path: []const u8, argv: []co
std.debug.print("[exec] {s}", .{command});
if (result.term.Exited != 0) {
- printFailure();
+ util.printFailure();
std.debug.print(
\\
\\Error running command: {s}
@@ -244,9 +236,9 @@ fn runCommand(allocator: std.mem.Allocator, install_path: []const u8, argv: []co
\\{s}
\\
, .{ command, result.stdout, result.stderr });
- return error.JetzigRunCommandError;
+ return error.JetzigCommandError;
} else {
- printSuccess();
+ util.printSuccess();
}
}
@@ -267,7 +259,7 @@ fn copySourceFile(
var content: []const u8 = undefined;
if (replace) |capture| {
const initial = readSourceFile(allocator, src) catch |err| {
- printFailure();
+ util.printFailure();
return err;
};
defer allocator.free(initial);
@@ -276,25 +268,25 @@ fn copySourceFile(
}
} else {
content = readSourceFile(allocator, src) catch |err| {
- printFailure();
+ util.printFailure();
return err;
};
}
defer allocator.free(content);
writeSourceFile(install_dir, dest, content) catch |err| {
- printFailure();
+ util.printFailure();
return err;
};
- printSuccess();
+ util.printSuccess();
}
// Read a file from Jetzig source code.
fn readSourceFile(allocator: std.mem.Allocator, path: []const u8) ![]const u8 {
inline for (init_data) |file| {
- if (std.mem.eql(u8, path, file.path)) return try base64Decode(allocator, file.data);
+ if (std.mem.eql(u8, path, file.path)) return try util.base64Decode(allocator, file.data);
}
- return error.SourceFileNotFound;
+ return error.JetzigCommandError;
}
// Write a file to the new project's directory.
@@ -335,7 +327,7 @@ fn githubUrl(allocator: std.mem.Allocator) ![]const u8 {
if (fetch_result.status != .ok) {
std.debug.print("Error fetching from GitHub: {s}\n", .{url});
- return error.JetzigGitHubFetchError;
+ return error.JetzigCommandError;
}
const parsed_response = try std.json.parseFromSlice(
@@ -381,20 +373,15 @@ fn promptInput(
const input = try reader.readUntilDelimiterOrEofAlloc(allocator, '\n', max_read_bytes);
if (input) |capture| {
defer allocator.free(capture);
- const stripped_input = strip(capture);
+ const stripped_input = util.strip(capture);
if (std.mem.eql(u8, stripped_input, "")) {
- if (options.default) |default| return try allocator.dupe(u8, strip(default));
+ if (options.default) |default| return try allocator.dupe(u8, util.strip(default));
} else return try allocator.dupe(u8, stripped_input);
}
}
}
-// Strip leading and trailing whitespace from a u8 slice.
-fn strip(input: []const u8) []const u8 {
- return std.mem.trim(u8, input, &std.ascii.whitespace);
-}
-
// Initialize a new Git repository when setting up a new project (optional).
fn gitSetup(allocator: std.mem.Allocator, install_dir: *std.fs.Dir) !void {
try runCommand(allocator, install_dir, &[_][]const u8{
@@ -416,13 +403,3 @@ fn gitSetup(allocator: std.mem.Allocator, install_dir: *std.fs.Dir) !void {
"Initialize Jetzig project",
});
}
-
-/// Print a success confirmation.
-fn printSuccess() void {
- std.debug.print(" ✅\n", .{});
-}
-
-/// Print a failure confirmation.
-fn printFailure() void {
- std.debug.print(" ❌\n", .{});
-}
diff --git a/cli/util.zig b/cli/util.zig
new file mode 100644
index 0000000..449b90e
--- /dev/null
+++ b/cli/util.zig
@@ -0,0 +1,96 @@
+const std = @import("std");
+
+/// Decode a base64 string, used for parsing out build artifacts generated by the CLI program's
+/// build.zig which are stored in the executable as a module.
+pub fn base64Decode(allocator: std.mem.Allocator, input: []const u8) ![]const u8 {
+ const decoder = std.base64.Base64Decoder.init(
+ std.base64.url_safe_no_pad.alphabet_chars,
+ std.base64.url_safe_no_pad.pad_char,
+ );
+ const size = try decoder.calcSizeForSlice(input);
+ const ptr = try allocator.alloc(u8, size);
+ try decoder.decode(ptr, input);
+ return ptr;
+}
+
+/// Print a success confirmation.
+pub fn printSuccess() void {
+ std.debug.print(" ✅\n", .{});
+}
+
+/// Print a failure confirmation.
+pub fn printFailure() void {
+ std.debug.print(" ❌\n", .{});
+}
+
+/// Verifies that cwd is the root of a Jetzig project
+pub fn detectJetzigProjectDir() !std.fs.Dir {
+ var dir = try std.fs.cwd().openDir(".", .{});
+ const max_parent_dirs: usize = 100; // Prevent symlink loops or other weird stuff.
+
+ for (0..max_parent_dirs) |_| {
+ if (try isPath(dir, "build.zig", .file) and try isPath(dir, "src/app/views", .dir)) return dir;
+
+ dir = dir.openDir("..", .{}) catch |err| {
+ switch (err) {
+ error.FileNotFound, error.NotDir => {
+ std.debug.print(
+ "Encountered unexpected detecting Jetzig project directory: {s}\n",
+ .{@errorName(err)},
+ );
+ return error.JetzigCommandError;
+ },
+ else => return err,
+ }
+ };
+ continue;
+ }
+
+ std.debug.print(
+ \\Exceeded maximum parent directory depth.
+ \\Unable to detect Jetzig project directory.
+ \\
+ ,
+ .{},
+ );
+ return error.JetzigCommandError;
+}
+
+fn isPath(dir: std.fs.Dir, sub_path: []const u8, path_type: enum { file, dir }) !bool {
+ switch (path_type) {
+ .file => {
+ _ = dir.statFile(sub_path) catch |err| {
+ switch (err) {
+ error.FileNotFound => return false,
+ else => return err,
+ }
+ };
+ return true;
+ },
+ .dir => {
+ var test_dir = dir.openDir(sub_path, .{}) catch |err| {
+ switch (err) {
+ error.FileNotFound, error.NotDir => return false,
+ else => return err,
+ }
+ };
+ test_dir.close();
+ return true;
+ },
+ }
+}
+
+// Strip leading and trailing whitespace from a u8 slice.
+pub fn strip(input: []const u8) []const u8 {
+ return std.mem.trim(u8, input, &std.ascii.whitespace);
+}
+
+/// Attempts to confirm if a string input is in CamelCase. False if the first character is
+/// not alphabetic lower-case or if the input contains underscores.
+pub fn isCamelCase(input: []const u8) bool {
+ if (input.len == 0) return false;
+ if (!std.mem.containsAtLeast(u8, "ABCDEFGHIJKLMNOPQRSTUVWXYZ", 1, &[_]u8{input[0]})) return false;
+ if (std.mem.containsAtLeast(u8, input, 1, "_")) return false;
+
+ return true;
+}
diff --git a/demo/src/app/lib/example.zig b/demo/src/app/lib/example.zig
new file mode 100644
index 0000000..eb10c1f
--- /dev/null
+++ b/demo/src/app/lib/example.zig
@@ -0,0 +1,3 @@
+pub fn exampleFunction() []const u8 {
+ return "example value";
+}
diff --git a/src/cli.gitignore b/src/cli.gitignore
new file mode 100644
index 0000000..ee7098f
--- /dev/null
+++ b/src/cli.gitignore
@@ -0,0 +1,2 @@
+zig-out/
+zig-cache/