aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorNathan Reiner <nathan@nathanreiner.xyz>2026-05-20 17:29:28 +0200
committerNathan Reiner <nathan@nathanreiner.xyz>2026-05-20 17:29:28 +0200
commita7d0a6c409850b3603abb162fa6fd22d2dfd0177 (patch)
tree6ea221fb7b1d314409b897b818a33e7fccd51626
parent755c794883467609e425207f6a543d2e8c6d6b46 (diff)
frontend: add comptime bundler
-rw-r--r--src/frontend/Component.zig30
-rw-r--r--src/frontend/Document.zig46
-rw-r--r--src/frontend/File.zig91
-rw-r--r--src/frontend/Head.zig58
-rw-r--r--src/frontend/root.zig2
-rw-r--r--src/frontend/web/hello-world/index.css7
-rw-r--r--src/frontend/web/hello-world/index.html3
-rw-r--r--src/frontend/web/hello-world/index.js3
-rw-r--r--src/frontend/web/index.css4
-rw-r--r--src/frontend/web/index.html1
-rw-r--r--src/frontend/z-component.js17
11 files changed, 262 insertions, 0 deletions
diff --git a/src/frontend/Component.zig b/src/frontend/Component.zig
new file mode 100644
index 0000000..d90ed3b
--- /dev/null
+++ b/src/frontend/Component.zig
@@ -0,0 +1,30 @@
+const std = @import("std");
+
+const File = @import("File.zig");
+
+const Self = @This();
+
+name: []const u8,
+parent_dir: []const u8,
+
+pub fn path(p: []const u8) Self {
+ const name = p[if (std.mem.lastIndexOfScalar(
+ u8,
+ std.mem.trim(u8, p, "/"),
+ '/',
+ )) |index| index + 1 else 0..];
+
+ return .{
+ .name = name,
+ .parent_dir = p,
+ };
+}
+
+pub fn to_html_string(comptime self: Self) []const u8 {
+ const html: File.Html = .minify(self.parent_dir ++ "/index.html");
+ const js: File.Script = .minify(self.parent_dir ++ "/index.js");
+ const css: File.Style = .minify(self.parent_dir ++ "/index.css");
+ return "<template id=\"--" ++ self.name ++ "\">" ++ css.to_html_string() ++ html.to_html_string() ++ js.to_html_string() ++ "</template>";
+}
+
+test {}
diff --git a/src/frontend/Document.zig b/src/frontend/Document.zig
new file mode 100644
index 0000000..0ba46b1
--- /dev/null
+++ b/src/frontend/Document.zig
@@ -0,0 +1,46 @@
+const std = @import("std");
+
+const Component = @import("Component.zig");
+const Head = @import("Head.zig");
+const File = @import("File.zig");
+
+const Self = @This();
+
+components: []const Component,
+head: Head,
+styles: []const File.Style,
+contents: []const File.Html,
+
+pub const empty: Self = .{
+ .head = .empty,
+ .components = &.{},
+ .styles = &.{},
+ .contents = &.{},
+};
+
+pub fn to_string(comptime self: Self) []const u8 {
+ comptime var content: []const u8 = "<!DOCTYPE html><html>";
+ content = content ++ comptime self.head.to_html_string(self.components, self.styles);
+ content = content ++ "<body>";
+
+ inline for (self.contents) |c| {
+ content = content ++ comptime c.to_html_string();
+ }
+
+ content = content ++ "</body></html>";
+
+ return content;
+}
+
+test {
+ const document: Self = comptime .{
+ .head = .default("test"),
+ .components = &.{
+ .path("./web/hello-world"),
+ },
+ .styles = &.{},
+ .contents = &.{.minify("./web/index.html")},
+ };
+
+ std.debug.print("{s}", .{document.to_string()});
+}
diff --git a/src/frontend/File.zig b/src/frontend/File.zig
new file mode 100644
index 0000000..e7c3b6d
--- /dev/null
+++ b/src/frontend/File.zig
@@ -0,0 +1,91 @@
+const std = @import("std");
+
+pub fn File(comptime tag: ?[]const u8, Iterator: type) type {
+ const opening = if (tag) |t| "<" ++ t ++ ">" else "";
+ const closing = if (tag) |t| "</" ++ t ++ ">" else "";
+
+ return struct {
+ file: []const u8,
+ do_minify: bool,
+
+ pub fn path(comptime p: []const u8) @This() {
+ return .{
+ .file = p,
+ .do_minify = false,
+ };
+ }
+
+ pub fn minify(comptime p: []const u8) @This() {
+ return .{
+ .file = p,
+ .do_minify = true,
+ };
+ }
+
+ fn content(comptime self: @This()) []const u8 {
+ return opening ++ @embedFile(self.file) ++ closing;
+ }
+
+ fn content_minified(comptime self: @This()) []const u8 {
+ var minified: []const u8 = "";
+ var iterator: Iterator = .init(@embedFile(self.file));
+
+ while (iterator.next()) |segment| {
+ minified = minified ++ segment;
+ }
+
+ return opening ++ minified ++ closing;
+ }
+
+ pub fn to_html_string(comptime self: @This()) []const u8 {
+ return if (self.do_minify) self.content_minified() else self.content();
+ }
+ };
+}
+
+fn StringIterator(comptime quotes: []const u8) type {
+ return struct {
+ content: []const u8,
+
+ pub fn init(content: []const u8) @This() {
+ return .{ .content = content };
+ }
+
+ pub fn next(self: *@This()) ?[]const u8 {
+ @setEvalBranchQuota(self.content.len * 20);
+
+ self.content = std.mem.trimStart(u8, self.content, &std.ascii.whitespace);
+
+ if (self.content.len == 0) return null;
+
+ var index: usize = 0;
+
+ while (self.content.len > index) {
+ const char = self.content[index];
+
+ if (std.mem.indexOfScalar(u8, quotes, char) != null) {
+ index += 1;
+ while (self.content.len > index) {
+ if (self.content[index] == char) {
+ index += 1;
+ break;
+ }
+ index += 1;
+ }
+ } else if (std.ascii.isWhitespace(char)) {
+ break;
+ }
+ index += 1;
+ }
+
+ const block = self.content[0..index];
+ self.content = self.content[index..];
+
+ return block ++ " ";
+ }
+ };
+}
+
+pub const Style = File("style", StringIterator("\"'"));
+pub const Script = File("script", StringIterator("\"'`"));
+pub const Html = File(null, StringIterator("\"'"));
diff --git a/src/frontend/Head.zig b/src/frontend/Head.zig
new file mode 100644
index 0000000..0eb090f
--- /dev/null
+++ b/src/frontend/Head.zig
@@ -0,0 +1,58 @@
+const File = @import("File.zig");
+const Component = @import("Component.zig");
+
+const z_component_code: File.Script = .minify("z-component.js");
+
+pub const Meta = struct {
+ name: []const u8,
+ content: []const u8,
+
+ pub fn to_html_string(comptime self: @This()) []const u8 {
+ return "<meta name=\"" ++ self.name ++ "\" content=\"" ++ self.content ++ "\">";
+ }
+};
+
+title: []const u8,
+meta: []const Meta,
+
+pub const empty: @This() = .{
+ .title = "",
+ .meta = &.{},
+};
+
+pub fn default(comptime title: []const u8) @This() {
+ return .{
+ .title = title,
+ .meta = &.{
+ .{
+ .name = "viewport",
+ .content = "width=device-width, initial-scale=1.0",
+ },
+ },
+ };
+}
+
+pub fn to_html_string(
+ comptime self: @This(),
+ comptime components: []const Component,
+ comptime styles: []const File.Style,
+) []const u8 {
+ var content: []const u8 = "<head>" ++ z_component_code.to_html_string();
+
+ content = content ++ "<title>" ++ self.title ++ "</title>";
+
+ inline for (components) |component| {
+ content = content ++ comptime component.to_html_string();
+ }
+
+ inline for (styles) |style| {
+ content = content ++ comptime style.to_html_string();
+ }
+
+ inline for (self.meta) |meta| {
+ content = content ++ comptime meta.to_html_string();
+ }
+
+ content = content ++ "</head>";
+ return content;
+}
diff --git a/src/frontend/root.zig b/src/frontend/root.zig
index f2cce4c..20c725e 100644
--- a/src/frontend/root.zig
+++ b/src/frontend/root.zig
@@ -2,6 +2,8 @@
//! which will be embedded right into the ELF file to improve deployment.
const std = @import("std");
+pub const Component = @import("Component.zig");
+pub const Document = @import("Document.zig");
test {
_ = std.testing.refAllDecls(@This());
diff --git a/src/frontend/web/hello-world/index.css b/src/frontend/web/hello-world/index.css
new file mode 100644
index 0000000..41b0b29
--- /dev/null
+++ b/src/frontend/web/hello-world/index.css
@@ -0,0 +1,7 @@
+#container {
+ background: #efefef;
+ padding: 10px;
+ border-radius: 4px;
+ user-select: none;
+ cursor: pointer;
+}
diff --git a/src/frontend/web/hello-world/index.html b/src/frontend/web/hello-world/index.html
new file mode 100644
index 0000000..a9b0be7
--- /dev/null
+++ b/src/frontend/web/hello-world/index.html
@@ -0,0 +1,3 @@
+<div id="container" onclick="clicked()">
+ Click Me!
+</div>
diff --git a/src/frontend/web/hello-world/index.js b/src/frontend/web/hello-world/index.js
new file mode 100644
index 0000000..8e55c52
--- /dev/null
+++ b/src/frontend/web/hello-world/index.js
@@ -0,0 +1,3 @@
+function clicked() {
+ console.log('clicked');
+}
diff --git a/src/frontend/web/index.css b/src/frontend/web/index.css
new file mode 100644
index 0000000..a93ea47
--- /dev/null
+++ b/src/frontend/web/index.css
@@ -0,0 +1,4 @@
+body {
+ padding: 0;
+ margin: 0;
+}
diff --git a/src/frontend/web/index.html b/src/frontend/web/index.html
new file mode 100644
index 0000000..885f2c5
--- /dev/null
+++ b/src/frontend/web/index.html
@@ -0,0 +1 @@
+<z-component type="hello-world"></z-component>
diff --git a/src/frontend/z-component.js b/src/frontend/z-component.js
new file mode 100644
index 0000000..e9b63ff
--- /dev/null
+++ b/src/frontend/z-component.js
@@ -0,0 +1,17 @@
+customElements.define(
+ "z-component",
+ class extends HTMLElement {
+ constructor() {
+ super();
+ this.attachShadow({ mode: 'open' })
+ }
+
+ connectedCallback() {
+ const type = this.getAttribute('type');
+ const template = document.getElementById(`--${type}`);
+ const node = document.importNode(template.content, true);
+ this.shadowRoot.id = 'root';
+ this.shadowRoot.appendChild(node);
+ }
+ }
+);