diff options
| -rw-r--r-- | build.zig | 2 | ||||
| -rw-r--r-- | demos/css-stress-test/nextjs-framework.tsx | 15 | ||||
| -rw-r--r-- | demos/css-stress-test/src/index.tsx | 3 | ||||
| -rw-r--r-- | src/http.zig | 31 | ||||
| -rw-r--r-- | src/javascript/jsc/base.zig | 2 | ||||
| -rw-r--r-- | src/javascript/jsc/javascript.zig | 240 | ||||
| -rw-r--r-- | src/javascript/jsc/webcore/response.zig | 3 | ||||
| -rw-r--r-- | types.d.ts | 5 |
8 files changed, 255 insertions, 46 deletions
@@ -120,7 +120,7 @@ pub fn build(b: *std.build.Builder) void { addPicoHTTP(exe, cwd); javascript = b.addExecutable("spjs", "src/main_javascript.zig"); addPicoHTTP(javascript, cwd); - javascript.packages = std.ArrayList(std.build.Pkg).fromOwnedSlice(std.heap.c_allocator, std.heap.c_allocator.dupe(std.build.Pkg, exe.packages.items) catch unreachable); + javascript.packages = std.ArrayList(std.build.Pkg).fromOwnedSlice(std.heap.page_allocator, std.heap.page_allocator.dupe(std.build.Pkg, exe.packages.items) catch unreachable); javascript.setOutputDir(output_dir); javascript.setBuildMode(mode); javascript.linkLibC(); diff --git a/demos/css-stress-test/nextjs-framework.tsx b/demos/css-stress-test/nextjs-framework.tsx new file mode 100644 index 000000000..38948e482 --- /dev/null +++ b/demos/css-stress-test/nextjs-framework.tsx @@ -0,0 +1,15 @@ +import { renderNextJSPage } from "speedy-nextjs/server"; + +addEventListener("fetch", (event: FetchEvent) => { + const AppComponent = module.requireFirst( + "pages/_app", + "speedy-nextjs/pages/_app" + ); + const Document = module.requireFirst( + "pages/_document", + "speedy-nextjs/pages/_document" + ); +}); + +// typescript isolated modules +export {}; diff --git a/demos/css-stress-test/src/index.tsx b/demos/css-stress-test/src/index.tsx index 16855fd11..9c317fc08 100644 --- a/demos/css-stress-test/src/index.tsx +++ b/demos/css-stress-test/src/index.tsx @@ -20,6 +20,9 @@ if (typeof window !== "undefined") { }); startReact(); +} else { + const ReactDOMServer = require("react-dom/server.browser"); + console.log(ReactDOMServer.renderToString(<Base />)); } export { Base }; diff --git a/src/http.zig b/src/http.zig index a0f899ddd..8ac4ad18e 100644 --- a/src/http.zig +++ b/src/http.zig @@ -637,6 +637,8 @@ pub const RequestContext = struct { ctx: RequestContext, conn: tcp.Connection, + pub var javascript_vm: *JavaScript.VirtualMachine = undefined; + pub const HandlerThread = struct { args: Api.TransformOptions, existing_bundle: ?*NodeModuleBundle, @@ -685,6 +687,7 @@ pub const RequestContext = struct { .entry_point, ); JavaScript.VirtualMachine.instance = vm; + javascript_vm = JavaScript.VirtualMachine.instance; var exception: js.JSValueRef = null; var load_result = try JavaScript.Module.loadFromResolveResult(vm, vm.ctx, resolved_entry_point, &exception); @@ -1488,6 +1491,8 @@ pub const Server = struct { timer: std.time.Timer = undefined, transform_options: Api.TransformOptions, + javascript_enabled: bool = false, + pub fn adjustUlimit() !void { var limit = try std.os.getrlimit(.NOFILE); if (limit.cur < limit.max) { @@ -1509,6 +1514,19 @@ pub const Server = struct { threadlocal var filechange_buf: [32]u8 = undefined; pub fn onFileUpdate(ctx: *Server, events: []watcher.WatchEvent, watchlist: watcher.Watchlist) void { + if (ctx.javascript_enabled) { + _onFileUpdate(ctx, events, watchlist, true); + } else { + _onFileUpdate(ctx, events, watchlist, false); + } + } + + fn _onFileUpdate( + ctx: *Server, + events: []watcher.WatchEvent, + watchlist: watcher.Watchlist, + comptime is_javascript_enabled: bool, + ) void { var fbs = std.io.fixedBufferStream(&filechange_buf); var writer = ByteApiWriter.init(&fbs); const message_type = Api.WebsocketMessage{ @@ -1517,18 +1535,28 @@ pub const Server = struct { }; message_type.encode(&writer) catch unreachable; var header = fbs.getWritten(); - for (events) |event| { const file_path = watchlist.items(.file_path)[event.index]; + // so it's consistent with the rest // if we use .extname we might run into an issue with whether or not the "." is included. const path = Fs.PathName.init(file_path); const id = watchlist.items(.hash)[event.index]; var content_fbs = std.io.fixedBufferStream(filechange_buf[header.len..]); + + defer { + if (comptime is_javascript_enabled) { + // TODO: does this need a lock? + if (RequestContext.JavaScriptHandler.javascript_vm.require_cache.get(id)) |module| { + module.reload_pending = true; + } + } + } const change_message = Api.WebsocketMessageFileChangeNotification{ .id = id, .loader = (ctx.bundler.options.loaders.get(path.ext) orelse .file).toAPI(), }; + var content_writer = ByteApiWriter.init(&content_fbs); change_message.encode(&content_writer) catch unreachable; const change_buf = content_fbs.getWritten(); @@ -1642,6 +1670,7 @@ pub const Server = struct { if (req_ctx.url.extname.len == 0 and !RequestContext.JavaScriptHandler.javascript_disabled) { if (server.transform_options.javascript_framework_file != null) { RequestContext.JavaScriptHandler.enqueue(&req_ctx, server) catch unreachable; + server.javascript_enabled = !RequestContext.JavaScriptHandler.javascript_disabled; } } diff --git a/src/javascript/jsc/base.zig b/src/javascript/jsc/base.zig index 307bf2da9..bae18a0a3 100644 --- a/src/javascript/jsc/base.zig +++ b/src/javascript/jsc/base.zig @@ -33,7 +33,7 @@ pub const To = struct { return function; } - pub fn Finalize( + pub fn Finalize(n comptime ZigContextType: type, comptime ctxfn: fn ( this: *ZigContextType, diff --git a/src/javascript/jsc/javascript.zig b/src/javascript/jsc/javascript.zig index 3e6d4c185..f9aae96ee 100644 --- a/src/javascript/jsc/javascript.zig +++ b/src/javascript/jsc/javascript.zig @@ -255,6 +255,12 @@ pub const Module = struct { loaded: bool = false, exports_function: js.JSValueRef = null, + // When the Watcher detects the source file changed, we bust the require cache + // However, we want to lazily bust the require cache. + // We don't want to actually reload the references until the code is next executed + // reload_pending should not be applied to bundled modules + reload_pending: bool = false, + pub var module_class: js.JSClassRef = undefined; pub var module_global_class: js.JSClassRef = undefined; pub var module_global_class_def: js.JSClassDefinition = undefined; @@ -408,6 +414,7 @@ pub const Module = struct { call_ctx, call_ctx, &exception, + false, ); } }; @@ -728,15 +735,131 @@ pub const Module = struct { .{ import_path, module.path.name.dirWithTrailingSlash(), @errorName(err) }, ); Output.flush(); - exception.* = js.JSObjectMakeError(ctx, 0, null, null); + JSError( + getAllocator(ctx), + "{s}: failed to load module \"{s}\" from \"{s}\"", + .{ + @errorName(err), + import_path, + module.path.name.dirWithTrailingSlash(), + }, + ctx, + exception, + ); + return null; + } + } + + pub fn requireFirst( + this: *Module, + ctx: js.JSContextRef, + function: js.JSObjectRef, + thisObject: js.JSObjectRef, + arguments: []const js.JSValueRef, + exception: js.ExceptionRef, + ) js.JSValueRef { + if (arguments.len == 0 or js.JSStringGetMaximumUTF8CStringSize(arguments[0]) == 0) { + defer Output.flush(); + if (arguments.len == 0) { + Output.prettyErrorln("<r><red>error<r>: <b>requireFirst<r> needs a string, e.g. requireFirst(\"left-pad\")", .{}); + } else { + Output.prettyErrorln("<r><red>error<r>: <b>requireFirst(\"\")<r> string cannot be empty.", .{}); + } return null; } + + var total_len: usize = 0; + for (arguments) |argument| { + const len = js.JSStringGetLength(argument); + + if (!require_buf_loaded) { + require_buf = MutableString.init(this.vm.allocator, len + 1) catch unreachable; + require_buf_loaded = true; + } else { + require_buf.reset(); + require_buf.growIfNeeded(len + 1) catch {}; + } + + require_buf.list.resize(this.vm.allocator, len + 1) catch unreachable; + + const end = js.JSStringGetUTF8CString(argument, require_buf.list.items.ptr, require_buf.list.items.len); + total_len += end; + const import_path = require_buf.list.items[0 .. end - 1]; + var module = this; + + if (this.vm.bundler.linker.resolver.resolve(module.path.name.dirWithTrailingSlash(), import_path, .require)) |resolved| { + var load_result = Module.loadFromResolveResult(this.vm, ctx, resolved, exception) catch |err| { + return null; + }; + + switch (load_result) { + .Module => |new_module| { + // if (isDebug) { + // Output.prettyln( + // "Input: {s}\nOutput: {s}", + // .{ import_path, load_result.Module.path.text }, + // ); + // Output.flush(); + // } + return new_module.internalGetExports(js.JSContextGetGlobalContext(ctx)); + }, + .Path => |path| { + return js.JSStringCreateWithUTF8CString(path.text.ptr); + }, + } + } else |err| { + switch (err) { + error.ModuleNotFound => {}, + else => { + JSError( + getAllocator(ctx), + "{s}: failed to resolve module \"{s}\" from \"{s}\"", + .{ + @errorName(err), + import_path, + module.path.name.dirWithTrailingSlash(), + }, + ctx, + exception, + ); + return null; + }, + } + } + } + + require_buf.reset(); + require_buf.growIfNeeded(total_len) catch {}; + var used_len: usize = 0; + var remainder = require_buf.list.items; + for (arguments) |argument| { + const end = js.JSStringGetUTF8CString(argument, remainder.ptr, total_len - used_len); + used_len += end; + remainder[end - 1] = ","; + remainder = remainder[end..]; + } + + // If we get this far, it means there were no matches + JSError( + getAllocator(ctx), + "RequireError: failed to resolve modules \"{s}\" from \"{s}\"", + .{ + require_buf.list.items[0..used_len], + module.path.name.dirWithTrailingSlash(), + }, + ctx, + exception, + ); + return null; } const ModuleClass = NewClass( Module, "Module", - .{ .@"require" = require }, + .{ + .@"require" = require, + .@"requireFirst" = requireFirst, + }, .{ .@"id" = .{ .get = getId, @@ -789,18 +912,7 @@ pub const Module = struct { threadlocal var module_wrapper_params: [2]js.JSStringRef = undefined; threadlocal var module_wrapper_loaded = false; - pub fn load( - module: *Module, - vm: *VirtualMachine, - allocator: *std.mem.Allocator, - log: *logger.Log, - source: [:0]u8, - path: Fs.Path, - global_ctx: js.JSContextRef, - call_ctx: js.JSContextRef, - function_ctx: js.JSContextRef, - exception: js.ExceptionRef, - ) !void { + pub fn load(module: *Module, vm: *VirtualMachine, allocator: *std.mem.Allocator, log: *logger.Log, source: [:0]u8, path: Fs.Path, global_ctx: js.JSContextRef, call_ctx: js.JSContextRef, function_ctx: js.JSContextRef, exception: js.ExceptionRef, comptime is_reload: bool) !void { var source_code_ref = js.JSStringCreateWithUTF8CString(source.ptr); defer js.JSStringRelease(source_code_ref); var source_url = try allocator.dupeZ(u8, path.text); @@ -813,13 +925,18 @@ pub const Module = struct { Output.flush(); } - module.* = Module{ - .path = path, - .ref = undefined, - .vm = vm, - }; - module.ref = js.JSObjectMake(global_ctx, Module.module_class, module); - js.JSValueProtect(global_ctx, module.ref); + if (comptime !is_reload) { + module.* = Module{ + .path = path, + .ref = undefined, + .vm = vm, + }; + module.ref = js.JSObjectMake(global_ctx, Module.module_class, module); + js.JSValueProtect(global_ctx, module.ref); + } else { + js.JSValueUnprotect(global_ctx, module.exports.?); + } + // if (!module_wrapper_loaded) { module_wrapper_params[0] = js.JSStringRetain(js.JSStringCreateWithUTF8CString(Properties.UTF8.module[0.. :0])); module_wrapper_params[1] = js.JSStringRetain(js.JSStringCreateWithUTF8CString(Properties.UTF8.exports[0.. :0])); @@ -886,8 +1003,15 @@ pub const Module = struct { exception: js.ExceptionRef, ) !LoadResult { const hash = http.Watcher.getHash(resolved.path_pair.primary.text); + var reload_pending = false; if (vm.require_cache.get(hash)) |mod| { - return LoadResult{ .Module = mod }; + // require_cache should only contain local modules, not bundled ones. + // so we don't need to check for node_modlues here + reload_pending = mod.reload_pending; + + if (!reload_pending) { + return LoadResult{ .Module = mod }; + } } const path = resolved.path_pair.primary; @@ -1000,22 +1124,50 @@ pub const Module = struct { if (written == 0) { return error.PrintingErrorWriteFailed; } - var module = try vm.allocator.create(Module); - errdefer vm.allocator.destroy(module); - try vm.require_cache.put(hash, module); + var module: *Module = undefined; - try Module.load( - module, - vm, - vm.allocator, - vm.log, - source_code_printer.ctx.sentinel, - path, - js.JSContextGetGlobalContext(ctx), - ctx, - ctx, - exception, - ); + if (needs_reload) { + module = vm.require_cache.get(hash).?; + } else { + module = try vm.allocator.create(Module); + try vm.require_cache.put(hash, module); + } + + errdefer { + if (!needs_reload) { + vm.allocator.destroy(module); + } + } + + if (needs_reload) { + try Module.load( + module, + vm, + vm.allocator, + vm.log, + source_code_printer.ctx.sentinel, + path, + js.JSContextGetGlobalContext(ctx), + ctx, + ctx, + exception, + true, + ); + } else { + try Module.load( + module, + vm, + vm.allocator, + vm.log, + source_code_printer.ctx.sentinel, + path, + js.JSContextGetGlobalContext(ctx), + ctx, + ctx, + exception, + false, + ); + } return LoadResult{ .Module = module }; }, @@ -1191,13 +1343,16 @@ pub const EventListenerMixin = struct { pub const EventType = enum { fetch, + err, - pub fn match(str: string) ?EventType { - if (strings.eqlComptime(str, "fetch")) { - return EventType.fetch; - } + const SizeMatcher = strings.ExactSizeMatcher("fetch".len); - return null; + pub fn match(str: string) ?EventType { + return switch (SizeMatcher.match(str)) { + SizeMatcher.case("fetch") => EventType.fetch, + SizeMatcher.case("error") => EventType.err, + else => null, + }; } }; @@ -1226,6 +1381,7 @@ pub const EventListenerMixin = struct { ); var exception: js.JSValueRef = null; + // Rely on JS finalizer var fetch_event = try vm.allocator.create(FetchEvent); fetch_event.* = FetchEvent{ .request_context = request_context, diff --git a/src/javascript/jsc/webcore/response.zig b/src/javascript/jsc/webcore/response.zig index d65ac0732..669d55dfe 100644 --- a/src/javascript/jsc/webcore/response.zig +++ b/src/javascript/jsc/webcore/response.zig @@ -1129,4 +1129,5 @@ pub const FetchEvent = struct { ) js.JSValueRef { return js.JSValueMakeUndefined(ctx); } -}; + +};
\ No newline at end of file diff --git a/types.d.ts b/types.d.ts new file mode 100644 index 000000000..319d98760 --- /dev/null +++ b/types.d.ts @@ -0,0 +1,5 @@ +interface SpeedyNodeModule extends NodeJS.Module { + requireFirst(...id: string[]): any; +} + +declare var module: SpeedyNodeModule; |
