aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGravatar Jarred Sumner <jarred@jarredsumner.com> 2021-06-20 18:15:13 -0700
committerGravatar Jarred Sumner <jarred@jarredsumner.com> 2021-06-20 18:15:13 -0700
commitd09194f05a372e3ed136aa288ae76cae8c1dc641 (patch)
treeab7e49f9793bc493d89274773d444ac59c0d3163
parent6fbfd696990e77020a3d7359fdcbc3e01de40a60 (diff)
downloadbun-d09194f05a372e3ed136aa288ae76cae8c1dc641.tar.gz
bun-d09194f05a372e3ed136aa288ae76cae8c1dc641.tar.zst
bun-d09194f05a372e3ed136aa288ae76cae8c1dc641.zip
Support live-reload and fallback
Former-commit-id: c3f9d77391589b65951616a632af87107fba469f
-rw-r--r--demos/react-fast-refresh-test/src/components/RenderCounter.tsx21
-rw-r--r--demos/react-fast-refresh-test/src/components/app.tsx14
-rw-r--r--demos/react-fast-refresh-test/src/components/button.tsx38
-rw-r--r--demos/react-fast-refresh-test/src/components/new-comp.tsx3
-rw-r--r--demos/react-fast-refresh-test/src/index.css200
-rw-r--r--demos/react-fast-refresh-test/src/index.tsx11
-rw-r--r--src/api/schema.d.ts35
-rw-r--r--src/api/schema.js126
-rw-r--r--src/api/schema.peechy11
-rw-r--r--src/api/schema.zig88
-rw-r--r--src/http.zig12
-rw-r--r--src/js_ast.zig15
-rw-r--r--src/js_parser/js_parser.zig106
-rw-r--r--src/node_module_bundle.zig5
-rw-r--r--src/runtime.footer.js14
-rw-r--r--src/runtime.version2
-rw-r--r--src/runtime.zig1
-rw-r--r--src/runtime/hmr.ts1698
18 files changed, 1464 insertions, 936 deletions
diff --git a/demos/react-fast-refresh-test/src/components/RenderCounter.tsx b/demos/react-fast-refresh-test/src/components/RenderCounter.tsx
new file mode 100644
index 000000000..ed2f00b56
--- /dev/null
+++ b/demos/react-fast-refresh-test/src/components/RenderCounter.tsx
@@ -0,0 +1,21 @@
+import React from "react";
+
+export function RenderCounter({ name, children }) {
+ const counter = React.useRef(1);
+ return (
+ <div className="RenderCounter">
+ <div className="RenderCounter-meta">
+ <div className="RenderCounter-title">
+ {name} rendered <strong>{counter.current++} times</strong>
+ </div>
+ <div className="RenderCounter-lastRender">
+ LAST RENDER:{" "}
+ {new Intl.DateTimeFormat([], {
+ timeStyle: "long",
+ }).format(new Date())}
+ </div>
+ </div>
+ <div className="RenderCounter-children">{children}</div>
+ </div>
+ );
+}
diff --git a/demos/react-fast-refresh-test/src/components/app.tsx b/demos/react-fast-refresh-test/src/components/app.tsx
new file mode 100644
index 000000000..2edc02545
--- /dev/null
+++ b/demos/react-fast-refresh-test/src/components/app.tsx
@@ -0,0 +1,14 @@
+import * as React from "react";
+import { Button } from "./Button";
+import { RenderCounter } from "./RenderCounter";
+export function App() {
+ return (
+ <RenderCounter name="App">
+ <div className="AppRoot">
+ <h1>This is the root element</h1>
+
+ <Button>Click</Button>
+ </div>
+ </RenderCounter>
+ );
+}
diff --git a/demos/react-fast-refresh-test/src/components/button.tsx b/demos/react-fast-refresh-test/src/components/button.tsx
index 3f55fae34..4c3388670 100644
--- a/demos/react-fast-refresh-test/src/components/button.tsx
+++ b/demos/react-fast-refresh-test/src/components/button.tsx
@@ -1,37 +1,9 @@
-import React from "react";
-import { NewComponent } from "./new-comp";
-
-const Toast = () => {
- const [baconyes, baconno] = useBacon();
- return <div>false</div>;
-};
-const Button = ({ label, label2, onClick }) => {
- const useCustomHookInsideFunction = (what, arr) => {
- return [true, false];
- };
- const [on, setOn] = React.useState(false);
-
- React.useEffect(() => {
- console.log({ on });
- }, [on]);
-
- // const [foo1, foo2] = useCustomHookInsideFunction(() => {}, [on]);
+import { RenderCounter } from "./RenderCounter";
+export const Button = ({ children }) => {
return (
- <div className="Button" onClick={onClick}>
- <Toast>f</Toast>
- <div className="Button-label">{label}12</div>
- <NewComponent />
- </div>
+ <RenderCounter name="Button">
+ <div className="Button">{children}</div>
+ </RenderCounter>
);
};
-
-const Bacon = Button;
-
-export { Bacon, Bacon as Button };
-
-const RefreshLike = () => {};
-
-const useBacon = () => {
- return [1, 8];
-};
diff --git a/demos/react-fast-refresh-test/src/components/new-comp.tsx b/demos/react-fast-refresh-test/src/components/new-comp.tsx
deleted file mode 100644
index f09c64a54..000000000
--- a/demos/react-fast-refresh-test/src/components/new-comp.tsx
+++ /dev/null
@@ -1,3 +0,0 @@
-export const NewComponent = () => {
- return <div>NEW!</div>;
-};
diff --git a/demos/react-fast-refresh-test/src/index.css b/demos/react-fast-refresh-test/src/index.css
index 0917f6c7a..c4514199c 100644
--- a/demos/react-fast-refresh-test/src/index.css
+++ b/demos/react-fast-refresh-test/src/index.css
@@ -42,18 +42,6 @@ body {
height: 100%;
}
-.Subtitle {
- text-align: center;
- font-size: 4em;
- margin: 0;
- padding: 0;
- margin-bottom: 0.25em;
-
- align-items: center;
- display: flex;
- flex-direction: row;
-}
-
#reactroot,
#__next,
body,
@@ -61,176 +49,50 @@ html {
height: 100%;
}
-.Title {
- color: var(--color-brand);
- font-family: var(--heading-font);
- font-weight: 700;
- margin-top: 48px;
- font-size: 48px;
- text-transform: capitalize;
- text-align: center;
-}
-
-.Description {
- text-align: center;
-}
-
-.main {
- display: flex;
- flex-direction: column;
- height: 100%;
-}
-
-header,
-.main {
- width: 650px;
- margin: 0 auto;
-}
-
-section {
- width: 650px;
-}
-
-header {
- margin-bottom: 48px;
-}
-
-footer {
- flex-shrink: 0;
-}
-
-#reactroot,
-#__next {
- display: flex;
- flex-direction: column;
- justify-content: center;
+.RenderCounter {
+ border: 10px solid var(--snippets_container-background-focused);
+ margin: 10px;
+ padding: 10px;
+ animation: flash 0.2s linear;
+ animation-fill-mode: forwards;
}
-section {
- height: 300px;
+.RenderCounter-meta {
display: flex;
- flex-direction: column;
+ flex-direction: row;
+ justify-content: space-between;
+ margin: -10px;
+ padding: 10px;
+ background-color: #111;
}
-.timer {
- font-weight: normal;
+.RenderCounter-lastRender,
+.RenderCounter-title {
+ white-space: nowrap;
+ color: rgb(153, 153, 153);
}
-.ProgressBar-container {
- width: 100%;
- display: block;
- position: relative;
- border: 1px solid var(--color-brand-muted);
- border-radius: 4px;
+@keyframes flash {
+ from {
+ border-color: var(--snippets_container-background-focused);
+ }
- height: 92px;
+ to {
+ border-color: var(--snippets_container-background-unfocused);
+ }
}
-.ProgressBar {
- position: absolute;
- top: 0;
- bottom: 0;
- right: 0;
- left: 0;
- width: 100%;
- height: 100%;
+.Button {
display: block;
- background-color: var(--color-brand);
- transform-origin: top left;
- border-radius: 4px;
- transform: scaleX(var(--progress-bar, 0%));
-}
-
-.Bundler-container {
- background-color: var(--snippets_container-background-focused);
- font-size: 64px;
- font-weight: bold;
- color: white;
- left: 0;
- right: 0;
- padding: 0.8em 0.8em;
-}
-
-.Bundler-updateRate {
- font-size: 0.8em;
- font-weight: normal;
- display: flex;
- color: var(--result__muted-color);
-}
-.interval:before {
- content: var(--interval, "16");
-}
-
-.highlight {
+ border: 1px solid rgb(20, 180, 0);
+ background-color: rgb(2, 150, 0);
color: white;
-}
-
-.timer:after {
- content: var(--timestamp);
- font-variant-numeric: tabular-nums;
- font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen,
- Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif;
- display: inline;
font-weight: 500;
- color: white;
- width: 100%;
-}
-
-.SectionLabel {
- font-weight: 300;
- font-family: var(--heading-font);
+ padding: 10px 12px;
+ border-radius: 4px;
+ text-transform: uppercase;
text-align: center;
- width: 100%;
- font-weight: 700;
- margin-top: 24px;
-}
-
-.FooterLabel {
- margin-top: 0;
- margin-bottom: 12px;
-}
-
-.Spinner-container {
- --spinner-muted: rgb(0, 255, 0);
- --spinner-primary: rgb(0, 60, 255);
-
- width: 96px;
- height: 96px;
- border-radius: 50%;
- background-color: var(--page-background);
- border-top: 1.1em solid var(--spinner-muted);
- border-right: 1.1em solid var(--spinner-muted);
- border-bottom: 1.1em solid var(--spinner-muted);
- border-left: 1.1em solid var(--spinner-primary);
-
- transform: rotate(var(--spinner-rotate, 12deg));
-}
-
-.Spinners {
- display: grid;
- grid-auto-flow: column;
- justify-content: space-between;
-
- width: 100%;
-}
-
-.Spinner-1.Spinner-container {
- --spinner-muted: var(--spinner-1-muted);
- --spinner-primary: var(--spinner-1-primary);
-}
-
-.Spinner-2.Spinner-container {
- --spinner-muted: var(--spinner-2-muted);
- --spinner-primary: var(--spinner-2-primary);
-}
-
-.Spinner-3.Spinner-container {
- --spinner-muted: var(--spinner-3-muted);
- --spinner-primary: var(--spinner-3-primary);
-}
-
-.Spinner-4.Spinner-container {
- --spinner-muted: var(--spinner-4-muted);
- --spinner-primary: var(--spinner-4-primary);
+ width: fit-content;
+ cursor: pointer;
}
diff --git a/demos/react-fast-refresh-test/src/index.tsx b/demos/react-fast-refresh-test/src/index.tsx
index 3db53a67f..348bd80f2 100644
--- a/demos/react-fast-refresh-test/src/index.tsx
+++ b/demos/react-fast-refresh-test/src/index.tsx
@@ -1,15 +1,10 @@
import ReactDOM from "react-dom";
import React from "react";
-import { Main } from "./main";
+import { App } from "./components/app";
import classNames from "classnames";
-const Base = ({}) => {
- const name = decodeURIComponent(location.search.substring(1));
- return <Main productName={name || "Bundler"} />;
-};
-
function startReact() {
- ReactDOM.render(<Base />, document.querySelector("#reactroot"));
+ ReactDOM.render(<App />, document.querySelector("#reactroot"));
}
globalThis.addEventListener("DOMContentLoaded", () => {
@@ -17,4 +12,4 @@ globalThis.addEventListener("DOMContentLoaded", () => {
});
startReact();
-export { Base };
+export { App };
diff --git a/src/api/schema.d.ts b/src/api/schema.d.ts
index 367932105..a15f7b878 100644
--- a/src/api/schema.d.ts
+++ b/src/api/schema.d.ts
@@ -119,6 +119,19 @@ type uint32 = number;
4: "debug",
debug: "debug"
}
+ export enum Reloader {
+ disable = 1,
+ live = 2,
+ fast_refresh = 3
+ }
+ export const ReloaderKeys = {
+ 1: "disable",
+ disable: "disable",
+ 2: "live",
+ live: "live",
+ 3: "fast_refresh",
+ fast_refresh: "fast_refresh"
+ }
export enum WebsocketMessageKind {
welcome = 1,
file_change_notification = 2,
@@ -301,6 +314,7 @@ type uint32 = number;
export interface WebsocketMessageWelcome {
epoch: uint32;
+ javascriptReloader: Reloader;
}
export interface WebsocketMessageFileChangeNotification {
@@ -341,6 +355,21 @@ type uint32 = number;
ids: Uint32Array;
}
+ export interface FileList {
+ ptrs: StringPointer[];
+ files: string;
+ }
+
+ export interface WebsocketMessageResolveIDs {
+ id: Uint32Array;
+ list: FileList;
+ }
+
+ export interface WebsocketCommandResolveIDs {
+ ptrs: StringPointer[];
+ files: string;
+ }
+
export interface WebsocketMessageManifestSuccess {
id: uint32;
module_path: string;
@@ -411,6 +440,12 @@ type uint32 = number;
export declare function decodeWebsocketMessageBuildFailure(buffer: ByteBuffer): WebsocketMessageBuildFailure;
export declare function encodeDependencyManifest(message: DependencyManifest, bb: ByteBuffer): void;
export declare function decodeDependencyManifest(buffer: ByteBuffer): DependencyManifest;
+ export declare function encodeFileList(message: FileList, bb: ByteBuffer): void;
+ export declare function decodeFileList(buffer: ByteBuffer): FileList;
+ export declare function encodeWebsocketMessageResolveIDs(message: WebsocketMessageResolveIDs, bb: ByteBuffer): void;
+ export declare function decodeWebsocketMessageResolveIDs(buffer: ByteBuffer): WebsocketMessageResolveIDs;
+ export declare function encodeWebsocketCommandResolveIDs(message: WebsocketCommandResolveIDs, bb: ByteBuffer): void;
+ export declare function decodeWebsocketCommandResolveIDs(buffer: ByteBuffer): WebsocketCommandResolveIDs;
export declare function encodeWebsocketMessageManifestSuccess(message: WebsocketMessageManifestSuccess, bb: ByteBuffer): void;
export declare function decodeWebsocketMessageManifestSuccess(buffer: ByteBuffer): WebsocketMessageManifestSuccess;
export declare function encodeWebsocketMessageManifestFailure(message: WebsocketMessageManifestFailure, bb: ByteBuffer): void;
diff --git a/src/api/schema.js b/src/api/schema.js
index f460064e5..c4341c1d4 100644
--- a/src/api/schema.js
+++ b/src/api/schema.js
@@ -1236,6 +1236,22 @@ function encodeLog(message, bb) {
}
}
+const Reloader = {
+ "1": 1,
+ "2": 2,
+ "3": 3,
+ "disable": 1,
+ "live": 2,
+ "fast_refresh": 3
+};
+const ReloaderKeys = {
+ "1": "disable",
+ "2": "live",
+ "3": "fast_refresh",
+ "disable": "disable",
+ "live": "live",
+ "fast_refresh": "fast_refresh"
+};
const WebsocketMessageKind = {
"1": 1,
"2": 2,
@@ -1309,6 +1325,7 @@ function decodeWebsocketMessageWelcome(bb) {
var result = {};
result["epoch"] = bb.readUint32();
+ result["javascriptReloader"] = Reloader[bb.readByte()];
return result;
}
@@ -1321,6 +1338,15 @@ function encodeWebsocketMessageWelcome(message, bb) {
throw new Error("Missing required field \"epoch\"");
}
+ var value = message["javascriptReloader"];
+ if (value != null) {
+ var encoded = Reloader[value];
+if (encoded === void 0) throw new Error("Invalid value " + JSON.stringify(value) + " for enum \"Reloader\"");
+bb.writeByte(encoded);
+ } else {
+ throw new Error("Missing required field \"javascriptReloader\"");
+ }
+
}
function decodeWebsocketMessageFileChangeNotification(bb) {
@@ -1537,6 +1563,98 @@ function encodeDependencyManifest(message, bb) {
}
+function decodeFileList(bb) {
+ var result = {};
+
+ var length = bb.readVarUint();
+ var values = result["ptrs"] = Array(length);
+ for (var i = 0; i < length; i++) values[i] = decodeStringPointer(bb);
+ result["files"] = bb.readString();
+ return result;
+}
+
+function encodeFileList(message, bb) {
+
+ var value = message["ptrs"];
+ if (value != null) {
+ var values = value, n = values.length;
+ bb.writeVarUint(n);
+ for (var i = 0; i < n; i++) {
+ value = values[i];
+ encodeStringPointer(value, bb);
+ }
+ } else {
+ throw new Error("Missing required field \"ptrs\"");
+ }
+
+ var value = message["files"];
+ if (value != null) {
+ bb.writeString(value);
+ } else {
+ throw new Error("Missing required field \"files\"");
+ }
+
+}
+
+function decodeWebsocketMessageResolveIDs(bb) {
+ var result = {};
+
+ result["id"] = bb.readUint32ByteArray();
+ result["list"] = decodeFileList(bb);
+ return result;
+}
+
+function encodeWebsocketMessageResolveIDs(message, bb) {
+
+ var value = message["id"];
+ if (value != null) {
+ bb.writeUint32ByteArray(value);
+ } else {
+ throw new Error("Missing required field \"id\"");
+ }
+
+ var value = message["list"];
+ if (value != null) {
+ encodeFileList(value, bb);
+ } else {
+ throw new Error("Missing required field \"list\"");
+ }
+
+}
+
+function decodeWebsocketCommandResolveIDs(bb) {
+ var result = {};
+
+ var length = bb.readVarUint();
+ var values = result["ptrs"] = Array(length);
+ for (var i = 0; i < length; i++) values[i] = decodeStringPointer(bb);
+ result["files"] = bb.readString();
+ return result;
+}
+
+function encodeWebsocketCommandResolveIDs(message, bb) {
+
+ var value = message["ptrs"];
+ if (value != null) {
+ var values = value, n = values.length;
+ bb.writeVarUint(n);
+ for (var i = 0; i < n; i++) {
+ value = values[i];
+ encodeStringPointer(value, bb);
+ }
+ } else {
+ throw new Error("Missing required field \"ptrs\"");
+ }
+
+ var value = message["files"];
+ if (value != null) {
+ bb.writeString(value);
+ } else {
+ throw new Error("Missing required field \"files\"");
+ }
+
+}
+
function decodeWebsocketMessageManifestSuccess(bb) {
var result = {};
@@ -1679,6 +1797,8 @@ export { decodeMessage }
export { encodeMessage }
export { decodeLog }
export { encodeLog }
+export { Reloader }
+export { ReloaderKeys }
export { WebsocketMessageKind }
export { WebsocketMessageKindKeys }
export { WebsocketCommandKind }
@@ -1701,6 +1821,12 @@ export { decodeWebsocketMessageBuildFailure }
export { encodeWebsocketMessageBuildFailure }
export { decodeDependencyManifest }
export { encodeDependencyManifest }
+export { decodeFileList }
+export { encodeFileList }
+export { decodeWebsocketMessageResolveIDs }
+export { encodeWebsocketMessageResolveIDs }
+export { decodeWebsocketCommandResolveIDs }
+export { encodeWebsocketCommandResolveIDs }
export { decodeWebsocketMessageManifestSuccess }
export { encodeWebsocketMessageManifestSuccess }
export { decodeWebsocketMessageManifestFailure }
diff --git a/src/api/schema.peechy b/src/api/schema.peechy
index 477ce5d0b..74050e90f 100644
--- a/src/api/schema.peechy
+++ b/src/api/schema.peechy
@@ -235,9 +235,17 @@ struct Log {
}
+smol Reloader {
+ disable = 1;
+ // equivalent of CMD + R
+ live = 2;
+ // React Fast Refresh
+ fast_refresh = 3;
+}
+
// The WebSocket protocol
// Server: "hey, this file changed. Does anyone want it?"
-// Client: *checks hash table* "uhh yeah, ok. rebuild that for me"
+// Browser: *checks array* "uhh yeah, ok. rebuild that for me"
// Server: "here u go"
// This makes the client responsible for tracking which files it needs to listen for.
// From a server perspective, this means the filesystem watching thread can send the same WebSocket message
@@ -267,6 +275,7 @@ struct WebsocketMessage {
// This is the first.
struct WebsocketMessageWelcome {
uint32 epoch;
+ Reloader javascriptReloader;
}
struct WebsocketMessageFileChangeNotification {
diff --git a/src/api/schema.zig b/src/api/schema.zig
index 00460fcc6..8227f5e88 100644
--- a/src/api/schema.zig
+++ b/src/api/schema.zig
@@ -1271,6 +1271,24 @@ pub const Api = struct {
}
};
+ pub const Reloader = enum(u8) {
+ _none,
+ /// disable
+ disable,
+
+ /// live
+ live,
+
+ /// fast_refresh
+ fast_refresh,
+
+ _,
+
+ pub fn jsonStringify(self: *const @This(), opts: anytype, o: anytype) !void {
+ return try std.json.stringify(@tagName(self), opts, o);
+ }
+ };
+
pub const WebsocketMessageKind = enum(u8) {
_none,
/// welcome
@@ -1334,19 +1352,24 @@ pub const Api = struct {
}
};
- pub const WebsocketMessageWelcome = packed struct {
+ pub const WebsocketMessageWelcome = struct {
/// epoch
epoch: u32 = 0,
+ /// javascriptReloader
+ javascript_reloader: Reloader,
+
pub fn decode(reader: anytype) anyerror!WebsocketMessageWelcome {
var this = std.mem.zeroes(WebsocketMessageWelcome);
this.epoch = try reader.readValue(u32);
+ this.javascript_reloader = try reader.readValue(Reloader);
return this;
}
pub fn encode(this: *const @This(), writer: anytype) anyerror!void {
try writer.writeInt(this.epoch);
+ try writer.writeEnum(this.javascript_reloader);
}
};
@@ -1512,6 +1535,69 @@ pub const Api = struct {
}
};
+ pub const FileList = struct {
+ /// ptrs
+ ptrs: []const StringPointer,
+
+ /// files
+ files: []const u8,
+
+ pub fn decode(reader: anytype) anyerror!FileList {
+ var this = std.mem.zeroes(FileList);
+
+ this.ptrs = try reader.readArray(StringPointer);
+ this.files = try reader.readValue([]const u8);
+ return this;
+ }
+
+ pub fn encode(this: *const @This(), writer: anytype) anyerror!void {
+ try writer.writeArray(StringPointer, this.ptrs);
+ try writer.writeValue(this.files);
+ }
+ };
+
+ pub const WebsocketMessageResolveIDs = struct {
+ /// id
+ id: []const u32,
+
+ /// list
+ list: FileList,
+
+ pub fn decode(reader: anytype) anyerror!WebsocketMessageResolveIDs {
+ var this = std.mem.zeroes(WebsocketMessageResolveIDs);
+
+ this.id = try reader.readArray(u32);
+ this.list = try reader.readValue(FileList);
+ return this;
+ }
+
+ pub fn encode(this: *const @This(), writer: anytype) anyerror!void {
+ try writer.writeArray(u32, this.id);
+ try writer.writeValue(this.list);
+ }
+ };
+
+ pub const WebsocketCommandResolveIDs = struct {
+ /// ptrs
+ ptrs: []const StringPointer,
+
+ /// files
+ files: []const u8,
+
+ pub fn decode(reader: anytype) anyerror!WebsocketCommandResolveIDs {
+ var this = std.mem.zeroes(WebsocketCommandResolveIDs);
+
+ this.ptrs = try reader.readArray(StringPointer);
+ this.files = try reader.readValue([]const u8);
+ return this;
+ }
+
+ pub fn encode(this: *const @This(), writer: anytype) anyerror!void {
+ try writer.writeArray(StringPointer, this.ptrs);
+ try writer.writeValue(this.files);
+ }
+ };
+
pub const WebsocketMessageManifestSuccess = struct {
/// id
id: u32 = 0,
diff --git a/src/http.zig b/src/http.zig
index 269274b56..665f7e978 100644
--- a/src/http.zig
+++ b/src/http.zig
@@ -757,8 +757,20 @@ pub const RequestContext = struct {
var writer = ByteApiWriter.init(&fbs);
try msg.encode(&writer);
+ var reloader = Api.Reloader.disable;
+ if (ctx.bundler.options.hot_module_reloading) {
+ reloader = Api.Reloader.live;
+ if (ctx.bundler.options.jsx.supports_fast_refresh) {
+ if (ctx.bundler.options.node_modules_bundle) |bundle| {
+ if (bundle.hasFastRefresh()) {
+ reloader = Api.Reloader.fast_refresh;
+ }
+ }
+ }
+ }
const welcome_message = Api.WebsocketMessageWelcome{
.epoch = WebsocketHandler.toTimestamp(handler.ctx.timer.start_time),
+ .javascript_reloader = reloader,
};
try welcome_message.encode(&writer);
if ((try handler.websocket.writeBinary(fbs.getWritten())) == 0) {
diff --git a/src/js_ast.zig b/src/js_ast.zig
index 9df3e36f0..4eb2450e3 100644
--- a/src/js_ast.zig
+++ b/src/js_ast.zig
@@ -758,6 +758,21 @@ pub const Symbol = struct {
pub fn isKindFunction(kind: Symbol.Kind) bool {
return kind == Symbol.Kind.hoisted_function or kind == Symbol.Kind.generator_or_async_function;
}
+
+ pub fn isReactComponentishName(symbol: *const Symbol) bool {
+ switch (symbol.kind) {
+ .hoisted, .hoisted_function, .cconst, .class, .other => {
+ return switch (symbol.original_name[0]) {
+ 'A'...'Z' => true,
+ else => false,
+ };
+ },
+
+ else => {
+ return false;
+ },
+ }
+ }
};
pub const OptionalChain = packed enum(u2) {
diff --git a/src/js_parser/js_parser.zig b/src/js_parser/js_parser.zig
index 76ffef336..f8b565fe7 100644
--- a/src/js_parser/js_parser.zig
+++ b/src/js_parser/js_parser.zig
@@ -1677,12 +1677,27 @@ pub const Parser = struct {
pub fn parse(self: *Parser) !js_ast.Result {
if (self.options.ts and self.options.jsx.parse) {
+ if (self.options.features.react_fast_refresh) {
+ return try self._parse(TSXParserFastRefresh);
+ }
return try self._parse(TSXParser);
} else if (self.options.ts) {
+ if (self.options.features.react_fast_refresh) {
+ return try self._parse(TypeScriptParserFastRefresh);
+ }
+
return try self._parse(TypeScriptParser);
} else if (self.options.jsx.parse) {
+ if (self.options.features.react_fast_refresh) {
+ return try self._parse(JSXParserFastRefresh);
+ }
+
return try self._parse(JSXParser);
} else {
+ if (self.options.features.react_fast_refresh) {
+ return try self._parse(JavaScriptParserFastRefresh);
+ }
+
return try self._parse(JavaScriptParser);
}
}
@@ -2194,11 +2209,73 @@ pub const ImportOrRequireScanResults = struct {
import_records: List(ImportRecord),
};
+const ParserFeatures = struct {
+ typescript: bool = false,
+ jsx: bool = false,
+ scan_only: bool = false,
+
+ // *** How React Fast Refresh works ***
+ //
+ // Implmenetations:
+ // [0]: https://github.com/facebook/react/blob/master/packages/react-refresh/src/ReactFreshBabelPlugin.js
+ // [1]: https://github.com/swc-project/swc/blob/master/ecmascript/transforms/react/src/refresh/mod.rs
+ //
+ // Additional reading:
+ // - https://github.com/facebook/react/issues/16604#issuecomment-528663101
+ // - https://github.com/facebook/react/blob/master/packages/react-refresh/src/__tests__/ReactFreshIntegration-test.js
+ //
+ // From reading[0] and Dan Abramov's comment, there are really five parts.
+ // 1. At the top of the file:
+ // 1. Declare a $RefreshReg$ if it doesn't exist
+ // - This really just does "RefreshRuntime.register(ComponentIdentifier, ComponentIdentifier.name);"
+ // 2. Run "var _s${componentIndex} = $RefreshSig$()" to generate a function for updating react refresh scoped to the component. So it's one per *component*.
+ // - This really just does "RefreshRuntime.createSignatureFunctionForTransform();"
+ // 2. Register all React components[2] defined in the module scope by calling the equivalent of $RefreshReg$(ComponentIdentifier, "ComponentName")
+ // 3. For each registered component:
+ // 1. Call "_s()" to mark the first render of this component for "react-refresh/runtime". Call this at the start of the React component's function body
+ // 2. Track every call expression to a hook[3] inside the component, including:
+ // - Identifier of the hook function
+ // - Arguments passed
+ // 3. For each hook's call expression, generate a signature key which is
+ // - The hook's identifier ref
+ // - The S.Decl ("VariableDeclarator")'s source
+ // "var [foo, bar] = useFooBar();"
+ // ^--------^ This region, I think. Judging from this line: https://github.com/facebook/react/blob/master/packages/react-refresh/src/ReactFreshBabelPlugin.js#L407
+ // - For the "useState" hook, also hash the source of the first argument if it exists e.g. useState(foo => true);
+ // - For the "useReducer" hook, also hash the source of the second argument if it exists e.g. useReducer({}, () => ({}));
+ // 4. If the hook component is not builtin and is defined inside a component, always reset the component state
+ // - See this test: https://github.com/facebook/react/blob/568dc3532e25b30eee5072de08503b1bbc4f065d/packages/react-refresh/src/__tests__/ReactFreshIntegration-test.js#L909
+ // 4. From the signature key generated in 3., call one of the following:
+ // - _s(ComponentIdentifier, hash(signature));
+ // - _s(ComponentIdentifier, hash(signature), true /* forceReset */);
+ // - _s(ComponentIdentifier, hash(signature), false /* forceReset */, () => [customHook1, customHook2, customHook3]);
+ // Note: This step is only strictly required on rebuild.
+ // 5. if (isReactComponentBoundary(exports)) enqueueUpdateAndHandleErrors();
+ // **** FAQ ****
+ // [2]: Q: From a parser's perspective, what's a component?
+ // A: typeof name === 'string' && name[0] >= 'A' && name[0] <= 'Z -- https://github.com/facebook/react/blob/568dc3532e25b30eee5072de08503b1bbc4f065d/packages/react-refresh/src/ReactFreshBabelPlugin.js#L42-L44
+ // [3]: Q: From a parser's perspective, what's a hook?
+ // A: /^use[A-Z]/ -- https://github.com/facebook/react/blob/568dc3532e25b30eee5072de08503b1bbc4f065d/packages/react-refresh/src/ReactFreshBabelPlugin.js#L390
+ //
+ //
+ //
+
+ react_fast_refresh: bool = false,
+};
+
+// Our implementation diverges somewhat from the official implementation
+// Specifically, we use a subclass of HMRModule - FastRefreshModule
+// Instead of creating a globally-scoped
+const FastRefresh = struct {};
+
pub fn NewParser(
- comptime is_typescript_enabled: bool,
- comptime is_jsx_enabled: bool,
- comptime only_scan_imports_and_do_not_visit: bool,
+ comptime js_parser_features: ParserFeatures,
) type {
+ const is_typescript_enabled = js_parser_features.typescript;
+ const is_jsx_enabled = js_parser_features.jsx;
+ const only_scan_imports_and_do_not_visit = js_parser_features.scan_only;
+ const is_react_fast_refresh_enabled = js_parser_features.react_fast_refresh;
+
const ImportRecordList = if (only_scan_imports_and_do_not_visit) *std.ArrayList(ImportRecord) else std.ArrayList(ImportRecord);
const NamedImportsType = if (only_scan_imports_and_do_not_visit) *js_ast.Ast.NamedImports else js_ast.Ast.NamedImports;
const NeedsJSXType = if (only_scan_imports_and_do_not_visit) bool else void;
@@ -13905,15 +13982,20 @@ pub fn NewParser(
// Range (min … max): 24.1 ms … 39.7 ms 500 runs
// '../../build/macos-x86_64/esdev node_modules/react-dom/cjs/react-dom.development.js --resolve=disable' ran
// 1.02 ± 0.07 times faster than '../../esdev.before-comptime-js-parser node_modules/react-dom/cjs/react-dom.development.js --resolve=disable'
-const JavaScriptParser = NewParser(false, false, false);
-const JSXParser = NewParser(false, true, false);
-const TSXParser = NewParser(true, true, false);
-const TypeScriptParser = NewParser(true, false, false);
-
-const JavaScriptImportScanner = NewParser(false, false, true);
-const JSXImportScanner = NewParser(false, true, true);
-const TSXImportScanner = NewParser(true, true, true);
-const TypeScriptImportScanner = NewParser(true, false, true);
+const JavaScriptParser = NewParser(.{});
+const JSXParser = NewParser(.{ .jsx = true });
+const TSXParser = NewParser(.{ .jsx = true, .typescript = true });
+const TypeScriptParser = NewParser(.{ .typescript = true });
+
+const JavaScriptParserFastRefresh = NewParser(.{ .react_fast_refresh = true });
+const JSXParserFastRefresh = NewParser(.{ .jsx = true, .react_fast_refresh = true });
+const TSXParserFastRefresh = NewParser(.{ .jsx = true, .typescript = true, .react_fast_refresh = true });
+const TypeScriptParserFastRefresh = NewParser(.{ .typescript = true, .react_fast_refresh = true });
+
+const JavaScriptImportScanner = NewParser(.{ .scan_only = true });
+const JSXImportScanner = NewParser(.{ .jsx = true, .scan_only = true });
+const TSXImportScanner = NewParser(.{ .jsx = true, .typescript = true, .scan_only = true });
+const TypeScriptImportScanner = NewParser(.{ .typescript = true, .scan_only = true });
// The "await" and "yield" expressions are never allowed in argument lists but
// may or may not be allowed otherwise depending on the details of the enclosing
diff --git a/src/node_module_bundle.zig b/src/node_module_bundle.zig
index 8642336e3..4605df2ef 100644
--- a/src/node_module_bundle.zig
+++ b/src/node_module_bundle.zig
@@ -36,6 +36,11 @@ pub const NodeModuleBundle = struct {
pub const magic_bytes = "#!/usr/bin/env speedy\n\n";
threadlocal var jsbundle_prefix: [magic_bytes.len + 5]u8 = undefined;
+ // TODO: support preact-refresh, others by not hard coding
+ pub fn hasFastRefresh(this: *const NodeModuleBundle) bool {
+ return this.package_name_map.contains("react-refresh");
+ }
+
pub fn loadPackageMap(this: *NodeModuleBundle) !void {
this.package_name_map = PackageNameMap.init(this.allocator);
var ids = PackageIDMap.init(this.allocator);
diff --git a/src/runtime.footer.js b/src/runtime.footer.js
new file mode 100644
index 000000000..94a358eb5
--- /dev/null
+++ b/src/runtime.footer.js
@@ -0,0 +1,14 @@
+// ---
+// Public exports from runtime
+export var $$m = SPEEDY_RUNTIME.$$m;
+export var __HMRModule = SPEEDY_RUNTIME.__HMRModule;
+export var __FastRefreshModule = SPEEDY_RUNTIME.__FastRefreshModule;
+export var __HMRClient = SPEEDY_RUNTIME.__HMRClient;
+export var __markAsModule = SPEEDY_RUNTIME.__markAsModule;
+export var $$lzy = SPEEDY_RUNTIME.$$lzy;
+export var __toModule = SPEEDY_RUNTIME.__toModule;
+export var __commonJS = SPEEDY_RUNTIME.__commonJS;
+export var __require = SPEEDY_RUNTIME.__require;
+export var __name = SPEEDY_RUNTIME.__name;
+export var __export = SPEEDY_RUNTIME.__export;
+export var __reExport = SPEEDY_RUNTIME.__reExport;
diff --git a/src/runtime.version b/src/runtime.version
index b9f5adbff..38984f56a 100644
--- a/src/runtime.version
+++ b/src/runtime.version
@@ -1 +1 @@
-b735428795c2ee2d \ No newline at end of file
+2a14be7fb8890d70 \ No newline at end of file
diff --git a/src/runtime.zig b/src/runtime.zig
index 4fd1354c9..39518e58c 100644
--- a/src/runtime.zig
+++ b/src/runtime.zig
@@ -31,6 +31,7 @@ pub const Runtime = struct {
pub const ActivateFunction = "activate";
};
+ // If you change this, remember to update "runtime.footer.js" and rebuild the runtime.js
pub const Imports = struct {
__name: ?Ref = null,
__toModule: ?Ref = null,
diff --git a/src/runtime/hmr.ts b/src/runtime/hmr.ts
index cef229de2..7260f28a0 100644
--- a/src/runtime/hmr.ts
+++ b/src/runtime/hmr.ts
@@ -1,858 +1,1140 @@
-import { ByteBuffer } from "peechy/bb";
+import { ByteBuffer } from "peechy";
import * as API from "../api/schema";
-var runOnce = false;
-var clientStartTime = 0;
+var __HMRModule, __FastRefreshModule, __HMRClient;
+// We add a scope here to minimize chances of namespace collisions
+{
+ var runOnce = false;
+ var clientStartTime = 0;
-function formatDuration(duration: number) {
- return Math.round(duration * 1000) / 1000;
-}
-
-type HTMLStylableElement = HTMLLinkElement | HTMLStyleElement;
-type CSSHMRInsertionPoint = {
- id: number;
- node?: HTMLStylableElement;
- file: string;
- bundle_id: number;
- sheet: CSSStyleSheet;
-};
-
-enum CSSUpdateMethod {
- // CSS OM allows synchronous style updates
- cssObjectModel,
- // Blob URLs allow us to skip converting to JavaScript strings
- // However, they run asynchronously. Frequent updates cause FOUC
- blobURL,
-}
+ function formatDuration(duration: number) {
+ return Math.round(duration * 1000) / 1000;
+ }
-// How this works
-// We keep
-export class CSSLoader {
- hmr: HMRClient;
- private static cssLoadId: CSSHMRInsertionPoint = {
- id: 0,
- bundle_id: 0,
- node: null,
- file: "",
- sheet: null,
+ type HTMLStylableElement = HTMLLinkElement | HTMLStyleElement;
+ type CSSHMRInsertionPoint = {
+ id: number;
+ node?: HTMLStylableElement;
+ file: string;
+ bundle_id: number;
+ sheet: CSSStyleSheet;
};
- updateMethod: CSSUpdateMethod;
- decoder: TextDecoder;
-
- constructor() {
- if ("replaceSync" in CSSStyleSheet.prototype) {
- this.updateMethod = CSSUpdateMethod.cssObjectModel;
- } else {
- this.updateMethod = CSSUpdateMethod.blobURL;
- }
+ enum CSSUpdateMethod {
+ // CSS OM allows synchronous style updates
+ cssObjectModel,
+ // Blob URLs allow us to skip converting to JavaScript strings
+ // However, they run asynchronously. Frequent updates cause FOUC
+ blobURL,
}
- // 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;
- }
-
- const startIndex = "hmr-wid:".length + 1;
- const endIDRegion = rule.conditionText.indexOf(")", startIndex);
- if (endIDRegion === -1) return null;
+ enum ReloadBehavior {
+ fullReload,
+ hotReload,
+ ignore,
+ }
- const int = parseInt(
- rule.conditionText.substring(startIndex, endIDRegion),
- 10
- );
+ const FastRefreshLoader = {
+ RefreshRuntime: null,
+ isUpdateInProgress: false,
+ hasInjectedFastRefresh: false,
- if (int !== id) {
- return null;
- }
+ performFullRefresh() {
+ HMRClient.client.performFullReload();
+ },
- 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 HTMLStylableElement;
- CSSLoader.cssLoadId.sheet = sheet;
- CSSLoader.cssLoadId.file = rule.conditionText.substring(
- startFileRegion - 1,
- endFileRegion
- );
+ async hotReload() {
+ if (FastRefreshLoader.isUpdateInProgress) return;
- return CSSLoader.cssLoadId;
+ try {
+ FastRefreshLoader.isUpdateInProgress = true;
+ } finally {
+ FastRefreshLoader.isUpdateInProgress = false;
}
- default: {
- return null;
+ },
+ };
+
+ class CSSLoader {
+ hmr: HMRClient;
+ private static cssLoadId: CSSHMRInsertionPoint = {
+ id: 0,
+ bundle_id: 0,
+ node: null,
+ file: "",
+ sheet: null,
+ };
+
+ updateMethod: CSSUpdateMethod;
+ decoder: TextDecoder;
+
+ constructor() {
+ if ("replaceSync" in CSSStyleSheet.prototype) {
+ this.updateMethod = CSSUpdateMethod.cssObjectModel;
+ } else {
+ this.updateMethod = CSSUpdateMethod.blobURL;
}
}
- }
-
- bundleId(): number {
- return CSSLoader.cssLoadId.bundle_id;
- }
-
- private findCSSLinkTag(id: number): CSSHMRInsertionPoint | null {
- let count = 0;
- let match: CSSHMRInsertionPoint = null;
- if (this.updateMethod === CSSUpdateMethod.cssObjectModel) {
- if (document.adoptedStyleSheets.length > 0) {
- count = document.adoptedStyleSheets.length;
-
- 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.adoptedStyleSheets[i];
- cssRules = sheet.rules;
- ruleCount = sheet.rules.length;
- } catch (exception) {
- continue;
+ // 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;
}
- if (sheet.disabled || sheet.rules.length === 0) {
- continue;
- }
+ const startIndex = "hmr-wid:".length + 1;
+ const endIDRegion = rule.conditionText.indexOf(")", startIndex);
+ if (endIDRegion === -1) return null;
- const bundleIdRule = cssRules[0] as CSSSupportsRule;
- if (
- bundleIdRule.type !== 12 ||
- !bundleIdRule.conditionText.startsWith("(hmr-bid:")
- ) {
- continue;
+ const int = parseInt(
+ rule.conditionText.substring(startIndex, endIDRegion),
+ 10
+ );
+
+ if (int !== id) {
+ return null;
}
- const bundleIdEnd = bundleIdRule.conditionText.indexOf(
- ")",
- "(hmr-bid:".length + 1
+ let startFileRegion = rule.conditionText.indexOf(
+ '(hmr-file:"',
+ endIDRegion
);
- if (bundleIdEnd === -1) continue;
+ if (startFileRegion === -1) return null;
+ startFileRegion += '(hmr-file:"'.length + 1;
- CSSLoader.cssLoadId.bundle_id = parseInt(
- bundleIdRule.conditionText.substring(
- "(hmr-bid:".length,
- bundleIdEnd
- ),
- 10
+ 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 HTMLStylableElement;
+ CSSLoader.cssLoadId.sheet = sheet;
+ CSSLoader.cssLoadId.file = rule.conditionText.substring(
+ startFileRegion - 1,
+ endFileRegion
);
- for (let j = 1; j < ruleCount && match === null; j++) {
- match = this.findMatchingSupportsRule(
- cssRules[j] as CSSSupportsRule,
- id,
- sheet
- );
- }
+ return CSSLoader.cssLoadId;
+ }
+ default: {
+ return null;
}
}
}
- count = document.styleSheets.length;
+ bundleId(): number {
+ return CSSLoader.cssLoadId.bundle_id;
+ }
- 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;
- }
+ private findCSSLinkTag(id: number): CSSHMRInsertionPoint | null {
+ let count = 0;
+ let match: CSSHMRInsertionPoint = null;
+
+ if (this.updateMethod === CSSUpdateMethod.cssObjectModel) {
+ if (document.adoptedStyleSheets.length > 0) {
+ count = document.adoptedStyleSheets.length;
+
+ 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.adoptedStyleSheets[i];
+ cssRules = sheet.rules;
+ ruleCount = sheet.rules.length;
+ } catch (exception) {
+ continue;
+ }
+
+ if (sheet.disabled || 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
+ );
+ if (bundleIdEnd === -1) continue;
+
+ CSSLoader.cssLoadId.bundle_id = parseInt(
+ bundleIdRule.conditionText.substring(
+ "(hmr-bid:".length,
+ bundleIdEnd
+ ),
+ 10
+ );
- if (sheet.disabled || sheet.rules.length === 0) {
- continue;
+ for (let j = 1; j < ruleCount && match === null; j++) {
+ match = this.findMatchingSupportsRule(
+ cssRules[j] as CSSSupportsRule,
+ id,
+ sheet
+ );
+ }
+ }
+ }
}
- const bundleIdRule = cssRules[0] as CSSSupportsRule;
- if (
- bundleIdRule.type !== 12 ||
- !bundleIdRule.conditionText.startsWith("(hmr-bid:")
- ) {
- continue;
- }
+ count = document.styleSheets.length;
+
+ 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;
+ }
- const bundleIdEnd = bundleIdRule.conditionText.indexOf(
- ")",
- "(hmr-bid:".length + 1
- );
- if (bundleIdEnd === -1) continue;
+ if (sheet.disabled || sheet.rules.length === 0) {
+ continue;
+ }
- CSSLoader.cssLoadId.bundle_id = parseInt(
- bundleIdRule.conditionText.substring("(hmr-bid:".length, bundleIdEnd),
- 10
- );
+ const bundleIdRule = cssRules[0] as CSSSupportsRule;
+ if (
+ bundleIdRule.type !== 12 ||
+ !bundleIdRule.conditionText.startsWith("(hmr-bid:")
+ ) {
+ continue;
+ }
- for (let j = 1; j < ruleCount && match === null; j++) {
- match = this.findMatchingSupportsRule(
- cssRules[j] as CSSSupportsRule,
- id,
- sheet
+ const bundleIdEnd = bundleIdRule.conditionText.indexOf(
+ ")",
+ "(hmr-bid:".length + 1
);
- }
- }
+ if (bundleIdEnd === -1) continue;
- // 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;
- CSSLoader.cssLoadId.sheet = null;
- }
+ CSSLoader.cssLoadId.bundle_id = parseInt(
+ bundleIdRule.conditionText.substring("(hmr-bid:".length, bundleIdEnd),
+ 10
+ );
- return match;
- }
+ for (let j = 1; j < ruleCount && match === null; j++) {
+ match = this.findMatchingSupportsRule(
+ cssRules[j] as CSSSupportsRule,
+ id,
+ sheet
+ );
+ }
+ }
- handleBuildSuccess(
- buffer: ByteBuffer,
- build: API.WebsocketMessageBuildSuccess,
- timestamp: number
- ) {
- const start = performance.now();
- var update = this.findCSSLinkTag(build.id);
- let bytes =
- buffer._data.length > buffer._index
- ? buffer._data.subarray(buffer._index)
- : new Uint8Array(0);
- if (update === null) {
- __hmrlog.debug("Skipping unused CSS.");
- 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;
+ CSSLoader.cssLoadId.sheet = null;
+ }
- if (bytes.length === 0) {
- __hmrlog.debug("Skipping empty file");
- return;
+ return match;
}
- let filepath = update.file;
- const _timestamp = timestamp;
- const from_timestamp = build.from_timestamp;
- function onLoadHandler() {
- const localDuration = formatDuration(performance.now() - start);
- const fsDuration = _timestamp - from_timestamp;
- __hmrlog.log(
- "Reloaded in",
- `${localDuration + fsDuration}ms`,
- "-",
- filepath
- );
+ handleBuildSuccess(
+ buffer: ByteBuffer,
+ build: API.WebsocketMessageBuildSuccess,
+ timestamp: number
+ ) {
+ const start = performance.now();
+ var update = this.findCSSLinkTag(build.id);
+ let bytes =
+ buffer.data.length > buffer.index
+ ? buffer.data.subarray(buffer.index)
+ : new Uint8Array(0);
+ if (update === null) {
+ __hmrlog.debug("Skipping unused CSS.");
+ return;
+ }
- update = null;
- filepath = null;
- }
+ if (bytes.length === 0) {
+ __hmrlog.debug("Skipping empty file");
+ return;
+ }
- // Whenever
- switch (this.updateMethod) {
- case CSSUpdateMethod.blobURL: {
- let blob = new Blob([bytes], { type: "text/css" });
-
- const blobURL = URL.createObjectURL(blob);
- // 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);
- update.node.setAttribute("href", blobURL);
- blob = null;
- URL.revokeObjectURL(blobURL);
- break;
+ let filepath = update.file;
+ const _timestamp = timestamp;
+ const from_timestamp = build.from_timestamp;
+ function onLoadHandler() {
+ const localDuration = formatDuration(performance.now() - start);
+ const fsDuration = _timestamp - from_timestamp;
+ __hmrlog.log(
+ "Reloaded in",
+ `${localDuration + fsDuration}ms`,
+ "-",
+ filepath
+ );
+
+ update = null;
+ filepath = null;
}
- case CSSUpdateMethod.cssObjectModel: {
- if (!this.decoder) {
- this.decoder = new TextDecoder("UTF8");
+
+ // Whenever
+ switch (this.updateMethod) {
+ case CSSUpdateMethod.blobURL: {
+ let blob = new Blob([bytes], { type: "text/css" });
+
+ const blobURL = URL.createObjectURL(blob);
+ // 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);
+ update.node.setAttribute("href", blobURL);
+ blob = null;
+ URL.revokeObjectURL(blobURL);
+ break;
}
+ case CSSUpdateMethod.cssObjectModel: {
+ if (!this.decoder) {
+ this.decoder = new TextDecoder("UTF8");
+ }
- // This is an adoptedStyleSheet, call replaceSync and be done with it.
- if (!update.node || update.node.tagName === "HTML") {
- update.sheet.replaceSync(this.decoder.decode(bytes));
- } else if (
- update.node.tagName === "LINK" ||
- update.node.tagName === "STYLE"
- ) {
- // This might cause CSS specifity issues....
- // I'm not 100% sure this is a safe operation
- const sheet = new CSSStyleSheet();
- sheet.replaceSync(this.decoder.decode(bytes));
- update.node.remove();
- document.adoptedStyleSheets = [...document.adoptedStyleSheets, sheet];
+ // This is an adoptedStyleSheet, call replaceSync and be done with it.
+ if (!update.node || update.node.tagName === "HTML") {
+ update.sheet.replaceSync(this.decoder.decode(bytes));
+ } else if (
+ update.node.tagName === "LINK" ||
+ update.node.tagName === "STYLE"
+ ) {
+ // This might cause CSS specifity issues....
+ // I'm not 100% sure this is a safe operation
+ const sheet = new CSSStyleSheet();
+ sheet.replaceSync(this.decoder.decode(bytes));
+ update.node.remove();
+ document.adoptedStyleSheets = [
+ ...document.adoptedStyleSheets,
+ sheet,
+ ];
+ }
+ break;
}
- break;
}
+
+ buffer = null;
+ bytes = null;
}
- buffer = null;
- bytes = null;
- }
+ 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;
+ }
- 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());
+ return tag.file;
+ }
}
- 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);
+ class HMRClient {
+ static client: HMRClient;
+ socket: WebSocket;
+ hasWelcomed: boolean = false;
+ reconnect: number = 0;
+ // Server timestamps are relative to the time the server's HTTP server launched
+ // This so we can send timestamps as uint32 instead of 128-bit integers
+ epoch: number = 0;
+ javascriptReloader: API.Reloader = API.Reloader.disable;
+ loaders = {
+ css: new CSSLoader(),
+ };
+ start() {
+ if (runOnce) {
+ __hmrlog.warn(
+ "Attempted to start HMR client multiple times. This may be a bug."
+ );
+ return;
+ }
+
+ this.loaders.css.hmr = this;
+ runOnce = true;
+ this.connect();
- if (!tag) {
- return null;
+ // Explicitly send a socket close event so the thread doesn't have to wait for a timeout
+ var origUnload = globalThis.onunload;
+ globalThis.onunload = (ev: Event) => {
+ if (this.socket && this.socket.readyState === this.socket.OPEN) {
+ this.socket.close(0, "unload");
+ }
+ origUnload && origUnload.call(globalThis, [ev]);
+ };
}
- return tag.file;
- }
-}
+ connect() {
+ clientStartTime = performance.now();
-class HMRClient {
- static client: HMRClient;
- socket: WebSocket;
- hasWelcomed: boolean = false;
- reconnect: number = 0;
- // Server timestamps are relative to the time the server's HTTP server launched
- // This so we can send timestamps as uint32 instead of 128-bit integers
- epoch: number = 0;
-
- loaders = {
- css: new CSSLoader(),
- };
- start() {
- if (runOnce) {
- __hmrlog.warn(
- "Attempted to start HMR client multiple times. This may be a bug."
- );
- return;
- }
+ if (this.reconnect) {
+ globalThis.clearInterval(this.reconnect);
+ this.reconnect = 0;
+ }
- this.loaders.css.hmr = this;
- runOnce = true;
- this.connect();
- }
+ const baseURL = new URL(location.origin + "/_api");
+ baseURL.protocol = location.protocol === "https" ? "wss" : "ws";
+ this.socket = new WebSocket(baseURL.toString(), ["speedy-hmr"]);
+ this.socket.binaryType = "arraybuffer";
+ this.socket.onclose = this.handleClose;
+ this.socket.onerror = this.handleError;
+ this.socket.onopen = this.handleOpen;
+ this.socket.onmessage = this.handleMessage;
+ }
- connect() {
- clientStartTime = performance.now();
- const baseURL = new URL(location.origin + "/_api");
- baseURL.protocol = location.protocol === "https" ? "wss" : "ws";
- this.socket = new WebSocket(baseURL.toString(), ["speedy-hmr"]);
- this.socket.binaryType = "arraybuffer";
- this.socket.onclose = this.handleClose;
- this.socket.onerror = this.handleError;
- this.socket.onopen = this.handleOpen;
- this.socket.onmessage = this.handleMessage;
- }
+ // key: module id
+ // value: server-timestamp
+ builds = new Map<number, number>();
- // key: module id
- // value: server-timestamp
- builds = new Map<number, number>();
+ indexOfModuleId(id: number): number {
+ return HMRModule.dependencies.graph.indexOf(id);
+ }
- indexOfModuleId(id: number): number {
- return HMRModule.dependencies.graph.indexOf(id);
- }
+ static activate(verbose: boolean = false) {
+ // Support browser-like envirnments where location and WebSocket exist
+ // Maybe it'll work in Deno! Who knows.
+ if (
+ this.client ||
+ typeof location === "undefined" ||
+ typeof WebSocket === "undefined"
+ ) {
+ return;
+ }
- static activate(verbose: boolean = false) {
- // Support browser-like envirnments where location and WebSocket exist
- // Maybe it'll work in Deno! Who knows.
- if (
- this.client ||
- typeof location === "undefined" ||
- typeof WebSocket === "undefined"
- ) {
- return;
+ this.client = new HMRClient();
+ this.client.verbose = verbose;
+ this.client.start();
+ globalThis["SPEEDY_HMR"] = this.client;
}
- this.client = new HMRClient();
- this.client.verbose = verbose;
- this.client.start();
- globalThis["SPEEDY_HMR"] = this.client;
- }
+ handleBuildFailure(buffer: ByteBuffer, timestamp: number) {
+ const build = API.decodeWebsocketMessageBuildFailure(buffer);
+ const id = build.id;
- handleBuildFailure(buffer: ByteBuffer, timestamp: number) {
- const build = API.decodeWebsocketMessageBuildFailure(buffer);
- const id = build.id;
+ const index = this.indexOfModuleId(id);
+ // Ignore build failures of modules that are not loaded
+ if (index === -1) {
+ return;
+ }
- const index = this.indexOfModuleId(id);
- // Ignore build failures of modules that are not loaded
- if (index === -1) {
- return;
+ // Build failed for a module we didn't request?
+ const minTimestamp = this.builds.get(index);
+ if (!minTimestamp) {
+ return;
+ }
+ const fail = API.decodeWebsocketMessageBuildFailure(buffer);
+ // TODO: finish this.
+ __hmrlog.error("Build failed", fail.module_path);
}
- // Build failed for a module we didn't request?
- const minTimestamp = this.builds.get(index);
- if (!minTimestamp) {
- return;
- }
- const fail = API.decodeWebsocketMessageBuildFailure(buffer);
- // TODO: finish this.
- __hmrlog.error("Build failed", fail.module_path);
- }
+ verbose = false;
- verbose = false;
+ handleError = (error: ErrorEvent) => {
+ __hmrlog.error("Websocket error", error.error);
+ if (this.reconnect !== 0) {
+ return;
+ }
- handleError = (error: ErrorEvent) => {
- __hmrlog.error("Websocket error", error.error);
- if (this.reconnect !== 0) {
- return;
- }
+ this.reconnect = setInterval(this.connect, 500) as any as number;
+ };
- this.reconnect = setInterval(this.connect, 500) as any as number;
- };
+ handleBuildSuccess(buffer: ByteBuffer, timestamp: number) {
+ const build = API.decodeWebsocketMessageBuildSuccess(buffer);
+
+ // Ignore builds of modules we expect a later version of
+ const currentVersion = this.builds.get(build.id) || -Infinity;
- handleBuildSuccess(buffer: ByteBuffer, timestamp: number) {
- const build = API.decodeWebsocketMessageBuildSuccess(buffer);
+ if (currentVersion > build.from_timestamp) {
+ if (this.verbose) {
+ __hmrlog.debug(
+ `Ignoring outdated update for "${build.module_path}".\n Expected: >=`,
+ currentVersion,
+ `\n Received:`,
+ build.from_timestamp
+ );
+ }
+ return;
+ }
- // Ignore builds of modules we expect a later version of
- const currentVersion = this.builds.get(build.id) || -Infinity;
+ 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 (currentVersion > build.from_timestamp) {
if (this.verbose) {
__hmrlog.debug(
- `Ignoring outdated update for "${build.module_path}".\n Expected: >=`,
- currentVersion,
- `\n Received:`,
- build.from_timestamp
+ "Preparing to reload",
+ HMRModule.dependencies.modules[index].file_path
);
}
- return;
+
+ var reload = new HotReload(
+ build.id,
+ index,
+ build,
+ // These are the bytes!!
+ buffer.data.length > buffer.index
+ ? buffer.data.subarray(buffer.index)
+ : new Uint8Array(0),
+ ReloadBehavior.hotReload
+ );
+ reload.timings.notify = timestamp - build.from_timestamp;
+ reload.run().then(
+ ([module, timings]) => {
+ __hmrlog.log(
+ `Reloaded in ${formatDuration(timings.total)}ms :`,
+ module.file_path
+ );
+ },
+ (err) => {
+ if (
+ typeof err === "object" &&
+ err &&
+ err instanceof ThrottleModuleUpdateError
+ ) {
+ return;
+ }
+ __hmrlog.error("Hot Module Reload failed!", err);
+ debugger;
+ }
+ );
}
- if (build.loader === API.Loader.css) {
- return this.loaders.css.handleBuildSuccess(buffer, build, timestamp);
+ performFullReload() {
+ if (typeof location !== "undefined") {
+ if (this.socket.readyState === this.socket.OPEN) {
+ // Disable reconnecting
+ this.reconnect = 1;
+ this.socket.close();
+ }
+ location.reload();
+ }
}
- 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);
+ handleFileChangeNotification(buffer: ByteBuffer, timestamp: number) {
+ const notification =
+ API.decodeWebsocketMessageFileChangeNotification(buffer);
+ let file_path = "";
+ switch (notification.loader) {
+ case API.Loader.css: {
+ file_path = this.loaders.css.filePath(notification);
+ break;
+ }
+
+ default: {
+ const index = HMRModule.dependencies.graph.indexOf(notification.id);
+
+ if (index > -1) {
+ file_path = HMRModule.dependencies.modules[index].file_path;
+ }
+ break;
+ }
}
- return;
- }
+ const accept = file_path && file_path.length > 0;
- if (this.verbose) {
- __hmrlog.debug(
- "Preparing to reload",
- HMRModule.dependencies.modules[index].file_path
- );
- }
+ if (!accept) {
+ if (this.verbose) {
+ __hmrlog.debug("Unknown module changed, skipping");
+ }
+ return;
+ }
- var reload = new HotReload(
- build.id,
- index,
- build,
- // These are the bytes!!
- buffer._data.length > buffer._index
- ? buffer._data.subarray(buffer._index)
- : new Uint8Array(0)
- );
- reload.timings.notify = timestamp - build.from_timestamp;
- reload.run().then(
- ([module, timings]) => {
- __hmrlog.log(
- `Reloaded in ${formatDuration(timings.total)}ms :`,
- module.file_path
- );
- },
- (err) => {
- __hmrlog.error("Hot Module Reload failed!", err);
- debugger;
+ if ((this.builds.get(notification.id) || -Infinity) > timestamp) {
+ __hmrlog.debug(`Received stale update for ${file_path}`);
+ return;
}
- );
- }
- handleFileChangeNotification(buffer: ByteBuffer, timestamp: number) {
- const notification =
- API.decodeWebsocketMessageFileChangeNotification(buffer);
- let file_path = "";
- switch (notification.loader) {
- case API.Loader.css: {
- file_path = this.loaders.css.filePath(notification);
- break;
+ let reloadBehavior = ReloadBehavior.ignore;
+
+ switch (notification.loader) {
+ // CSS always supports hot reloading
+ case API.Loader.css: {
+ 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.
+ notification.id = this.loaders.css.bundleId();
+ this.builds.set(notification.id, timestamp);
+ reloadBehavior = ReloadBehavior.hotReload;
+ break;
+ }
+ // The backend will detect if they have react-refresh in their bundle
+ // If so, it will use it.
+ // Else, it will fall back to live reloading.
+ case API.Loader.js:
+ case API.Loader.json:
+ case API.Loader.ts:
+ case API.Loader.tsx: {
+ switch (this.javascriptReloader) {
+ case API.Reloader.disable: {
+ break;
+ }
+ case API.Reloader.fast_refresh: {
+ this.builds.set(notification.id, timestamp);
+ reloadBehavior = ReloadBehavior.hotReload;
+ break;
+ }
+ case API.Reloader.live: {
+ reloadBehavior = ReloadBehavior.fullReload;
+ break;
+ }
+ }
+ break;
+ }
}
- default: {
- const index = HMRModule.dependencies.graph.indexOf(notification.id);
+ switch (reloadBehavior) {
+ // This is the same command/logic for both JS and CSS hot reloading.
+ case ReloadBehavior.hotReload: {
+ this.buildCommandBuf[0] = API.WebsocketCommandKind.build;
+ this.buildCommandUArray[0] = timestamp;
+ this.buildCommandBuf.set(this.buildCommandUArrayEight, 1);
+ this.buildCommandUArray[0] = notification.id;
+ this.buildCommandBuf.set(this.buildCommandUArrayEight, 5);
+ this.socket.send(this.buildCommandBuf);
+ if (this.verbose) {
+ __hmrlog.debug(`Requesting update for ${file_path}`);
+ }
+ break;
+ }
- if (index > -1) {
- file_path = HMRModule.dependencies.modules[index].file_path;
+ case ReloadBehavior.fullReload: {
+ this.performFullReload();
+ break;
}
- break;
}
}
+ buildCommandBuf = new Uint8Array(9);
+ buildCommandUArray = new Uint32Array(1);
+ buildCommandUArrayEight = new Uint8Array(this.buildCommandUArray.buffer);
+
+ handleOpen = (event: Event) => {
+ globalThis.clearInterval(this.reconnect);
+ this.reconnect = 0;
+ };
+
+ handleMessage = (event: MessageEvent) => {
+ const data = new Uint8Array(event.data);
+ const message_header_byte_buffer = new ByteBuffer(data);
+ const header = API.decodeWebsocketMessage(message_header_byte_buffer);
+ const buffer = new ByteBuffer(
+ data.subarray(message_header_byte_buffer.index)
+ );
- const accept = file_path && file_path.length > 0;
+ switch (header.kind) {
+ case API.WebsocketMessageKind.build_fail: {
+ this.handleBuildFailure(buffer, header.timestamp);
+ break;
+ }
+ case API.WebsocketMessageKind.build_success: {
+ this.handleBuildSuccess(buffer, header.timestamp);
+ break;
+ }
- if (!accept) {
- if (this.verbose) {
- __hmrlog.debug("Unknown module changed, skipping");
+ case API.WebsocketMessageKind.file_change_notification: {
+ this.handleFileChangeNotification(buffer, header.timestamp);
+ break;
+ }
+ case API.WebsocketMessageKind.welcome: {
+ const now = performance.now();
+ __hmrlog.log(
+ "HMR connected in",
+ formatDuration(now - clientStartTime),
+ "ms"
+ );
+ clientStartTime = now;
+ this.hasWelcomed = true;
+ const welcome = API.decodeWebsocketMessageWelcome(buffer);
+ this.epoch = welcome.epoch;
+ this.javascriptReloader = welcome.javascriptReloader;
+ if (!this.epoch) {
+ __hmrlog.warn("Internal HMR error");
+ }
+ break;
+ }
}
- return;
- }
+ };
+
+ handleClose = (event: CloseEvent) => {
+ if (this.reconnect !== 0) {
+ return;
+ }
+
+ this.reconnect = globalThis.setInterval(
+ this.connect,
+ 500
+ ) as any as number;
+ __hmrlog.warn("HMR disconnected. Attempting to reconnect.");
+ };
+ }
+ let pendingUpdateCount = 0;
- if ((this.builds.get(notification.id) || -Infinity) > timestamp) {
- __hmrlog.debug(`Received update for ${file_path}`);
- return;
+ class ThrottleModuleUpdateError extends Error {
+ constructor(message) {
+ super(message);
}
+ }
- if (this.verbose) {
- __hmrlog.debug(`Requesting update for ${file_path}`);
+ class HotReload {
+ module_id: number = 0;
+ module_index: number = 0;
+ build: API.WebsocketMessageBuildSuccess;
+
+ timings = {
+ notify: 0,
+ decode: 0,
+ import: 0,
+ callbacks: 0,
+ total: 0,
+ start: 0,
+ };
+ static VERBOSE = false;
+ bytes: Uint8Array;
+ reloader: ReloadBehavior;
+
+ constructor(
+ module_id: HotReload["module_id"],
+ module_index: HotReload["module_index"],
+ build: HotReload["build"],
+ bytes: Uint8Array,
+ reloader: ReloadBehavior
+ ) {
+ this.module_id = module_id;
+ this.module_index = module_index;
+ this.build = build;
+ this.bytes = bytes;
+ this.reloader = reloader;
}
- this.builds.set(notification.id, timestamp);
+ async run() {
+ pendingUpdateCount++;
+ let result: [HMRModule, HotReload["timings"]];
+ try {
+ result = await this._run();
+ } finally {
+ pendingUpdateCount--;
+ }
- // 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);
+ return result;
}
- this.buildCommandBuf[0] = API.WebsocketCommandKind.build;
- this.buildCommandUArray[0] = timestamp;
- this.buildCommandBuf.set(this.buildCommandUArrayEight, 1);
- this.buildCommandUArray[0] = notification.id;
- this.buildCommandBuf.set(this.buildCommandUArrayEight, 5);
- this.socket.send(this.buildCommandBuf);
- }
- buildCommandBuf = new Uint8Array(9);
- buildCommandUArray = new Uint32Array(1);
- buildCommandUArrayEight = new Uint8Array(this.buildCommandUArray.buffer);
+ private async _run(): Promise<[HMRModule, HotReload["timings"]]> {
+ const currentPendingUpdateCount = pendingUpdateCount;
- handleOpen = (event: Event) => {
- globalThis.clearInterval(this.reconnect);
- this.reconnect = 0;
- };
+ const importStart = performance.now();
+ let orig_deps = HMRModule.dependencies;
+ // we must preserve the updater since that holds references to the real exports.
+ // this is a fundamental limitation of using esmodules for HMR.
+ // we cannot export new modules. we can only mutate existing ones.
- handleMessage = (event: MessageEvent) => {
- const data = new Uint8Array(event.data);
- const message_header_byte_buffer = new ByteBuffer(data);
- const header = API.decodeWebsocketMessage(message_header_byte_buffer);
- const buffer = new ByteBuffer(
- data.subarray(message_header_byte_buffer._index)
- );
+ const oldGraphUsed = HMRModule.dependencies.graph_used;
+ var oldModule = HMRModule.dependencies.modules[this.module_index];
+ HMRModule.dependencies = orig_deps.fork(this.module_index);
+ var blobURL = null;
+ try {
+ const blob = new Blob([this.bytes], { type: "text/javascript" });
+ blobURL = URL.createObjectURL(blob);
+ await import(blobURL);
+ this.timings.import = performance.now() - importStart;
+ } catch (exception) {
+ HMRModule.dependencies = orig_deps;
+ URL.revokeObjectURL(blobURL);
+ // Ensure we don't keep the bytes around longer than necessary
+ this.bytes = null;
+ oldModule = null;
+ throw exception;
+ }
- switch (header.kind) {
- case API.WebsocketMessageKind.build_fail: {
- this.handleBuildFailure(buffer, header.timestamp);
- break;
+ // We didn't import any new modules, so we resume as before.
+ if (HMRModule.dependencies.graph_used === this.module_index) {
+ HMRModule.dependencies.graph_used = oldGraphUsed;
+ } else {
+ // If we do import a new module, we have to do a full page reload for now
}
- case API.WebsocketMessageKind.build_success: {
- this.handleBuildSuccess(buffer, header.timestamp);
- break;
+
+ URL.revokeObjectURL(blobURL);
+ // Ensure we don't keep the bytes around longer than necessary
+ this.bytes = null;
+
+ if (HotReload.VERBOSE) {
+ __hmrlog.debug(
+ "Re-imported",
+ HMRModule.dependencies.modules[this.module_index].file_path,
+ "in",
+ formatDuration(this.timings.import),
+ ". Running callbacks"
+ );
}
- case API.WebsocketMessageKind.file_change_notification: {
- this.handleFileChangeNotification(buffer, header.timestamp);
- break;
+ const callbacksStart = performance.now();
+ const origUpdaters =
+ HMRModule.dependencies.modules[
+ this.module_index
+ ].additional_updaters.slice();
+ try {
+ switch (this.reloader) {
+ case ReloadBehavior.hotReload: {
+ let foundBoundary = false;
+
+ if (oldModule) {
+ HMRModule.dependencies.modules[
+ this.module_index
+ ].additional_updaters.push(oldModule.update.bind(oldModule));
+ }
+ // -- For generic hot reloading --
+ // ES Modules delay execution until all imports are parsed
+ // They execute depth-first
+ // If you load N modules and append each module ID to the array, 0 is the *last* unique module imported.
+ // modules.length - 1 is the first.
+ // Therefore, to reload all the modules in the correct order, we traverse the graph backwards
+ // This only works when the graph is up to date.
+ // If the import order changes, we need to regenerate the entire graph
+ // Which sounds expensive, until you realize that we are mostly talking about an array that will be typically less than 1024 elements
+ // Computers can create an array of < 1024 pointer-sized elements in < 1ms easy!
+ // --
+
+ // -- For React Fast Refresh --
+ // We must find a React Refresh boundary. This is a module that only exports React components.
+ // If we do not find a React Refresh boundary, we must instead perform a full page reload.
+ for (
+ let i = HMRModule.dependencies.graph_used;
+ i > this.module_index;
+ i--
+ ) {
+ let handled =
+ !HMRModule.dependencies.modules[i].exports.__hmrDisable;
+ if (
+ typeof HMRModule.dependencies.modules[i].dispose === "function"
+ ) {
+ HMRModule.dependencies.modules[i].dispose();
+ handled = true;
+ }
+ if (
+ typeof HMRModule.dependencies.modules[i].accept === "function"
+ ) {
+ HMRModule.dependencies.modules[i].accept();
+ handled = true;
+ }
+
+ // Automatically re-initialize the dependency
+ if (!handled) {
+ HMRModule.dependencies.modules[i].update();
+ }
+
+ // If we don't find a boundary, we will need to do a full page load
+ if (
+ (HMRModule.dependencies.modules[i] as FastRefreshModule)
+ .isRefreshBoundary
+ ) {
+ foundBoundary = true;
+ }
+ }
+
+ // By the time we get here, it's entirely possible that another update is waiting
+ // Instead of scheduling it, we are going to just ignore this update.
+ // But we still need to re-initialize modules regardless because otherwise a dependency may not reload properly
+ if (
+ pendingUpdateCount === currentPendingUpdateCount &&
+ foundBoundary
+ ) {
+ FastRefreshLoader.RefreshRuntime.performReactRefresh();
+ } else if (pendingUpdateCount === currentPendingUpdateCount) {
+ FastRefreshLoader.performFullRefresh();
+ } else {
+ return Promise.reject(
+ new ThrottleModuleUpdateError(
+ `Expected pendingUpdateCount: ${currentPendingUpdateCount} but received: ${pendingUpdateCount}`
+ )
+ );
+ }
+ break;
+ }
+ }
+ } catch (exception) {
+ HMRModule.dependencies = orig_deps;
+ HMRModule.dependencies.modules[this.module_index].additional_updaters =
+ origUpdaters;
+ throw exception;
}
- case API.WebsocketMessageKind.welcome: {
- const now = performance.now();
- __hmrlog.log(
- "HMR connected in",
- formatDuration(now - clientStartTime),
+ this.timings.callbacks = performance.now() - callbacksStart;
+
+ if (HotReload.VERBOSE) {
+ __hmrlog.debug(
+ "Ran callbacks",
+ HMRModule.dependencies.modules[this.module_index].file_path,
+ "in",
+ formatDuration(this.timings.callbacks),
"ms"
);
- clientStartTime = now;
- this.hasWelcomed = true;
- const welcome = API.decodeWebsocketMessageWelcome(buffer);
- this.epoch = welcome.epoch;
- if (!this.epoch) {
- __hmrlog.warn("Internal HMR error");
- }
- break;
}
- }
- };
- handleClose = (event: CloseEvent) => {
- if (this.reconnect !== 0) {
- return;
+ orig_deps = null;
+ this.timings.total =
+ this.timings.import + this.timings.callbacks + this.timings.notify;
+ return Promise.resolve([
+ HMRModule.dependencies.modules[this.module_index],
+ this.timings,
+ ]);
}
-
- this.reconnect = globalThis.setInterval(this.connect, 500) as any as number;
- __hmrlog.warn("HMR disconnected. Attempting to reconnect.");
- };
-}
-
-export { HMRClient as __HMRClient };
-
-class HotReload {
- module_id: number = 0;
- module_index: number = 0;
- build: API.WebsocketMessageBuildSuccess;
- timings = {
- notify: 0,
- decode: 0,
- import: 0,
- callbacks: 0,
- total: 0,
- start: 0,
- };
- static VERBOSE = false;
- bytes: Uint8Array;
-
- constructor(
- module_id: HotReload["module_id"],
- module_index: HotReload["module_index"],
- build: HotReload["build"],
- bytes: Uint8Array
- ) {
- this.module_id = module_id;
- this.module_index = module_index;
- this.build = build;
- this.bytes = bytes;
}
- async run(): Promise<[HMRModule, HotReload["timings"]]> {
- const importStart = performance.now();
- let orig_deps = HMRModule.dependencies;
- // we must preserve the updater since that holds references to the real exports.
- // this is a fundamental limitation of using esmodules for HMR.
- // we cannot export new modules. we can only mutate existing ones.
-
- HMRModule.dependencies = orig_deps.fork(this.module_index);
- var blobURL = null;
- try {
- const blob = new Blob([this.bytes], { type: "text/javascript" });
- blobURL = URL.createObjectURL(blob);
- await import(blobURL);
- this.timings.import = performance.now() - importStart;
- } catch (exception) {
- HMRModule.dependencies = orig_deps;
- URL.revokeObjectURL(blobURL);
- // Ensure we don't keep the bytes around longer than necessary
- this.bytes = null;
- throw exception;
- }
+ type AnyHMRModule = HMRModule | FastRefreshModule;
+ class DependencyGraph {
+ modules: AnyHMRModule[];
+ graph: Uint32Array;
+ graph_used = 0;
- URL.revokeObjectURL(blobURL);
- // Ensure we don't keep the bytes around longer than necessary
- this.bytes = null;
-
- if (HotReload.VERBOSE) {
- __hmrlog.debug(
- "Re-imported",
- HMRModule.dependencies.modules[this.module_index].file_path,
- "in",
- formatDuration(this.timings.import),
- ". Running callbacks"
- );
+ loadDefaults() {
+ this.modules = new Array<AnyHMRModule>(32);
+ this.graph = new Uint32Array(32);
+ this.graph_used = 0;
}
- const callbacksStart = performance.now();
- try {
- // ES Modules delay execution until all imports are parsed
- // They execute depth-first
- // If you load N modules and append each module ID to the array, 0 is the *last* unique module imported.
- // modules.length - 1 is the first.
- // Therefore, to reload all the modules in the correct order, we traverse the graph backwards
- // This only works when the graph is up to date.
- // If the import order changes, we need to regenerate the entire graph
- // Which sounds expensive, until you realize that we are mostly talking about an array that will be typically less than 1024 elements
- // Computers can create an array of < 1024 pointer-sized elements in < 1ms easy!
- for (
- let i = HMRModule.dependencies.graph_used;
- i > this.module_index;
- i--
- ) {
- let handled = !HMRModule.dependencies.modules[i].exports.__hmrDisable;
- if (typeof HMRModule.dependencies.modules[i].dispose === "function") {
- HMRModule.dependencies.modules[i].dispose();
- handled = true;
- }
- if (typeof HMRModule.dependencies.modules[i].accept === "function") {
- HMRModule.dependencies.modules[i].accept();
- handled = true;
- }
- if (!handled) {
- HMRModule.dependencies.modules[i]._load();
- }
- }
- } catch (exception) {
- HMRModule.dependencies = orig_deps;
- throw exception;
- }
- this.timings.callbacks = performance.now() - callbacksStart;
-
- if (HotReload.VERBOSE) {
- __hmrlog.debug(
- "Ran callbacks",
- HMRModule.dependencies.modules[this.module_index].file_path,
- "in",
- formatDuration(this.timings.callbacks),
- "ms"
- );
+ static loadWithDefaults() {
+ const graph = new DependencyGraph();
+ graph.loadDefaults();
+ return graph;
}
- orig_deps = null;
- this.timings.total =
- this.timings.import + this.timings.callbacks + this.timings.notify;
- return Promise.resolve([
- HMRModule.dependencies.modules[this.module_index],
- this.timings,
- ]);
+ fork(offset: number) {
+ const graph = new DependencyGraph();
+ graph.modules = this.modules.slice();
+ graph.graph_used = offset;
+ graph.graph = this.graph.slice();
+ return graph;
+ }
}
-}
-class DependencyGraph {
- modules: HMRModule[];
- graph: Uint32Array;
- graph_used = 0;
+ class HMRModule {
+ constructor(id: number, file_path: string) {
+ this.id = id;
+ this.file_path = file_path;
+
+ Object.defineProperty(this, "name", {
+ get() {
+ return this.file_path;
+ },
+ configurable: false,
+ enumerable: false,
+ });
- loadDefaults() {
- this.modules = new Array<HMRModule>(32);
- this.graph = new Uint32Array(32);
- this.graph_used = 0;
- }
+ if (!HMRModule.dependencies) {
+ HMRModule.dependencies = HMRModule._dependencies;
+ }
- static loadWithDefaults() {
- const graph = new DependencyGraph();
- graph.loadDefaults();
- return graph;
- }
+ this.graph_index = HMRModule.dependencies.graph_used++;
- fork(offset: number) {
- const graph = new DependencyGraph();
- graph.modules = this.modules.slice();
- graph.graph_used = offset > 0 ? offset - 1 : 0;
- graph.graph = this.graph.slice();
- return graph;
- }
-}
+ // Grow the dependencies graph
+ if (HMRModule.dependencies.graph.length <= this.graph_index) {
+ const new_graph = new Uint32Array(
+ HMRModule.dependencies.graph.length * 4
+ );
+ new_graph.set(HMRModule.dependencies.graph);
+ HMRModule.dependencies.graph = new_graph;
-class HMRModule {
- constructor(id: number, file_path: string) {
- this.id = id;
- this.file_path = file_path;
+ // In-place grow. This creates a holey array, which is bad, but less bad than pushing potentially 1000 times
+ HMRModule.dependencies.modules.length = new_graph.length;
+ }
- if (!HMRModule.dependencies) {
- HMRModule.dependencies = HMRModule._dependencies;
+ HMRModule.dependencies.modules[this.graph_index] = this;
+ HMRModule.dependencies.graph[this.graph_index] = this.id;
}
- this.graph_index = HMRModule.dependencies.graph_used++;
-
- // Grow the dependencies graph
- if (HMRModule.dependencies.graph.length <= this.graph_index) {
- const new_graph = new Uint32Array(
- HMRModule.dependencies.graph.length * 4
- );
- new_graph.set(HMRModule.dependencies.graph);
- HMRModule.dependencies.graph = new_graph;
+ additional_files = [];
+ additional_updaters = [];
+ _update: (exports: Object) => void;
+ update() {
+ for (let update of this.additional_updaters) {
+ update(this.exports);
+ }
- // In-place grow. This creates a holey array, which is bad, but less bad than pushing potentially 1000 times
- HMRModule.dependencies.modules.length = new_graph.length;
+ this._update(this.exports);
}
- if (
- typeof HMRModule.dependencies.modules[this.graph_index] === "object" &&
- HMRModule.dependencies.modules[this.graph_index] instanceof HMRModule &&
- HMRModule.dependencies.modules[this.graph_index].id === id &&
- typeof HMRModule.dependencies.modules[this.graph_index]._update ===
- "function"
- ) {
- this.additional_updaters.push(
- HMRModule.dependencies.modules[this.graph_index]._update
- );
+ static _dependencies = DependencyGraph.loadWithDefaults();
+ exportAll(object: Object) {
+ // object[alias] must be a function
+ for (let alias in object) {
+ this._exports[alias] = object[alias];
+ Object.defineProperty(this.exports, alias, {
+ get: this._exports[alias],
+ configurable: true,
+ enumerable: true,
+ });
+ }
}
- HMRModule.dependencies.modules[this.graph_index] = this;
- HMRModule.dependencies.graph[this.graph_index] = this.id;
+ static dependencies: DependencyGraph;
+ file_path: string;
+ _load = function () {};
+ id = 0;
+ graph_index = 0;
+ _exports = {};
+ exports = {};
}
- additional_files = [];
- additional_updaters = [];
- _update: (exports: Object) => void;
- update() {
- for (let update of this.additional_updaters) {
- update(this.exports);
+ class FastRefreshModule extends HMRModule {
+ constructor(id: number, file_path: string, RefreshRuntime: any) {
+ super(id, file_path);
+
+ // 4,000,000,000 in base36 occupies 7 characters
+ // file path is probably longer
+ // small strings are better strings
+ this.refreshRuntimeBaseID =
+ (this.file_path.length > 7 ? this.id.toString(36) : this.file_path) +
+ "/";
+ FastRefreshLoader.RefreshRuntime =
+ FastRefreshLoader.RefreshRuntime || RefreshRuntime;
+
+ if (!FastRefreshLoader.hasInjectedFastRefresh) {
+ RefreshRuntime.injectIntoGlobalHook(globalThis);
+ FastRefreshLoader.hasInjectedFastRefresh = true;
+ }
}
- this._update(this.exports);
- }
+ refreshRuntimeBaseID: string;
+ isRefreshBoundary = false;
- static _dependencies = DependencyGraph.loadWithDefaults();
- exportAll(object: Object) {
- // object[alias] must be a function
- for (let alias in object) {
- this._exports[alias] = object[alias];
- Object.defineProperty(this.exports, alias, {
- get: this._exports[alias],
- configurable: true,
- enumerable: true,
- });
+ // $RefreshReg$
+ $r_(Component: any, id: string) {
+ FastRefreshLoader.RefreshRuntime.register(
+ Component,
+ this.refreshRuntimeBaseID + id
+ );
+ }
+ // $RefreshReg$(Component, Component.name || Component.displayName)
+ $r(Component: any) {
+ if (!FastRefreshLoader.RefreshRuntime.isLikelyComponentType(Component)) {
+ return;
+ }
+
+ this.$r_(Component, Component.name || Component.displayName);
+ }
+
+ // Auto-register exported React components so we only have to manually register the non-exported ones
+ // This is what Metro does: https://github.com/facebook/metro/blob/9f2b1210a0f66378dd93e5fcaabc464c86c9e236/packages/metro-runtime/src/polyfills/require.js#L905
+ exportAll(object: any) {
+ super.exportAll(object);
+
+ // One thing I'm unsure of:
+ // Do we need to register the exports object iself? Is it important for some namespacing thing?
+ // Metro seems to do that. However, that might be an artifact of CommonJS modules. People do module.exports = SomeReactComponent.
+ var hasExports = false;
+ var onlyExportsComponents = true;
+ for (const key in object) {
+ if (key === "__esModule") {
+ continue;
+ }
+
+ hasExports = true;
+
+ // Everything in here should always be a function
+ // exportAll({blah: () => blah})
+ // If you see exception right here, please file an issue and include the source file in the issue.
+ const Component = object[key]();
+
+ // Ensure exported React components always have names
+ // This is for simpler debugging
+ if (
+ Component &&
+ typeof Component === "function" &&
+ !("name" in Component) &&
+ Object.isExtensible(Component)
+ ) {
+ const named = {
+ get() {
+ return key;
+ },
+ enumerable: false,
+ configurable: true,
+ };
+ // Ignore any errors if it turns out this was already set as not configurable
+ try {
+ // "name" is the official JavaScript way
+ // "displayName" is the legacy React way
+ Object.defineProperties(Component, {
+ name: named,
+ displayName: named,
+ });
+ } catch (exception) {}
+ }
+
+ if (
+ !FastRefreshLoader.RefreshRuntime.isLikelyComponentType(Component)
+ ) {
+ onlyExportsComponents = false;
+ // We can't stop here because we may have other exports which are components that need to be registered.
+ continue;
+ }
+
+ this.$r_(Component, key);
+ }
+
+ this.isRefreshBoundary = hasExports && onlyExportsComponents;
+ }
+
+ loaded(_onUpdate) {
+ this._update = _onUpdate;
}
}
- static dependencies: DependencyGraph;
- file_path: string;
- _load = function () {};
- id = 0;
- graph_index = 0;
- _exports = {};
- exports = {};
+ var __hmrlog = {
+ debug(...args) {
+ // console.debug("[speedy]", ...args);
+ console.debug(...args);
+ },
+ error(...args) {
+ // console.error("[speedy]", ...args);
+ console.error(...args);
+ },
+ log(...args) {
+ // console.log("[speedy]", ...args);
+ console.log(...args);
+ },
+ warn(...args) {
+ // console.warn("[speedy]", ...args);
+ console.warn(...args);
+ },
+ };
+
+ __HMRModule = HMRModule;
+ __FastRefreshModule = FastRefreshModule;
+ __HMRClient = HMRClient;
}
-var __hmrlog = {
- debug(...args) {
- // console.debug("[speedy]", ...args);
- console.debug(...args);
- },
- error(...args) {
- // console.error("[speedy]", ...args);
- console.error(...args);
- },
- log(...args) {
- // console.log("[speedy]", ...args);
- console.log(...args);
- },
- warn(...args) {
- // console.warn("[speedy]", ...args);
- console.warn(...args);
- },
-};
-
-export { HMRModule as __HMRModule };
+export { __HMRModule, __FastRefreshModule, __HMRClient };