Merge branch 'inertia' into websockets

This commit is contained in:
Bob Farrell 2025-04-27 14:31:11 +01:00
commit ff6d8cf942
14 changed files with 108 additions and 8 deletions

View File

@ -6,6 +6,8 @@ _Jetzig_ is a web framework written in 100% pure [Zig](https://ziglang.org) :liz
Official website: [jetzig.dev](https://www.jetzig.dev/)
Please note that _Jetzig_'s `main` branch aims to be compatible with the latest [Zig nightly master build](https://ziglang.org/download/) and older versions of _Zig_ are not supported.
_Jetzig_ aims to provide a rich set of user-friendly tools for building modern web applications quickly. See the checklist below.
Join us on Discord ! [https://discord.gg/eufqssz7X6](https://discord.gg/eufqssz7X6).

View File

@ -12,11 +12,22 @@ const use_llvm_default = builtin.os.tag != .linux;
pub fn build(b: *std.Build) !void {
const target = b.standardTargetOptions(.{});
const optimize = b.standardOptimizeOption(.{});
var jetzig_templates_path = std.ArrayList([]const u8).init(b.allocator);
try jetzig_templates_path.append("/");
var it = std.mem.splitSequence(
u8,
try b.path("src/jetzig/templates").getPath3(b, null).toString(b.allocator),
std.fs.path.sep_str,
);
while (it.next()) |segment| {
try jetzig_templates_path.append(segment);
}
const templates_paths = try zmpl_build.templatesPaths(
b.allocator,
&.{
.{ .prefix = "views", .path = &.{ "src", "app", "views" } },
.{ .prefix = "mailers", .path = &.{ "src", "app", "mailers" } },
.{ .prefix = "jetzig", .path = jetzig_templates_path.items },
},
);

View File

@ -0,0 +1 @@
<title>My Inertia App</title>

View File

@ -16,9 +16,10 @@ pub const jetzig_options = struct {
pub const middleware: []const type = &.{
// jetzig.middleware.AuthMiddleware,
// jetzig.middleware.AntiCsrfMiddleware,
// jetzig.middleware.HtmxMiddleware,
// jetzig.middleware.CompressionMiddleware,
// @import("app/middleware/DemoMiddleware.zig"),
jetzig.middleware.HtmxMiddleware,
// jetzig.middleware.InertiaMiddleware,
// jetzig.middleware.CompressionMiddleware,
// @import("app/middleware/DemoMiddleware.zig"),
};
// Maximum bytes to allow in request body.

View File

@ -147,7 +147,7 @@ fn renderMarkdown(
if (zmpl.findPrefixed("views", prefixed_name)) |layout| {
view.data.content = .{ .data = content };
return try layout.render(view.data, jetzig.TemplateContext, .{}, .{});
return try layout.render(view.data, jetzig.TemplateContext, .{}, &.{}, .{});
} else {
std.debug.print("Unknown layout: {s}\n", .{layout_name});
return content;
@ -174,6 +174,7 @@ fn renderZmplTemplate(
view.data,
jetzig.TemplateContext,
.{},
&.{},
.{ .layout = layout },
);
} else {
@ -181,7 +182,7 @@ fn renderZmplTemplate(
return try allocator.dupe(u8, "");
}
} else {
return try template.render(view.data, jetzig.TemplateContext, .{}, .{});
return try template.render(view.data, jetzig.TemplateContext, .{}, &.{}, .{});
}
} else return null;
}

View File

@ -1,13 +1,18 @@
const std = @import("std");
pub const http = @import("http.zig");
pub const views = @import("views.zig");
pub const config = @import("config.zig");
/// Context available in every Zmpl template as `context`.
pub const TemplateContext = @This();
request: ?*http.Request = null,
route: ?views.Route = null,
/// Return an authenticity token stored in the current request's session. If no token exists,
/// generate and store before returning.
/// Use to create a form element which can be verified by `AntiCsrfMiddleware`.
pub fn authenticityToken(self: TemplateContext) !?[]const u8 {
return if (self.request) |request|
try request.authenticityToken()
@ -15,6 +20,8 @@ pub fn authenticityToken(self: TemplateContext) !?[]const u8 {
null;
}
/// Generate a hidden form element containing an authenticity token provided by
/// `authenticityToken`. Use as `{{context.authenticityFormElement()}}` in a Zmpl template.
pub fn authenticityFormElement(self: TemplateContext) !?[]const u8 {
return if (self.request) |request| blk: {
const token = try request.authenticityToken();
@ -23,3 +30,10 @@ pub fn authenticityFormElement(self: TemplateContext) !?[]const u8 {
, .{ config.get([]const u8, "authenticity_token_name"), token });
} else null;
}
pub fn path(self: TemplateContext) ?[]const u8 {
return if (self.request) |request|
request.path.path
else
null;
}

View File

@ -15,6 +15,7 @@ pub const RequestState = enum {
initial, // No processing has taken place
processed, // Request headers have been processed
after_request, // Initial middleware processing
after_view, // View returned, response data ready for full response render
rendered, // Rendered by middleware or view
redirected, // Redirected by middleware or view
failed, // Failed by middleware or view
@ -239,6 +240,20 @@ pub fn render(self: *Request, status_code: jetzig.http.status_codes.StatusCode)
return self.rendered_view.?;
}
/// Render a response with pre-rendered content. This function can only be called once per
/// request (repeat calls will trigger an error).
pub fn renderContent(
self: *Request,
status_code: jetzig.http.status_codes.StatusCode,
content: []const u8,
) jetzig.views.View {
if (self.isRendered()) self.rendered_multiple = true;
self.state = .rendered;
self.rendered_view = .{ .data = self.response_data, .status_code = status_code, .content = content };
return self.rendered_view.?;
}
/// Render an error. This function can only be called once per request (repeat calls will
/// trigger an error).
pub fn fail(self: *Request, status_code: jetzig.http.status_codes.StatusCode) jetzig.views.View {
@ -252,7 +267,7 @@ pub fn fail(self: *Request, status_code: jetzig.http.status_codes.StatusCode) je
pub inline fn isRendered(self: *const Request) bool {
return switch (self.state) {
.initial, .processed, .after_request, .before_response => false,
.rendered, .redirected, .failed, .finalized => true,
.after_view, .rendered, .redirected, .failed, .finalized => true,
};
}
@ -328,6 +343,7 @@ pub fn renderRedirect(self: *Request, state: RedirectState) !void {
self.response_data,
jetzig.TemplateContext,
.{ .request = self },
&.{},
.{},
);
} else try std.fmt.allocPrint(self.allocator, "Redirecting to {s}", .{state.location}),

View File

@ -502,6 +502,7 @@ pub fn RoutedServer(Routes: type) type {
view.data,
jetzig.TemplateContext,
template_context,
&.{},
.{},
);
}

View File

@ -94,6 +94,38 @@ pub fn afterRequest(request: *jetzig.http.Request) !MiddlewareData {
return middleware_data;
}
pub fn afterView(middleware_data: *MiddlewareData, request: *jetzig.http.Request, route: jetzig.views.Route) !void {
if (request.state != .failed) request.state = .after_view;
inline for (middlewares, 0..) |middleware, index| {
if (comptime !@hasDecl(middleware, "afterView")) continue;
if (request.state == .after_view) {
if (comptime @hasDecl(middleware, "init")) {
const data = middleware_data.get(index).?;
try @call(
.always_inline,
middleware.afterView,
.{ @as(*middleware, @ptrCast(@alignCast(data))), request, route },
);
} else {
try @call(
.always_inline,
middleware.afterView,
.{ request, route },
);
}
}
if (request.state != .after_view) {
request.middleware_rendered = .{
.name = @typeName(middleware),
.action = "afterView",
};
break;
}
}
}
pub fn beforeResponse(
middleware_data: *MiddlewareData,
request: *jetzig.http.Request,

View File

@ -148,7 +148,7 @@ fn defaultHtml(
try data.addConst("jetzig_view", data.string(""));
try data.addConst("jetzig_action", data.string(""));
return if (jetzig.zmpl.findPrefixed("mailers", mailer.html_template)) |template|
try template.render(&data, jetzig.TemplateContext, .{}, .{})
try template.render(&data, jetzig.TemplateContext, .{}, &.{}, .{})
else
null;
}
@ -166,7 +166,7 @@ fn defaultText(
try data.addConst("jetzig_view", data.string(""));
try data.addConst("jetzig_action", data.string(""));
return if (jetzig.zmpl.findPrefixed("mailers", mailer.text_template)) |template|
try template.render(&data, jetzig.TemplateContext, .{}, .{})
try template.render(&data, jetzig.TemplateContext, .{}, &.{}, .{})
else
null;
}

View File

@ -5,6 +5,7 @@ pub const HtmxMiddleware = @import("middleware/HtmxMiddleware.zig");
pub const CompressionMiddleware = @import("middleware/CompressionMiddleware.zig");
pub const AuthMiddleware = @import("middleware/AuthMiddleware.zig");
pub const AntiCsrfMiddleware = @import("middleware/AntiCsrfMiddleware.zig");
pub const InertiaMiddleware = @import("middleware/InertiaMiddleware.zig");
const RouteOptions = struct {
content: ?[]const u8 = null,

View File

@ -0,0 +1,6 @@
const std = @import("std");
const jetzig = @import("../../jetzig.zig");
// WIP
const InertiaMiddleware = @This();

View File

@ -0,0 +1,13 @@
<!DOCTYPE html>
<html>
<head>
@partial views:inertia/head
</head>
<body>
<div
id="app"
data-page='{"component":"{{jetzig_view}}","props":{{zmpl.toJson()}},"url":"{{context.path()}}","version":"c32b8e4965f418ad16eaebba1d4e960f"}'
>
</div>
</body>
</html>

View File

@ -6,6 +6,7 @@ const jetzig = @import("../../jetzig.zig");
data: *jetzig.data.Data,
status_code: jetzig.http.status_codes.StatusCode = .ok,
content: ?[]const u8 = null,
pub fn deinit(self: Self) void {
_ = self;