aboutsummaryrefslogtreecommitdiff
path: root/src/bun.js/javascript.zig
diff options
context:
space:
mode:
authorGravatar Jarred Sumner <709451+Jarred-Sumner@users.noreply.github.com> 2022-10-06 00:47:03 -0700
committerGravatar Jarred Sumner <709451+Jarred-Sumner@users.noreply.github.com> 2022-10-06 00:47:03 -0700
commit2c762f47c9c8ece73ce6d430e7814cac9e031bab (patch)
tree55145a5536201b26c51e347bbc78b86668182085 /src/bun.js/javascript.zig
parent3246efa5809c3bd031e1d08863e4b06beedd318e (diff)
downloadbun-2c762f47c9c8ece73ce6d430e7814cac9e031bab.tar.gz
bun-2c762f47c9c8ece73ce6d430e7814cac9e031bab.tar.zst
bun-2c762f47c9c8ece73ce6d430e7814cac9e031bab.zip
Add simple Hot Module Reloading to bun's runtime
Diffstat (limited to 'src/bun.js/javascript.zig')
-rw-r--r--src/bun.js/javascript.zig270
1 files changed, 263 insertions, 7 deletions
diff --git a/src/bun.js/javascript.zig b/src/bun.js/javascript.zig
index 020399040..c337372e5 100644
--- a/src/bun.js/javascript.zig
+++ b/src/bun.js/javascript.zig
@@ -305,7 +305,8 @@ pub const VirtualMachine = struct {
has_loaded_constructors: bool = false,
node_modules: ?*NodeModuleBundle = null,
bundler: Bundler,
- watcher: ?*http.Watcher = null,
+ bun_dev_watcher: ?*http.Watcher = null,
+ bun_watcher: ?*JSC.Watcher = null,
console: *ZigConsoleClient,
log: *logger.Log,
event_listeners: EventListenerMixin.Map,
@@ -379,6 +380,12 @@ pub const VirtualMachine = struct {
poller: JSC.Poller = JSC.Poller{},
us_loop_reference_count: usize = 0,
is_us_loop_entered: bool = false,
+ pending_internal_promise: *JSC.JSInternalPromise = undefined,
+ pub fn reload(this: *VirtualMachine) void {
+ Output.debug("Reloading...", .{});
+ this.global.reload();
+ this.pending_internal_promise = this.loadEntryPoint(this.main) catch @panic("Failed to reload");
+ }
pub fn io(this: *VirtualMachine) *IO {
if (this.io_ == null) {
@@ -516,6 +523,10 @@ pub const VirtualMachine = struct {
return slice;
}
+ pub fn isWatcherEnabled(this: *VirtualMachine) bool {
+ return this.bun_dev_watcher != null or this.bun_watcher != null;
+ }
+
pub fn init(
allocator: std.mem.Allocator,
_args: Api.TransformOptions,
@@ -1483,15 +1494,17 @@ pub const VirtualMachine = struct {
}
promise = JSModuleLoader.loadAndEvaluateModule(this.global, &ZigString.init(std.mem.span(main_file_name)));
+ this.pending_internal_promise = promise;
} else {
promise = JSModuleLoader.loadAndEvaluateModule(this.global, &ZigString.init(this.main));
+ this.pending_internal_promise = promise;
}
this.waitForPromise(promise);
this.eventLoop().autoTick();
- return promise;
+ return this.pending_internal_promise;
}
pub fn loadMacroEntryPoint(this: *VirtualMachine, entry_path: string, function_name: string, specifier: string, hash: i32) !*JSInternalPromise {
@@ -2852,7 +2865,13 @@ pub const ModuleLoader = struct {
var fd: ?StoredFileDescriptorType = null;
var package_json: ?*PackageJSON = null;
- if (jsc_vm.watcher) |watcher| {
+ if (jsc_vm.bun_dev_watcher) |watcher| {
+ if (watcher.indexOf(hash)) |index| {
+ const _fd = watcher.watchlist.items(.fd)[index];
+ fd = if (_fd > 0) _fd else null;
+ package_json = watcher.watchlist.items(.package_json)[index];
+ }
+ } else if (jsc_vm.bun_watcher) |watcher| {
if (watcher.indexOf(hash)) |index| {
const _fd = watcher.watchlist.items(.fd)[index];
fd = if (_fd > 0) _fd else null;
@@ -2979,14 +2998,30 @@ pub const ModuleLoader = struct {
return error.PrintingErrorWriteFailed;
}
- if (jsc_vm.has_loaded) {
- return jsc_vm.refCountedResolvedSource(printer.ctx.written, specifier, path.text, null);
- }
-
if (comptime Environment.dump_source) {
try dumpSource(specifier, &printer);
}
+ if (jsc_vm.isWatcherEnabled()) {
+ const resolved_source = jsc_vm.refCountedResolvedSource(printer.ctx.written, specifier, path.text, null);
+
+ if (parse_result.input_fd) |fd_| {
+ if (jsc_vm.bun_watcher != null and !is_node_override and std.fs.path.isAbsolute(path.text) and !strings.contains(path.text, "node_modules")) {
+ jsc_vm.bun_watcher.?.addFile(
+ fd_,
+ path.text,
+ hash,
+ loader,
+ 0,
+ package_json,
+ true,
+ ) catch {};
+ }
+ }
+
+ return resolved_source;
+ }
+
return ResolvedSource{
.allocator = null,
.source_code = ZigString.init(try default_allocator.dupe(u8, printer.ctx.getWritten())),
@@ -3363,3 +3398,224 @@ const FetchFlags = enum {
return this != .transpile;
}
};
+
+pub const Watcher = @import("../watcher.zig").NewWatcher(*HotReloader);
+
+pub const HotReloader = struct {
+ const watcher = @import("../watcher.zig");
+
+ onAccept: std.ArrayHashMapUnmanaged(Watcher.HashType, bun.BabyList(OnAcceptCallback), bun.ArrayIdentityContext, false) = .{},
+ vm: *JSC.VirtualMachine,
+
+ pub const HotReloadTask = struct {
+ reloader: *HotReloader,
+ count: u8 = 0,
+ hashes: [8]u32 = [_]u32{0} ** 8,
+ concurrent_task: JSC.ConcurrentTask = undefined,
+
+ pub fn append(this: *HotReloadTask, id: u32) void {
+ if (this.count == 8) {
+ this.enqueue();
+ var reloader = this.reloader;
+ this.* = .{
+ .reloader = reloader,
+ .count = 0,
+ };
+ }
+
+ this.hashes[this.count] = id;
+ this.count += 1;
+ }
+
+ pub fn run(this: *HotReloadTask) void {
+ this.reloader.vm.reload();
+ }
+
+ pub fn enqueue(this: *HotReloadTask) void {
+ if (this.count == 0)
+ return;
+ var that = bun.default_allocator.create(HotReloadTask) catch unreachable;
+
+ that.* = this.*;
+ this.count = 0;
+ that.concurrent_task.task = Task.init(that);
+ that.reloader.vm.eventLoop().enqueueTaskConcurrent(&that.concurrent_task);
+ }
+
+ pub fn deinit(this: *HotReloadTask) void {
+ bun.default_allocator.destroy(this);
+ }
+ };
+
+ fn NewCallback(comptime FunctionSignature: type) type {
+ return union(enum) {
+ javascript_callback: JSC.Strong,
+ zig_callback: struct {
+ ptr: *anyopaque,
+ function: FunctionSignature,
+ },
+ };
+ }
+
+ pub const OnAcceptCallback = NewCallback(fn (
+ vm: *JSC.VirtualMachine,
+ specifier: []const u8,
+ ) void);
+
+ pub fn enableHotModuleReloading(this: *VirtualMachine) void {
+ if (this.bun_watcher != null)
+ return;
+
+ var reloader = bun.default_allocator.create(HotReloader) catch @panic("OOM");
+ reloader.* = .{
+ .vm = this,
+ };
+ this.bun_watcher = JSC.Watcher.init(
+ reloader,
+ this.bundler.fs,
+ bun.default_allocator,
+ ) catch @panic("Failed to enable File Watcher");
+
+ this.bundler.resolver.watcher = Resolver.ResolveWatcher(*Watcher, onMaybeWatchDirectory).init(this.bun_watcher.?);
+
+ this.bun_watcher.?.start() catch @panic("Failed to start File Watcher");
+ }
+
+ pub fn onMaybeWatchDirectory(watch: *Watcher, file_path: string, dir_fd: StoredFileDescriptorType) void {
+ // We don't want to watch:
+ // - Directories outside the root directory
+ // - Directories inside node_modules
+ if (std.mem.indexOf(u8, file_path, "node_modules") == null and std.mem.indexOf(u8, file_path, watch.fs.top_level_dir) != null) {
+ watch.addDirectory(dir_fd, file_path, Watcher.getHash(file_path), false) catch {};
+ }
+ }
+
+ pub fn onFileUpdate(
+ this: *HotReloader,
+ events: []watcher.WatchEvent,
+ changed_files: []?[:0]u8,
+ watchlist: watcher.Watchlist,
+ ) void {
+ var slice = watchlist.slice();
+ const file_paths = slice.items(.file_path);
+ var counts = slice.items(.count);
+ const kinds = slice.items(.kind);
+ const hashes = slice.items(.hash);
+ var file_descriptors = slice.items(.fd);
+ var ctx = this.vm.bun_watcher.?;
+ defer ctx.flushEvictions();
+ defer Output.flush();
+
+ var bundler = &this.vm.bundler;
+ var fs: *Fs.FileSystem = bundler.fs;
+ var rfs: *Fs.FileSystem.RealFS = &fs.fs;
+ var resolver = &bundler.resolver;
+ var _on_file_update_path_buf: [bun.MAX_PATH_BYTES]u8 = undefined;
+
+ var current_task: HotReloadTask = .{
+ .reloader = this,
+ };
+ defer current_task.enqueue();
+
+ for (events) |event| {
+ const file_path = file_paths[event.index];
+ const update_count = counts[event.index] + 1;
+ counts[event.index] = update_count;
+ const kind = kinds[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 = hashes[event.index];
+
+ if (comptime Environment.isDebug) {
+ Output.prettyErrorln("[watcher] {s}: -- {}", .{ @tagName(kind), event.op });
+ }
+
+ switch (kind) {
+ .file => {
+ if (event.op.delete or event.op.rename) {
+ ctx.removeAtIndex(
+ event.index,
+ 0,
+ &.{},
+ .file,
+ );
+ }
+
+ if (comptime bun.FeatureFlags.verbose_watcher) {
+ Output.prettyErrorln("<r><d>File changed: {s}<r>", .{fs.relativeTo(file_path)});
+ }
+
+ if (event.op.write) {
+ current_task.append(id);
+ }
+ },
+ .directory => {
+ const affected = event.names(changed_files);
+ var entries_option: ?*Fs.FileSystem.RealFS.EntriesOption = null;
+ if (affected.len > 0) {
+ entries_option = rfs.entries.get(file_path);
+ }
+
+ rfs.bustEntriesCache(file_path);
+ resolver.dir_cache.remove(file_path);
+
+ if (entries_option) |dir_ent| {
+ var last_file_hash: Watcher.HashType = std.math.maxInt(Watcher.HashType);
+ for (affected) |changed_name_ptr| {
+ const changed_name: []const u8 = std.mem.span((changed_name_ptr orelse continue));
+ if (changed_name.len == 0 or changed_name[0] == '~' or changed_name[0] == '.') continue;
+
+ const loader = (bundler.options.loaders.get(Fs.PathName.init(changed_name).ext) orelse .file);
+ if (loader.isJavaScriptLikeOrJSON() or loader == .css) {
+ var path_string: bun.PathString = undefined;
+ var file_hash: Watcher.HashType = last_file_hash;
+ const abs_path: string = brk: {
+ if (dir_ent.entries.get(changed_name)) |file_ent| {
+ // reset the file descriptor
+ file_ent.entry.cache.fd = 0;
+ file_ent.entry.need_stat = true;
+ path_string = file_ent.entry.abs_path;
+ file_hash = Watcher.getHash(path_string.slice());
+ for (hashes) |hash, entry_id| {
+ if (hash == file_hash) {
+ file_descriptors[entry_id] = 0;
+ break;
+ }
+ }
+
+ break :brk path_string.slice();
+ } else {
+ var file_path_without_trailing_slash = std.mem.trimRight(u8, file_path, std.fs.path.sep_str);
+ @memcpy(&_on_file_update_path_buf, file_path_without_trailing_slash.ptr, file_path_without_trailing_slash.len);
+ _on_file_update_path_buf[file_path_without_trailing_slash.len] = std.fs.path.sep;
+
+ @memcpy(_on_file_update_path_buf[file_path_without_trailing_slash.len + 1 ..].ptr, changed_name.ptr, changed_name.len);
+ const path_slice = _on_file_update_path_buf[0 .. file_path_without_trailing_slash.len + changed_name.len + 1];
+ file_hash = Watcher.getHash(path_slice);
+ break :brk path_slice;
+ }
+ };
+
+ // skip consecutive duplicates
+ if (last_file_hash == file_hash) continue;
+ last_file_hash = file_hash;
+
+ Output.prettyErrorln("<r> <d>File change: {s}<r>", .{fs.relativeTo(abs_path)});
+ }
+ }
+ }
+
+ // if (event.op.delete or event.op.rename)
+ // ctx.watcher.removeAtIndex(event.index, hashes[event.index], parent_hashes, .directory);
+ if (comptime false) {
+ Output.prettyErrorln("<r>📁 <d>Dir change: {s}<r>", .{fs.relativeTo(file_path)});
+ } else {
+ Output.prettyErrorln("<r> <d>Dir change: {s}<r>", .{fs.relativeTo(file_path)});
+ }
+ },
+ }
+ }
+ }
+};