aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--build.zig2
-rw-r--r--demos/css-stress-test/nextjs-framework.tsx15
-rw-r--r--demos/css-stress-test/src/index.tsx3
-rw-r--r--src/http.zig31
-rw-r--r--src/javascript/jsc/base.zig2
-rw-r--r--src/javascript/jsc/javascript.zig240
-rw-r--r--src/javascript/jsc/webcore/response.zig3
-rw-r--r--types.d.ts5
8 files changed, 255 insertions, 46 deletions
diff --git a/build.zig b/build.zig
index 5b2a37e22..2972956be 100644
--- a/build.zig
+++ b/build.zig
@@ -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;