mirror of
https://github.com/ziex-dev/ziex.git
synced 2026-07-22 11:39:34 -06:00
feat(cli): remove template for init
This commit is contained in:
parent
1ff589506a
commit
6ed681c651
@ -15,7 +15,7 @@ pub fn register(writer: *std.Io.Writer, reader: *std.Io.Reader, allocator: std.m
|
||||
const template_flag = zli.Flag{
|
||||
.name = "template",
|
||||
.shortcut = "t",
|
||||
.description = "Template to use (default, docker)",
|
||||
.description = "Template to use: a builtin (default, docker) or any github:ziex-dev/template-<name> (e.g. cloudflare, vercel)",
|
||||
.type = .String,
|
||||
.default_value = .{ .String = "default" },
|
||||
};
|
||||
@ -87,13 +87,24 @@ fn init(ctx: zli.CommandContext) !void {
|
||||
std.debug.print("{s}Initializing with existing files, overriding if files already exist.{s}\n", .{ colors.yellow, colors.reset });
|
||||
}
|
||||
|
||||
const template_name = if (std.meta.stringToEnum(TemplateFile.Name, t_val)) |name| name else {
|
||||
std.debug.print("\x1b[33mUnknown template:\x1b[0m {s}\n\nTemplates:\n", .{t_val});
|
||||
// Non-builtin templates are fetched from `github:ziex-dev/template-<name>`.
|
||||
const template_name = std.meta.stringToEnum(TemplateFile.Name, t_val) orelse {
|
||||
printer.header("{s} Initializing ZX project!", .{tui.Printer.emoji("○")});
|
||||
printer.info("[{s}] (github:{s}/template-{s})", .{ t_val, "ziex-dev", t_val });
|
||||
|
||||
for (std.enums.values(TemplateFile.Name)) |name| {
|
||||
std.debug.print(" - \x1b[34m{s}\x1b[0m\n", .{@tagName(name)});
|
||||
}
|
||||
std.debug.print("\n", .{});
|
||||
remote.fetch(io, ctx.allocator, t_val, init_path) catch |err| switch (err) {
|
||||
error.TemplateNotFound, error.NoReleaseFound => {
|
||||
printer.warning("Template '{s}' not found in the ziex-dev org (looked for github:ziex-dev/template-{s} with a published release).", .{ t_val, t_val });
|
||||
return;
|
||||
},
|
||||
error.NetworkError => {
|
||||
printer.warning("Failed to download template '{s}'. Check your network connection and try again.", .{t_val});
|
||||
return;
|
||||
},
|
||||
else => return err,
|
||||
};
|
||||
|
||||
try printInitFooter(&printer, ctx.allocator, init_path, has_init_path_arg);
|
||||
return;
|
||||
};
|
||||
|
||||
@ -147,9 +158,13 @@ fn init(ctx: zli.CommandContext) !void {
|
||||
try file.writeStreamingAll(io, content);
|
||||
}
|
||||
|
||||
try printInitFooter(&printer, ctx.allocator, init_path, has_init_path_arg);
|
||||
}
|
||||
|
||||
fn printInitFooter(printer: *tui.Printer, allocator: std.mem.Allocator, init_path: []const u8, has_init_path_arg: bool) !void {
|
||||
if (has_init_path_arg) {
|
||||
const suggested_cmd = try std.fmt.allocPrint(ctx.allocator, "cd {s} && zig build dev", .{init_path});
|
||||
defer ctx.allocator.free(suggested_cmd);
|
||||
const suggested_cmd = try std.fmt.allocPrint(allocator, "cd {s} && zig build dev", .{init_path});
|
||||
defer allocator.free(suggested_cmd);
|
||||
printer.footer("Now run {s}\n\n{s}{s}{s}", .{ tui.Printer.emoji("→"), colors.cyan, suggested_cmd, colors.reset });
|
||||
} else {
|
||||
printer.footer("Now run {s}\n\n{s}", .{ tui.Printer.emoji("→"), colors.Fns.cyan("zig build dev") });
|
||||
@ -228,6 +243,7 @@ const templates = app_template.files;
|
||||
|
||||
const std = @import("std");
|
||||
const app_template = @import("app_template");
|
||||
const remote = @import("init/remote.zig");
|
||||
const zli = @import("zli");
|
||||
const tui = @import("../tui/main.zig");
|
||||
const AppContext = @import("shared/context.zig").AppContext;
|
||||
|
||||
183
src/cli/init/remote.zig
Normal file
183
src/cli/init/remote.zig
Normal file
@ -0,0 +1,183 @@
|
||||
const org = "ziex-dev";
|
||||
|
||||
pub fn fetch(
|
||||
io: std.Io,
|
||||
allocator: std.mem.Allocator,
|
||||
template: []const u8,
|
||||
dest_dir: []const u8,
|
||||
) !void {
|
||||
var client: std.http.Client = .{ .allocator = allocator, .io = io };
|
||||
defer client.deinit();
|
||||
|
||||
const repo = try std.fmt.allocPrint(allocator, "template-{s}", .{template});
|
||||
defer allocator.free(repo);
|
||||
|
||||
const tag = try fetchLatestReleaseTag(&client, allocator, repo);
|
||||
defer allocator.free(tag);
|
||||
|
||||
// codeload serves the gzipped source tarball for a given ref.
|
||||
const tarball_url = try std.fmt.allocPrint(
|
||||
allocator,
|
||||
"https://codeload.github.com/{s}/{s}/tar.gz/refs/tags/{s}",
|
||||
.{ org, repo, tag },
|
||||
);
|
||||
defer allocator.free(tarball_url);
|
||||
|
||||
const gz = try get(&client, allocator, tarball_url);
|
||||
defer allocator.free(gz);
|
||||
|
||||
try std.Io.Dir.cwd().createDirPath(io, dest_dir);
|
||||
var dir = try std.Io.Dir.cwd().openDir(io, dest_dir, .{});
|
||||
defer dir.close(io);
|
||||
|
||||
var gz_reader: std.Io.Reader = .fixed(gz);
|
||||
var decompress_buf: [std.compress.flate.max_window_len]u8 = undefined;
|
||||
var decompress: std.compress.flate.Decompress = .init(&gz_reader, .gzip, &decompress_buf);
|
||||
|
||||
try std.tar.extract(io, dir, &decompress.reader, .{
|
||||
.strip_components = 1,
|
||||
.mode_mode = .executable_bit_only,
|
||||
});
|
||||
|
||||
try customize(io, allocator, dest_dir);
|
||||
}
|
||||
|
||||
/// Query the GitHub API for the latest release tag of `repo`.
|
||||
fn fetchLatestReleaseTag(
|
||||
client: *std.http.Client,
|
||||
allocator: std.mem.Allocator,
|
||||
repo: []const u8,
|
||||
) ![]const u8 {
|
||||
const url = try std.fmt.allocPrint(
|
||||
allocator,
|
||||
"https://api.github.com/repos/{s}/{s}/releases/latest",
|
||||
.{ org, repo },
|
||||
);
|
||||
defer allocator.free(url);
|
||||
|
||||
const body = try get(client, allocator, url);
|
||||
defer allocator.free(body);
|
||||
|
||||
const parsed = std.json.parseFromSlice(struct {
|
||||
tag_name: []const u8,
|
||||
}, allocator, body, .{ .ignore_unknown_fields = true }) catch return error.NoReleaseFound;
|
||||
defer parsed.deinit();
|
||||
|
||||
return allocator.dupe(u8, parsed.value.tag_name);
|
||||
}
|
||||
|
||||
fn get(client: *std.http.Client, allocator: std.mem.Allocator, url: []const u8) ![]u8 {
|
||||
var aw = std.Io.Writer.Allocating.init(allocator);
|
||||
defer aw.deinit();
|
||||
|
||||
const result = client.fetch(.{
|
||||
.location = .{ .url = url },
|
||||
.method = .GET,
|
||||
.redirect_behavior = @enumFromInt(5),
|
||||
.response_writer = &aw.writer,
|
||||
.extra_headers = &.{
|
||||
.{ .name = "user-agent", .value = "ziex-cli" },
|
||||
.{ .name = "accept", .value = "application/vnd.github+json" },
|
||||
},
|
||||
}) catch return error.NetworkError;
|
||||
|
||||
switch (result.status) {
|
||||
.ok => {},
|
||||
.not_found => return error.TemplateNotFound,
|
||||
else => return error.NetworkError,
|
||||
}
|
||||
|
||||
return allocator.dupe(u8, aw.written());
|
||||
}
|
||||
|
||||
fn customize(io: std.Io, allocator: std.mem.Allocator, dest_dir: []const u8) !void {
|
||||
const project_name = try sanitizeProjectName(allocator, std.fs.path.basename(dest_dir));
|
||||
defer allocator.free(project_name);
|
||||
|
||||
var dir = try std.Io.Dir.cwd().openDir(io, dest_dir, .{ .iterate = true });
|
||||
defer dir.close(io);
|
||||
|
||||
try replaceInTree(io, allocator, dir, "ziex_app", project_name);
|
||||
try updateFingerprint(io, allocator, dest_dir, project_name);
|
||||
}
|
||||
|
||||
fn replaceInTree(
|
||||
io: std.Io,
|
||||
allocator: std.mem.Allocator,
|
||||
dir: std.Io.Dir,
|
||||
from: []const u8,
|
||||
to: []const u8,
|
||||
) !void {
|
||||
var walker = try dir.walk(allocator);
|
||||
defer walker.deinit();
|
||||
|
||||
while (try walker.next(io)) |entry| {
|
||||
if (entry.kind != .file) continue;
|
||||
|
||||
const data = entry.dir.readFileAlloc(io, entry.basename, allocator, .limited(8 * 1024 * 1024)) catch continue;
|
||||
defer allocator.free(data);
|
||||
|
||||
if (std.mem.indexOf(u8, data, from) == null) continue;
|
||||
|
||||
const replaced = try std.mem.replaceOwned(u8, allocator, data, from, to);
|
||||
defer allocator.free(replaced);
|
||||
|
||||
try entry.dir.writeFile(io, .{ .sub_path = entry.basename, .data = replaced });
|
||||
}
|
||||
}
|
||||
|
||||
fn updateFingerprint(
|
||||
io: std.Io,
|
||||
allocator: std.mem.Allocator,
|
||||
dest_dir: []const u8,
|
||||
project_name: []const u8,
|
||||
) !void {
|
||||
const zon_path = try std.fs.path.join(allocator, &.{ dest_dir, "build.zig.zon" });
|
||||
defer allocator.free(zon_path);
|
||||
|
||||
const data = std.Io.Dir.cwd().readFileAlloc(io, zon_path, allocator, .limited(1024 * 1024)) catch return;
|
||||
defer allocator.free(data);
|
||||
|
||||
const needle = ".fingerprint = 0x";
|
||||
const start = std.mem.indexOf(u8, data, needle) orelse return;
|
||||
const hex_start = start + needle.len;
|
||||
var hex_end = hex_start;
|
||||
while (hex_end < data.len and std.ascii.isHex(data[hex_end])) hex_end += 1;
|
||||
|
||||
const fingerprint = Fingerprint.generate(io, project_name);
|
||||
|
||||
var out = std.Io.Writer.Allocating.init(allocator);
|
||||
defer out.deinit();
|
||||
try out.writer.writeAll(data[0..start]);
|
||||
try out.writer.print(".fingerprint = 0x{x:0>16}", .{fingerprint.int()});
|
||||
try out.writer.writeAll(data[hex_end..]);
|
||||
|
||||
try std.Io.Dir.cwd().writeFile(io, .{ .sub_path = zon_path, .data = out.written() });
|
||||
}
|
||||
|
||||
const Fingerprint = packed struct(u64) {
|
||||
id: u32,
|
||||
checksum: u32,
|
||||
|
||||
fn generate(io: std.Io, name: []const u8) Fingerprint {
|
||||
var source: std.Random.IoSource = .{ .io = io };
|
||||
return .{
|
||||
.id = source.interface().intRangeLessThan(u32, 1, 0xffffffff),
|
||||
.checksum = std.hash.Crc32.hash(name),
|
||||
};
|
||||
}
|
||||
|
||||
fn int(n: Fingerprint) u64 {
|
||||
return @bitCast(n);
|
||||
}
|
||||
};
|
||||
|
||||
fn sanitizeProjectName(allocator: std.mem.Allocator, name: []const u8) ![]const u8 {
|
||||
const out = try allocator.alloc(u8, name.len);
|
||||
for (name, 0..) |c, i| {
|
||||
out[i] = if (std.ascii.isAlphanumeric(c) or c == '_') c else '_';
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
const std = @import("std");
|
||||
@ -233,6 +233,32 @@ test "init -t docker" {
|
||||
});
|
||||
}
|
||||
|
||||
test "init -t <remote>" {
|
||||
if (!test_util.shouldRunSlowTest()) return error.SkipZigTest;
|
||||
// Fetches github:ziex-dev/template-cloudflare, renames the project to the
|
||||
// target directory name, and regenerates the build.zig.zon fingerprint.
|
||||
try test_cmd(.{
|
||||
.args = &.{ "init", "--template", "cloudflare", "remote-app", "--force" },
|
||||
.expected_exit_code = 0,
|
||||
.expected_stderr_strings = &.{
|
||||
"Initializing ZX project!",
|
||||
"github:ziex-dev/template-cloudflare",
|
||||
},
|
||||
.expected_files = &.{
|
||||
"remote-app/build.zig.zon",
|
||||
"remote-app/build.zig",
|
||||
"remote-app/app/main.zig",
|
||||
},
|
||||
.expected_file_contains = &.{
|
||||
// Project renamed from ziex_app to the directory name.
|
||||
.{ .path = "remote-app/build.zig.zon", .needle = ".name = .remote_app" },
|
||||
},
|
||||
.expected_file_excludes = &.{
|
||||
.{ .path = "remote-app/build.zig.zon", .needle = "ziex_app" },
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
test "fmt" {
|
||||
try test_cmd(.{
|
||||
.args = &.{ "fmt", "app" ++ std.fs.path.sep_str ++ "pages" },
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user