mirror of
https://github.com/jetzig-framework/jetzig.git
synced 2025-05-14 22:16:08 +00:00
Merge pull request #14 from jetzig-framework/cli-generate
CLI generate command
This commit is contained in:
commit
647833ca5a
12
.github/workflows/CI.yml
vendored
12
.github/workflows/CI.yml
vendored
@ -24,7 +24,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
|
||||
@ -32,10 +32,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
|
||||
|
||||
@ -51,13 +51,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
|
||||
@ -74,7 +74,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' }}
|
||||
|
46
cli/cli.zig
46
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 },
|
||||
),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
81
cli/commands/generate.zig
Normal file
81
cli/commands/generate.zig
Normal file
@ -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;
|
||||
}
|
||||
}
|
101
cli/commands/generate/middleware.zig
Normal file
101
cli/commands/generate/middleware.zig
Normal file
@ -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);
|
||||
\\}
|
||||
\\
|
||||
;
|
46
cli/commands/generate/partial.zig
Normal file
46
cli/commands/generate/partial.zig
Normal file
@ -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(
|
||||
\\<div>Partial content goes here.</div>
|
||||
\\
|
||||
);
|
||||
|
||||
file.close();
|
||||
|
||||
const realpath = try dir.realpathAlloc(allocator, filename);
|
||||
defer allocator.free(realpath);
|
||||
std.debug.print("Generated partial template: {s}\n", .{realpath});
|
||||
}
|
220
cli/commands/generate/view.zig
Normal file
220
cli/commands/generate/view.zig
Normal file
@ -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(
|
||||
\\<div>
|
||||
\\ <span>Content goes here</span>
|
||||
\\</div>
|
||||
\\
|
||||
);
|
||||
|
||||
file.close();
|
||||
|
||||
const realpath = try view_dir.realpathAlloc(allocator, filename);
|
||||
defer allocator.free(realpath);
|
||||
std.debug.print("Generated template: {s}\n", .{realpath});
|
||||
}
|
@ -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", .{});
|
||||
}
|
96
cli/util.zig
Normal file
96
cli/util.zig
Normal file
@ -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;
|
||||
}
|
3
demo/src/app/lib/example.zig
Normal file
3
demo/src/app/lib/example.zig
Normal file
@ -0,0 +1,3 @@
|
||||
pub fn exampleFunction() []const u8 {
|
||||
return "example value";
|
||||
}
|
2
src/cli.gitignore
Normal file
2
src/cli.gitignore
Normal file
@ -0,0 +1,2 @@
|
||||
zig-out/
|
||||
zig-cache/
|
Loading…
x
Reference in New Issue
Block a user