From 036aec1682c219cfc4b07797f0dd4be7991595bf Mon Sep 17 00:00:00 2001 From: Bob Farrell Date: Sat, 23 Nov 2024 13:10:46 +0000 Subject: [PATCH 1/5] Misc. tidying --- .github/workflows/CI.yml | 2 -- demo/src/main.zig | 1 + 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 2b86562..f24529b 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -15,8 +15,6 @@ jobs: build: strategy: matrix: - #Deactivated windows for I don't know why it fails - #os: [ubuntu-latest, macos-latest, windows-latest] os: [ubuntu-latest, macos-latest, windows-latest] runs-on: ${{ matrix.os }} steps: diff --git a/demo/src/main.zig b/demo/src/main.zig index 3360536..f051ee5 100644 --- a/demo/src/main.zig +++ b/demo/src/main.zig @@ -90,6 +90,7 @@ pub const jetzig_options = struct { .same_site = true, .secure = true, .http_only = true, + .path = "/", }, }; From 8095bbcb76018c9f442e655a634f2f7f696b5a94 Mon Sep 17 00:00:00 2001 From: Bob Farrell Date: Sat, 23 Nov 2024 13:42:14 +0000 Subject: [PATCH 2/5] Implement `request.renderTemplate` Clean up request state. --- demo/src/app/views/render_template.zig | 15 ++++++++ demo/src/app/views/render_template/index.zmpl | 3 ++ src/jetzig/http/Request.zig | 34 +++++++++++-------- src/jetzig/http/Server.zig | 18 ++++++---- src/jetzig/http/middleware.zig | 7 ++-- src/jetzig/loggers/DevelopmentLogger.zig | 2 +- src/jetzig/loggers/ProductionLogger.zig | 2 +- 7 files changed, 57 insertions(+), 24 deletions(-) create mode 100644 demo/src/app/views/render_template.zig create mode 100644 demo/src/app/views/render_template/index.zmpl diff --git a/demo/src/app/views/render_template.zig b/demo/src/app/views/render_template.zig new file mode 100644 index 0000000..bb352bf --- /dev/null +++ b/demo/src/app/views/render_template.zig @@ -0,0 +1,15 @@ +const std = @import("std"); +const jetzig = @import("jetzig"); + +pub fn index(request: *jetzig.Request) !jetzig.View { + return request.renderTemplate("basic/index", .ok); +} + +test "index" { + var app = try jetzig.testing.app(std.testing.allocator, @import("routes")); + defer app.deinit(); + + const response = try app.request(.GET, "/render_template", .{}); + try response.expectStatus(.ok); + try response.expectBodyContains("Hello"); +} diff --git a/demo/src/app/views/render_template/index.zmpl b/demo/src/app/views/render_template/index.zmpl new file mode 100644 index 0000000..76457d0 --- /dev/null +++ b/demo/src/app/views/render_template/index.zmpl @@ -0,0 +1,3 @@ +
+ Content goes here +
diff --git a/src/jetzig/http/Request.zig b/src/jetzig/http/Request.zig index b864913..b8e74de 100644 --- a/src/jetzig/http/Request.zig +++ b/src/jetzig/http/Request.zig @@ -30,16 +30,11 @@ parsed_multipart: ?*jetzig.data.Data = null, _cookies: ?*jetzig.http.Cookies = null, _session: ?*jetzig.http.Session = null, body: []const u8 = undefined, -state: enum { initial, processed } = .initial, +state: enum { initial, rendered, redirected, failed, processed } = .initial, response_started: bool = false, dynamic_assigned_template: ?[]const u8 = null, layout: ?[]const u8 = null, layout_disabled: bool = false, -// TODO: Squash rendered/redirected/failed into -// `state: enum { initial, rendered, redirected, failed }` -rendered: bool = false, -redirected: bool = false, -failed: bool = false, redirect_state: ?RedirectState = null, middleware_rendered: ?struct { name: []const u8, action: []const u8 } = null, middleware_rendered_during_response: bool = false, @@ -191,9 +186,9 @@ pub fn data(self: Request, comptime root: @TypeOf(.enum_literal)) !*jetzig.Data. /// Render a response. This function can only be called once per request (repeat calls will /// trigger an error). pub fn render(self: *Request, status_code: jetzig.http.status_codes.StatusCode) jetzig.views.View { - if (self.rendered or self.failed) self.rendered_multiple = true; + if (self.state != .processed) self.rendered_multiple = true; - self.rendered = true; + self.state = .rendered; if (self.response_started) self.middleware_rendered_during_response = true; self.rendered_view = .{ .data = self.response_data, .status_code = status_code }; return self.rendered_view.?; @@ -202,10 +197,9 @@ pub fn render(self: *Request, status_code: jetzig.http.status_codes.StatusCode) /// Render an error. This function can only be called once per request (repeat calls will /// trigger an error). pub fn fail(self: *Request, status_code: jetzig.http.status_codes.StatusCode) jetzig.views.View { - if (self.rendered or self.redirected) self.rendered_multiple = true; + if (self.state != .processed) self.rendered_multiple = true; - self.rendered = true; - self.failed = true; + self.state = .failed; if (self.response_started) self.middleware_rendered_during_response = true; self.rendered_view = .{ .data = self.response_data, .status_code = status_code }; return self.rendered_view.?; @@ -224,10 +218,9 @@ pub fn redirect( location: []const u8, redirect_status: enum { moved_permanently, found }, ) jetzig.views.View { - if (self.rendered or self.failed) self.rendered_multiple = true; + if (self.state != .processed) self.rendered_multiple = true; - self.rendered = true; - self.redirected = true; + self.state = .redirected; if (self.response_started) self.middleware_rendered_during_response = true; const status_code = switch (redirect_status) { @@ -687,6 +680,19 @@ pub fn setTemplate(self: *Request, name: []const u8) void { self.dynamic_assigned_template = name; } +/// Set a custom template and render the response. +/// ```zig +/// return request.renderTemplate("blogs/comments/get", .ok); +/// ``` +pub fn renderTemplate( + self: *Request, + name: []const u8, + status_code: jetzig.http.StatusCode, +) jetzig.views.View { + self.dynamic_assigned_template = name; + return self.render(status_code); +} + pub fn joinPath(self: *const Request, args: anytype) ![]const u8 { const fields = std.meta.fields(@TypeOf(args)); var buf: [fields.len][]const u8 = undefined; diff --git a/src/jetzig/http/Server.zig b/src/jetzig/http/Server.zig index 819067a..28568fa 100644 --- a/src/jetzig/http/Server.zig +++ b/src/jetzig/http/Server.zig @@ -197,9 +197,9 @@ fn renderResponse(self: *Server, request: *jetzig.http.Request) !void { for (route.before_callbacks) |callback| { try callback(request, route); if (request.rendered_view) |view| { - if (request.failed) { + if (request.state == .failed) { request.setResponse(try self.renderError(request, view.status_code), .{}); - } else if (request.rendered) { + } else if (request.state == .rendered) { // TODO: Allow callbacks to set content } return; @@ -259,7 +259,9 @@ fn renderHTML( return request.setResponse(rendered_error, .{}); }; - return if (request.redirected or request.failed or request.dynamic_assigned_template != null) + return if (request.state == .redirected or + request.state == .failed or + request.dynamic_assigned_template != null) request.setResponse(rendered, .{}) else request.setResponse(try self.renderNotFound(request), .{}); @@ -327,7 +329,7 @@ fn renderView( return try self.renderInternalServerError(request, @errorReturnTrace(), err); }; - if (request.failed) { + if (request.state == .failed) { const view: jetzig.views.View = request.rendered_view orelse .{ .data = request.response_data, .status_code = .internal_server_error, @@ -343,7 +345,7 @@ fn renderView( if (request.rendered_multiple) return error.JetzigMultipleRenderError; if (request.rendered_view) |rendered_view| { - if (request.redirected) return .{ .view = rendered_view, .content = "" }; + if (request.state == .redirected) return .{ .view = rendered_view, .content = "" }; if (template) |capture| { return .{ @@ -351,13 +353,17 @@ fn renderView( .content = try self.renderTemplateWithLayout(request, capture, rendered_view, route), }; } else { + try self.logger.DEBUG( + "Missing template for route `{s}.{s}`. Expected: `src/app/views/{s}.zmpl`.", + .{ route.view_name, @tagName(route.action), route.template }, + ); return switch (request.requestFormat()) { .HTML, .UNKNOWN => try self.renderNotFound(request), .JSON => .{ .view = rendered_view, .content = "" }, }; } } else { - if (!request.redirected) { + if (request.state != .redirected) { try self.logger.WARN("`request.render` was not invoked. Rendering empty content.", .{}); } request.response_data.reset(); diff --git a/src/jetzig/http/middleware.zig b/src/jetzig/http/middleware.zig index a6feace..31dc50c 100644 --- a/src/jetzig/http/middleware.zig +++ b/src/jetzig/http/middleware.zig @@ -72,7 +72,7 @@ pub fn afterRequest(request: *jetzig.http.Request) !MiddlewareData { } else { try @call(.always_inline, middleware.afterRequest, .{request}); } - if (request.rendered or request.redirected) { + if (request.state != .initial) { request.middleware_rendered = .{ .name = @typeName(middleware), .action = "afterRequest" }; break; } @@ -103,7 +103,10 @@ pub fn beforeResponse( } } if (request.middleware_rendered_during_response) { - request.middleware_rendered = .{ .name = @typeName(middleware), .action = "beforeResponse" }; + request.middleware_rendered = .{ + .name = @typeName(middleware), + .action = "beforeResponse", + }; break; } } diff --git a/src/jetzig/loggers/DevelopmentLogger.zig b/src/jetzig/loggers/DevelopmentLogger.zig index 3ae6ba9..8ff0dbf 100644 --- a/src/jetzig/loggers/DevelopmentLogger.zig +++ b/src/jetzig/loggers/DevelopmentLogger.zig @@ -107,7 +107,7 @@ pub fn logRequest(self: DevelopmentLogger, request: *const jetzig.http.Request) if (request.middleware_rendered) |middleware| middleware.action else "", if (request.middleware_rendered) |_| jetzig.colors.codes.escape ++ jetzig.colors.codes.white ++ ":" else "", if (request.middleware_rendered) |_| jetzig.colors.codes.escape ++ jetzig.colors.codes.bright_cyan else "", - if (request.middleware_rendered) |_| if (request.redirected) "redirect" else "render" else "", + if (request.middleware_rendered) |_| @tagName(request.state) else "", if (request.middleware_rendered) |_| jetzig.colors.codes.escape ++ jetzig.colors.codes.reset else "", if (request.middleware_rendered) |_| "]" else "", request.path.path, diff --git a/src/jetzig/loggers/ProductionLogger.zig b/src/jetzig/loggers/ProductionLogger.zig index 6c7c51c..65c7ffb 100644 --- a/src/jetzig/loggers/ProductionLogger.zig +++ b/src/jetzig/loggers/ProductionLogger.zig @@ -86,7 +86,7 @@ pub fn logRequest(self: ProductionLogger, request: *const jetzig.http.Request) ! if (request.middleware_rendered) |_| ":" else "", if (request.middleware_rendered) |middleware| middleware.action else "", if (request.middleware_rendered) |_| ":" else "", - if (request.middleware_rendered) |_| if (request.redirected) "redirect" else "render" else "", + if (request.middleware_rendered) |_| @tagName(request.state) else "", if (request.middleware_rendered) |_| "]" else "", request.path.path, }, .stdout); From 1565ae3b737e9548c38465d8404690ad9383b53b Mon Sep 17 00:00:00 2001 From: Bob Farrell Date: Sat, 23 Nov 2024 18:07:19 +0000 Subject: [PATCH 3/5] Debug console Use `jetzig server` or `zig build -Ddebug_console=true run` to launch a development server with the debug console enabled. When an error is encountered a formatted stack trace is dumped to the browser along with the current response data. Both outputs are syntax-highlighted using Prism JS. --- LICENSE-Zig | 21 +++ build.zig | 14 ++ cli/commands/server.zig | 1 + demo/src/app/views/anti_csrf.zig | 7 +- demo/src/app/views/anti_csrf/index.zmpl | 2 + src/assets/debug.css | 121 +++++++++++++ src/assets/debug.js | 5 + src/jetzig.zig | 1 + src/jetzig/debug.zig | 218 ++++++++++++++++++++++++ src/jetzig/http/Server.zig | 87 ++++++++-- 10 files changed, 464 insertions(+), 13 deletions(-) create mode 100644 LICENSE-Zig create mode 100644 src/assets/debug.css create mode 100644 src/assets/debug.js create mode 100644 src/jetzig/debug.zig diff --git a/LICENSE-Zig b/LICENSE-Zig new file mode 100644 index 0000000..9ce0137 --- /dev/null +++ b/LICENSE-Zig @@ -0,0 +1,21 @@ +The MIT License (Expat) + +Copyright (c) Zig contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/build.zig b/build.zig index 9be5667..3f20f31 100644 --- a/build.zig +++ b/build.zig @@ -110,6 +110,7 @@ pub fn build(b: *std.Build) !void { const test_build_options = b.addOptions(); test_build_options.addOption(Environment, "environment", .testing); test_build_options.addOption(bool, "build_static", true); + test_build_options.addOption(bool, "debug_console", false); const run_main_tests = b.addRunArtifact(main_tests); main_tests.root_module.addOptions("build_options", test_build_options); @@ -138,12 +139,24 @@ pub fn jetzigInit(b: *std.Build, exe: *std.Build.Step.Compile, options: JetzigIn "environment", "Jetzig server environment.", ) orelse .development; + const build_static = b.option( bool, "build_static", "Pre-render static routes. [default: false in development, true in testing/production]", ) orelse (environment != .development); + const debug_console = b.option( + bool, + "debug_console", + "Render a debug console on error. Default: false.", + ) orelse false; + + if (debug_console == true and environment != .development) { + std.debug.print("Environment must be `development` when `debug_console` is enabled.", .{}); + return error.JetzigBuildError; + } + const jetzig_dep = b.dependency( "jetzig", .{ @@ -171,6 +184,7 @@ pub fn jetzigInit(b: *std.Build, exe: *std.Build.Step.Compile, options: JetzigIn const build_options = b.addOptions(); build_options.addOption(Environment, "environment", environment); build_options.addOption(bool, "build_static", build_static); + build_options.addOption(bool, "debug_console", debug_console); jetzig_module.addOptions("build_options", build_options); exe.root_module.addImport("jetzig", jetzig_module); diff --git a/cli/commands/server.zig b/cli/commands/server.zig index 87f6e22..ea859fa 100644 --- a/cli/commands/server.zig +++ b/cli/commands/server.zig @@ -70,6 +70,7 @@ pub fn run( "build", util.environmentBuildOption(main_options.options.environment), "-Djetzig_runner=true", + "-Ddebug_console=true", "install", "--color", "on", diff --git a/demo/src/app/views/anti_csrf.zig b/demo/src/app/views/anti_csrf.zig index e6f98ff..e30ecf4 100644 --- a/demo/src/app/views/anti_csrf.zig +++ b/demo/src/app/views/anti_csrf.zig @@ -1,7 +1,12 @@ const std = @import("std"); const jetzig = @import("jetzig"); -pub const layout = "application"; +// Anti-CSRF middleware can be included in the view's `actions` declaration to apply CSRF +// protection just to this specific view, or it can be added to your application's global +// middleware stack defined in `jetzig_options` in `src/main.zig`. +// +// Use `{{context.authenticityToken()}}` or `{{context.authenticityFormField()}}` in a Zmpl +// template to generate a token, store it in the user's session, and inject it into the page. pub const actions = .{ .before = .{jetzig.middleware.AntiCsrfMiddleware}, diff --git a/demo/src/app/views/anti_csrf/index.zmpl b/demo/src/app/views/anti_csrf/index.zmpl index 4f89eaa..4b914a7 100644 --- a/demo/src/app/views/anti_csrf/index.zmpl +++ b/demo/src/app/views/anti_csrf/index.zmpl @@ -6,3 +6,5 @@ + +
Try clearing `_jetzig_session` cookie before clicking "Submit Spam"
diff --git a/src/assets/debug.css b/src/assets/debug.css new file mode 100644 index 0000000..8ed7f05 --- /dev/null +++ b/src/assets/debug.css @@ -0,0 +1,121 @@ +/* reset https://www.joshwcomeau.com/css/custom-css-reset/ */ +*, *::before, *::after { + box-sizing: border-box; +} + +* { + margin: 0; +} + +body { + line-height: 1.5; + -webkit-font-smoothing: antialiased; +} + +img, picture, video, canvas, svg { + display: block; + max-width: 100%; +} + +input, button, textarea, select { + font: inherit; +} + +p, h1, h2, h3, h4, h5, h6 { + overflow-wrap: break-word; +} + +p { + text-wrap: pretty; +} + +h1, h2, h3, h4, h5, h6 { + text-wrap: balance; +} + +#root, #__next { + isolation: isolate; +} + +/* styles */ + +body { + background-color: #222; +} + +h1 { + font-family: monospace; + color: #e55; + padding: 1rem; + font-size: 1.6rem; +} + +h2 { + font-family: monospace; + color: #a0a0a0; + padding: 1rem; + font-size: 1.6rem; +} + +.stack-trace { + /* background-color: #e555; */ + padding: 1rem; + font-family: monospace; +} + +.stack-trace .stack-source-line .file-name { + color: #90bfd7; + display: block; + padding: 0.4rem; + font-weight: bold; +} + +.stack-trace .stack-source-line { + background-color: #333; + padding: 1rem; + margin: 0; + border-bottom: 1px solid #ffa3; +} + +.stack-trace .stack-source-line:last-child { + border-bottom: none; +} + +.stack-trace .stack-source-line .line-content { + margin: 0; + padding: 0; +} + +.stack-trace .stack-source-line .line-content.surrounding { + color: #ffa; +} + +.stack-trace .stack-source-line .line-content.target { + color: #faa; + background-color: #e552; +} + +.stack-trace .stack-source-line .line-content .line-number { + display: inline; + +} + +pre { + display: inline; + margin-right: 1rem; +} + +.response-data { + color: #fff; + background-color: #333; + padding: 1rem; + margin: 1rem; +} + +/* PrismJS 1.29.0 +https://prismjs.com/download.html#themes=prism-tomorrow&languages=json+zig */ +code[class*=language-],pre[class*=language-]{color:#ccc;font-family:monospace;text-align:left;white-space:pre;word-spacing:normal;word-break:normal;word-wrap:normal;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-hyphens:none;-moz-hyphens:none;-ms-hyphens:none;hyphens:none}pre[class*=language-]{overflow:auto}:not(pre)>code[class*=language-],pre[class*=language-]:not(pre)>code[class*=language-]{white-space:normal}.token.block-comment,.token.cdata,.token.comment,.token.doctype,.token.prolog{color:#999}.token.punctuation{color:#ccc}.token.attr-name,.token.deleted,.token.namespace,.token.tag{color:#e2777a}.token.function-name{color:#6196cc}.token.boolean,.token.function,.token.number{color:#f08d49}.token.class-name,.token.constant,.token.property,.token.symbol{color:#f8c555}.token.atrule,.token.builtin,.token.important,.token.keyword,.token.selector{color:#cc99cd}.token.attr-value,.token.char,.token.regex,.token.string,.token.variable{color:#7ec699}.token.entity,.token.operator,.token.url{color:#67cdcc}.token.bold,.token.important{font-weight:700}.token.italic{font-style:italic}.token.entity{cursor:help}.token.inserted{color:green} + +.stack-trace .stack-source-line .line-number { + color: #ffa !important; +} diff --git a/src/assets/debug.js b/src/assets/debug.js new file mode 100644 index 0000000..644900a --- /dev/null +++ b/src/assets/debug.js @@ -0,0 +1,5 @@ +/* PrismJS 1.29.0 +https://prismjs.com/download.html#themes=prism-tomorrow&languages=json+zig */ +var _self="undefined"!=typeof window?window:"undefined"!=typeof WorkerGlobalScope&&self instanceof WorkerGlobalScope?self:{},Prism=function(e){var n=/(?:^|\s)lang(?:uage)?-([\w-]+)(?=\s|$)/i,t=0,r={},a={manual:e.Prism&&e.Prism.manual,disableWorkerMessageHandler:e.Prism&&e.Prism.disableWorkerMessageHandler,util:{encode:function e(n){return n instanceof i?new i(n.type,e(n.content),n.alias):Array.isArray(n)?n.map(e):n.replace(/&/g,"&").replace(/=g.reach);A+=w.value.length,w=w.next){var E=w.value;if(n.length>e.length)return;if(!(E instanceof i)){var P,L=1;if(y){if(!(P=l(b,A,e,m))||P.index>=e.length)break;var S=P.index,O=P.index+P[0].length,j=A;for(j+=w.value.length;S>=j;)j+=(w=w.next).value.length;if(A=j-=w.value.length,w.value instanceof i)continue;for(var C=w;C!==n.tail&&(jg.reach&&(g.reach=W);var z=w.prev;if(_&&(z=u(n,z,_),A+=_.length),c(n,z,L),w=u(n,z,new i(f,p?a.tokenize(N,p):N,k,N)),M&&u(n,w,M),L>1){var I={cause:f+","+d,reach:W};o(e,n,t,w.prev,A,I),g&&I.reach>g.reach&&(g.reach=I.reach)}}}}}}function s(){var e={value:null,prev:null,next:null},n={value:null,prev:e,next:null};e.next=n,this.head=e,this.tail=n,this.length=0}function u(e,n,t){var r=n.next,a={value:t,prev:n,next:r};return n.next=a,r.prev=a,e.length++,a}function c(e,n,t){for(var r=n.next,a=0;a"+i.content+""},!e.document)return e.addEventListener?(a.disableWorkerMessageHandler||e.addEventListener("message",(function(n){var t=JSON.parse(n.data),r=t.language,i=t.code,l=t.immediateClose;e.postMessage(a.highlight(i,a.languages[r],r)),l&&e.close()}),!1),a):a;var g=a.util.currentScript();function f(){a.manual||a.highlightAll()}if(g&&(a.filename=g.src,g.hasAttribute("data-manual")&&(a.manual=!0)),!a.manual){var h=document.readyState;"loading"===h||"interactive"===h&&g&&g.defer?document.addEventListener("DOMContentLoaded",f):window.requestAnimationFrame?window.requestAnimationFrame(f):window.setTimeout(f,16)}return a}(_self);"undefined"!=typeof module&&module.exports&&(module.exports=Prism),"undefined"!=typeof global&&(global.Prism=Prism); +Prism.languages.json={property:{pattern:/(^|[^\\])"(?:\\.|[^\\"\r\n])*"(?=\s*:)/,lookbehind:!0,greedy:!0},string:{pattern:/(^|[^\\])"(?:\\.|[^\\"\r\n])*"(?!\s*:)/,lookbehind:!0,greedy:!0},comment:{pattern:/\/\/.*|\/\*[\s\S]*?(?:\*\/|$)/,greedy:!0},number:/-?\b\d+(?:\.\d+)?(?:e[+-]?\d+)?\b/i,punctuation:/[{}[\],]/,operator:/:/,boolean:/\b(?:false|true)\b/,null:{pattern:/\bnull\b/,alias:"keyword"}},Prism.languages.webmanifest=Prism.languages.json; +!function(e){function n(e){return function(){return e}}var r=/\b(?:align|allowzero|and|anyframe|anytype|asm|async|await|break|cancel|catch|comptime|const|continue|defer|else|enum|errdefer|error|export|extern|fn|for|if|inline|linksection|nakedcc|noalias|nosuspend|null|or|orelse|packed|promise|pub|resume|return|stdcallcc|struct|suspend|switch|test|threadlocal|try|undefined|union|unreachable|usingnamespace|var|volatile|while)\b/,a="\\b(?!"+r.source+")(?!\\d)\\w+\\b",o="align\\s*\\((?:[^()]|\\([^()]*\\))*\\)",s="(?!\\s)(?:!?\\s*(?:"+"(?:\\?|\\bpromise->|(?:\\[[^[\\]]*\\]|\\*(?!\\*)|\\*\\*)(?:\\s*|\\s*const\\b|\\s*volatile\\b|\\s*allowzero\\b)*)".replace(//g,n(o))+"\\s*)*"+"(?:\\bpromise\\b|(?:\\berror\\.)?(?:\\.)*(?!\\s+))".replace(//g,n(a))+")+";e.languages.zig={comment:[{pattern:/\/\/[/!].*/,alias:"doc-comment"},/\/{2}.*/],string:[{pattern:/(^|[^\\@])c?"(?:[^"\\\r\n]|\\.)*"/,lookbehind:!0,greedy:!0},{pattern:/([\r\n])([ \t]+c?\\{2}).*(?:(?:\r\n?|\n)\2.*)*/,lookbehind:!0,greedy:!0}],char:{pattern:/(^|[^\\])'(?:[^'\\\r\n]|[\uD800-\uDFFF]{2}|\\(?:.|x[a-fA-F\d]{2}|u\{[a-fA-F\d]{1,6}\}))'/,lookbehind:!0,greedy:!0},builtin:/\B@(?!\d)\w+(?=\s*\()/,label:{pattern:/(\b(?:break|continue)\s*:\s*)\w+\b|\b(?!\d)\w+\b(?=\s*:\s*(?:\{|while\b))/,lookbehind:!0},"class-name":[/\b(?!\d)\w+(?=\s*=\s*(?:(?:extern|packed)\s+)?(?:enum|struct|union)\s*[({])/,{pattern:RegExp("(:\\s*)(?=\\s*(?:\\s*)?[=;,)])|(?=\\s*(?:\\s*)?\\{)".replace(//g,n(s)).replace(//g,n(o))),lookbehind:!0,inside:null},{pattern:RegExp("(\\)\\s*)(?=\\s*(?:\\s*)?;)".replace(//g,n(s)).replace(//g,n(o))),lookbehind:!0,inside:null}],"builtin-type":{pattern:/\b(?:anyerror|bool|c_u?(?:int|long|longlong|short)|c_longdouble|c_void|comptime_(?:float|int)|f(?:16|32|64|128)|[iu](?:8|16|32|64|128|size)|noreturn|type|void)\b/,alias:"keyword"},keyword:r,function:/\b(?!\d)\w+(?=\s*\()/,number:/\b(?:0b[01]+|0o[0-7]+|0x[a-fA-F\d]+(?:\.[a-fA-F\d]*)?(?:[pP][+-]?[a-fA-F\d]+)?|\d+(?:\.\d*)?(?:[eE][+-]?\d+)?)\b/,boolean:/\b(?:false|true)\b/,operator:/\.[*?]|\.{2,3}|[-=]>|\*\*|\+\+|\|\||(?:<<|>>|[-+*]%|[-+*/%^&|<>!=])=?|[?~]/,punctuation:/[.:,;(){}[\]]/},e.languages.zig["class-name"].forEach((function(n){null===n.inside&&(n.inside=e.languages.zig)}))}(Prism); diff --git a/src/jetzig.zig b/src/jetzig.zig index de8af9e..db5a2c1 100644 --- a/src/jetzig.zig +++ b/src/jetzig.zig @@ -23,6 +23,7 @@ pub const testing = @import("jetzig/testing.zig"); pub const config = @import("jetzig/config.zig"); pub const auth = @import("jetzig/auth.zig"); pub const callbacks = @import("jetzig/callbacks.zig"); +pub const debug = @import("jetzig/debug.zig"); pub const TemplateContext = @import("jetzig/TemplateContext.zig"); pub const DateTime = jetcommon.types.DateTime; diff --git a/src/jetzig/debug.zig b/src/jetzig/debug.zig new file mode 100644 index 0000000..80d634e --- /dev/null +++ b/src/jetzig/debug.zig @@ -0,0 +1,218 @@ +const std = @import("std"); +const builtin = @import("builtin"); + +pub const ErrorInfo = struct { + stack_trace: ?*std.builtin.StackTrace = null, + err: ?anyerror = null, +}; + +pub fn sourceLocations( + allocator: std.mem.Allocator, + debug_info: *std.debug.SelfInfo, + stack_trace: *std.builtin.StackTrace, +) ![]const std.debug.SourceLocation { + var source_locations = std.ArrayList(std.debug.SourceLocation).init(allocator); + + if (builtin.strip_debug_info) return error.MissingDebugInfo; + + var frame_index: usize = 0; + var frames_left: usize = @min(stack_trace.index, stack_trace.instruction_addresses.len); + + while (frames_left != 0) : ({ + frames_left -= 1; + frame_index = (frame_index + 1) % stack_trace.instruction_addresses.len; + }) { + const return_address = stack_trace.instruction_addresses[frame_index]; + const address = return_address - 1; + const module = try debug_info.getModuleForAddress(address); + const symbol_info = try module.getSymbolAtAddress(debug_info.allocator, address); + + if (symbol_info.source_location) |source_location| { + try source_locations.append(source_location); + } + } + + return try source_locations.toOwnedSlice(); +} + +pub const HtmlStackTrace = struct { + allocator: std.mem.Allocator, + stack_trace: *std.builtin.StackTrace, + + pub fn format(self: HtmlStackTrace, _: anytype, _: anytype, writer: anytype) !void { + const debug_info = try std.debug.getSelfDebugInfo(); + const source_locations = try sourceLocations( + self.allocator, + debug_info, + self.stack_trace, + ); + for (source_locations) |source_location| { + defer debug_info.allocator.free(source_location.file_name); + try writer.print( + \\
+ \\ {s}:{d} + \\ + , + .{ + source_location.file_name, + source_location.line, + }, + ); + const surrounding_previous = try surroundingLinesFromFile( + self.allocator, + .previous, + 3, + source_location.file_name, + source_location.line, + ); + const surrounding_next = try surroundingLinesFromFile( + self.allocator, + .next, + 3, + source_location.file_name, + source_location.line, + ); + const target_source_line = try readLineFromFile( + self.allocator, + source_location.file_name, + source_location.line, + ); + + for (surrounding_previous) |source_line| { + try writer.print(surrounding_line_template, .{ source_line.line, source_line.content }); + } + try writer.print(target_line_template, .{ target_source_line.line, target_source_line.content }); + for (surrounding_next) |source_line| { + try writer.print(surrounding_line_template, .{ source_line.line, source_line.content }); + } + + try writer.print( + \\
+ \\ + , .{}); + } + } +}; + +const SourceLine = struct { content: []const u8, line: usize }; + +pub fn readLineFromFile(allocator: std.mem.Allocator, path: []const u8, line: usize) !SourceLine { + const file = try std.fs.openFileAbsolute(path, .{}); + var buf: [std.mem.page_size]u8 = undefined; + + var count: usize = 1; + var cursor: usize = 0; + + seek: { + while (true) { + const bytes_read = try file.readAll(buf[0..]); + for (buf[0..bytes_read]) |char| { + if (char == '\n') count += 1; + cursor += 1; + if (count == line) { + cursor += 1; + break :seek; + } + } + if (bytes_read < buf.len) return error.EndOfFile; + } + } + + var size: usize = 0; + try file.seekTo(cursor); + read: { + const bytes_read = try file.readAll(buf[0..]); + if (std.mem.indexOf(u8, buf[0..bytes_read], "\n")) |index| { + size += index; + break :read; + } else if (bytes_read < buf.len) { + size += bytes_read; + break :read; + } else { + while (true) { + const more_bytes_read = try file.readAll(buf[0..]); + if (std.mem.indexOf(u8, buf[0..more_bytes_read], "\n")) |index| { + size += index; + break :read; + } else if (more_bytes_read < buf.len) { + size += more_bytes_read; + break :read; + } else { + size += more_bytes_read; + } + } + } + } + const line_content = try allocator.alloc(u8, size); + try file.seekTo(cursor); + const bytes_read = try file.readAll(line_content[0..]); + + std.debug.assert(bytes_read == size); + + return .{ .content = line_content[0..], .line = line }; +} + +fn surroundingLinesFromFile( + allocator: std.mem.Allocator, + context: enum { previous, next }, + desired_count: usize, + path: []const u8, + target_line: usize, +) ![]SourceLine { + // This isn't very efficient but we only use it in debug mode so not a huge deal. + const start = switch (context) { + .previous => if (target_line > desired_count) + target_line - desired_count + else + target_line, + .next => target_line + 2, + }; + + var lines = std.ArrayList(SourceLine).init(allocator); + + switch (context) { + .previous => { + for (start..target_line) |line| { + try lines.append(try readLineFromFile(allocator, path, line)); + } + }, + .next => { + for (0..desired_count, start..) |_, line| { + try lines.append(try readLineFromFile(allocator, path, line)); + } + }, + } + + return try lines.toOwnedSlice(); +} + +pub const console_template = + \\ + \\ + \\ + \\ + \\ + \\ + \\

Encountered Error: {0s}

+ \\
+ \\ {1} + \\
+ \\

Response Data

+ \\
+ \\
{2s}
+ \\
+ \\ + \\ + \\ +; + +const surrounding_line_template = + \\
{d: >4}
{s}
+ \\ +; +const target_line_template = + \\
{d: >4}
{s}
+ \\ +; diff --git a/src/jetzig/http/Server.zig b/src/jetzig/http/Server.zig index 28568fa..fad3456 100644 --- a/src/jetzig/http/Server.zig +++ b/src/jetzig/http/Server.zig @@ -198,7 +198,10 @@ fn renderResponse(self: *Server, request: *jetzig.http.Request) !void { try callback(request, route); if (request.rendered_view) |view| { if (request.state == .failed) { - request.setResponse(try self.renderError(request, view.status_code), .{}); + request.setResponse( + try self.renderError(request, view.status_code, .{}), + .{}, + ); } else if (request.state == .rendered) { // TODO: Allow callbacks to set content } @@ -334,7 +337,7 @@ fn renderView( .data = request.response_data, .status_code = .internal_server_error, }; - return try self.renderError(request, view.status_code); + return try self.renderError(request, view.status_code, .{}); } const template: ?zmpl.Template = if (request.dynamic_assigned_template) |request_template| @@ -353,12 +356,17 @@ fn renderView( .content = try self.renderTemplateWithLayout(request, capture, rendered_view, route), }; } else { - try self.logger.DEBUG( - "Missing template for route `{s}.{s}`. Expected: `src/app/views/{s}.zmpl`.", - .{ route.view_name, @tagName(route.action), route.template }, - ); return switch (request.requestFormat()) { - .HTML, .UNKNOWN => try self.renderNotFound(request), + .HTML, .UNKNOWN => blk: { + try self.logger.DEBUG( + "Missing template for route `{s}.{s}`. Expected: `src/app/views/{s}.zmpl`.", + .{ route.view_name, @tagName(route.action), route.template }, + ); + if (comptime jetzig.build_options.debug_console) { + return error.ZmplTemplateNotFound; + } + break :blk try self.renderNotFound(request); + }, .JSON => .{ .view = rendered_view, .content = "" }, }; } @@ -467,32 +475,44 @@ fn renderInternalServerError( stack_trace: ?*std.builtin.StackTrace, err: anyerror, ) !RenderedView { - request.response_data.reset(); + // request.response_data.reset(); try self.logger.logError(stack_trace, err); - const status = .internal_server_error; - return try self.renderError(request, status); + const status = jetzig.http.StatusCode.internal_server_error; + + return try self.renderError(request, status, .{ .stack_trace = stack_trace, .err = err }); } fn renderNotFound(self: *Server, request: *jetzig.http.Request) !RenderedView { request.response_data.reset(); const status: jetzig.http.StatusCode = .not_found; - return try self.renderError(request, status); + return try self.renderError(request, status, .{}); } fn renderBadRequest(self: *Server, request: *jetzig.http.Request) !RenderedView { request.response_data.reset(); const status: jetzig.http.StatusCode = .bad_request; - return try self.renderError(request, status); + return try self.renderError(request, status, .{}); } fn renderError( self: Server, request: *jetzig.http.Request, status_code: jetzig.http.StatusCode, + error_info: jetzig.debug.ErrorInfo, +) !RenderedView { + if (comptime jetzig.build_options.debug_console) { + return try self.renderDebugConsole(request, status_code, error_info); + } else return try self.renderGeneralError(request, status_code); +} + +fn renderGeneralError( + self: Server, + request: *jetzig.http.Request, + status_code: jetzig.http.StatusCode, ) !RenderedView { if (try self.renderErrorView(request, status_code)) |view| return view; if (try renderStaticErrorPage(request, status_code)) |view| return view; @@ -500,6 +520,42 @@ fn renderError( return try renderDefaultError(request, status_code); } +fn renderDebugConsole( + self: Server, + request: *jetzig.http.Request, + status_code: jetzig.http.StatusCode, + error_info: jetzig.debug.ErrorInfo, +) !RenderedView { + if (comptime jetzig.build_options.debug_console) { + var buf = std.ArrayList(u8).init(request.allocator); + const writer = buf.writer(); + + if (error_info.stack_trace) |stack_trace| { + const debug_content = jetzig.debug.HtmlStackTrace{ + .allocator = request.allocator, + .stack_trace = stack_trace, + }; + const error_name = if (error_info.err) |err| @errorName(err) else "[UnknownError]"; + try writer.print( + jetzig.debug.console_template, + .{ + error_name, + debug_content, + try request.response_data.toJsonOptions(.{ .pretty = true }), + @embedFile("../../assets/debug.css"), + }, + ); + } else return try self.renderGeneralError(request, status_code); + + const content = try buf.toOwnedSlice(); + + return .{ + .view = .{ .data = request.response_data, .status_code = status_code }, + .content = if (content.len == 0) "" else content, + }; + } else unreachable; +} + fn renderErrorView( self: Server, request: *jetzig.http.Request, @@ -647,6 +703,13 @@ const StaticResource = struct { }; fn matchStaticResource(self: *Server, request: *jetzig.http.Request) !?StaticResource { + if (comptime jetzig.build_options.debug_console) { + if (std.mem.eql(u8, request.path.path, "/_jetzig_debug.js")) return .{ + .content = @embedFile("../../assets/debug.js"), + .mime_type = "text/javascript", + }; + } + // TODO: Map public and static routes at launch to avoid accessing the file system when // matching any route - currently every request causes file system traversal. const public_resource = try self.matchPublicContent(request); From 9684181c64f4a11ea7099498c1a1682dbba1086b Mon Sep 17 00:00:00 2001 From: Bob Farrell Date: Sat, 23 Nov 2024 19:31:37 +0000 Subject: [PATCH 4/5] Fix middleware render detection --- src/jetzig/http/Request.zig | 31 +++++++++++++++++------- src/jetzig/http/Server.zig | 9 +++---- src/jetzig/http/middleware.zig | 19 +++++++++++---- src/jetzig/middleware/HtmxMiddleware.zig | 6 ++++- 4 files changed, 45 insertions(+), 20 deletions(-) diff --git a/src/jetzig/http/Request.zig b/src/jetzig/http/Request.zig index b8e74de..41de5c9 100644 --- a/src/jetzig/http/Request.zig +++ b/src/jetzig/http/Request.zig @@ -11,6 +11,16 @@ pub const Method = enum { DELETE, GET, PATCH, POST, HEAD, PUT, CONNECT, OPTIONS, pub const Modifier = enum { edit, new }; pub const Format = enum { HTML, JSON, UNKNOWN }; pub const Protocol = enum { http, https }; +pub const RequestState = enum { + initial, // No processing has taken place + processed, // Request headers have been processed + after_request, // Initial middleware processing + rendered, // Rendered by middleware or view + redirected, // Redirected by middleware or view + failed, // Failed by middleware or view + before_response, // Post middleware processing + finalized, // Response generated +}; allocator: std.mem.Allocator, path: jetzig.http.Path, @@ -30,14 +40,12 @@ parsed_multipart: ?*jetzig.data.Data = null, _cookies: ?*jetzig.http.Cookies = null, _session: ?*jetzig.http.Session = null, body: []const u8 = undefined, -state: enum { initial, rendered, redirected, failed, processed } = .initial, -response_started: bool = false, +state: RequestState = .initial, dynamic_assigned_template: ?[]const u8 = null, layout: ?[]const u8 = null, layout_disabled: bool = false, redirect_state: ?RedirectState = null, middleware_rendered: ?struct { name: []const u8, action: []const u8 } = null, -middleware_rendered_during_response: bool = false, middleware_data: jetzig.http.middleware.MiddlewareData = undefined, rendered_multiple: bool = false, rendered_view: ?jetzig.views.View = null, @@ -172,6 +180,7 @@ pub fn respond(self: *Request) !void { const status = jetzig.http.status_codes.get(self.response.status_code); self.httpz_response.status = try status.getCodeInt(); self.httpz_response.body = self.response.content; + self.state = .finalized; } /// Set the root value for response data. @@ -186,10 +195,9 @@ pub fn data(self: Request, comptime root: @TypeOf(.enum_literal)) !*jetzig.Data. /// Render a response. This function can only be called once per request (repeat calls will /// trigger an error). pub fn render(self: *Request, status_code: jetzig.http.status_codes.StatusCode) jetzig.views.View { - if (self.state != .processed) self.rendered_multiple = true; + if (self.isRendered()) self.rendered_multiple = true; self.state = .rendered; - if (self.response_started) self.middleware_rendered_during_response = true; self.rendered_view = .{ .data = self.response_data, .status_code = status_code }; return self.rendered_view.?; } @@ -197,14 +205,20 @@ pub fn render(self: *Request, status_code: jetzig.http.status_codes.StatusCode) /// Render an error. This function can only be called once per request (repeat calls will /// trigger an error). pub fn fail(self: *Request, status_code: jetzig.http.status_codes.StatusCode) jetzig.views.View { - if (self.state != .processed) self.rendered_multiple = true; + if (self.isRendered()) self.rendered_multiple = true; self.state = .failed; - if (self.response_started) self.middleware_rendered_during_response = true; self.rendered_view = .{ .data = self.response_data, .status_code = status_code }; return self.rendered_view.?; } +pub inline fn isRendered(self: *const Request) bool { + return switch (self.state) { + .initial, .processed, .after_request, .before_response => false, + .rendered, .redirected, .failed, .finalized => true, + }; +} + /// Issue a redirect to a new location. /// ```zig /// return request.redirect("https://www.example.com/", .moved_permanently); @@ -218,10 +232,9 @@ pub fn redirect( location: []const u8, redirect_status: enum { moved_permanently, found }, ) jetzig.views.View { - if (self.state != .processed) self.rendered_multiple = true; + if (self.isRendered()) self.rendered_multiple = true; self.state = .redirected; - if (self.response_started) self.middleware_rendered_during_response = true; const status_code = switch (redirect_status) { .moved_permanently => jetzig.http.status_codes.StatusCode.moved_permanently, diff --git a/src/jetzig/http/Server.zig b/src/jetzig/http/Server.zig index fad3456..ca0c5d4 100644 --- a/src/jetzig/http/Server.zig +++ b/src/jetzig/http/Server.zig @@ -138,7 +138,8 @@ pub fn processNextRequest( var middleware_data = try jetzig.http.middleware.afterRequest(&request); - if (request.middleware_rendered) |_| { // Request processing ends when a middleware renders or redirects. + if (request.middleware_rendered) |_| { + // Request processing ends when a middleware renders or redirects. if (request.redirect_state) |state| { try request.renderRedirect(state); } else if (request.rendered_view) |rendered| { @@ -150,8 +151,8 @@ pub fn processNextRequest( } else { try self.renderResponse(&request); try request.response.headers.append("Content-Type", response.content_type); - try jetzig.http.middleware.beforeResponse(&middleware_data, &request); + try jetzig.http.middleware.beforeResponse(&middleware_data, &request); try request.respond(); try jetzig.http.middleware.afterResponse(&middleware_data, &request); jetzig.http.middleware.deinit(&middleware_data, &request); @@ -371,7 +372,7 @@ fn renderView( }; } } else { - if (request.state != .redirected) { + if (request.state == .processed) { try self.logger.WARN("`request.render` was not invoked. Rendering empty content.", .{}); } request.response_data.reset(); @@ -475,8 +476,6 @@ fn renderInternalServerError( stack_trace: ?*std.builtin.StackTrace, err: anyerror, ) !RenderedView { - // request.response_data.reset(); - try self.logger.logError(stack_trace, err); const status = jetzig.http.StatusCode.internal_server_error; diff --git a/src/jetzig/http/middleware.zig b/src/jetzig/http/middleware.zig index 31dc50c..e049a17 100644 --- a/src/jetzig/http/middleware.zig +++ b/src/jetzig/http/middleware.zig @@ -60,8 +60,11 @@ pub fn afterRequest(request: *jetzig.http.Request) !MiddlewareData { } } + request.state = .after_request; + inline for (middlewares, 0..) |middleware, index| { if (comptime !@hasDecl(middleware, "afterRequest")) continue; + if (comptime @hasDecl(middleware, "init")) { const data = middleware_data.get(index).?; try @call( @@ -72,7 +75,8 @@ pub fn afterRequest(request: *jetzig.http.Request) !MiddlewareData { } else { try @call(.always_inline, middleware.afterRequest, .{request}); } - if (request.state != .initial) { + + if (request.state != .after_request) { request.middleware_rendered = .{ .name = @typeName(middleware), .action = "afterRequest" }; break; } @@ -86,11 +90,11 @@ pub fn beforeResponse( middleware_data: *MiddlewareData, request: *jetzig.http.Request, ) !void { - request.response_started = true; + request.state = .before_response; inline for (middlewares, 0..) |middleware, index| { if (comptime !@hasDecl(middleware, "beforeResponse")) continue; - if (!request.middleware_rendered_during_response) { + if (request.state == .before_response) { if (comptime @hasDecl(middleware, "init")) { const data = middleware_data.get(index).?; try @call( @@ -99,10 +103,15 @@ pub fn beforeResponse( .{ @as(*middleware, @ptrCast(@alignCast(data))), request, request.response }, ); } else { - try @call(.always_inline, middleware.beforeResponse, .{ request, request.response }); + try @call( + .always_inline, + middleware.beforeResponse, + .{ request, request.response }, + ); } } - if (request.middleware_rendered_during_response) { + + if (request.state != .before_response) { request.middleware_rendered = .{ .name = @typeName(middleware), .action = "beforeResponse", diff --git a/src/jetzig/middleware/HtmxMiddleware.zig b/src/jetzig/middleware/HtmxMiddleware.zig index 022d010..a727f0e 100644 --- a/src/jetzig/middleware/HtmxMiddleware.zig +++ b/src/jetzig/middleware/HtmxMiddleware.zig @@ -20,7 +20,11 @@ pub fn afterRequest(request: *jetzig.http.Request) !void { /// If a redirect was issued during request processing, reset any response data, set response /// status to `200 OK` and replace the `Location` header with a `HX-Redirect` header. pub fn beforeResponse(request: *jetzig.http.Request, response: *jetzig.http.Response) !void { - if (response.status_code != .moved_permanently and response.status_code != .found) return; + switch (response.status_code) { + .moved_permanently, .found => {}, + else => return, + } + if (request.headers.get("HX-Request") == null) return; if (response.headers.get("Location")) |location| { From 773eb1dc6e8c95638713b92a3f9024bca1bdda46 Mon Sep 17 00:00:00 2001 From: Bob Farrell Date: Sat, 23 Nov 2024 19:58:52 +0000 Subject: [PATCH 5/5] Add `--debug` option to `jetzig server` Defaults to true, allow disabling new debug console. --- cli/commands/server.zig | 39 ++++++++++++++++++++++----------------- 1 file changed, 22 insertions(+), 17 deletions(-) diff --git a/cli/commands/server.zig b/cli/commands/server.zig index ea859fa..e51c29f 100644 --- a/cli/commands/server.zig +++ b/cli/commands/server.zig @@ -9,22 +9,20 @@ pub const watch_changes_pause_duration = 1 * 1000 * 1000 * 1000; /// Command line options for the `server` command. pub const Options = struct { reload: bool = true, + debug: bool = true, pub const meta = .{ .full_text = \\Launches a development server. \\ - \\The development server reloads when files in `src/` are updated. - \\ - \\To disable this behaviour, pass `--reload=false` - \\ \\Example: \\ \\ jetzig server - \\ jetzig server --reload=false + \\ jetzig server --reload=false --debug=false , .option_docs = .{ .reload = "Enable or disable automatic reload on update (default: true)", + .debug = "Enable or disable the development debug console (default: true)", }, }; }; @@ -62,19 +60,27 @@ pub fn run( }, ); + var argv = std.ArrayList([]const u8).init(allocator); + defer argv.deinit(); + + try argv.appendSlice(&.{ + "zig", + "build", + util.environmentBuildOption(main_options.options.environment), + "-Djetzig_runner=true", + }); + + if (options.debug) try argv.append("-Ddebug_console=true"); + try argv.appendSlice(&.{ + "install", + "--color", + "on", + }); + while (true) { util.runCommandInDir( allocator, - &.{ - "zig", - "build", - util.environmentBuildOption(main_options.options.environment), - "-Djetzig_runner=true", - "-Ddebug_console=true", - "install", - "--color", - "on", - }, + argv.items, .{ .path = realpath }, ) catch { std.debug.print("Build failed, waiting for file change...\n", .{}); @@ -89,10 +95,9 @@ pub fn run( std.process.exit(1); } - const argv = &[_][]const u8{exe_path.?}; defer allocator.free(exe_path.?); - var process = std.process.Child.init(argv, allocator); + var process = std.process.Child.init(&.{exe_path.?}, allocator); process.stdin_behavior = .Inherit; process.stdout_behavior = .Inherit; process.stderr_behavior = .Inherit;