diff options
author | 2021-06-18 20:48:07 -0700 | |
---|---|---|
committer | 2021-06-18 20:48:07 -0700 | |
commit | 3f10c8790629ab157d9377759cc50a4b962cc6f4 (patch) | |
tree | cb15e8a1e41e879e91bafff104355365a2236a4b | |
parent | e0fa2e78da8083dc590c4b1f3d016ba545261b84 (diff) | |
download | bun-3f10c8790629ab157d9377759cc50a4b962cc6f4.tar.gz bun-3f10c8790629ab157d9377759cc50a4b962cc6f4.tar.zst bun-3f10c8790629ab157d9377759cc50a4b962cc6f4.zip |
CSS HMR!
-rw-r--r-- | build.zig | 2 | ||||
-rw-r--r-- | demos/simple-react/index.html | 4 | ||||
-rw-r--r-- | demos/simple-react/src/button.css | 15 | ||||
-rw-r--r-- | demos/simple-react/src/index.css | 2 | ||||
-rw-r--r-- | src/bundler.zig | 51 | ||||
-rw-r--r-- | src/css_scanner.zig | 24 | ||||
-rw-r--r-- | src/http.zig | 90 | ||||
-rw-r--r-- | src/http/websocket.zig | 16 | ||||
-rw-r--r-- | src/runtime.version | 2 | ||||
-rw-r--r-- | src/runtime/hmr.ts | 377 | ||||
-rw-r--r-- | src/watcher.zig | 4 |
11 files changed, 412 insertions, 175 deletions
@@ -107,7 +107,7 @@ pub fn build(b: *std.build.Builder) void { } } - const runtime_hash = std.hash.Wyhash.hash(0, @embedFile("./src/runtime.js")); + const runtime_hash = std.hash.Wyhash.hash(0, @embedFile("./src/runtime.out.js")); const runtime_version_file = std.fs.cwd().openFile("src/runtime.version", .{ .write = true }) catch unreachable; runtime_version_file.writer().print("{x}", .{runtime_hash}) catch unreachable; defer runtime_version_file.close(); diff --git a/demos/simple-react/index.html b/demos/simple-react/index.html index 0d1277743..3e227399a 100644 --- a/demos/simple-react/index.html +++ b/demos/simple-react/index.html @@ -1,8 +1,8 @@ <!DOCTYPE html> <html> <head> - <link rel="stylesheet" href="../src/index.css" /> - <script async src="../src/index.tsx" type="module"></script> + <link rel="stylesheet" href="src/index.css" /> + <script async src="src/index.tsx" type="module"></script> </head> <body> <div id="reactroot"></div> diff --git a/demos/simple-react/src/button.css b/demos/simple-react/src/button.css index 6940cffb3..15cca59e0 100644 --- a/demos/simple-react/src/button.css +++ b/demos/simple-react/src/button.css @@ -1,21 +1,20 @@ body { - background-color: yellow; + background-color: green; + border: 10px solid pink; + color: pink; + box-shadow: 10px 10px 32px red; } body { - background-color: yellow; + background-color: blue; } body { - background-color: yellow; + background-color: aliceblue; } body { - background-color: yellow; -} - -body { - background-color: yellow; + background-color: red; } body { diff --git a/demos/simple-react/src/index.css b/demos/simple-react/src/index.css index e1bfbb825..cbcf1e654 100644 --- a/demos/simple-react/src/index.css +++ b/demos/simple-react/src/index.css @@ -1,5 +1,5 @@ @import "./button.css"; body { - background-color: pink; + background-color: orange; } diff --git a/src/bundler.zig b/src/bundler.zig index b4226d4d9..1bcf12a58 100644 --- a/src/bundler.zig +++ b/src/bundler.zig @@ -874,27 +874,54 @@ pub fn NewBundler(cache_files: bool) type { switch (loader) { .css => { + const CSSBundlerHMR = Css.NewBundler( + Writer, + @TypeOf(&bundler.linker), + @TypeOf(&bundler.resolver.caches.fs), + WatcherType, + @TypeOf(bundler.fs), + true, + ); + const CSSBundler = Css.NewBundler( Writer, @TypeOf(&bundler.linker), @TypeOf(&bundler.resolver.caches.fs), WatcherType, @TypeOf(bundler.fs), + false, ); return BuildResolveResultPair{ - .written = try CSSBundler.runWithResolveResult( - resolve_result, - bundler.fs, - writer, - watcher, - &bundler.resolver.caches.fs, - filepath_hash, - file_descriptor, - allocator, - bundler.log, - &bundler.linker, - ), + .written = brk: { + if (bundler.options.hot_module_reloading) { + break :brk try CSSBundlerHMR.bundle( + resolve_result.path_pair.primary.text, + bundler.fs, + writer, + watcher, + &bundler.resolver.caches.fs, + filepath_hash, + file_descriptor, + allocator, + bundler.log, + &bundler.linker, + ); + } else { + break :brk try CSSBundler.bundle( + resolve_result.path_pair.primary.text, + bundler.fs, + writer, + watcher, + &bundler.resolver.caches.fs, + filepath_hash, + file_descriptor, + allocator, + bundler.log, + &bundler.linker, + ); + } + }, .input_fd = file_descriptor, }; }, diff --git a/src/css_scanner.zig b/src/css_scanner.zig index 85acc6de4..1bfada0a0 100644 --- a/src/css_scanner.zig +++ b/src/css_scanner.zig @@ -994,6 +994,7 @@ pub fn NewBundler( comptime FileReader: type, comptime Watcher: type, comptime FSType: type, + comptime hot_module_reloading: bool, ) type { return struct { const CSSBundler = @This(); @@ -1005,8 +1006,8 @@ pub fn NewBundler( fs_reader: FileReader, fs: FSType, allocator: *std.mem.Allocator, - pub fn runWithResolveResult( - resolve_result: resolver.Result, + pub fn bundle( + absolute_path: string, fs: FSType, writer: Writer, watcher: *Watcher, @@ -1049,7 +1050,7 @@ pub fn NewBundler( ); css.buildCtx = &this; - try this.addCSSImport(resolve_result.path_pair.primary.text); + try this.addCSSImport(absolute_path); while (this.import_queue.readItem()) |item| { const watcher_id = this.watcher.indexOf(item) orelse unreachable; @@ -1059,6 +1060,19 @@ pub fn NewBundler( try css.scan(log, allocator); } + // This exists to identify the entry point + // When we do HMR, ask the entire bundle to be regenerated + // But, we receive a file change event for a file within the bundle + // So the inner ID is used to say "does this bundle need to be reloaded?" + // The outer ID is used to say "go ahead and reload this" + + if (hot_module_reloading and FeatureFlags.css_supports_fence and this.bundle_queue.items.len > 0) { + try this.writeAll("\n@supports (hmr-bid:"); + const int_buf_size = std.fmt.formatIntBuf(&int_buf_print, hash, 10, .upper, .{}); + try this.writeAll(int_buf_print[0..int_buf_size]); + try this.writeAll(") {}\n"); + } + // We LIFO var i: i32 = @intCast(i32, this.bundle_queue.items.len - 1); while (i >= 0) : (i -= 1) { @@ -1068,8 +1082,8 @@ pub fn NewBundler( const source = try this.getSource(watch_item.file_path, watch_item.fd); css.source = &source; const file_path = fs.relativeTo(watch_item.file_path); - if (FeatureFlags.css_supports_fence) { - try this.writeAll("\n@supports (hmr-watch-id:"); + if (hot_module_reloading and FeatureFlags.css_supports_fence) { + try this.writeAll("\n@supports (hmr-wid:"); const int_buf_size = std.fmt.formatIntBuf(&int_buf_print, item, 10, .upper, .{}); try this.writeAll(int_buf_print[0..int_buf_size]); try this.writeAll(") and (hmr-file:\""); diff --git a/src/http.zig b/src/http.zig index 51949b713..bd7b3477f 100644 --- a/src/http.zig +++ b/src/http.zig @@ -11,6 +11,8 @@ const bundler = @import("bundler.zig"); const logger = @import("logger.zig"); const Fs = @import("./fs.zig"); const Options = @import("./options.zig"); +const Css = @import("css_scanner.zig"); + pub fn constStrToU8(s: string) []u8 { return @intToPtr([*]u8, @ptrToInt(s.ptr))[0..s.len]; } @@ -443,6 +445,11 @@ pub const RequestContext = struct { const fd = this.watcher.watchlist.items(.fd)[index]; const loader = this.watcher.watchlist.items(.loader)[index]; + const path = Fs.Path.init(file_path_str); + var old_log = this.bundler.log; + defer this.bundler.log = old_log; + this.bundler.log = &log; + switch (loader) { .json, .ts, .tsx, .js, .jsx => { // Since we already have: @@ -452,11 +459,8 @@ pub const RequestContext = struct { // We can skip resolving. We will need special handling for renaming where basically we: // - Update the watch item. // - Clear directory cache - const path = Fs.Path.init(file_path_str); - var old_log = this.bundler.log; - defer this.bundler.log = old_log; - this.bundler.log = &log; this.bundler.resetStore(); + var parse_result = this.bundler.parse( this.bundler.allocator, path, @@ -508,6 +512,75 @@ pub const RequestContext = struct { .timestamp = WebsocketHandler.toTimestamp(this.timer.read()), }; }, + .css => { + const CSSBundlerHMR = Css.NewBundler( + @TypeOf(&this.printer), + @TypeOf(&this.bundler.linker), + @TypeOf(&this.bundler.resolver.caches.fs), + Watcher, + @TypeOf(this.bundler.fs), + true, + ); + + const CSSBundler = Css.NewBundler( + @TypeOf(&this.printer), + @TypeOf(&this.bundler.linker), + @TypeOf(&this.bundler.resolver.caches.fs), + Watcher, + @TypeOf(this.bundler.fs), + false, + ); + + this.printer.ctx.reset(); + + const written = brk: { + if (this.bundler.options.hot_module_reloading) { + break :brk try CSSBundlerHMR.bundle( + file_path_str, + this.bundler.fs, + &this.printer, + this.watcher, + &this.bundler.resolver.caches.fs, + this.watcher.watchlist.items(.hash)[index], + fd, + this.allocator, + &log, + &this.bundler.linker, + ); + } else { + break :brk try CSSBundler.bundle( + file_path_str, + this.bundler.fs, + &this.printer, + this.watcher, + &this.bundler.resolver.caches.fs, + this.watcher.watchlist.items(.hash)[index], + fd, + this.allocator, + &log, + &this.bundler.linker, + ); + } + }; + + return WatchBuildResult{ + .value = .{ + .success = .{ + .id = id, + .from_timestamp = from_timestamp, + .loader = .css, + .module_path = this.bundler.fs.relativeTo(file_path_str), + .blob_length = @truncate(u32, written), + // .log = std.mem.zeroes(Api.Log), + }, + }, + .id = id, + .bytes = this.printer.ctx.written, + .approximate_newline_count = 0, + // .approximate_newline_count = parse_result.ast.approximate_newline_count, + .timestamp = WebsocketHandler.toTimestamp(this.timer.read()), + }; + }, else => { return WatchBuildResult{ .value = .{ .fail = std.mem.zeroes(Api.WebsocketMessageBuildFailure) }, @@ -547,7 +620,7 @@ pub const RequestContext = struct { .watcher = ctx.watcher, }; - clone.websocket = Websocket.Websocket.create(ctx, SOCKET_FLAGS); + clone.websocket = Websocket.Websocket.create(&clone.conn, SOCKET_FLAGS); clone.tombstone = false; try open_websockets.append(clone); return clone; @@ -570,13 +643,17 @@ pub const RequestContext = struct { var markForClosing = false; for (open_websockets.items) |item| { var socket: *WebsocketHandler = item; + if (socket.tombstone) { + continue; + } + const written = socket.websocket.writeBinary(message) catch |err| brk: { Output.prettyError("<r>WebSocket error: <b>{d}", .{@errorName(err)}); markForClosing = true; break :brk 0; }; - if (written < message.len) { + if (socket.tombstone or written < message.len) { markForClosing = true; } @@ -977,7 +1054,6 @@ pub const RequestContext = struct { if (buf.len < 16 * 16 * 16 * 16 or chunky._loader == .css or chunky._loader == .json) { const strong_etag = std.hash.Wyhash.hash(1, buf); const etag_content_slice = std.fmt.bufPrintIntToSlice(strong_etag_buffer[0..49], strong_etag, 16, .upper, .{}); - chunky.rctx.appendHeader("ETag", etag_content_slice); if (chunky.rctx.header("If-None-Match")) |etag_header| { diff --git a/src/http/websocket.zig b/src/http/websocket.zig index be1645d49..2cfa3f620 100644 --- a/src/http/websocket.zig +++ b/src/http/websocket.zig @@ -100,7 +100,7 @@ pub const Websocket = struct { EndOfStream, } || std.fs.File.WriteError; - request: *RequestContext, + conn: *tcp.Connection, err: ?anyerror = null, buf: [4096]u8 = undefined, @@ -108,7 +108,7 @@ pub const Websocket = struct { reader: ReadStream.Reader, flags: u32 = 0, pub fn create( - ctx: *RequestContext, + conn: *tcp.Connection, comptime flags: u32, ) Websocket { var stream = ReadStream{ @@ -118,7 +118,7 @@ pub const Websocket = struct { var socket = Websocket{ .read_stream = undefined, .reader = undefined, - .request = ctx, + .conn = conn, .flags = flags, }; @@ -187,7 +187,7 @@ pub const Websocket = struct { } pub fn writeHeader(self: *Websocket, header: WebsocketHeader, n: usize) anyerror!void { - var stream = self.request.conn.client.writer(self.flags); + var stream = self.conn.client.writer(self.flags); try stream.writeIntBig(u16, @bitCast(u16, header)); @@ -203,7 +203,7 @@ pub const Websocket = struct { } pub fn writeIterator(self: *Websocket, header: WebsocketHeader, count: usize, comptime BodyIterator: type, body_iter: BodyIterator) anyerror!usize { - var stream = self.request.conn.client.writer(self.flags); + var stream = self.conn.client.writer(self.flags); if (!dataframe.isValid()) return error.InvalidMessage; @@ -233,7 +233,7 @@ pub const Websocket = struct { // Write a raw data frame pub fn writeDataFrame(self: *Websocket, dataframe: WebsocketDataFrame) anyerror!usize { - var stream = self.request.conn.client.writer(self.flags); + var stream = self.conn.client.writer(self.flags); if (!dataframe.isValid()) return error.InvalidMessage; @@ -271,7 +271,7 @@ pub const Websocket = struct { @memset(&self.buf, 0, self.buf.len); // Read and retry if we hit the end of the stream buffer - var start = try self.request.conn.client.read(&self.buf, self.flags); + var start = try self.conn.client.read(&self.buf, self.flags); if (start == 0) { return error.ConnectionClosed; } @@ -329,7 +329,7 @@ pub const Websocket = struct { const end = start + length; if (end > self.read_stream.pos) { - var extend_length = try self.request.conn.client.read(self.buf[self.read_stream.pos..], self.flags); + var extend_length = try self.conn.client.read(self.buf[self.read_stream.pos..], self.flags); if (self.read_stream.pos + extend_length > self.buf.len) { return error.MessageTooLarge; } diff --git a/src/runtime.version b/src/runtime.version index d7de1f38d..83772425a 100644 --- a/src/runtime.version +++ b/src/runtime.version @@ -1 +1 @@ -727d9b7284639b19
\ No newline at end of file +a1b2ed0e7019e499
\ No newline at end of file diff --git a/src/runtime/hmr.ts b/src/runtime/hmr.ts index 41e756778..583d0b34d 100644 --- a/src/runtime/hmr.ts +++ b/src/runtime/hmr.ts @@ -8,117 +8,238 @@ function formatDuration(duration: number) { return Math.round(duration * 1000) / 1000; } -class StringListPointer { - ptr: API.StringPointer; - source_index: number; -} +type CSSHMRInsertionPoint = { + id: number; + node: HTMLLinkElement; + file: string; + bundle_id: number; +}; // How this works -// The first time you load a <link rel="stylesheet"> -// It loads via @import. The natural way. -// Then, you change a file. Say, button.css: -// @import chain: -// index.css -> link.css -> button.css -> foo.css -// HTML: -// <link rel="stylesheet" href="./index.css"> -// Now, we need to update "button.css". But, we can't control that. -// Instead, we replace '<link rel="stylesheet" href="./index.css">' -// With: -// - <link rel="stylesheet" href="/_assets/1290123980123.css?noimport"> -// - <link rel="stylesheet" href="/_assets/1290123980123.css?noimport"> -// - <link rel="stylesheet" href="/_assets/1290123980123.css?noimport"> -// - <link rel="stylesheet" href="/_assets/1290123980123.css?noimport"> -// Now, say you update "link.css". -// This time, we replace: -// <link rel="stylesheet" href="./link.css?noimport"> -// With: -// <link rel="stylesheet" href="./link.css?noimport&${from_timestamp}"> +// We keep export class CSSLoader { hmr: HMRClient; - manifest?: API.DependencyManifest; + private static cssLoadId: CSSHMRInsertionPoint = { + id: 0, + bundle_id: 0, + node: null, + file: "", + }; - stringList: string[] = []; - idMap: Map<number, StringListPointer> = new Map(); + // This is a separate function because calling a small function 2000 times is more likely to cause it to be JIT'd + // We want it to be JIT'd + // It's possible that returning null may be a de-opt though. + private findMatchingSupportsRule( + rule: CSSSupportsRule, + id: number, + sheet: CSSStyleSheet + ): CSSHMRInsertionPoint | null { + switch (rule.type) { + // 12 is result.SUPPORTS_RULE + case 12: { + if (!rule.conditionText.startsWith("(hmr-wid:")) { + return null; + } - selectorForId(id: number) { - return `hmr__${id.toString(10)}`; - } + const startIndex = "hmr-wid:".length + 1; + const endIDRegion = rule.conditionText.indexOf(")", startIndex); + if (endIDRegion === -1) return null; - fetchLinkTagById(id: number) { - const selector = this.selectorForId(id); - var element: HTMLLinkElement = document.querySelector(selector); + const int = parseInt( + rule.conditionText.substring(startIndex, endIDRegion), + 10 + ); - if (!element) { - element = document.createElement("link"); - element.setAttribute("rel", "stylesheet"); - element.setAttribute("id", selector); - element.setAttribute("href", `/_assets/${id}.css?noimport`); + if (int !== id) { + return null; + } + + let startFileRegion = rule.conditionText.indexOf( + '(hmr-file:"', + endIDRegion + ); + if (startFileRegion === -1) return null; + startFileRegion += '(hmr-file:"'.length + 1; + + const endFileRegion = rule.conditionText.indexOf('"', startFileRegion); + if (endFileRegion === -1) return null; + // Empty file strings are invalid + if (endFileRegion - startFileRegion <= 0) return null; + + CSSLoader.cssLoadId.id = int; + CSSLoader.cssLoadId.node = sheet.ownerNode as HTMLLinkElement; + CSSLoader.cssLoadId.file = rule.conditionText.substring( + startFileRegion - 1, + endFileRegion + ); + + return CSSLoader.cssLoadId; + } + default: { + return null; + } } + } - return element; + bundleId(): number { + return CSSLoader.cssLoadId.bundle_id; } - handleManifestSuccess(buffer: ByteBuffer, timestamp: number) { - const success = API.decodeWebsocketMessageManifestSuccess(buffer); - if (success.loader !== API.Loader.css) { - __hmrlog.warn( - "Ignoring unimplemented loader:", - API.LoaderKeys[success.loader] + private findCSSLinkTag(id: number): CSSHMRInsertionPoint | null { + const count = document.styleSheets.length; + let match: CSSHMRInsertionPoint = null; + for (let i = 0; i < count && match === null; i++) { + let cssRules: CSSRuleList; + let sheet: CSSStyleSheet; + let ruleCount = 0; + // Non-same origin stylesheets will potentially throw "Security error" + // We will ignore those stylesheets and look at others. + try { + sheet = document.styleSheets.item(i); + cssRules = sheet.rules; + ruleCount = sheet.rules.length; + } catch (exception) { + continue; + } + + if ( + sheet.disabled || + !sheet.href || + sheet.href.length === 0 || + sheet.rules.length === 0 + ) { + continue; + } + + const bundleIdRule = cssRules[0] as CSSSupportsRule; + if ( + bundleIdRule.type !== 12 || + !bundleIdRule.conditionText.startsWith("(hmr-bid:") + ) { + continue; + } + + const bundleIdEnd = bundleIdRule.conditionText.indexOf( + ")", + "(hmr-bid:".length + 1 ); - return; - } + if (bundleIdEnd === -1) continue; - const rootSelector = this.selectorForId(success.id); - let rootLinkTag: HTMLLinkElement = document.querySelector(rootSelector); - if (!rootLinkTag) { - for (let linkTag of document.querySelectorAll("link")) { - if ( - new URL(linkTag.href, location.href).pathname.substring(1) === - success.module_path - ) { - rootLinkTag = linkTag; - break; - } + CSSLoader.cssLoadId.bundle_id = parseInt( + bundleIdRule.conditionText.substring("(hmr-bid:".length, bundleIdEnd), + 10 + ); + + for (let j = 1; j < ruleCount && match === null; j++) { + match = this.findMatchingSupportsRule( + cssRules[j] as CSSSupportsRule, + id, + sheet + ); } } - if (!rootLinkTag) { - __hmrlog.debug("Skipping unknown CSS file", success.module_path); - return; + // Ensure we don't leak the HTMLLinkElement + if (match === null) { + CSSLoader.cssLoadId.file = ""; + CSSLoader.cssLoadId.bundle_id = CSSLoader.cssLoadId.id = 0; + CSSLoader.cssLoadId.node = null; } - const elementList: HTMLLinkElement = new Array(); - for (let i = 0; i < success.manifest.files.length; i++) {} + return match; } - handleManifestFail(buffer: ByteBuffer, timestamp: number) {} - static request_manifest_buf: Uint8Array = undefined; - handleFileChangeNotification( - file_change_notification: API.WebsocketMessageFileChangeNotification, + + handleBuildSuccess( + buffer: ByteBuffer, + build: API.WebsocketMessageBuildSuccess, timestamp: number ) { - if (!CSSLoader.request_manifest_buf) { - CSSLoader.request_manifest_buf = new Uint8Array(255); - } - var buf = new ByteBuffer(CSSLoader.request_manifest_buf); - API.encodeWebsocketCommand( - { - kind: API.WebsocketCommandKind.manifest, - timestamp, - }, - buf - ); - API.encodeWebsocketCommandManifest( - { - id: file_change_notification.id, - }, - buf + const start = performance.now(); + var update = this.findCSSLinkTag(build.id); + if (update === null) { + __hmrlog.debug("Skipping unused CSS."); + return; + } + + let blob = new Blob( + [ + buffer._data.length > buffer._index + ? buffer._data.subarray(buffer._index) + : new Uint8Array(0), + ], + { type: "text/css" } ); + buffer = null; + const blobURL = URL.createObjectURL(blob); + let filepath = update.file; + const _timestamp = timestamp; + const from_timestamp = build.from_timestamp; + function onLoadHandler(load: Event) { + const localDuration = formatDuration(performance.now() - start); + const fsDuration = _timestamp - from_timestamp; + __hmrlog.log( + "Reloaded in", + `${localDuration + fsDuration}ms`, + "-", + filepath + ); - try { - this.hmr.socket.send(buf._data.subarray(0, buf._index)); - } catch (exception) { - __hmrlog.error(exception); + blob = null; + update = null; + filepath = null; + + if (this.href.includes("blob:")) { + URL.revokeObjectURL(this.href); + } + } + // onLoad doesn't fire in Chrome. + // I'm not sure why. + // Guessing it only triggers when an element is added/removed, not when the href just changes + // So we say on the next tick, we're loaded. + setTimeout(onLoadHandler.bind(update.node), 0); + if (update.node.href.includes("blob:")) { + URL.revokeObjectURL(update.node.href); + } + update.node.setAttribute("href", blobURL); + URL.revokeObjectURL(blobURL); + } + + reload(timestamp: number) { + // function onLoadHandler(load: Event) { + // const localDuration = formatDuration(performance.now() - start); + // const fsDuration = _timestamp - from_timestamp; + // __hmrlog.log( + // "Reloaded in", + // `${localDuration + fsDuration}ms`, + // "-", + // filepath + // ); + + // blob = null; + // update = null; + // filepath = null; + + // if (this.href.includes("blob:")) { + // URL.revokeObjectURL(this.href); + // } + // } + + const url = new URL(CSSLoader.cssLoadId.node.href, location.href); + url.searchParams.set("v", timestamp.toString(10)); + CSSLoader.cssLoadId.node.setAttribute("href", url.toString()); + } + + filePath( + file_change_notification: API.WebsocketMessageFileChangeNotification + ): string | null { + if (file_change_notification.loader !== API.Loader.css) return null; + const tag = this.findCSSLinkTag(file_change_notification.id); + + if (!tag) { + return null; } + + return tag.file; } } @@ -217,24 +338,14 @@ class HMRClient { handleBuildSuccess(buffer: ByteBuffer, timestamp: number) { const build = API.decodeWebsocketMessageBuildSuccess(buffer); - const id = build.id; - const index = this.indexOfModuleId(id); - // Ignore builds of modules that are not loaded - if (index === -1) { - if (this.verbose) { - __hmrlog.debug(`Skipping reload for unknown module id:`, id); - } - - return; - } // Ignore builds of modules we expect a later version of - const currentVersion = this.builds.get(id) || -Infinity; + const currentVersion = this.builds.get(build.id) || -Infinity; if (currentVersion > build.from_timestamp) { if (this.verbose) { __hmrlog.debug( - `Ignoring outdated update for "${HMRModule.dependencies.modules[index].file_path}".\n Expected: >=`, + `Ignoring outdated update for "${build.module_path}".\n Expected: >=`, currentVersion, `\n Received:`, build.from_timestamp @@ -243,6 +354,21 @@ class HMRClient { return; } + if (build.loader === API.Loader.css) { + return this.loaders.css.handleBuildSuccess(buffer, build, timestamp); + } + + const id = build.id; + const index = this.indexOfModuleId(id); + // Ignore builds of modules that are not loaded + if (index === -1) { + if (this.verbose) { + __hmrlog.debug(`Skipping reload for unknown module id:`, id); + } + + return; + } + if (this.verbose) { __hmrlog.debug( "Preparing to reload", @@ -277,25 +403,26 @@ class HMRClient { handleFileChangeNotification(buffer: ByteBuffer, timestamp: number) { const notification = API.decodeWebsocketMessageFileChangeNotification(buffer); - if (notification.loader === API.Loader.css) { - if (typeof window === "undefined") { - __hmrlog.debug(`Skipping CSS on non-webpage environment`); - return; + let file_path = ""; + switch (notification.loader) { + case API.Loader.css: { + file_path = this.loaders.css.filePath(notification); + break; } - if ((this.builds.get(notification.id) || -Infinity) > timestamp) { - __hmrlog.debug(`Skipping outdated update`); - return; - } + default: { + const index = HMRModule.dependencies.graph.indexOf(notification.id); - this.loaders.css.handleFileChangeNotification(notification, timestamp); - this.builds.set(notification.id, timestamp); - return; + if (index > -1) { + file_path = HMRModule.dependencies.modules[index].file_path; + } + break; + } } - const index = HMRModule.dependencies.graph.indexOf(notification.id); + const accept = file_path && file_path.length > 0; - if (index === -1) { + if (!accept) { if (this.verbose) { __hmrlog.debug("Unknown module changed, skipping"); } @@ -303,19 +430,24 @@ class HMRClient { } if ((this.builds.get(notification.id) || -Infinity) > timestamp) { - __hmrlog.debug( - `Received update for ${HMRModule.dependencies.modules[index].file_path}` - ); + __hmrlog.debug(`Received update for ${file_path}`); return; } if (this.verbose) { - __hmrlog.debug( - `Requesting update for ${HMRModule.dependencies.modules[index].file_path}` - ); + __hmrlog.debug(`Requesting update for ${file_path}`); } this.builds.set(notification.id, timestamp); + + // When we're dealing with CSS, even though the watch event happened for a file in the bundle + // We want it to regenerate the entire bundle + // So we must swap out the ID we send for the ID of the corresponding bundle. + if (notification.loader === API.Loader.css) { + notification.id = this.loaders.css.bundleId(); + this.builds.set(notification.id, timestamp); + } + this.buildCommandBuf[0] = API.WebsocketCommandKind.build; this.buildCommandUArray[0] = timestamp; this.buildCommandBuf.set(this.buildCommandUArrayEight, 1); @@ -349,14 +481,7 @@ class HMRClient { this.handleBuildSuccess(buffer, header.timestamp); break; } - case API.WebsocketMessageKind.manifest_success: { - this.loaders.css.handleManifestSuccess(buffer, header.timestamp); - break; - } - case API.WebsocketMessageKind.manifest_fail: { - this.loaders.css.handleManifestFail(buffer, header.timestamp); - break; - } + case API.WebsocketMessageKind.file_change_notification: { this.handleFileChangeNotification(buffer, header.timestamp); break; @@ -385,7 +510,7 @@ class HMRClient { return; } - this.reconnect = setInterval(this.connect, 500) as any as number; + this.reconnect = globalThis.setInterval(this.connect, 500) as any as number; __hmrlog.warn("HMR disconnected. Attempting to reconnect."); }; } diff --git a/src/watcher.zig b/src/watcher.zig index e840bbca6..ea1ba44ac 100644 --- a/src/watcher.zig +++ b/src/watcher.zig @@ -111,12 +111,8 @@ pub fn NewWatcher(comptime ContextType: type) type { pub fn watchLoop(this: *Watcher) !void { this.watchloop_handle = std.Thread.getCurrentThreadId(); var stdout = std.io.getStdOut(); - // var stdout = std.io.bufferedWriter(stdout_file.writer()); var stderr = std.io.getStdErr(); - // var stderr = std.io.bufferedWriter(stderr_file.writer()); var output_source = Output.Source.init(stdout, stderr); - // defer stdout.flush() catch {}; - // defer stderr.flush() catch {}; Output.Source.set(&output_source); Output.enable_ansi_colors = stderr.isTty(); |