feat: app.base_path configuration

This commit is contained in:
Nurul Huda (Apon) 2026-04-10 00:34:36 +06:00
parent d40104ad18
commit 384b2904aa
No known key found for this signature in database
GPG Key ID: 5D3F1DE2855A2F79
23 changed files with 221 additions and 80 deletions

View File

@ -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);

View File

@ -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 (

View File

@ -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

View File

@ -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());
}

View File

@ -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();

View File

@ -0,0 +1,7 @@
@import "tailwindcss";
@source "./app/**/*.zx";
.test2 {
@apply bg-red-500;
}

View File

@ -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"),

View File

@ -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);

View File

@ -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

View File

@ -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)

View File

@ -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});
};

View File

@ -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

View File

@ -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 = &.{
&.{

View File

@ -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;

View File

@ -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 => {},

View File

@ -892,6 +892,7 @@ pub const ServerMeta = struct {
routes: []const Route,
rootdir: ?[]const u8,
base_path: ?[]const u8 = null,
cli_command: ?CliCommand = null,
};

View File

@ -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 };
}
}

View File

@ -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;
};

View File

@ -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);

View File

@ -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()});

View File

@ -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");
}

View File

@ -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);

View File

@ -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);
}