aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGravatar Jarred Sumner <jarred@jarredsumner.com> 2021-06-18 20:48:07 -0700
committerGravatar Jarred Sumner <jarred@jarredsumner.com> 2021-06-18 20:48:07 -0700
commit3f10c8790629ab157d9377759cc50a4b962cc6f4 (patch)
treecb15e8a1e41e879e91bafff104355365a2236a4b
parente0fa2e78da8083dc590c4b1f3d016ba545261b84 (diff)
downloadbun-3f10c8790629ab157d9377759cc50a4b962cc6f4.tar.gz
bun-3f10c8790629ab157d9377759cc50a4b962cc6f4.tar.zst
bun-3f10c8790629ab157d9377759cc50a4b962cc6f4.zip
CSS HMR!
-rw-r--r--build.zig2
-rw-r--r--demos/simple-react/index.html4
-rw-r--r--demos/simple-react/src/button.css15
-rw-r--r--demos/simple-react/src/index.css2
-rw-r--r--src/bundler.zig51
-rw-r--r--src/css_scanner.zig24
-rw-r--r--src/http.zig90
-rw-r--r--src/http/websocket.zig16
-rw-r--r--src/runtime.version2
-rw-r--r--src/runtime/hmr.ts377
-rw-r--r--src/watcher.zig4
11 files changed, 412 insertions, 175 deletions
diff --git a/build.zig b/build.zig
index d9db8368b..c603a8006 100644
--- a/build.zig
+++ b/build.zig
@@ -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();