diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 94394583..82325731 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -41,7 +41,7 @@ jobs: - name: Setup Zig uses: mlugg/setup-zig@v2 with: - version: 0.15.2 + version: 0.16.0 - name: Build project run: zig build @@ -66,7 +66,7 @@ jobs: - name: Setup Zig uses: mlugg/setup-zig@v2 with: - version: 0.15.2 + version: 0.16.0 - name: Run tests run: zig build test diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 63055afa..fa322799 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -31,7 +31,7 @@ jobs: - name: Setup Zig uses: mlugg/setup-zig@v2 with: - version: 0.15.2 + version: 0.16.0 - name: Setup Bun uses: oven-sh/setup-bun@v1 @@ -160,7 +160,7 @@ jobs: - name: Setup Zig uses: mlugg/setup-zig@v2 with: - version: 0.15.2 + version: 0.16.0 - name: Setup Bun uses: oven-sh/setup-bun@v1 diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index b5770a57..1f699134 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -44,7 +44,7 @@ jobs: if: ${{ !inputs.base_url }} uses: mlugg/setup-zig@v2 with: - version: 0.15.2 + version: 0.16.0 - name: Setup Node uses: actions/setup-node@v4 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 0b721257..bd91939b 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -100,7 +100,7 @@ jobs: - name: Setup Zig uses: mlugg/setup-zig@v2 with: - version: 0.15.2 + version: 0.16.0 - name: Cache Zig build artifacts uses: actions/cache@v4 @@ -316,7 +316,7 @@ jobs: - name: Setup Zig uses: mlugg/setup-zig@v2 with: - version: 0.15.2 + version: 0.16.0 - name: Setup Git run: | diff --git a/.github/workflows/template.yml b/.github/workflows/template.yml index 1f68f285..5ce524d9 100644 --- a/.github/workflows/template.yml +++ b/.github/workflows/template.yml @@ -61,7 +61,7 @@ jobs: - name: Setup Zig uses: mlugg/setup-zig@v2 with: - version: 0.15.2 + version: 0.16.0 - name: Update ziex dependency to current commit run: | @@ -101,7 +101,7 @@ jobs: - name: Setup Zig uses: mlugg/setup-zig@v2 with: - version: 0.15.2 + version: 0.16.0 - name: Update ziex dependency to current commit run: | diff --git a/build.zig b/build.zig index 2abf00eb..db220300 100644 --- a/build.zig +++ b/build.zig @@ -36,6 +36,11 @@ pub fn build(b: *std.Build) !void { 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); + zx_runtime_options.addOption(?u16, "server_port", null); + zx_runtime_options.addOption(?[]const u8, "server_address", null); + zx_runtime_options.addOption(?[]const u8, "server_rootdir", null); + zx_runtime_options.addOption(?[]const u8, "cli_command", null); + zx_runtime_options.addOption(bool, "introspect", false); const cli_options_dev = b.addOptions(); cli_options_dev.addOption([]const u8, "zig_exe", b.graph.zig_exe); @@ -93,7 +98,6 @@ pub fn build(b: *std.Build) !void { // --- ZX CLI (Transpiler, Exporter, Dev Server) --- // const zli_dep = b.dependency("zli", .{ .target = target, .optimize = optimize }); - const zls_dep = b.dependency("zls", .{ .target = target, .optimize = optimize }); const exe_rootmod_opts: std.Build.Module.CreateOptions = .{ .root_source_file = b.path("src/main.zig"), .target = target, @@ -112,7 +116,10 @@ pub fn build(b: *std.Build) !void { const exe = b.addExecutable(.{ .name = "zx", .root_module = b.createModule(exe_rootmod_opts) }); exe.root_module.addOptions("build_options", exe_build_options); - if (!exclude_lsp) exe.root_module.addImport("zls", zls_dep.module("zls")); + if (!exclude_lsp) { + const zls_dep = b.dependency("zls", .{ .target = target, .optimize = optimize }); + exe.root_module.addImport("zls", zls_dep.module("zls")); + } b.installArtifact(exe); // --- Steps: Run --- // @@ -210,7 +217,7 @@ pub fn build(b: *std.Build) !void { }); const css_gen_run = b.addRunArtifact(css_gen_exe); - if (std.fs.cwd().access("vendor/webref", .{})) |_| {} else |_| { + if (b.build_root.handle.access(b.graph.io, "vendor/webref", .{})) |_| {} else |_| { const sync_cmd = b.addSystemCommand(&.{ "./tools/syncvendor", "webref" }); css_gen_run.step.dependOn(&sync_cmd.step); } @@ -232,7 +239,7 @@ pub fn build(b: *std.Build) !void { }); const events_gen_run = b.addRunArtifact(events_gen_exe); - if (std.fs.cwd().access("vendor/webref", .{})) |_| {} else |_| { + if (b.build_root.handle.access(b.graph.io, "vendor/webref", .{})) |_| {} else |_| { const sync_cmd = b.addSystemCommand(&.{ "./tools/syncvendor", "webref" }); events_gen_run.step.dependOn(&sync_cmd.step); } @@ -266,7 +273,6 @@ pub fn build(b: *std.Build) !void { const release_tree_sitter_dep = b.dependency("tree_sitter", .{ .target = resolved_target, .optimize = .ReleaseSafe }); const release_tree_sitter_zx_dep = b.dependency("tree_sitter_zx", .{ .target = resolved_target, .optimize = .ReleaseSafe, .@"build-shared" = false }); const release_tree_sitter_mdzx_dep = b.dependency("tree_sitter_mdzx", .{ .target = resolved_target, .optimize = .ReleaseSafe, .@"build-shared" = false }); - const release_zls_dep = b.dependency("zls", .{ .target = resolved_target, .optimize = .ReleaseSafe }); // Sub-modules for release const release_style_mod = b.createModule(.{ .root_source_file = b.path("src/style/root.zig"), .target = resolved_target, .optimize = .ReleaseSafe }); @@ -299,7 +305,6 @@ pub fn build(b: *std.Build) !void { .{ .name = "cli_options", .module = cli_options_rel.createModule() }, .{ .name = "zx", .module = release_mod }, .{ .name = "zli", .module = zli_dep.module("zli") }, - .{ .name = "zls", .module = release_zls_dep.module("zls") }, .{ .name = "tree_sitter", .module = release_tree_sitter_dep.module("tree_sitter") }, .{ .name = "tree_sitter_zx", .module = release_tree_sitter_zx_dep.module("tree_sitter_zx") }, }, diff --git a/build.zig.zon b/build.zig.zon index d1679660..3c123a7b 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -14,8 +14,8 @@ }, // httpz - HTTP server for Ziex .httpz = .{ - .url = "git+https://github.com/nurulhudaapon/httpz.git#7268154f43f5827bf78668e8e79a00f2ebe4db13", - .hash = "httpz-0.0.0-PNVzrBgtBwCVkSJyophIX6WHwDR0r8XhBGQr96Kk-1El", + .url = "git+https://github.com/karlseguin/http.zig.git#569bba10f22afd4ea1815416b546a8065905f820", + .hash = "httpz-0.0.0-PNVzrJUWCADf96eygXxMPMSWJ-SMNkGDB1B6BUEP7dZZ", }, .adapters = .{ .path = "pkg/adapters" }, .zli = .{ .path = "vendor/cliz" }, // Get rid of this in favor of copying from Zig's new builtin args parser once 0.16.0 is released @@ -26,8 +26,8 @@ .cachez = .{ .path = "vendor/cachez" }, // Ziex Language Server utilities .zls = .{ - .url = "git+https://github.com/ziex-dev/zls.git?ref=zx-0.15.1#6afd4d52580899022d24f41689781ec4a9350602", - .hash = "zls-0.15.1-rmm5fgg2JABtvFCDJk6Up7CuP4Khm62Li4NkCfwniFnU", + .url = "git+https://github.com/ziex-dev/zls.git?ref=zx-0.16.0#9dc776aae7db3d522be18a8d20f77f2caa085554", + .hash = "zls-0.16.0-rmm5ftHOJgC9W6p-o3hJMFIbjWE4hKV1CIU_tOSjNiSj", }, }, .paths = .{ diff --git a/patchs/zls.patch b/patchs/zls.patch new file mode 100644 index 00000000..c87c49a9 --- /dev/null +++ b/patchs/zls.patch @@ -0,0 +1,83 @@ +zx: make ZLS resolve build-graph modules in .zx files + +zxls forwards .zx files to ZLS under their real .zx URI (the *content* is +transpiled to Zig, but the URI keeps the .zx extension). This patches enables .zx files in ZLS + +Apply from the ZLS package root: git apply patchs/zls.patch + (or) patch -p1 < patchs/zls.patch + +Generated against ZLS 0.16.0 @ 494486203c3a48927f2383aa3d5ce5fca112186d + +--- a/src/DocumentStore.zig ++++ b/src/DocumentStore.zig +@@ -51,6 +51,16 @@ + }, + }; + ++// zx: zxls forwards `.zx` files to ZLS under their real `.zx` URI (zxls ++// transpiles the *content* to Zig but keeps the URI). For cross-file imports to ++// resolve, ZLS must treat `.zx` as a Zig file import just like `.zig`. ++ ++/// Whether `import_string` refers to a Zig source file (`.zig` or `.zx`). ++fn isZigFileImport(import_string: []const u8) bool { ++ return std.mem.endsWith(u8, import_string, ".zig") or ++ std.mem.endsWith(u8, import_string, ".zx"); ++} ++ + /// Represents a `build.zig` + pub const BuildFile = struct { + uri: Uri, +@@ -115,6 +125,12 @@ + + var module_root_source_file_paths: std.ArrayList([]const u8) = .empty; + ++ // zx: `.zx` page files are routed by the zx framework rather than reached ++ // via `@import`, so they never appear in the import graph below. As a ++ // fallback, associate any `.zx` file located under the build file's ++ // directory with the first compilation's root module. ++ var zx_fallback_root: ?[]const u8 = null; ++ + { + const build_config = build_file.tryLockConfig(io) orelse return .unknown; + defer build_file.unlockConfig(io); +@@ -124,6 +140,13 @@ + try module_root_source_file_paths.ensureUnusedCapacity(arena, module_paths.len); + for (module_paths) |module_path| { + module_root_source_file_paths.appendAssumeCapacity(try arena.dupe(u8, module_path)); ++ } ++ ++ if (std.mem.endsWith(u8, uri.raw, ".zx") and build_config.compilations.len != 0) { ++ const build_dir = std.Io.Dir.path.dirname(build_file.uri.raw) orelse build_file.uri.raw; ++ if (std.mem.startsWith(u8, uri.raw, build_dir)) { ++ zx_fallback_root = try arena.dupe(u8, build_config.compilations[0].root_module); ++ } + } + } + +@@ -148,6 +171,8 @@ + } + } + ++ if (zx_fallback_root) |root| return .{ .yes = try allocator.dupe(u8, root) }; ++ + return .no; + } + +@@ -516,7 +541,7 @@ + var import_string = offsets.tokenToSlice(tree, tree.nodeMainToken(params[0])); + import_string = import_string[1 .. import_string.len - 1]; + +- if (!std.mem.endsWith(u8, import_string, ".zig")) continue; ++ if (!isZigFileImport(import_string)) continue; + + const import_uri = try Uri.resolveImport(allocator, uri, parsed_uri, import_string); + file_imports.appendAssumeCapacity(import_uri); +@@ -1984,7 +2009,7 @@ + const tracy_zone = tracy.trace(@src()); + defer tracy_zone.end(); + +- if (std.mem.endsWith(u8, import_str, ".zig") or std.mem.endsWith(u8, import_str, ".zon")) { ++ if (isZigFileImport(import_str) or std.mem.endsWith(u8, import_str, ".zon")) { + const parsed_uri = handle.uri.toStdUri(); + return .{ .one = try Uri.resolveImport(allocator, handle.uri, parsed_uri, import_str) }; + } diff --git a/pkg/adapters/src/db.zig b/pkg/adapters/src/db.zig index a51a5403..137ad196 100644 --- a/pkg/adapters/src/db.zig +++ b/pkg/adapters/src/db.zig @@ -125,7 +125,7 @@ var _ctx: *anyopaque = @ptrCast(&_stateless); var _vtable: *const DriverVTable = &noop.vtable; var _default_config: DefaultConfig = .{}; var _default_connection: ?Connection = null; -var _default_connection_mutex: std.Thread.Mutex = .{}; +var _default_connection_mutex: std.atomic.Mutex = .unlocked; pub fn adapter(ctx: *anyopaque, vtable: *const DriverVTable) void { _ctx = ctx; @@ -158,7 +158,7 @@ pub fn deserialize(bytes: []const u8, options: OpenOptions) !Connection { } pub fn connection() !*Connection { - _default_connection_mutex.lock(); + while (!_default_connection_mutex.tryLock()) std.Thread.yield() catch {}; defer _default_connection_mutex.unlock(); if (_default_connection == null) { @@ -169,7 +169,7 @@ pub fn connection() !*Connection { } pub fn closeDefault() void { - _default_connection_mutex.lock(); + while (!_default_connection_mutex.tryLock()) std.Thread.yield() catch {}; defer _default_connection_mutex.unlock(); if (_default_connection) |*conn| { @@ -538,12 +538,8 @@ fn resolveDatabaseLocation(filename: ?[]const u8) !?[]const u8 { fn configuredUrl() ?[]const u8 { if (_default_config.url) |url| return url; if (builtin.os.tag == .wasi or builtin.os.tag == .freestanding) return null; - if (std.process.getEnvVarOwned(std.heap.page_allocator, "ZX_DB_URL")) |value| { - return value; - } else |_| {} - if (std.process.getEnvVarOwned(std.heap.page_allocator, "DATABASE_URL")) |value| { - return value; - } else |_| {} + if (std.c.getenv("ZX_DB_URL")) |value| return std.mem.span(value); + if (std.c.getenv("DATABASE_URL")) |value| return std.mem.span(value); return null; } diff --git a/pkg/adapters/src/db/sqlite.zig b/pkg/adapters/src/db/sqlite.zig index b963a55c..1ade7b9f 100644 --- a/pkg/adapters/src/db/sqlite.zig +++ b/pkg/adapters/src/db/sqlite.zig @@ -41,9 +41,10 @@ const DatabaseCtx = struct { } fn acquire(self: *DatabaseCtx) !BorrowedConn { + const io = std.Options.debug_io; return switch (self.mode) { .single => .{ .conn = self.conn orelse return db.DbError.InvalidState }, - .pooled => .{ .conn = (self.pool orelse return db.DbError.InvalidState).acquire(), .pool = self.pool }, + .pooled => .{ .conn = try (self.pool orelse return db.DbError.InvalidState).acquire(io), .pool = self.pool }, }; } }; @@ -53,7 +54,7 @@ const BorrowedConn = struct { pool: ?*zqlite.Pool = null, fn deinit(self: *BorrowedConn) void { - if (self.pool != null) self.conn.release(); + if (self.pool != null) self.conn.release(std.Options.debug_io); } }; @@ -695,7 +696,14 @@ fn ensureParentDir(path: []const u8) !void { if (isMemoryPath(path) or isUriPath(path)) return; const parent = std.fs.path.dirname(path) orelse return; if (parent.len == 0) return; - try std.fs.cwd().makePath(parent); + const io = std.Options.debug_io; + if (std.Io.Dir.cwd().statFile(io, parent, .{ .follow_symlinks = true })) |st| { + if (st.kind == .directory) return; + } else |_| {} + std.Io.Dir.cwd().createDirPath(io, parent) catch |err| switch (err) { + error.PathAlreadyExists => {}, + else => return err, + }; } fn isUriPath(path: []const u8) bool { diff --git a/pkg/adapters/vendor/zqlite/build.zig.zon b/pkg/adapters/vendor/zqlite/build.zig.zon index a6e026a5..99b30cfa 100644 --- a/pkg/adapters/vendor/zqlite/build.zig.zon +++ b/pkg/adapters/vendor/zqlite/build.zig.zon @@ -10,5 +10,6 @@ }, .version = "0.0.1", .fingerprint = 0x9fb4d74a63da6245, + .minimum_zig_version = "0.16.0", .dependencies = .{}, } diff --git a/pkg/adapters/vendor/zqlite/src/conn.zig b/pkg/adapters/vendor/zqlite/src/conn.zig index 4b335989..1ff27c15 100644 --- a/pkg/adapters/vendor/zqlite/src/conn.zig +++ b/pkg/adapters/vendor/zqlite/src/conn.zig @@ -1,4 +1,5 @@ const std = @import("std"); +const Io = std.Io; const c = @cImport(@cInclude("sqlite3.h")); const zqlite = @import("zqlite.zig"); @@ -24,8 +25,8 @@ pub const Conn = struct { return .{ .conn = conn.? }; } - pub fn release(self: Conn) void { - self._pool.?.release(self); + pub fn release(self: Conn, io: Io) void { + self._pool.?.release(io, self); } pub fn close(self: Conn) void { diff --git a/pkg/adapters/vendor/zqlite/src/pool.zig b/pkg/adapters/vendor/zqlite/src/pool.zig index 8a9693bd..680860a7 100644 --- a/pkg/adapters/vendor/zqlite/src/pool.zig +++ b/pkg/adapters/vendor/zqlite/src/pool.zig @@ -2,14 +2,14 @@ const std = @import("std"); const zqlite = @import("zqlite.zig"); const Conn = zqlite.Conn; -const Thread = std.Thread; const Allocator = std.mem.Allocator; +const Io = std.Io; pub const Pool = struct { conns: []Conn, available: usize, - mutex: Thread.Mutex, - cond: Thread.Condition, + mutex: Io.Mutex, + cond: Io.Condition, allocator: Allocator, pub const Config = struct { @@ -31,8 +31,8 @@ pub const Pool = struct { errdefer allocator.free(conns); pool.* = .{ - .cond = .{}, - .mutex = .{}, + .cond = .init, + .mutex = .init, .conns = conns, .available = size, .allocator = allocator, @@ -78,37 +78,39 @@ pub const Pool = struct { allocator.destroy(self); } - pub fn acquire(self: *Pool) Conn { - self.mutex.lock(); + pub fn acquire(self: *Pool, io: Io) Io.Cancelable!Conn { + try self.mutex.lock(io); while (true) { const conns = self.conns; const available = self.available; if (available == 0) { - self.cond.wait(&self.mutex); + self.cond.waitUncancelable(io, &self.mutex); continue; } const index = available - 1; const conn = conns[index]; self.available = index; - self.mutex.unlock(); + self.mutex.unlock(io); return conn; } } - pub fn release(self: *Pool, conn: Conn) void { - self.mutex.lock(); + pub fn release(self: *Pool, io: Io, conn: Conn) void { + self.mutex.lockUncancelable(io); var conns = self.conns; const available = self.available; conns[available] = conn; self.available = available + 1; - self.mutex.unlock(); - self.cond.signal(); + self.mutex.unlock(io); + self.cond.signal(io); } }; const t = std.testing; test "pool" { + const io = t.io; + var context = TestCallbackContext{ .a = 5, .b = 6, @@ -124,16 +126,19 @@ test "pool" { }); defer pool.deinit(); - const t1 = try std.Thread.spawn(.{}, testPool, .{pool}); - const t2 = try std.Thread.spawn(.{}, testPool, .{pool}); - const t3 = try std.Thread.spawn(.{}, testPool, .{pool}); + var t1 = try io.concurrent(testPool, .{ pool, io }); + defer t1.cancel(io) catch {}; + var t2 = try io.concurrent(testPool, .{ pool, io }); + defer t2.cancel(io) catch {}; + var t3 = try io.concurrent(testPool, .{ pool, io }); + defer t3.cancel(io) catch {}; - t1.join(); - t2.join(); - t3.join(); + try t1.await(io); + try t2.await(io); + try t3.await(io); - const c1 = pool.acquire(); - defer c1.release(); + const c1 = try pool.acquire(io); + defer c1.release(io); const row = (try c1.row("select cnt from pool_test", .{})).?; try t.expectEqual(@as(i64, 3000), row.int(0)); @@ -147,14 +152,14 @@ const TestCallbackContext = struct { b: i32, }; -fn testPool(p: *Pool) void { +fn testPool(p: *Pool, io: Io) Io.Cancelable!void { for (0..1000) |_| { - const conn = p.acquire(); + const conn = try p.acquire(io); conn.execNoArgs("update pool_test set cnt = cnt + 1") catch |err| { std.debug.print("update err: {any}\n", .{err}); unreachable; }; - p.release(conn); + p.release(io, conn); } } diff --git a/pkg/create-ziex/test/fingerprint.zig b/pkg/create-ziex/test/fingerprint.zig index bcb3c24d..c467f52f 100644 --- a/pkg/create-ziex/test/fingerprint.zig +++ b/pkg/create-ziex/test/fingerprint.zig @@ -5,8 +5,9 @@ pub const Fingerprint = packed struct(u64) { checksum: u32, pub fn generate(name: []const u8) Fingerprint { + var source: std.Random.IoSource = .{ .io = std.testing.io }; return .{ - .id = std.crypto.random.intRangeLessThan(u32, 1, 0xffffffff), + .id = source.interface().intRangeLessThan(u32, 1, 0xffffffff), .checksum = std.hash.Crc32.hash(name), }; } diff --git a/pkg/plugin-bun/build.zig b/pkg/plugin-bun/build.zig index b642dd8d..a2f1db77 100644 --- a/pkg/plugin-bun/build.zig +++ b/pkg/plugin-bun/build.zig @@ -37,10 +37,10 @@ fn innerInitSingle(b: *std.Build, build_item: Build) !Output { const alloc = arena.allocator(); // Create config for single build (outdir is NOT in config - injected by exe from CLI arg) - var obj = std.json.ObjectMap.init(alloc); - try obj.put("name", .{ .string = build_item.name orelse "bun build" }); + var obj = std.json.ObjectMap.empty; + try obj.put(alloc, "name", .{ .string = build_item.name orelse "bun build" }); const config_val = try build_item.config.toJsonValue(b, alloc); - try obj.put("config", config_val); + try obj.put(alloc, "config", config_val); var arr = std.json.Array.init(alloc); try arr.append(.{ .object = obj }); const json_buf = try std.json.Stringify.valueAlloc(alloc, std.json.Value{ .array = arr }, .{}); diff --git a/pkg/plugin-bun/src/BunBuildConfig.zig b/pkg/plugin-bun/src/BunBuildConfig.zig index c43d37b7..44ddf88f 100644 --- a/pkg/plugin-bun/src/BunBuildConfig.zig +++ b/pkg/plugin-bun/src/BunBuildConfig.zig @@ -38,32 +38,32 @@ splitting: ?bool = null, /// Resolve all lazy paths and serialize to a `std.json.Value` that `bunjs` /// can pass directly as `Bun.BuildConfig`. pub fn toJsonValue(self: BunBuildConfig, b: *std.Build, arena: std.mem.Allocator) !std.json.Value { - var obj = std.json.ObjectMap.init(arena); + var obj = std.json.ObjectMap.empty; // entrypoints - required array var eps = std.json.Array.init(arena); for (self.entrypoints) |lp| { try eps.append(.{ .string = lp.getPath(b) }); } - try obj.put("entrypoints", .{ .array = eps }); + try obj.put(arena, "entrypoints", .{ .array = eps }); - if (self.target) |v| try obj.put("target", .{ .string = @tagName(v) }); - if (self.format) |v| try obj.put("format", .{ .string = @tagName(v) }); - if (self.sourcemap) |v| try obj.put("sourcemap", .{ .string = @tagName(v) }); - if (self.minify) |v| try obj.put("minify", .{ .bool = v }); - if (self.splitting) |v| try obj.put("splitting", .{ .bool = v }); - if (self.public_path) |v| try obj.put("publicPath", .{ .string = v }); + if (self.target) |v| try obj.put(arena, "target", .{ .string = @tagName(v) }); + if (self.format) |v| try obj.put(arena, "format", .{ .string = @tagName(v) }); + if (self.sourcemap) |v| try obj.put(arena, "sourcemap", .{ .string = @tagName(v) }); + if (self.minify) |v| try obj.put(arena, "minify", .{ .bool = v }); + if (self.splitting) |v| try obj.put(arena, "splitting", .{ .bool = v }); + if (self.public_path) |v| try obj.put(arena, "publicPath", .{ .string = v }); if (self.external.len > 0) { var arr = std.json.Array.init(arena); for (self.external) |e| try arr.append(.{ .string = e }); - try obj.put("external", .{ .array = arr }); + try obj.put(arena, "external", .{ .array = arr }); } if (self.define.len > 0) { - var def_obj = std.json.ObjectMap.init(arena); - for (self.define) |d| try def_obj.put(d.key, .{ .string = d.value }); - try obj.put("define", .{ .object = def_obj }); + var def_obj = std.json.ObjectMap.empty; + for (self.define) |d| try def_obj.put(arena, d.key, .{ .string = d.value }); + try obj.put(arena, "define", .{ .object = def_obj }); } return .{ .object = obj }; diff --git a/pkg/plugin-bun/src/main.zig b/pkg/plugin-bun/src/main.zig index f669e5b6..cbd27c8f 100644 --- a/pkg/plugin-bun/src/main.zig +++ b/pkg/plugin-bun/src/main.zig @@ -10,27 +10,34 @@ const BuildEvent = struct { dependencies: []const []const u8 = &.{}, }; -pub fn main() !void { - var gpa = std.heap.GeneralPurposeAllocator(.{}){}; - defer _ = gpa.deinit(); +pub fn main(init: std.process.Init) !void { + var gpa = std.heap.ArenaAllocator.init(std.heap.page_allocator); + defer gpa.deinit(); const allocator = gpa.allocator(); - var args = try std.process.argsWithAllocator(allocator); - defer args.deinit(); - // --- Flags --- // var bun_path: []const u8 = "bun"; // default to "bun" in PATH var outdir_path: ?[]const u8 = null; var dep_file_path: ?[]const u8 = null; const runner_script = @embedFile("builder.ts"); + var args = try init.minimal.args.iterateAllocator(allocator); + defer args.deinit(); + _ = args.next(); // skip program name + while (args.next()) |arg| { if (std.mem.eql(u8, arg, "--bun-path")) bun_path = args.next() orelse return error.MissingBunPath; if (std.mem.eql(u8, arg, "--outdir")) outdir_path = args.next() orelse return error.MissingOutdirPath; if (std.mem.eql(u8, arg, "--dep-file")) dep_file_path = args.next() orelse return error.MissingDepFilePath; } - const input_json = try std.fs.File.stdin().readToEndAlloc(allocator, 64 * 1024 * 1024); + // Read stdin into memory + var stdin_buffer: [4096]u8 = undefined; + var stdin_reader = std.Io.File.stdin().readerStreaming(init.io, &stdin_buffer); + var stdin_writer = std.Io.Writer.Allocating.init(allocator); + defer stdin_writer.deinit(); + _ = try stdin_reader.interface.streamRemaining(&stdin_writer.writer); + const input_json = try stdin_writer.toOwnedSlice(); defer allocator.free(input_json); // Parse and inject outdir into each build config @@ -43,7 +50,7 @@ pub fn main() !void { if (outdir_path) |od| { for (builds) |*build_item| { const config_ptr = build_item.object.getPtr("config").?; - try config_ptr.object.put("outdir", .{ .string = od }); + try config_ptr.object.put(allocator, "outdir", .{ .string = od }); } } @@ -51,21 +58,27 @@ pub fn main() !void { const modified_json = try std.json.Stringify.valueAlloc(allocator, parsed.value, .{}); defer allocator.free(modified_json); - var child = std.process.Child.init( - &.{ bun_path, "-e", runner_script }, - allocator, - ); - child.stdin_behavior = .Pipe; - child.stdout_behavior = .Pipe; - child.stderr_behavior = .Inherit; - try child.spawn(); - - // Write config to bun's stdin, then close so bun sees EOF - try child.stdin.?.writeAll(modified_json); - child.stdin.?.close(); - child.stdin = null; - - var progress = std.Progress.start(.{ + var child = try std.process.spawn(init.io, .{ + .argv = &.{ bun_path, "-e", runner_script }, + .stdin = .pipe, + .stdout = .pipe, + .stderr = .inherit, + }); + + // Best-effort cleanup if we exit before the explicit wait below. + defer if (child.id != null) { + _ = child.wait(init.io) catch {}; + }; + + // Write config to bun's stdin, then close so bun sees EOF. + // Clear child.stdin so wait()'s cleanup doesn't double-close the handle. + if (child.stdin) |stdin_file| { + try stdin_file.writeStreamingAll(init.io, modified_json); + stdin_file.close(init.io); + child.stdin = null; + } + + var progress = std.Progress.start(init.io, .{ .root_name = "bun build", .estimated_total_items = build_count, }); @@ -86,66 +99,67 @@ pub fn main() !void { var all_deps = std.ArrayList([]const u8).empty; defer all_deps.deinit(allocator); - var stdout = child.stdout.?; - var buffer: [4096]u8 = undefined; - var streaming_reader = stdout.readerStreaming(&buffer); - const io_reader = &streaming_reader.interface; - var line_writer = std.Io.Writer.Allocating.init(allocator); - defer line_writer.deinit(); - - var aa = std.heap.ArenaAllocator.init(allocator); - const arena = aa.allocator(); - defer aa.deinit(); - while (io_reader.streamDelimiter(&line_writer.writer, '\n')) |_| { - const line = line_writer.written(); - _ = io_reader.takeByte() catch break; - - const ev_parsed = std.json.parseFromSlice(BuildEvent, allocator, line, .{ - .ignore_unknown_fields = true, - }) catch continue; // skip malformed lines - defer ev_parsed.deinit(); - const ev = ev_parsed.value; - - const name = try std.fmt.allocPrint(arena, "{s} ({d})", .{ ev.name, ev.id }); - - switch (ev.type) { - .start => { - const node = progress.start(name, 0); - try nodes.put(name, node); - }, - .result => { - if (ev.success == false) failed += 1; - for (ev.dependencies) |dep| { - try all_deps.append(allocator, try arena.dupe(u8, dep)); - } - }, - .@"error" => { - failed += 1; - if (ev.@"error") |msg| { - std.debug.print("bun build [{s}] error: {s}\n", .{ ev.name, msg }); - } - }, - .end => { - if (nodes.fetchRemove(name)) |kv| { - kv.value.end(); - } - progress.completeOne(); - }, - } + if (child.stdout) |stdout_file| { + var buffer: [4096]u8 = undefined; + var streaming_reader = stdout_file.readerStreaming(init.io, &buffer); + const io_reader = &streaming_reader.interface; + var line_writer = std.Io.Writer.Allocating.init(allocator); + defer line_writer.deinit(); + + var aa = std.heap.ArenaAllocator.init(allocator); + const arena = aa.allocator(); + defer aa.deinit(); + while (io_reader.streamDelimiter(&line_writer.writer, '\n')) |_| { + const line = line_writer.written(); + _ = io_reader.takeByte() catch break; + + const ev_parsed = std.json.parseFromSlice(BuildEvent, allocator, line, .{ + .ignore_unknown_fields = true, + }) catch continue; // skip malformed lines + defer ev_parsed.deinit(); + const ev = ev_parsed.value; + + const name = try std.fmt.allocPrint(arena, "{s} ({d})", .{ ev.name, ev.id }); + + switch (ev.type) { + .start => { + const node = progress.start(name, 0); + try nodes.put(name, node); + }, + .result => { + if (ev.success == false) failed += 1; + for (ev.dependencies) |dep| { + try all_deps.append(allocator, try arena.dupe(u8, dep)); + } + }, + .@"error" => { + failed += 1; + if (ev.@"error") |msg| { + std.debug.print("bun build [{s}] error: {s}\n", .{ ev.name, msg }); + } + }, + .end => { + if (nodes.fetchRemove(name)) |kv| { + kv.value.end(); + } + progress.completeOne(); + }, + } - line_writer.clearRetainingCapacity(); - } else |err| { - if (err == error.EndOfStream) {} + line_writer.clearRetainingCapacity(); + } else |err| { + if (err == error.EndOfStream) {} + } } // Write dep file before potential exit(1) if (dep_file_path) |dfp| { - writeDepFile(allocator, dfp, outdir_path orelse "dist", all_deps.items) catch |err| { + writeDepFile(allocator, init.io, dfp, outdir_path orelse "dist", all_deps.items) catch |err| { std.debug.print("Failed to write dep file: {any}\n", .{err}); }; } - const term = child.wait() catch |err| { + const term = child.wait(init.io) catch |err| { if (err == error.FileNotFound) { std.debug.print("Failed to execute bun: executable not found at '{s}'\n", .{bun_path}); return error.BunNotFound; @@ -154,7 +168,7 @@ pub fn main() !void { return error.WaitFailed; }; const exit_code: u8 = switch (term) { - .Exited => |c| c, + .exited => |c| c, else => 1, }; @@ -164,7 +178,7 @@ pub fn main() !void { } } -fn writeDepFile(allocator: std.mem.Allocator, path: []const u8, target: []const u8, deps: []const []const u8) !void { +fn writeDepFile(allocator: std.mem.Allocator, io: std.Io, path: []const u8, target: []const u8, deps: []const []const u8) !void { var buf = std.ArrayList(u8).empty; defer buf.deinit(allocator); try buf.appendSlice(allocator, target); @@ -180,7 +194,7 @@ fn writeDepFile(allocator: std.mem.Allocator, path: []const u8, target: []const } } try buf.appendSlice(allocator, "\n"); - const f = try std.fs.cwd().createFile(path, .{}); - defer f.close(); - try f.writeAll(buf.items); + const f = try std.Io.Dir.cwd().createFile(io, path, .{}); + defer f.close(io); + try f.writeStreamingAll(io, buf.items); } diff --git a/pkg/plugin-tailwindcss/build.zig b/pkg/plugin-tailwindcss/build.zig index 361ffcbd..da92670b 100644 --- a/pkg/plugin-tailwindcss/build.zig +++ b/pkg/plugin-tailwindcss/build.zig @@ -42,10 +42,10 @@ fn innerInitSingle(b: *std.Build, build_item: Build) !Output { const alloc = arena.allocator(); // Create config for single build (output is NOT in config - injected by exe from CLI arg) - var obj = std.json.ObjectMap.init(alloc); - try obj.put("name", .{ .string = build_item.name orelse "tailwindcss" }); + var obj = std.json.ObjectMap.empty; + try obj.put(alloc, "name", .{ .string = build_item.name orelse "tailwindcss" }); const config_val = try build_item.config.toJsonValue(b, alloc); - try obj.put("config", config_val); + try obj.put(alloc, "config", config_val); var arr = std.json.Array.init(alloc); try arr.append(.{ .object = obj }); const json_buf = try std.json.Stringify.valueAlloc(alloc, std.json.Value{ .array = arr }, .{}); diff --git a/pkg/plugin-tailwindcss/src/TailwindBuildConfig.zig b/pkg/plugin-tailwindcss/src/TailwindBuildConfig.zig index ff552a66..14bab3dc 100644 --- a/pkg/plugin-tailwindcss/src/TailwindBuildConfig.zig +++ b/pkg/plugin-tailwindcss/src/TailwindBuildConfig.zig @@ -21,21 +21,21 @@ base: ?std.Build.LazyPath = null, sources: ?[]const std.Build.LazyPath = null, pub fn toJsonValue(self: TailwindBuildConfig, b: *std.Build, arena: std.mem.Allocator) !std.json.Value { - var obj = std.json.ObjectMap.init(arena); + var obj = std.json.ObjectMap.empty; - try obj.put("input", .{ .string = self.input.getPath(b) }); - try obj.put("minify", .{ .bool = self.minify }); - try obj.put("optimize", .{ .bool = self.optimize }); - try obj.put("map", .{ .bool = self.map }); + try obj.put(arena, "input", .{ .string = self.input.getPath(b) }); + try obj.put(arena, "minify", .{ .bool = self.minify }); + try obj.put(arena, "optimize", .{ .bool = self.optimize }); + try obj.put(arena, "map", .{ .bool = self.map }); - if (self.base) |base| try obj.put("base", .{ .string = base.getPath(b) }); + if (self.base) |base| try obj.put(arena, "base", .{ .string = base.getPath(b) }); if (self.sources) |sources| { var arr = try std.json.Array.initCapacity(arena, sources.len); for (sources) |source| { arr.appendAssumeCapacity(.{ .string = source.getPath(b) }); } - try obj.put("sources", .{ .array = arr }); + try obj.put(arena, "sources", .{ .array = arr }); } return .{ .object = obj }; diff --git a/pkg/plugin-tailwindcss/src/main.zig b/pkg/plugin-tailwindcss/src/main.zig index 5d442c0b..8f01d58c 100644 --- a/pkg/plugin-tailwindcss/src/main.zig +++ b/pkg/plugin-tailwindcss/src/main.zig @@ -10,13 +10,14 @@ const BuildEvent = struct { dependencies: []const []const u8 = &.{}, }; -pub fn main() !void { - var gpa = std.heap.GeneralPurposeAllocator(.{}){}; - defer _ = gpa.deinit(); +pub fn main(init: std.process.Init) !void { + var gpa = std.heap.ArenaAllocator.init(std.heap.page_allocator); + defer gpa.deinit(); const allocator = gpa.allocator(); - var args = try std.process.argsWithAllocator(allocator); + var args = try init.minimal.args.iterateAllocator(allocator); defer args.deinit(); + _ = args.next(); // skip program name // --- Flags --- // var node_path: []const u8 = "node"; // default to "node" in PATH @@ -31,7 +32,13 @@ pub fn main() !void { if (std.mem.eql(u8, arg, "--dep-file")) dep_file_path = args.next() orelse return error.MissingDepFilePath; } - const input_json = try std.fs.File.stdin().readToEndAlloc(allocator, 64 * 1024 * 1024); + // Read stdin into memory + var buffer: [4096]u8 = undefined; + var reader = std.Io.File.stdin().readerStreaming(init.io, &buffer); + var writer = std.Io.Writer.Allocating.init(allocator); + defer writer.deinit(); + _ = try reader.interface.streamRemaining(&writer.writer); + const input_json = try writer.toOwnedSlice(); defer allocator.free(input_json); // Parse and inject output path into each build config @@ -44,7 +51,7 @@ pub fn main() !void { if (output_path) |op| { for (builds) |*build_item| { const config_ptr = build_item.object.getPtr("config").?; - try config_ptr.object.put("output", .{ .string = op }); + try config_ptr.object.put(allocator, "output", .{ .string = op }); } } @@ -52,36 +59,27 @@ pub fn main() !void { const modified_json = try std.json.Stringify.valueAlloc(allocator, parsed.value, .{}); defer allocator.free(modified_json); - var child = std.process.Child.init( - &.{ node_path, "-e", runner_script }, - allocator, - ); - - // Node warns when both NO_COLOR and FORCE_COLOR are set. - const env_map = try allocator.create(std.process.EnvMap); - env_map.* = try std.process.getEnvMap(allocator); - errdefer { - env_map.deinit(); - allocator.destroy(env_map); - } - _ = env_map.remove("NO_COLOR"); + var child = try std.process.spawn(init.io, .{ + .argv = &.{ node_path, "-e", runner_script }, + .stdin = .pipe, + .stdout = .pipe, + .stderr = .inherit, + }); - child.env_map = env_map; - child.stdin_behavior = .Pipe; - child.stdout_behavior = .Pipe; - child.stderr_behavior = .Inherit; - try child.spawn(); - defer { - env_map.deinit(); - allocator.destroy(env_map); - } + // Best-effort cleanup if we exit before the explicit wait below. + defer if (child.id != null) { + _ = child.wait(init.io) catch {}; + }; - // Write config to the JS runtime's stdin, then close so it sees EOF - try child.stdin.?.writeAll(modified_json); - child.stdin.?.close(); - child.stdin = null; + // Write config to the JS runtime's stdin, then close so it sees EOF. + // Clear child.stdin so wait()'s cleanup doesn't double-close the handle. + if (child.stdin) |stdin_file| { + try stdin_file.writeStreamingAll(init.io, modified_json); + stdin_file.close(init.io); + child.stdin = null; + } - var progress = std.Progress.start(.{ + var progress = std.Progress.start(init.io, .{ .root_name = "tailwindcss", .estimated_total_items = build_count, }); @@ -102,69 +100,70 @@ pub fn main() !void { var all_deps = std.ArrayList([]const u8).empty; defer all_deps.deinit(allocator); - var stdout = child.stdout.?; - var buffer: [4096]u8 = undefined; - var streaming_reader = stdout.readerStreaming(&buffer); - const io_reader = &streaming_reader.interface; - var line_writer = std.Io.Writer.Allocating.init(allocator); - defer line_writer.deinit(); - - var aa = std.heap.ArenaAllocator.init(allocator); - const arena = aa.allocator(); - defer aa.deinit(); - while (io_reader.streamDelimiter(&line_writer.writer, '\n')) |_| { - const line = line_writer.written(); - _ = io_reader.takeByte() catch break; - - const ev_parsed = std.json.parseFromSlice(BuildEvent, allocator, line, .{ - .ignore_unknown_fields = true, - }) catch continue; // skip malformed lines - defer ev_parsed.deinit(); - const ev = ev_parsed.value; - - std.Thread.sleep(10 * std.time.ns_per_ms); - - const name = try std.fmt.allocPrint(arena, "{s} ({d})", .{ ev.name, ev.id }); - - switch (ev.type) { - .start => { - const node = progress.start(name, 0); - try nodes.put(name, node); - }, - .result => { - if (ev.success == false) failed += 1; - // Collect discovered dependencies for dep file - for (ev.dependencies) |dep| { - try all_deps.append(allocator, try arena.dupe(u8, dep)); - } - }, - .@"error" => { - failed += 1; - if (ev.@"error") |msg| { - std.debug.print("tailwindcss [{s}] error: {s}\n", .{ ev.name, msg }); - } - }, - .end => { - if (nodes.fetchRemove(name)) |kv| { - kv.value.end(); - } - progress.completeOne(); - }, - } + if (child.stdout) |stdout_file| { + var stdout_buffer: [4096]u8 = undefined; + var streaming_reader = stdout_file.readerStreaming(init.io, &stdout_buffer); + const io_reader = &streaming_reader.interface; + var line_writer = std.Io.Writer.Allocating.init(allocator); + defer line_writer.deinit(); + + var aa = std.heap.ArenaAllocator.init(allocator); + const arena = aa.allocator(); + defer aa.deinit(); + while (io_reader.streamDelimiter(&line_writer.writer, '\n')) |_| { + const line = line_writer.written(); + _ = io_reader.takeByte() catch break; + + const ev_parsed = std.json.parseFromSlice(BuildEvent, allocator, line, .{ + .ignore_unknown_fields = true, + }) catch continue; // skip malformed lines + defer ev_parsed.deinit(); + const ev = ev_parsed.value; + + try std.Io.sleep(init.io, .fromMilliseconds(10), .awake); + + const name = try std.fmt.allocPrint(arena, "{s} ({d})", .{ ev.name, ev.id }); + + switch (ev.type) { + .start => { + const node = progress.start(name, 0); + try nodes.put(name, node); + }, + .result => { + if (ev.success == false) failed += 1; + // Collect discovered dependencies for dep file + for (ev.dependencies) |dep| { + try all_deps.append(allocator, try arena.dupe(u8, dep)); + } + }, + .@"error" => { + failed += 1; + if (ev.@"error") |msg| { + std.debug.print("tailwindcss [{s}] error: {s}\n", .{ ev.name, msg }); + } + }, + .end => { + if (nodes.fetchRemove(name)) |kv| { + kv.value.end(); + } + progress.completeOne(); + }, + } - line_writer.clearRetainingCapacity(); - } else |err| { - if (err == error.EndOfStream) {} + line_writer.clearRetainingCapacity(); + } else |err| { + if (err == error.EndOfStream) {} + } } // Write dep file before potential exit(1) if (dep_file_path) |dfp| { - writeDepFile(allocator, dfp, output_path orelse "output.css", all_deps.items) catch |err| { + writeDepFile(allocator, init.io, dfp, output_path orelse "output.css", all_deps.items) catch |err| { std.debug.print("Failed to write dep file: {any}\n", .{err}); }; } - const term = child.wait() catch |err| { + const term = child.wait(init.io) catch |err| { if (err == error.FileNotFound) { std.debug.print("Failed to execute JS runtime: executable not found at '{s}'\n", .{node_path}); return error.NodeNotFound; @@ -173,7 +172,7 @@ pub fn main() !void { return error.WaitFailed; }; const exit_code: u8 = switch (term) { - .Exited => |c| c, + .exited => |c| c, else => 1, }; @@ -183,7 +182,7 @@ pub fn main() !void { } } -fn writeDepFile(allocator: std.mem.Allocator, path: []const u8, target: []const u8, deps: []const []const u8) !void { +fn writeDepFile(allocator: std.mem.Allocator, io: std.Io, path: []const u8, target: []const u8, deps: []const []const u8) !void { // Build dep file content in memory, then write in one shot var buf = std.ArrayList(u8).empty; defer buf.deinit(allocator); @@ -200,7 +199,7 @@ fn writeDepFile(allocator: std.mem.Allocator, path: []const u8, target: []const } } try buf.appendSlice(allocator, "\n"); - const f = try std.fs.cwd().createFile(path, .{}); - defer f.close(); - try f.writeAll(buf.items); + const f = try std.Io.Dir.cwd().createFile(io, path, .{}); + defer f.close(io); + try f.writeStreamingAll(io, buf.items); } diff --git a/pkg/tree-sitter-mdzx/build.zig b/pkg/tree-sitter-mdzx/build.zig index f96e5392..f512bdfd 100644 --- a/pkg/tree-sitter-mdzx/build.zig +++ b/pkg/tree-sitter-mdzx/build.zig @@ -68,17 +68,9 @@ pub fn build(b: *std.Build) !void { }); tests.root_module.addImport(library_name, module); - // HACK: fetch tree-sitter dependency only when testing this module - if (b.pkg_hash.len == 0) { - var args = try std.process.argsWithAllocator(b.allocator); - defer args.deinit(); - while (args.next()) |a| { - if (std.mem.eql(u8, a, "test")) { - const ts_dep = b.lazyDependency("tree_sitter", .{}) orelse continue; - tests.root_module.addImport("tree_sitter", ts_dep.module("tree_sitter")); - break; - } - } + // Fetch tree-sitter lazily when available so tests work across build API versions. + if (b.lazyDependency("tree_sitter", .{})) |ts_dep| { + tests.root_module.addImport("tree_sitter", ts_dep.module("tree_sitter")); } const run_tests = b.addRunArtifact(tests); @@ -88,6 +80,6 @@ pub fn build(b: *std.Build) !void { inline fn fileExists(b: *std.Build, filename: []const u8) bool { const dir = b.build_root.handle; - dir.access(filename, .{}) catch return false; + dir.access(b.graph.io, filename, .{}) catch return false; return true; } diff --git a/pkg/tree-sitter-zx/build.zig b/pkg/tree-sitter-zx/build.zig index 4db45519..5c23537c 100644 --- a/pkg/tree-sitter-zx/build.zig +++ b/pkg/tree-sitter-zx/build.zig @@ -68,17 +68,9 @@ pub fn build(b: *std.Build) !void { }); tests.root_module.addImport(library_name, module); - // HACK: fetch tree-sitter dependency only when testing this module - if (b.pkg_hash.len == 0) { - var args = try std.process.argsWithAllocator(b.allocator); - defer args.deinit(); - while (args.next()) |a| { - if (std.mem.eql(u8, a, "test")) { - const ts_dep = b.lazyDependency("tree_sitter", .{}) orelse continue; - tests.root_module.addImport("tree_sitter", ts_dep.module("tree_sitter")); - break; - } - } + // Fetch tree-sitter lazily when available so tests work across build API versions. + if (b.lazyDependency("tree_sitter", .{})) |ts_dep| { + tests.root_module.addImport("tree_sitter", ts_dep.module("tree_sitter")); } const run_tests = b.addRunArtifact(tests); @@ -88,6 +80,6 @@ pub fn build(b: *std.Build) !void { inline fn fileExists(b: *std.Build, filename: []const u8) bool { const dir = b.build_root.handle; - dir.access(filename, .{}) catch return false; + dir.access(b.graph.io, filename, .{}) catch return false; return true; } diff --git a/site/app/main.zig b/site/app/main.zig index 56c1f220..f9392359 100644 --- a/site/app/main.zig +++ b/site/app/main.zig @@ -4,7 +4,7 @@ const zx = @import("zx"); pub fn main() !void { var app_ctx = AppCtx{ .port = 5588 }; - var app = try zx.App(*AppCtx).init(zx.allocator, .{ .server = .{ .port = 5588 } }, &app_ctx); + var app = try zx.App(*AppCtx).init(zx.io(), zx.allocator, .{ .server = .{ .port = 5588 } }, &app_ctx); defer app.deinit(); try app.start(); diff --git a/site/app/pages/examples/page.zx b/site/app/pages/examples/page.zx index bed9960c..99b6b2e1 100644 --- a/site/app/pages/examples/page.zx +++ b/site/app/pages/examples/page.zx @@ -14,7 +14,7 @@ pub fn Page(ctx: zx.PageContext) zx.Component { const allocator = ctx.arena; // For Loop example - var aw_for: std.io.Writer.Allocating = .init(allocator); + var aw_for: std.Io.Writer.Allocating = .init(allocator); 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); @@ -22,7 +22,7 @@ pub fn Page(ctx: zx.PageContext) zx.Component { const html_for = allocator.dupe(u8, aw_for.written()) catch unreachable; // If/Else example - var aw_if: std.io.Writer.Allocating = .init(allocator); + var aw_if: std.Io.Writer.Allocating = .init(allocator); 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); @@ -30,7 +30,7 @@ pub fn Page(ctx: zx.PageContext) zx.Component { const html_if = allocator.dupe(u8, aw_if.written()) catch unreachable; // Switch example - var aw_switch: std.io.Writer.Allocating = .init(allocator); + var aw_switch: std.Io.Writer.Allocating = .init(allocator); 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); @@ -38,7 +38,7 @@ pub fn Page(ctx: zx.PageContext) zx.Component { const html_switch = allocator.dupe(u8, aw_switch.written()) catch unreachable; // Form example - var aw_form: std.io.Writer.Allocating = .init(allocator); + var aw_form: std.Io.Writer.Allocating = .init(allocator); 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); @@ -46,7 +46,7 @@ pub fn Page(ctx: zx.PageContext) zx.Component { const html_form = allocator.dupe(u8, aw_form.written()) catch unreachable; // Components example - var aw_components: std.io.Writer.Allocating = .init(allocator); + var aw_components: std.Io.Writer.Allocating = .init(allocator); const zx_components = @embedFile("./sandbox/components.zx"); const zig_components = @embedFile("./sandbox/components.zig"); const html_page_components = @import("./sandbox/components.zig").Page(allocator); @@ -54,7 +54,7 @@ pub fn Page(ctx: zx.PageContext) zx.Component { const html_components = allocator.dupe(u8, aw_components.written()) catch unreachable; // Dynamic attributes example - var aw_dynattr: std.io.Writer.Allocating = .init(allocator); + var aw_dynattr: std.Io.Writer.Allocating = .init(allocator); 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); diff --git a/site/app/pages/examples/realtime/page.zx b/site/app/pages/examples/realtime/page.zx index ca9f1afc..90b7a921 100644 --- a/site/app/pages/examples/realtime/page.zx +++ b/site/app/pages/examples/realtime/page.zx @@ -37,7 +37,7 @@ fn JoinScreen(allocator: zx.Allocator) zx.Component { } pub fn ChatRoom(ctx: *zx.ComponentCtx(struct { username: []const u8 })) zx.Component { - const msgs_state = ctx.state(MessageList, .{}); + const msgs_state = ctx.state(MessageList, .empty); messages = msgs_state; const connected = ws != null; @@ -130,7 +130,7 @@ fn leaveChat(_: zx.client.Event) void { if (messages) |msgs_state| { var msgs = msgs_state.get(); msgs.deinit(zx.allocator); - msgs_state.set(.{}); + msgs_state.set(.empty); } // Clear input via DOM diff --git a/site/app/pages/examples/streaming/page.zx b/site/app/pages/examples/streaming/page.zx index 41b914da..a29ac89a 100644 --- a/site/app/pages/examples/streaming/page.zx +++ b/site/app/pages/examples/streaming/page.zx @@ -107,7 +107,7 @@ fn wait(duration_ms: u64) void { if (comptime builtin.cpu.arch == .wasm32) { sleep_ms(@intCast(duration_ms)); } else { - std.Thread.sleep(duration_ms * std.time.ns_per_ms); + std.Io.sleep(zx.io(), .fromMilliseconds(@intCast(duration_ms)), .awake) catch {}; } } diff --git a/site/app/pages/examples/wasm/page.zx b/site/app/pages/examples/wasm/page.zx index 39e74f9d..1d758304 100644 --- a/site/app/pages/examples/wasm/page.zx +++ b/site/app/pages/examples/wasm/page.zx @@ -11,7 +11,7 @@ const TodoList = std.ArrayListUnmanaged(Todo); pub fn TodoApp(ctx: *zx.ComponentCtx(void)) !zx.Component { const todos_state = ctx.state(TodoList, blk: { - var list = TodoList{}; + var list = TodoList.empty; try list.appendSlice(ctx.allocator, &initial_todos); break :blk list; }); diff --git a/site/app/pages/examples/wasm/progress/route.zig b/site/app/pages/examples/wasm/progress/route.zig index 5d167d47..7ffa6df5 100644 --- a/site/app/pages/examples/wasm/progress/route.zig +++ b/site/app/pages/examples/wasm/progress/route.zig @@ -6,7 +6,7 @@ pub fn POST(ctx: zx.RouteContext) !void { defer end(ctx); const delay = prng.random().intRangeAtMost(u64, 200, 800); - std.Thread.sleep(delay * std.time.ns_per_ms); + std.Io.sleep(zx.io(), .fromMilliseconds(@intCast(delay)), .awake) catch {}; const increment = prng.random().intRangeAtMost(u32, 5, 25); progress += increment; diff --git a/site/app/pages/page.zx b/site/app/pages/page.zx index 97329ff3..a995d487 100644 --- a/site/app/pages/page.zx +++ b/site/app/pages/page.zx @@ -349,7 +349,7 @@ const benchmark_tabs = [_]BenchmarkTab{ pub fn Page(ctx: zx.PageContext) zx.Component { const allocator = ctx.arena; const current_year = blk: { - const timestamp = std.time.timestamp(); + const timestamp: i64 = @intCast(@divFloor(std.Io.Timestamp.now(zx.io(), .real).nanoseconds, std.time.ns_per_s)); const seconds_per_year = 365.25 * 24 * 60 * 60; const years_since_epoch = @as(i64, @intFromFloat(@as(f64, @floatFromInt(timestamp)) / seconds_per_year)); break :blk 1970 + years_since_epoch; diff --git a/site/app/pages/reference/util.zig b/site/app/pages/reference/util.zig index 94b4425e..766992d3 100644 --- a/site/app/pages/reference/util.zig +++ b/site/app/pages/reference/util.zig @@ -154,7 +154,7 @@ pub fn stripDedupePrefixes(allocator: zx.Allocator, content: []const u8) []const } pub fn renderComponentToHtml(allocator: zx.Allocator, component: zx.Component) []const u8 { - var aw: std.io.Writer.Allocating = .init(allocator); + var aw: std.Io.Writer.Allocating = .init(allocator); component.render(&aw.writer, .{}) catch unreachable; return allocator.dupe(u8, aw.written()) catch unreachable; } @@ -434,7 +434,7 @@ const HighlightCache = struct { parser: *ts.Parser, language: *const ts.Language, query: *ts.Query, - mutex: std.Thread.Mutex = .{}, + mutex: std.Io.Mutex = .init, var instance: ?*HighlightCache = null; @@ -470,16 +470,16 @@ const HighlightCache = struct { pub fn highlightZx(allocator: std.mem.Allocator, source: []const u8) ![]u8 { if (builtin.os.tag == .freestanding) return try allocator.dupe(u8, source); - var total_timer = try std.time.Timer.start(); + var total_timer = Timer.start(); // Get cached objects (first call initializes, subsequent calls reuse) - var timer = try std.time.Timer.start(); + var timer = Timer.start(); const cache = try HighlightCache.getOrInit(std.heap.page_allocator); logTiming("Cache lookup/init", timer.lap()); // Lock for thread safety (important in concurrent requests) - cache.mutex.lock(); - defer cache.mutex.unlock(); + cache.mutex.lock(zx.io()) catch return error.Cancelled; + defer cache.mutex.unlock(zx.io()); timer.reset(); const tree = cache.parser.parseString(source, null) orelse return error.ParseError; @@ -566,6 +566,30 @@ fn appendHtmlEscapedPreserveWhitespace(out: *std.array_list.Managed(u8), text: [ } } +const Timer = struct { + start_ts: std.Io.Timestamp, + + fn start() Timer { + return .{ .start_ts = .now(zx.io(), .awake) }; + } + + fn lap(self: *Timer) u64 { + const now = std.Io.Timestamp.now(zx.io(), .awake); + const elapsed: u64 = @intCast(now.nanoseconds - self.start_ts.nanoseconds); + self.start_ts = now; + return elapsed; + } + + fn read(self: Timer) u64 { + const now = std.Io.Timestamp.now(zx.io(), .awake); + return @intCast(now.nanoseconds - self.start_ts.nanoseconds); + } + + fn reset(self: *Timer) void { + self.start_ts = .now(zx.io(), .awake); + } +}; + fn logTiming(comptime label: []const u8, elapsed_ns: u64) void { if (true) return; const elapsed_ms = @as(f64, @floatFromInt(elapsed_ns)) / @as(f64, @floatFromInt(std.time.ns_per_ms)); diff --git a/site/app/pages/time/page.zx b/site/app/pages/time/page.zx index 3df80bc6..c346ba3d 100644 --- a/site/app/pages/time/page.zx +++ b/site/app/pages/time/page.zx @@ -18,7 +18,7 @@ pub fn Page(ctx: zx.PageContext) zx.Component { } fn Time(allocator: zx.Allocator) zx.Component { - const time = std.time.timestamp(); + const time: i64 = @intCast(@divFloor(std.Io.Timestamp.now(zx.io(), .real).nanoseconds, std.time.ns_per_s)); return (
diff --git a/site/app/routes/api/route.zig b/site/app/routes/api/route.zig index 9c445c2a..3357d3d6 100644 --- a/site/app/routes/api/route.zig +++ b/site/app/routes/api/route.zig @@ -6,7 +6,7 @@ pub fn Socket(ctx: zx.SocketContext) !void { var count: usize = 0; while (count < 10) : (count += 1) { - std.Thread.sleep(1000 * std.time.ns_per_ms); + std.Io.sleep(zx.io(), .fromMilliseconds(1000), .awake) catch {}; try ctx.socket.write( try ctx.fmt("You said: {s}, count {d}", .{ ctx.message, diff --git a/site/build.zig b/site/build.zig index 2b7884f2..2f6e7292 100644 --- a/site/build.zig +++ b/site/build.zig @@ -15,74 +15,74 @@ pub fn build(b: *std.Build) !void { // const tree_sitter_mdzx_dep = ziex_dep.builder.dependency("tree_sitter_mdzx", .{ .optimize = optimize, .target = target, .@"build-shared" = false }); // --- Playground Assets --- // - const wasm_target = b.resolveTargetQuery(.{ .cpu_arch = .wasm32, .os_tag = .wasi }); - const wasm_optimize: std.builtin.OptimizeMode = .ReleaseSmall; - - const playground_dep = b.dependency("playground", .{}); - const zls_dep = playground_dep.builder.dependency("zls", .{ .target = wasm_target, .optimize = wasm_optimize }); - const zx_wasm_dep = b.dependency("ziex", .{ .target = wasm_target, .optimize = wasm_optimize }); - const zig_dep = playground_dep.builder.dependency("zig", .{ - .target = wasm_target, - .optimize = wasm_optimize, - .@"version-string" = @as([]const u8, "0.15.1"), - .@"no-lib" = true, - .dev = "wasm", - }); - - const zx_exe = zx_wasm_dep.artifact("zx"); - const zls_exe = b.addExecutable(.{ - .name = "zls", - .root_module = b.createModule(.{ - .root_source_file = playground_dep.path("src/zls.zig"), - .target = wasm_target, - .optimize = wasm_optimize, - .imports = &.{ - .{ .name = "zls", .module = zls_dep.module("zls") }, - }, - }), - }); - zls_exe.entry = .disabled; - zls_exe.rdynamic = true; - const zig_exe = zig_dep.artifact("zig"); - - // -- zig.tar.gz - const run_tar = b.addSystemCommand(&.{ "tar", "-czf" }); - const zig_tar_gz = run_tar.addOutputFileArg("zig.tar.gz"); - run_tar.addArg("-C"); - run_tar.addDirectoryArg(zig_dep.path(".")); - run_tar.addArg("lib/std"); - - // -- zx.tar.gz (only include files needed for playground compilation) - const run_zx_tar = b.addSystemCommand(&.{ "tar", "-czf" }); - run_zx_tar.has_side_effects = true; - const zx_tar_gz = run_zx_tar.addOutputFileArg("zx.tar.gz"); - run_zx_tar.addArgs(&.{ - "--exclude", "src/cli", - "--exclude", "src/lsp", - "--exclude", "src/tui", - "--exclude", "src/build", - "--exclude", "src/main.zig", - }); - run_zx_tar.addArg("-C"); - run_zx_tar.addDirectoryArg(zx_wasm_dep.path(".")); - run_zx_tar.addArg("src"); - - const playground_assets = b.addNamedWriteFiles("playground_assets"); - _ = playground_assets.addCopyFile(zls_exe.getEmittedBin(), "zls.wasm"); - _ = playground_assets.addCopyFile(zig_exe.getEmittedBin(), b.fmt("zig-{s}.wasm", .{ziex.info.minimum_zig_version})); - _ = playground_assets.addCopyFile(zx_exe.getEmittedBin(), b.fmt("zx-{s}.wasm", .{ziex.info.version})); - _ = playground_assets.addCopyFile(zig_tar_gz, b.fmt("zig-{s}.tar.gz", .{ziex.info.minimum_zig_version})); - _ = playground_assets.addCopyFile(zx_tar_gz, b.fmt("zx-{s}.tar.gz", .{ziex.info.version})); - - const install_pg = b.addInstallDirectory(.{ - .source_dir = playground_assets.getDirectory(), - .install_dir = .prefix, - .install_subdir = "static/assets/playground", - }); - - // -- Steps: pg - installs playground assets --- // - const pg_step = b.step("pg", "Install playground assets"); - pg_step.dependOn(&install_pg.step); + // const wasm_target = b.resolveTargetQuery(.{ .cpu_arch = .wasm32, .os_tag = .wasi }); + // const wasm_optimize: std.builtin.OptimizeMode = .ReleaseSmall; + // + // const playground_dep = b.dependency("playground", .{}); + // const zls_dep = playground_dep.builder.dependency("zls", .{ .target = wasm_target, .optimize = wasm_optimize }); + // const zx_wasm_dep = b.dependency("ziex", .{ .target = wasm_target, .optimize = wasm_optimize }); + // const zig_dep = playground_dep.builder.dependency("zig", .{ + // .target = wasm_target, + // .optimize = wasm_optimize, + // .@"version-string" = @as([]const u8, "0.15.1"), + // .@"no-lib" = true, + // .dev = "wasm", + // }); + // + // const zx_exe = zx_wasm_dep.artifact("zx"); + // const zls_exe = b.addExecutable(.{ + // .name = "zls", + // .root_module = b.createModule(.{ + // .root_source_file = playground_dep.path("src/zls.zig"), + // .target = wasm_target, + // .optimize = wasm_optimize, + // .imports = &.{ + // .{ .name = "zls", .module = zls_dep.module("zls") }, + // }, + // }), + // }); + // zls_exe.entry = .disabled; + // zls_exe.rdynamic = true; + // const zig_exe = zig_dep.artifact("zig"); + // + // // -- zig.tar.gz + // const run_tar = b.addSystemCommand(&.{ "tar", "-czf" }); + // const zig_tar_gz = run_tar.addOutputFileArg("zig.tar.gz"); + // run_tar.addArg("-C"); + // run_tar.addDirectoryArg(zig_dep.path(".")); + // run_tar.addArg("lib/std"); + // + // // -- zx.tar.gz (only include files needed for playground compilation) + // const run_zx_tar = b.addSystemCommand(&.{ "tar", "-czf" }); + // run_zx_tar.has_side_effects = true; + // const zx_tar_gz = run_zx_tar.addOutputFileArg("zx.tar.gz"); + // run_zx_tar.addArgs(&.{ + // "--exclude", "src/cli", + // "--exclude", "src/lsp", + // "--exclude", "src/tui", + // "--exclude", "src/build", + // "--exclude", "src/main.zig", + // }); + // run_zx_tar.addArg("-C"); + // run_zx_tar.addDirectoryArg(zx_wasm_dep.path(".")); + // run_zx_tar.addArg("src"); + // + // const playground_assets = b.addNamedWriteFiles("playground_assets"); + // _ = playground_assets.addCopyFile(zls_exe.getEmittedBin(), "zls.wasm"); + // _ = playground_assets.addCopyFile(zig_exe.getEmittedBin(), b.fmt("zig-{s}.wasm", .{ziex.info.minimum_zig_version})); + // _ = playground_assets.addCopyFile(zx_exe.getEmittedBin(), b.fmt("zx-{s}.wasm", .{ziex.info.version})); + // _ = playground_assets.addCopyFile(zig_tar_gz, b.fmt("zig-{s}.tar.gz", .{ziex.info.minimum_zig_version})); + // _ = playground_assets.addCopyFile(zx_tar_gz, b.fmt("zx-{s}.tar.gz", .{ziex.info.version})); + // + // const install_pg = b.addInstallDirectory(.{ + // .source_dir = playground_assets.getDirectory(), + // .install_dir = .prefix, + // .install_subdir = "static/assets/playground", + // }); + // + // // -- Steps: pg - installs playground assets --- // + // const pg_step = b.step("pg", "Install playground assets"); + // pg_step.dependOn(&install_pg.step); // --- ZX App Executable --- // const app_exe = b.addExecutable(.{ @@ -96,7 +96,7 @@ pub fn build(b: *std.Build) !void { app_exe.root_module.addImport("tree_sitter", tree_sitter_dep.module("tree_sitter")); app_exe.root_module.addImport("tree_sitter_zx", tree_sitter_zx_dep.module("tree_sitter_zx")); - app_exe.step.dependOn(&install_pg.step); + // app_exe.step.dependOn(&install_pg.step); // Playground disabled // --- ZX setup: wires dependencies and adds `zx`/`dev` build steps --- // var ziex_b = try ziex.init(b, app_exe, .{ diff --git a/site/build.zig.zon b/site/build.zig.zon index 51d375fc..fd369b10 100644 --- a/site/build.zig.zon +++ b/site/build.zig.zon @@ -5,12 +5,16 @@ .minimum_zig_version = "0.15.2", .dependencies = .{ .ziex = .{ .path = "../" }, - .playground = .{ - .url = "git+https://github.com/zigtools/playground.git#aee957d5adf39e0c6075696cd5f85cd4f1fcaf90", - .hash = "playground-0.0.0-iNkPNkONAQCpX8DVLyQVFizjYQw6b7bukdtfJyw_cT-U", - }, + // .playground = .{ + // .url = "git+https://github.com/zigtools/playground.git#aee957d5adf39e0c6075696cd5f85cd4f1fcaf90", + // .hash = "playground-0.0.0-iNkPNkONAQCpX8DVLyQVFizjYQw6b7bukdtfJyw_cT-U", + // }, .bunjs = .{ .path = "../pkg/plugin-bun" }, .tailwindcss = .{ .path = "../pkg/plugin-tailwindcss" }, + .ziex_js = .{ + .url = "https://registry.npmjs.org/ziex/-/ziex-0.1.0-dev.1000.tgz", + .hash = "ziex_js-0.1.0-dev.804-v1W0GT1tAwAjZsTzmZWS4c8VEaNPFV0ABzqEMhmR6-xj", + }, }, .paths = .{ "build.zig", diff --git a/src/App.zig b/src/App.zig index ce9ca9f5..c79e3694 100644 --- a/src/App.zig +++ b/src/App.zig @@ -1,3 +1,29 @@ +const std = @import("std"); +const builtin = @import("builtin"); +const platform = @import("platform.zig").platform; +const server = @import("runtime/server/Server.zig"); +const server_wasi = @import("runtime/server/wasm/entrypoint.zig"); +const client = @import("runtime/client/Client.zig").Client; + +pub const Config = @import("AppConfig.zig"); + +var debug_allocator: std.heap.DebugAllocator(.{}) = .{}; +pub const allocator = switch (builtin.os.tag) { + .wasi, .freestanding => std.heap.wasm_allocator, + else => switch (builtin.mode) { + .Debug => debug_allocator.allocator(), + .ReleaseFast, .ReleaseSafe, .ReleaseSmall => std.heap.smp_allocator, + }, +}; + +const Io = if (platform.os == .freestanding) void else std.Io; +pub fn io() Io { + if (platform.os == .freestanding) return {}; + + var threaded = std.Io.Threaded.init(allocator, .{}); + return threaded.io(); +} + pub fn App(comptime H: type) type { return AppInstance(H); } @@ -15,18 +41,28 @@ fn AppInstance(comptime H: type) type { const Self = @This(); instance: Instance, + io: ?std.Io, - pub fn init(alloc: std.mem.Allocator, config: Config, app_ctx: H) !Self { + pub fn init(process_io: anytype, alloc: std.mem.Allocator, config: Config, app_ctx: H) !Self { const instance: Instance = switch (platform.role) { .client => {}, .server => switch (platform.os) { .wasi => {}, - else => try server.Server(H).init(alloc, config, app_ctx), + else => try server.Server(H).init( + if (@TypeOf(process_io) == std.Io) process_io else return error.InvalidIo, + alloc, + config, + app_ctx, + ), }, }; if (platform.role == .server and platform.os != .wasi) instance.info(); - return .{ .instance = instance }; + + return .{ + .instance = instance, + .io = if (@TypeOf(process_io) == std.Io) process_io else null, + }; } pub fn deinit(self: *Self) void { @@ -39,29 +75,17 @@ fn AppInstance(comptime H: type) type { switch (platform.role) { .client => try client.run(), .server => switch (platform.os) { - .wasi => try server_wasi.run(), + .wasi => try server_wasi.run(.{ + .minimal = .{ .args = .{}, .environ = .{} }, + .arena = undefined, + .gpa = allocator, + .io = self.io orelse undefined, + .environ_map = undefined, + .preopens = .empty, + }), else => try self.instance.start(), }, } } }; } - -pub const Config = @import("AppConfig.zig"); - -var debug_allocator: std.heap.DebugAllocator(.{}) = .{}; -pub const allocator = switch (builtin.os.tag) { - .wasi, .freestanding => std.heap.wasm_allocator, - else => switch (builtin.mode) { - .Debug => debug_allocator.allocator(), - .ReleaseFast, .ReleaseSafe, .ReleaseSmall => std.heap.smp_allocator, - }, -}; - -const server = @import("runtime/server/Server.zig"); -const server_wasi = @import("runtime/server/wasm/entrypoint.zig"); -const client = @import("runtime/client/Client.zig").Client; -const platform = @import("platform.zig").platform; - -const builtin = @import("builtin"); -const std = @import("std"); diff --git a/src/build/init.zig b/src/build/init.zig index 655d612c..116dcdf9 100644 --- a/src/build/init.zig +++ b/src/build/init.zig @@ -225,6 +225,11 @@ pub fn initInner( 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_options.addOption(?u16, "server_port", b.option(u16, "port", "Port to run the Ziex server on")); + zx_options.addOption(?[]const u8, "server_address", b.option([]const u8, "address", "Address to bind the Ziex server to")); + zx_options.addOption(?[]const u8, "server_rootdir", b.option([]const u8, "rootdir", "Static root directory for the Ziex server")); + zx_options.addOption(?[]const u8, "cli_command", b.option([]const u8, "cli-command", "Ziex CLI command mode for the app")); + zx_options.addOption(bool, "introspect", b.option(bool, "introspect", "Print Ziex app metadata and exit") orelse false); zx_module.addOptions("zx_options", zx_options); @@ -242,6 +247,7 @@ pub fn initInner( transpile_cmd.setName("zx transpile"); transpile_cmd.addArg("transpile"); transpile_cmd.addDirectoryArg(opts.site_path); + // transpile_cmd.addArg("--verbose"); transpile_cmd.addArg("--outdir"); const transpile_outdir = getTranspileOutdir(transpile_cmd, opts); transpile_cmd.addArg("--rootdir"); @@ -268,7 +274,8 @@ pub fn initInner( { // Install public directory into static (only if the directory exists) const public_abs_path = opts.site_path.path(b, "public").getPath(b); - if (std.fs.accessAbsolute(public_abs_path, .{})) |_| { + + if (std.Io.Dir.accessAbsolute(b.graph.io, public_abs_path, .{})) |_| { const install_static = b.addInstallDirectory(.{ .source_dir = opts.site_path.path(b, "public"), .install_dir = .prefix, @@ -279,7 +286,7 @@ pub fn initInner( // Also install the generated assets into static/assets (only if the directory exists) const assets_abs_path = opts.site_path.path(b, "assets").getPath(b); - if (std.fs.accessAbsolute(assets_abs_path, .{})) |_| { + if (std.Io.Dir.accessAbsolute(b.graph.io, assets_abs_path, .{})) |_| { const install_assets = b.addInstallDirectory(.{ .source_dir = opts.site_path.path(b, "assets"), .install_dir = .prefix, @@ -489,9 +496,9 @@ pub fn initInner( const dev_cmd = getZxRun(b, zx_exe, opts); dev_cmd.addArgs(&.{ "dev", - // "--binpath", + "--binpath", }); - // dev_cmd.addFileArg(exe.getEmittedBin()); + dev_cmd.addFileArg(exe.getEmittedBin()); const dev_step = b.step(dev_step_name, "Run the Ziex app in development mode"); dev_step.dependOn(&dev_cmd.step); if (b.args) |args| dev_cmd.addArgs(args); diff --git a/src/build/init/injection.zig b/src/build/init/injection.zig index c89d755f..26982ad9 100644 --- a/src/build/init/injection.zig +++ b/src/build/init/injection.zig @@ -32,7 +32,7 @@ pub const InjectionsGenStep = struct { .owner = b, .makeFn = make, }), - .injections = .{}, + .injections = .empty, .output_file = .{ .step = &self.step }, }; return self; @@ -51,8 +51,9 @@ pub const InjectionsGenStep = struct { const self: *InjectionsGenStep = @fieldParentPtr("step", step); const b = step.owner; const allocator = b.allocator; + const io = std.Io.Threaded.global_single_threaded.io(); - var content = std.ArrayListUnmanaged(u8){}; + var content = std.ArrayListUnmanaged(u8).empty; defer content.deinit(allocator); try content.appendSlice(allocator, "// Generated by Ziex build system\n"); @@ -65,7 +66,9 @@ pub const InjectionsGenStep = struct { for (points) |p| { const html = renderInjections(allocator, self.injections.items, p.parent, p.pos); - try content.writer(allocator).print("pub const {s}: []const u8 = ", .{p.name}); + const formatted = try std.fmt.allocPrint(allocator, "pub const {s}: []const u8 = ", .{p.name}); + defer allocator.free(formatted); + try content.appendSlice(allocator, formatted); // Serialize string literal try content.appendSlice(allocator, "\""); @@ -80,7 +83,9 @@ pub const InjectionsGenStep = struct { if (std.ascii.isPrint(c)) { try content.append(allocator, c); } else { - try content.writer(allocator).print("\\x{x:0>2}", .{c}); + const hex = try std.fmt.allocPrint(allocator, "\\x{x:0>2}", .{c}); + defer allocator.free(hex); + try content.appendSlice(allocator, hex); } }, } @@ -90,7 +95,7 @@ pub const InjectionsGenStep = struct { const name = "zx_injections.zig"; const path = try b.cache_root.join(allocator, &.{name}); - try b.cache_root.handle.writeFile(.{ .sub_path = name, .data = content.items }); + try b.cache_root.handle.writeFile(io, .{ .sub_path = name, .data = content.items }); self.output_file.path = path; } @@ -102,7 +107,7 @@ fn renderInjections( parent: AddElementOptions.Parent, position: AddElementOptions.Position, ) []const u8 { - var matching = std.ArrayListUnmanaged(AddElementOptions){}; + var matching = std.ArrayListUnmanaged(AddElementOptions).empty; for (injections) |inj| { if (inj.parent == parent and inj.position == position) { matching.append(allocator, inj) catch @panic("OOM"); @@ -115,7 +120,7 @@ fn renderInjections( } }.lessThan); - var result = std.ArrayListUnmanaged(u8){}; + var result = std.ArrayListUnmanaged(u8).empty; for (matching.items) |inj| { result.appendSlice(allocator, renderSingleElement(allocator, inj.element)) catch @panic("OOM"); } @@ -123,7 +128,7 @@ fn renderInjections( } fn renderSingleElement(allocator: std.mem.Allocator, elem: AddElementOptions.ElementDef) []const u8 { - var html = std.ArrayListUnmanaged(u8){}; + var html = std.ArrayListUnmanaged(u8).empty; html.appendSlice(allocator, "<") catch @panic("OOM"); html.appendSlice(allocator, elem.tag) catch @panic("OOM"); if (elem.attributes.len > 0) { @@ -144,8 +149,8 @@ fn renderSingleElement(allocator: std.mem.Allocator, elem: AddElementOptions.Ele fn isVoidElement(tag: []const u8) bool { const void_tags = [_][]const u8{ - "area", "base", "br", "col", "embed", "hr", - "img", "input", "link", "meta", "param", "source", + "area", "base", "br", "col", "embed", "hr", + "img", "input", "link", "meta", "param", "source", "track", "wbr", }; for (void_tags) |vt| { diff --git a/src/cli/bundle.zig b/src/cli/bundle.zig index fc9877f3..1b088238 100644 --- a/src/cli/bundle.zig +++ b/src/cli/bundle.zig @@ -40,13 +40,15 @@ const docker_compose_flag = zli.Flag{ }; fn bundle(ctx: zli.CommandContext) !void { + const app = AppContext.from(&ctx); + const io = app.io; const outdir = ctx.flag("outdir", []const u8); const binpath = ctx.flag("binpath", []const u8); const docker = ctx.flag("docker", bool); const docker_compose = ctx.flag("docker-compose", bool); const build_args = ctx.flag("build-args", []const u8); - var app_meta = util.findprogram(ctx.allocator, binpath) catch |err| { + var app_meta = util.findprogram(io, ctx.allocator, binpath) catch |err| { if (err == error.FileNotFound or err == error.ProgramNotFound) { try ctx.writer.print("Run \x1b[34mzig build\x1b[0m to build the ZX executable first!\n", .{}); return; @@ -81,17 +83,17 @@ fn bundle(ctx: zli.CommandContext) !void { log.debug("Copying bin from {s} to outdir {s}", .{ final_binpath, dest_binpath }); if (!(docker or docker_compose)) { - std.fs.cwd().makePath(outdir) catch |err| switch (err) { + std.Io.Dir.cwd().createDirPath(io, outdir) catch |err| switch (err) { error.PathAlreadyExists => {}, else => return err, }; - try std.fs.cwd().copyFile(final_binpath, std.fs.cwd(), dest_binpath, .{}); + try std.Io.Dir.copyFile(std.Io.Dir.cwd(), final_binpath, std.Io.Dir.cwd(), dest_binpath, io, .{}); printer.filepath(bin_name); const static_outdir = try std.fs.path.join(ctx.allocator, &.{ outdir, "static" }); defer ctx.allocator.free(static_outdir); log.debug("Copying static directory! {s}", .{appoutdir}); - util.copydirs(ctx.allocator, appoutdir, &.{"."}, static_outdir, false, &printer) catch |err| { + util.copydirs(io, ctx.allocator, appoutdir, &.{"."}, static_outdir, false, &printer) catch |err| { std.log.err("Failed to copy static directories: {any}", .{err}); }; @@ -100,14 +102,14 @@ fn bundle(ctx: zli.CommandContext) !void { const old_assets = try std.fs.path.join(ctx.allocator, &.{ outdir, "assets" }); defer ctx.allocator.free(old_public); defer ctx.allocator.free(old_assets); - std.fs.cwd().deleteTree(old_public) catch {}; - std.fs.cwd().deleteTree(old_assets) catch {}; + std.Io.Dir.cwd().deleteTree(io, old_public) catch {}; + std.Io.Dir.cwd().deleteTree(io, old_assets) catch {}; } // Delete {outdir}/.well-known/_zx if it exists const assets_zx_path = try std.fs.path.join(ctx.allocator, &.{ outdir, ".well-known", "_zx" }); defer ctx.allocator.free(assets_zx_path); - std.fs.cwd().deleteTree(assets_zx_path) catch |err| switch (err) { + std.Io.Dir.cwd().deleteTree(io, assets_zx_path) catch |err| switch (err) { else => {}, }; @@ -138,13 +140,13 @@ fn bundle(ctx: zli.CommandContext) !void { defer ctx.allocator.free(compose_path); defer ctx.allocator.free(dockerignore_path); - try std.fs.cwd().writeFile(.{ .sub_path = dockerfile_path, .data = dockerfile_content_with_build_args }); + try std.Io.Dir.cwd().writeFile(io, .{ .sub_path = dockerfile_path, .data = dockerfile_content_with_build_args }); printer.filepath(std.fs.path.basename(dockerfile_path)); if (docker_compose) { - try std.fs.cwd().writeFile(.{ .sub_path = compose_path, .data = compose_content_with_port }); + try std.Io.Dir.cwd().writeFile(io, .{ .sub_path = compose_path, .data = compose_content_with_port }); printer.filepath(std.fs.path.basename(compose_path)); } - try std.fs.cwd().writeFile(.{ .sub_path = dockerignore_path, .data = @embedFile("init/template/.dockerignore") }); + try std.Io.Dir.cwd().writeFile(io, .{ .sub_path = dockerignore_path, .data = @embedFile("init/template/.dockerignore") }); printer.filepath(std.fs.path.basename(dockerignore_path)); } @@ -163,6 +165,7 @@ const std = @import("std"); const zli = @import("zli"); const util = @import("shared/util.zig"); const flag = @import("shared/flag.zig"); +const AppContext = @import("shared/context.zig").AppContext; const zx = @import("zx"); const tui = @import("../tui/main.zig"); const log = std.log.scoped(.cli); diff --git a/src/cli/dev.zig b/src/cli/dev.zig index e4c0972b..bf363da1 100644 --- a/src/cli/dev.zig +++ b/src/cli/dev.zig @@ -2,10 +2,9 @@ const std = @import("std"); const zli = @import("zli"); const zx = @import("zx"); const cli_options = @import("cli_options"); -const builtin = @import("builtin"); - const util = @import("shared/util.zig"); const flag = @import("shared/flag.zig"); +const AppContext = @import("shared/context.zig").AppContext; const Builder = @import("dev/Builder.zig"); const tui = @import("../tui/main.zig"); const Diagnostics = @import("dev/Diagnostics.zig"); @@ -73,6 +72,7 @@ fn dev(ctx: zli.CommandContext) !void { var build_args_array = std.ArrayList([]const u8).empty; var initial_build_args_array = std.ArrayList([]const u8).empty; + defer build_args_array.deinit(allocator); defer initial_build_args_array.deinit(allocator); try build_args_array.appendSlice(allocator, &.{ cli_options.zig_exe, "build", "--watch", "--verbose", "--summary", "all", "--color", "off" }); @@ -85,18 +85,18 @@ fn dev(ctx: zli.CommandContext) !void { try initial_build_args_array.appendSlice(allocator, &.{trimmed_arg}); } - // Force color output even when piped (for error display) - var env_map = try std.process.getEnvMap(allocator); - defer env_map.deinit(); + const app = AppContext.from(&ctx); + const io = app.io; + const env_map = app.environ_map; - var initial_build = std.process.Child.init(initial_build_args_array.items, allocator); - const initial_term = initial_build.spawnAndWait() catch |err| { + var initial_build = try std.process.spawn(io, .{ .argv = initial_build_args_array.items }); + const initial_term = initial_build.wait(io) catch |err| { log.err("Failed to run initial build: {any}", .{err}); std.process.exit(1); }; switch (initial_term) { - .Exited => |code| { + .exited => |code| { if (code != 0) { if (env_map.get("CI") != null) { std.process.exit(code); @@ -124,8 +124,10 @@ fn dev(ctx: zli.CommandContext) !void { log.debug("starting devserver, inner: {d}: outer: {d}", .{ inner_port, outer_port }); var dev_server = DevServer.init(.{ .gpa = allocator, - .address = try std.net.Address.parseIp("0.0.0.0", outer_port), + .env_map = env_map, + .address = try std.Io.net.IpAddress.parse("0.0.0.0", outer_port), .inner_port = inner_port, + .io = io, }); defer dev_server.deinit(); dev_server.start() catch |err| { @@ -133,14 +135,14 @@ fn dev(ctx: zli.CommandContext) !void { return; }; - var builder = std.process.Child.init(build_args_array.items, allocator); - builder.stderr_behavior = .Pipe; - builder.stdout_behavior = .Pipe; - - try builder.spawn(); - defer _ = builder.kill() catch unreachable; + var builder = try std.process.spawn(io, .{ + .argv = build_args_array.items, + .stderr = .pipe, + .stdout = .pipe, + }); + defer builder.kill(io); - var build_state = Builder.BuildState.init(allocator, null, 0); + var build_state = Builder.BuildState.init(allocator, io, null, std.Io.Timestamp.fromNanoseconds(0)); defer build_state.deinit(); var runner: ?std.process.Child = null; @@ -149,15 +151,14 @@ fn dev(ctx: zli.CommandContext) !void { defer { if (runner) |*r| { - _ = r.kill() catch {}; - _ = r.wait() catch {}; + r.kill(io); } if (runner_output) |*o| o.deinit(); if (program_path) |p| allocator.free(p); } // Tracks wall-clock time from "change detected" to runner restart complete. - var rebuild_timer: ?std.time.Timer = null; + var rebuild_timer: ?std.Io.Timestamp = null; var rebuilding_shown = false; var is_first_run = true; var last_was_no_change = false; @@ -166,7 +167,7 @@ fn dev(ctx: zli.CommandContext) !void { var stderr_file = builder.stderr.?; var raw_buf: [8192]u8 = undefined; - var streaming_reader = stderr_file.readerStreaming(&raw_buf); + var streaming_reader = stderr_file.readerStreaming(io, &raw_buf); const io_reader = &streaming_reader.interface; var line_writer = std.Io.Writer.Allocating.init(allocator); defer line_writer.deinit(); @@ -183,7 +184,7 @@ fn dev(ctx: zli.CommandContext) !void { allocator.free(prev); last_error_formatted = null; } - rebuild_timer = std.time.Timer.start() catch null; + rebuild_timer = std.Io.Timestamp.now(io, .awake); dev_server.notify(.{ .type = .building }); if (use_spinner) { if (rebuilding_shown) { @@ -247,7 +248,7 @@ fn dev(ctx: zli.CommandContext) !void { try ctx.writer.print("\n{s}✓ {s}All build errors have been resolved!{s}\n", .{ Colors.green, Colors.bold, Colors.reset }); dev_server.notify(.{ .type = .clear }); }, - .build_complete_no_change => |_| { + .build_complete_no_change => { if (rebuilding_shown) { const dim = "\x1b[2m"; if (use_spinner) { @@ -292,14 +293,13 @@ fn dev(ctx: zli.CommandContext) !void { last_was_no_change = false; log.debug("Processing startup/restart request...", .{}); - const wall_build_ms: u64 = if (rebuild_timer) |*t| t.read() / std.time.ns_per_ms else build_duration_ms; + const wall_build_ms: u64 = if (rebuild_timer) |t| @intCast(t.durationTo(std.Io.Timestamp.now(io, .awake)).toMilliseconds()) else build_duration_ms; rebuild_timer = null; - var timer = std.time.Timer.start() catch unreachable; + var start_time = std.Io.Timestamp.now(io, .awake); if (runner) |*r| { - _ = r.kill() catch {}; - _ = r.wait() catch {}; + r.kill(io); } if (runner_output) |*o| { o.wait(); @@ -308,20 +308,17 @@ fn dev(ctx: zli.CommandContext) !void { } if (program_path == null) { - var program_meta = util.findprogram(allocator, binpath) catch |err| { + program_path = resolveProgramPath(io, allocator, binpath) catch |err| { log.debug("Error finding ZX executable: {any}", .{err}); continue; }; - program_path = program_meta.binpath; // Owned by program_meta, we keep it - program_meta.binpath = null; - program_meta.deinit(allocator); - const current_stat = try std.fs.cwd().statFile(program_path.?); + const current_stat = try std.Io.Dir.cwd().statFile(io, program_path.?, .{}); build_state.binary_path = try allocator.dupe(u8, program_path.?); build_state.last_binary_mtime = current_stat.mtime; } - const runnable_path = try util.getRunnablePath(allocator, program_path.?); + const runnable_path = try util.getRunnablePath(io, allocator, program_path.?); if (clear_on_restart) { try ctx.writer.print("\x1b[2J\x1b[H", .{}); @@ -343,21 +340,21 @@ fn dev(ctx: zli.CommandContext) !void { defer runner_args.deinit(allocator); try runner_args.appendSlice(allocator, &.{ runnable_path, "--cli-command", "dev" }); - runner = std.process.Child.init(runner_args.items, allocator); - runner.?.env_map = &env_map; - runner.?.stderr_behavior = .Pipe; - runner.?.stdout_behavior = .Pipe; - - try runner.?.spawn(); + runner = try std.process.spawn(io, .{ + .argv = runner_args.items, + .environ_map = env_map, + .stderr = .pipe, + .stdout = .pipe, + }); - runner_output = try util.captureChildOutput(ctx.allocator, &runner.?, .{ + runner_output = try util.captureChildOutput(io, ctx.allocator, &runner.?, .{ .stderr = .{ .mode = .first_line_then_transparent, .target = .stderr }, .stdout = .{ .mode = .transparent, .target = .stdout }, }); - runner_output.?.waitForFirstLine(); + _ = runner_output.?.waitForFirstLine(250); - const restart_time_ms = timer.lap() / std.time.ns_per_ms; + const restart_time_ms: u64 = @intCast(start_time.durationTo(std.Io.Timestamp.now(io, .awake)).toMilliseconds()); if (rebuilding_shown) { const total_ms = wall_build_ms + restart_time_ms; @@ -377,7 +374,7 @@ fn dev(ctx: zli.CommandContext) !void { is_first_run = false; dev_server.notify(.{ .type = .reload }); - const current_stat = std.fs.cwd().statFile(program_path.?) catch |err| { + const current_stat = std.Io.Dir.cwd().statFile(io, program_path.?, .{}) catch |err| { log.debug("Failed to stat binary after restart: {any}", .{err}); continue; }; @@ -392,6 +389,40 @@ fn dev(ctx: zli.CommandContext) !void { } } +fn resolveProgramPath(io: std.Io, allocator: std.mem.Allocator, binpath: []const u8) ![]const u8 { + if (binpath.len != 0) { + return allocator.dupe(u8, binpath); + } + + var files = try std.Io.Dir.cwd().openDir(io, BIN_DIR, .{ .iterate = true }); + defer files.close(io); + + var found_path: ?[]const u8 = null; + errdefer if (found_path) |path| allocator.free(path); + + var it = files.iterate(); + while (try it.next(io)) |entry| { + if (entry.kind != .file or !isLikelyRunnableFile(entry.name)) continue; + + if (found_path != null) return error.MultipleProgramsFound; + found_path = try std.fs.path.join(allocator, &.{ BIN_DIR, entry.name }); + } + + return found_path orelse error.ProgramNotFound; +} + +fn isLikelyRunnableFile(name: []const u8) bool { + const ignored_extensions = [_][]const u8{ + ".a", ".dll", ".dylib", ".lib", ".o", ".obj", ".pdb", ".so", ".wasm", + }; + + inline for (ignored_extensions) |ext| { + if (std.mem.endsWith(u8, name, ext)) return false; + } + + return true; +} + /// Print the first captured line (prefer stderr, fallback to stdout) fn printFirstLine(output: *util.ChildOutput, is_first_run: bool) void { if (output.getLastStderrLine()) |first_line| { diff --git a/src/cli/dev/Builder.zig b/src/cli/dev/Builder.zig index b88120d0..e0e7e942 100644 --- a/src/cli/dev/Builder.zig +++ b/src/cli/dev/Builder.zig @@ -68,9 +68,10 @@ pub const Event = union(enum) { pub const BuildState = struct { allocator: std.mem.Allocator, + io: std.Io, os_tag: std.Target.Os.Tag, binary_path: ?[]const u8, - last_binary_mtime: i128, + last_binary_mtime: std.Io.Timestamp, last_restart_time_ns: i128, first_build_done: bool, previous_had_errors: bool, @@ -85,11 +86,13 @@ pub const BuildState = struct { pub fn init( allocator: std.mem.Allocator, + io: std.Io, binary_path: ?[]const u8, - initial_mtime: i128, + initial_mtime: std.Io.Timestamp, ) BuildState { return .{ .allocator = allocator, + .io = io, .os_tag = builtin.os.tag, .binary_path = binary_path, .last_binary_mtime = initial_mtime, @@ -117,8 +120,9 @@ pub const BuildState = struct { self.installed_asset_paths.deinit(self.allocator); } - pub fn markRestartComplete(self: *BuildState, new_mtime: i128) void { - self.last_restart_time_ns = std.time.nanoTimestamp(); + pub fn markRestartComplete(self: *BuildState, new_mtime: std.Io.Timestamp) void { + const io = std.Io.Threaded.global_single_threaded.io(); + self.last_restart_time_ns = std.Io.Clock.now(.real, io).nanoseconds; self.last_binary_mtime = new_mtime; } @@ -218,22 +222,22 @@ pub const BuildState = struct { } fn handleBuildSummary(self: *BuildState, succeeded: bool) ?Event { - const now = std.time.nanoTimestamp(); + const now = std.Io.Timestamp.now(self.io, .awake).toNanoseconds(); log.debug("Build Summary, succeeded={}", .{succeeded}); const binary_changed = if (self.binary_path) |path| blk: { - const stat = std.fs.cwd().statFile(path) catch |err| { + const stat = std.Io.Dir.cwd().statFile(self.io, path, .{}) catch |err| { log.debug("stat failed: {s}", .{@errorName(err)}); self.build_in_progress = false; return null; }; - break :blk stat.mtime != self.last_binary_mtime; + break :blk stat.mtime.nanoseconds != self.last_binary_mtime.nanoseconds; } else false; if (!self.first_build_done) { self.first_build_done = true; if (self.binary_path) |path| { - const stat = std.fs.cwd().statFile(path) catch null; + const stat = std.Io.Dir.cwd().statFile(self.io, path, .{}) catch null; if (stat) |s| self.last_binary_mtime = s.mtime; } self.last_restart_time_ns = now; @@ -252,7 +256,7 @@ pub const BuildState = struct { const elapsed = now - self.last_restart_time_ns; if (elapsed >= RESTART_COOLDOWN_NS) { if (self.binary_path) |path| { - const stat = std.fs.cwd().statFile(path) catch null; + const stat = std.Io.Dir.cwd().statFile(self.io, path, .{}) catch null; if (stat) |s| self.last_binary_mtime = s.mtime; } log.debug("Binary changed, restart triggered", .{}); @@ -579,7 +583,7 @@ test "parseDurationMs handles common units" { test "error build cycle emits errors" { const allocator = std.testing.allocator; - var state = BuildState.init(allocator, "nonexistent", 0); + var state = BuildState.init(allocator, std.testing.io, "nonexistent", std.Io.Timestamp.fromNanoseconds(0)); state.first_build_done = true; defer state.deinit(); @@ -615,7 +619,7 @@ test "error build cycle emits errors" { test "error then fix then error again: errors detected each time" { const allocator = std.testing.allocator; - var state = BuildState.init(allocator, "nonexistent", 0); + var state = BuildState.init(allocator, std.testing.io, "nonexistent", std.Io.Timestamp.fromNanoseconds(0)); state.first_build_done = true; defer state.deinit(); @@ -678,17 +682,18 @@ test "error then fix then error again: errors detected each time" { test "rebuild error detection with real watch output" { const allocator = std.testing.allocator; + const io = std.testing.io; const tmp_path = "zig-out/.builder-test-bin"; { - var f = try std.fs.cwd().createFile(tmp_path, .{}); - f.close(); + var f = try std.Io.Dir.cwd().createFile(io, tmp_path, .{}); + f.close(io); } - defer std.fs.cwd().deleteFile(tmp_path) catch {}; + defer std.Io.Dir.cwd().deleteFile(io, tmp_path) catch {}; - const initial_stat = try std.fs.cwd().statFile(tmp_path); + const initial_stat = try std.Io.Dir.cwd().statFile(io, tmp_path, .{}); - var state = BuildState.init(allocator, tmp_path, initial_stat.mtime); + var state = BuildState.init(allocator, io, tmp_path, initial_stat.mtime); defer state.deinit(); var events = std.ArrayList(Event).empty; @@ -734,17 +739,18 @@ test "rebuild error detection with real watch output" { test "full lifecycle: initial error build, fix, then error again" { const allocator = std.testing.allocator; + const io = std.testing.io; const tmp_path = "zig-out/.builder-test-bin2"; { - var f = try std.fs.cwd().createFile(tmp_path, .{}); - f.close(); + var f = try std.Io.Dir.cwd().createFile(io, tmp_path, .{}); + f.close(io); } - defer std.fs.cwd().deleteFile(tmp_path) catch {}; + defer std.Io.Dir.cwd().deleteFile(io, tmp_path) catch {}; - const initial_stat = try std.fs.cwd().statFile(tmp_path); + const initial_stat = try std.Io.Dir.cwd().statFile(io, tmp_path, .{}); - var state = BuildState.init(allocator, tmp_path, initial_stat.mtime); + var state = BuildState.init(allocator, io, tmp_path, initial_stat.mtime); defer state.deinit(); var events = std.ArrayList(Event).empty; @@ -799,7 +805,7 @@ test "full lifecycle: initial error build, fix, then error again" { test "windows watch output detects build start and restart" { const allocator = std.testing.allocator; - var state = BuildState.init(allocator, null, 0); + var state = BuildState.init(allocator, std.testing.io, null, std.Io.Timestamp.fromNanoseconds(0)); state.os_tag = .windows; state.first_build_done = true; defer state.deinit(); @@ -839,7 +845,7 @@ pub fn main() !void { const allocator = gpa.allocator(); // Open log file for raw stderr capture - var log_file = try std.fs.cwd().createFile("zig-out/build-stderr.log", .{}); + var log_file = try std.Io.Dir.cwd().createFile("zig-out/build-stderr.log", .{}); defer log_file.close(); var child = std.process.Child.init( diff --git a/src/cli/dev/DevServer.zig b/src/cli/dev/DevServer.zig index 20dee2ed..cc9ef1bf 100644 --- a/src/cli/dev/DevServer.zig +++ b/src/cli/dev/DevServer.zig @@ -63,26 +63,30 @@ const QueuedEvent = struct { const EVENT_QUEUE_CAP = 16; +io: std.Io, gpa: Allocator, -address: std.net.Address, +env_map: *const std.process.Environ.Map, +address: std.Io.net.IpAddress, inner_port: u16, -tcp_server: ?std.net.Server, +tcp_server: ?std.Io.net.Server, serve_thread: ?std.Thread, /// Incremented on each event. WebSocket threads block on this with Futex. update_id: std.atomic.Value(u32), /// Bounded event queue so rapid transitions (building → reload) don't drop events. -event_mutex: std.Thread.Mutex, +event_mutex: std.Io.Mutex, event_queue: [EVENT_QUEUE_CAP]QueuedEvent = undefined, event_head: u32 = 0, // next write position event_tail: u32 = 0, // next read position sticky_state_json: ?[]u8 = null, pub const Options = struct { + io: std.Io, gpa: Allocator, + env_map: *const std.process.Environ.Map, /// Address to bind the user-facing proxy to. - address: std.net.Address, + address: std.Io.net.IpAddress, /// Port the app binary will listen on. inner_port: u16, }; @@ -91,21 +95,23 @@ pub fn init(opts: Options) DevServer { log.debug("devserver init port: {d}", .{opts.address.getPort()}); return .{ .gpa = opts.gpa, + .env_map = opts.env_map, .address = opts.address, .inner_port = opts.inner_port, .tcp_server = null, .serve_thread = null, .update_id = .init(0), - .event_mutex = .{}, + .event_mutex = .init, + .io = opts.io, }; } pub fn deinit(ds: *DevServer) void { if (ds.serve_thread) |t| { - if (ds.tcp_server) |*s| s.stream.close(); + if (ds.tcp_server) |*s| s.socket.close(ds.io); t.join(); } - if (ds.tcp_server) |*s| s.deinit(); + if (ds.tcp_server) |*s| s.deinit(ds.io); // Drain any remaining queued events while (ds.event_tail != ds.event_head) { const idx = ds.event_tail % EVENT_QUEUE_CAP; @@ -123,13 +129,13 @@ pub fn start(ds: *DevServer) error{AlreadyReported}!void { log.debug("devserver start", .{}); - ds.tcp_server = ds.address.listen(.{ .reuse_address = true }) catch |err| { + ds.tcp_server = ds.address.listen(ds.io, .{ .reuse_address = true }) catch |err| { log.err("failed to listen on {f}: {s}", .{ ds.address, @errorName(err) }); return error.AlreadyReported; }; ds.serve_thread = std.Thread.spawn(.{}, serve, .{ds}) catch |err| { log.err("unable to spawn dev server thread: {s}", .{@errorName(err)}); - ds.tcp_server.?.deinit(); + ds.tcp_server.?.deinit(ds.io); ds.tcp_server = null; return error.AlreadyReported; }; @@ -138,7 +144,8 @@ pub fn start(ds: *DevServer) error{AlreadyReported}!void { /// Push a serialized notification onto the queue and wake WS threads. /// Thread-safe. fn pushEvent(ds: *DevServer, json: []u8) void { - ds.event_mutex.lock(); + const io = std.Io.Threaded.global_single_threaded.io(); + ds.event_mutex.lockUncancelable(io); const idx = ds.event_head % EVENT_QUEUE_CAP; // If queue is full, drop oldest event if (ds.event_head -% ds.event_tail >= EVENT_QUEUE_CAP) { @@ -148,9 +155,9 @@ fn pushEvent(ds: *DevServer, json: []u8) void { } ds.event_queue[idx] = .{ .json = json }; ds.event_head +%= 1; - ds.event_mutex.unlock(); + ds.event_mutex.unlock(io); _ = ds.update_id.rmw(.Add, 1, .release); - std.Thread.Futex.wake(&ds.update_id, std.math.maxInt(u32)); + io.futexWake(u32, &ds.update_id.raw, std.math.maxInt(u32)); } pub fn notify(ds: *DevServer, notification: Notification) void { @@ -160,8 +167,9 @@ pub fn notify(ds: *DevServer, notification: Notification) void { } fn updateStickyState(ds: *DevServer, notification: Notification, json: []const u8) void { - ds.event_mutex.lock(); - defer ds.event_mutex.unlock(); + const io = std.Io.Threaded.global_single_threaded.io(); + ds.event_mutex.lockUncancelable(io); + defer ds.event_mutex.unlock(io); switch (notification.type) { .building, .@"error" => { @@ -180,16 +188,17 @@ fn updateStickyState(ds: *DevServer, notification: Notification, json: []const u /// Find a free OS-assigned port by briefly binding to port 0. pub fn findFreePort() !u16 { - var server = try (try std.net.Address.parseIp("127.0.0.1", 0)).listen(.{}); - defer server.deinit(); - return server.listen_address.getPort(); + const io = std.Io.Threaded.global_single_threaded.io(); + var server = try (try std.Io.net.IpAddress.parse("127.0.0.1", 0)).listen(io, .{}); + defer server.deinit(io); + return server.socket.address.getPort(); } fn serve(ds: *DevServer) void { var retry_count: u8 = 0; while (true) { - const connection = ds.tcp_server.?.accept() catch |err| { + const connection = ds.tcp_server.?.accept(ds.io) catch |err| { switch (err) { error.Unexpected => { retry_count += 1; @@ -198,7 +207,7 @@ fn serve(ds: *DevServer) void { return; } log.warn("accept() failed (transient): {s}", .{@errorName(err)}); - std.Thread.sleep(50 * std.time.ns_per_ms); + std.Io.sleep(ds.io, .fromMilliseconds(50), .awake) catch {}; continue; }, else => { @@ -210,26 +219,24 @@ fn serve(ds: *DevServer) void { retry_count = 0; const thread = std.Thread.spawn(.{}, handleConnection, .{ ds, connection }) catch |err| { log.err("unable to spawn connection thread: {s}", .{@errorName(err)}); - connection.stream.close(); + connection.close(ds.io); continue; }; thread.detach(); } } -fn handleConnection(ds: *DevServer, conn: std.net.Server.Connection) void { - defer conn.stream.close(); +fn handleConnection(ds: *DevServer, stream: std.Io.net.Stream) void { + defer stream.close(ds.io); - // Get a formatted IP string to avoid ambiguity in std.log - var addr_buf: [64]u8 = undefined; - const addr_str = std.fmt.bufPrint(&addr_buf, "{any}", .{conn.address}) catch "unknown"; - log.debug("connection accepted from {s}", .{addr_str}); + // Connection accepted + log.debug("connection accepted", .{}); var send_buffer: [4096]u8 = undefined; var recv_buffer: [4096]u8 = undefined; - var connection_reader = conn.stream.reader(&recv_buffer); - var connection_writer = conn.stream.writer(&send_buffer); - var server: http.Server = .init(connection_reader.interface(), &connection_writer.interface); + var connection_reader = stream.reader(ds.io, &recv_buffer); + var connection_writer = stream.writer(ds.io, &send_buffer); + var server: http.Server = .init(&connection_reader.interface, &connection_writer.interface); while (true) { var request = server.receiveHead() catch |err| switch (err) { @@ -251,7 +258,7 @@ fn handleConnection(ds: *DevServer, conn: std.net.Server.Connection) void { }, .other => |name| return log.err("unknown upgrade request: {s}", .{name}), .none => { - ds.serveRequest(&request, conn.stream) catch |err| switch (err) { + ds.serveRequest(&request, stream) catch |err| switch (err) { error.AlreadyReported => return, else => { log.err("failed to serve '{s}': {s}", .{ request.head.target, @errorName(err) }); @@ -263,7 +270,7 @@ fn handleConnection(ds: *DevServer, conn: std.net.Server.Connection) void { } } -fn serveRequest(ds: *DevServer, req: *http.Server.Request, client_stream: std.net.Stream) !void { +fn serveRequest(ds: *DevServer, req: *http.Server.Request, client_stream: std.Io.net.Stream) !void { const target = req.head.target; var target_split = std.mem.splitScalar(u8, target, '?'); const target_path = target_split.first(); @@ -342,14 +349,15 @@ fn serveWebSocket(ds: *DevServer, sock: *http.Server.WebSocket) !noreturn { defer recv_thread.join(); log.debug("ws: recv thread spawned, entering event loop", .{}); + const io = std.Io.Threaded.global_single_threaded.io(); var sticky_snapshot: ?[]u8 = null; var last_id: u32 = 0; - ds.event_mutex.lock(); + ds.event_mutex.lockUncancelable(io); last_id = ds.event_head; if (ds.sticky_state_json) |json| { sticky_snapshot = ds.gpa.dupe(u8, json) catch null; } - ds.event_mutex.unlock(); + ds.event_mutex.unlock(io); if (sticky_snapshot) |json| { defer ds.gpa.free(json); @@ -360,7 +368,7 @@ fn serveWebSocket(ds: *DevServer, sock: *http.Server.WebSocket) !noreturn { const cur = ds.update_id.load(.acquire); if (cur == last_id) { // No pending event - wait up to 30 s then ping to keep the connection alive. - std.Thread.Futex.timedWait(&ds.update_id, last_id, 30 * std.time.ns_per_s) catch { + io.futexWaitTimeout(u32, &ds.update_id.raw, last_id, .{ .duration = .{ .raw = .{ .nanoseconds = 30 * std.time.ns_per_s }, .clock = .awake } }) catch { try sock.writeMessage("", .ping); continue; }; @@ -368,10 +376,10 @@ fn serveWebSocket(ds: *DevServer, sock: *http.Server.WebSocket) !noreturn { } // Read the current event under the lock, then release before I/O. - ds.event_mutex.lock(); + ds.event_mutex.lockUncancelable(io); const head = ds.event_head; if (head == last_id) { - ds.event_mutex.unlock(); + ds.event_mutex.unlock(io); continue; } if (head -% last_id > EVENT_QUEUE_CAP) { @@ -379,11 +387,11 @@ fn serveWebSocket(ds: *DevServer, sock: *http.Server.WebSocket) !noreturn { } const event_index = last_id % EVENT_QUEUE_CAP; const json_copy = ds.gpa.dupe(u8, ds.event_queue[event_index].json) catch { - ds.event_mutex.unlock(); + ds.event_mutex.unlock(io); last_id +%= 1; continue; }; - ds.event_mutex.unlock(); + ds.event_mutex.unlock(io); last_id +%= 1; @@ -418,17 +426,17 @@ fn serializeNotification(gpa: Allocator, notification: Notification) ![]u8 { /// `buffered_extra` - body bytes already consumed by the http.Server reader. fn proxyToInner( ds: *DevServer, - client: std.net.Stream, + client: std.Io.net.Stream, head_buffer: []const u8, buffered_extra: []const u8, ) !void { - const inner_addr = try std.net.Address.parseIp("127.0.0.1", ds.inner_port); + const inner_addr = try std.Io.net.IpAddress.parse("127.0.0.1", ds.inner_port); // Retry while the inner server is (re)starting - up to 2 s. - const inner: std.net.Stream = for (0..200) |_| { - if (std.net.tcpConnectToAddress(inner_addr)) |s| break s else |_| std.Thread.sleep(10 * std.time.ns_per_ms); + const inner: std.Io.net.Stream = for (0..200) |_| { + if (inner_addr.connect(ds.io, .{ .mode = .stream })) |s| break s else |_| std.Io.sleep(ds.io, .fromMicroseconds(10), .real) catch {}; } else return error.ConnectionRefused; - defer inner.close(); + defer inner.close(ds.io); // We MUST force the inner server to close the connection, otherwise the browser // will try to reuse this connection (which we are currently piping raw) // for subsequent requests that might need to be intercepted by the DevServer. @@ -456,46 +464,49 @@ fn proxyToInner( } try transformed.appendSlice(ds.gpa, "\r\n\r\n"); - try inner.writeAll(transformed.items); + var inner_writer_buf: [4096]u8 = undefined; + var inner_writer = inner.writer(ds.io, &inner_writer_buf); + try inner_writer.interface.writeAll(transformed.items); // Forward any body bytes already buffered by the http.Server reader. - if (buffered_extra.len > 0) try inner.writeAll(buffered_extra); + if (buffered_extra.len > 0) try inner_writer.interface.writeAll(buffered_extra); + try inner_writer.interface.flush(); // Windows can report ERROR_INVALID_PARAMETER from ReadFile when combining // this shutdown-based bidirectional copy pattern with sockets. Use a // simpler one-way response copy there. if (builtin.os.tag == .windows) { - copyStream(inner, client); + copyStream(ds.io, inner, client); return; } // Bidirectional pipe: remaining request body client→inner, response inner→client. // The inner→client thread shuts down the client write side when inner closes, // which unblocks the client→inner copy loop below. - const fwd = std.Thread.spawn(.{}, copyStreamThenShutdown, .{ inner, client }) catch return; + const fwd = std.Thread.spawn(.{}, copyStreamThenShutdown, .{ ds.io, inner, client }) catch return; defer fwd.join(); - copyStream(client, inner); + copyStream(ds.io, client, inner); // Unblock the inner→client thread if client closed first. - std.posix.shutdown(inner.handle, .recv) catch {}; + inner.shutdown(ds.io, .recv) catch {}; } /// Copy src→dst, then shut down the dst send side so the peer's read unblocks. -fn copyStreamThenShutdown(src: std.net.Stream, dst: std.net.Stream) void { - copyStream(src, dst); - std.posix.shutdown(dst.handle, .send) catch {}; +fn copyStreamThenShutdown(io: std.Io, src: std.Io.net.Stream, dst: std.Io.net.Stream) void { + copyStream(io, src, dst); + dst.shutdown(io, .send) catch {}; } -fn copyStream(src: std.net.Stream, dst: std.net.Stream) void { +fn copyStream(io: std.Io, src: std.Io.net.Stream, dst: std.Io.net.Stream) void { var read_buf: [65536]u8 = undefined; var reader_state: [1024]u8 = undefined; var writer_state: [1024]u8 = undefined; - var reader = src.reader(&reader_state); - var writer = dst.writer(&writer_state); + var reader = src.reader(io, &reader_state); + var writer = dst.writer(io, &writer_state); while (true) { - const n = reader.interface().readSliceShort(&read_buf) catch return; + const n = reader.interface.readSliceShort(&read_buf) catch return; if (n == 0) return; writer.interface.writeAll(read_buf[0..n]) catch return; writer.interface.flush() catch return; @@ -532,7 +543,7 @@ fn handleOpenInEditor(ds: *DevServer, target: []const u8) !void { const file_arg = try std.fmt.allocPrint(ds.gpa, "{s}:{s}:{s}", .{ decoded_file, l, c }); defer ds.gpa.free(file_arg); - const args = try IdeScheme.detect(ds.gpa, decoded_file, l, c); + const args = try IdeScheme.detect(ds.gpa, ds.env_map, decoded_file, l, c); defer { for (args) |arg| ds.gpa.free(arg); ds.gpa.free(args); @@ -541,16 +552,16 @@ fn handleOpenInEditor(ds: *DevServer, target: []const u8) !void { if (args.len == 0) return; log.debug("opening in editor: {s}", .{args[0]}); - var child_proc = std.process.Child.init(args, ds.gpa); - child_proc.stdin_behavior = .Ignore; - child_proc.stdout_behavior = .Ignore; - child_proc.stderr_behavior = .Ignore; - - child_proc.spawn() catch |err| { + var child_proc = std.process.spawn(ds.io, .{ + .argv = args, + .stdin = .ignore, + .stdout = .ignore, + .stderr = .ignore, + }) catch |err| { log.debug("editor failed to spawn: {s}", .{@errorName(err)}); return; }; - _ = child_proc.wait() catch {}; + _ = child_proc.wait(ds.io) catch {}; } } diff --git a/src/cli/dev/Diagnostics.zig b/src/cli/dev/Diagnostics.zig index 62de3e33..8bd2199d 100644 --- a/src/cli/dev/Diagnostics.zig +++ b/src/cli/dev/Diagnostics.zig @@ -68,7 +68,8 @@ fn normalizePath(allocator: std.mem.Allocator, d: *Builder.Diagnostic) !void { fn remapSingle(allocator: std.mem.Allocator, d: *Builder.Diagnostic) !void { // Read the generated file and look for inlined sourcemap - const file_content = std.fs.cwd().readFileAlloc(allocator, d.file, 10 * 1024 * 1024) catch return; + const io = std.Io.Threaded.global_single_threaded.io(); + const file_content = std.Io.Dir.cwd().readFileAlloc(io, d.file, allocator, .unlimited) catch return; defer allocator.free(file_content); // Find the sourcemap comment (last occurrence) @@ -152,7 +153,8 @@ fn extractJsonStringField(json: []const u8, key: []const u8) ?[]const u8 { /// Read a few lines of source context around a given line number. pub fn readSourceContext(allocator: std.mem.Allocator, file_path: []const u8, target_line: u32, context_lines: u32) ?[]const u8 { - const source = std.fs.cwd().readFileAlloc(allocator, file_path, 10 * 1024 * 1024) catch return null; + const io = std.Io.Threaded.global_single_threaded.io(); + const source = std.Io.Dir.cwd().readFileAlloc(io, file_path, allocator, .limited(10 * 1024 * 1024)) catch return null; defer allocator.free(source); const start_line = if (target_line > context_lines) target_line - context_lines else 1; @@ -189,7 +191,8 @@ pub fn readHighlightedSourceContext( context_lines: u32, highlightFn: fn (std.mem.Allocator, []const u8) anyerror![]u8, ) !?[]u8 { - const source = std.fs.cwd().readFileAlloc(allocator, file_path, 10 * 1024 * 1024) catch return null; + const io = std.Io.Threaded.global_single_threaded.io(); + const source = std.Io.Dir.cwd().readFileAlloc(io, file_path, allocator, .limited(10 * 1024 * 1024)) catch return null; defer allocator.free(source); const start_line = if (target_line > context_lines) target_line - context_lines else 1; @@ -436,7 +439,9 @@ fn colorizeErrorLine(allocator: std.mem.Allocator, result: *std.ArrayList(u8), l /// Helper to get a single line from a file without loading the whole file every time pub fn getLineFromFile(allocator: std.mem.Allocator, file_path: []const u8, line_num: u32) !?[]const u8 { - const source = std.fs.cwd().readFileAlloc(allocator, file_path, 10 * 1024 * 1024) catch return null; + const io = std.Io.Threaded.global_single_threaded.io(); + + const source = std.Io.Dir.cwd().readFileAlloc(io, file_path, allocator, .unlimited) catch return null; defer allocator.free(source); var it = std.mem.splitScalar(u8, source, '\n'); @@ -449,10 +454,10 @@ pub fn getLineFromFile(allocator: std.mem.Allocator, file_path: []const u8, line } pub fn formatOxlint(allocator: std.mem.Allocator, diagnostics: []const Builder.Diagnostic) ![]u8 { - var buf = std.ArrayList(u8).empty; - defer buf.deinit(allocator); + var buf = std.Io.Writer.Allocating.init(allocator); + defer buf.deinit(); - const w = buf.writer(allocator); + var w = buf.writer; for (diagnostics) |d| { const kind_symbol = switch (d.kind) { @@ -528,5 +533,5 @@ pub fn formatOxlint(allocator: std.mem.Allocator, diagnostics: []const Builder.D try w.print(" {s}╰────{s}\n\n", .{ Colors.gray, Colors.reset }); } - return try allocator.dupe(u8, buf.items); + return try allocator.dupe(u8, buf.written()); } diff --git a/src/cli/dev/Highlight.zig b/src/cli/dev/Highlight.zig index 0c679a53..f47fc618 100644 --- a/src/cli/dev/Highlight.zig +++ b/src/cli/dev/Highlight.zig @@ -10,7 +10,7 @@ const HighlightCache = struct { parser: *ts.Parser, language: *const ts.Language, query: *ts.Query, - mutex: std.Thread.Mutex = .{}, + mutex: std.Io.Mutex = .init, var instance: ?*HighlightCache = null; @@ -43,9 +43,10 @@ const HighlightCache = struct { pub fn highlightZx(allocator: std.mem.Allocator, source: []const u8) ![]u8 { if (zx.platform.role == .client) return try allocator.dupe(u8, source); + const io = std.Io.Threaded.global_single_threaded.io(); const cache = try HighlightCache.getOrInit(std.heap.page_allocator); - cache.mutex.lock(); - defer cache.mutex.unlock(); + try cache.mutex.lock(io); + defer cache.mutex.unlock(io); const tree = cache.parser.parseString(source, null) orelse return error.ParseError; defer tree.destroy(); diff --git a/src/cli/dev/IdeScheme.zig b/src/cli/dev/IdeScheme.zig index ec1ca662..8bbb9e2b 100644 --- a/src/cli/dev/IdeScheme.zig +++ b/src/cli/dev/IdeScheme.zig @@ -7,7 +7,7 @@ args: []const []const u8, /// environment keys that must exist OR matches "KEY=VALUE" (value can have *) envs: []const []const u8, -pub fn match(self: CodeEditorScheme, env_map: std.process.EnvMap) bool { +pub fn match(self: CodeEditorScheme, env_map: *const std.process.Environ.Map) bool { if (self.envs.len == 0) return false; for (self.envs) |env_spec| { @@ -50,10 +50,7 @@ pub fn format(self: CodeEditorScheme, allocator: std.mem.Allocator, file: []cons } // Detect editor and return command args to open file -pub fn detect(allocator: std.mem.Allocator, file: []const u8, line: []const u8, col: []const u8) ![]const []const u8 { - var env_map = try std.process.getEnvMap(allocator); - defer env_map.deinit(); - +pub fn detect(allocator: std.mem.Allocator, env_map: *const std.process.Environ.Map, file: []const u8, line: []const u8, col: []const u8) ![]const []const u8 { // 1. ZIEX_EDITOR override (e.g., "zed --open {file}:{line}:{col}") if (env_map.get("ZIEX_EDITOR")) |editor_cmd| { var args_list = std.ArrayList([]const u8).empty; diff --git a/src/cli/export.zig b/src/cli/export.zig index 6efddf2e..0873e7e4 100644 --- a/src/cli/export.zig +++ b/src/cli/export.zig @@ -19,10 +19,12 @@ const outdir_flag = zli.Flag{ }; fn @"export"(ctx: zli.CommandContext) !void { + const app = AppContext.from(&ctx); + const io = app.io; const outdir = ctx.flag("outdir", []const u8); const binpath = ctx.flag("binpath", []const u8); - var app_meta = util.findprogram(ctx.allocator, binpath) catch |err| { + var app_meta = util.findprogram(io, ctx.allocator, binpath) catch |err| { if (err == error.FileNotFound) { try ctx.writer.print("Run \x1b[34mzig build\x1b[0m to build the ZX executable first!\n", .{}); return; @@ -38,20 +40,21 @@ fn @"export"(ctx: zli.CommandContext) !void { const appoutdir = app_meta.rootdir orelse "site/.zx"; const host = app_meta.config.server.address orelse "0.0.0.0"; - var app_child = std.process.Child.init(&.{ app_meta.binpath.?, "--cli-command", "export" }, ctx.allocator); - app_child.stdout_behavior = .Ignore; - app_child.stderr_behavior = .Ignore; - const env_map = try ctx.allocator.create(std.process.EnvMap); + const environ_map = app.environ_map; + try environ_map.put("ZIEX_INNER_PORT", port_str); + + var app_child = try std.process.spawn(io, .{ + .argv = &.{ app_meta.binpath.?, "--cli-command", "export" }, + .environ_map = environ_map, + .stdout = .ignore, + .stderr = .ignore, + }); defer { - env_map.deinit(); - ctx.allocator.destroy(env_map); + app_child.kill(io); + } + errdefer { + app_child.kill(io); } - env_map.* = try std.process.getEnvMap(ctx.allocator); - try env_map.put("ZIEX_INNER_PORT", port_str); - app_child.env_map = env_map; - try app_child.spawn(); - defer _ = app_child.kill() catch {}; - errdefer _ = app_child.kill() catch {}; var printer = tui.Printer.init(ctx.allocator, .{ .file_path_mode = .flat, .file_tree_max_depth = 1 }); defer printer.deinit(); @@ -59,7 +62,7 @@ fn @"export"(ctx: zli.CommandContext) !void { printer.header("{s} Building static ZX site!", .{tui.Printer.emoji("○")}); printer.info("{s}", .{outdir}); // delete the outdir if it exists - // std.fs.cwd().deleteTree(outdir) catch |err| switch (err) { + // std.Io.Dir.cwd().deleteTree(outdir) catch |err| switch (err) { // else => {}, // }; @@ -77,7 +80,7 @@ fn @"export"(ctx: zli.CommandContext) !void { log.debug("Processing route! {s}", .{route.path}); if (route.is_dynamic) { - const static_params = fetchStaticParams(ctx.allocator, host, port, route.path) catch |err| { + const static_params = fetchStaticParams(io, ctx.allocator, host, port, route.path) catch |err| { if (err == error.ConnectionRefused) { continue :process_block; } @@ -95,7 +98,7 @@ fn @"export"(ctx: zli.CommandContext) !void { .has_notfound = route.has_notfound, .is_dynamic = false, }; - processRoute(ctx.allocator, host, port, expanded_route, outdir, &printer, .page) catch |err| { + processRoute(io, ctx.allocator, host, port, expanded_route, outdir, &printer, .page) catch |err| { if (err == error.ConnectionRefused) { continue :process_block; } @@ -105,7 +108,7 @@ fn @"export"(ctx: zli.CommandContext) !void { log.debug("No static params for dynamic route: {s}", .{route.path}); } } else { - processRoute(ctx.allocator, host, port, route, outdir, &printer, .page) catch |err| { + processRoute(io, ctx.allocator, host, port, route, outdir, &printer, .page) catch |err| { if (err == error.ConnectionRefused) { continue :process_block; } @@ -114,7 +117,7 @@ fn @"export"(ctx: zli.CommandContext) !void { // Also export 404.html for routes that have notfound handler if (route.has_notfound) { - processRoute(ctx.allocator, host, port, route, outdir, &printer, .notfound) catch |err| { + processRoute(io, ctx.allocator, host, port, route, outdir, &printer, .notfound) catch |err| { if (err == error.ConnectionRefused) { continue :process_block; } @@ -126,7 +129,7 @@ fn @"export"(ctx: zli.CommandContext) !void { log.debug("Copying public directory! {s}", .{appoutdir}); - util.copydirs(ctx.allocator, appoutdir, &.{"."}, outdir, false, &printer) catch |err| { + util.copydirs(io, ctx.allocator, appoutdir, &.{"."}, outdir, false, &printer) catch |err| { std.log.err("Failed to copy public directory: {any}", .{err}); // return err; }; @@ -134,7 +137,7 @@ fn @"export"(ctx: zli.CommandContext) !void { // Delete {outdir}/.well-known/_zx if it exists const assets_zx_path = try std.fs.path.join(ctx.allocator, &.{ outdir, ".well-known", "_zx" }); defer ctx.allocator.free(assets_zx_path); - std.fs.cwd().deleteTree(assets_zx_path) catch |err| switch (err) { + std.Io.Dir.cwd().deleteTree(io, assets_zx_path) catch |err| switch (err) { else => {}, }; @@ -158,6 +161,7 @@ const StaticParamsResult = struct { }; fn processRoute( + io: std.Io, allocator: std.mem.Allocator, host: []const u8, port: u16, @@ -167,7 +171,7 @@ fn processRoute( export_type: ExportType, ) !void { // Fetch the route's HTML content - var client = std.http.Client{ .allocator = allocator }; + var client = std.http.Client{ .allocator = allocator, .io = io }; defer client.deinit(); var aw = std.Io.Writer.Allocating.init(allocator); @@ -260,10 +264,10 @@ fn processRoute( // Create parent directories if they don't exist const output_dir = std.fs.path.dirname(output_path); if (output_dir) |dir| { - try std.fs.cwd().makePath(dir); + try std.Io.Dir.cwd().createDirPath(io, dir); } - try std.fs.cwd().writeFile(.{ + try std.Io.Dir.cwd().writeFile(io, .{ .sub_path = output_path, .data = response_text, }); @@ -273,8 +277,11 @@ fn processRoute( /// Fetch static params from server via x-zx-static-data header /// Returns expanded paths (e.g., "/blog/hello", "/blog/world") -fn fetchStaticParams(allocator: std.mem.Allocator, host: []const u8, port: u16, route_path: []const u8) !StaticParamsResult { - var client = std.http.Client{ .allocator = allocator }; +fn fetchStaticParams(io: std.Io, allocator: std.mem.Allocator, host: []const u8, port: u16, route_path: []const u8) !StaticParamsResult { + var client = std.http.Client{ + .allocator = allocator, + .io = io, + }; defer client.deinit(); var aw = std.Io.Writer.Allocating.init(allocator); @@ -301,7 +308,7 @@ fn fetchStaticParams(allocator: std.mem.Allocator, host: []const u8, port: u16, const response_z = try allocator.dupeZ(u8, response); defer allocator.free(response_z); - const parsed = std.zon.parse.fromSlice([]const []const zx.PageOptions.StaticParam, allocator, response_z, null, .{}) catch |err| { + const parsed = std.zon.parse.fromSliceAlloc([]const []const zx.PageOptions.StaticParam, allocator, response_z, null, .{}) catch |err| { log.warn("Failed to parse static params ZON: {any}", .{err}); return .{ .items = &.{}, .allocator = null }; }; @@ -357,6 +364,7 @@ const std = @import("std"); const zli = @import("zli"); const util = @import("shared/util.zig"); const flag = @import("shared/flag.zig"); +const AppContext = @import("shared/context.zig").AppContext; const zx = @import("zx"); const DevServer = @import("dev/DevServer.zig"); const tui = @import("../tui/main.zig"); diff --git a/src/cli/fmt.zig b/src/cli/fmt.zig index b8b499ae..8d267cca 100644 --- a/src/cli/fmt.zig +++ b/src/cli/fmt.zig @@ -1,5 +1,6 @@ const std = @import("std"); const zli = @import("zli"); +const AppContext = @import("shared/context.zig").AppContext; const log = std.log.scoped(.cli); const zx = @import("zx"); const tui = @import("../tui/main.zig"); @@ -47,17 +48,20 @@ pub fn register(writer: *std.Io.Writer, reader: *std.Io.Reader, allocator: std.m } fn fmt(ctx: zli.CommandContext) !void { + const app = AppContext.from(&ctx); + const io = app.io; + const use_stdio = ctx.flag("stdio", bool); const use_stdout = ctx.flag("stdout", bool); const use_error = ctx.flag("error", bool); if (use_error) { - try formatErrorFromStdin(ctx.allocator, ctx.writer); + try formatErrorFromStdin(io, ctx.allocator, ctx.writer); return; } if (use_stdio) { - try formatFromStdin(ctx.allocator, ctx.writer); + try formatFromStdin(io, ctx.allocator, ctx.writer); return; } @@ -87,23 +91,23 @@ fn fmt(ctx: zli.CommandContext) !void { } for (paths) |path| { - var dir = std.fs.cwd().openDir(path, .{ .iterate = true }) catch |err| switch (err) { + var dir = std.Io.Dir.cwd().openDir(io, path, .{ .iterate = true }) catch |err| switch (err) { error.NotDir => { - try formatFile(ctx.allocator, ctx.writer, std.fs.cwd(), path, path, use_stdout); + try formatFile(ctx.allocator, io, ctx.writer, std.Io.Dir.cwd(), path, path, use_stdout); continue; }, else => continue, }; - defer dir.close(); - try formatDir(ctx.allocator, ctx.writer, path, use_stdout); + defer dir.close(io); + try formatDir(ctx.allocator, io, ctx.writer, path, use_stdout); } } -fn formatErrorFromStdin(allocator: std.mem.Allocator, writer: *std.Io.Writer) !void { - var stdin_file = std.fs.File.stdin(); +fn formatErrorFromStdin(io: std.Io, allocator: std.mem.Allocator, writer: *std.Io.Writer) !void { + var stdin_file = std.Io.File.stdin(); var raw_buf: [8192]u8 = undefined; - var streaming_reader = stdin_file.readerStreaming(&raw_buf); + var streaming_reader = stdin_file.readerStreaming(io, &raw_buf); const io_reader = &streaming_reader.interface; var diagnostics = std.ArrayList(Builder.Diagnostic).empty; defer { @@ -159,8 +163,8 @@ fn formatErrorFromStdin(allocator: std.mem.Allocator, writer: *std.Io.Writer) !v try writer.writeAll(formatted); } -fn formatFromStdin(allocator: std.mem.Allocator, writer: *std.Io.Writer) !void { - var reader = std.fs.File.stdin().reader(&.{}); +fn formatFromStdin(io: std.Io, allocator: std.mem.Allocator, writer: *std.Io.Writer) !void { + var reader = std.Io.File.stdin().reader(io, &.{}); var buffer: std.Io.Writer.Allocating = .init(allocator); _ = try reader.interface.streamRemaining(&buffer.writer); const input = try buffer.toOwnedSliceSentinel(0); @@ -181,8 +185,9 @@ fn formatFromStdin(allocator: std.mem.Allocator, writer: *std.Io.Writer) !void { fn formatFile( allocator: std.mem.Allocator, + io: std.Io, writer: *std.Io.Writer, - base_dir: std.fs.Dir, + base_dir: std.Io.Dir, sub_path: []const u8, full_path: []const u8, use_stdout: bool, @@ -190,11 +195,11 @@ fn formatFile( if (!std.mem.endsWith(u8, sub_path, ".zx")) { return; // Skip non-.zx files } - const source = try base_dir.readFileAlloc( - allocator, + io, sub_path, - std.math.maxInt(usize), + allocator, + .unlimited, ); defer allocator.free(source); @@ -224,27 +229,28 @@ fn formatFile( } // Write formatted content back to file - var atomic_file = try base_dir.atomicFile(sub_path, .{ .write_buffer = &.{} }); - defer atomic_file.deinit(); + var atomic_file = try base_dir.createFileAtomic(io, sub_path, .{ .replace = true }); + defer atomic_file.deinit(io); - try atomic_file.file_writer.interface.writeAll(formatted); - try atomic_file.finish(); + try atomic_file.file.writeStreamingAll(io, formatted); + try atomic_file.replace(io); try writer.print("{s}\n", .{full_path}); } fn formatDir( allocator: std.mem.Allocator, + io: std.Io, writer: *std.Io.Writer, path: []const u8, use_stdout: bool, ) !void { - var dir = try std.fs.cwd().openDir(path, .{ .iterate = true }); - defer dir.close(); + var dir = try std.Io.Dir.cwd().openDir(io, path, .{ .iterate = true }); + defer dir.close(io); var walker = try dir.walk(allocator); defer walker.deinit(); - while (try walker.next()) |entry| { + while (try walker.next(io)) |entry| { if (entry.kind != .file) continue; // Check if file ends with .zx before processing @@ -258,9 +264,10 @@ fn formatDir( // Read file using entry.dir (which is the directory containing the file) const source = try entry.dir.readFileAlloc( - allocator, + io, entry.basename, - std.math.maxInt(usize), + allocator, + .limited(std.math.maxInt(usize)), ); defer allocator.free(source); @@ -290,11 +297,11 @@ fn formatDir( } // Write formatted content back to file using entry.dir - var atomic_file = try entry.dir.atomicFile(entry.basename, .{ .write_buffer = &.{} }); - defer atomic_file.deinit(); + var atomic_file = try entry.dir.createFileAtomic(io, entry.basename, .{}); + defer atomic_file.deinit(io); - try atomic_file.file_writer.interface.writeAll(formatted); - try atomic_file.finish(); + try atomic_file.file.writeStreamingAll(io, formatted); + try atomic_file.replace(io); try writer.print("{s}\n", .{full_path}); } } diff --git a/src/cli/init.zig b/src/cli/init.zig index dfbd7701..ee248f74 100644 --- a/src/cli/init.zig +++ b/src/cli/init.zig @@ -42,6 +42,8 @@ const init_path_arg = zli.PositionalArg{ }; fn init(ctx: zli.CommandContext) !void { + const app = AppContext.from(&ctx); + const io = app.io; const t_val = ctx.flag("template", []const u8); const force_init = ctx.flag("force", bool); const existing_init = ctx.flag("existing", bool); @@ -51,7 +53,7 @@ fn init(ctx: zli.CommandContext) !void { defer printer.deinit(); // Validations - const is_clean_dir = try isDirEmpty(init_path); + const is_clean_dir = try isDirEmpty(io, init_path); const has_init_path_arg = init_path.len > 0 and !std.mem.eql(u8, init_path, "."); if (!is_clean_dir and !force_init and !existing_init) { printer.warning("Directory is not empty.", .{}); @@ -98,7 +100,7 @@ fn init(ctx: zli.CommandContext) !void { printer.header("{s} Initializing ZX project!", .{tui.Printer.emoji("○")}); printer.info("[{s}]", .{@tagName(template_name)}); - try std.fs.cwd().makePath(init_path); + try std.Io.Dir.cwd().createDirPath(io, init_path); for (templates) |template| { if (template.name != null and template.name.? != template_name) continue; @@ -108,7 +110,7 @@ fn init(ctx: zli.CommandContext) !void { // Skip if file exists and existing flag is set if (existing_init) { const file_exists = blk: { - std.fs.cwd().access(output_path, .{}) catch |err| switch (err) { + std.Io.Dir.cwd().access(io, output_path, .{}) catch |err| switch (err) { error.FileNotFound => break :blk false, else => continue, }; @@ -118,13 +120,13 @@ fn init(ctx: zli.CommandContext) !void { } if (std.fs.path.dirname(output_path)) |parent_dir| { - try std.fs.cwd().makePath(parent_dir); + try std.Io.Dir.cwd().createDirPath(io, parent_dir); } - var file = try std.fs.cwd().createFile(output_path, .{ .truncate = true }); + var file = try std.Io.Dir.cwd().createFile(io, output_path, .{ .truncate = true }); printer.filepath(template.path); - defer file.close(); + defer file.close(io); if (template.lines) |lines| { var line_iter = std.mem.splitScalar(u8, template.content, '\n'); @@ -134,14 +136,14 @@ fn init(ctx: zli.CommandContext) !void { for (lines) |line_range| { const start, const end = line_range; if (line_n < start or line_n > end) continue; - try file.writeAll(line); - try file.writeAll("\n"); + try file.writeStreamingAll(io, line); + try file.writeStreamingAll(io, "\n"); } line_n += 1; } } else { - try file.writeAll(template.content); + try file.writeStreamingAll(io, template.content); } } @@ -154,15 +156,15 @@ fn init(ctx: zli.CommandContext) !void { } } -pub fn isDirEmpty(path: []const u8) !bool { - var dir = std.fs.cwd().openDir(path, .{ .iterate = true }) catch |err| switch (err) { +pub fn isDirEmpty(io: std.Io, path: []const u8) !bool { + var dir = std.Io.Dir.cwd().openDir(io, path, .{ .iterate = true }) catch |err| switch (err) { error.FileNotFound => return true, else => return err, }; - defer dir.close(); + defer dir.close(io); var iter = dir.iterate(); - return try iter.next() == null; + return try iter.next(io) == null; } const TemplateFile = struct { @@ -214,4 +216,5 @@ const templates = [_]TemplateFile{ const std = @import("std"); const zli = @import("zli"); const tui = @import("../tui/main.zig"); +const AppContext = @import("shared/context.zig").AppContext; const colors = tui.Colors; diff --git a/src/cli/init/template/.tool-versions b/src/cli/init/template/.tool-versions new file mode 100644 index 00000000..a73bd9cb --- /dev/null +++ b/src/cli/init/template/.tool-versions @@ -0,0 +1 @@ +zig 0.16.0 diff --git a/src/cli/init/template/app/main.zig b/src/cli/init/template/app/main.zig index f9117940..7bbc8c9b 100644 --- a/src/cli/init/template/app/main.zig +++ b/src/cli/init/template/app/main.zig @@ -1,7 +1,7 @@ const zx = @import("zx"); pub fn main() !void { - var app = try zx.App(void).init(zx.allocator, .{}, {}); + var app = try zx.App(void).init(zx.io(), zx.allocator, .{}, {}); defer app.deinit(); try app.start(); diff --git a/src/cli/init/template/build.zig b/src/cli/init/template/build.zig index 72d864e5..dfc2d340 100644 --- a/src/cli/init/template/build.zig +++ b/src/cli/init/template/build.zig @@ -17,5 +17,5 @@ pub fn build(b: *std.Build) !void { }); // --- ZX setup: wires dependencies and adds `zx`/`dev` build steps --- - _ = try zx.init(b, app_exe, .{}); + _ = try zx.init(b, app_exe, .{ .cli = .{ .optimize = optimize } }); } diff --git a/src/cli/init/template/build.zig.zon b/src/cli/init/template/build.zig.zon index 10f9440b..02336a14 100644 --- a/src/cli/init/template/build.zig.zon +++ b/src/cli/init/template/build.zig.zon @@ -5,8 +5,7 @@ .minimum_zig_version = "0.15.2", .dependencies = .{ .zx = .{ - .url = "git+https://github.com/ziex-dev/ziex#32b4b8de432d4d13e694a1d0dc9d9476bf79ce72", - .hash = "zx-0.1.0-dev.1014-8okzKifFmgGcrf29PJbfbHzlXhV70dHbbAq3Zp3j8bFb", + .path = "../../../../", }, }, .paths = .{ diff --git a/src/cli/serve.zig b/src/cli/serve.zig index fadfdcb7..4084ae73 100644 --- a/src/cli/serve.zig +++ b/src/cli/serve.zig @@ -24,12 +24,14 @@ const port_flag = zli.Flag{ }; fn serve(ctx: zli.CommandContext) !void { + const app = AppContext.from(&ctx); const port = ctx.flag("port", u32); const port_str = try std.fmt.allocPrint(ctx.allocator, "{d}", .{port}); defer ctx.allocator.free(port_str); const binpath = ctx.flag("binpath", []const u8); var build_args = std.ArrayList([]const u8).empty; + defer build_args.deinit(ctx.allocator); try build_args.appendSlice(ctx.allocator, &.{ cli_options.zig_exe, "build", "serve" }); var i_build_args = std.mem.splitSequence(u8, ctx.flag("build-args", []const u8), " "); @@ -43,21 +45,15 @@ fn serve(ctx: zli.CommandContext) !void { if (port != 0) try build_args.appendSlice(ctx.allocator, &.{ "--port", port_str }); try build_args.appendSlice(ctx.allocator, &.{ "--cli-command", "serve" }); - var system = std.process.Child.init(build_args.items, ctx.allocator); - try system.spawn(); + var system = try std.process.spawn(app.io, .{ .argv = build_args.items }); - var program_meta = util.findprogram(ctx.allocator, binpath) catch |err| { + var program_meta = util.findprogram(app.io, ctx.allocator, binpath) catch |err| { log.debug("Error finding ZX executable! {any}\n", .{err}); return; }; defer program_meta.deinit(ctx.allocator); - // TODO: Move logic of building js to the post transpilation process in the build system steps - jsutil.buildjs(ctx, binpath, false, false) catch |err| { - log.debug("Error building JS! {any}", .{err}); - }; - - const term = try system.wait(); + const term = try system.wait(app.io); _ = term; } @@ -65,6 +61,6 @@ const std = @import("std"); const zli = @import("zli"); const util = @import("shared/util.zig"); const flags = @import("shared/flag.zig"); -const jsutil = @import("shared/js.zig"); +const AppContext = @import("shared/context.zig").AppContext; const cli_options = @import("cli_options"); const log = std.log.scoped(.cli); diff --git a/src/cli/shared/builder.zig b/src/cli/shared/builder.zig index 2dcc1d68..1063c931 100644 --- a/src/cli/shared/builder.zig +++ b/src/cli/shared/builder.zig @@ -125,7 +125,7 @@ pub const BuildWatcher = struct { self.mutex.lock(); defer self.mutex.unlock(); self.restart_pending = false; - self.last_restart_time_ns = std.time.nanoTimestamp(); + self.last_restart_time_ns = std.Io.Clock.awake.now(std.testing.io).nanoseconds; self.last_binary_mtime = new_mtime; } @@ -229,10 +229,10 @@ pub fn watchBuildOutput(watcher: *BuildWatcher) !void { // Detect build completion via "Build Summary:" if (std.mem.indexOf(u8, pattern_buf.items, "Build Summary:") != null) { - const now = std.time.nanoTimestamp(); + const now = std.Io.Clock.awake.now(std.testing.io).nanoseconds; log.debug("Build Summary detected", .{}); - const stat = std.fs.cwd().statFile(watcher.binary_path) catch |err| { + const stat = std.Io.Dir.cwd().statFile(watcher.binary_path) catch |err| { log.debug("Failed to stat binary: {any}", .{err}); pattern_buf.clearRetainingCapacity(); build_in_progress = false; @@ -280,7 +280,7 @@ pub fn watchBuildOutput(watcher: *BuildWatcher) !void { // First build watcher.first_build_done = true; watcher.last_binary_mtime = stat.mtime; - watcher.last_restart_time_ns = std.time.nanoTimestamp(); + watcher.last_restart_time_ns = std.Io.Clock.awake.now(std.testing.io).nanoseconds; watcher.previous_build_had_errors = watcher.current_build_has_errors; log.debug("First build completed", .{}); } diff --git a/src/cli/shared/context.zig b/src/cli/shared/context.zig new file mode 100644 index 00000000..e334d94e --- /dev/null +++ b/src/cli/shared/context.zig @@ -0,0 +1,18 @@ +const std = @import("std"); + +/// Shared per-process context threaded through CLI commands via +/// `zli`'s `CommandContext.data`. Carries the working `Io` and +/// `Environ.Map` provided by `std.process.Init` in `main`. +/// +/// The single-threaded global `Io` (`std.Io.Threaded.global_single_threaded`) +/// has a `failing` allocator and cannot be used for any operation that +/// allocates (e.g. `std.process.spawn`). Commands must use this context's +/// `io` instead. +pub const AppContext = struct { + io: std.Io, + environ_map: *std.process.Environ.Map, + + pub fn from(ctx: anytype) *AppContext { + return @ptrCast(@alignCast(ctx.data.?)); + } +}; diff --git a/src/cli/shared/js.zig b/src/cli/shared/js.zig index 143e274e..08e8e1dd 100644 --- a/src/cli/shared/js.zig +++ b/src/cli/shared/js.zig @@ -16,12 +16,12 @@ pub const PackageJson = struct { bun, }; - pub fn parse(allocator: std.mem.Allocator) !std.json.Parsed(PackageJson) { - const cwd = std.fs.cwd(); + pub fn parse(allocator: std.mem.Allocator, io: std.Io) !std.json.Parsed(PackageJson) { + const cwd = std.Io.Dir.cwd(); var pkg_final_path: ?[]const u8 = null; const package_json_str = blk: { for (pkg_find_paths) |pkg_find_path| { - const package_json_str = cwd.readFileAlloc(allocator, pkg_find_path, std.math.maxInt(usize)) catch |err| switch (err) { + const package_json_str = cwd.readFileAlloc(io, pkg_find_path, allocator, .limited(std.math.maxInt(usize))) catch |err| switch (err) { error.FileNotFound => continue, else => return err, }; @@ -52,7 +52,7 @@ pub const PackageJson = struct { return package_json_parsed; } - fn getPackageManager(self: *PackageJson) PM { + fn getPackageManager(self: *PackageJson, io: std.Io) PM { if (self.packageManager) |pm| return pm; if (self.dependencies) |deps| { switch (deps) { @@ -67,35 +67,37 @@ pub const PackageJson = struct { } if (self.pkg_path) |pkg_path| { const dir_from_pkg_path = std.fs.path.dirname(pkg_path) orelse return .npm; - const cwd = std.fs.cwd().openDir(dir_from_pkg_path, .{}) catch return .npm; + const cwd = std.Io.Dir.cwd().openDir(io, dir_from_pkg_path, .{}) catch return .npm; // Check for lockfiles - if (cwd.statFile("package-lock.json") catch null) |_| return .npm; - if (cwd.statFile("pnpm-lock.yaml") catch null) |_| return .pnpm; - if (cwd.statFile("yarn.lock") catch null) |_| return .yarn; - if (cwd.statFile("bun.lock") catch null) |_| return .bun; - if (cwd.statFile("bun.lockb") catch null) |_| return .bun; + if (cwd.statFile(io, "package-lock.json", .{}) catch null) |_| return .npm; + if (cwd.statFile(io, "pnpm-lock.yaml", .{}) catch null) |_| return .pnpm; + if (cwd.statFile(io, "yarn.lock", .{}) catch null) |_| return .yarn; + if (cwd.statFile(io, "bun.lock", .{}) catch null) |_| return .bun; + if (cwd.statFile(io, "bun.lockb", .{}) catch null) |_| return .bun; } // Check for binary in path return .npm; } }; -pub fn checkEsbuildBin(allocator: std.mem.Allocator, pkg_rootdir: []const u8) bool { +pub fn checkEsbuildBin(io: std.Io, allocator: std.mem.Allocator, pkg_rootdir: []const u8) bool { const esbuild_bin_path = std.fs.path.join(allocator, &.{ pkg_rootdir, "node_modules", ".bin", "esbuild" }) catch return false; defer allocator.free(esbuild_bin_path); - return if (std.fs.cwd().statFile(esbuild_bin_path) catch null) |_| true else false; + return if (std.Io.Dir.cwd().statFile(io, esbuild_bin_path, .{}) catch null) |_| true else false; } pub fn buildjs(ctx: zli.CommandContext, binpath: []const u8, is_dev: bool, verbose: bool) !void { - var program_meta = try util.findprogram(ctx.allocator, binpath); + const app = AppContext.from(&ctx); + const io = app.io; + var program_meta = try util.findprogram(io, ctx.allocator, binpath); defer program_meta.deinit(ctx.allocator); const rootdir = program_meta.rootdir orelse return error.RootdirNotFound; log.debug("Parsing package.json", .{}); - var package_json_parsed = try PackageJson.parse(ctx.allocator); + var package_json_parsed = try PackageJson.parse(ctx.allocator, io); defer package_json_parsed.deinit(); var package_json = package_json_parsed.value; log.debug("Found and parsed package.json in ./{s}", .{package_json.pkg_path orelse "na"}); @@ -103,27 +105,31 @@ pub fn buildjs(ctx: zli.CommandContext, binpath: []const u8, is_dev: bool, verbo const pkg_path = package_json.pkg_path orelse return error.PkgPathNotFound; const pkg_rootdir = std.fs.path.dirname(pkg_path) orelse "."; - const pm = package_json.getPackageManager(); + const pm = package_json.getPackageManager(io); log.debug("Package manager: {s}", .{@tagName(pm)}); - if (!checkEsbuildBin(ctx.allocator, pkg_rootdir)) { + if (!checkEsbuildBin(io, ctx.allocator, pkg_rootdir)) { log.debug("Installing dependencies for JavaScript", .{}); log.debug("We try bun first", .{}); - var bun_installer = std.process.Child.init(&.{ "bun", "install" }, ctx.allocator); - bun_installer.cwd = pkg_rootdir; + var bun_installer = try std.process.spawn(io, .{ + .argv = &.{ "bun", "install" }, + .cwd = .{ .path = pkg_rootdir }, + }); + defer bun_installer.kill(io); - try bun_installer.spawn(); - const status = try bun_installer.wait(); + const status = try bun_installer.wait(io); log.debug("Bun installer status: {s}", .{@tagName(status)}); - if (!checkEsbuildBin(ctx.allocator, pkg_rootdir)) { - var installer = std.process.Child.init(&.{ @tagName(pm), "install" }, ctx.allocator); - installer.cwd = pkg_rootdir; - try installer.spawn(); - _ = try installer.wait(); + if (!checkEsbuildBin(io, ctx.allocator, pkg_rootdir)) { + var installer = try std.process.spawn(io, .{ + .argv = &.{ @tagName(pm), "install" }, + .cwd = .{ .path = pkg_rootdir }, + }); + defer installer.kill(io); + _ = try installer.wait(io); } - if (!checkEsbuildBin(ctx.allocator, pkg_rootdir)) { + if (!checkEsbuildBin(io, ctx.allocator, pkg_rootdir)) { std.debug.print( \\ \\Could not find a Node.js package manager on your system. @@ -164,21 +170,39 @@ pub fn buildjs(ctx: zli.CommandContext, binpath: []const u8, is_dev: bool, verbo const esbuild_args_str = try std.mem.join(ctx.allocator, " ", esbuild_args.items); defer ctx.allocator.free(esbuild_args_str); log.debug("Esbuild args: {s}", .{esbuild_args_str}); - var esbuild_cmd = std.process.Child.init(esbuild_args.items, ctx.allocator); - esbuild_cmd.stderr_behavior = .Pipe; - esbuild_cmd.stdout_behavior = .Pipe; - try esbuild_cmd.spawn(); + var esbuild_cmd = try std.process.spawn(io, .{ + .argv = esbuild_args.items, + .stdout = .pipe, + .stderr = .pipe, + }); + + var stdout_writer = std.Io.Writer.Allocating.init(ctx.allocator); + defer stdout_writer.deinit(); + var stderr_writer = std.Io.Writer.Allocating.init(ctx.allocator); + defer stderr_writer.deinit(); + + // Read output from pipes + if (esbuild_cmd.stdout) |stdout_file| { + var buf: [4096]u8 = undefined; + var streaming_reader = stdout_file.readerStreaming(io, &buf); + const reader = &streaming_reader.interface; + _ = reader.streamRemaining(&stdout_writer.writer) catch {}; + } + if (esbuild_cmd.stderr) |stderr_file| { + var buf: [4096]u8 = undefined; + var streaming_reader = stderr_file.readerStreaming(io, &buf); + const reader = &streaming_reader.interface; + _ = reader.streamRemaining(&stderr_writer.writer) catch {}; + } - var stdout = std.ArrayList(u8).empty; - var stderr = std.ArrayList(u8).empty; - esbuild_cmd.collectOutput(ctx.allocator, &stdout, &stderr, 8192) catch |err| { - std.debug.print("Error collecting output: {any}", .{err}); - }; + const stdout = stdout_writer.written(); + const stderr = stderr_writer.written(); + _ = try esbuild_cmd.wait(io); - log.debug("Esbuild stdout: {s} \n stderr: {s}", .{ stdout.items, stderr.items }); + log.debug("Esbuild stdout: {s} \n stderr: {s}", .{ stdout, stderr }); - const esbuild_output = try parseEsbuildOutput(stderr.items); + const esbuild_output = try parseEsbuildOutput(stderr); // Pretty print esbuild output with colors if (verbose and esbuild_output.path.len > 0 and esbuild_output.size.len > 0 and esbuild_output.time.len > 0) { @@ -465,4 +489,5 @@ const std = @import("std"); const zli = @import("zli"); const util = @import("util.zig"); const tui = @import("../../tui/main.zig"); +const AppContext = @import("context.zig").AppContext; const log = std.log.scoped(.cli); diff --git a/src/cli/shared/stdio.zig b/src/cli/shared/stdio.zig index a3451e8c..d5802690 100644 --- a/src/cli/shared/stdio.zig +++ b/src/cli/shared/stdio.zig @@ -20,7 +20,7 @@ pub const OutputTarget = union(enum) { /// Forward to stdout stdout, /// Forward to a custom file - file: std.fs.File, + file: std.Io.File, /// Forward to a custom writer writer: *std.Io.Writer, /// Discard output @@ -84,14 +84,15 @@ pub const ChildOutputOptions = struct { /// Context for reading from a child process stream const StreamContext = struct { - file: std.fs.File, + io: std.Io, + file: std.Io.File, stream_name: []const u8, allocator: std.mem.Allocator, options: StreamOptions, last_line: ?[]const u8 = null, - done: std.Thread.Mutex = .{}, + done: std.Io.Mutex = .init, done_flag: bool = false, - first_line_captured: std.Thread.Mutex = .{}, + first_line_captured: std.Io.Mutex = .init, first_line_captured_flag: bool = false, }; @@ -103,17 +104,17 @@ pub const ChildOutput = struct { /// Check if both streams have finished reading pub fn isDone(self: *const ChildOutput) bool { - self.stderr.done.lock(); - defer self.stderr.done.unlock(); - self.stdout.done.lock(); - defer self.stdout.done.unlock(); + self.stderr.done.lockUncancelable(self.stderr.io); + defer self.stderr.done.unlock(self.stderr.io); + self.stdout.done.lockUncancelable(self.stdout.io); + defer self.stdout.done.unlock(self.stdout.io); return self.stderr.done_flag and self.stdout.done_flag; } /// Wait for both streams to finish reading pub fn wait(self: *const ChildOutput) void { while (!self.isDone()) { - std.Thread.sleep(10 * std.time.ns_per_ms); + self.stderr.io.sleep(.fromMilliseconds(10), .awake) catch {}; } } @@ -127,23 +128,27 @@ pub const ChildOutput = struct { return self.stdout.last_line; } - /// Wait for the first line to be captured (for first_line_then_transparent mode) - pub fn waitForFirstLine(self: *const ChildOutput) void { + /// Wait for the first line to be captured (for first_line_then_transparent mode). + /// Returns false if no line arrived before timeout_ms. + pub fn waitForFirstLine(self: *const ChildOutput, timeout_ms: u64) bool { + var elapsed_ms: u64 = 0; while (true) { const stderr_captured = blk: { - self.stderr.first_line_captured.lock(); - defer self.stderr.first_line_captured.unlock(); + self.stderr.first_line_captured.lockUncancelable(self.stderr.io); + defer self.stderr.first_line_captured.unlock(self.stderr.io); break :blk self.stderr.first_line_captured_flag; }; const stdout_captured = blk: { - self.stdout.first_line_captured.lock(); - defer self.stdout.first_line_captured.unlock(); + self.stdout.first_line_captured.lockUncancelable(self.stdout.io); + defer self.stdout.first_line_captured.unlock(self.stdout.io); break :blk self.stdout.first_line_captured_flag; }; - if (stderr_captured or stdout_captured) break; - std.Thread.sleep(1 * std.time.ns_per_ms); + if (stderr_captured or stdout_captured) return true; + if (elapsed_ms >= timeout_ms) return false; + self.stderr.io.sleep(.fromMilliseconds(1), .awake) catch {}; + elapsed_ms += 1; } } @@ -158,6 +163,7 @@ pub const ChildOutput = struct { /// Returns handles to the stream contexts which can be used to check completion or access last_line /// The returned ChildOutput must be deinitialized with deinit() when done pub fn captureChildOutput( + io: std.Io, allocator: std.mem.Allocator, child: *std.process.Child, options: ChildOutputOptions, @@ -168,6 +174,7 @@ pub fn captureChildOutput( if (child.stderr) |stderr_file| { stderr_ctx.* = StreamContext{ + .io = io, .file = stderr_file, .stream_name = "stderr", .allocator = allocator, @@ -179,9 +186,11 @@ pub fn captureChildOutput( _ = try std.Thread.spawn(.{}, readChildStream, .{stderr_ctx}); } } else { - // No stderr pipe - mark as done immediately + // No stderr pipe - mark as done immediately. + // `file` is never read because done_flag is set true below. stderr_ctx.* = StreamContext{ - .file = std.fs.File{ .handle = undefined }, + .io = io, + .file = undefined, .stream_name = "stderr", .allocator = allocator, .options = options.stderr, @@ -195,6 +204,7 @@ pub fn captureChildOutput( if (child.stdout) |stdout_file| { stdout_ctx.* = StreamContext{ + .io = io, .file = stdout_file, .stream_name = "stdout", .allocator = allocator, @@ -208,7 +218,8 @@ pub fn captureChildOutput( } else { // No stdout pipe - mark as done immediately stdout_ctx.* = StreamContext{ - .file = std.fs.File{ .handle = undefined }, + .io = io, + .file = undefined, .stream_name = "stdout", .allocator = allocator, .options = options.stdout, @@ -224,32 +235,29 @@ pub fn captureChildOutput( } fn readChildStream(ctx: *StreamContext) void { + const io = ctx.io; switch (ctx.options.mode) { .discard => { // Discard mode - just read and throw away var buffer: [4096]u8 = undefined; while (true) { - _ = ctx.file.read(&buffer) catch |err| { - if (err == error.BrokenPipe) break; + const bytes_read = ctx.file.readStreaming(io, &.{&buffer}) catch |err| { + if (err == error.BrokenPipe or err == error.EndOfStream) break; log.debug("Error reading {s}: {any}", .{ ctx.stream_name, err }); break; }; + if (bytes_read == 0) break; } }, .transparent => { // Transparent mode: read raw bytes and forward immediately without processing var buffer: [4096]u8 = undefined; - while (true) { - const bytes_read = ctx.file.read(&buffer) catch |err| { - if (err == error.BrokenPipe) { - break; - } else { - log.debug("Error reading {s}: {any}", .{ ctx.stream_name, err }); - break; - } + const bytes_read = ctx.file.readStreaming(io, &.{&buffer}) catch |err| { + if (err == error.BrokenPipe or err == error.EndOfStream) break; + log.debug("Error reading {s}: {any}", .{ ctx.stream_name, err }); + break; }; - if (bytes_read == 0) break; const bytes = buffer[0..bytes_read]; @@ -258,7 +266,7 @@ fn readChildStream(ctx: *StreamContext) void { if (ctx.options.on_bytes) |callback| { callback(bytes, ctx.stream_name); } else { - writeToTarget(ctx.options.target, bytes) catch |err| { + writeToTarget(io, ctx.options.target, bytes) catch |err| { log.debug("Error writing {s}: {any}", .{ ctx.stream_name, err }); break; }; @@ -268,14 +276,14 @@ fn readChildStream(ctx: *StreamContext) void { .line_buffered => { // Line buffered mode: read line by line var buffer: [4096]u8 = undefined; - var streaming_reader = ctx.file.readerStreaming(&buffer); + var streaming_reader = ctx.file.readerStreaming(io, &buffer); const io_reader = &streaming_reader.interface; var line_writer = std.Io.Writer.Allocating.init(ctx.allocator); defer line_writer.deinit(); // Continuously read lines and process them while (io_reader.streamDelimiter(&line_writer.writer, '\n')) |_| { - std.Thread.sleep(10 * std.time.ns_per_ms); + io.sleep(.fromMilliseconds(10), .awake) catch {}; const line = line_writer.written(); if (line.len > 0) { @@ -319,7 +327,7 @@ fn readChildStream(ctx: *StreamContext) void { .first_line_then_transparent => { // First, capture the first line var buffer: [4096]u8 = undefined; - var streaming_reader = ctx.file.readerStreaming(&buffer); + var streaming_reader = ctx.file.readerStreaming(io, &buffer); const io_reader = &streaming_reader.interface; var line_writer = std.Io.Writer.Allocating.init(ctx.allocator); defer line_writer.deinit(); @@ -342,20 +350,16 @@ fn readChildStream(ctx: *StreamContext) void { } // Mark first line as captured - ctx.first_line_captured.lock(); + ctx.first_line_captured.lockUncancelable(io); ctx.first_line_captured_flag = true; - ctx.first_line_captured.unlock(); + ctx.first_line_captured.unlock(io); // Continue in transparent mode - first line already consumed by streamDelimiter var transparent_buffer: [4096]u8 = undefined; - while (true) { - const bytes_read = ctx.file.read(&transparent_buffer) catch |err| { - if (err == error.BrokenPipe) break; - log.debug("Error reading {s}: {any}", .{ ctx.stream_name, err }); - break; - }; - - std.Thread.sleep(ctx.options.transparent_delay_ms * std.time.ns_per_ms); + var transparent_reader = ctx.file.readerStreaming(io, &transparent_buffer); + const trans_reader = &transparent_reader.interface; + while (trans_reader.readSliceShort(&transparent_buffer)) |bytes_read| { + io.sleep(.fromMilliseconds(@intCast(ctx.options.transparent_delay_ms)), .awake) catch {}; if (bytes_read == 0) break; @@ -365,26 +369,41 @@ fn readChildStream(ctx: *StreamContext) void { if (ctx.options.on_bytes) |callback| { callback(bytes, ctx.stream_name); } else { - writeToTarget(ctx.options.target, bytes) catch |err| { + writeToTarget(io, ctx.options.target, bytes) catch |err| { log.debug("Error writing {s}: {any}", .{ ctx.stream_name, err }); break; }; } + } else |err| { + if (err != error.BrokenPipe and err != error.EndOfStream) { + log.debug("Error reading {s}: {any}", .{ ctx.stream_name, err }); + } } }, } // Mark as done - ctx.done.lock(); + ctx.done.lockUncancelable(io); + defer ctx.done.unlock(io); ctx.done_flag = true; - ctx.done.unlock(); } -fn writeToTarget(target: OutputTarget, bytes: []const u8) !void { +fn writeToTarget(io: std.Io, target: OutputTarget, bytes: []const u8) !void { switch (target) { - .stderr => try std.fs.File.stderr().writeAll(bytes), - .stdout => try std.fs.File.stdout().writeAll(bytes), - .file => |file| try file.writeAll(bytes), + .stderr => { + const stderr_file = std.Io.File.stderr(); + var writer = stderr_file.writerStreaming(io, undefined); + try writer.interface.writeAll(bytes); + }, + .stdout => { + const stdout_file = std.Io.File.stdout(); + var writer = stdout_file.writerStreaming(io, undefined); + try writer.interface.writeAll(bytes); + }, + .file => |file| { + var writer = file.writerStreaming(io, undefined); + try writer.interface.writeAll(bytes); + }, .writer => |writer| try writer.writeAll(bytes), .discard => {}, } diff --git a/src/cli/shared/util.zig b/src/cli/shared/util.zig index ab4815e6..9a9c6c18 100644 --- a/src/cli/shared/util.zig +++ b/src/cli/shared/util.zig @@ -1,21 +1,21 @@ const BIN_DIR = "zig-out" ++ std.fs.path.sep_str ++ "bin"; /// Find the ZX executable from the bin directory -pub fn findprogram(allocator: std.mem.Allocator, binpath: []const u8) !SerilizableAppMeta { +pub fn findprogram(io: std.Io, allocator: std.mem.Allocator, binpath: []const u8) !SerilizableAppMeta { if (!std.mem.eql(u8, binpath, "")) { - var app_meta = try inspectProgram(allocator, binpath); + var app_meta = try inspectProgram(io, allocator, binpath); // defer std.zon.parse.free(allocator, app_meta); // errdefer std.zon.parse.free(allocator, app_meta); app_meta.binpath = binpath; return app_meta; } - var files = try std.fs.cwd().openDir(BIN_DIR, .{ .iterate = true }); - defer files.close(); + var files = try std.Io.Dir.cwd().openDir(io, BIN_DIR, .{ .iterate = true }); + defer files.close(io); var exe_count: usize = 0; var it = files.iterate(); - while (try it.next()) |entry| { + while (try it.next(io)) |entry| { if (entry.kind == .file) { exe_count += 1; @@ -24,7 +24,7 @@ pub fn findprogram(allocator: std.mem.Allocator, binpath: []const u8) !Serilizab log.debug("Inspecting exe: {s}", .{full_path}); - var app_meta = inspectProgram(allocator, full_path) catch |err| switch (err) { + var app_meta = inspectProgram(io, allocator, full_path) catch |err| switch (err) { error.ProgramNotFound, error.ParseZon, error.InvalidExe => continue, else => return err, }; @@ -41,29 +41,41 @@ pub fn findprogram(allocator: std.mem.Allocator, binpath: []const u8) !Serilizab return error.ProgramNotFound; } -pub fn inspectProgram(allocator: std.mem.Allocator, binpath: []const u8) !SerilizableAppMeta { - var exe = std.process.Child.init(&.{ binpath, "--introspect" }, allocator); - exe.stdout_behavior = .Pipe; - exe.stderr_behavior = .Ignore; - try exe.spawn(); - - const source = if (exe.stdout) |estdout| estdout.readToEndAlloc(allocator, std.math.maxInt(usize)) catch |err| { - _ = exe.kill() catch {}; - return err; +pub fn inspectProgram(io: std.Io, allocator: std.mem.Allocator, binpath: []const u8) !SerilizableAppMeta { + // The binary only prints metadata + exits when built with `-Dintrospect=true` + // (see runtime/server/Server.zig:introspect). The dev command is responsible + // for producing such a binary before calling this; here we just run it. + var exe = try std.process.spawn(io, .{ + .argv = &.{binpath}, + .stdout = .pipe, + .stderr = .ignore, + }); + + const source = if (exe.stdout) |estdout| blk: { + var buf: [4096]u8 = undefined; + var reader_streaming = estdout.readerStreaming(io, &buf); + const reader = &reader_streaming.interface; + var aw: std.Io.Writer.Allocating = .init(allocator); + defer aw.deinit(); + _ = reader.streamRemaining(&aw.writer) catch |err| { + exe.kill(io); + return err; + }; + break :blk try aw.toOwnedSlice(); } else { - _ = exe.kill() catch {}; + exe.kill(io); return error.ProgramNotFound; }; defer allocator.free(source); - _ = exe.wait() catch {}; + _ = exe.wait(io) catch {}; if (source.len == 0) return error.ProgramNotFound; const source_z = try allocator.dupeZ(u8, source); defer allocator.free(source_z); - const app_meta = try std.zon.parse.fromSlice(SerilizableAppMeta, allocator, source_z, null, .{}); + const app_meta = try std.zon.parse.fromSliceAlloc(SerilizableAppMeta, allocator, source_z, null, .{}); return app_meta; } @@ -76,6 +88,7 @@ fn shouldIgnorePath(path: []const u8) bool { return false; } pub fn copydirs( + io: std.Io, allocator: std.mem.Allocator, base_dir: []const u8, source_dirs: []const []const u8, @@ -87,25 +100,25 @@ pub fn copydirs( const source_path = try std.fs.path.join(allocator, &.{ base_dir, source_dir }); defer allocator.free(source_path); - var source = std.fs.cwd().openDir(source_path, .{ .iterate = true }) catch |err| switch (err) { + var source = std.Io.Dir.cwd().openDir(io, source_path, .{ .iterate = true }) catch |err| switch (err) { error.FileNotFound => continue, error.NotDir => continue, else => return err, }; - defer source.close(); + defer source.close(io); - std.fs.cwd().makePath(dest_dir) catch |err| switch (err) { + std.Io.Dir.cwd().createDirPath(io, dest_dir) catch |err| switch (err) { error.PathAlreadyExists => {}, else => return err, }; - var dest = try std.fs.cwd().openDir(dest_dir, .{}); - defer dest.close(); + var dest = try std.Io.Dir.cwd().openDir(io, dest_dir, .{}); + defer dest.close(io); var walker = try source.walk(allocator); defer walker.deinit(); - while (try walker.next()) |entry| { + while (try walker.next(io)) |entry| { const src_path = try std.fs.path.join(allocator, &.{ source_path, entry.path }); defer allocator.free(src_path); @@ -124,20 +137,20 @@ pub fn copydirs( // Create parent directory if needed if (std.fs.path.dirname(dst_abs_path)) |parent| { - std.fs.cwd().makePath(parent) catch |err| switch (err) { + std.Io.Dir.cwd().createDirPath(io, parent) catch |err| switch (err) { error.PathAlreadyExists => {}, else => return err, }; } // Copy file - try std.fs.cwd().copyFile(src_path, std.fs.cwd(), dst_abs_path, .{}); + try std.Io.Dir.copyFile(std.Io.Dir.cwd(), src_path, dest, std.fs.path.basename(src_path), io, .{}); printer.filepath(dst_rel_path); }, .directory => { if (shouldIgnorePath(dst_abs_path)) continue; // Create directory if needed - std.fs.cwd().makePath(dst_abs_path) catch |err| switch (err) { + std.Io.Dir.cwd().createDirPath(io, dst_abs_path) catch |err| switch (err) { error.PathAlreadyExists => {}, else => return err, }; @@ -148,17 +161,18 @@ pub fn copydirs( } } -pub fn getRunnablePath(allocator: std.mem.Allocator, program_path: []const u8) ![]const u8 { +pub fn getRunnablePath(io: std.Io, allocator: std.mem.Allocator, program_path: []const u8) ![]const u8 { if (builtin.os.tag == .windows) { // Create .zig-cache/tmp/.zx directory if it doesn't exist const cache_dir = ".zig-cache/tmp/.zx"; - try std.fs.cwd().makePath(cache_dir); + try std.Io.Dir.cwd().createDirPath(io, cache_dir); - const dest_dir = try std.fs.cwd().openDir(cache_dir, .{}); + const dest_dir = try std.Io.Dir.cwd().openDir(io, cache_dir, .{}); + defer dest_dir.close(io); const bin_name = std.fs.path.basename(program_path); // Copy the executable to the cache directory - try std.fs.cwd().copyFile(program_path, dest_dir, bin_name, .{}); + try std.Io.Dir.cwd().copyFile(program_path, dest_dir, bin_name, io, .{}); const copied_program_path = try std.fs.path.join(allocator, &.{ cache_dir, bin_name }); return copied_program_path; diff --git a/src/cli/transformjs.zig b/src/cli/transformjs.zig index 4cb3839e..87ea3d8e 100644 --- a/src/cli/transformjs.zig +++ b/src/cli/transformjs.zig @@ -59,7 +59,7 @@ fn transformjs(ctx: zli.CommandContext) !void { }; // Check if path is a directory first - if (std.fs.cwd().openDir(path_value, .{ .iterate = true })) |dir| { + if (std.Io.Dir.cwd().openDir(path_value, .{ .iterate = true })) |dir| { var dir_mut = dir; dir_mut.close(); // It's a directory, transform it @@ -74,7 +74,7 @@ fn transformjs(ctx: zli.CommandContext) !void { try transformFile( ctx.allocator, ctx.writer, - std.fs.cwd(), + std.Io.Dir.cwd(), path_value, path_value, use_stdout, @@ -166,7 +166,7 @@ fn transformDir( path: []const u8, use_stdout: bool, ) !void { - var dir = try std.fs.cwd().openDir(path, .{ .iterate = true }); + var dir = try std.Io.Dir.cwd().openDir(path, .{ .iterate = true }); defer dir.close(); var walker = try dir.walk(allocator); diff --git a/src/cli/transpile.zig b/src/cli/transpile.zig index 8e017f72..4b6ab351 100644 --- a/src/cli/transpile.zig +++ b/src/cli/transpile.zig @@ -3,7 +3,6 @@ const zli = @import("zli"); const zx = @import("zx"); const log = std.log.scoped(.cli); const util = @import("shared/util.zig"); -const jsutil = @import("shared/js.zig"); const flags = @import("shared/flag.zig"); const base64 = std.base64.standard; @@ -135,8 +134,10 @@ fn transpile(ctx: zli.CommandContext) !void { const default_outdir = ".zx"; const is_default_outdir = std.mem.eql(u8, outdir, default_outdir); + const io = std.Io.Threaded.global_single_threaded.io(); + // Check if path is a file (not a directory) - const stat = std.fs.cwd().statFile(path) catch |err| switch (err) { + const stat = std.Io.Dir.cwd().statFile(io, path, .{}) catch |err| switch (err) { error.IsDir => { // It's a directory, proceed with normal transpileCommand try transpileCommand(ctx.allocator, .{ @@ -167,11 +168,7 @@ fn transpile(ctx: zli.CommandContext) !void { // If outdir is default and path is a file, output to stdout if (is_default_outdir) { // Read the source file - const source = try std.fs.cwd().readFileAlloc( - ctx.allocator, - path, - std.math.maxInt(usize), - ); + const source = try std.Io.Dir.cwd().readFileAlloc(io, path, ctx.allocator, std.Io.Limit.limited(std.math.maxInt(usize))); defer ctx.allocator.free(source); const source_z = try ctx.allocator.dupeZ(u8, source); @@ -199,7 +196,7 @@ fn transpile(ctx: zli.CommandContext) !void { ); defer ctx.allocator.free(sourcemap_json); - try std.fs.cwd().writeFile(.{ + try std.Io.Dir.cwd().writeFile(io, .{ .sub_path = map_path, .data = sourcemap_json, }); @@ -266,9 +263,10 @@ fn writeDepFile(allocator: std.mem.Allocator, path: []const u8, target: []const } } try buf.appendSlice(allocator, "\n"); - const f = try std.fs.cwd().createFile(path, .{}); - defer f.close(); - try f.writeAll(buf.items); + const io = std.Io.Threaded.global_single_threaded.io(); + const f = try std.Io.Dir.cwd().createFile(io, path, .{}); + defer f.close(io); + try f.writePositionalAll(io, buf.items, 0); } /// Scan source for `@embedFile("...")` references and add resolved paths as dependencies. @@ -293,8 +291,21 @@ fn collectEmbedFileDeps( const resolved = std.fs.path.join(allocator, &.{ source_dir, embed_path }) catch continue; defer allocator.free(resolved); - const abs_path = std.fs.cwd().realpathAlloc(allocator, resolved) catch continue; - input_files.append(abs_path) catch continue; + const abs_path = std.fs.path.resolve(allocator, &.{resolved}) catch continue; + + // Only record embeds that actually exist on disk. .zx files often + // @embedFile() the transpiled .zig output of a sibling .zx — that + // file doesn't exist as a source and listing it in the dep file + // makes the build harness fail with FileNotFound. + const io_local = std.Io.Threaded.global_single_threaded.io(); + if (std.Io.Dir.cwd().statFile(io_local, abs_path, .{})) |_| { + input_files.append(abs_path) catch { + allocator.free(abs_path); + continue; + }; + } else |_| { + allocator.free(abs_path); + } } } @@ -307,7 +318,8 @@ fn writeComponentCache( ) !void { const json = try std.json.Stringify.valueAlloc(allocator, components, .{}); defer allocator.free(json); - try std.fs.cwd().writeFile(.{ .sub_path = cache_path, .data = json }); + const io = std.Io.Threaded.global_single_threaded.io(); + try std.Io.Dir.cwd().writeFile(io, .{ .sub_path = cache_path, .data = json }); } fn readComponentCache( @@ -315,7 +327,8 @@ fn readComponentCache( cache_path: []const u8, global_components: *std.array_list.Managed(ClientComponentSerializable), ) !void { - const json = try std.fs.cwd().readFileAlloc(allocator, cache_path, 4 * 1024 * 1024); + const io = std.Io.Threaded.global_single_threaded.io(); + const json = try std.Io.Dir.cwd().readFileAlloc(io, cache_path, allocator, std.Io.Limit.limited(4 * 1024 * 1024)); defer allocator.free(json); const parsed = try std.json.parseFromSlice( []const ClientComponentSerializable, @@ -337,7 +350,8 @@ fn readComponentCache( } fn copyOnly(ctx: zli.CommandContext, source_path: []const u8, dest_dir: []const u8) !void { - const stat = std.fs.cwd().statFile(source_path) catch |err| switch (err) { + const io = std.Io.Threaded.global_single_threaded.io(); + const stat = std.Io.Dir.cwd().statFile(io, source_path, .{}) catch |err| switch (err) { error.IsDir => return try copyDirectory(ctx.allocator, source_path, dest_dir), else => return err, }; @@ -391,31 +405,38 @@ fn extractRouteFromPath(allocator: std.mem.Allocator, source_path: []const u8) ! /// Get the package root directory (where node_modules is located) /// This function finds package.json and returns its directory -fn getPackageRootDir(allocator: std.mem.Allocator) ![]const u8 { - const package_json_parsed = try jsutil.PackageJson.parse(allocator); - errdefer package_json_parsed.deinit(); - - // Extract pkg_path before deinit since it's manually allocated - const pkg_path = package_json_parsed.value.pkg_path orelse { - package_json_parsed.deinit(); - return error.PkgPathNotFound; - }; - - const pkg_rootdir = std.fs.path.dirname(pkg_path) orelse { - allocator.free(pkg_path); - package_json_parsed.deinit(); - // When package.json is in root, return empty string (current directory) - return try allocator.dupe(u8, ""); +/// Walk up from `start_dir` looking for a directory that contains a +/// package.json. Returns the absolute path to that directory (the package +/// root). The caller owns the returned slice. +fn findPackageRoot(allocator: std.mem.Allocator, io: std.Io, start_dir: []const u8) ![]const u8 { + // Resolve start_dir to an absolute path so the walk-up terminates. + var abs_buf: ?[]u8 = null; + defer if (abs_buf) |b| allocator.free(b); + + var dir_path: []const u8 = if (std.fs.path.isAbsolute(start_dir)) start_dir else blk: { + const cwd_abs = try std.process.currentPathAlloc(io, allocator); + defer allocator.free(cwd_abs); + const joined = try std.fs.path.join(allocator, &.{ cwd_abs, start_dir }); + abs_buf = joined; + break :blk joined; }; - const result = try allocator.dupe(u8, pkg_rootdir); + const cwd = std.Io.Dir.cwd(); + while (true) { + const candidate = try std.fs.path.join(allocator, &.{ dir_path, "package.json" }); + defer allocator.free(candidate); - // Free pkg_path manually since it's not part of the JSON structure - // and won't be freed by package_json_parsed.deinit() - allocator.free(pkg_path); - package_json_parsed.deinit(); + if (cwd.statFile(io, candidate, .{})) |_| { + return try allocator.dupe(u8, dir_path); + } else |err| switch (err) { + error.FileNotFound => {}, + else => return err, + } - return result; + const parent = std.fs.path.dirname(dir_path) orelse return error.PackageJsonNotFound; + if (parent.len == dir_path.len) return error.PackageJsonNotFound; + dir_path = parent; + } } fn getBasename(path: []const u8) []const u8 { @@ -431,9 +452,9 @@ fn getBasename(path: []const u8) []const u8 { /// Escapes backslashes in a path string for use in Zig string literals. /// On Windows, backslashes need to be escaped as \\ in string literals. fn escapePathForZigString(allocator: std.mem.Allocator, path: []const u8) ![]const u8 { - var result = std.array_list.Managed(u8).init(allocator); + var result = std.Io.Writer.Allocating.init(allocator); errdefer result.deinit(); - const writer = result.writer(); + const writer = &result.writer; for (path) |byte| { if (byte == '\\') { @@ -478,9 +499,9 @@ fn relativePath(allocator: std.mem.Allocator, base: []const u8, target: []const target_normalized = target[0 .. target.len - sep.len]; } - var base_parts = std.ArrayList([]const u8){}; + var base_parts = std.ArrayList([]const u8).empty; defer base_parts.deinit(allocator); - var target_parts = std.ArrayList([]const u8){}; + var target_parts = std.ArrayList([]const u8).empty; defer target_parts.deinit(allocator); var base_iter = std.mem.splitScalar(u8, base_normalized, std.fs.path.sep); @@ -503,7 +524,7 @@ fn relativePath(allocator: std.mem.Allocator, base: []const u8, target: []const common_len += 1; } - var result = std.ArrayList(u8){}; + var result = std.ArrayList(u8).empty; defer result.deinit(allocator); var i = common_len; @@ -574,13 +595,15 @@ fn copyFileToDir( source_file: []const u8, dest_dir: []const u8, ) !void { + const io = std.Io.Threaded.global_single_threaded.io(); const dest_file = try std.fs.path.join(allocator, &.{ dest_dir, std.fs.path.basename(source_file) }); defer allocator.free(dest_file); - std.fs.cwd().makePath(dest_dir) catch |err| switch (err) { + std.Io.Dir.cwd().createDirPath(io, dest_dir) catch |err| switch (err) { error.PathAlreadyExists => {}, else => return err, }; - try std.fs.cwd().copyFile(source_file, std.fs.cwd(), dest_file, .{}); + const dest_file_basename = std.fs.path.basename(dest_file); + try std.Io.Dir.cwd().copyFile(source_file, std.Io.Dir.cwd(), dest_file_basename, io, .{}); } /// Copy a directory recursively from source to destination @@ -589,21 +612,22 @@ fn copyDirectory( source_dir: []const u8, dest_dir: []const u8, ) !void { - var source = try std.fs.cwd().openDir(source_dir, .{ .iterate = true }); - defer source.close(); + const io = std.Io.Threaded.global_single_threaded.io(); + var source = try std.Io.Dir.cwd().openDir(io, source_dir, .{ .iterate = true }); + defer source.close(io); - std.fs.cwd().makePath(dest_dir) catch |err| switch (err) { + std.Io.Dir.cwd().createDirPath(io, dest_dir) catch |err| switch (err) { error.PathAlreadyExists => {}, else => return err, }; - var dest = try std.fs.cwd().openDir(dest_dir, .{}); - defer dest.close(); + var dest = try std.Io.Dir.cwd().openDir(io, dest_dir, .{}); + defer dest.close(io); var walker = try source.walk(allocator); defer walker.deinit(); - while (try walker.next()) |entry| { + while (try walker.next(io)) |entry| { const src_path = try std.fs.path.join(allocator, &.{ source_dir, entry.path }); defer allocator.free(src_path); @@ -613,15 +637,15 @@ fn copyDirectory( switch (entry.kind) { .file => { if (std.fs.path.dirname(dst_path)) |parent| { - std.fs.cwd().makePath(parent) catch |err| switch (err) { + std.Io.Dir.cwd().createDirPath(io, parent) catch |err| switch (err) { error.PathAlreadyExists => {}, else => return err, }; } - try std.fs.cwd().copyFile(src_path, std.fs.cwd(), dst_path, .{}); + try std.Io.Dir.cwd().copyFile(src_path, std.Io.Dir.cwd(), dst_path, io, .{}); }, .directory => { - std.fs.cwd().makePath(dst_path) catch |err| switch (err) { + std.Io.Dir.cwd().createDirPath(io, dst_path) catch |err| switch (err) { error.PathAlreadyExists => {}, else => return err, }; @@ -636,7 +660,7 @@ const ClientComponentSerializable = struct { type: zx.Ast.ClientComponentMetadat fn genClientComponents(allocator: std.mem.Allocator, components: []const ClientComponentSerializable, output_dir: []const u8, verbose: bool) !void { _ = verbose; // Generate Zig array literal contents (without outer array declaration) - var aw = std.io.Writer.Allocating.init(allocator); + var aw = std.Io.Writer.Allocating.init(allocator); defer aw.deinit(); std.zon.stringify.serialize(components, .{ .whitespace = true }, &aw.writer) catch @panic("OOM"); @@ -703,39 +727,39 @@ fn genClientComponents(allocator: std.mem.Allocator, components: []const ClientC const cmps_client_path = try std.fs.path.join(allocator, &.{ output_dir, "components.zig" }); defer allocator.free(cmps_client_path); - try std.fs.cwd().writeFile(.{ + const io = std.Io.Threaded.global_single_threaded.io(); + try std.Io.Dir.cwd().writeFile(io, .{ .sub_path = cmps_client_path, .data = cmps_client_z, }); } -fn genReactComponents(allocator: std.mem.Allocator, components: []const ClientComponentSerializable, output_dir: []const u8, verbose: bool) !void { +fn genReactComponents(allocator: std.mem.Allocator, components: []const ClientComponentSerializable, output_dir: []const u8, input_dir: []const u8, verbose: bool) !void { if (components.len == 0) return; var json_str = std.json.Stringify.valueAlloc(allocator, components, .{ .whitespace = .indent_2, }) catch @panic("OOM"); - errdefer allocator.free(json_str); + defer allocator.free(json_str); // Replace all instances of "@ and @" with empty string const placeHolder_start = "\"@"; const placeHolder_end = "@\""; while (std.mem.indexOf(u8, json_str, placeHolder_start)) |index| { - const old_json_str = json_str; const before = json_str[0..index]; const after = json_str[index + placeHolder_start.len ..]; - json_str = try std.mem.concat(allocator, u8, &.{ before, "", after }); - allocator.free(old_json_str); + const new_json_str = try std.mem.concat(allocator, u8, &.{ before, "", after }); + allocator.free(json_str); + json_str = new_json_str; } while (std.mem.indexOf(u8, json_str, placeHolder_end)) |index| { - const old_json_str = json_str; const before = json_str[0..index]; const after = json_str[index + placeHolder_end.len ..]; - json_str = try std.mem.concat(allocator, u8, &.{ before, "", after }); - allocator.free(old_json_str); + const new_json_str = try std.mem.concat(allocator, u8, &.{ before, "", after }); + allocator.free(json_str); + json_str = new_json_str; } - defer allocator.free(json_str); const main_csr_react = @embedFile("./transpile/template/components.ts"); const placeholder = "`{[ZX_COMPONENTS]s}`"; @@ -752,17 +776,20 @@ fn genReactComponents(allocator: std.mem.Allocator, components: []const ClientCo _ = output_dir; if (verbose) { - log.debug("node_modules path: {s}", .{"node_modules"}); - log.debug("ziex path: {s}", .{"ziex"}); - log.debug("components.ts path: {s}", .{"components.ts"}); + log.debug("components input dir: {s}", .{input_dir}); } - const pkg_rootdir = try getPackageRootDir(allocator); + // Locate the package root by walking up from the input directory looking + // for a package.json. The generated registry must live under the + // package's node_modules so bare-specifier imports like + // `@ziex/components` resolve correctly. + const io = std.Io.Threaded.global_single_threaded.io(); + const pkg_rootdir = try findPackageRoot(allocator, io, input_dir); defer allocator.free(pkg_rootdir); - const ziex_dir = try std.fs.path.join(allocator, &.{ pkg_rootdir, "node_modules", "@ziex/components" }); + const ziex_dir = try std.fs.path.join(allocator, &.{ pkg_rootdir, "node_modules", "@ziex", "components" }); defer allocator.free(ziex_dir); - std.fs.cwd().makePath(ziex_dir) catch |err| switch (err) { + std.Io.Dir.cwd().createDirPath(io, ziex_dir) catch |err| switch (err) { error.PathAlreadyExists => {}, else => return err, }; @@ -770,7 +797,7 @@ fn genReactComponents(allocator: std.mem.Allocator, components: []const ClientCo const main_csr_react_path = try std.fs.path.join(allocator, &.{ ziex_dir, "index.ts" }); defer allocator.free(main_csr_react_path); - try std.fs.cwd().writeFile(.{ + try std.Io.Dir.cwd().writeFile(io, .{ .sub_path = main_csr_react_path, .data = main_csr_react_z, }); @@ -823,7 +850,8 @@ fn genRoutes(allocator: std.mem.Allocator, output_dir: []const u8, rootdir: ?[]c defer allocator.free(pages_dir); const has_pages = blk: { - std.fs.cwd().access(pages_dir, .{}) catch break :blk false; + const io = std.Io.Threaded.global_single_threaded.io(); + std.Io.Dir.cwd().access(io, pages_dir, .{}) catch break :blk false; break :blk true; }; @@ -848,7 +876,8 @@ fn genRoutes(allocator: std.mem.Allocator, output_dir: []const u8, rootdir: ?[]c defer allocator.free(routes_dir); const has_routes = blk: { - std.fs.cwd().access(routes_dir, .{}) catch break :blk false; + const io = std.Io.Threaded.global_single_threaded.io(); + std.Io.Dir.cwd().access(io, routes_dir, .{}) catch break :blk false; break :blk true; }; @@ -865,9 +894,9 @@ fn genRoutes(allocator: std.mem.Allocator, output_dir: []const u8, rootdir: ?[]c return error.NoPagesOrRoutes; } - var content = std.array_list.Managed(u8).init(allocator); + var content = std.Io.Writer.Allocating.init(allocator); defer content.deinit(); - const writer = content.writer(); + const writer = &content.writer; try writer.writeAll("pub const routes = [_]zx.server.ServerMeta.Route{\n"); for (routes.items) |route| { @@ -881,9 +910,9 @@ fn genRoutes(allocator: std.mem.Allocator, output_dir: []const u8, rootdir: ?[]c // Convert to relative path using std.fs.path.relative and escape for Zig string literal var path_to_use: []const u8 = meta_rootdir; var path_allocated = false; - if (std.fs.cwd().realpathAlloc(allocator, ".")) |cwd| { + if (std.fs.path.resolve(allocator, &.{"."})) |cwd| { defer allocator.free(cwd); - if (std.fs.path.relative(allocator, cwd, meta_rootdir)) |relative| { + if (std.fs.path.relative(allocator, cwd, null, cwd, meta_rootdir)) |relative| { path_to_use = relative; path_allocated = true; } else |_| {} @@ -911,7 +940,7 @@ fn genRoutes(allocator: std.mem.Allocator, output_dir: []const u8, rootdir: ?[]c const meta_path = try std.fs.path.join(allocator, &.{ output_dir, "meta.zig" }); defer allocator.free(meta_path); - const content_z = try allocator.dupeZ(u8, content.items); + const content_z = try allocator.dupeZ(u8, content.written()); defer allocator.free(content_z); var ast = try std.zig.Ast.parse(allocator, content_z, .zig); defer ast.deinit(allocator); @@ -923,7 +952,8 @@ fn genRoutes(allocator: std.mem.Allocator, output_dir: []const u8, rootdir: ?[]c const rendered_zig_source = try ast.renderAlloc(allocator); defer allocator.free(rendered_zig_source); - try std.fs.cwd().writeFile(.{ + const io = std.Io.Threaded.global_single_threaded.io(); + try std.Io.Dir.cwd().writeFile(io, .{ .sub_path = meta_path, .data = rendered_zig_source, }); @@ -1022,32 +1052,38 @@ fn scanPagesRecursive( defer allocator.free(proxy_file_path); const has_page = blk: { - std.fs.cwd().access(page_path, .{}) catch break :blk false; + const io = std.Io.Threaded.global_single_threaded.io(); + std.Io.Dir.cwd().access(io, page_path, .{}) catch break :blk false; break :blk true; }; const has_layout = blk: { - std.fs.cwd().access(layout_path, .{}) catch break :blk false; + const io = std.Io.Threaded.global_single_threaded.io(); + std.Io.Dir.cwd().access(io, layout_path, .{}) catch break :blk false; break :blk true; }; const has_notfound = blk: { - std.fs.cwd().access(notfound_path, .{}) catch break :blk false; + const io = std.Io.Threaded.global_single_threaded.io(); + std.Io.Dir.cwd().access(io, notfound_path, .{}) catch break :blk false; break :blk true; }; const has_error = blk: { - std.fs.cwd().access(error_path, .{}) catch break :blk false; + const io = std.Io.Threaded.global_single_threaded.io(); + std.Io.Dir.cwd().access(io, error_path, .{}) catch break :blk false; break :blk true; }; const has_route = blk: { - std.fs.cwd().access(route_file_path, .{}) catch break :blk false; + const io = std.Io.Threaded.global_single_threaded.io(); + std.Io.Dir.cwd().access(io, route_file_path, .{}) catch break :blk false; break :blk true; }; const has_proxy = blk: { - std.fs.cwd().access(proxy_file_path, .{}) catch break :blk false; + const io = std.Io.Threaded.global_single_threaded.io(); + std.Io.Dir.cwd().access(io, proxy_file_path, .{}) catch break :blk false; break :blk true; }; @@ -1133,11 +1169,12 @@ fn scanPagesRecursive( } } - var dir = try std.fs.cwd().openDir(current_dir, .{ .iterate = true }); - defer dir.close(); + const io = std.Io.Threaded.global_single_threaded.io(); + var dir = try std.Io.Dir.cwd().openDir(io, current_dir, .{ .iterate = true }); + defer dir.close(io); var iter = dir.iterate(); - while (try iter.next()) |entry| { + while (try iter.next(io)) |entry| { if (entry.kind != .directory) continue; if (std.mem.eql(u8, entry.name, ".zx")) continue; @@ -1177,12 +1214,14 @@ fn scanRoutesRecursive( defer allocator.free(proxy_file_path); const has_route = blk: { - std.fs.cwd().access(route_file_path, .{}) catch break :blk false; + const io = std.Io.Threaded.global_single_threaded.io(); + std.Io.Dir.cwd().access(io, route_file_path, .{}) catch break :blk false; break :blk true; }; const has_proxy = blk: { - std.fs.cwd().access(proxy_file_path, .{}) catch break :blk false; + const io = std.Io.Threaded.global_single_threaded.io(); + std.Io.Dir.cwd().access(io, proxy_file_path, .{}) catch break :blk false; break :blk true; }; @@ -1220,11 +1259,12 @@ fn scanRoutesRecursive( } // Recurse into subdirectories - var dir = std.fs.cwd().openDir(current_dir, .{ .iterate = true }) catch return; - defer dir.close(); + const io = std.Io.Threaded.global_single_threaded.io(); + var dir = std.Io.Dir.cwd().openDir(io, current_dir, .{ .iterate = true }) catch return; + defer dir.close(io); var iter = dir.iterate(); - while (try iter.next()) |entry| { + while (try iter.next(io)) |entry| { if (entry.kind != .directory) continue; if (std.mem.eql(u8, entry.name, ".zx")) continue; @@ -1252,10 +1292,12 @@ fn transpileFile( source_path: []const u8, output_path: []const u8, ) !void { - const source = try std.fs.cwd().readFileAlloc( - allocator, + const io = std.Io.Threaded.global_single_threaded.io(); + const source = try std.Io.Dir.cwd().readFileAlloc( + io, source_path, - std.math.maxInt(usize), + allocator, + std.Io.Limit.limited(std.math.maxInt(usize)), ); defer allocator.free(source); @@ -1266,7 +1308,7 @@ fn transpileFile( var relative_source_path: []const u8 = source_path; var rel_path_allocated = false; if (std.fs.path.isAbsolute(source_path)) { - if (std.fs.cwd().realpathAlloc(allocator, ".")) |cwd| { + if (std.fs.path.resolve(allocator, &.{"."})) |cwd| { defer allocator.free(cwd); if (relativePath(allocator, cwd, source_path)) |rel| { relative_source_path = rel; @@ -1321,10 +1363,10 @@ fn transpileFile( }, .react => { // For .react components, the path is relative to project root (e.g., site/pages/...). - const abs_component_path = std.fs.cwd().realpathAlloc(allocator, component.path) catch |err| blk: { + const abs_component_path = std.fs.path.resolve(allocator, &.{component.path}) catch |err| blk: { // Fallback to resolving relative to CWD if realpath fails std.log.warn("Warning: could not get realpath for {s}: {}\n", .{ component.path, err }); - const cwd = std.fs.cwd().realpathAlloc(allocator, ".") catch |err2| { + const cwd = std.fs.path.resolve(allocator, &.{"."}) catch |err2| { std.log.err("Error: realpath(.) failed: {}\n", .{err2}); break :blk try allocator.dupe(u8, component.path); }; @@ -1354,13 +1396,14 @@ fn transpileFile( } if (std.fs.path.dirname(output_path)) |dir| { - std.fs.cwd().makePath(dir) catch |err| switch (err) { + std.Io.Dir.cwd().createDirPath(io, dir) catch |err| switch (err) { error.PathAlreadyExists => {}, else => return err, }; } - try std.fs.cwd().writeFile(.{ + + try std.Io.Dir.cwd().writeFile(io, .{ .sub_path = output_path, .data = result.zig_source, }); @@ -1380,7 +1423,7 @@ fn transpileFile( ); defer allocator.free(sourcemap_json); - try std.fs.cwd().writeFile(.{ + try std.Io.Dir.cwd().writeFile(io, .{ .sub_path = map_path, .data = sourcemap_json, }); @@ -1409,10 +1452,11 @@ fn transpileFile( defer allocator.free(inline_comment); // Append to the output file - var file = try std.fs.cwd().openFile(output_path, .{ .mode = .read_write }); - defer file.close(); - try file.seekFromEnd(0); - try file.writeAll(inline_comment); + + var file = try std.Io.Dir.cwd().openFile(io, output_path, .{ .mode = .read_write }); + defer file.close(io); + const len = try file.length(io); + try file.writePositionalAll(io, inline_comment, len); if (opts.verbose) std.debug.print("Inlined sourcemap in: {s}\n", .{output_path}); }, @@ -1432,8 +1476,9 @@ fn transpileDirectory( var task = progress.start("Transpiling .zx files", 0); defer task.end(); - var dir = try std.fs.cwd().openDir(opts.path, .{ .iterate = true }); - defer dir.close(); + const io = std.Io.Threaded.global_single_threaded.io(); + var dir = try std.Io.Dir.cwd().openDir(io, opts.path, .{ .iterate = true }); + defer dir.close(io); const output_dir_relative = try getOutputDirRelativePath(allocator, opts.path, opts.outdir); defer if (output_dir_relative) |rel| allocator.free(rel); @@ -1443,11 +1488,11 @@ fn transpileDirectory( var walker = try dir.walk(allocator); defer walker.deinit(); - while (try walker.next()) |entry| { + while (try walker.next(io)) |entry| { task.completeOne(); var actual_kind = entry.kind; if (entry.kind == .sym_link) { - const entry_stat = dir.statFile(entry.path) catch continue; + const entry_stat = dir.statFile(io, entry.path, .{}) catch continue; actual_kind = entry_stat.kind; } @@ -1472,7 +1517,7 @@ fn transpileDirectory( if (is_zx or is_mdzx) { const output_rel_path = try std.mem.concat(allocator, u8, &.{ - entry.path[0 .. entry.path.len - (if (is_zx) ".zx" else ".mdzx").len], + entry.path[0 .. entry.path.len - (if (is_zx) @as([]const u8, ".zx") else @as([]const u8, ".mdzx")).len], ".zig", }); defer allocator.free(output_rel_path); @@ -1481,13 +1526,13 @@ fn transpileDirectory( defer allocator.free(output_path); // Track this input for the dep file (absolute path for Make format) - const abs_input = std.fs.cwd().realpathAlloc(allocator, input_path) catch + const abs_input = std.fs.path.resolve(allocator, &.{input_path}) catch try allocator.dupe(u8, input_path); try input_files.append(abs_input); // Scan for @embedFile references and track them as dependencies const source_dir = std.fs.path.dirname(input_path) orelse "."; - if (std.fs.cwd().readFileAlloc(allocator, input_path, 4 * 1024 * 1024)) |source| { + if (std.Io.Dir.cwd().readFileAlloc(io, input_path, allocator, std.Io.Limit.limited(4 * 1024 * 1024))) |source| { defer allocator.free(source); try collectEmbedFileDeps(allocator, input_files, source, source_dir); } else |_| {} @@ -1502,10 +1547,10 @@ fn transpileDirectory( // Check if output is up-to-date (mtime comparison + cache file existence) const should_skip = blk: { - const input_stat = std.fs.cwd().statFile(input_path) catch break :blk false; - const cache_stat = std.fs.cwd().statFile(cache_out_path) catch break :blk false; - std.fs.cwd().access(cache_path, .{}) catch break :blk false; - break :blk cache_stat.mtime >= input_stat.mtime; + const input_stat = std.Io.Dir.cwd().statFile(io, input_path, .{}) catch break :blk false; + const cache_stat = std.Io.Dir.cwd().statFile(io, cache_out_path, .{}) catch break :blk false; + std.Io.Dir.cwd().access(io, cache_path, .{}) catch break :blk false; + break :blk cache_stat.mtime.nanoseconds >= input_stat.mtime.nanoseconds; }; if (should_skip) { @@ -1514,9 +1559,9 @@ fn transpileDirectory( }; if (opts.cache_dir) |_| { if (std.fs.path.dirname(output_path)) |parent_dir| { - std.fs.cwd().makePath(parent_dir) catch {}; + std.Io.Dir.cwd().createDirPath(io, parent_dir) catch {}; } - std.fs.cwd().copyFile(cache_out_path, std.fs.cwd(), output_path, .{}) catch |err| { + std.Io.Dir.cwd().copyFile(cache_out_path, std.Io.Dir.cwd(), output_path, io, .{}) catch |err| { std.debug.print("Warning: Failed to copy cached file {s} to {s}: {}\n", .{ cache_out_path, output_path, err }); }; } @@ -1531,9 +1576,9 @@ fn transpileDirectory( if (opts.cache_dir) |_| { if (std.fs.path.dirname(cache_out_path)) |parent_dir| { - std.fs.cwd().makePath(parent_dir) catch {}; + std.Io.Dir.cwd().createDirPath(io, parent_dir) catch {}; } - std.fs.cwd().copyFile(output_path, std.fs.cwd(), cache_out_path, .{}) catch |err| { + std.Io.Dir.cwd().copyFile(output_path, std.Io.Dir.cwd(), cache_out_path, io, .{}) catch |err| { std.debug.print("Warning: Failed to update cache file {s}: {}\n", .{ cache_out_path, err }); }; } @@ -1549,7 +1594,7 @@ fn transpileDirectory( defer allocator.free(zx_output_path); if (std.fs.path.dirname(zx_output_path)) |parent| { - std.fs.cwd().makePath(parent) catch |err| switch (err) { + std.Io.Dir.cwd().createDirPath(io, parent) catch |err| switch (err) { error.PathAlreadyExists => {}, else => { std.debug.print("Error creating directory {s}: {}\n", .{ parent, err }); @@ -1558,7 +1603,7 @@ fn transpileDirectory( }; } - std.fs.cwd().copyFile(input_path, std.fs.cwd(), zx_output_path, .{}) catch |err| { + std.Io.Dir.cwd().copyFile(input_path, std.Io.Dir.cwd(), zx_output_path, io, .{}) catch |err| { std.debug.print("Warning: Could not copy .zx source {s}: {}\n", .{ input_path, err }); continue; }; @@ -1597,7 +1642,7 @@ fn transpileDirectory( defer allocator.free(output_path); if (std.fs.path.dirname(output_path)) |parent| { - std.fs.cwd().makePath(parent) catch |err| switch (err) { + std.Io.Dir.cwd().createDirPath(io, parent) catch |err| switch (err) { error.PathAlreadyExists => {}, else => { std.debug.print("Error creating directory {s}: {}\n", .{ parent, err }); @@ -1606,10 +1651,10 @@ fn transpileDirectory( }; } - try std.fs.cwd().copyFile(input_path, std.fs.cwd(), output_path, .{}); + try std.Io.Dir.cwd().copyFile(input_path, std.Io.Dir.cwd(), output_path, io, .{}); if (opts.verbose) std.debug.print("Copied: {s} -> {s}\n", .{ input_path, output_path }); - const abs_input = std.fs.cwd().realpathAlloc(allocator, input_path) catch + const abs_input = std.fs.path.resolve(allocator, &.{input_path}) catch try allocator.dupe(u8, input_path); try input_files.append(abs_input); } @@ -1629,8 +1674,9 @@ const TranspileOptions = struct { }; fn transpileCommand(allocator: std.mem.Allocator, opts: TranspileOptions) !void { + const io = std.Io.Threaded.global_single_threaded.io(); // Start root progress for the entire transpile operation - var progress = std.Progress.start(.{ .root_name = "Transpile" }); + var progress = std.Progress.start(io, .{ .root_name = "Transpile" }); defer progress.end(); var all_client_cmps = std.array_list.Managed(ClientComponentSerializable).init(allocator); @@ -1651,8 +1697,18 @@ fn transpileCommand(allocator: std.mem.Allocator, opts: TranspileOptions) !void input_files.deinit(); } - const stat = std.fs.cwd().statFile(opts.path) catch |err| switch (err) { - error.IsDir => std.fs.File.Stat{ .kind = .directory, .size = 0, .mode = 0, .atime = 0, .mtime = 0, .ctime = 0, .inode = 0 }, + const stat = std.Io.Dir.cwd().statFile(io, opts.path, .{}) catch |err| switch (err) { + error.IsDir => std.Io.File.Stat{ + .inode = 0, + .size = 0, + .nlink = 0, + .permissions = .default_dir, + .kind = .directory, + .atime = null, + .mtime = .{ .nanoseconds = 0 }, + .ctime = .{ .nanoseconds = 0 }, + .block_size = 4096, + }, else => { std.debug.print("Error: Could not access path '{s}': {}\n", .{ opts.path, err }); return err; @@ -1718,7 +1774,7 @@ fn transpileCommand(allocator: std.mem.Allocator, opts: TranspileOptions) !void } // @rendering={.react} - genReactComponents(allocator, react_cmps.items, opts.outdir, opts.verbose) catch |err| { + genReactComponents(allocator, react_cmps.items, opts.outdir, opts.path, opts.verbose) catch |err| { std.debug.print("Warning: Failed to generate main.tsx: {}\n", .{err}); }; diff --git a/src/cli/update.zig b/src/cli/update.zig index 8674bb0f..d96ebd9e 100644 --- a/src/cli/update.zig +++ b/src/cli/update.zig @@ -10,6 +10,7 @@ pub fn register(writer: *std.Io.Writer, reader: *std.Io.Reader, allocator: std.m } fn update(ctx: zli.CommandContext) !void { + const app = AppContext.from(&ctx); const version = ctx.flag("version", []const u8); const version_str = if (std.mem.eql(u8, version, "latest")) "" else try std.fmt.allocPrint(ctx.allocator, "#v{s}", .{version}); defer ctx.allocator.free(version_str); @@ -17,10 +18,8 @@ fn update(ctx: zli.CommandContext) !void { const fetch_uri = try std.fmt.allocPrint(ctx.allocator, "git+{s}{s}", .{ zx.info.repository, version_str }); defer ctx.allocator.free(fetch_uri); - var system = std.process.Child.init(&.{ cli_options.zig_exe, "fetch", "--save", fetch_uri }, ctx.allocator); - try system.spawn(); - - const term = try system.wait(); + var system = try std.process.spawn(app.io, .{ .argv = &.{ cli_options.zig_exe, "fetch", "--save", fetch_uri } }); + const term = try system.wait(app.io); _ = term; } @@ -34,5 +33,6 @@ const version_flag = zli.Flag{ const std = @import("std"); const zli = @import("zli"); +const AppContext = @import("shared/context.zig").AppContext; const zx = @import("zx"); const cli_options = @import("cli_options"); diff --git a/src/cli/upgrade.zig b/src/cli/upgrade.zig index b8f5a34a..d8903698 100644 --- a/src/cli/upgrade.zig +++ b/src/cli/upgrade.zig @@ -10,6 +10,7 @@ pub fn register(writer: *std.Io.Writer, reader: *std.Io.Reader, allocator: std.m } fn upgrade(ctx: zli.CommandContext) !void { + const app = AppContext.from(&ctx); const version = ctx.flag("version", []const u8); var maybe_cmd_str: ?[:0]u8 = null; @@ -37,10 +38,8 @@ fn upgrade(ctx: zli.CommandContext) !void { else => return error.UnsupportedOS, }; - var system = std.process.Child.init(&install_cmd, ctx.allocator); - try system.spawn(); - - const term = try system.wait(); + var system = try std.process.spawn(app.io, .{ .argv = &install_cmd }); + const term = try system.wait(app.io); _ = term; // try ctx.writer.print("Upgraded to: ", .{}); @@ -59,5 +58,6 @@ const version_flag = zli.Flag{ const std = @import("std"); const zli = @import("zli"); +const AppContext = @import("shared/context.zig").AppContext; const zx = @import("zx"); const builtin = @import("builtin"); diff --git a/src/core/fmt/html/Ast.zig b/src/core/fmt/html/Ast.zig index a45f5dcd..6899d708 100644 --- a/src/core/fmt/html/Ast.zig +++ b/src/core/fmt/html/Ast.zig @@ -1270,7 +1270,7 @@ fn renderMultilineContent(content: []const u8, base_indent: u32, arena: Allocato }; // Render to a buffer at indentation 0 (HTML renderer will add its own indentation) - var buffer_writer: std.io.Writer.Allocating = .init(arena); + var buffer_writer: std.Io.Writer.Allocating = .init(arena); defer buffer_writer.deinit(); const buffer_writer_ptr: *Writer = @ptrCast(&buffer_writer.writer); const render_result: anyerror!void = wrapped_ast.render(arena, wrapped_content, buffer_writer_ptr); @@ -1435,7 +1435,7 @@ const LineIndentWriter = struct { }; pub fn render(ast: Ast, arena: Allocator, src: []const u8, w: *Writer) !void { - var aw = std.io.Writer.Allocating.init(arena); + var aw = std.Io.Writer.Allocating.init(arena); defer aw.deinit(); try ast.printErrors(src, null, &aw.writer); const errors = aw.written(); diff --git a/src/core/sourcemap.zig b/src/core/sourcemap.zig index c19971f4..4eba11e0 100644 --- a/src/core/sourcemap.zig +++ b/src/core/sourcemap.zig @@ -171,10 +171,10 @@ pub const SourceMap = struct { source_content: []const u8, generated_content: ?[]const u8, ) ![]const u8 { - var json = std.array_list.Managed(u8).init(allocator); + var json = std.Io.Writer.Allocating.init(allocator); errdefer json.deinit(); - const writer = json.writer(); + const writer = &json.writer; try writer.writeAll("{\"version\":3,\"file\":\""); try escapeJSONString(writer, generated_file); try writer.writeAll("\",\"sources\":[\""); diff --git a/src/lsp/main.zig b/src/lsp/main.zig index e7d2dd54..ccc6e86a 100644 --- a/src/lsp/main.zig +++ b/src/lsp/main.zig @@ -15,7 +15,7 @@ const ByteRange = struct { var debug_allocator: std.heap.DebugAllocator(.{}) = .init; -pub fn main() !void { +pub fn main(init: std.process.Init) !void { const gpa, const is_debug = switch (builtin.os.tag) { .wasi, .freestanding => .{ std.heap.wasm_allocator, false }, else => switch (builtin.mode) { @@ -34,29 +34,31 @@ pub fn main() !void { const transport: *lsp.Transport = &stdio_transport.transport; const global_cache_path: ?[]const u8 = blk: { - const home = std.process.getEnvVarOwned(gpa, "HOME") catch break :blk null; + const home = init.minimal.environ.getAlloc(gpa, "HOME") catch break :blk null; defer gpa.free(home); const cache_suffix = if (builtin.os.tag == .macos) "Library/Caches/zls" else ".cache/zls"; break :blk std.fs.path.join(gpa, &.{ home, cache_suffix }) catch null; }; defer if (global_cache_path) |p| gpa.free(p); - var config = zls.Config{ + var cm = zls.configuration.Manager.init(init.io, gpa, init.environ_map) catch unreachable; + + try cm.setConfiguration(.frontend, &.{ .global_cache_path = global_cache_path, - // .enable_build_on_save = false, - // .prefer_ast_check_as_child_process = false, - }; + }); - const zls_server = zls.Server.create(.{ + const zls_server = try zls.Server.create(.{ + .io = init.io, .allocator = gpa, .transport = transport, - .config = &config, - }) catch unreachable; + .config_manager = &cm, + }); - var handler: Handler = .init(gpa, zls_server, transport); + var handler: Handler = .init(gpa, zls_server, transport, init.io); defer handler.deinit(); lsp.basic_server.run( + init.io, gpa, transport, &handler, @@ -84,14 +86,16 @@ pub const Handler = struct { allocator: std.mem.Allocator, zls: *zls.Server, transport: *lsp.Transport, + io: std.Io, offset_encoding: lsp.offsets.Encoding, zx_files: std.StringHashMap(ZxFileState), - fn init(allocator: std.mem.Allocator, zls_server: *zls.Server, transport: *lsp.Transport) Handler { + fn init(allocator: std.mem.Allocator, zls_server: *zls.Server, transport: *lsp.Transport, io: std.Io) Handler { return .{ .allocator = allocator, .zls = zls_server, .transport = transport, + .io = io, .offset_encoding = .@"utf-16", .zx_files = std.StringHashMap(ZxFileState).init(allocator), }; @@ -113,23 +117,16 @@ pub const Handler = struct { } fn toZigUri(allocator: std.mem.Allocator, zx_uri: []const u8) ![]const u8 { - const base = zx_uri[0 .. zx_uri.len - 3]; - return std.fmt.allocPrint(allocator, "{s}.zig", .{base}); + return allocator.dupe(u8, zx_uri); } - /// Get the ZLS-facing URI for a document (maps .zx → .zig, passes others through). fn getZlsUri(handler: *Handler, uri: []const u8) []const u8 { - if (handler.zx_files.get(uri)) |state| return state.zig_uri; + _ = handler; return uri; } fn getEditorUri(handler: *Handler, uri: []const u8) []const u8 { - var it = handler.zx_files.iterator(); - while (it.next()) |entry| { - if (std.mem.eql(u8, entry.value_ptr.zig_uri, uri)) { - return entry.key_ptr.*; - } - } + _ = handler; return uri; } @@ -185,35 +182,6 @@ pub const Handler = struct { return remapped; } - /// Rewrite `@import("*.zx")` → `@import("*.zig")` so ZLS can resolve cross-file imports. - fn rewriteZxImports(allocator: std.mem.Allocator, source: []const u8) ?[]const u8 { - const needle = "@import(\""; - var buf = std.ArrayList(u8).empty; - var copied_to: usize = 0; - var search_from: usize = 0; - var found_any = false; - - while (std.mem.indexOfPos(u8, source, search_from, needle)) |start| { - const path_start = start + needle.len; - if (std.mem.indexOfPos(u8, source, path_start, "\")")) |path_end| { - const import_path = source[path_start..path_end]; - if (std.mem.endsWith(u8, import_path, ".zx")) { - found_any = true; - const ext_start = path_end - 3; // points to ".zx" - buf.appendSlice(allocator, source[copied_to..ext_start]) catch return null; - buf.appendSlice(allocator, ".zig") catch return null; - copied_to = path_end; // resume copying after ".zx" - } - search_from = path_end + 2; - } else break; - } - - if (!found_any) return null; - - buf.appendSlice(allocator, source[copied_to..]) catch return null; - return buf.toOwnedSlice(allocator) catch null; - } - /// Resolve file:// URI to a filesystem path (strips the file:// prefix). fn uriToPath(uri: []const u8) ?[]const u8 { if (std.mem.startsWith(u8, uri, "file://")) return uri[7..]; @@ -248,7 +216,7 @@ pub const Handler = struct { const resolved_path = switch (builtin.os.tag) { .wasi, .freestanding => handler.allocator.dupe(u8, joined) catch return, - else => std.fs.cwd().realpathAlloc(handler.allocator, joined) catch return, + else => std.Io.Dir.cwd().realPathFileAlloc(handler.io, joined, handler.allocator) catch return, }; defer handler.allocator.free(resolved_path); @@ -257,7 +225,7 @@ pub const Handler = struct { if (handler.zx_files.contains(zx_uri)) return; - const content = std.fs.cwd().readFileAlloc(handler.allocator, resolved_path, 4 * 1024 * 1024) catch return; + const content = std.Io.Dir.cwd().readFileAlloc(handler.io, resolved_path, handler.allocator, .limited(4 * 1024 * 1024)) catch return; defer handler.allocator.free(content); handler.storeAndDiagnose(zx_uri, content); @@ -272,17 +240,14 @@ pub const Handler = struct { const zls_text: []const u8 = if (parse_result) |r| r.zig_source else content; - const rewritten = rewriteZxImports(handler.allocator, zls_text) orelse zls_text; - defer if (rewritten.ptr != zls_text.ptr) handler.allocator.free(rewritten); - handler.openZxImportsInZls(arena, zx_uri, content); handler.zls.sendNotificationSync(arena, "textDocument/didOpen", .{ .textDocument = .{ .uri = zig_uri, - .languageId = "zig", + .languageId = .{ .custom_value = "zig" }, .version = @as(i32, 0), - .text = rewritten, + .text = zls_text, }, }) catch {}; } @@ -367,12 +332,12 @@ pub const Handler = struct { fn filterInlayHintsForZxBlocks( arena: std.mem.Allocator, - hints: []const lsp.types.InlayHint, + hints: []const lsp.types.flat.InlayHint, state: *const ZxFileState, - ) ![]const lsp.types.InlayHint { + ) ![]const lsp.types.flat.InlayHint { if (hints.len == 0 or state.zx_block_ranges.len == 0) return hints; - var filtered = std.ArrayList(lsp.types.InlayHint).empty; + var filtered = std.ArrayList(lsp.types.flat.InlayHint).empty; defer filtered.deinit(arena); try filtered.ensureTotalCapacity(arena, hints.len); @@ -394,7 +359,7 @@ pub const Handler = struct { defer aa.deinit(); const arena = aa.allocator(); - const lsp_diags = try arena.alloc(lsp.types.Diagnostic, diag_list.items.len); + const lsp_diags = try arena.alloc(lsp.types.flat.Diagnostic, diag_list.items.len); for (diag_list.items, 0..) |d, i| { lsp_diags[i] = .{ .range = .{ @@ -411,9 +376,10 @@ pub const Handler = struct { } handler.transport.writeNotification( + handler.io, arena, "textDocument/publishDiagnostics", - lsp.types.PublishDiagnosticsParams, + lsp.types.flat.PublishDiagnosticsParams, .{ .uri = uri, .diagnostics = lsp_diags }, .{ .emit_null_optional_fields = false }, ) catch |err| { @@ -435,8 +401,8 @@ pub const Handler = struct { pub fn initialize( handler: *Handler, arena: std.mem.Allocator, - request: lsp.types.InitializeParams, - ) lsp.types.InitializeResult { + request: lsp.types.flat.InitializeParams, + ) lsp.types.flat.InitializeResult { const client_encoding = choosePositionEncodingKind(request); var zls_request = request; if (zls_request.capabilities.textDocument) |*text_document| { @@ -459,7 +425,7 @@ pub const Handler = struct { return result; } - fn choosePositionEncodingKind(request: lsp.types.InitializeParams) lsp.types.PositionEncodingKind { + fn choosePositionEncodingKind(request: lsp.types.flat.InitializeParams) lsp.types.flat.PositionEncodingKind { if (request.capabilities.general) |general| { if (general.positionEncodings) |encodings| { for (encodings) |encoding| { @@ -481,7 +447,7 @@ pub const Handler = struct { return .@"utf-16"; } - fn toOffsetEncoding(encoding: lsp.types.PositionEncodingKind) lsp.offsets.Encoding { + fn toOffsetEncoding(encoding: lsp.types.flat.PositionEncodingKind) lsp.offsets.Encoding { return switch (encoding) { .@"utf-8" => .@"utf-8", .@"utf-16" => .@"utf-16", @@ -494,7 +460,7 @@ pub const Handler = struct { pub fn initialized( handler: *Handler, arena: std.mem.Allocator, - params: lsp.types.InitializedParams, + params: lsp.types.flat.InitializedParams, ) void { handler.zls.sendNotificationSync(arena, "initialized", params) catch {}; } @@ -517,30 +483,26 @@ pub const Handler = struct { handler.zls.sendNotificationSync(arena, "exit", {}) catch {}; } - // -- Document sync: send raw .zx source to ZLS (as .zig URI) -- + // -- Document sync: forward .zx documents to ZLS under their real .zx URI -- /// https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_didOpen pub fn @"textDocument/didOpen"( handler: *Handler, arena: std.mem.Allocator, - params: lsp.types.DidOpenTextDocumentParams, + params: lsp.types.flat.DidOpenTextDocumentParams, ) !void { if (isZxUri(params.textDocument.uri)) { handler.storeAndDiagnose(params.textDocument.uri, params.textDocument.text); const zig_uri = handler.getZlsUri(params.textDocument.uri); - // Rewrite .zx imports to .zig so ZLS can resolve them - const zls_text = rewriteZxImports(handler.allocator, params.textDocument.text) orelse params.textDocument.text; - defer if (zls_text.ptr != params.textDocument.text.ptr) handler.allocator.free(zls_text); - handler.openZxImportsInZls(arena, params.textDocument.uri, params.textDocument.text); handler.zls.sendNotificationSync(arena, "textDocument/didOpen", .{ .textDocument = .{ .uri = zig_uri, - .languageId = "zig", + .languageId = .{ .custom_value = "zig" }, .version = params.textDocument.version, - .text = zls_text, + .text = params.textDocument.text, }, }) catch {}; return; @@ -552,7 +514,7 @@ pub const Handler = struct { pub fn @"textDocument/didChange"( handler: *Handler, arena: std.mem.Allocator, - params: lsp.types.DidChangeTextDocumentParams, + params: lsp.types.flat.DidChangeTextDocumentParams, ) !void { if (isZxUri(params.textDocument.uri)) { // Build full text by applying incremental changes to stored source. @@ -567,12 +529,12 @@ pub const Handler = struct { for (params.contentChanges) |change| { switch (change) { - .literal_1 => |full| { + .text_document_content_change_whole_document => |full| { if (needs_free) handler.allocator.free(full_text); full_text = full.text; needs_free = false; }, - .literal_0 => |inc| { + .text_document_content_change_partial => |inc| { const new_text = applyIncrementalChange(handler.allocator, full_text, inc.range, inc.text) catch { continue; }; @@ -585,10 +547,6 @@ pub const Handler = struct { handler.storeAndDiagnose(params.textDocument.uri, full_text); - // Rewrite .zx imports to .zig so ZLS can resolve them - const zls_text = rewriteZxImports(handler.allocator, full_text) orelse full_text; - defer if (zls_text.ptr != full_text.ptr) handler.allocator.free(zls_text); - handler.openZxImportsInZls(arena, params.textDocument.uri, full_text); const zig_uri = handler.getZlsUri(params.textDocument.uri); @@ -597,7 +555,7 @@ pub const Handler = struct { .uri = zig_uri, .version = params.textDocument.version, }, - .contentChanges = &.{.{ .literal_1 = .{ .text = zls_text } }}, + .contentChanges = &.{.{ .text_document_content_change_whole_document = .{ .text = full_text } }}, }) catch {}; return; } @@ -608,7 +566,7 @@ pub const Handler = struct { fn applyIncrementalChange( allocator: std.mem.Allocator, source: []const u8, - range: lsp.types.Range, + range: lsp.types.flat.Range, new_text: []const u8, ) ![]const u8 { const start_offset = positionToOffset(source, range.start) orelse return error.InvalidRange; @@ -623,7 +581,7 @@ pub const Handler = struct { } /// Convert an LSP Position (line/character) to a byte offset in the source. - fn positionToOffset(source: []const u8, pos: lsp.types.Position) ?usize { + fn positionToOffset(source: []const u8, pos: lsp.types.flat.Position) ?usize { var line: u32 = 0; var i: usize = 0; while (line < pos.line and i < source.len) { @@ -641,7 +599,7 @@ pub const Handler = struct { pub fn @"textDocument/didSave"( handler: *Handler, arena: std.mem.Allocator, - params: lsp.types.DidSaveTextDocumentParams, + params: lsp.types.flat.DidSaveTextDocumentParams, ) !void { if (isZxUri(params.textDocument.uri)) { handler.zls.sendNotificationSync(arena, "textDocument/didSave", .{ @@ -657,14 +615,15 @@ pub const Handler = struct { pub fn @"textDocument/didClose"( handler: *Handler, arena: std.mem.Allocator, - params: lsp.types.DidCloseTextDocumentParams, + params: lsp.types.flat.DidCloseTextDocumentParams, ) !void { if (isZxUri(params.textDocument.uri)) { // Clear diagnostics for the closed file handler.transport.writeNotification( + handler.io, arena, "textDocument/publishDiagnostics", - lsp.types.PublishDiagnosticsParams, + lsp.types.flat.PublishDiagnosticsParams, .{ .uri = params.textDocument.uri, .diagnostics = &.{} }, .{ .emit_null_optional_fields = false }, ) catch {}; @@ -689,9 +648,9 @@ pub const Handler = struct { pub fn @"textDocument/hover"( handler: *Handler, arena: std.mem.Allocator, - params: lsp.types.HoverParams, - ) ?lsp.types.Hover { - const mapped = handler.remapUri(lsp.types.HoverParams, params); + params: lsp.types.flat.HoverParams, + ) ?lsp.types.flat.Hover { + const mapped = handler.remapUri(lsp.types.flat.HoverParams, params); return handler.zls.sendRequestSync(arena, "textDocument/hover", mapped) catch null; } @@ -699,9 +658,9 @@ pub const Handler = struct { pub fn @"textDocument/completion"( handler: *Handler, arena: std.mem.Allocator, - params: lsp.types.CompletionParams, + params: lsp.types.flat.CompletionParams, ) error{OutOfMemory}!lsp.ResultType("textDocument/completion") { - const mapped = handler.remapUri(lsp.types.CompletionParams, params); + const mapped = handler.remapUri(lsp.types.flat.CompletionParams, params); return handler.zls.sendRequestSync(arena, "textDocument/completion", mapped) catch null; } @@ -709,9 +668,9 @@ pub const Handler = struct { pub fn @"textDocument/signatureHelp"( handler: *Handler, arena: std.mem.Allocator, - params: lsp.types.SignatureHelpParams, - ) error{OutOfMemory}!?lsp.types.SignatureHelp { - const mapped = handler.remapUri(lsp.types.SignatureHelpParams, params); + params: lsp.types.flat.SignatureHelpParams, + ) error{OutOfMemory}!?lsp.types.flat.SignatureHelp { + const mapped = handler.remapUri(lsp.types.flat.SignatureHelpParams, params); return handler.zls.sendRequestSync(arena, "textDocument/signatureHelp", mapped) catch null; } @@ -719,9 +678,9 @@ pub const Handler = struct { pub fn @"textDocument/definition"( handler: *Handler, arena: std.mem.Allocator, - params: lsp.types.DefinitionParams, + params: lsp.types.flat.DefinitionParams, ) error{OutOfMemory}!lsp.ResultType("textDocument/definition") { - const mapped = handler.remapUri(lsp.types.DefinitionParams, params); + const mapped = handler.remapUri(lsp.types.flat.DefinitionParams, params); const result = handler.zls.sendRequestSync(arena, "textDocument/definition", mapped) catch null; return handler.remapResponseUris(result); } @@ -730,9 +689,9 @@ pub const Handler = struct { pub fn @"textDocument/typeDefinition"( handler: *Handler, arena: std.mem.Allocator, - params: lsp.types.TypeDefinitionParams, + params: lsp.types.flat.TypeDefinitionParams, ) error{OutOfMemory}!lsp.ResultType("textDocument/typeDefinition") { - const mapped = handler.remapUri(lsp.types.TypeDefinitionParams, params); + const mapped = handler.remapUri(lsp.types.flat.TypeDefinitionParams, params); const result = handler.zls.sendRequestSync(arena, "textDocument/typeDefinition", mapped) catch null; return handler.remapResponseUris(result); } @@ -741,9 +700,9 @@ pub const Handler = struct { pub fn @"textDocument/implementation"( handler: *Handler, arena: std.mem.Allocator, - params: lsp.types.ImplementationParams, + params: lsp.types.flat.ImplementationParams, ) error{OutOfMemory}!lsp.ResultType("textDocument/implementation") { - const mapped = handler.remapUri(lsp.types.ImplementationParams, params); + const mapped = handler.remapUri(lsp.types.flat.ImplementationParams, params); const result = handler.zls.sendRequestSync(arena, "textDocument/implementation", mapped) catch null; return handler.remapResponseUris(result); } @@ -752,9 +711,9 @@ pub const Handler = struct { pub fn @"textDocument/declaration"( handler: *Handler, arena: std.mem.Allocator, - params: lsp.types.DeclarationParams, + params: lsp.types.flat.DeclarationParams, ) error{OutOfMemory}!lsp.ResultType("textDocument/declaration") { - const mapped = handler.remapUri(lsp.types.DeclarationParams, params); + const mapped = handler.remapUri(lsp.types.flat.DeclarationParams, params); const result = handler.zls.sendRequestSync(arena, "textDocument/declaration", mapped) catch null; return handler.remapResponseUris(result); } @@ -763,9 +722,9 @@ pub const Handler = struct { pub fn @"textDocument/prepareRename"( handler: *Handler, arena: std.mem.Allocator, - params: lsp.types.PrepareRenameParams, - ) ?lsp.types.PrepareRenameResult { - const mapped = handler.remapUri(lsp.types.PrepareRenameParams, params); + params: lsp.types.flat.PrepareRenameParams, + ) ?lsp.types.flat.PrepareRenameResult { + const mapped = handler.remapUri(lsp.types.flat.PrepareRenameParams, params); return handler.zls.sendRequestSync(arena, "textDocument/prepareRename", mapped) catch null; } @@ -773,9 +732,9 @@ pub const Handler = struct { pub fn @"textDocument/rename"( handler: *Handler, arena: std.mem.Allocator, - params: lsp.types.RenameParams, - ) error{OutOfMemory}!?lsp.types.WorkspaceEdit { - const mapped = handler.remapUri(lsp.types.RenameParams, params); + params: lsp.types.flat.RenameParams, + ) error{OutOfMemory}!?lsp.types.flat.WorkspaceEdit { + const mapped = handler.remapUri(lsp.types.flat.RenameParams, params); const result = handler.zls.sendRequestSync(arena, "textDocument/rename", mapped) catch null; return handler.remapResponseUris(result); } @@ -784,9 +743,9 @@ pub const Handler = struct { pub fn @"textDocument/references"( handler: *Handler, arena: std.mem.Allocator, - params: lsp.types.ReferenceParams, - ) error{OutOfMemory}!?[]const lsp.types.Location { - const mapped = handler.remapUri(lsp.types.ReferenceParams, params); + params: lsp.types.flat.ReferenceParams, + ) error{OutOfMemory}!?[]const lsp.types.flat.Location { + const mapped = handler.remapUri(lsp.types.flat.ReferenceParams, params); const result = handler.zls.sendRequestSync(arena, "textDocument/references", mapped) catch null; return handler.remapResponseUris(result); } @@ -795,9 +754,9 @@ pub const Handler = struct { pub fn @"textDocument/documentHighlight"( handler: *Handler, arena: std.mem.Allocator, - params: lsp.types.DocumentHighlightParams, - ) error{OutOfMemory}!?[]const lsp.types.DocumentHighlight { - const mapped = handler.remapUri(lsp.types.DocumentHighlightParams, params); + params: lsp.types.flat.DocumentHighlightParams, + ) error{OutOfMemory}!?[]const lsp.types.flat.DocumentHighlight { + const mapped = handler.remapUri(lsp.types.flat.DocumentHighlightParams, params); return handler.zls.sendRequestSync(arena, "textDocument/documentHighlight", mapped) catch null; } @@ -807,8 +766,8 @@ pub const Handler = struct { pub fn @"textDocument/willSaveWaitUntil"( handler: *Handler, arena: std.mem.Allocator, - params: lsp.types.WillSaveTextDocumentParams, - ) error{OutOfMemory}!?[]const lsp.types.TextEdit { + params: lsp.types.flat.WillSaveTextDocumentParams, + ) error{OutOfMemory}!?[]const lsp.types.flat.TextEdit { return handler.zls.sendRequestSync(arena, "textDocument/willSaveWaitUntil", params) catch null; } @@ -816,8 +775,8 @@ pub const Handler = struct { pub fn @"textDocument/semanticTokens/full"( handler: *Handler, arena: std.mem.Allocator, - params: lsp.types.SemanticTokensParams, - ) error{OutOfMemory}!?lsp.types.SemanticTokens { + params: lsp.types.flat.SemanticTokensParams, + ) error{OutOfMemory}!?lsp.types.flat.SemanticTokens { if (isZxUri(params.textDocument.uri)) { var new_params = params; new_params.textDocument = .{ .uri = handler.getZlsUri(params.textDocument.uri) }; @@ -830,8 +789,8 @@ pub const Handler = struct { pub fn @"textDocument/semanticTokens/range"( handler: *Handler, arena: std.mem.Allocator, - params: lsp.types.SemanticTokensRangeParams, - ) error{OutOfMemory}!?lsp.types.SemanticTokens { + params: lsp.types.flat.SemanticTokensRangeParams, + ) error{OutOfMemory}!?lsp.types.flat.SemanticTokens { return handler.zls.sendRequestSync(arena, "textDocument/semanticTokens/range", params) catch null; } @@ -839,8 +798,8 @@ pub const Handler = struct { pub fn @"textDocument/inlayHint"( handler: *Handler, arena: std.mem.Allocator, - params: lsp.types.InlayHintParams, - ) error{OutOfMemory}!?[]const lsp.types.InlayHint { + params: lsp.types.flat.InlayHintParams, + ) error{OutOfMemory}!?[]const lsp.types.flat.InlayHint { if (isZxUri(params.textDocument.uri)) { var new_params = params; new_params.textDocument = .{ .uri = handler.getZlsUri(params.textDocument.uri) }; @@ -859,7 +818,7 @@ pub const Handler = struct { pub fn @"textDocument/documentSymbol"( handler: *Handler, arena: std.mem.Allocator, - params: lsp.types.DocumentSymbolParams, + params: lsp.types.flat.DocumentSymbolParams, ) error{OutOfMemory}!lsp.ResultType("textDocument/documentSymbol") { if (isZxUri(params.textDocument.uri)) { var new_params = params; @@ -873,8 +832,8 @@ pub const Handler = struct { pub fn @"textDocument/formatting"( handler: *Handler, arena: std.mem.Allocator, - params: lsp.types.DocumentFormattingParams, - ) error{OutOfMemory}!?[]const lsp.types.TextEdit { + params: lsp.types.flat.DocumentFormattingParams, + ) error{OutOfMemory}!?[]const lsp.types.flat.TextEdit { if (isZxUri(params.textDocument.uri)) { if (handler.zx_files.get(params.textDocument.uri)) |state| { const source_z = try handler.allocator.dupeZ(u8, state.source); @@ -891,7 +850,7 @@ pub const Handler = struct { return null; } - const edits = try arena.alloc(lsp.types.TextEdit, 1); + const edits = try arena.alloc(lsp.types.flat.TextEdit, 1); edits[0] = .{ .range = .{ .start = .{ .line = 0, .character = 0 }, @@ -909,7 +868,7 @@ pub const Handler = struct { pub fn @"textDocument/codeAction"( handler: *Handler, arena: std.mem.Allocator, - params: lsp.types.CodeActionParams, + params: lsp.types.flat.CodeActionParams, ) error{OutOfMemory}!lsp.ResultType("textDocument/codeAction") { if (isZxUri(params.textDocument.uri)) { var new_params = params; @@ -923,8 +882,8 @@ pub const Handler = struct { pub fn @"textDocument/foldingRange"( handler: *Handler, arena: std.mem.Allocator, - params: lsp.types.FoldingRangeParams, - ) error{OutOfMemory}!?[]const lsp.types.FoldingRange { + params: lsp.types.flat.FoldingRangeParams, + ) error{OutOfMemory}!?[]const lsp.types.flat.FoldingRange { if (isZxUri(params.textDocument.uri)) { var new_params = params; new_params.textDocument = .{ .uri = handler.getZlsUri(params.textDocument.uri) }; @@ -937,8 +896,8 @@ pub const Handler = struct { pub fn @"textDocument/selectionRange"( handler: *Handler, arena: std.mem.Allocator, - params: lsp.types.SelectionRangeParams, - ) error{OutOfMemory}!?[]const lsp.types.SelectionRange { + params: lsp.types.flat.SelectionRangeParams, + ) error{OutOfMemory}!?[]const lsp.types.flat.SelectionRange { return handler.zls.sendRequestSync(arena, "textDocument/selectionRange", params) catch null; } @@ -948,7 +907,7 @@ pub const Handler = struct { pub fn @"workspace/didChangeWatchedFiles"( handler: *Handler, arena: std.mem.Allocator, - params: lsp.types.DidChangeWatchedFilesParams, + params: lsp.types.flat.DidChangeWatchedFilesParams, ) !void { handler.zls.sendNotificationSync(arena, "workspace/didChangeWatchedFiles", params) catch {}; } @@ -957,7 +916,7 @@ pub const Handler = struct { pub fn @"workspace/didChangeWorkspaceFolders"( handler: *Handler, arena: std.mem.Allocator, - params: lsp.types.DidChangeWorkspaceFoldersParams, + params: lsp.types.flat.DidChangeWorkspaceFoldersParams, ) !void { handler.zls.sendNotificationSync(arena, "workspace/didChangeWorkspaceFolders", params) catch {}; } @@ -966,7 +925,7 @@ pub const Handler = struct { pub fn @"workspace/didChangeConfiguration"( handler: *Handler, arena: std.mem.Allocator, - params: lsp.types.DidChangeConfigurationParams, + params: lsp.types.flat.DidChangeConfigurationParams, ) !void { handler.zls.sendNotificationSync(arena, "workspace/didChangeConfiguration", params) catch {}; } @@ -976,11 +935,21 @@ pub const Handler = struct { /// responses and other client-to-server responses. pub fn onResponse( handler: *Handler, - _: std.mem.Allocator, + arena: std.mem.Allocator, response: lsp.JsonRPCMessage.Response, ) void { - handler.zls.handleResponse(response) catch |err| { - std.log.err("zls handleResponse failed: {}", .{err}); + const json_message = std.json.Stringify.valueAlloc( + arena, + lsp.JsonRPCMessage{ .response = response }, + .{ .emit_null_optional_fields = false }, + ) catch |err| { + std.log.err("zls onResponse stringify failed: {}", .{err}); + return; + }; + const reply = handler.zls.sendJsonMessageSync(json_message) catch |err| { + std.log.err("zls sendJsonMessageSync failed: {}", .{err}); + return; }; + if (reply) |r| handler.zls.allocator.free(r); } }; diff --git a/src/lsp/proxy/main.zig b/src/lsp/proxy/main.zig index 6695da18..bfa663e7 100644 --- a/src/lsp/proxy/main.zig +++ b/src/lsp/proxy/main.zig @@ -8,7 +8,7 @@ const std = @import("std"); pub fn main() !void { - var gpa: std.heap.GeneralPurposeAllocator(.{}) = .init; + var gpa: std.heap.DebugAllocator(.{}) = .init; defer _ = gpa.deinit(); const allocator = gpa.allocator(); @@ -141,7 +141,7 @@ fn tryFilterDiagnostics(allocator: std.mem.Allocator, body: []const u8) ![]u8 { if (removed == 0) return error.NothingFiltered; - var aw: std.io.Writer.Allocating = .init(allocator); + var aw: std.Io.Writer.Allocating = .init(allocator); errdefer aw.deinit(); try std.json.Stringify.value(parsed.value, .{}, &aw.writer); return aw.toOwnedSlice(); diff --git a/src/main.zig b/src/main.zig index 92fb6565..8b39a74f 100644 --- a/src/main.zig +++ b/src/main.zig @@ -1,4 +1,4 @@ -pub fn main() !void { +pub fn main(init: std.process.Init) !void { var dbg = std.heap.DebugAllocator(.{}).init; const allocator = switch (builtin.os.tag) { @@ -12,51 +12,43 @@ pub fn main() !void { defer if (builtin.mode == .Debug) std.debug.assert(dbg.deinit() == .ok); if (comptime (!build_options.exclude_lsp)) { - var args = try std.process.argsWithAllocator(allocator); + var args = try init.minimal.args.iterateAllocator(allocator); defer args.deinit(); _ = args.next(); const subcmd = args.next(); - if (std.mem.eql(u8, subcmd orelse "", "lsp")) return try lsp.main(); + if (std.mem.eql(u8, subcmd orelse "", "lsp")) return try lsp.main(init); } - if (builtin.os.tag == .wasi) return try main_wasm(); - if (builtin.os.tag == .windows) _ = std.os.windows.kernel32.SetConsoleOutputCP(65001); + if (builtin.os.tag == .wasi) return try main_wasm(init); + if (builtin.os.tag == .windows) _ = SetConsoleOutputCP(65001); - var stdout_writer = std.fs.File.stdout().writerStreaming(&.{}); + var stdout_writer = std.Io.File.stdout().writerStreaming(init.io, &.{}); var stdout = &stdout_writer.interface; var buf: [4096]u8 = undefined; - var stdin_reader = std.fs.File.stdin().readerStreaming(&buf); + var stdin_reader = std.Io.File.stdin().readerStreaming(init.io, &buf); const stdin = &stdin_reader.interface; const root = try cli.build(stdout, stdin, allocator); defer root.deinit(); - root.execute(.{}) catch |err| { - const c = tui.Colors; - const err_name = @errorName(err); - const base_url = std.fmt.comptimePrint("{s}/issues/new", .{zx.info.repository}); - var url_buf: [512]u8 = undefined; - const full_url = std.fmt.bufPrint(&url_buf, "{s}?title=CLI%20Error:%20{s}&body=**Error:**%20{s}%0A**Version:**%20{s}", .{ - base_url, - err_name, - err_name, - zx.info.version, - }) catch base_url; - // OSC 8 hyperlink: \x1b]8;;URL\x07DISPLAY_TEXT\x1b]8;;\x07 - std.debug.print("\n{s}An unexpected problem occurred while running ZX CLI.{s}\n", .{ c.red, c.reset }); - std.debug.print("Please report it at {s}\x1b]8;;{s}\x07{s}\x1b]8;;\x07{s}\n", .{ c.cyan, full_url, base_url, c.reset }); - std.debug.print("{s}Details: {s}{s}\n\n", .{ c.gray, err_name, c.reset }); + var app_ctx: AppContext = .{ + .io = init.io, + .environ_map = init.environ_map, }; + try root.execute(.{ .process_args = init.minimal.args, .data = &app_ctx }); + try stdout.flush(); } -fn main_wasm() !void { +extern "kernel32" fn SetConsoleOutputCP(wCodePageID: std.os.windows.UINT) callconv(.winapi) std.os.windows.BOOL; + +fn main_wasm(init: std.process.Init) !void { var dbg = std.heap.DebugAllocator(.{}).init; const allocator = dbg.allocator(); - var args = try std.process.argsWithAllocator(allocator); + var args = try init.minimal.args.iterateAllocator(allocator); defer args.deinit(); // --- Sub Command --- // @@ -70,7 +62,7 @@ fn main_wasm() !void { if (std.mem.eql(u8, sub_cmd, "transpile")) is_transpile = true; if (std.mem.eql(u8, sub_cmd, "fmt")) is_fmt = true; if (std.mem.eql(u8, sub_cmd, "lsp")) is_lsp = true; - if (is_lsp) return try lsp.main(); + // if (is_lsp) return try lsp.main(); var files = std.ArrayList([]const u8).empty; @@ -78,17 +70,17 @@ fn main_wasm() !void { try files.append(allocator, arg); } - var cwd = try std.fs.openDirAbsolute("/codes", .{}); - defer cwd.close(); + var cwd = try std.Io.Dir.openDirAbsolute(init.io, "/codes", .{}); + defer cwd.close(init.io); // Transpile/Fmt file_path.zx and write with file_path.zig for (files.items) |file_path| { - const zx_source = try cwd.readFileAlloc(allocator, file_path, std.math.maxInt(usize)); + const zx_source = try cwd.readFileAlloc(init.io, file_path, allocator, .unlimited); const zx_sourcez = try allocator.dupeZ(u8, zx_source); const ast = try zx.Ast.parse(allocator, zx_sourcez, .{}); const output = if (is_transpile) ast.zig_source else ast.zx_source; - try std.fs.File.stdout().writeAll(output); + try std.Io.File.stdout().writeStreamingAll(init.io, output); } } @@ -98,6 +90,7 @@ const build_options = @import("build_options"); const zx = @import("zx"); const cli = @import("cli/root.zig"); const tui = @import("tui/main.zig"); +const AppContext = @import("cli/shared/context.zig").AppContext; const lsp = if (build_options.exclude_lsp) void else @import("lsp/main.zig"); pub const std_options = std.Options{ diff --git a/src/props.zig b/src/props.zig index 8460b9c2..8d85115b 100644 --- a/src/props.zig +++ b/src/props.zig @@ -150,14 +150,22 @@ pub fn MergedPropsType(comptime BaseType: type, comptime OverrideType: type) typ } } - return @Type(.{ - .@"struct" = .{ - .layout = .auto, - .fields = &fields, - .decls = &.{}, - .is_tuple = false, - }, - }); + // Extract field names, types, and attributes for @Struct + comptime var field_names: [field_count][]const u8 = undefined; + comptime var field_types: [field_count]type = undefined; + comptime var field_attrs: [field_count]std.builtin.Type.StructField.Attributes = undefined; + + inline for (fields, 0..) |f, i| { + field_names[i] = f.name; + field_types[i] = f.type; + field_attrs[i] = .{ + .@"align" = f.alignment, + .@"comptime" = f.is_comptime, + .default_value_ptr = f.default_value_ptr, + }; + } + + return @Struct(.auto, null, &field_names, &field_types, &field_attrs); } fn isSerializable(comptime T: type) bool { diff --git a/src/root.zig b/src/root.zig index 899f5146..a7ee8efe 100644 --- a/src/root.zig +++ b/src/root.zig @@ -105,8 +105,9 @@ pub const fetch = Fetch.fetch; // --- Values --- // pub const allocator = app_mod.allocator; +pub const io = app_mod.io; pub const platform: Platform = plfm.platform; -pub const std_options: std.Options = opts.std_options; +pub const std_options = opts.std_options; // --- StyleSheet (separate cached module) --- // pub const style = @import("zx_style"); diff --git a/src/runtime/core/Cache.zig b/src/runtime/core/Cache.zig index 57a296ec..ac13b3d1 100644 --- a/src/runtime/core/Cache.zig +++ b/src/runtime/core/Cache.zig @@ -68,7 +68,8 @@ const cachez = switch (builtin.os.tag) { const Self = @This(); - pub fn init(allocator: Allocator, _: Config) !Self { + pub fn init(io: std.Io, allocator: Allocator, _: Config) !Self { + _ = io; return .{ .allocator = allocator, }; @@ -157,7 +158,15 @@ const StoredEntry = struct { }; var state: ?State = null; -var state_mutex: std.Thread.Mutex = .{}; +var state_mutex: struct { + inner: std.Io.Mutex = .init, + pub fn lock(self: *@This()) void { + self.inner.lockUncancelable(std.Options.debug_io); + } + pub fn unlock(self: *@This()) void { + self.inner.unlock(std.Options.debug_io); + } +} = .{}; /// Initialize the cache (called once at app startup) pub fn init(allocator: std.mem.Allocator, config: cachez.Config) !void { @@ -167,7 +176,7 @@ pub fn init(allocator: std.mem.Allocator, config: cachez.Config) !void { if (state != null) return; state = .{ .allocator = allocator, - .memory = try cachez.Cache(CacheEntry).init(allocator, config), + .memory = try cachez.Cache(CacheEntry).init(std.Options.debug_io, allocator, config), }; } @@ -436,7 +445,8 @@ fn decodeStoredEntry(encoded: []const u8) !StoredEntry { fn now() u64 { if (comptime builtin.os.tag == .freestanding or builtin.os.tag == .wasi) return 0; - return @as(u64, @intCast(std.time.timestamp())); + const ts = std.Io.Timestamp.now(std.Options.debug_io, .real); + return @as(u64, @intCast(@divTrunc(ts.nanoseconds, 1_000_000_000))); } fn expirationFromOptions(opts: PutOptions) !?u64 { diff --git a/src/runtime/core/File.zig b/src/runtime/core/File.zig index 06b09a0f..3457ec6e 100644 --- a/src/runtime/core/File.zig +++ b/src/runtime/core/File.zig @@ -1,6 +1,6 @@ //! Represents an uploaded file from a multipart form submission. //! -//! The `data` field is a type-erased `std.io.AnyReader` so call-site code +//! The `data` field is a type-erased `std.Io.AnyReader` so call-site code //! is written against the reader interface today and will continue to compile //! unchanged when a streaming multipart parser is introduced in the future. //! Currently the reader is backed by the in-memory request body bytes. @@ -22,7 +22,7 @@ size: usize = 0, /// Reader interface for the file content. /// Do not retain this value after the action handler returns; the backing /// memory belongs to the request arena. -data: std.io.AnyReader = emptyReader(), +data: std.Io.AnyReader = emptyReader(), /// Build a File backed by an in-memory byte slice. /// `fbs_alloc` must outlive the returned File (use the request arena). @@ -32,12 +32,12 @@ pub fn fromBytes( content_type: []const u8, fbs_alloc: std.mem.Allocator, ) File { - const fbs = fbs_alloc.create(std.io.FixedBufferStream([]const u8)) catch return .{ + const fbs = fbs_alloc.create(std.Io.FixedBufferStream([]const u8)) catch return .{ .name = name, .content_type = content_type, .size = bytes.len, }; - fbs.* = std.io.fixedBufferStream(bytes); + fbs.* = std.Io.fixedBufferStream(bytes); return .{ .name = name, .content_type = content_type, @@ -54,6 +54,6 @@ fn emptyReadFn(_: *const anyopaque, _: []u8) anyerror!usize { var empty_ctx: u8 = 0; -fn emptyReader() std.io.AnyReader { +fn emptyReader() std.Io.AnyReader { return .{ .context = &empty_ctx, .readFn = &emptyReadFn }; } diff --git a/src/runtime/core/kv.zig b/src/runtime/core/kv.zig index 26f3e19e..838820fc 100644 --- a/src/runtime/core/kv.zig +++ b/src/runtime/core/kv.zig @@ -193,38 +193,41 @@ fn nsDir(ns: []const u8, buf: *[256]u8) ?[]u8 { } fn fsGet(_: *anyopaque, ns: []const u8, allocator: std.mem.Allocator, key: []const u8) !?[]u8 { + const io = std.Options.debug_io; var buf: [1024]u8 = undefined; const path = keyPath(ns, key, &buf) orelse return null; - const file = std.fs.cwd().openFile(path, .{}) catch return null; - defer file.close(); - return file.readToEndAlloc(allocator, 10 * 1024 * 1024) catch null; + return std.Io.Dir.cwd().readFileAlloc(io, path, allocator, .limited(10 * 1024 * 1024)) catch null; } fn fsPut(_: *anyopaque, ns: []const u8, key: []const u8, value: []const u8, _: PutOptions) !void { - var dir_buf: [256]u8 = undefined; - const dir_path = nsDir(ns, &dir_buf) orelse return error.KeyTooLong; - try std.fs.cwd().makePath(dir_path); + const io = std.Options.debug_io; var buf: [1024]u8 = undefined; const path = keyPath(ns, key, &buf) orelse return error.KeyTooLong; - const file = try std.fs.cwd().createFile(path, .{}); - defer file.close(); - try file.writeAll(value); + var file = try std.Io.Dir.cwd().createFileAtomic(io, path, .{ + .make_path = true, + .replace = true, + }); + defer file.deinit(io); + try file.file.writeStreamingAll(io, value); + try file.replace(io); } fn fsDelete(_: *anyopaque, ns: []const u8, key: []const u8) !void { + const io = std.Options.debug_io; var buf: [1024]u8 = undefined; const path = keyPath(ns, key, &buf) orelse return; - std.fs.cwd().deleteFile(path) catch {}; + std.Io.Dir.cwd().deleteFile(io, path) catch {}; } fn fsList(_: *anyopaque, ns: []const u8, allocator: std.mem.Allocator, prefix: []const u8) ![][]u8 { + const io = std.Options.debug_io; var dir_buf: [256]u8 = undefined; const dir_path = nsDir(ns, &dir_buf) orelse return &[_][]u8{}; - var dir = std.fs.cwd().openDir(dir_path, .{ .iterate = true }) catch return &[_][]u8{}; - defer dir.close(); + var dir = std.Io.Dir.cwd().openDir(io, dir_path, .{ .iterate = true }) catch return &[_][]u8{}; + defer dir.close(io); var keys = std.ArrayList([]u8).empty; var iter = dir.iterate(); - while (try iter.next()) |entry| { + while (try iter.next(io)) |entry| { if (entry.kind != .file) continue; const decoded_len = std.base64.url_safe_no_pad.Decoder.calcSizeForSlice(entry.name) catch continue; const key = try allocator.alloc(u8, decoded_len); diff --git a/src/runtime/server/Server.zig b/src/runtime/server/Server.zig index 3adde62e..e253a911 100644 --- a/src/runtime/server/Server.zig +++ b/src/runtime/server/Server.zig @@ -34,12 +34,13 @@ pub fn Server(comptime H: type) type { server: httpz.Server(*HandlerType), config: AppConfig, app_ctx: H, + io: std.Io, _is_listening: bool = false, const HandlerType = Handler(AppCtxType); - pub fn init(allocator: std.mem.Allocator, config: AppConfig, app_ctx: H) !*Self { + pub fn init(io: std.Io, allocator: std.mem.Allocator, config: AppConfig, app_ctx: H) !*Self { if (!@import("zx_module_options").exclude_db) default_db.use(); const self = try allocator.create(Self); @@ -48,6 +49,7 @@ pub fn Server(comptime H: type) type { self.allocator = allocator; self.meta = zx.meta; self.app_ctx = app_ctx; + self.io = io; // Get pointer to app context for handler initialization // When H is void, pass undefined; when H is pointer, use directly; when H is value, get pointer from self @@ -59,9 +61,9 @@ pub fn Server(comptime H: type) type { &self.app_ctx; self.config = config; - self.handler = try HandlerType.init(allocator, &self.meta, config, app_ctx_ptr); + self.handler = try HandlerType.init(self.io, allocator, &self.meta, config, app_ctx_ptr); errdefer self.handler.deinit(); - self.server = try httpz.Server(*HandlerType).init(allocator, mapStruct(httpz.Config, config.server), &self.handler); + self.server = try httpz.Server(*HandlerType).init(self.io, allocator, mapStruct(httpz.Config, config.server), &self.handler); // -- Routing -- // var router = try self.server.router(.{}); @@ -157,12 +159,11 @@ pub fn Server(comptime H: type) type { // When running under the dev proxy, bind to the inner port on // loopback only - the proxy owns the user-facing port. - if (std.process.getEnvVarOwned(self.allocator, "ZIEX_INNER_PORT")) |port_str| { + if (envVar("ZIEX_INNER_PORT")) |port_str| { if (std.fmt.parseInt(u16, port_str, 10)) |inner_port| { - self.server.config.port = inner_port; - self.server.config.address = "127.0.0.1"; + setServerAddress(&self.server.config, "127.0.0.1", inner_port); } else |_| {} - } else |_| {} + } self.server.listen() catch |err| { self._is_listening = false; @@ -170,7 +171,7 @@ pub fn Server(comptime H: type) type { switch (err) { error.AddressInUse => { const is_dev = self.meta.cli_command == .dev; - const port = self.server.config.port.?; + const port = serverPort(&self.server.config).?; var max_retries: u8 = 10; if (is_dev) while (max_retries > 0) : (max_retries -= 1) { @@ -178,12 +179,12 @@ pub fn Server(comptime H: type) type { self.infoWithCrossedOutPort(port); std.debug.print("{s}Port {d} is already in use, {s}trying with port {d}...{s}\n\n", .{ colors.yellow, port, colors.reset_all, new_port, colors.reset_all }); std.debug.print("To kill the port, run:\n {s}kill -9 $(lsof -t -i:{d}){s}\n\n", .{ colors.dim, port, colors.reset_all }); - self.server.config.port = new_port; + setServerAddress(&self.server.config, "127.0.0.1", new_port); var retry_config = self.config; retry_config.server.port = new_port; self.server.deinit(); - var retry_server = try init(self.allocator, retry_config, self.app_ctx); + var retry_server = try init(self.io, self.allocator, retry_config, self.app_ctx); defer retry_server.deinit(); retry_server.info(); @@ -207,10 +208,10 @@ pub fn Server(comptime H: type) type { /// Print the server info to the console /// ZX - v{version} | http://localhost:{port} pub fn info(self: *Self) void { - const display_port: u16 = if (std.process.getEnvVarOwned(self.allocator, "ZIEX_OUTER_PORT")) |s| - std.fmt.parseInt(u16, s, 10) catch self.server.config.port.? - else |_| - self.server.config.port.?; + const display_port: u16 = if (envVar("ZIEX_OUTER_PORT")) |s| + std.fmt.parseInt(u16, s, 10) catch serverPort(&self.server.config).? + else + serverPort(&self.server.config).?; std.debug.print("{s}ZX{s} {s}- v{s}{s} | http://localhost:{d}\n", .{ colors.bold, colors.reset_all, colors.dim, Self.version, colors.reset_all, display_port }); } @@ -235,58 +236,24 @@ pub fn Server(comptime H: type) type { } fn introspect(self: *Self) !void { - var args = try std.process.argsWithAllocator(self.allocator); - defer args.deinit(); - - // --- Flags --- // - // --introspect: Print the metadata to stdout and exit - var is_introspect = false; - var is_stdio = false; - var port = self.server.config.port orelse Constant.default_port; - var address = self.server.config.address orelse Constant.default_address; - - while (args.next()) |arg| { - // --introspect: Print the metadata to stdout and exit - if (std.mem.eql(u8, arg, "--introspect")) is_introspect = true; - - // --stdio: Start the server in stdio mode, where request responses will be read from stdin and written to stdout - if (std.mem.eql(u8, arg, "--stdio")) is_stdio = true; - - // --port: Override the configured/default port - if (std.mem.eql(u8, arg, "--port")) { - const port_str = args.next() orelse return error.MissingPort; - const port_int = std.fmt.parseInt(u16, port_str, 10) catch return error.InvalidPort; - port = port_int; - } - - // --address: Override the configured/default address - if (std.mem.eql(u8, arg, "--address")) address = args.next() orelse return error.MissingAddress; - - // --rootdir: Override the configured/default root directory - if (std.mem.eql(u8, arg, "--rootdir")) self.meta.rootdir = args.next() orelse return error.MissingRootdir; + const port = (if (zx_options.server_port != null) zx_options.server_port else serverPort(&self.server.config)) orelse Constant.default_port; + const address = zx_options.server_address orelse self.config.server.address orelse Constant.default_address; - // --cli-command: Override the CLI command - if (std.mem.eql(u8, arg, "--cli-command")) { - const cli_command_str = args.next() orelse return error.MissingCliCommand; - const cli_command = std.meta.stringToEnum(ServerMeta.CliCommand, cli_command_str) orelse return error.InvalidCliCommand; - self.meta.cli_command = cli_command; - } + if (zx_options.server_rootdir) |rootdir| { + self.meta.rootdir = rootdir; } - var stdout_writer = std.fs.File.stdout().writerStreaming(&.{}); - var stdout = &stdout_writer.interface; - - var stdin_reader = std.fs.File.stdin().readerStreaming(&.{}); - var stdin = &stdin_reader.interface; - stdin = stdin; + if (zx_options.cli_command) |cli_command_str| { + const cli_command = std.meta.stringToEnum(ServerMeta.CliCommand, cli_command_str) orelse return error.InvalidCliCommand; + self.meta.cli_command = cli_command; + } // Overriding or setting default configs - self.server.config.port = port; - self.server.config.address = address; + setServerAddress(&self.server.config, address, port); self.server.config.request.max_form_count = self.server.config.request.max_form_count orelse Constant.default_max_form_count; self.server.config.request.max_multiform_count = self.server.config.request.max_multiform_count orelse Constant.default_max_multiform_count; - if (is_introspect) { + if (zx_options.introspect) { var aw = std.Io.Writer.Allocating.init(self.allocator); defer aw.deinit(); @@ -294,7 +261,8 @@ pub fn Server(comptime H: type) type { defer serilizable_meta.deinit(self.allocator); try serilizable_meta.serialize(&aw.writer); - try stdout.print("{s}\n", .{aw.written()}); + try std.Io.File.stdout().writeStreamingAll(self.io, aw.written()); + try std.Io.File.stdout().writeStreamingAll(self.io, "\n"); std.process.exit(0); } @@ -306,8 +274,6 @@ pub fn Server(comptime H: type) type { zx_routes.all("/devtool", HandlerType.devtool, .{}); } - - try stdout.flush(); } }; } @@ -875,7 +841,10 @@ pub fn mapStruct(comptime T: type, src: anytype) T { var out: T = .{}; const S = @TypeOf(src); inline for (@typeInfo(T).@"struct".fields) |f| { - if (@hasField(S, f.name)) { + if (comptime T == httpz.Config and std.mem.eql(u8, f.name, "address")) { + // handled after the loop because our app config stores host/port + // separately while httpz expects a tagged union + } else if (@hasField(S, f.name)) { const sv = @field(src, f.name); switch (@typeInfo(f.type)) { .@"struct" => @field(out, f.name) = mapStruct(f.type, sv), @@ -883,15 +852,49 @@ pub fn mapStruct(comptime T: type, src: anytype) T { } } } + if (comptime T == httpz.Config) out.address = mapServerAddress(src); return out; } +fn mapServerAddress(src: AppConfig.ServerConfig) httpz.Config.Address { + if (src.unix_path) |unix_path| return .{ .unix = unix_path }; + const port = src.port orelse Constant.default_port; + const address = src.address orelse Constant.default_address; + + if (std.mem.eql(u8, address, "localhost")) return httpz.Config.Address.localhost(port); + if (std.mem.eql(u8, address, "0.0.0.0")) return httpz.Config.Address.all(port); + + return .{ .ip = std.Io.net.IpAddress.parse(address, port) catch .{ .ip4 = .{ .bytes = .{ 127, 0, 0, 1 }, .port = port } } }; +} + +fn serverPort(config: *const httpz.Config) ?u16 { + return switch (config.address) { + .ip => |ip| ip.getPort(), + .unix => null, + }; +} + +fn setServerAddress(config: *httpz.Config, address: []const u8, port: u16) void { + config.address = if (std.mem.eql(u8, address, "localhost")) + httpz.Config.Address.localhost(port) + else if (std.mem.eql(u8, address, "0.0.0.0")) + httpz.Config.Address.all(port) + else + .{ .ip = std.Io.net.IpAddress.parse(address, port) catch .{ .ip4 = .{ .bytes = .{ 127, 0, 0, 1 }, .port = port } } }; +} + +fn envVar(name_z: [*:0]const u8) ?[]const u8 { + const value = std.c.getenv(name_z) orelse return null; + return std.mem.span(value); +} + const std = @import("std"); const builtin = @import("builtin"); const httpz = @import("httpz"); const cachez = @import("cachez"); const zx = @import("../../root.zig"); const module_config = @import("zx_info"); +const zx_options = @import("zx_options"); const Constant = @import("../../constant.zig"); const Handler = @import("handler.zig").Handler; const AppConfig = @import("../../AppConfig.zig"); diff --git a/src/runtime/server/fetch.zig b/src/runtime/server/fetch.zig index 8f7a298b..5c54d61e 100644 --- a/src/runtime/server/fetch.zig +++ b/src/runtime/server/fetch.zig @@ -14,7 +14,7 @@ const FetchError = Fetch.FetchError; /// Perform an HTTP fetch request using std.http.Client. pub fn fetch(allocator: std.mem.Allocator, url: []const u8, init: RequestInit) FetchError!Response { // Create HTTP client - var client: std.http.Client = .{ .allocator = allocator }; + var client: std.http.Client = .{ .allocator = allocator, .io = std.Options.debug_io }; defer client.deinit(); // Build extra headers as a slice diff --git a/src/runtime/server/handler.zig b/src/runtime/server/handler.zig index 535794df..93232790 100644 --- a/src/runtime/server/handler.zig +++ b/src/runtime/server/handler.zig @@ -132,11 +132,11 @@ const PageCache = struct { config: AppConfig.CacheConfig, allocator: Allocator, - pub fn init(allocator: Allocator, config: AppConfig.CacheConfig) !PageCache { + pub fn init(io: std.Io, allocator: Allocator, config: AppConfig.CacheConfig) !PageCache { return .{ .allocator = allocator, .config = config, - .cache = try cachez.Cache(CacheValue).init(allocator, .{ + .cache = try cachez.Cache(CacheValue).init(io, allocator, .{ .max_size = config.max_size, }), }; @@ -271,8 +271,9 @@ pub fn Handler(comptime AppCtxType: type) type { page_cache: PageCache, allocator: std.mem.Allocator, app_ctx: *AppCtxType, + io: std.Io, - pub fn init(allocator: std.mem.Allocator, meta: *App.Meta, config: AppConfig, app_ctx: *AppCtxType) !Self { + pub fn init(io: std.Io, allocator: std.mem.Allocator, meta: *App.Meta, config: AppConfig, app_ctx: *AppCtxType) !Self { const cache_config = config.cache; // Initialize unified component cache try zx.cache.init(allocator, .{ @@ -283,8 +284,9 @@ pub fn Handler(comptime AppCtxType: type) type { .meta = meta, .config = config, .allocator = allocator, - .page_cache = try PageCache.init(allocator, cache_config), + .page_cache = try PageCache.init(io, allocator, cache_config), .app_ctx = app_ctx, + .io = io, }; } @@ -294,7 +296,7 @@ pub fn Handler(comptime AppCtxType: type) type { pub fn dispatch(self: *Self, action: httpz.Action(*Self), req: *httpz.Request, res: *httpz.Response) !void { const is_dev = self.meta.cli_command == .dev; - var timer = if (is_dev) try std.time.Timer.start() else null; + var start_time = if (is_dev) std.Io.Timestamp.now(self.io, .awake) else std.Io.Timestamp.zero; // Reset proxy status for this request (dev mode tracking) if (is_dev) ProxyStatus.reset(); @@ -309,7 +311,8 @@ pub fn Handler(comptime AppCtxType: type) type { // Dev mode logging (skip noisy paths) if (is_dev and !isNoisyPath(req.url.path)) { - const elapsed_ns = timer.?.lap(); + const end_time = std.Io.Timestamp.now(self.io, .awake); + const elapsed_ns = start_time.durationTo(end_time).nanoseconds; const elapsed_ms = @as(f64, @floatFromInt(elapsed_ns)) / @as(f64, @floatFromInt(std.time.ns_per_ms)); const c = struct { const reset_c = "\x1b[0m"; @@ -707,7 +710,7 @@ 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, res: *httpz.Response, page_component: *Component, arena: std.mem.Allocator) !void { + fn renderStreaming(self: *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, .{ .base_path = zx_options.app_base_path }) catch |err| { std.debug.print("Error streaming page: {}\n", .{err}); @@ -818,7 +821,7 @@ pub fn Handler(comptime AppCtxType: type) type { } } if (completed < async_components.len and !connection_closed) { - std.Thread.sleep(5 * std.time.ns_per_ms); + _ = try std.Io.sleep(self.io, std.Io.Duration.fromMilliseconds(5), .awake); } } @@ -838,13 +841,10 @@ pub fn Handler(comptime AppCtxType: type) type { } pub inline fn static(self: *Self, req: *httpz.Request, res: *httpz.Response) !void { - const allocator_s = self.allocator; + const assets_path = try std.fs.path.join(self.allocator, &.{ zx_options.staticdir, req.url.path }); + defer self.allocator.free(assets_path); - const rootdir = self.meta.rootdir orelse zx_options.staticdir; - const assets_path = try std.fs.path.join(allocator_s, &.{ rootdir, req.url.path }); - defer allocator_s.free(assets_path); - - const body = std.fs.cwd().readFileAlloc(allocator_s, assets_path, std.math.maxInt(usize)) catch |err| { + const body = std.Io.Dir.cwd().readFileAlloc(self.io, assets_path, self.allocator, .unlimited) catch |err| { switch (err) { error.FileNotFound => return self.notFound(req, res), else => return self.uncaughtError(req, res, err), diff --git a/src/runtime/server/pubsub.zig b/src/runtime/server/pubsub.zig index 8608d399..8ef41837 100644 --- a/src/runtime/server/pubsub.zig +++ b/src/runtime/server/pubsub.zig @@ -85,7 +85,7 @@ pub const PubSub = struct { /// Maps topic names to sets of subscribers topics: std.StringHashMapUnmanaged(SubscriberSet) = .{}, /// RwLock for concurrent access (readers don't block each other) - lock: std.Thread.RwLock = .{}, + lock: std.Io.RwLock = .init, /// Allocator for topic keys and sets allocator: Allocator, @@ -112,8 +112,8 @@ pub const PubSub = struct { /// Add a subscriber to a topic pub fn addSubscriber(self: *PubSub, topic: []const u8, subscriber: *SubscriberData) void { - self.lock.lock(); - defer self.lock.unlock(); + self.lock.lockUncancelable(std.Options.debug_io); + defer self.lock.unlock(std.Options.debug_io); const result = self.topics.getOrPut(self.allocator, topic) catch return; if (!result.found_existing) { @@ -126,8 +126,8 @@ pub const PubSub = struct { /// Remove a subscriber from a topic pub fn removeSubscriber(self: *PubSub, topic: []const u8, subscriber: *SubscriberData) void { - self.lock.lock(); - defer self.lock.unlock(); + self.lock.lockUncancelable(std.Options.debug_io); + defer self.lock.unlock(std.Options.debug_io); if (self.topics.getPtr(topic)) |subscriber_set| { _ = subscriber_set.remove(subscriber); @@ -146,8 +146,8 @@ pub const PubSub = struct { /// Returns the number of messages sent pub fn publish(self: *PubSub, sender: ?*SubscriberData, topic: []const u8, message: []const u8) usize { // Use read lock - multiple publishers can run concurrently - self.lock.lockShared(); - defer self.lock.unlockShared(); + self.lock.lockSharedUncancelable(std.Options.debug_io); + defer self.lock.unlockShared(std.Options.debug_io); var sent: usize = 0; @@ -173,8 +173,8 @@ pub const PubSub = struct { /// Get the number of subscribers for a topic pub fn subscriberCount(self: *PubSub, topic: []const u8) usize { - self.lock.lockShared(); - defer self.lock.unlockShared(); + self.lock.lockSharedUncancelable(std.Options.debug_io); + defer self.lock.unlockShared(std.Options.debug_io); if (self.topics.get(topic)) |subscriber_set| { return subscriber_set.count(); diff --git a/src/runtime/server/registry.zig b/src/runtime/server/registry.zig index 46a34885..fc4d0c11 100644 --- a/src/runtime/server/registry.zig +++ b/src/runtime/server/registry.zig @@ -34,7 +34,7 @@ fn LinearMap(comptime V: type) type { }; } -var mu: std.Thread.Mutex = .{}; +var mu: std.Io.Mutex = .init; var event_map: LinearMap(ServerEventFn) = .{}; const ActionEntry = struct { @@ -47,8 +47,8 @@ var action_entries: std.ArrayListUnmanaged(ActionEntry) = .empty; /// Register an action handler for a route and return its stable action ID. pub fn register(route_path: []const u8, preferred_action_id: u32, action_fn: ActionFn) u32 { - mu.lock(); - defer mu.unlock(); + mu.lockUncancelable(std.Options.debug_io); + defer mu.unlock(std.Options.debug_io); if (preferred_action_id != 0) { for (action_entries.items) |*entry| { @@ -83,8 +83,8 @@ pub fn register(route_path: []const u8, preferred_action_id: u32, action_fn: Act } pub fn registerEvent(route_path: []const u8, handler_id: u32, event_fn: ServerEventFn) void { - mu.lock(); - defer mu.unlock(); + mu.lockUncancelable(std.Options.debug_io); + defer mu.unlock(std.Options.debug_io); // Composite key: "route_path:handler_id" var buf: [1024]u8 = undefined; @@ -95,8 +95,8 @@ pub fn registerEvent(route_path: []const u8, handler_id: u32, event_fn: ServerEv } pub fn getEvent(route_path: []const u8, handler_id: u32) ?ServerEventFn { - mu.lock(); - defer mu.unlock(); + mu.lockUncancelable(std.Options.debug_io); + defer mu.unlock(std.Options.debug_io); var buf: [1024]u8 = undefined; const key = std.fmt.bufPrint(&buf, "{s}:{d}", .{ route_path, handler_id }) catch return null; @@ -106,8 +106,8 @@ pub fn getEvent(route_path: []const u8, handler_id: u32) ?ServerEventFn { /// Look up a registered action handler by route path and action ID. pub fn get(route_path: []const u8, action_id: u32) ?ActionFn { - mu.lock(); - defer mu.unlock(); + mu.lockUncancelable(std.Options.debug_io); + defer mu.unlock(std.Options.debug_io); for (action_entries.items) |entry| { if (entry.action_id == action_id and std.mem.eql(u8, entry.route_path, route_path)) { diff --git a/src/runtime/server/render.zig b/src/runtime/server/render.zig index 5ea68059..868f8462 100644 --- a/src/runtime/server/render.zig +++ b/src/runtime/server/render.zig @@ -17,14 +17,14 @@ pub const AsyncComponent = struct { component: zx.Component, pub fn renderScript(self: AsyncComponent, allocator: std.mem.Allocator) ![]const u8 { - var aw = std.io.Writer.Allocating.init(allocator); + var aw = std.Io.Writer.Allocating.init(allocator); errdefer aw.deinit(); try self.component.render(&aw.writer, .{}); const html = aw.written(); // Build minimal script: - var script_writer = std.io.Writer.Allocating.init(allocator); + var script_writer = std.Io.Writer.Allocating.init(allocator); errdefer script_writer.deinit(); try script_writer.writer.print("