mirror of
https://github.com/ziex-dev/ziex.git
synced 2026-07-22 11:39:34 -06:00
Few regressions bugs are there but merging to main to continue development on main and will make release only after all known bugs related to Zig 0.16.0 has been fixed.
- [x] Upstream http.zig - Core web server and routing - [PR](https://github.com/karlseguin/http.zig/pull/191)
- [x] Upstream zli - CLI Parser - remove dependancies and use [proposed std.cli](4b4d625ca9/lib/std/cli.zig)
- [x] Upstream zig-tree-sitter - [PR](https://github.com/tree-sitter/zig-tree-sitter/pull/40)
- [x] Core Ziex Language
- [x] Core Ziex app
- [x] Core Ziex CLI
- [x] Ziex Dev Server (zx dev)
- [x] LSP
- [x] Playground
- [x] Upgrade to Zig 0.16.0
- [x] Fix WASM Regression
- [ ] Fix regressions
- [ ] Runtime: File Form
- [x] CLI: Export
- [x] CLI: Bundle
- [ ] Runtime: Websocket
- [ ] DevServer: Error Overlay
- [x] WASI Build
434 lines
19 KiB
Zig
434 lines
19 KiB
Zig
const std = @import("std");
|
|
const astgen = @import("astgen.zig");
|
|
|
|
// WebIDL primitive -> Zig type mapping.
|
|
// Returns null to signal "skip this field" for unsupported types.
|
|
fn idlToZig(idl_type: []const u8, nullable: bool) ?[]const u8 {
|
|
const inner: []const u8 = blk: {
|
|
if (eql(idl_type, "boolean")) break :blk "bool";
|
|
if (eql(idl_type, "long") or eql(idl_type, "signed long")) break :blk "i32";
|
|
if (eql(idl_type, "unsigned long")) break :blk "u32";
|
|
if (eql(idl_type, "short")) break :blk "i16";
|
|
if (eql(idl_type, "unsigned short")) break :blk "u16";
|
|
if (eql(idl_type, "long long")) break :blk "i64";
|
|
if (eql(idl_type, "unsigned long long")) break :blk "u64";
|
|
if (eql(idl_type, "octet") or eql(idl_type, "byte")) break :blk "u8";
|
|
if (eql(idl_type, "double") or
|
|
eql(idl_type, "float") or
|
|
eql(idl_type, "DOMHighResTimeStamp") or
|
|
eql(idl_type, "CSSNumberish")) break :blk "f64";
|
|
if (eql(idl_type, "DOMString") or
|
|
eql(idl_type, "USVString") or
|
|
eql(idl_type, "CSSOMString")) break :blk "[]const u8";
|
|
// EventTarget gets a common-fields sub-struct (see EventTarget decl below)
|
|
if (eql(idl_type, "EventTarget") or
|
|
eql(idl_type, "Element") or
|
|
eql(idl_type, "HTMLElement") or
|
|
eql(idl_type, "Node")) break :blk "EventTarget";
|
|
// Anything else (exotic interface, union, array, promise, …) is skipped
|
|
return null;
|
|
};
|
|
|
|
if (nullable) {
|
|
// Allocate a short buffer — caller uses arena so leaking is fine
|
|
const buf = std.heap.page_allocator.alloc(u8, inner.len + 1) catch return null;
|
|
buf[0] = '?';
|
|
@memcpy(buf[1..], inner);
|
|
return buf;
|
|
}
|
|
return inner;
|
|
}
|
|
|
|
fn eql(a: []const u8, b: []const u8) bool {
|
|
return std.mem.eql(u8, a, b);
|
|
}
|
|
|
|
fn isZigKeyword(name: []const u8) bool {
|
|
const keywords = [_][]const u8{
|
|
"addrspace", "align", "allowzero", "and", "anyframe", "anytype",
|
|
"asm", "async", "await", "break", "callconv", "catch",
|
|
"comptime", "const", "continue", "defer", "else", "enum",
|
|
"errdefer", "error", "export", "extern", "fn", "for",
|
|
"if", "inline", "linksection", "noalias", "noinline", "nosuspend",
|
|
"opaque", "or", "orelse", "packed", "pub", "resume",
|
|
"return", "struct", "suspend", "switch", "test", "threadlocal",
|
|
"try", "union", "unreachable", "usingnamespace", "var", "volatile",
|
|
"while",
|
|
};
|
|
for (keywords) |keyword| {
|
|
if (eql(name, keyword)) return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
fn zigIdent(allocator: std.mem.Allocator, name: []const u8) ![]const u8 {
|
|
if (isValidIdent(name) and !isZigKeyword(name)) return name;
|
|
return try std.fmt.allocPrint(allocator, "@\"{s}\"", .{name});
|
|
}
|
|
|
|
// Converts a camelCase WebIDL identifier to snake_case for Zig field naming.
|
|
// Runs of uppercase letters are treated as acronyms, so `blockedURI` → `blocked_uri`
|
|
// and `newURL` → `new_url`. The original camelCase is preserved separately as the
|
|
// JS property name to read from the underlying DOM event object.
|
|
fn camelToSnake(allocator: std.mem.Allocator, name: []const u8) ![]const u8 {
|
|
var out: std.ArrayListUnmanaged(u8) = .empty;
|
|
for (name, 0..) |c, i| {
|
|
const is_upper = c >= 'A' and c <= 'Z';
|
|
if (is_upper and i > 0) {
|
|
const prev = name[i - 1];
|
|
const prev_is_lower = prev >= 'a' and prev <= 'z';
|
|
const next_is_lower = (i + 1 < name.len) and name[i + 1] >= 'a' and name[i + 1] <= 'z';
|
|
const prev_is_upper = prev >= 'A' and prev <= 'Z';
|
|
if (prev_is_lower or (prev_is_upper and next_is_lower)) {
|
|
try out.append(allocator, '_');
|
|
}
|
|
}
|
|
if (is_upper) {
|
|
try out.append(allocator, c - 'A' + 'a');
|
|
} else {
|
|
try out.append(allocator, c);
|
|
}
|
|
}
|
|
return out.toOwnedSlice(allocator);
|
|
}
|
|
|
|
// A parsed interface: flat list of (name, zig_type) after resolving inheritance.
|
|
const Interface = struct {
|
|
name: []const u8,
|
|
fields: std.ArrayListUnmanaged(Field),
|
|
href: []const u8 = "",
|
|
|
|
const Field = struct {
|
|
name: []const u8,
|
|
js_name: []const u8,
|
|
zig_type: []const u8,
|
|
};
|
|
};
|
|
|
|
pub fn main() !void {
|
|
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
|
|
defer _ = gpa.deinit();
|
|
const allocator = gpa.allocator();
|
|
const source = try generate(allocator);
|
|
defer allocator.free(source);
|
|
const file = try std.Io.Dir.cwd().createFile("src/runtime/client/events/generated.zig", .{});
|
|
defer file.close();
|
|
try file.writeAll(source);
|
|
std.debug.print("Generated src/runtime/client/events/generated.zig\n", .{});
|
|
}
|
|
|
|
pub fn generate(allocator: std.mem.Allocator) ![]const u8 {
|
|
var arena = std.heap.ArenaAllocator.init(allocator);
|
|
defer arena.deinit();
|
|
const a = arena.allocator();
|
|
|
|
// -------------------------------------------------------------------------
|
|
// Step 1: build a global name->members map by scanning all idlparsed specs.
|
|
// Key: interface name, Value: array of raw member JSON objects.
|
|
// -------------------------------------------------------------------------
|
|
var raw_members = std.StringHashMap(
|
|
std.ArrayListUnmanaged(std.json.Value),
|
|
).init(a);
|
|
var raw_inheritance = std.StringHashMap([]const u8).init(a);
|
|
|
|
var spec_dir = try std.Io.Dir.cwd().openDir("vendor/webref/ed/idlparsed", .{ .iterate = true });
|
|
defer spec_dir.close();
|
|
var it = spec_dir.iterate();
|
|
while (try it.next()) |entry| {
|
|
if (!std.mem.endsWith(u8, entry.name, ".json")) continue;
|
|
const f = try spec_dir.openFile(entry.name, .{});
|
|
defer f.close();
|
|
const content = try f.readToEndAlloc(a, 8 * 1024 * 1024);
|
|
const parsed = std.json.parseFromSlice(std.json.Value, a, content, .{}) catch continue;
|
|
const idlp = parsed.value.object.get("idlparsed") orelse continue;
|
|
const idl_names = (idlp.object.get("idlNames") orelse continue).object;
|
|
|
|
for (idl_names.keys(), idl_names.values()) |name, def| {
|
|
const def_type = (def.object.get("type") orelse continue).string;
|
|
if (!eql(def_type, "interface")) continue;
|
|
if (!std.mem.endsWith(u8, name, "Event")) continue;
|
|
if (std.mem.indexOf(u8, name, "Init") != null) continue;
|
|
|
|
// Record inheritance
|
|
if (def.object.get("inheritance")) |inh| {
|
|
if (inh != .null and inh != .string) {} else if (inh == .string) {
|
|
try raw_inheritance.put(name, inh.string);
|
|
}
|
|
}
|
|
|
|
// Collect attribute members — merge with existing if partial
|
|
const members_json = (def.object.get("members") orelse continue).array;
|
|
const entry2 = try raw_members.getOrPut(name);
|
|
if (!entry2.found_existing) {
|
|
entry2.value_ptr.* = .empty;
|
|
}
|
|
for (members_json.items) |m| {
|
|
const mtype = (m.object.get("type") orelse continue).string;
|
|
if (!eql(mtype, "attribute")) continue;
|
|
try entry2.value_ptr.append(a, m);
|
|
}
|
|
}
|
|
|
|
// Also handle idlExtendedNames (partials)
|
|
const idl_ext = (idlp.object.get("idlExtendedNames") orelse continue).object;
|
|
for (idl_ext.keys(), idl_ext.values()) |name, partials| {
|
|
if (!std.mem.endsWith(u8, name, "Event")) continue;
|
|
if (std.mem.indexOf(u8, name, "Init") != null) continue;
|
|
for (partials.array.items) |partial| {
|
|
const members_json = (partial.object.get("members") orelse continue).array;
|
|
const entry2 = try raw_members.getOrPut(name);
|
|
if (!entry2.found_existing) {
|
|
entry2.value_ptr.* = .empty;
|
|
}
|
|
for (members_json.items) |m| {
|
|
const mtype = (m.object.get("type") orelse continue).string;
|
|
if (!eql(mtype, "attribute")) continue;
|
|
try entry2.value_ptr.append(a, m);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// -------------------------------------------------------------------------
|
|
// Step 2: flatten inheritance chains into resolved Interface structs.
|
|
// -------------------------------------------------------------------------
|
|
var interfaces = std.StringHashMap(Interface).init(a);
|
|
|
|
var key_it = raw_members.keyIterator();
|
|
while (key_it.next()) |key| {
|
|
const name = key.*;
|
|
if (interfaces.contains(name)) continue;
|
|
try resolveInterface(name, a, &raw_members, &raw_inheritance, &interfaces);
|
|
}
|
|
|
|
// -------------------------------------------------------------------------
|
|
// Step 3: load events.json → event-name to interface map
|
|
// -------------------------------------------------------------------------
|
|
const events_file = try std.Io.Dir.cwd().openFile("vendor/webref/ed/events.json", .{});
|
|
defer events_file.close();
|
|
const events_content = try events_file.readToEndAlloc(a, 4 * 1024 * 1024);
|
|
const events_parsed = try std.json.parseFromSlice(std.json.Value, a, events_content, .{});
|
|
const events_array = events_parsed.value.array;
|
|
|
|
// Deduplicate: one canonical event name -> interface name
|
|
var event_map = std.StringHashMap([]const u8).init(a);
|
|
for (events_array.items) |ev| {
|
|
const ev_type = (ev.object.get("type") orelse continue).string;
|
|
const iface = (ev.object.get("interface") orelse continue).string;
|
|
if (!event_map.contains(ev_type)) {
|
|
try event_map.put(ev_type, iface);
|
|
}
|
|
}
|
|
|
|
// -------------------------------------------------------------------------
|
|
// Step 4: emit Zig source via astgen
|
|
// -------------------------------------------------------------------------
|
|
var file = astgen.File.init(allocator);
|
|
defer file.deinit();
|
|
|
|
try file.addRaw(
|
|
\\//! Generated by tools/codegen/events.zig — do not edit manually.
|
|
\\//! Run `zig build eventsgen` to regenerate.
|
|
\\//!
|
|
\\//! Provides typed Zig structs for all browser DOM events.
|
|
\\//! Use with `event.as(events.MouseEvent, allocator)`.
|
|
\\const std = @import("std");
|
|
);
|
|
|
|
// Emit EventTarget helper struct first. Field names are snake_case; the
|
|
// original camelCase DOM property names are resolved via `jsName` below.
|
|
try file.addRaw(
|
|
\\/// Minimal representation of EventTarget/Element for use as a nested field.
|
|
\\/// Contains the most commonly accessed DOM element properties.
|
|
\\pub const EventTarget = struct {
|
|
\\ tag_name: []const u8 = "",
|
|
\\ id: []const u8 = "",
|
|
\\ name: []const u8 = "",
|
|
\\ value: []const u8 = "",
|
|
\\ @"type": []const u8 = "",
|
|
\\ checked: bool = false,
|
|
\\ disabled: bool = false,
|
|
\\};
|
|
);
|
|
|
|
// Track snake_case → camelCase for every field whose names differ, so that
|
|
// `Event.as(T, …)` can translate Zig field names back to the JS property
|
|
// name when reading from the native DOM event object.
|
|
var js_name_pairs = std.StringHashMap([]const u8).init(a);
|
|
try js_name_pairs.put("tag_name", "tagName");
|
|
|
|
// Emit each interface struct
|
|
var iface_it = interfaces.iterator();
|
|
while (iface_it.next()) |iface_entry| {
|
|
const iface = iface_entry.value_ptr.*;
|
|
if (iface.fields.items.len == 0) continue;
|
|
const container = try file.addStruct(iface.name);
|
|
for (iface.fields.items) |field| {
|
|
// For string types we need a default of "" so zero-init works
|
|
const default: ?[]const u8 = blk: {
|
|
if (eql(field.zig_type, "[]const u8")) break :blk "\"\"";
|
|
if (eql(field.zig_type, "bool")) break :blk "false";
|
|
if (std.mem.startsWith(u8, field.zig_type, "?")) break :blk "null";
|
|
if (eql(field.zig_type, "EventTarget")) break :blk ".{}";
|
|
break :blk "0";
|
|
};
|
|
try container.addField(file.arena.allocator(), "", try zigIdent(file.arena.allocator(), field.name), field.zig_type, default);
|
|
if (!eql(field.name, field.js_name)) {
|
|
_ = try js_name_pairs.getOrPutValue(field.name, field.js_name);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Collect (event_name, iface_name) pairs that have a backing struct so we can
|
|
// emit the aliases, the `Kind` enum, and the `Data` switch in one pass.
|
|
const Pair = struct { ev: []const u8, iface: []const u8 };
|
|
var pairs: std.ArrayListUnmanaged(Pair) = .empty;
|
|
var ev_it = event_map.iterator();
|
|
while (ev_it.next()) |entry| {
|
|
const ev_name = entry.key_ptr.*;
|
|
const iface_name = entry.value_ptr.*;
|
|
if (!interfaces.contains(iface_name)) continue;
|
|
if (interfaces.get(iface_name).?.fields.items.len == 0) continue;
|
|
try pairs.append(a, .{ .ev = ev_name, .iface = iface_name });
|
|
}
|
|
|
|
// Emit event name -> struct alias namespace
|
|
var aliases: std.ArrayListUnmanaged(u8) = .empty;
|
|
const w = aliases.writer(a);
|
|
try w.writeAll("/// Maps browser event names to their typed structs.\n");
|
|
try w.writeAll("/// Example: `event.as(events.click, allocator)` or `event.as(events.MouseEvent, allocator)`.\n");
|
|
try w.writeAll("pub const events = struct {\n");
|
|
for (pairs.items) |p| {
|
|
const safe = isValidIdent(p.ev) and !isZigKeyword(p.ev);
|
|
if (safe) {
|
|
try w.print(" pub const {s} = {s};\n", .{ p.ev, p.iface });
|
|
} else {
|
|
try w.print(" pub const @\"{s}\" = {s};\n", .{ p.ev, p.iface });
|
|
}
|
|
}
|
|
try w.writeAll("};\n\n");
|
|
|
|
// Emit an explicit `Kind` enum. This is what gives editors like ZLS proper
|
|
// dot-completion on `event.data(.…)` — an enum derived via
|
|
// `std.meta.DeclEnum` at comptime does not currently surface its tags to
|
|
// the language server, so we declare the tags literally.
|
|
try w.writeAll("/// Enum of all supported DOM event names. Use with `event.data(.kind, …)`.\n");
|
|
try w.writeAll("pub const Kind = enum {\n");
|
|
for (pairs.items) |p| {
|
|
const safe = isValidIdent(p.ev) and !isZigKeyword(p.ev);
|
|
if (safe) {
|
|
try w.print(" {s},\n", .{p.ev});
|
|
} else {
|
|
try w.print(" @\"{s}\",\n", .{p.ev});
|
|
}
|
|
}
|
|
try w.writeAll("};\n\n");
|
|
|
|
// Emit a `Data` function mapping each `Kind` tag to its typed struct.
|
|
// This mirrors `@field(events, @tagName(kind))` but keeps the mapping in
|
|
// one generated place so consumers can `const T = generated.Data(.click);`.
|
|
try w.writeAll("/// Returns the typed struct corresponding to a given event `Kind`.\n");
|
|
try w.writeAll("pub fn Data(comptime kind: Kind) type {\n");
|
|
try w.writeAll(" return switch (kind) {\n");
|
|
for (pairs.items) |p| {
|
|
const safe = isValidIdent(p.ev) and !isZigKeyword(p.ev);
|
|
if (safe) {
|
|
try w.print(" .{s} => {s},\n", .{ p.ev, p.iface });
|
|
} else {
|
|
try w.print(" .@\"{s}\" => {s},\n", .{ p.ev, p.iface });
|
|
}
|
|
}
|
|
try w.writeAll(" };\n");
|
|
try w.writeAll("}\n\n");
|
|
|
|
// Emit the snake_case → camelCase map used by `Event.as` at runtime to
|
|
// translate Zig field names back to the original DOM property names.
|
|
try w.writeAll("/// Maps snake_case Zig field names to their camelCase DOM property names.\n");
|
|
try w.writeAll("/// Fields whose snake_case equals the DOM name are omitted; callers should\n");
|
|
try w.writeAll("/// fall back to the original identifier when no mapping is present.\n");
|
|
try w.writeAll("pub const js_field_names = std.StaticStringMap([]const u8).initComptime(.{\n");
|
|
var pair_it = js_name_pairs.iterator();
|
|
// Sort entries for deterministic output.
|
|
var sorted_keys: std.ArrayListUnmanaged([]const u8) = .empty;
|
|
while (pair_it.next()) |e| try sorted_keys.append(a, e.key_ptr.*);
|
|
std.mem.sort([]const u8, sorted_keys.items, {}, struct {
|
|
fn lt(_: void, l: []const u8, r: []const u8) bool {
|
|
return std.mem.lessThan(u8, l, r);
|
|
}
|
|
}.lt);
|
|
for (sorted_keys.items) |k| {
|
|
const v = js_name_pairs.get(k).?;
|
|
try w.print(" .{{ \"{s}\", \"{s}\" }},\n", .{ k, v });
|
|
}
|
|
try w.writeAll("});\n\n");
|
|
try w.writeAll("/// Resolves the DOM property name for a given Zig field name.\n");
|
|
try w.writeAll("/// Returns the mapped camelCase identifier, or the input unchanged when none exists.\n");
|
|
try w.writeAll("pub fn jsName(name: []const u8) []const u8 {\n");
|
|
try w.writeAll(" return js_field_names.get(name) orelse name;\n");
|
|
try w.writeAll("}");
|
|
|
|
try file.addRaw(try aliases.toOwnedSlice(a));
|
|
|
|
return try file.finish();
|
|
}
|
|
|
|
fn isValidIdent(name: []const u8) bool {
|
|
if (name.len == 0) return false;
|
|
for (name, 0..) |c, i| {
|
|
const ok = (c >= 'a' and c <= 'z') or
|
|
(c >= 'A' and c <= 'Z') or
|
|
c == '_' or
|
|
(i > 0 and c >= '0' and c <= '9');
|
|
if (!ok) return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
fn resolveInterface(
|
|
name: []const u8,
|
|
a: std.mem.Allocator,
|
|
raw_members: *std.StringHashMap(std.ArrayListUnmanaged(std.json.Value)),
|
|
raw_inheritance: *std.StringHashMap([]const u8),
|
|
out: *std.StringHashMap(Interface),
|
|
) !void {
|
|
if (out.contains(name)) return;
|
|
|
|
var fields: std.ArrayListUnmanaged(Interface.Field) = .empty;
|
|
var seen_names = std.StringHashMap(void).init(a);
|
|
|
|
// Walk inheritance chain depth-first: child fields first, then parent
|
|
var current: ?[]const u8 = name;
|
|
while (current) |iname| {
|
|
if (raw_members.get(iname)) |members| {
|
|
for (members.items) |m| {
|
|
const field_name = (m.object.get("name") orelse continue).string;
|
|
if (seen_names.contains(field_name)) continue;
|
|
|
|
const idt = m.object.get("idlType") orelse continue;
|
|
const generic = (idt.object.get("generic") orelse continue).string;
|
|
// Skip arrays/promises/sequences
|
|
if (generic.len > 0) continue;
|
|
const is_union = (idt.object.get("union") orelse continue).bool;
|
|
if (is_union) continue;
|
|
const nullable = (idt.object.get("nullable") orelse continue).bool;
|
|
const raw_type = idt.object.get("idlType") orelse continue;
|
|
if (raw_type != .string) continue;
|
|
|
|
const zig_type = idlToZig(raw_type.string, nullable) orelse continue;
|
|
|
|
try seen_names.put(field_name, {});
|
|
const snake = try camelToSnake(a, field_name);
|
|
try fields.append(a, .{ .name = snake, .js_name = field_name, .zig_type = zig_type });
|
|
}
|
|
}
|
|
current = raw_inheritance.get(iname);
|
|
// Break cycles
|
|
if (current != null and std.mem.eql(u8, current.?, name)) break;
|
|
}
|
|
|
|
try out.put(name, .{
|
|
.name = try a.dupe(u8, name),
|
|
.fields = fields,
|
|
});
|
|
}
|