mirror of
https://github.com/jetzig-framework/jetzig.git
synced 2025-05-14 05:56:07 +00:00
Implement init command, get rid of old init script/artifacts
Remove old bash script for setting up a new project, do everything in Zig to make it platform agnostic and give us an easy place to add scaffolding commands in future.
This commit is contained in:
parent
01ac93ce81
commit
d3a3582136
5
.gitignore
vendored
5
.gitignore
vendored
@ -1,9 +1,4 @@
|
||||
TODO.md
|
||||
main
|
||||
get/
|
||||
zig-out/
|
||||
zig-cache/
|
||||
*.core
|
||||
src/app/views/**/*.compiled.zig
|
||||
archive.tar.gz
|
||||
static/
|
||||
|
BIN
archive.tar.gz
Normal file
BIN
archive.tar.gz
Normal file
Binary file not shown.
@ -22,7 +22,7 @@ pub fn build(b: *std.Build) !void {
|
||||
|
||||
const mime_module = try GenerateMimeTypes.generateMimeModule(b);
|
||||
|
||||
b.installArtifact(lib);
|
||||
const zig_args_dep = b.dependency("args", .{ .target = target, .optimize = optimize });
|
||||
|
||||
const jetzig_module = b.addModule("jetzig", .{ .root_source_file = .{ .path = "src/jetzig.zig" } });
|
||||
jetzig_module.addImport("mime_types", mime_module);
|
||||
@ -39,6 +39,7 @@ pub fn build(b: *std.Build) !void {
|
||||
|
||||
lib.root_module.addImport("zmpl", zmpl_dep.module("zmpl"));
|
||||
jetzig_module.addImport("zmpl", zmpl_dep.module("zmpl"));
|
||||
lib.root_module.addImport("args", zig_args_dep.module("args"));
|
||||
|
||||
// This is the way to make it look nice in the zig build script
|
||||
// If we would do it the other way around, we would have to do
|
||||
|
@ -3,8 +3,12 @@
|
||||
.version = "0.0.0",
|
||||
.dependencies = .{
|
||||
.zmpl = .{
|
||||
.url = "https://github.com/jetzig-framework/zmpl/archive/aa7147f8a52d927dce6cdd4f5b3bb2de5080f28c.tar.gz",
|
||||
.hash = "12203d262b39b2328adb981e41c5127507f3d47e977c1a4e69a96688a4213b986d04",
|
||||
.url = "https://github.com/jetzig-framework/zmpl/archive/ed99a1604b37fb05b0f5843a3288588f3dfe2e63.tar.gz",
|
||||
.hash = "1220771fe742fc620872051b92082d370549ed857e5a93ae43f92c5767ca2aaf42b1",
|
||||
},
|
||||
.args = .{
|
||||
.url = "https://github.com/MasterQ32/zig-args/archive/89f18a104d9c13763b90e97d6b4ce133da8a3e2b.tar.gz",
|
||||
.hash = "12203ded54c85878eea7f12744066dcb4397177395ac49a7b2aa365bf6047b623829",
|
||||
},
|
||||
},
|
||||
|
||||
|
33
cli/build.zig
Normal file
33
cli/build.zig
Normal file
@ -0,0 +1,33 @@
|
||||
const std = @import("std");
|
||||
|
||||
const compile = @import("compile.zig");
|
||||
|
||||
pub fn build(b: *std.Build) !void {
|
||||
const target = b.standardTargetOptions(.{});
|
||||
const optimize = b.standardOptimizeOption(.{});
|
||||
|
||||
const exe = b.addExecutable(.{
|
||||
.name = "jetzig",
|
||||
.root_source_file = .{ .path = "cli.zig" },
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
});
|
||||
|
||||
const zig_args_dep = b.dependency("args", .{ .target = target, .optimize = optimize });
|
||||
|
||||
exe.root_module.addImport("args", zig_args_dep.module("args"));
|
||||
exe.root_module.addImport(
|
||||
"init_data",
|
||||
try compile.initDataModule(b),
|
||||
);
|
||||
|
||||
b.installArtifact(exe);
|
||||
|
||||
const run_cmd = b.addRunArtifact(exe);
|
||||
run_cmd.step.dependOn(b.getInstallStep());
|
||||
if (b.args) |args| {
|
||||
run_cmd.addArgs(args);
|
||||
}
|
||||
const run_step = b.step("run", "Run the app");
|
||||
run_step.dependOn(&run_cmd.step);
|
||||
}
|
15
cli/build.zig.zon
Normal file
15
cli/build.zig.zon
Normal file
@ -0,0 +1,15 @@
|
||||
.{
|
||||
.name = "jetzig-cli",
|
||||
.version = "0.0.0",
|
||||
.minimum_zig_version = "0.12.0",
|
||||
|
||||
.dependencies = .{
|
||||
.args = .{
|
||||
.url = "https://github.com/MasterQ32/zig-args/archive/89f18a104d9c13763b90e97d6b4ce133da8a3e2b.tar.gz",
|
||||
.hash = "12203ded54c85878eea7f12744066dcb4397177395ac49a7b2aa365bf6047b623829",
|
||||
},
|
||||
},
|
||||
.paths = .{
|
||||
"",
|
||||
},
|
||||
}
|
60
cli/cli.zig
Normal file
60
cli/cli.zig
Normal file
@ -0,0 +1,60 @@
|
||||
const std = @import("std");
|
||||
const args = @import("args");
|
||||
const init = @import("init.zig");
|
||||
|
||||
const Options = struct {
|
||||
help: bool = false,
|
||||
|
||||
pub const shorthands = .{
|
||||
.h = "help",
|
||||
};
|
||||
|
||||
pub const meta = .{
|
||||
.option_docs = .{
|
||||
.init = "Initialize a new project",
|
||||
.help = "Print help and exit",
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
const Verb = union(enum) {
|
||||
init: init.Options,
|
||||
};
|
||||
|
||||
/// Main entrypoint for `jetzig` executable. Parses command line args and generates a new
|
||||
/// project, scaffolding, etc.
|
||||
pub fn main() !void {
|
||||
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
|
||||
const allocator = gpa.allocator();
|
||||
defer std.debug.assert(gpa.deinit() == .ok);
|
||||
|
||||
const options = try args.parseWithVerbForCurrentProcess(Options, Verb, allocator, .print);
|
||||
defer options.deinit();
|
||||
|
||||
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 },
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
if (options.options.help) {
|
||||
try args.printHelp(Options, "jetzig", writer);
|
||||
try writer.writeAll(
|
||||
\\
|
||||
\\Commands:
|
||||
\\
|
||||
\\ init Initialize a new project.
|
||||
\\
|
||||
\\ Pass --help to any command for more information, e.g. `jetzig init --help`
|
||||
\\
|
||||
);
|
||||
}
|
||||
}
|
71
cli/compile.zig
Normal file
71
cli/compile.zig
Normal file
@ -0,0 +1,71 @@
|
||||
const std = @import("std");
|
||||
|
||||
fn base64Encode(allocator: std.mem.Allocator, input: []const u8) []const u8 {
|
||||
const encoder = std.base64.Base64Encoder.init(
|
||||
std.base64.url_safe_no_pad.alphabet_chars,
|
||||
std.base64.url_safe_no_pad.pad_char,
|
||||
);
|
||||
const size = encoder.calcSize(input.len);
|
||||
const ptr = allocator.alloc(u8, size) catch @panic("OOM");
|
||||
_ = encoder.encode(ptr, input);
|
||||
return ptr;
|
||||
}
|
||||
|
||||
pub fn initDataModule(build: *std.Build) !*std.Build.Module {
|
||||
const root_path = build.pathFromRoot("..");
|
||||
|
||||
var buf = std.ArrayList(u8).init(build.allocator);
|
||||
defer buf.deinit();
|
||||
|
||||
const writer = buf.writer();
|
||||
|
||||
const paths = .{
|
||||
"demo/build.zig",
|
||||
"demo/src/main.zig",
|
||||
"demo/src/app/middleware/DemoMiddleware.zig",
|
||||
"demo/src/app/views/init.zig",
|
||||
"demo/src/app/views/init/index.zmpl",
|
||||
"demo/src/app/views/init/_content.zmpl",
|
||||
"demo/public/jetzig.png",
|
||||
"demo/public/zmpl.png",
|
||||
"demo/public/favicon.ico",
|
||||
"demo/public/styles.css",
|
||||
".gitignore",
|
||||
};
|
||||
|
||||
try writer.writeAll(
|
||||
\\pub const init_data = .{
|
||||
\\
|
||||
);
|
||||
|
||||
var dir = try std.fs.openDirAbsolute(root_path, .{});
|
||||
defer dir.close();
|
||||
|
||||
inline for (paths) |path| {
|
||||
const stat = try dir.statFile(path);
|
||||
const encoded = base64Encode(
|
||||
build.allocator,
|
||||
try dir.readFileAlloc(build.allocator, path, stat.size),
|
||||
);
|
||||
defer build.allocator.free(encoded);
|
||||
|
||||
const output = try std.fmt.allocPrint(
|
||||
build.allocator,
|
||||
\\.{{ .path = "{s}", .data = "{s}" }},
|
||||
,
|
||||
.{ path, encoded },
|
||||
);
|
||||
defer build.allocator.free(output);
|
||||
|
||||
try writer.writeAll(output);
|
||||
}
|
||||
|
||||
try writer.writeAll(
|
||||
\\};
|
||||
\\
|
||||
);
|
||||
|
||||
const write_files = build.addWriteFiles();
|
||||
const init_data_source = write_files.add("init_data.zig", buf.items);
|
||||
return build.createModule(.{ .root_source_file = init_data_source });
|
||||
}
|
427
cli/init.zig
Normal file
427
cli/init.zig
Normal file
@ -0,0 +1,427 @@
|
||||
const std = @import("std");
|
||||
const args = @import("args");
|
||||
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;
|
||||
}
|
||||
|
||||
pub const Options = struct {
|
||||
path: ?[]const u8 = null,
|
||||
|
||||
pub const shorthands = .{
|
||||
.p = "path",
|
||||
};
|
||||
|
||||
pub const meta = .{
|
||||
.usage_summary = "[--path PATH]",
|
||||
.full_text =
|
||||
\\Initializes a new Jetzig project in the current directory or attempts to
|
||||
\\create a new directory specified by PATH
|
||||
\\
|
||||
\\Creates build.zig, build.zig.zon, src/main.zig, and an example view with a template.
|
||||
\\
|
||||
\\Run `zig build run` to launch a development server when complete.
|
||||
,
|
||||
.option_docs = .{
|
||||
.path = "Set the output path relative to the current directory (default: current directory)",
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
/// 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 {
|
||||
_ = options;
|
||||
var install_path: ?[]const u8 = null;
|
||||
|
||||
for (positionals) |arg| {
|
||||
if (install_path != null) {
|
||||
std.debug.print("Unexpected positional argument: {s}\n", .{arg});
|
||||
return error.JetzigUnexpectedPositionalArgumentsError;
|
||||
}
|
||||
install_path = arg;
|
||||
}
|
||||
|
||||
const github_url = try githubUrl(allocator);
|
||||
defer allocator.free(github_url);
|
||||
|
||||
if (other_options.help) {
|
||||
try args.printHelp(Options, "jetzig init", writer);
|
||||
return;
|
||||
}
|
||||
|
||||
var install_dir: std.fs.Dir = undefined;
|
||||
defer install_dir.close();
|
||||
|
||||
var project_name: []const u8 = undefined;
|
||||
defer allocator.free(project_name);
|
||||
|
||||
if (install_path) |path| {
|
||||
install_dir = try std.fs.cwd().makeOpenPath(path, .{});
|
||||
project_name = try allocator.dupe(u8, std.fs.path.basename(path));
|
||||
} else {
|
||||
const cwd_realpath = try std.fs.cwd().realpathAlloc(allocator, ".");
|
||||
defer allocator.free(cwd_realpath);
|
||||
|
||||
const default_project_name = std.fs.path.basename(cwd_realpath);
|
||||
project_name = try promptInput(allocator, "Project name", .{ .default = default_project_name });
|
||||
const sub_path = if (std.mem.eql(u8, project_name, default_project_name)) "" else project_name;
|
||||
|
||||
const default_install_path = try std.fs.path.join(
|
||||
allocator,
|
||||
&[_][]const u8{ cwd_realpath, sub_path },
|
||||
);
|
||||
defer allocator.free(default_install_path);
|
||||
|
||||
const input_install_path = try promptInput(
|
||||
allocator,
|
||||
"Install path",
|
||||
.{ .default = default_install_path },
|
||||
);
|
||||
defer allocator.free(input_install_path);
|
||||
install_dir = try std.fs.cwd().makeOpenPath(input_install_path, .{});
|
||||
}
|
||||
|
||||
const real_path = try install_dir.realpathAlloc(allocator, ".");
|
||||
defer allocator.free(real_path);
|
||||
|
||||
const output = try std.fmt.allocPrint(allocator, "Creating new project in {s}\n\n", .{real_path});
|
||||
defer allocator.free(output);
|
||||
try writer.writeAll(output);
|
||||
|
||||
try copySourceFile(
|
||||
allocator,
|
||||
install_dir,
|
||||
"demo/build.zig",
|
||||
"build.zig",
|
||||
&[_]Replace{.{ .from = "jetzig-demo", .to = project_name }},
|
||||
);
|
||||
|
||||
try copySourceFile(
|
||||
allocator,
|
||||
install_dir,
|
||||
"demo/src/main.zig",
|
||||
"src/main.zig",
|
||||
null,
|
||||
);
|
||||
|
||||
try copySourceFile(
|
||||
allocator,
|
||||
install_dir,
|
||||
"demo/src/app/middleware/DemoMiddleware.zig",
|
||||
"src/app/middleware/DemoMiddleware.zig",
|
||||
null,
|
||||
);
|
||||
|
||||
try copySourceFile(
|
||||
allocator,
|
||||
install_dir,
|
||||
"demo/src/app/views/init.zig",
|
||||
"src/app/views/root.zig",
|
||||
null,
|
||||
);
|
||||
|
||||
try copySourceFile(
|
||||
allocator,
|
||||
install_dir,
|
||||
"demo/src/app/views/init/index.zmpl",
|
||||
"src/app/views/root/index.zmpl",
|
||||
&[_]Replace{
|
||||
.{ .from = "init/", .to = "root/" },
|
||||
},
|
||||
);
|
||||
|
||||
try copySourceFile(
|
||||
allocator,
|
||||
install_dir,
|
||||
"demo/src/app/views/init/_content.zmpl",
|
||||
"src/app/views/root/_content.zmpl",
|
||||
null,
|
||||
);
|
||||
|
||||
try copySourceFile(
|
||||
allocator,
|
||||
install_dir,
|
||||
"demo/public/jetzig.png",
|
||||
"public/jetzig.png",
|
||||
null,
|
||||
);
|
||||
|
||||
try copySourceFile(
|
||||
allocator,
|
||||
install_dir,
|
||||
"demo/public/zmpl.png",
|
||||
"public/zmpl.png",
|
||||
null,
|
||||
);
|
||||
|
||||
try copySourceFile(
|
||||
allocator,
|
||||
install_dir,
|
||||
"demo/public/favicon.ico",
|
||||
"public/favicon.ico",
|
||||
null,
|
||||
);
|
||||
|
||||
try copySourceFile(
|
||||
allocator,
|
||||
install_dir,
|
||||
"demo/public/styles.css",
|
||||
"public/styles.css",
|
||||
null,
|
||||
);
|
||||
|
||||
try copySourceFile(
|
||||
allocator,
|
||||
install_dir,
|
||||
".gitignore",
|
||||
".gitignore",
|
||||
null,
|
||||
);
|
||||
|
||||
try runCommand(allocator, install_dir, &[_][]const u8{
|
||||
"zig",
|
||||
"fetch",
|
||||
"--save",
|
||||
github_url,
|
||||
});
|
||||
|
||||
// TODO: Use arg or interactive prompt to do Git setup in net project, default to no.
|
||||
// const git_setup = false;
|
||||
// if (git_setup) try gitSetup(allocator, install_dir);
|
||||
|
||||
std.debug.print(
|
||||
\\
|
||||
\\Setup complete! ✈️ 🦎
|
||||
\\
|
||||
\\Launch your new application:
|
||||
\\
|
||||
\\ $ cd {s}
|
||||
\\
|
||||
\\ $ zig build run
|
||||
\\
|
||||
\\And then browse to http://localhost:8080/
|
||||
\\
|
||||
\\
|
||||
, .{real_path});
|
||||
}
|
||||
|
||||
fn runCommand(allocator: std.mem.Allocator, install_dir: std.fs.Dir, argv: []const []const u8) !void {
|
||||
const result = try std.process.Child.run(.{ .allocator = allocator, .argv = argv, .cwd_dir = install_dir });
|
||||
defer allocator.free(result.stdout);
|
||||
defer allocator.free(result.stderr);
|
||||
|
||||
const command = try std.mem.join(allocator, " ", argv);
|
||||
defer allocator.free(command);
|
||||
|
||||
std.debug.print("[exec] {s}", .{command});
|
||||
|
||||
if (result.term.Exited != 0) {
|
||||
printFailure();
|
||||
std.debug.print(
|
||||
\\
|
||||
\\Error running command: {s}
|
||||
\\
|
||||
\\[stdout]:
|
||||
\\
|
||||
\\{s}
|
||||
\\
|
||||
\\[stderr]:
|
||||
\\
|
||||
\\{s}
|
||||
\\
|
||||
, .{ command, result.stdout, result.stderr });
|
||||
return error.JetzigRunCommandError;
|
||||
} else {
|
||||
printSuccess();
|
||||
}
|
||||
}
|
||||
|
||||
const Replace = struct {
|
||||
from: []const u8,
|
||||
to: []const u8,
|
||||
};
|
||||
|
||||
fn copySourceFile(
|
||||
allocator: std.mem.Allocator,
|
||||
install_dir: std.fs.Dir,
|
||||
src: []const u8,
|
||||
dest: []const u8,
|
||||
replace: ?[]const Replace,
|
||||
) !void {
|
||||
std.debug.print("[create] {s}", .{dest});
|
||||
|
||||
var content: []const u8 = undefined;
|
||||
if (replace) |capture| {
|
||||
const initial = readSourceFile(allocator, src) catch |err| {
|
||||
printFailure();
|
||||
return err;
|
||||
};
|
||||
defer allocator.free(initial);
|
||||
for (capture) |item| {
|
||||
content = try std.mem.replaceOwned(u8, allocator, initial, item.from, item.to);
|
||||
}
|
||||
} else {
|
||||
content = readSourceFile(allocator, src) catch |err| {
|
||||
printFailure();
|
||||
return err;
|
||||
};
|
||||
}
|
||||
defer allocator.free(content);
|
||||
|
||||
writeSourceFile(install_dir, dest, content) catch |err| {
|
||||
printFailure();
|
||||
return err;
|
||||
};
|
||||
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);
|
||||
}
|
||||
return error.SourceFileNotFound;
|
||||
}
|
||||
|
||||
// Write a file to the new project's directory.
|
||||
fn writeSourceFile(install_dir: std.fs.Dir, path: []const u8, content: []const u8) !void {
|
||||
// TODO: Detect presence and ask for confirmation if necessary.
|
||||
if (std.fs.path.dirname(path)) |dirname| {
|
||||
var dir = try install_dir.makeOpenPath(dirname, .{});
|
||||
defer dir.close();
|
||||
|
||||
const file = try dir.createFile(std.fs.path.basename(path), .{ .truncate = true });
|
||||
defer file.close();
|
||||
|
||||
try file.writeAll(content);
|
||||
} else {
|
||||
const file = try install_dir.createFile(path, .{ .truncate = true });
|
||||
defer file.close();
|
||||
|
||||
try file.writeAll(content);
|
||||
}
|
||||
}
|
||||
|
||||
// Generate a full GitHub URL for passing to `zig fetch`.
|
||||
fn githubUrl(allocator: std.mem.Allocator) ![]const u8 {
|
||||
var client = std.http.Client{ .allocator = allocator };
|
||||
defer client.deinit();
|
||||
|
||||
const url = "https://api.github.com/repos/jetzig-framework/jetzig/branches/main";
|
||||
const extra_headers = &[_]std.http.Header{.{ .name = "X-GitHub-Api-Version", .value = "2022-11-28" }};
|
||||
|
||||
var response_storage = std.ArrayList(u8).init(allocator);
|
||||
defer response_storage.deinit();
|
||||
|
||||
const fetch_result = try client.fetch(.{
|
||||
.location = .{ .url = url },
|
||||
.extra_headers = extra_headers,
|
||||
.response_storage = .{ .dynamic = &response_storage },
|
||||
});
|
||||
|
||||
if (fetch_result.status != .ok) {
|
||||
std.debug.print("Error fetching from GitHub: {s}\n", .{url});
|
||||
return error.JetzigGitHubFetchError;
|
||||
}
|
||||
|
||||
const parsed_response = try std.json.parseFromSlice(
|
||||
struct { commit: struct { sha: []const u8 } },
|
||||
allocator,
|
||||
response_storage.items,
|
||||
.{ .ignore_unknown_fields = true },
|
||||
);
|
||||
defer parsed_response.deinit();
|
||||
|
||||
return try std.mem.concat(
|
||||
allocator,
|
||||
u8,
|
||||
&[_][]const u8{
|
||||
"https://github.com/jetzig-framework/jetzig/archive/",
|
||||
parsed_response.value.commit.sha,
|
||||
".tar.gz",
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// Prompt a user for input and return the result. Accepts an optional default value.
|
||||
fn promptInput(
|
||||
allocator: std.mem.Allocator,
|
||||
prompt: []const u8,
|
||||
options: struct { default: ?[]const u8 },
|
||||
) ![]const u8 {
|
||||
const stdin = std.io.getStdIn();
|
||||
const reader = stdin.reader();
|
||||
|
||||
const max_read_bytes = 1024;
|
||||
|
||||
while (true) {
|
||||
if (options.default) |default| {
|
||||
std.debug.print(
|
||||
\\{s} [default: "{s}"]:
|
||||
, .{ prompt, default });
|
||||
} else {
|
||||
std.debug.print(
|
||||
\\{s}:
|
||||
, .{prompt});
|
||||
}
|
||||
const input = try reader.readUntilDelimiterOrEofAlloc(allocator, '\n', max_read_bytes);
|
||||
if (input) |capture| {
|
||||
defer allocator.free(capture);
|
||||
|
||||
if (std.mem.eql(u8, capture, "")) {
|
||||
if (options.default) |default| return try allocator.dupe(u8, strip(default));
|
||||
} else return try allocator.dupe(u8, strip(capture));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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{
|
||||
"git",
|
||||
"init",
|
||||
".",
|
||||
});
|
||||
|
||||
try runCommand(allocator, install_dir, &[_][]const u8{
|
||||
"git",
|
||||
"add",
|
||||
".",
|
||||
});
|
||||
|
||||
try runCommand(allocator, install_dir, &[_][]const u8{
|
||||
"git",
|
||||
"commit",
|
||||
"-m",
|
||||
"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", .{});
|
||||
}
|
@ -1,5 +1,5 @@
|
||||
.{
|
||||
.name = "sandbox-jetzig",
|
||||
.name = "jetzig-demo",
|
||||
.version = "0.0.0",
|
||||
.minimum_zig_version = "0.12.0",
|
||||
.dependencies = .{
|
||||
|
@ -0,0 +1,10 @@
|
||||
/* Root stylesheet. Load into your Zmpl template with:
|
||||
*
|
||||
* <link rel="stylesheet" href="/styles.css" />
|
||||
*
|
||||
*/
|
||||
|
||||
.message {
|
||||
font-weight: bold;
|
||||
font-size: 3rem;
|
||||
}
|
@ -1,26 +0,0 @@
|
||||
const std = @import("std");
|
||||
const jetzig = @import("jetzig");
|
||||
|
||||
my_data: u8,
|
||||
|
||||
const Self = @This();
|
||||
|
||||
pub fn init(request: *jetzig.http.Request) !*Self {
|
||||
var middleware = try request.allocator.create(Self);
|
||||
middleware.my_data = 42;
|
||||
return middleware;
|
||||
}
|
||||
|
||||
pub fn beforeRequest(self: *Self, request: *jetzig.http.Request) !void {
|
||||
request.server.logger.debug("[DemoMiddleware] Before request, custom data: {d}", .{self.my_data});
|
||||
self.my_data = 43;
|
||||
}
|
||||
|
||||
pub fn afterRequest(self: *Self, request: *jetzig.http.Request, response: *jetzig.http.Response) !void {
|
||||
request.server.logger.debug("[DemoMiddleware] After request, custom data: {d}", .{self.my_data});
|
||||
request.server.logger.debug("[DemoMiddleware] content-type: {s}", .{response.content_type});
|
||||
}
|
||||
|
||||
pub fn deinit(self: *Self, request: *jetzig.http.Request) void {
|
||||
request.allocator.destroy(self);
|
||||
}
|
53
demo/src/app/middleware/DemoMiddleware.zig
Normal file
53
demo/src/app/middleware/DemoMiddleware.zig
Normal file
@ -0,0 +1,53 @@
|
||||
/// Demo middleware. Assign middleware by declaring `pub const middleware` in the
|
||||
/// `jetzig_options` defined in your application's `src/main.zig`.
|
||||
///
|
||||
/// Middleware is called before and after the request, providing full access to the active
|
||||
/// request, allowing you to execute any custom code for logging, tracking, inserting response
|
||||
/// headers, etc.
|
||||
///
|
||||
/// This middleware is configured in the demo app's `src/main.zig`:
|
||||
///
|
||||
/// ```
|
||||
/// pub const jetzig_options = struct {
|
||||
/// pub const middleware: []const type = &.{@import("app/middleware/DemoMiddleware.zig")};
|
||||
/// };
|
||||
/// ```
|
||||
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("[DemoMiddleware] 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(
|
||||
"[DemoMiddleware] 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);
|
||||
}
|
43
demo/src/app/views/init.zig
Normal file
43
demo/src/app/views/init.zig
Normal file
@ -0,0 +1,43 @@
|
||||
const jetzig = @import("jetzig");
|
||||
|
||||
/// `src/app/views/root.zig` represents the root URL `/`
|
||||
/// The `index` view function is invoked when when the HTTP verb is `GET`.
|
||||
/// Other view types are invoked either by passing a resource ID value (e.g. `/1234`) or by using
|
||||
/// a different HTTP verb:
|
||||
///
|
||||
/// GET / => index(request, data)
|
||||
/// GET /1234 => get(id, request, data)
|
||||
/// POST / => post(request, data)
|
||||
/// PUT /1234 => put(id, request, data)
|
||||
/// PATCH /1234 => patch(id, request, data)
|
||||
/// DELETE /1234 => delete(id, request, data)
|
||||
pub fn index(request: *jetzig.Request, data: *jetzig.Data) !jetzig.View {
|
||||
// The first call to `data.object()` or `data.array()` sets the root response data value.
|
||||
// JSON requests return a JSON string representation of the root data value.
|
||||
// Zmpl templates can access all values in the root data value.
|
||||
var root = try data.object();
|
||||
|
||||
// Add a string to the root object.
|
||||
try root.put("message", data.string("Welcome to Jetzig!"));
|
||||
|
||||
// Request params have the same type as a `data.object()` so they can be inserted them
|
||||
// directly into the response data. Fetch `http://localhost:8080/?message=hello` to set the
|
||||
// param. JSON data is also accepted when the `content-type: application/json` header is
|
||||
// present.
|
||||
const params = try request.params();
|
||||
|
||||
if (params.get("message")) |value| {
|
||||
try root.put("message_param", value);
|
||||
}
|
||||
|
||||
// Set arbitrary response headers as required. `content-type` is automatically assigned for
|
||||
// HTML, JSON responses.
|
||||
//
|
||||
// Static files located in `public/` in the root of your project directory are accessible
|
||||
// from the root path (e.g. `public/jetzig.png`) is available at `/jetzig.png` and the
|
||||
// content type is inferred from the extension using MIME types.
|
||||
try request.response.headers.append("x-example-header", "example header value");
|
||||
|
||||
// Render the response and set the response code.
|
||||
return request.render(.ok);
|
||||
}
|
18
demo/src/app/views/init/_content.zmpl
Normal file
18
demo/src/app/views/init/_content.zmpl
Normal file
@ -0,0 +1,18 @@
|
||||
// Renders the `message` response data value.
|
||||
<h3 class="message text-[#39b54a]">{.message}</h3>
|
||||
|
||||
<div><img class="p-3 mx-auto" src="/jetzig.png" /></div>
|
||||
|
||||
<div>
|
||||
<a href="https://github.com/jetzig-framework/zmpl">
|
||||
<img class="p-3 m-3 mx-auto" src="/zmpl.png" />
|
||||
</a>
|
||||
|
||||
</div>
|
||||
|
||||
<div>Visit <a class="font-bold text-[#39b54a]" href="https://jetzig.dev/">jetzig.dev</a> to get started.</div>
|
||||
<div>Join our Discord server and introduce yourself:</div>
|
||||
<div>
|
||||
<a class="font-bold text-[#39b54a]" href="https://discord.gg/eufqssz7X6">https://discord.gg/eufqssz7X6</a>
|
||||
</div>
|
||||
</div>
|
20
demo/src/app/views/init/index.zmpl
Normal file
20
demo/src/app/views/init/index.zmpl
Normal file
@ -0,0 +1,20 @@
|
||||
<html>
|
||||
<head>
|
||||
<script src="https://unpkg.com/htmx.org@1.9.10"></script>
|
||||
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<link rel="stylesheet" href="/styles.css" />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="text-center pt-10 m-auto">
|
||||
// If present, renders the `message_param` response data value, add `?message=hello` to the
|
||||
// URL to see the output:
|
||||
<h2 class="param text-3xl text-[#f7931e]">{.message_param}</h2>
|
||||
|
||||
// Renders `src/app/views/init/_content.zmpl` with the same template data available:
|
||||
<div>{^init/content}</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
@ -33,7 +33,9 @@ const Quote = struct {
|
||||
|
||||
// Quotes taken from: https://gist.github.com/natebass/b0a548425a73bdf8ea5c618149fe1fce
|
||||
fn randomQuote(allocator: std.mem.Allocator) !Quote {
|
||||
const json = try std.fs.cwd().readFileAlloc(allocator, "src/app/config/quotes.json", 12684);
|
||||
const path = "src/app/config/quotes.json";
|
||||
const stat = try std.fs.cwd().statFile(path);
|
||||
const json = try std.fs.cwd().readFileAlloc(allocator, path, stat.size);
|
||||
const quotes = try std.json.parseFromSlice([]Quote, allocator, json, .{});
|
||||
return quotes.value[std.crypto.random.intRangeLessThan(usize, 0, quotes.value.len)];
|
||||
}
|
||||
|
@ -4,7 +4,7 @@ pub const jetzig = @import("jetzig");
|
||||
pub const routes = @import("routes").routes;
|
||||
|
||||
pub const jetzig_options = struct {
|
||||
pub const middleware: []const type = &.{@import("DemoMiddleware.zig")};
|
||||
pub const middleware: []const type = &.{@import("app/middleware/DemoMiddleware.zig")};
|
||||
};
|
||||
|
||||
pub fn main() !void {
|
||||
|
79
init.bash
79
init.bash
@ -1,79 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
printf 'Enter project name (default: "jetzig-demo"): '
|
||||
read -a project
|
||||
if [ -z "${project}" ]
|
||||
then
|
||||
project="jetzig-demo"
|
||||
fi
|
||||
|
||||
pwd="$(pwd)"
|
||||
printf "Enter project parent directory (default: \"${pwd}\"): "
|
||||
read -a dir
|
||||
if [ -z "${dir}" ]
|
||||
then
|
||||
dir="${pwd}"
|
||||
fi
|
||||
|
||||
|
||||
set -eu
|
||||
|
||||
project_path="${dir}/${project}"
|
||||
echo
|
||||
echo "Initializing new project in: ${project_path}"
|
||||
|
||||
mkdir -p "${project_path}"
|
||||
|
||||
do_exit () {
|
||||
echo "Error fetching '$1':"
|
||||
echo "$2"
|
||||
echo "Exiting."
|
||||
exit 1
|
||||
}
|
||||
|
||||
remote_base=https://raw.githubusercontent.com/jetzig-framework/jetzig/main/src/init
|
||||
|
||||
objects=(
|
||||
'build.zig'
|
||||
'build.zig.zon'
|
||||
'src/main.zig'
|
||||
'src/app/views/index.zig'
|
||||
'src/app/views/index.zmpl'
|
||||
'src/app/views/quotes.zig'
|
||||
'src/app/views/quotes/get.zmpl'
|
||||
'src/app/config/quotes.json'
|
||||
'public/jetzig.png'
|
||||
'.gitignore'
|
||||
)
|
||||
|
||||
for object in "${objects[@]}"
|
||||
do
|
||||
printf "Creating output: ${object} "
|
||||
url="${remote_base}/${object}"
|
||||
mkdir -p "$(dirname "${project_path}/${object}")"
|
||||
set +e
|
||||
output=$(curl -s --fail --output "${project_path}/${object}" "${url}" 2>&1)
|
||||
set -e
|
||||
if (($?))
|
||||
then
|
||||
do_exit "${url}" "${output}"
|
||||
else
|
||||
echo "✅"
|
||||
fi
|
||||
done
|
||||
|
||||
sed -i.bak -e "s,%%project_name%%,${project},g" "${project_path}/build.zig"
|
||||
rm "${project_path}/build.zig.bak"
|
||||
sed -i.bak -e "s,%%project_name%%,${project},g" "${project_path}/build.zig.zon"
|
||||
rm "${project_path}/build.zig.zon.bak"
|
||||
|
||||
echo
|
||||
echo "Finished creating new project in: ${project_path}"
|
||||
echo
|
||||
echo "Run your new project:"
|
||||
echo
|
||||
echo " cd '${project_path}'"
|
||||
echo ' zig build run'
|
||||
echo
|
||||
echo "Welcome to Jetzig. ✈️🦎 "
|
||||
echo
|
2
src/init/.gitignore
vendored
2
src/init/.gitignore
vendored
@ -1,2 +0,0 @@
|
||||
# Compiled Zmpl views:
|
||||
src/app/views/**/*.compiled.zig
|
@ -1,86 +0,0 @@
|
||||
const std = @import("std");
|
||||
const jetzig_build = @import("jetzig");
|
||||
// Although this function looks imperative, note that its job is to
|
||||
// declaratively construct a build graph that will be executed by an external
|
||||
// runner.
|
||||
pub fn build(b: *std.Build) void {
|
||||
// Standard target options allows the person running `zig build` to choose
|
||||
// what target to build for. Here we do not override the defaults, which
|
||||
// means any target is allowed, and the default is native. Other options
|
||||
// for restricting supported target set are available.
|
||||
const target = b.standardTargetOptions(.{});
|
||||
|
||||
// Standard optimization options allow the person running `zig build` to select
|
||||
// between Debug, ReleaseSafe, ReleaseFast, and ReleaseSmall. Here we do not
|
||||
// set a preferred release mode, allowing the user to decide how to optimize.
|
||||
const optimize = b.standardOptimizeOption(.{});
|
||||
const jetzig_dep = b.dependency("jetzig", .{ .optimize = optimize, .target = target });
|
||||
const compile_view_step = jetzig_build.CompileViewsStep.create(b, .{ .template_path = "src/app/views/" });
|
||||
|
||||
// This declares intent for the library to be installed into the standard
|
||||
// location when the user invokes the "install" step (the default step when
|
||||
// running `zig build`).
|
||||
|
||||
const exe = b.addExecutable(.{
|
||||
.name = "%%project_name%%",
|
||||
.root_source_file = .{ .path = "src/main.zig" },
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
});
|
||||
exe.root_module.addImport("jetzig", jetzig_dep.module("jetzig"));
|
||||
|
||||
exe.root_module.addImport("zmpl", jetzig_dep.module("zmpl"));
|
||||
// This declares intent for the executable to be installed into the
|
||||
// standard location when the user invokes the "install" step (the default
|
||||
// step when running `zig build`).
|
||||
b.installArtifact(exe);
|
||||
exe.step.dependOn(&compile_view_step.step);
|
||||
|
||||
// This *creates* a Run step in the build graph, to be executed when another
|
||||
// step is evaluated that depends on it. The next line below will establish
|
||||
// such a dependency.
|
||||
const run_cmd = b.addRunArtifact(exe);
|
||||
|
||||
// By making the run step depend on the install step, it will be run from the
|
||||
// installation directory rather than directly from within the cache directory.
|
||||
// This is not necessary, however, if the application depends on other installed
|
||||
// files, this ensures they will be present and in the expected location.
|
||||
run_cmd.step.dependOn(b.getInstallStep());
|
||||
|
||||
// This allows the user to pass arguments to the application in the build
|
||||
// command itself, like this: `zig build run -- arg1 arg2 etc`
|
||||
if (b.args) |args| {
|
||||
run_cmd.addArgs(args);
|
||||
}
|
||||
|
||||
// This creates a build step. It will be visible in the `zig build --help` menu,
|
||||
// and can be selected like this: `zig build run`
|
||||
// This will evaluate the `run` step rather than the default, which is "install".
|
||||
const run_step = b.step("run", "Run the app");
|
||||
run_step.dependOn(&run_cmd.step);
|
||||
|
||||
// Creates a step for unit testing. This only builds the test executable
|
||||
// but does not run it.
|
||||
const lib_unit_tests = b.addTest(.{
|
||||
.root_source_file = .{ .path = "src/root.zig" },
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
});
|
||||
|
||||
const run_lib_unit_tests = b.addRunArtifact(lib_unit_tests);
|
||||
|
||||
const exe_unit_tests = b.addTest(.{
|
||||
.root_source_file = .{ .path = "src/main.zig" },
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
});
|
||||
|
||||
const run_exe_unit_tests = b.addRunArtifact(exe_unit_tests);
|
||||
|
||||
// Similar to creating the run step earlier, this exposes a `test` step to
|
||||
// the `zig build --help` menu, providing a way for the user to request
|
||||
// running the unit tests.
|
||||
const test_step = b.step("test", "Run unit tests");
|
||||
test_step.dependOn(&run_lib_unit_tests.step);
|
||||
test_step.dependOn(&run_exe_unit_tests.step);
|
||||
}
|
@ -1,14 +0,0 @@
|
||||
.{
|
||||
.name = "%%project_name%%",
|
||||
.version = "0.0.0",
|
||||
.dependencies = .{
|
||||
.jetzig = .{
|
||||
.url = "https://github.com/jetzig-framework/jetzig/archive/0395d00b4b0f9e87a4bfab62c73ac047abb2e5da.tar.gz",
|
||||
.hash = "122016141c39005c2149b3c6029b702332c346e3305f52376e1edbc5ee0295318e4c",
|
||||
},
|
||||
},
|
||||
|
||||
.paths = .{
|
||||
"",
|
||||
},
|
||||
}
|
Binary file not shown.
Before Width: | Height: | Size: 22 KiB |
@ -1,11 +0,0 @@
|
||||
const std = @import("std");
|
||||
const jetzig = @import("jetzig");
|
||||
const Request = jetzig.http.Request;
|
||||
const Data = jetzig.data.Data;
|
||||
const View = jetzig.views.View;
|
||||
|
||||
pub fn index(request: *jetzig.http.Request, data: *jetzig.data.Data) anyerror!jetzig.views.View {
|
||||
var object = try data.object();
|
||||
try object.put("message", data.string("Welcome to Jetzig!"));
|
||||
return request.render(.ok);
|
||||
}
|
@ -1,34 +0,0 @@
|
||||
const std = @import("std");
|
||||
const jetzig = @import("jetzig");
|
||||
|
||||
const Request = jetzig.http.Request;
|
||||
const Data = jetzig.data.Data;
|
||||
const View = jetzig.views.View;
|
||||
|
||||
pub fn get(id: []const u8, request: *Request, data: *Data) anyerror!View {
|
||||
var body = try data.object();
|
||||
|
||||
const random_quote = try randomQuote(request.allocator);
|
||||
|
||||
if (std.mem.eql(u8, id, "random")) {
|
||||
try body.put("quote", data.string(random_quote.quote));
|
||||
try body.put("author", data.string(random_quote.author));
|
||||
} else {
|
||||
try body.put("quote", data.string("If you can dream it, you can achieve it."));
|
||||
try body.put("author", data.string("Zig Ziglar"));
|
||||
}
|
||||
|
||||
return request.render(.ok);
|
||||
}
|
||||
|
||||
const Quote = struct {
|
||||
quote: []const u8,
|
||||
author: []const u8,
|
||||
};
|
||||
|
||||
// Quotes taken from: https://gist.github.com/natebass/b0a548425a73bdf8ea5c618149fe1fce
|
||||
fn randomQuote(allocator: std.mem.Allocator) !Quote {
|
||||
const json = try std.fs.cwd().readFileAlloc(allocator, "src/app/config/quotes.json", 12684);
|
||||
const quotes = try std.json.parseFromSlice([]Quote, allocator, json, .{});
|
||||
return quotes.value[std.crypto.random.intRangeLessThan(usize, 0, quotes.value.len)];
|
||||
}
|
@ -1,2 +0,0 @@
|
||||
<div>"{.quote}"</div>
|
||||
<div><b>--{.author}</b></div>
|
@ -1,15 +0,0 @@
|
||||
const std = @import("std");
|
||||
|
||||
pub const jetzig = @import("jetzig");
|
||||
pub const routes = @import("routes").routes;
|
||||
|
||||
pub fn main() !void {
|
||||
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
|
||||
defer std.debug.assert(gpa.deinit() == .ok);
|
||||
const allocator = gpa.allocator();
|
||||
|
||||
const app = try jetzig.init(allocator);
|
||||
defer app.deinit();
|
||||
|
||||
try app.start(comptime jetzig.route(routes));
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user