I think thats the JS part of HMR

Former-commit-id: 43380a4d68
This commit is contained in:
Jarred Sumner
2021-06-12 19:10:08 -07:00
parent f43234bc30
commit c51c65325f
12 changed files with 2021 additions and 1032 deletions

77
src/api/schema.d.ts vendored
View File

@@ -119,6 +119,29 @@ type uint32 = number;
4: "debug",
debug: "debug"
}
export enum WebsocketMessageKind {
welcome = 1,
file_change_notification = 2,
build_success = 3,
build_fail = 4
}
export const WebsocketMessageKindKeys = {
1: "welcome",
welcome: "welcome",
2: "file_change_notification",
file_change_notification: "file_change_notification",
3: "build_success",
build_success: "build_success",
4: "build_fail",
build_fail: "build_fail"
}
export enum WebsocketCommandKind {
build = 1
}
export const WebsocketCommandKindKeys = {
1: "build",
build: "build"
}
export interface JSX {
factory: string;
runtime: JSXRuntime;
@@ -262,6 +285,46 @@ type uint32 = number;
msgs: Message[];
}
export interface WebsocketMessage {
timestamp: uint32;
kind: WebsocketMessageKind;
}
export interface WebsocketMessageWelcome {
epoch: uint32;
}
export interface WebsocketMessageFileChangeNotification {
id: uint32;
loader: Loader;
}
export interface WebsocketCommand {
kind: WebsocketCommandKind;
timestamp: uint32;
}
export interface WebsocketCommandBuild {
id: uint32;
}
export interface WebsocketMessageBuildSuccess {
id: uint32;
from_timestamp: uint32;
loader: Loader;
module_path: alphanumeric;
log: Log;
bytes: Uint8Array;
}
export interface WebsocketMessageBuildFailure {
id: uint32;
from_timestamp: uint32;
loader: Loader;
module_path: alphanumeric;
log: Log;
}
export declare function encodeJSX(message: JSX, bb: ByteBuffer): void;
export declare function decodeJSX(buffer: ByteBuffer): JSX;
export declare function encodeStringPointer(message: StringPointer, bb: ByteBuffer): void;
@@ -300,3 +363,17 @@ type uint32 = number;
export declare function decodeMessage(buffer: ByteBuffer): Message;
export declare function encodeLog(message: Log, bb: ByteBuffer): void;
export declare function decodeLog(buffer: ByteBuffer): Log;
export declare function encodeWebsocketMessage(message: WebsocketMessage, bb: ByteBuffer): void;
export declare function decodeWebsocketMessage(buffer: ByteBuffer): WebsocketMessage;
export declare function encodeWebsocketMessageWelcome(message: WebsocketMessageWelcome, bb: ByteBuffer): void;
export declare function decodeWebsocketMessageWelcome(buffer: ByteBuffer): WebsocketMessageWelcome;
export declare function encodeWebsocketMessageFileChangeNotification(message: WebsocketMessageFileChangeNotification, bb: ByteBuffer): void;
export declare function decodeWebsocketMessageFileChangeNotification(buffer: ByteBuffer): WebsocketMessageFileChangeNotification;
export declare function encodeWebsocketCommand(message: WebsocketCommand, bb: ByteBuffer): void;
export declare function decodeWebsocketCommand(buffer: ByteBuffer): WebsocketCommand;
export declare function encodeWebsocketCommandBuild(message: WebsocketCommandBuild, bb: ByteBuffer): void;
export declare function decodeWebsocketCommandBuild(buffer: ByteBuffer): WebsocketCommandBuild;
export declare function encodeWebsocketMessageBuildSuccess(message: WebsocketMessageBuildSuccess, bb: ByteBuffer): void;
export declare function decodeWebsocketMessageBuildSuccess(buffer: ByteBuffer): WebsocketMessageBuildSuccess;
export declare function encodeWebsocketMessageBuildFailure(message: WebsocketMessageBuildFailure, bb: ByteBuffer): void;
export declare function decodeWebsocketMessageBuildFailure(buffer: ByteBuffer): WebsocketMessageBuildFailure;

View File

@@ -1235,6 +1235,266 @@ function encodeLog(message, bb) {
throw new Error("Missing required field \"msgs\"");
}
}
const WebsocketMessageKind = {
"1": 1,
"2": 2,
"3": 3,
"4": 4,
"welcome": 1,
"file_change_notification": 2,
"build_success": 3,
"build_fail": 4
};
const WebsocketMessageKindKeys = {
"1": "welcome",
"2": "file_change_notification",
"3": "build_success",
"4": "build_fail",
"welcome": "welcome",
"file_change_notification": "file_change_notification",
"build_success": "build_success",
"build_fail": "build_fail"
};
const WebsocketCommandKind = {
"1": 1,
"build": 1
};
const WebsocketCommandKindKeys = {
"1": "build",
"build": "build"
};
function decodeWebsocketMessage(bb) {
var result = {};
result["timestamp"] = bb.readUint32();
result["kind"] = WebsocketMessageKind[bb.readByte()];
return result;
}
function encodeWebsocketMessage(message, bb) {
var value = message["timestamp"];
if (value != null) {
bb.writeUint32(value);
} else {
throw new Error("Missing required field \"timestamp\"");
}
var value = message["kind"];
if (value != null) {
var encoded = WebsocketMessageKind[value];
if (encoded === void 0) throw new Error("Invalid value " + JSON.stringify(value) + " for enum \"WebsocketMessageKind\"");
bb.writeByte(encoded);
} else {
throw new Error("Missing required field \"kind\"");
}
}
function decodeWebsocketMessageWelcome(bb) {
var result = {};
result["epoch"] = bb.readUint32();
return result;
}
function encodeWebsocketMessageWelcome(message, bb) {
var value = message["epoch"];
if (value != null) {
bb.writeUint32(value);
} else {
throw new Error("Missing required field \"epoch\"");
}
}
function decodeWebsocketMessageFileChangeNotification(bb) {
var result = {};
result["id"] = bb.readUint32();
result["loader"] = Loader[bb.readByte()];
return result;
}
function encodeWebsocketMessageFileChangeNotification(message, bb) {
var value = message["id"];
if (value != null) {
bb.writeUint32(value);
} else {
throw new Error("Missing required field \"id\"");
}
var value = message["loader"];
if (value != null) {
var encoded = Loader[value];
if (encoded === void 0) throw new Error("Invalid value " + JSON.stringify(value) + " for enum \"Loader\"");
bb.writeByte(encoded);
} else {
throw new Error("Missing required field \"loader\"");
}
}
function decodeWebsocketCommand(bb) {
var result = {};
result["kind"] = WebsocketCommandKind[bb.readByte()];
result["timestamp"] = bb.readUint32();
return result;
}
function encodeWebsocketCommand(message, bb) {
var value = message["kind"];
if (value != null) {
var encoded = WebsocketCommandKind[value];
if (encoded === void 0) throw new Error("Invalid value " + JSON.stringify(value) + " for enum \"WebsocketCommandKind\"");
bb.writeByte(encoded);
} else {
throw new Error("Missing required field \"kind\"");
}
var value = message["timestamp"];
if (value != null) {
bb.writeUint32(value);
} else {
throw new Error("Missing required field \"timestamp\"");
}
}
function decodeWebsocketCommandBuild(bb) {
var result = {};
result["id"] = bb.readUint32();
return result;
}
function encodeWebsocketCommandBuild(message, bb) {
var value = message["id"];
if (value != null) {
bb.writeUint32(value);
} else {
throw new Error("Missing required field \"id\"");
}
}
function decodeWebsocketMessageBuildSuccess(bb) {
var result = {};
result["id"] = bb.readUint32();
result["from_timestamp"] = bb.readUint32();
result["loader"] = Loader[bb.readByte()];
result["module_path"] = bb.readAlphanumeric();
result["log"] = decodeLog(bb);
result["bytes"] = bb.readByteArray();
return result;
}
function encodeWebsocketMessageBuildSuccess(message, bb) {
var value = message["id"];
if (value != null) {
bb.writeUint32(value);
} else {
throw new Error("Missing required field \"id\"");
}
var value = message["from_timestamp"];
if (value != null) {
bb.writeUint32(value);
} else {
throw new Error("Missing required field \"from_timestamp\"");
}
var value = message["loader"];
if (value != null) {
var encoded = Loader[value];
if (encoded === void 0) throw new Error("Invalid value " + JSON.stringify(value) + " for enum \"Loader\"");
bb.writeByte(encoded);
} else {
throw new Error("Missing required field \"loader\"");
}
var value = message["module_path"];
if (value != null) {
bb.writeAlphanumeric(value);
} else {
throw new Error("Missing required field \"module_path\"");
}
var value = message["log"];
if (value != null) {
encodeLog(value, bb);
} else {
throw new Error("Missing required field \"log\"");
}
var value = message["bytes"];
if (value != null) {
bb.writeByteArray(value);
} else {
throw new Error("Missing required field \"bytes\"");
}
}
function decodeWebsocketMessageBuildFailure(bb) {
var result = {};
result["id"] = bb.readUint32();
result["from_timestamp"] = bb.readUint32();
result["loader"] = Loader[bb.readByte()];
result["module_path"] = bb.readAlphanumeric();
result["log"] = decodeLog(bb);
return result;
}
function encodeWebsocketMessageBuildFailure(message, bb) {
var value = message["id"];
if (value != null) {
bb.writeUint32(value);
} else {
throw new Error("Missing required field \"id\"");
}
var value = message["from_timestamp"];
if (value != null) {
bb.writeUint32(value);
} else {
throw new Error("Missing required field \"from_timestamp\"");
}
var value = message["loader"];
if (value != null) {
var encoded = Loader[value];
if (encoded === void 0) throw new Error("Invalid value " + JSON.stringify(value) + " for enum \"Loader\"");
bb.writeByte(encoded);
} else {
throw new Error("Missing required field \"loader\"");
}
var value = message["module_path"];
if (value != null) {
bb.writeAlphanumeric(value);
} else {
throw new Error("Missing required field \"module_path\"");
}
var value = message["log"];
if (value != null) {
encodeLog(value, bb);
} else {
throw new Error("Missing required field \"log\"");
}
}
export { Loader }
@@ -1290,4 +1550,22 @@ export { encodeMessageData }
export { decodeMessage }
export { encodeMessage }
export { decodeLog }
export { encodeLog }
export { encodeLog }
export { WebsocketMessageKind }
export { WebsocketMessageKindKeys }
export { WebsocketCommandKind }
export { WebsocketCommandKindKeys }
export { decodeWebsocketMessage }
export { encodeWebsocketMessage }
export { decodeWebsocketMessageWelcome }
export { encodeWebsocketMessageWelcome }
export { decodeWebsocketMessageFileChangeNotification }
export { encodeWebsocketMessageFileChangeNotification }
export { decodeWebsocketCommand }
export { encodeWebsocketCommand }
export { decodeWebsocketCommandBuild }
export { encodeWebsocketCommandBuild }
export { decodeWebsocketMessageBuildSuccess }
export { encodeWebsocketMessageBuildSuccess }
export { decodeWebsocketMessageBuildFailure }
export { encodeWebsocketMessageBuildFailure }

View File

@@ -243,12 +243,65 @@ struct Log {
// From a server perspective, this means the filesystem watching thread can send the same WebSocket message
// to every client, which is good for performance. It means if you have 5 tabs open it won't really be different than one tab
// The clients can just ignore files they don't care about
smol WebsocketMessageType {
file_change = 1,
build_success = 2,
build_fail = 3,
smol WebsocketMessageKind {
welcome = 1;
file_change_notification = 2;
build_success = 3;
build_fail = 4;
}
struct WeboscketMessage {
smol WebsocketCommandKind {
build = 1;
}
// Each websocket message has two messages in it!
// This is the first.
struct WebsocketMessage {
uint32 timestamp;
WebsocketMessageKind kind;
}
// This is the first.
struct WebsocketMessageWelcome {
uint32 epoch;
}
struct WebsocketMessageFileChangeNotification {
uint32 id;
Loader loader;
}
struct WebsocketCommand {
WebsocketCommandKind kind;
uint32 timestamp;
}
// The timestamp is used for client-side deduping
struct WebsocketCommandBuild {
uint32 id;
}
// We copy the module_path here incase they don't already have it
struct WebsocketMessageBuildSuccess {
uint32 id;
uint32 from_timestamp;
Loader loader;
alphanumeric module_path;
Log log;
byte[] bytes;
}
struct WebsocketMessageBuildFailure {
uint32 id;
uint32 from_timestamp;
Loader loader;
alphanumeric module_path;
Log log;
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1091,6 +1091,7 @@ pub fn NewBundler(cache_files: bool) type {
opts.enable_bundling = bundler.options.node_modules_bundle != null;
opts.transform_require_to_import = true;
opts.can_import_from_bundle = bundler.options.node_modules_bundle != null;
opts.features.hot_module_reloading = bundler.options.hot_module_reloading;
const value = (bundler.resolver.caches.js.parse(allocator, opts, bundler.options.define, bundler.log, &source) catch null) orelse return null;
return ParseResult{
.ast = value,

View File

@@ -7,7 +7,9 @@ pub const options = @import("../options.zig");
pub const alloc = @import("../alloc.zig");
pub const js_printer = @import("../js_printer.zig");
pub const renamer = @import("../renamer.zig");
pub const RuntimeImports = @import("../runtime.zig").Runtime.Imports;
const _runtime = @import("../runtime.zig");
pub const RuntimeImports = _runtime.Runtime.Imports;
pub const RuntimeFeatures = _runtime.Runtime.Features;
pub const fs = @import("../fs.zig");
const _hash_map = @import("../hash_map.zig");
pub usingnamespace @import("../global.zig");
@@ -23,6 +25,7 @@ pub const ExprNodeIndex = js_ast.ExprNodeIndex;
pub const ExprNodeList = js_ast.ExprNodeList;
pub const StmtNodeList = js_ast.StmtNodeList;
pub const BindingNodeList = js_ast.BindingNodeList;
pub const assert = std.debug.assert;
pub const LocRef = js_ast.LocRef;

View File

@@ -1552,6 +1552,8 @@ pub const Parser = struct {
use_define_for_class_fields: bool = false,
suppress_warnings_about_weird_code: bool = true,
features: RuntimeFeatures = RuntimeFeatures{},
// Used when bundling node_modules
enable_bundling: bool = false,
transform_require_to_import: bool = true,
@@ -1889,7 +1891,7 @@ pub const Parser = struct {
jsx_part_stmts[stmt_i] = p.s(S.Local{ .kind = .k_var, .decls = decls }, loc);
after.append(js_ast.Part{
before.append(js_ast.Part{
.stmts = jsx_part_stmts,
.declared_symbols = declared_symbols,
.import_record_indices = import_records,
@@ -1947,7 +1949,7 @@ pub const Parser = struct {
};
}
after.append(js_ast.Part{
before.append(js_ast.Part{
.stmts = p.cjs_import_stmts.items,
.declared_symbols = declared_symbols,
.import_record_indices = import_records,
@@ -4419,6 +4421,41 @@ pub fn NewParser(
}, loc);
}
// For HMR, we must convert syntax like this:
// export function leftPad() {
// export const guy = GUY_FIERI_ASCII_ART;
// export class Bacon {}
// export default GuyFieriAsciiArt;
// export {Bacon};
// export {Bacon as default};
// to:
// var __hmr__module = new __hmr_HMRModule(file_id, import.meta);
// (__hmr__module._load = function() {
// __hmr__module.exports.leftPad = function () {};
// __hmr__module.exports.npmProgressBar33 = true;
// __hmr__module.exports.Bacon = class {};
// })();
// export { __hmr__module.exports.leftPad as leftPad, __hmr__module.exports.npmProgressBar33 as npmProgressBar33, __hmr__module }
//
//
//
// At bottom of the file:
// -
// var __hmr__exports = new HMRModule({
// leftPad: () => leftPad,
// npmProgressBar33 () => npmProgressBar33,
// default: () => GuyFieriAsciiArt,
// [__hmr_ModuleIDSymbol]:
//});
// export { __hmr__exports.leftPad as leftPad, __hmr__ }
// -
// Then:
// if () {
//
// }
// pub fn maybeRewriteExportSymbol(p: *P, )
pub fn parseStmt(p: *P, opts: *ParseStatementOptions) anyerror!Stmt {
var loc = p.lexer.loc();

View File

@@ -301,12 +301,12 @@ pub fn NewLinker(comptime BundlerType: type) type {
// Change the import order so that any bundled imports appear last
// This is to make it so the bundle (which should be quite large) is least likely to block rendering
if (needs_bundle) {
const sorter = ImportStatementSorter{ .import_records = result.ast.import_records };
for (result.ast.parts) |*part, i| {
std.sort.sort(js_ast.Stmt, part.stmts, sorter, ImportStatementSorter.lessThan);
}
}
// if (needs_bundle) {
// const sorter = ImportStatementSorter{ .import_records = result.ast.import_records };
// for (result.ast.parts) |*part, i| {
// std.sort.sort(js_ast.Stmt, part.stmts, sorter, ImportStatementSorter.lessThan);
// }
// }
}
const ImportPathsList = allocators.BSSStringList(512, 128);

View File

@@ -540,7 +540,9 @@ pub const BundleOptions = struct {
loaders: std.StringHashMap(Loader),
resolve_dir: string = "/",
jsx: JSX.Pragma = JSX.Pragma{},
react_fast_refresh: bool = false,
hot_module_reloading: bool = false,
inject: ?[]string = null,
public_url: string = "",
public_dir: string = "public",
@@ -685,6 +687,7 @@ pub const BundleOptions = struct {
if (isWindows and opts.public_dir_handle != null) {
opts.public_dir_handle.?.close();
}
opts.hot_module_reloading = true;
}
if (opts.write and opts.output_dir.len > 0) {

View File

@@ -2,12 +2,12 @@ const options = @import("./options.zig");
usingnamespace @import("ast/base.zig");
usingnamespace @import("global.zig");
const std = @import("std");
pub const ProdSourceContent = @embedFile("./runtime.js");
pub const ProdSourceContent = @embedFile("./runtime.out.js");
pub const Runtime = struct {
pub fn sourceContent() string {
if (isDebug) {
var runtime_path = std.fs.path.join(std.heap.c_allocator, &[_]string{ std.fs.path.dirname(@src().file).?, "runtime.js" }) catch unreachable;
var runtime_path = std.fs.path.join(std.heap.c_allocator, &[_]string{ std.fs.path.dirname(@src().file).?, "runtime.out.js" }) catch unreachable;
const file = std.fs.openFileAbsolute(runtime_path, .{}) catch unreachable;
defer file.close();
return file.readToEndAlloc(std.heap.c_allocator, (file.stat() catch unreachable).size) catch unreachable;
@@ -19,7 +19,8 @@ pub const Runtime = struct {
pub fn version() string {
return version_hash;
}
pub const Features = packed struct {
pub const Features = struct {
react_fast_refresh: bool = false,
hot_module_reloading: bool = false,
keep_names_for_arrow_functions: bool = true,

391
src/runtime/hmr.ts Normal file
View File

@@ -0,0 +1,391 @@
import { ByteBuffer } from "peechy/bb";
import * as Schema from "../api/schema";
var runOnce = false;
var clientStartTime = 0;
function formatDuration(duration: number) {
return Math.round(duration * 100000) / 100;
}
export class Client {
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;
start() {
if (runOnce) {
console.warn(
"[speedy] Attempted to start HMR client multiple times. This may be a bug."
);
return;
}
runOnce = true;
this.connect();
}
connect() {
clientStartTime = performance.now();
this.socket = new WebSocket("/_api", ["speedy-hmr"]);
this.socket.binaryType = "arraybuffer";
this.socket.onclose = this.handleClose;
this.socket.onopen = this.handleOpen;
this.socket.onmessage = this.handleMessage;
}
// key: module id
// value: server-timestamp
builds = new Map<number, number>();
indexOfModuleId(id: number): number {
return Module.dependencies.graph.indexOf(id);
}
handleBuildFailure(buffer: ByteBuffer, timestamp: number) {
// 0: ID
// 1: Timestamp
const header_data = new Uint32Array(
buffer._data.buffer,
buffer._data.byteOffset,
buffer._data.byteOffset + 8
);
const index = this.indexOfModuleId(header_data[0]);
// 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 = Schema.decodeWebsocketMessageBuildFailure(buffer);
// TODO: finish this.
console.error("[speedy] Build failed", fail.module_path);
}
verbose = process.env.SPEEDY_HMR_VERBOSE;
handleBuildSuccess(buffer: ByteBuffer, timestamp: number) {
// 0: ID
// 1: Timestamp
const header_data = new Uint32Array(
buffer._data.buffer,
buffer._data.byteOffset,
buffer._data.byteOffset + 8
);
const index = this.indexOfModuleId(header_data[0]);
// Ignore builds of modules that are not loaded
if (index === -1) {
if (this.verbose) {
console.debug(
`[speedy] Skipping reload for unknown module id:`,
header_data[0]
);
}
return;
}
// Ignore builds of modules we expect a later version of
const currentVersion = this.builds.get(header_data[0]) || -Infinity;
if (currentVersion > header_data[1]) {
if (this.verbose) {
console.debug(
`[speedy] Ignoring module update for "${Module.dependencies.modules[index].url.pathname}" due to timestamp mismatch.\n Expected: >=`,
currentVersion,
`\n Received:`,
header_data[1]
);
}
return;
}
if (this.verbose) {
console.debug(
"[speedy] Preparing to reload",
Module.dependencies.modules[index].url.pathname
);
}
const build = Schema.decodeWebsocketMessageBuildSuccess(buffer);
var reload = new HotReload(header_data[0], index, build);
reload.timings.notify = timestamp - build.from_timestamp;
reload.run().then(
([module, timings]) => {
console.log(
`[speedy] Reloaded in ${formatDuration(timings.total)}ms :`,
module.url.pathname
);
},
(err) => {
console.error("[speedy] Hot Module Reload failed!", err);
debugger;
}
);
}
handleFileChangeNotification(buffer: ByteBuffer, timestamp: number) {
const notification =
Schema.decodeWebsocketMessageFileChangeNotification(buffer);
const index = Module.dependencies.graph.indexOf(notification.id);
if (index === -1) {
if (this.verbose) {
console.debug("[speedy] Unknown module changed, skipping");
}
return;
}
if ((this.builds.get(notification.id) || -Infinity) > timestamp) {
console.debug(
`[speedy] Received update for ${Module.dependencies.modules[index].url.pathname}`
);
return;
}
if (this.verbose) {
console.debug(
`[speedy] Requesting update for ${Module.dependencies.modules[index].url.pathname}`
);
}
this.builds.set(notification.id, timestamp);
this.buildCommandBuf[0] = Schema.WebsocketCommandKind.build;
this.buildCommandUArray[0] = timestamp;
this.buildCommandBuf.set(new Uint8Array(this.buildCommandUArray), 1);
this.buildCommandUArray[0] = notification.id;
this.buildCommandBuf.set(new Uint8Array(this.buildCommandUArray), 5);
this.socket.send(this.buildCommandBuf);
}
buildCommandBuf = new Uint8Array(9);
buildCommandUArray = new Uint32Array(1);
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 = Schema.decodeWebsocketMessage(message_header_byte_buffer);
const buffer = new ByteBuffer(
data.subarray(message_header_byte_buffer._index)
);
switch (header.kind) {
case Schema.WebsocketMessageKind.build_fail: {
this.handleBuildFailure(buffer, header.timestamp);
break;
}
case Schema.WebsocketMessageKind.build_success: {
this.handleBuildSuccess(buffer, header.timestamp);
break;
}
case Schema.WebsocketMessageKind.file_change_notification: {
this.handleFileChangeNotification(buffer, header.timestamp);
break;
}
case Schema.WebsocketMessageKind.welcome: {
const now = performance.now();
console.log(
"[speedy] HMR connected in",
formatDuration(now - clientStartTime),
"ms"
);
clientStartTime = now;
this.hasWelcomed = true;
const welcome = Schema.decodeWebsocketMessageWelcome(buffer);
this.epoch = welcome.epoch;
if (!this.epoch) {
console.warn("[speedy] Internal HMR error");
}
break;
}
}
};
handleClose = (event: CloseEvent) => {
if (this.reconnect !== 0) {
return;
}
this.reconnect = setInterval(this.connect, 500) as any as number;
console.warn("[speedy] HMR disconnected. Attempting to reconnect.");
};
}
class HotReload {
module_id: number = 0;
module_index: number = 0;
build: Schema.WebsocketMessageBuildSuccess;
timings = {
notify: 0,
decode: 0,
import: 0,
callbacks: 0,
total: 0,
start: 0,
};
constructor(
module_id: HotReload["module_id"],
module_index: HotReload["module_index"],
build: HotReload["build"]
) {
this.module_id = module_id;
this.module_index = module_index;
this.build = build;
}
async run(): Promise<[Module, HotReload["timings"]]> {
const importStart = performance.now();
let orig_deps = Module.dependencies;
Module.dependencies = orig_deps.fork(this.module_index);
var blobURL = null;
try {
const blob = new Blob([this.build.bytes], { type: "text/javascript" });
blobURL = URL.createObjectURL(blob);
await import(blobURL);
this.timings.import = performance.now() - importStart;
} catch (exception) {
Module.dependencies = orig_deps;
URL.revokeObjectURL(blobURL);
throw exception;
}
URL.revokeObjectURL(blobURL);
if (process.env.SPEEDY_HMR_VERBOSE) {
console.debug(
"[speedy] Re-imported",
Module.dependencies.modules[this.module_index].url.pathname,
"in",
formatDuration(this.timings.import),
". Running callbacks"
);
}
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* 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 do that in < 1ms easy!
for (let i = Module.dependencies.graph_used; i > this.module_index; i--) {
let handled = !Module.dependencies.modules[i].exports.__hmrDisable;
if (typeof Module.dependencies.modules[i].dispose === "function") {
Module.dependencies.modules[i].dispose();
handled = true;
}
if (typeof Module.dependencies.modules[i].accept === "function") {
Module.dependencies.modules[i].accept();
handled = true;
}
if (!handled) {
Module.dependencies.modules[i]._load();
}
}
} catch (exception) {
Module.dependencies = orig_deps;
throw exception;
}
this.timings.callbacks = performance.now() - callbacksStart;
if (process.env.SPEEDY_HMR_VERBOSE) {
console.debug(
"[speedy] Ran callbacks",
Module.dependencies.modules[this.module_index].url.pathname,
"in",
formatDuration(this.timings.callbacks),
"ms"
);
}
orig_deps = null;
this.timings.total =
this.timings.import + this.timings.callbacks + this.build.from_timestamp;
return Promise.resolve([
Module.dependencies.modules[this.module_index],
this.timings,
]);
}
}
var client: Client;
if ("SPEEDY_HMR_CLIENT" in globalThis) {
console.warn(
"[speedy] Attempted to load multiple copies of HMR. This may be a bug."
);
} else if (process.env.SPEEDY_HMR_ENABLED) {
client = new Client();
client.start();
globalThis.SPEEDY_HMR_CLIENT = client;
}
export class Module {
constructor(id: number, url: URL) {
// Ensure V8 knows this is a U32
this.id = id | 0;
this.url = url;
if (!Module._dependencies) {
Module.dependencies = Module._dependencies;
}
this.graph_index = Module.dependencies.graph_used++;
// Grow the dependencies graph
if (Module.dependencies.graph.length <= this.graph_index) {
const new_graph = new Uint32Array(Module.dependencies.graph.length * 4);
new_graph.set(Module.dependencies.graph);
Module.dependencies.graph = new_graph;
// In-place grow. This creates a holey array, which is bad, but less bad than pushing potentially 1000 times
Module.dependencies.modules.length = new_graph.length;
}
Module.dependencies.modules[this.graph_index] = this;
Module.dependencies.graph[this.graph_index] = this.id | 0;
}
additional_files = [];
// When a module updates, we need to re-initialize each dependent, recursively
// To do so:
// 1. Track which modules are imported by which *at runtime*
// 2. When A updates, loop through each dependent of A in insertion order
// 3. For each old dependent, call .dispose() if exists
// 3. For each new dependent, call .accept() if exists
// 4.
static _dependencies = {
modules: new Array<Module>(32),
graph: new Uint32Array(32),
graph_used: 0,
fork(offset: number) {
return {
modules: Module._dependencies.modules.slice(),
graph: Module._dependencies.graph.slice(),
graph_used: offset - 1,
};
},
};
static dependencies: Module["_dependencies"];
url: URL;
_load = function () {};
id = 0;
graph_index = 0;
_exports = {};
exports = {};
}

2
src/runtime/index.ts Normal file
View File

@@ -0,0 +1,2 @@
export * from "./hmr";
export * from "../runtime.js";