mirror of
https://github.com/ziex-dev/ziex.git
synced 2026-07-20 02:29:36 -06:00
feat: app.base_path configuration
This commit is contained in:
parent
d40104ad18
commit
384b2904aa
@ -35,6 +35,7 @@ pub fn build(b: *std.Build) !void {
|
||||
const zx_runtime_options = b.addOptions();
|
||||
zx_runtime_options.addOption([]const u8, "staticdir", "zig-out/static");
|
||||
zx_runtime_options.addOption([]const u8, "datadir", "zig-out/data");
|
||||
zx_runtime_options.addOption(?[]const u8, "app_base_path", null);
|
||||
|
||||
const cli_options_dev = b.addOptions();
|
||||
cli_options_dev.addOption([]const u8, "zig_exe", b.graph.zig_exe);
|
||||
|
||||
@ -18,7 +18,7 @@ pub fn Page(ctx: zx.PageContext) zx.Component {
|
||||
const zx_for = @embedFile("./sandbox/for_loop.zx");
|
||||
const zig_for = @embedFile("./sandbox/for_loop.zig");
|
||||
const html_page_for = @import("./sandbox/for_loop.zig").Page(allocator);
|
||||
html_page_for.render(&aw_for.writer) catch unreachable;
|
||||
html_page_for.render(&aw_for.writer, .{}) catch unreachable;
|
||||
const html_for = allocator.dupe(u8, aw_for.written()) catch unreachable;
|
||||
|
||||
// If/Else example
|
||||
@ -26,7 +26,7 @@ pub fn Page(ctx: zx.PageContext) zx.Component {
|
||||
const zx_if = @embedFile("./sandbox/if_else.zx");
|
||||
const zig_if = @embedFile("./sandbox/if_else.zig");
|
||||
const html_page_if = @import("./sandbox/if_else.zig").Page(allocator);
|
||||
html_page_if.render(&aw_if.writer) catch unreachable;
|
||||
html_page_if.render(&aw_if.writer, .{}) catch unreachable;
|
||||
const html_if = allocator.dupe(u8, aw_if.written()) catch unreachable;
|
||||
|
||||
// Switch example
|
||||
@ -34,7 +34,7 @@ pub fn Page(ctx: zx.PageContext) zx.Component {
|
||||
const zx_switch = @embedFile("./sandbox/switch_expr.zx");
|
||||
const zig_switch = @embedFile("./sandbox/switch_expr.zig");
|
||||
const html_page_switch = @import("./sandbox/switch_expr.zig").Page(allocator);
|
||||
html_page_switch.render(&aw_switch.writer) catch unreachable;
|
||||
html_page_switch.render(&aw_switch.writer, .{}) catch unreachable;
|
||||
const html_switch = allocator.dupe(u8, aw_switch.written()) catch unreachable;
|
||||
|
||||
// Form example
|
||||
@ -42,7 +42,7 @@ pub fn Page(ctx: zx.PageContext) zx.Component {
|
||||
const zx_form = @embedFile("./sandbox/form_handling.zx");
|
||||
const zig_form = @embedFile("./sandbox/form_handling.zig");
|
||||
const html_page_form = @import("./sandbox/form_handling.zig").Page(allocator);
|
||||
html_page_form.render(&aw_form.writer) catch unreachable;
|
||||
html_page_form.render(&aw_form.writer, .{}) catch unreachable;
|
||||
const html_form = allocator.dupe(u8, aw_form.written()) catch unreachable;
|
||||
|
||||
// Components example
|
||||
@ -50,7 +50,7 @@ pub fn Page(ctx: zx.PageContext) zx.Component {
|
||||
const zx_components = @embedFile("./sandbox/components.zx");
|
||||
const zig_components = @embedFile("./sandbox/components.zig");
|
||||
const html_page_components = @import("./sandbox/components.zig").Page(allocator);
|
||||
html_page_components.render(&aw_components.writer) catch unreachable;
|
||||
html_page_components.render(&aw_components.writer, .{}) catch unreachable;
|
||||
const html_components = allocator.dupe(u8, aw_components.written()) catch unreachable;
|
||||
|
||||
// Dynamic attributes example
|
||||
@ -58,7 +58,7 @@ pub fn Page(ctx: zx.PageContext) zx.Component {
|
||||
const zx_dynattr = @embedFile("./sandbox/dynamic_attr.zx");
|
||||
const zig_dynattr = @embedFile("./sandbox/dynamic_attr.zig");
|
||||
const html_page_dynattr = @import("./sandbox/dynamic_attr.zig").Page(allocator);
|
||||
html_page_dynattr.render(&aw_dynattr.writer) catch unreachable;
|
||||
html_page_dynattr.render(&aw_dynattr.writer, .{}) catch unreachable;
|
||||
const html_dynattr = allocator.dupe(u8, aw_dynattr.written()) catch unreachable;
|
||||
|
||||
return (
|
||||
|
||||
@ -14,7 +14,7 @@ pub fn Page(ctx: zx.PageContext) !zx.Component {
|
||||
const zx_greeting = @embedFile("./examples/greeting.zx");
|
||||
const zig_greeting = @embedFile("./examples/greeting.zig");
|
||||
const html_page_greeting = @import("./examples/greeting.zig").HelloWorld(allocator);
|
||||
html_page_greeting.render(&aw_greeting.writer) catch unreachable;
|
||||
html_page_greeting.render(&aw_greeting.writer, .{}) catch unreachable;
|
||||
const html_greeting = allocator.dupe(u8, aw_greeting.written()) catch unreachable;
|
||||
|
||||
// Markup example
|
||||
@ -22,7 +22,7 @@ pub fn Page(ctx: zx.PageContext) !zx.Component {
|
||||
const zx_markup = @embedFile("./examples/markup.zx");
|
||||
const zig_markup = @embedFile("./examples/markup.zig");
|
||||
const html_page_markup = @import("./examples/markup.zig").AboutSection(allocator);
|
||||
html_page_markup.render(&aw_markup.writer) catch unreachable;
|
||||
html_page_markup.render(&aw_markup.writer, .{}) catch unreachable;
|
||||
const html_markup = allocator.dupe(u8, aw_markup.written()) catch unreachable;
|
||||
|
||||
// Text expression example
|
||||
@ -30,7 +30,7 @@ pub fn Page(ctx: zx.PageContext) !zx.Component {
|
||||
const zx_text = @embedFile("./examples/text_expr.zx");
|
||||
const zig_text = @embedFile("./examples/text_expr.zig");
|
||||
const html_page_text = @import("./examples/text_expr.zig").UserGreeting(allocator);
|
||||
html_page_text.render(&aw_text.writer) catch unreachable;
|
||||
html_page_text.render(&aw_text.writer, .{}) catch unreachable;
|
||||
const html_text = allocator.dupe(u8, aw_text.written()) catch unreachable;
|
||||
|
||||
// Format expression example (now just showing types)
|
||||
@ -38,7 +38,7 @@ pub fn Page(ctx: zx.PageContext) !zx.Component {
|
||||
const zx_format = @embedFile("./examples/format_expr.zx");
|
||||
const zig_format = @embedFile("./examples/format_expr.zig");
|
||||
const html_page_format = @import("./examples/format_expr.zig").ProductInfo(allocator);
|
||||
html_page_format.render(&aw_format.writer) catch unreachable;
|
||||
html_page_format.render(&aw_format.writer, .{}) catch unreachable;
|
||||
const html_format = allocator.dupe(u8, aw_format.written()) catch unreachable;
|
||||
|
||||
// Conditional example
|
||||
@ -46,7 +46,7 @@ pub fn Page(ctx: zx.PageContext) !zx.Component {
|
||||
const zx_conditional = @embedFile("./examples/conditional.zx");
|
||||
const zig_conditional = @embedFile("./examples/conditional.zig");
|
||||
const html_page_conditional = @import("./examples/conditional.zig").UserStatus(allocator);
|
||||
html_page_conditional.render(&aw_conditional.writer) catch unreachable;
|
||||
html_page_conditional.render(&aw_conditional.writer, .{}) catch unreachable;
|
||||
const html_conditional = allocator.dupe(u8, aw_conditional.written()) catch unreachable;
|
||||
|
||||
// Switch example
|
||||
@ -54,7 +54,7 @@ pub fn Page(ctx: zx.PageContext) !zx.Component {
|
||||
const zx_switch = @embedFile("./examples/switch_expr.zx");
|
||||
const zig_switch = @embedFile("./examples/switch_expr.zig");
|
||||
const html_page_switch = @import("./examples/switch_expr.zig").RoleBadge(allocator);
|
||||
html_page_switch.render(&aw_switch.writer) catch unreachable;
|
||||
html_page_switch.render(&aw_switch.writer, .{}) catch unreachable;
|
||||
const html_switch = allocator.dupe(u8, aw_switch.written()) catch unreachable;
|
||||
|
||||
// List example
|
||||
@ -62,7 +62,7 @@ pub fn Page(ctx: zx.PageContext) !zx.Component {
|
||||
const zx_list = @embedFile("./examples/list.zx");
|
||||
const zig_list = @embedFile("./examples/list.zig");
|
||||
const html_page_list = @import("./examples/list.zig").ProductList(allocator);
|
||||
html_page_list.render(&aw_list.writer) catch unreachable;
|
||||
html_page_list.render(&aw_list.writer, .{}) catch unreachable;
|
||||
const html_list = allocator.dupe(u8, aw_list.written()) catch unreachable;
|
||||
|
||||
// Props example
|
||||
@ -70,7 +70,7 @@ pub fn Page(ctx: zx.PageContext) !zx.Component {
|
||||
const zx_props = @embedFile("./examples/props.zx");
|
||||
const zig_props = @embedFile("./examples/props.zig");
|
||||
const html_page_props = @import("./examples/props.zig").ButtonDemo(allocator);
|
||||
html_page_props.render(&aw_props.writer) catch unreachable;
|
||||
html_page_props.render(&aw_props.writer, .{}) catch unreachable;
|
||||
const html_props = allocator.dupe(u8, aw_props.written()) catch unreachable;
|
||||
|
||||
// Page example
|
||||
@ -78,7 +78,7 @@ pub fn Page(ctx: zx.PageContext) !zx.Component {
|
||||
const zx_page = @embedFile("./examples/page_example.zx");
|
||||
const zig_page = @embedFile("./examples/page_example.zig");
|
||||
const html_page_page = @import("./examples/page_example.zig").Page(ctx);
|
||||
html_page_page.render(&aw_page.writer) catch unreachable;
|
||||
html_page_page.render(&aw_page.writer, .{}) catch unreachable;
|
||||
const html_page = allocator.dupe(u8, aw_page.written()) catch unreachable;
|
||||
|
||||
// Layout example (just show code, no preview)
|
||||
@ -92,7 +92,7 @@ pub fn Page(ctx: zx.PageContext) !zx.Component {
|
||||
const zx_dynamic = @embedFile("./examples/dynamic_route.zx");
|
||||
const zig_dynamic = @embedFile("./examples/dynamic_route.zig");
|
||||
const html_page_dynamic = @import("./examples/dynamic_route.zig").UserProfile(ctx);
|
||||
html_page_dynamic.render(&aw_dynamic.writer) catch unreachable;
|
||||
html_page_dynamic.render(&aw_dynamic.writer, .{}) catch unreachable;
|
||||
const html_dynamic = allocator.dupe(u8, aw_dynamic.written()) catch unreachable;
|
||||
|
||||
// Fragment example
|
||||
@ -100,7 +100,7 @@ pub fn Page(ctx: zx.PageContext) !zx.Component {
|
||||
const zx_fragment = @embedFile("./examples/fragment.zx");
|
||||
const zig_fragment = @embedFile("./examples/fragment.zig");
|
||||
const html_page_fragment = @import("./examples/fragment.zig").FragmentDemo(allocator);
|
||||
html_page_fragment.render(&aw_fragment.writer) catch unreachable;
|
||||
html_page_fragment.render(&aw_fragment.writer, .{}) catch unreachable;
|
||||
const html_fragment = allocator.dupe(u8, aw_fragment.written()) catch unreachable;
|
||||
|
||||
// Children example
|
||||
@ -108,7 +108,7 @@ pub fn Page(ctx: zx.PageContext) !zx.Component {
|
||||
const zx_children = @embedFile("./examples/children.zx");
|
||||
const zig_children = @embedFile("./examples/children.zig");
|
||||
const html_page_children = @import("./examples/children.zig").CardDemo(allocator);
|
||||
html_page_children.render(&aw_children.writer) catch unreachable;
|
||||
html_page_children.render(&aw_children.writer, .{}) catch unreachable;
|
||||
const html_children = allocator.dupe(u8, aw_children.written()) catch unreachable;
|
||||
|
||||
// Dynamic attributes example
|
||||
@ -116,7 +116,7 @@ pub fn Page(ctx: zx.PageContext) !zx.Component {
|
||||
const zx_dynattr = @embedFile("./examples/dynamic_attr.zx");
|
||||
const zig_dynattr = @embedFile("./examples/dynamic_attr.zig");
|
||||
const html_page_dynattr = @import("./examples/dynamic_attr.zig").DynamicAttrs(allocator);
|
||||
html_page_dynattr.render(&aw_dynattr.writer) catch unreachable;
|
||||
html_page_dynattr.render(&aw_dynattr.writer, .{}) catch unreachable;
|
||||
const html_dynattr = allocator.dupe(u8, aw_dynattr.written()) catch unreachable;
|
||||
|
||||
// API route example
|
||||
|
||||
@ -11,7 +11,7 @@ pub fn main() !void {
|
||||
}
|
||||
|
||||
const component = resolveComponent(allocator);
|
||||
try component.render(&aw.writer);
|
||||
try component.render(&aw.writer, .{});
|
||||
try std.fs.File.stdout().writeAll(aw.written());
|
||||
}
|
||||
|
||||
|
||||
@ -9,7 +9,7 @@ pub fn Page(ctx: zx.PageContext) !zx.Component {
|
||||
const zx_example_if = @embedFile("../examples/if.zx");
|
||||
const zig_example_if = @embedFile("../examples/if.zig");
|
||||
const html_page_if = @import("../examples/if.zig").Conditional(allocator);
|
||||
html_page_if.render(&aw_if.writer) catch unreachable;
|
||||
html_page_if.render(&aw_if.writer, .{}) catch unreachable;
|
||||
const html_example_if = allocator.dupe(u8, aw_if.written()) catch unreachable;
|
||||
|
||||
// Switch example
|
||||
@ -17,7 +17,7 @@ pub fn Page(ctx: zx.PageContext) !zx.Component {
|
||||
const zx_example_switch = @embedFile("../examples/switch.zx");
|
||||
const zig_example_switch = @embedFile("../examples/switch.zig");
|
||||
const html_page_switch = @import("../examples/switch.zig").RoleSwitch(allocator);
|
||||
html_page_switch.render(&aw_switch.writer) catch unreachable;
|
||||
html_page_switch.render(&aw_switch.writer, .{}) catch unreachable;
|
||||
const html_example_switch = allocator.dupe(u8, aw_switch.written()) catch unreachable;
|
||||
|
||||
// For example
|
||||
@ -25,7 +25,7 @@ pub fn Page(ctx: zx.PageContext) !zx.Component {
|
||||
const zx_example_for = @embedFile("../examples/for.zx");
|
||||
const zig_example_for = @embedFile("../examples/for.zig");
|
||||
const html_page_for = @import("../examples/for.zig").UserList(allocator);
|
||||
html_page_for.render(&aw_for.writer) catch unreachable;
|
||||
html_page_for.render(&aw_for.writer, .{}) catch unreachable;
|
||||
const html_example_for = allocator.dupe(u8, aw_for.written()) catch unreachable;
|
||||
|
||||
// While example
|
||||
@ -33,7 +33,7 @@ pub fn Page(ctx: zx.PageContext) !zx.Component {
|
||||
const zx_example_while = @embedFile("../examples/while.zx");
|
||||
const zig_example_while = @embedFile("../examples/while.zig");
|
||||
const html_page_while = @import("../examples/while.zig").Counter(allocator);
|
||||
html_page_while.render(&aw_while.writer) catch unreachable;
|
||||
html_page_while.render(&aw_while.writer, .{}) catch unreachable;
|
||||
const html_example_while = allocator.dupe(u8, aw_while.written()) catch unreachable;
|
||||
|
||||
// Expression example
|
||||
@ -41,7 +41,7 @@ pub fn Page(ctx: zx.PageContext) !zx.Component {
|
||||
const zx_example_expr = @embedFile("../examples/text.zx");
|
||||
const zig_example_expr = @embedFile("../examples/text.zig");
|
||||
const html_page_expr = try @import("../examples/text.zig").Expressions(allocator);
|
||||
html_page_expr.render(&aw_expr.writer) catch unreachable;
|
||||
html_page_expr.render(&aw_expr.writer, .{}) catch unreachable;
|
||||
const html_example_expr = allocator.dupe(u8, aw_expr.written()) catch unreachable;
|
||||
|
||||
// Components example
|
||||
@ -49,7 +49,7 @@ pub fn Page(ctx: zx.PageContext) !zx.Component {
|
||||
const zx_example_components = @embedFile("../examples/components.zx");
|
||||
const zig_example_components = @embedFile("../examples/components.zig");
|
||||
const html_page_components = @import("../examples/components.zig").ButtonDemo(ctx);
|
||||
html_page_components.render(&aw_components.writer) catch unreachable;
|
||||
html_page_components.render(&aw_components.writer, .{}) catch unreachable;
|
||||
const html_example_components = allocator.dupe(u8, aw_components.written()) catch unreachable;
|
||||
|
||||
// Routing example
|
||||
@ -57,7 +57,7 @@ pub fn Page(ctx: zx.PageContext) !zx.Component {
|
||||
const zx_example_routing = @embedFile("../examples/routing.zx");
|
||||
const zig_example_routing = @embedFile("../examples/routing.zig");
|
||||
const html_page_routing = @import("../examples/routing.zig").Page(ctx);
|
||||
html_page_routing.render(&aw_routing.writer) catch unreachable;
|
||||
html_page_routing.render(&aw_routing.writer, .{}) catch unreachable;
|
||||
const html_example_routing = allocator.dupe(u8, aw_routing.written()) catch unreachable;
|
||||
|
||||
// Layout example - use simple example like learn page
|
||||
@ -66,7 +66,7 @@ pub fn Page(ctx: zx.PageContext) !zx.Component {
|
||||
const zig_example_layout = @embedFile("../learn/examples/layout_example.zig");
|
||||
const layoutctx = zx.LayoutContext.init(ctx.request, ctx.response, ctx.allocator);
|
||||
const html_page_layout = @import("../learn/examples/layout_example.zig").Layout(layoutctx, zx.Component{ .text = "Hello, World!" });
|
||||
html_page_layout.render(&aw_layout.writer) catch unreachable;
|
||||
html_page_layout.render(&aw_layout.writer, .{}) catch unreachable;
|
||||
const html_example_layout = allocator.dupe(u8, aw_layout.written()) catch unreachable;
|
||||
|
||||
// Allocator example
|
||||
@ -74,7 +74,7 @@ pub fn Page(ctx: zx.PageContext) !zx.Component {
|
||||
const zx_example_allocator = @embedFile("../examples/allocator.zx");
|
||||
const zig_example_allocator = @embedFile("../examples/allocator.zig");
|
||||
const html_page_allocator = @import("../examples/allocator.zig").AllocatorDemo(ctx);
|
||||
html_page_allocator.render(&aw_allocator.writer) catch unreachable;
|
||||
html_page_allocator.render(&aw_allocator.writer, .{}) catch unreachable;
|
||||
const html_example_allocator = allocator.dupe(u8, aw_allocator.written()) catch unreachable;
|
||||
|
||||
const expr_syntax = "{expression}";
|
||||
@ -234,7 +234,7 @@ pub fn Page(ctx: zx.PageContext) !zx.Component {
|
||||
const zx_example_escaping = @embedFile("../examples/escaping.zx");
|
||||
const zig_example_escaping = @embedFile("../examples/escaping.zig");
|
||||
const html_page_escaping = @import("../examples/escaping.zig").RawHtml(allocator);
|
||||
html_page_escaping.render(&aw_escaping.writer) catch unreachable;
|
||||
html_page_escaping.render(&aw_escaping.writer, .{}) catch unreachable;
|
||||
const html_example_escaping = allocator.dupe(u8, aw_escaping.written()) catch unreachable;
|
||||
|
||||
// CSR example
|
||||
@ -242,7 +242,7 @@ pub fn Page(ctx: zx.PageContext) !zx.Component {
|
||||
const zx_example_csr = @embedFile("../examples/react.zx");
|
||||
const zig_example_csr = @embedFile("../examples/react.zig");
|
||||
const html_page_csr = @import("../examples/react.zig").ReactDemo(allocator);
|
||||
html_page_csr.render(&aw_csr.writer) catch unreachable;
|
||||
html_page_csr.render(&aw_csr.writer, .{}) catch unreachable;
|
||||
const html_example_csr = allocator.dupe(u8, aw_csr.written()) catch unreachable;
|
||||
|
||||
// Importing example
|
||||
@ -250,7 +250,7 @@ pub fn Page(ctx: zx.PageContext) !zx.Component {
|
||||
const zx_example_importing = @embedFile("../examples/importing.zx");
|
||||
const zig_example_importing = @embedFile("../examples/importing.zig");
|
||||
const html_page_importing = @import("../examples/importing.zig").ImportDemo(allocator);
|
||||
html_page_importing.render(&aw_importing.writer) catch unreachable;
|
||||
html_page_importing.render(&aw_importing.writer, .{}) catch unreachable;
|
||||
const html_example_importing = allocator.dupe(u8, aw_importing.written()) catch unreachable;
|
||||
|
||||
// Dynamic route example
|
||||
@ -258,7 +258,7 @@ pub fn Page(ctx: zx.PageContext) !zx.Component {
|
||||
const zx_example_dynamic = @embedFile("../examples/dynamic_route.zx");
|
||||
const zig_example_dynamic = @embedFile("../examples/dynamic_route.zig");
|
||||
const html_page_dynamic = @import("../examples/dynamic_route.zig").UserProfile(ctx);
|
||||
html_page_dynamic.render(&aw_dynamic.writer) catch unreachable;
|
||||
html_page_dynamic.render(&aw_dynamic.writer, .{}) catch unreachable;
|
||||
const html_example_dynamic = allocator.dupe(u8, aw_dynamic.written()) catch unreachable;
|
||||
|
||||
// API and WebSocket examples
|
||||
@ -270,7 +270,7 @@ pub fn Page(ctx: zx.PageContext) !zx.Component {
|
||||
const zx_example_fragment = @embedFile("../examples/fragment.zx");
|
||||
const zig_example_fragment = @embedFile("../examples/fragment.zig");
|
||||
const html_page_fragment = @import("../examples/fragment.zig").FragmentDemo(allocator);
|
||||
html_page_fragment.render(&aw_fragment.writer) catch unreachable;
|
||||
html_page_fragment.render(&aw_fragment.writer, .{}) catch unreachable;
|
||||
const html_example_fragment = allocator.dupe(u8, aw_fragment.written()) catch unreachable;
|
||||
|
||||
// Children example
|
||||
@ -278,7 +278,7 @@ pub fn Page(ctx: zx.PageContext) !zx.Component {
|
||||
const zx_example_children = @embedFile("../examples/children.zx");
|
||||
const zig_example_children = @embedFile("../examples/children.zig");
|
||||
const html_page_children = @import("../examples/children.zig").CardDemo(allocator);
|
||||
html_page_children.render(&aw_children.writer) catch unreachable;
|
||||
html_page_children.render(&aw_children.writer, .{}) catch unreachable;
|
||||
const html_example_children = allocator.dupe(u8, aw_children.written()) catch unreachable;
|
||||
|
||||
// const setup_elapsed_ns = page_timer.read();
|
||||
|
||||
7
site/app/styles/tailwind.css
Normal file
7
site/app/styles/tailwind.css
Normal file
@ -0,0 +1,7 @@
|
||||
@import "tailwindcss";
|
||||
@source "./app/**/*.zx";
|
||||
|
||||
|
||||
.test2 {
|
||||
@apply bg-red-500;
|
||||
}
|
||||
@ -101,6 +101,7 @@ pub fn build(b: *std.Build) !void {
|
||||
var ziex_b = try ziex.init(b, app_exe, .{
|
||||
.app = .{
|
||||
.path = b.path("app"),
|
||||
// .base_path = "/test",
|
||||
.copy_embedded_sources = true,
|
||||
},
|
||||
.client = .{ .jsglue_href = "/assets/_/main.js" },
|
||||
@ -110,14 +111,14 @@ pub fn build(b: *std.Build) !void {
|
||||
var assetsdir = ziex_b.assetsdir;
|
||||
const tailwindcss_b = tailwindcss.addBuild(b, .{
|
||||
.config = .{
|
||||
.input = b.path("app/assets/docs.css"),
|
||||
// .output = assetsdir.path(b, "docs.css"),
|
||||
.minify = true,
|
||||
.optimize = true,
|
||||
.map = false,
|
||||
.input = b.path("app/styles/tailwind.css"),
|
||||
// .minify = true,
|
||||
// .optimize = true,
|
||||
// .map = false,
|
||||
},
|
||||
});
|
||||
_ = b.addInstallFile(tailwindcss_b.file, "static/assets/docs.css");
|
||||
const css_install = b.addInstallFile(tailwindcss_b.file, "static/assets/_/tailwind.css");
|
||||
b.default_step.dependOn(&css_install.step);
|
||||
|
||||
ziex_b.plugin(ziex.plugins.esbuild(b, .{
|
||||
.input = b.path("app/scripts/react.ts"),
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
const std = @import("std");
|
||||
const injection = @import("init/injection.zig");
|
||||
const html_util = @import("../util/html.zig");
|
||||
|
||||
const LazyPath = std.Build.LazyPath;
|
||||
const AddElementOptions = injection.AddElementOptions;
|
||||
@ -44,6 +45,7 @@ pub fn init(b: *std.Build, exe: *std.Build.Step.Compile, options: InitOptions) !
|
||||
const ziex_js_dep = zx_dep.builder.dependency("ziex_js", .{});
|
||||
|
||||
var opts: InitInnerOptions = .{
|
||||
.base_path = null,
|
||||
.site_path = b.path("app"),
|
||||
.cli_path = null,
|
||||
.site_outdir = null,
|
||||
@ -59,6 +61,7 @@ pub fn init(b: *std.Build, exe: *std.Build.Step.Compile, options: InitOptions) !
|
||||
if (options.app) |site_opts| {
|
||||
opts.site_path = site_opts.path;
|
||||
opts.copy_embedded_sources = site_opts.copy_embedded_sources;
|
||||
opts.base_path = site_opts.base_path;
|
||||
}
|
||||
|
||||
opts.cli_path = options.cli.path;
|
||||
@ -72,6 +75,7 @@ pub fn init(b: *std.Build, exe: *std.Build.Step.Compile, options: InitOptions) !
|
||||
|
||||
const InitInnerOptions = struct {
|
||||
site_path: LazyPath,
|
||||
base_path: ?[]const u8,
|
||||
cli_path: ?LazyPath,
|
||||
site_outdir: ?LazyPath,
|
||||
steps: InitOptions.CliOptions.Steps,
|
||||
@ -121,6 +125,9 @@ pub fn initInner(
|
||||
// --- ZX Options --- //
|
||||
const zx_options = b.addOptions();
|
||||
zx_options.addOption(?[]const u8, "jsglue_href", opts.client.jsglue_href);
|
||||
zx_options.addOption(?[]const u8, "wasm_href", opts.client.wasm_href);
|
||||
zx_options.addOption(?[]const u8, "app_base_path", opts.base_path);
|
||||
|
||||
zx_module.addOptions("zx_options", zx_options);
|
||||
|
||||
// --- Dirs Setup --- //
|
||||
@ -146,6 +153,9 @@ pub fn initInner(
|
||||
if (opts.copy_embedded_sources) {
|
||||
transpile_cmd.addArg("--copy-embedded-sources");
|
||||
}
|
||||
if (opts.base_path) |bp| {
|
||||
transpile_cmd.addArgs(&.{ "--base-path", bp });
|
||||
}
|
||||
// Always generate inlined sourcemaps so dev mode can remap errors to .zx files
|
||||
transpile_cmd.addArgs(&.{ "--map", "inline" });
|
||||
const cache_path_arg = b.pathJoin(&.{ b.cache_root.path orelse ".zig-cache", "zx_transpile" });
|
||||
@ -153,9 +163,9 @@ pub fn initInner(
|
||||
transpile_cmd.expectExitCode(0);
|
||||
|
||||
const zxjs_default_href = "/assets/_/main.js";
|
||||
var zxjs_href = opts.client.jsglue_href orelse zxjs_default_href;
|
||||
var zxjs_href = html_util.prefixPathWithBasePath(b.allocator, opts.base_path, opts.client.jsglue_href orelse zxjs_default_href);
|
||||
const wasm_default_href = "/assets/_/main.wasm";
|
||||
const wasm_href = opts.client.wasm_href orelse wasm_default_href;
|
||||
const wasm_href = html_util.prefixPathWithBasePath(b.allocator, opts.base_path, opts.client.wasm_href orelse wasm_default_href);
|
||||
// --- Static Directory Setup --- //
|
||||
{
|
||||
// Install public directory into static (only if the directory exists)
|
||||
@ -322,6 +332,7 @@ pub fn initInner(
|
||||
});
|
||||
|
||||
site_wasm_module.addImport("zx_meta", wasm_zx_meta_module);
|
||||
site_wasm_module.addOptions("zx_options", zx_options);
|
||||
wasm_exe.root_module.addImport("zx", site_wasm_module);
|
||||
wasm_exe.step.dependOn(&transpile_cmd.step);
|
||||
|
||||
|
||||
@ -63,6 +63,12 @@ pub const AppOptions = struct {
|
||||
/// and other app assets. Defaults to "app" if not specified in InitOptions.
|
||||
path: LazyPath,
|
||||
|
||||
/// Base path for all routes in your app (e.g. if your app is served from "/blog", set this to "/blog").
|
||||
///
|
||||
/// This will be used to prefix all route URLs and asset paths in your app.
|
||||
/// If `null`, defaults to root path ("/").
|
||||
base_path: ?[]const u8 = null,
|
||||
|
||||
/// Copy embedded `.zx` source files to the transpile output directory.
|
||||
///
|
||||
/// When enabled, any `.zx` files referenced via `@embedFile` in your templates
|
||||
|
||||
@ -7,6 +7,7 @@ pub const routes: []const zx.server.ServerMeta.Route = &.{};
|
||||
pub const meta = zx.server.ServerMeta{
|
||||
.routes = &routes,
|
||||
.rootdir = "",
|
||||
.base_path = null,
|
||||
};
|
||||
|
||||
/// Re-export components (stub for standalone builds)
|
||||
|
||||
@ -61,6 +61,13 @@ const cachedir_flag = zli.Flag{
|
||||
.default_value = .{ .String = "" },
|
||||
};
|
||||
|
||||
const base_path_flag = zli.Flag{
|
||||
.name = "base-path",
|
||||
.description = "Base path for the application (e.g., /test)",
|
||||
.type = .String,
|
||||
.default_value = .{ .String = "" },
|
||||
};
|
||||
|
||||
pub fn register(writer: *std.Io.Writer, reader: *std.Io.Reader, allocator: std.mem.Allocator) !*zli.Command {
|
||||
const cmd = try zli.Command.init(writer, reader, allocator, .{
|
||||
.name = "transpile",
|
||||
@ -75,6 +82,7 @@ pub fn register(writer: *std.Io.Writer, reader: *std.Io.Reader, allocator: std.m
|
||||
try cmd.addFlag(rootdir_flag);
|
||||
try cmd.addFlag(depfile_flag);
|
||||
try cmd.addFlag(cachedir_flag);
|
||||
try cmd.addFlag(base_path_flag);
|
||||
try cmd.addPositionalArg(.{
|
||||
.name = "path",
|
||||
.description = "Path to .zx file or directory",
|
||||
@ -96,6 +104,8 @@ fn transpile(ctx: zli.CommandContext) !void {
|
||||
const dep_file: ?[]const u8 = if (depfile_str.len > 0) depfile_str else null;
|
||||
const cache_dir_str = ctx.flag("cache-dir", []const u8);
|
||||
const cache_dir: ?[]const u8 = if (cache_dir_str.len > 0) cache_dir_str else null;
|
||||
const base_path_str = ctx.flag("base-path", []const u8);
|
||||
const base_path: ?[]const u8 = if (base_path_str.len > 0) base_path_str else null;
|
||||
const map: zx.Ast.ParseOptions.MapMode = if (std.mem.eql(u8, sourcemap_str, "inline"))
|
||||
.inlined
|
||||
else if (std.mem.eql(u8, sourcemap_str, "none"))
|
||||
@ -138,6 +148,7 @@ fn transpile(ctx: zli.CommandContext) !void {
|
||||
.rootdir = rootdir,
|
||||
.dep_file = dep_file,
|
||||
.cache_dir = cache_dir,
|
||||
.base_path = base_path,
|
||||
});
|
||||
return;
|
||||
},
|
||||
@ -226,6 +237,7 @@ fn transpile(ctx: zli.CommandContext) !void {
|
||||
.rootdir = rootdir,
|
||||
.dep_file = dep_file,
|
||||
.cache_dir = cache_dir,
|
||||
.base_path = base_path,
|
||||
});
|
||||
}
|
||||
|
||||
@ -797,7 +809,7 @@ const Route = struct {
|
||||
}
|
||||
};
|
||||
|
||||
fn genRoutes(allocator: std.mem.Allocator, output_dir: []const u8, rootdir: ?[]const u8, verbose: bool) !void {
|
||||
fn genRoutes(allocator: std.mem.Allocator, output_dir: []const u8, rootdir: ?[]const u8, base_path: ?[]const u8, verbose: bool) !void {
|
||||
var routes = std.array_list.Managed(Route).init(allocator);
|
||||
defer {
|
||||
for (routes.items) |*route| {
|
||||
@ -884,6 +896,9 @@ fn genRoutes(allocator: std.mem.Allocator, output_dir: []const u8, rootdir: ?[]c
|
||||
try writer.writeAll("pub const meta = zx.server.ServerMeta{\n");
|
||||
try writer.writeAll(" .routes = &routes,\n");
|
||||
try writer.print(" .rootdir = \"{s}\",\n", .{escaped_path});
|
||||
if (base_path) |bp| {
|
||||
try writer.print(" .base_path = \"{s}\",\n", .{bp});
|
||||
}
|
||||
try writer.writeAll("};\n\n");
|
||||
try writer.writeAll("const zx = @import(\"zx\");\n");
|
||||
// Re-export components from components.zig (same directory, same module tree)
|
||||
@ -1610,6 +1625,7 @@ const TranspileOptions = struct {
|
||||
rootdir: ?[]const u8 = null,
|
||||
dep_file: ?[]const u8 = null,
|
||||
cache_dir: ?[]const u8 = null,
|
||||
base_path: ?[]const u8 = null,
|
||||
};
|
||||
|
||||
fn transpileCommand(allocator: std.mem.Allocator, opts: TranspileOptions) !void {
|
||||
@ -1683,7 +1699,7 @@ fn transpileCommand(allocator: std.mem.Allocator, opts: TranspileOptions) !void
|
||||
}
|
||||
|
||||
// Generate routes
|
||||
genRoutes(allocator, opts.outdir, opts.rootdir, opts.verbose) catch |err| {
|
||||
genRoutes(allocator, opts.outdir, opts.rootdir, opts.base_path, opts.verbose) catch |err| {
|
||||
std.debug.print("Warning: Failed to generate meta.zig: {}\n", .{err});
|
||||
};
|
||||
|
||||
|
||||
@ -334,7 +334,7 @@ pub fn render(self: *Client, cmp: ComponentMeta) !void {
|
||||
const vtree_ptr = self.vtrees.getPtr(cmp.id).?;
|
||||
|
||||
// Map the VDOM to platform-specific nodes (DOM)
|
||||
const dom_node = try vtree_mod.createPlatformNodes(allocator, vtree_ptr.vtree, self);
|
||||
const dom_node = try vtree_mod.createPlatformNodes(allocator, vtree_ptr.vtree, self, .{});
|
||||
try marker.replaceContent(dom_node);
|
||||
|
||||
// registerVElement is already called recursively inside createPlatformNodes
|
||||
@ -352,7 +352,7 @@ pub fn render(self: *Client, cmp: ComponentMeta) !void {
|
||||
try self.vtrees.put(cmp.id, new_vtree);
|
||||
const vtree_ptr = self.vtrees.getPtr(cmp.id).?;
|
||||
|
||||
const dom_node = try vtree_mod.createPlatformNodes(allocator, vtree_ptr.vtree, self);
|
||||
const dom_node = try vtree_mod.createPlatformNodes(allocator, vtree_ptr.vtree, self, .{});
|
||||
try marker.replaceContent(dom_node);
|
||||
return;
|
||||
}
|
||||
@ -410,7 +410,7 @@ pub fn render(self: *Client, cmp: ComponentMeta) !void {
|
||||
patches.deinit(allocator);
|
||||
}
|
||||
|
||||
try vtree_mod.applyPatches(allocator, self, patches);
|
||||
try vtree_mod.applyPatches(allocator, self, patches, .{});
|
||||
|
||||
// Re-register VElements to pick up any new elements created by PLACEMENT patches
|
||||
// This ensures event handlers are registered for newly created elements
|
||||
|
||||
@ -25,7 +25,7 @@ pub fn component(
|
||||
const props_json = std.json.Stringify.valueAlloc(options.allocator, props, .{}) catch @panic("OOM");
|
||||
|
||||
var aw: std.Io.Writer.Allocating = .init(allocator);
|
||||
if (options.children) |c| c.render(&aw.writer);
|
||||
if (options.children) |c| c.render(&aw.writer, .{});
|
||||
|
||||
return zx.Component{ .element = .{ .tag = .div, .attributes = &.{
|
||||
&.{
|
||||
|
||||
@ -1,5 +1,10 @@
|
||||
const html_util = @import("../../util/html.zig");
|
||||
const vdom = @import("../core/vdom.zig");
|
||||
|
||||
pub const RenderOptions = struct {
|
||||
base_path: ?[]const u8 = base_path,
|
||||
};
|
||||
|
||||
pub const VDOMTree = vdom;
|
||||
pub const VNode = vdom.VNode;
|
||||
pub const VElement = vdom.VElement;
|
||||
@ -14,6 +19,7 @@ pub fn applyPatches(
|
||||
allocator: zx.Allocator,
|
||||
client: anytype, // *Client
|
||||
patches: std.ArrayList(Patch),
|
||||
options: RenderOptions,
|
||||
) !void {
|
||||
for (patches.items) |*patch| {
|
||||
switch (patch.type) {
|
||||
@ -36,7 +42,7 @@ pub fn applyPatches(
|
||||
.PLACEMENT => {
|
||||
const data = &patch.data.PLACEMENT;
|
||||
|
||||
_ = try createPlatformNodes(allocator, data.vnode, client);
|
||||
_ = try createPlatformNodes(allocator, data.vnode, client, options);
|
||||
|
||||
if (data.reference_id) |ref_id| {
|
||||
ext._ib(data.parent_id, data.vnode.id, ref_id);
|
||||
@ -71,7 +77,7 @@ pub fn applyPatches(
|
||||
.REPLACE => {
|
||||
const data = &patch.data.REPLACE;
|
||||
|
||||
_ = try createPlatformNodes(allocator, data.new_vnode, client);
|
||||
_ = try createPlatformNodes(allocator, data.new_vnode, client, options);
|
||||
|
||||
ext._rpc(data.parent_id, data.new_vnode.id, data.old_vnode_id);
|
||||
|
||||
@ -185,7 +191,7 @@ fn formActionCallback(ctx: *anyopaque, event: zx.client.Event) void {
|
||||
}
|
||||
|
||||
/// Build DOM nodes for a VNode subtree and register every node in the JS
|
||||
pub fn createPlatformNodes(allocator: zx.Allocator, vnode: *VNode, client: anytype) anyerror!Document.HTMLNode {
|
||||
pub fn createPlatformNodes(allocator: zx.Allocator, vnode: *VNode, client: anytype, options: RenderOptions) anyerror!Document.HTMLNode {
|
||||
if (!is_wasm) return .{ .text = Document.HTMLText.init(allocator, {}) };
|
||||
|
||||
const resolved_component = try vdom.resolveComponent(allocator, vnode.component);
|
||||
@ -220,7 +226,25 @@ pub fn createPlatformNodes(allocator: zx.Allocator, vnode: *VNode, client: anyty
|
||||
const val = attr.value orelse "";
|
||||
// defaultValue is a DOM property; the HTML attribute equivalent is "value"
|
||||
const attr_name = if (std.mem.eql(u8, attr.name, "defaultValue")) "value" else attr.name;
|
||||
ext._sa(vnode.id, attr_name.ptr, attr_name.len, val.ptr, val.len);
|
||||
|
||||
// Prefix href/src/action attributes with base_path when applicable
|
||||
var final_val = val;
|
||||
var prefixed_val: ?[]const u8 = null;
|
||||
if (options.base_path) |bp| {
|
||||
const normalized = html_util.normalizeBasePathForPrefixing(bp);
|
||||
if (normalized) |nb| {
|
||||
const is_prefixable = std.mem.eql(u8, attr_name, "href") or
|
||||
std.mem.eql(u8, attr_name, "src") or
|
||||
std.mem.eql(u8, attr_name, "action");
|
||||
if (is_prefixable and html_util.shouldPrefixPathWithBasePath(nb, val)) {
|
||||
prefixed_val = try std.mem.concat(allocator, u8, &.{ nb, val });
|
||||
final_val = prefixed_val.?;
|
||||
}
|
||||
}
|
||||
}
|
||||
defer if (prefixed_val) |pv| allocator.free(pv);
|
||||
|
||||
ext._sa(vnode.id, attr_name.ptr, attr_name.len, final_val.ptr, final_val.len);
|
||||
}
|
||||
|
||||
// Mimic Next.js: auto-inject method="post" enctype="multipart/form-data"
|
||||
@ -248,7 +272,7 @@ pub fn createPlatformNodes(allocator: zx.Allocator, vnode: *VNode, client: anyty
|
||||
}
|
||||
|
||||
for (vnode.children.items) |child| {
|
||||
_ = try createPlatformNodes(allocator, child, client);
|
||||
_ = try createPlatformNodes(allocator, child, client, options);
|
||||
ext._ac(vnode.id, child.id);
|
||||
}
|
||||
|
||||
@ -286,3 +310,7 @@ const ext = @import("window/extern.zig");
|
||||
const zx = @import("../../root.zig");
|
||||
const std = @import("std");
|
||||
const Document = zx.client.Document;
|
||||
const zx_options = @import("zx_options");
|
||||
|
||||
/// Base path for the application, read from build options at comptime.
|
||||
pub const base_path: ?[]const u8 = zx_options.app_base_path;
|
||||
|
||||
@ -80,6 +80,7 @@ pub fn handlePage(
|
||||
arena: Allocator,
|
||||
app_ctx: ?*anyopaque,
|
||||
proxy_state_ptr: ?*const anyopaque,
|
||||
base_path: ?[]const u8,
|
||||
) !PageResult {
|
||||
var pagectx = zx.PageContext.initWithAppPtr(app_ctx, request, response, allocator);
|
||||
pagectx._state_ptr = proxy_state_ptr;
|
||||
@ -87,7 +88,7 @@ pub fn handlePage(
|
||||
const page_fn = route.page orelse return .not_found;
|
||||
|
||||
// -- Server action dispatch --
|
||||
switch (try server_dispatch.dispatchAction(request, response, allocator, arena, route.path, pagectx, page_fn)) {
|
||||
switch (try server_dispatch.dispatchAction(request, response, allocator, arena, route.path, pagectx, page_fn, base_path)) {
|
||||
.not_triggered => {},
|
||||
.ok => |r| return .{ .action_handled = .{ .body = r.body } },
|
||||
.ok_native => {},
|
||||
@ -96,7 +97,7 @@ pub fn handlePage(
|
||||
}
|
||||
|
||||
// -- Server event dispatch --
|
||||
switch (try server_dispatch.dispatchServerEvent(request, allocator, arena, route.path, pagectx, page_fn)) {
|
||||
switch (try server_dispatch.dispatchServerEvent(request, allocator, arena, route.path, pagectx, page_fn, base_path)) {
|
||||
.not_triggered => {},
|
||||
.ok => |r| return .{ .event_handled = .{ .body = r.body } },
|
||||
.ok_native => {},
|
||||
|
||||
@ -892,6 +892,7 @@ pub const ServerMeta = struct {
|
||||
|
||||
routes: []const Route,
|
||||
rootdir: ?[]const u8,
|
||||
base_path: ?[]const u8 = null,
|
||||
cli_command: ?CliCommand = null,
|
||||
};
|
||||
|
||||
|
||||
@ -60,11 +60,12 @@ fn slowPathRender(
|
||||
pagectx: zx.PageContext,
|
||||
route_path: []const u8,
|
||||
arena: std.mem.Allocator,
|
||||
base_path: ?[]const u8,
|
||||
) ?anyerror {
|
||||
var page_component = page_fn(pagectx) catch |err| return err;
|
||||
var discard = std.Io.Writer.Allocating.init(arena);
|
||||
render.current_route_path = route_path;
|
||||
page_component.render(&discard.writer) catch {};
|
||||
page_component.render(&discard.writer, .{ .base_path = base_path }) catch {};
|
||||
render.current_route_path = null;
|
||||
return null;
|
||||
}
|
||||
@ -80,6 +81,7 @@ pub fn dispatchAction(
|
||||
route_path: []const u8,
|
||||
pagectx: zx.PageContext,
|
||||
page_fn: ?PageFn,
|
||||
base_path: ?[]const u8,
|
||||
) !DispatchResult {
|
||||
if (!isActionRequest(request)) return .not_triggered;
|
||||
|
||||
@ -117,7 +119,7 @@ pub fn dispatchAction(
|
||||
|
||||
// Slow path: render the page to populate the registry, then retry.
|
||||
if (page_fn) |pfn| {
|
||||
if (slowPathRender(pfn, pagectx, route_path, arena)) |err| {
|
||||
if (slowPathRender(pfn, pagectx, route_path, arena, base_path)) |err| {
|
||||
return .{ .page_error = err };
|
||||
}
|
||||
}
|
||||
@ -148,6 +150,7 @@ pub fn dispatchServerEvent(
|
||||
route_path: []const u8,
|
||||
pagectx: zx.PageContext,
|
||||
page_fn: ?PageFn,
|
||||
base_path: ?[]const u8,
|
||||
) !DispatchResult {
|
||||
if (!request.headers.has("x-zx-server-event")) return .not_triggered;
|
||||
|
||||
@ -166,7 +169,7 @@ pub fn dispatchServerEvent(
|
||||
|
||||
// Slow path: render the page to populate the registry, then retry.
|
||||
if (page_fn) |pfn| {
|
||||
if (slowPathRender(pfn, pagectx, route_path, arena)) |err| {
|
||||
if (slowPathRender(pfn, pagectx, route_path, arena, base_path)) |err| {
|
||||
return .{ .page_error = err };
|
||||
}
|
||||
}
|
||||
|
||||
@ -400,7 +400,7 @@ pub fn Handler(comptime AppCtxType: type) type {
|
||||
res.body = "404 Not Found";
|
||||
return;
|
||||
};
|
||||
component.render(writer) catch {
|
||||
component.render(writer, .{ .base_path = zx_options.app_base_path }) catch {
|
||||
res.body = "404 Not Found";
|
||||
};
|
||||
} else {
|
||||
@ -433,7 +433,7 @@ pub fn Handler(comptime AppCtxType: type) type {
|
||||
res.body = "500 Internal Server Error";
|
||||
return;
|
||||
};
|
||||
component.render(writer) catch {
|
||||
component.render(writer, .{ .base_path = zx_options.app_base_path }) catch {
|
||||
res.body = "500 Internal Server Error";
|
||||
};
|
||||
} else {
|
||||
@ -579,6 +579,7 @@ pub fn Handler(comptime AppCtxType: type) type {
|
||||
req.arena,
|
||||
self.app_ctx,
|
||||
proxy_result.state_ptr,
|
||||
zx_options.app_base_path,
|
||||
);
|
||||
|
||||
switch (result) {
|
||||
@ -610,7 +611,7 @@ pub fn Handler(comptime AppCtxType: type) type {
|
||||
std.debug.print("Error writing HTML: {}\n", .{err});
|
||||
return;
|
||||
};
|
||||
page_component.render(writer) catch |err| {
|
||||
page_component.render(writer, .{ .base_path = zx_options.app_base_path }) catch |err| {
|
||||
std.debug.print("Error rendering page: {}\n", .{err});
|
||||
return self.uncaughtError(req, res, err);
|
||||
};
|
||||
@ -635,6 +636,7 @@ pub fn Handler(comptime AppCtxType: type) type {
|
||||
req.arena,
|
||||
self.app_ctx,
|
||||
proxy_result.state_ptr,
|
||||
zx_options.app_base_path,
|
||||
);
|
||||
switch (re_result) {
|
||||
.component => |cmp| {
|
||||
@ -642,7 +644,7 @@ pub fn Handler(comptime AppCtxType: type) type {
|
||||
if (is_dev_mode) injectDevScript(req.arena, &page_component);
|
||||
const writer = &res.buffer.writer;
|
||||
_ = writer.write("<!DOCTYPE html>\n") catch return;
|
||||
page_component.render(writer) catch |err| return self.uncaughtError(req, res, err);
|
||||
page_component.render(writer, .{ .base_path = zx_options.app_base_path }) catch |err| return self.uncaughtError(req, res, err);
|
||||
res.content_type = .HTML;
|
||||
},
|
||||
.page_error => |err| return self.uncaughtError(req, res, err),
|
||||
@ -713,11 +715,9 @@ pub fn Handler(comptime AppCtxType: type) type {
|
||||
|
||||
/// Render a page with streaming SSR support
|
||||
/// Sends the initial shell immediately, then streams async components as they complete
|
||||
fn renderStreaming(self: *Self, res: *httpz.Response, page_component: *Component, arena: std.mem.Allocator) !void {
|
||||
_ = self;
|
||||
|
||||
fn renderStreaming(_: *Self, res: *httpz.Response, page_component: *Component, arena: std.mem.Allocator) !void {
|
||||
var shell_writer = std.Io.Writer.Allocating.init(arena);
|
||||
const async_components = rndr.stream(page_component.*, arena, &shell_writer.writer) catch |err| {
|
||||
const async_components = rndr.stream(page_component.*, arena, &shell_writer.writer, .{ .base_path = zx_options.app_base_path }) catch |err| {
|
||||
std.debug.print("Error streaming page: {}\n", .{err});
|
||||
return err;
|
||||
};
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
const std = @import("std");
|
||||
const zx = @import("../../root.zig");
|
||||
const registry = @import("registry.zig");
|
||||
const html_util = zx.util.html;
|
||||
|
||||
/// Set by handler.zig before calling render/stream so that any ActionContext
|
||||
/// handlers encountered during the render pass are registered for this route.
|
||||
@ -19,7 +20,7 @@ pub const AsyncComponent = struct {
|
||||
var aw = std.io.Writer.Allocating.init(allocator);
|
||||
errdefer aw.deinit();
|
||||
|
||||
try self.component.render(&aw.writer);
|
||||
try self.component.render(&aw.writer, .{});
|
||||
const html = aw.written();
|
||||
|
||||
// Build minimal script: <script>$ZX(id,`content`)</script>
|
||||
@ -44,9 +45,13 @@ pub const AsyncComponent = struct {
|
||||
}
|
||||
};
|
||||
|
||||
pub const RenderOptions = struct {
|
||||
base_path: ?[]const u8 = null,
|
||||
};
|
||||
|
||||
/// Stream method that renders HTML while collecting async components
|
||||
/// Writes placeholders for @async={.stream} components and returns them for parallel rendering
|
||||
pub fn stream(self: zx.Component, allocator: std.mem.Allocator, writer: *std.Io.Writer) ![]AsyncComponent {
|
||||
pub fn stream(self: zx.Component, allocator: std.mem.Allocator, writer: *std.Io.Writer, options: RenderOptions) ![]AsyncComponent {
|
||||
var async_components = std.array_list.Managed(AsyncComponent).init(allocator);
|
||||
errdefer async_components.deinit();
|
||||
|
||||
@ -56,6 +61,7 @@ pub fn stream(self: zx.Component, allocator: std.mem.Allocator, writer: *std.Io.
|
||||
.rendering = .server,
|
||||
.async_components = &async_components,
|
||||
.async_counter = &counter,
|
||||
.base_path = options.base_path,
|
||||
});
|
||||
return async_components.toOwnedSlice();
|
||||
}
|
||||
@ -65,10 +71,11 @@ pub const RenderInnerOptions = struct {
|
||||
rendering: ?zx.BuiltinAttribute.Rendering = .server,
|
||||
async_components: ?*std.array_list.Managed(AsyncComponent) = null,
|
||||
async_counter: ?*u32 = null,
|
||||
base_path: ?[]const u8 = null,
|
||||
};
|
||||
|
||||
pub fn render(self: zx.Component, writer: *std.Io.Writer) !void {
|
||||
try renderInner(self, writer, .{ .escaping = .html, .rendering = .server });
|
||||
pub fn render(self: zx.Component, writer: *std.Io.Writer, options: RenderOptions) !void {
|
||||
try renderInner(self, writer, .{ .escaping = .html, .rendering = .server, .base_path = options.base_path });
|
||||
}
|
||||
|
||||
pub fn renderInner(self: zx.Component, writer: *std.Io.Writer, options: RenderInnerOptions) !void {
|
||||
@ -243,7 +250,20 @@ pub fn renderInner(self: zx.Component, writer: *std.Io.Writer, options: RenderIn
|
||||
}
|
||||
if (attribute.value) |value| {
|
||||
try writer.writeAll("=\"");
|
||||
try zx.util.html.escapeAttr(writer, value);
|
||||
// Prefix href/src/action attributes with base_path when applicable
|
||||
if (options.base_path) |bp| {
|
||||
const normalized = html_util.normalizeBasePathForPrefixing(bp);
|
||||
if (normalized) |nb| {
|
||||
const name = attribute.name;
|
||||
const is_prefixable = std.mem.eql(u8, name, "href") or
|
||||
std.mem.eql(u8, name, "src") or
|
||||
std.mem.eql(u8, name, "action");
|
||||
if (is_prefixable and html_util.shouldPrefixPathWithBasePath(nb, value)) {
|
||||
try writer.writeAll(nb);
|
||||
}
|
||||
}
|
||||
}
|
||||
try html_util.escapeAttr(writer, value);
|
||||
try writer.writeAll("\"");
|
||||
}
|
||||
}
|
||||
@ -275,6 +295,7 @@ pub fn renderInner(self: zx.Component, writer: *std.Io.Writer, options: RenderIn
|
||||
.rendering = elem.rendering orelse options.rendering,
|
||||
.async_components = options.async_components,
|
||||
.async_counter = options.async_counter,
|
||||
.base_path = options.base_path,
|
||||
};
|
||||
for (children) |child| {
|
||||
try renderInner(child, writer, child_options);
|
||||
|
||||
@ -5,6 +5,7 @@ const kv = @import("kv.zig");
|
||||
const render = @import("../../server/render.zig");
|
||||
const ext = @import("extern.zig");
|
||||
const core_handler = @import("../../core/Handler.zig");
|
||||
const app_meta = @import("zx_meta").meta;
|
||||
|
||||
const Router = zx.Router;
|
||||
const Component = zx.Component;
|
||||
@ -152,6 +153,7 @@ pub fn run() !void {
|
||||
allocator,
|
||||
null, // no app_ctx in WASI
|
||||
proxy_result.state_ptr,
|
||||
app_meta.base_path,
|
||||
);
|
||||
|
||||
switch (page_result) {
|
||||
@ -195,7 +197,7 @@ pub fn run() !void {
|
||||
wasi_res.body.deinit();
|
||||
wasi_res.body = .init(allocator);
|
||||
render.current_route_path = pathname;
|
||||
cmp.render(&wasi_res.body.writer) catch {};
|
||||
cmp.render(&wasi_res.body.writer, .{ .base_path = app_meta.base_path }) catch {};
|
||||
}
|
||||
try sendResponse(stdout, stderr, &wasi_res);
|
||||
return;
|
||||
@ -220,10 +222,10 @@ pub fn run() !void {
|
||||
render.current_route_path = pathname;
|
||||
var shell_writer = std.Io.Writer.Allocating.init(allocator);
|
||||
var page_component = cmp;
|
||||
const async_components = render.stream(page_component, allocator, &shell_writer.writer) catch {
|
||||
const async_components = render.stream(page_component, allocator, &shell_writer.writer, .{ .base_path = app_meta.base_path }) catch {
|
||||
// Fallback: render the whole page at once
|
||||
var aw = std.Io.Writer.Allocating.init(allocator);
|
||||
page_component.render(&aw.writer) catch {};
|
||||
page_component.render(&aw.writer, .{ .base_path = app_meta.base_path }) catch {};
|
||||
render.current_route_path = null;
|
||||
try stdout.writeAll("<!DOCTYPE html>");
|
||||
try stdout.writeAll(aw.written());
|
||||
@ -251,7 +253,7 @@ pub fn run() !void {
|
||||
defer aw.deinit();
|
||||
render.current_route_path = pathname;
|
||||
var page_cmp = cmp;
|
||||
page_cmp.render(&aw.writer) catch {};
|
||||
page_cmp.render(&aw.writer, .{ .base_path = app_meta.base_path }) catch {};
|
||||
render.current_route_path = null;
|
||||
|
||||
try writeEdgeMeta(stderr, &wasi_res, false);
|
||||
@ -288,7 +290,7 @@ pub fn run() !void {
|
||||
wasi_res.body.deinit();
|
||||
wasi_res.body = .init(allocator);
|
||||
render.current_route_path = pathname;
|
||||
error_cmp.render(&wasi_res.body.writer) catch {};
|
||||
error_cmp.render(&wasi_res.body.writer, .{ .base_path = app_meta.base_path }) catch {};
|
||||
}
|
||||
}
|
||||
},
|
||||
@ -338,7 +340,7 @@ pub fn run() !void {
|
||||
var aw = std.Io.Writer.Allocating.init(allocator);
|
||||
defer aw.deinit();
|
||||
render.current_route_path = pathname;
|
||||
not_found_cmp.render(&aw.writer) catch {};
|
||||
not_found_cmp.render(&aw.writer, .{ .base_path = app_meta.base_path }) catch {};
|
||||
|
||||
try writeEdgeMeta(stderr, &wasi_res, false);
|
||||
try stdout.print("<!DOCTYPE html>{s}", .{aw.written()});
|
||||
|
||||
@ -61,3 +61,26 @@ pub fn unescape(writer: *std.Io.Writer, value: []const u8) !void {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn normalizeBasePathForPrefixing(base_path: ?[]const u8) ?[]const u8 {
|
||||
const value = base_path orelse return null;
|
||||
if (value.len == 0 or std.mem.eql(u8, value, "/")) return null;
|
||||
if (value.len > 1 and value[value.len - 1] == '/') return value[0 .. value.len - 1];
|
||||
return value;
|
||||
}
|
||||
|
||||
/// Returns true when `path` should be prefixed by `normalized_base_path`.
|
||||
pub fn shouldPrefixPathWithBasePath(normalized_base_path: []const u8, path: []const u8) bool {
|
||||
if (path.len == 0 or path[0] != '/') return false;
|
||||
if (std.mem.startsWith(u8, path, "//")) return false;
|
||||
if (!std.mem.startsWith(u8, path, normalized_base_path)) return true;
|
||||
if (path.len == normalized_base_path.len) return false;
|
||||
return path[normalized_base_path.len] != '/';
|
||||
}
|
||||
|
||||
/// Prefix `path` with `base_path` when needed.
|
||||
pub fn prefixPathWithBasePath(allocator: std.mem.Allocator, base_path: ?[]const u8, path: []const u8) []const u8 {
|
||||
const normalized_base = normalizeBasePathForPrefixing(base_path) orelse return path;
|
||||
if (!shouldPrefixPathWithBasePath(normalized_base, path)) return path;
|
||||
return std.fmt.allocPrint(allocator, "{s}{s}", .{ normalized_base, path }) catch @panic("OOM");
|
||||
}
|
||||
|
||||
@ -598,14 +598,14 @@ fn test_render_inner_with_cmp(comptime file_path: []const u8, comptime cmp: fn (
|
||||
if (no_expect) {
|
||||
var trash: [4096]u8 = undefined;
|
||||
var dw = std.io.Writer.Discarding.init(&trash);
|
||||
try component.render(&dw.writer);
|
||||
try component.render(&dw.writer, .{});
|
||||
try testing.expect(dw.fullCount() > 0);
|
||||
return;
|
||||
}
|
||||
|
||||
var aw = std.io.Writer.Allocating.init(allocator);
|
||||
defer aw.deinit();
|
||||
try component.render(&aw.writer);
|
||||
try component.render(&aw.writer, .{});
|
||||
const rendered = aw.written();
|
||||
try testing.expect(rendered.len > 0);
|
||||
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
const std = @import("std");
|
||||
const html = @import("zx").util.html;
|
||||
const zx = @import("zx");
|
||||
|
||||
const testing = std.testing;
|
||||
|
||||
@ -161,3 +162,21 @@ test "roundtrip: escapeText then unescape" {
|
||||
defer testing.allocator.free(unescaped);
|
||||
try testing.expectEqualStrings(original, unescaped);
|
||||
}
|
||||
|
||||
test "prefixBasePath: prefixes root-relative paths" {
|
||||
const prefixed = html.prefixPathWithBasePath(testing.allocator, "/docs", "/guide");
|
||||
defer if (prefixed.ptr != "/guide".ptr) testing.allocator.free(prefixed);
|
||||
try testing.expectEqualStrings("/docs/guide", prefixed);
|
||||
}
|
||||
|
||||
test "prefixBasePath: skips already-prefixed paths" {
|
||||
const prefixed = html.prefixPathWithBasePath(testing.allocator, "/docs", "/docs/guide");
|
||||
try testing.expectEqualStrings("/docs/guide", prefixed);
|
||||
}
|
||||
|
||||
test "prefixBasePath: skips external and protocol-relative URLs" {
|
||||
const external = html.prefixPathWithBasePath(testing.allocator, "/docs", "https://example.com/a");
|
||||
const protocol_relative = html.prefixPathWithBasePath(testing.allocator, "/docs", "//cdn.example.com/a");
|
||||
try testing.expectEqualStrings("https://example.com/a", external);
|
||||
try testing.expectEqualStrings("//cdn.example.com/a", protocol_relative);
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user