jetzig/cli/commands/init.zig
Bob Farrell 29d13917a6 Add a dependency to demo app
Use as a regression test to ensure dependencies always work.
2024-03-28 21:07:16 +00:00

341 lines
9.2 KiB
Zig

const std = @import("std");
const args = @import("args");
const util = @import("../util.zig");
const init_data = @import("init_data").init_data;
/// Command line options for the `init` command.
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.JetzigCommandError;
}
install_path = arg;
}
const github_url = try util.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 realpath = try install_dir.realpathAlloc(allocator, ".");
defer allocator.free(realpath);
const output = try std.fmt.allocPrint(allocator, "Creating new project in {s}\n\n", .{realpath});
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 util.runCommand(allocator, realpath, &[_][]const u8{
"zig",
"fetch",
"--save",
github_url,
});
try util.runCommand(allocator, realpath, &[_][]const u8{
"zig",
"fetch",
"--save",
"https://github.com/jetzig-framework/iguanas/archive/89c2abf29de0bc31054a9a6feac5a6a83bab0459.tar.gz",
});
// 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/
\\
\\
, .{realpath});
}
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| {
util.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| {
util.printFailure();
return err;
};
}
defer allocator.free(content);
writeSourceFile(install_dir, dest, content) catch |err| {
util.printFailure();
return err;
};
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 util.base64Decode(allocator, file.data);
}
return error.JetzigCommandError;
}
// 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);
}
}
// 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);
const stripped_input = util.strip(capture);
if (std.mem.eql(u8, stripped_input, "")) {
if (options.default) |default| return try allocator.dupe(u8, util.strip(default));
} else return try allocator.dupe(u8, stripped_input);
}
}
}
// 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 util.runCommand(allocator, install_dir, &[_][]const u8{
"git",
"init",
".",
});
try util.runCommand(allocator, install_dir, &[_][]const u8{
"git",
"add",
".",
});
try util.runCommand(allocator, install_dir, &[_][]const u8{
"git",
"commit",
"-m",
"Initialize Jetzig project",
});
}