Skip to content

Commit

Permalink
ts HMR runtime (#38)
Browse files Browse the repository at this point in the history
  • Loading branch information
ForsakenHarmony authored Oct 18, 2022
1 parent 793262f commit ff7c02a
Show file tree
Hide file tree
Showing 84 changed files with 5,059 additions and 4,964 deletions.
11 changes: 9 additions & 2 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ module.exports = {
"target",
"dist",
"node_modules",
"crates",
"crates/*/tests",
"crates/*/benches",
"packages/create-turbo/templates",
"packages/turbo-tracing-next-plugin/test/with-mongodb-mongoose",
],
Expand All @@ -20,10 +21,16 @@ module.exports = {
},
overrides: [
{
files: ["./docs/theme.config.js"],
files: ["docs/theme.config.js"],
rules: {
"react-hooks/rules-of-hooks": "off",
},
},
{
files: "crates/*/js/**",
rules: {
"prefer-const": "error",
},
},
],
};
22 changes: 22 additions & 0 deletions crates/next-core/js/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{
"name": "@vercel/turbopack-next",
"version": "0.0.0",
"description": "turbopack next runtime",
"license": "UNLICENSED",
"private": true,
"scripts": {
"check": "tsc --noEmit"
},
"dependencies": {
"@next/react-refresh-utils": "^12.2.5",
"@vercel/turbopack-runtime": "latest",
"next": "^12.3.1",
"react": "^18.2.0",
"react-dom": "^18.2.0"
},
"devDependencies": {
"@types/node": "^18.8.4",
"@types/react": "^18.0.21",
"@types/react-dom": "^18.0.6"
}
}
10 changes: 10 additions & 0 deletions crates/next-core/js/src/dev/client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { connect } from "./hmr-client";
import { connectHMR } from "./websocket";

export function initializeHMR(options: { assetPrefix: string }) {
connect();
connectHMR({
path: "/turbopack-hmr",
assetPrefix: options.assetPrefix,
});
}
123 changes: 123 additions & 0 deletions crates/next-core/js/src/dev/hmr-client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
import type {
ClientMessage,
ServerMessage,
} from "@vercel/turbopack-runtime/types/protocol";
import type {
ChunkId,
ChunkUpdateCallback,
TurbopackGlobals,
} from "@vercel/turbopack-runtime/types";

import { addEventListener, sendMessage } from "./websocket";

declare var globalThis: TurbopackGlobals;

export function connect() {
addEventListener((event) => {
switch (event.type) {
case "connected":
handleSocketConnected();
break;
case "message":
handleSocketMessage(event.message);
break;
}
});

const queued = globalThis.TURBOPACK_CHUNK_UPDATE_LISTENERS;
if (queued != null && !Array.isArray(queued)) {
throw new Error("A separate HMR handler was already registered");
}
globalThis.TURBOPACK_CHUNK_UPDATE_LISTENERS = {
push: ([chunkId, callback]: [ChunkId, ChunkUpdateCallback]) => {
onChunkUpdate(chunkId, callback);
},
};

if (Array.isArray(queued)) {
for (const [chunkId, callback] of queued) {
onChunkUpdate(chunkId, callback);
}
}

subscribeToInitialCssChunksUpdates();
}

const chunkUpdateCallbacks: Map<ChunkId, ChunkUpdateCallback[]> = new Map();

function sendJSON(message: ClientMessage) {
sendMessage(JSON.stringify(message));
}

function subscribeToChunkUpdates(chunkId: ChunkId) {
sendJSON({
type: "subscribe",
chunkId,
});
}

function handleSocketConnected() {
for (const chunkId of chunkUpdateCallbacks.keys()) {
subscribeToChunkUpdates(chunkId);
}
}

function handleSocketMessage(event: MessageEvent) {
const data: ServerMessage = JSON.parse(event.data);

triggerChunkUpdate(data);
}

export function onChunkUpdate(chunkId: ChunkId, callback: ChunkUpdateCallback) {
const callbacks = chunkUpdateCallbacks.get(chunkId);
if (!callbacks) {
chunkUpdateCallbacks.set(chunkId, [callback]);
} else {
callbacks.push(callback);
}

subscribeToChunkUpdates(chunkId);
}

function triggerChunkUpdate(update: ServerMessage) {
const callbacks = chunkUpdateCallbacks.get(update.chunkId);
if (!callbacks) {
return;
}

try {
for (const callback of callbacks) {
callback(update);
}
} catch (err) {
console.error(
`An error occurred during the update of chunk \`${update.chunkId}\``,
err
);
location.reload();
}
}

// Unlike ES chunks, CSS chunks cannot contain the logic to accept updates.
// They must be reloaded here instead.
function subscribeToInitialCssChunksUpdates() {
const initialCssChunkLinks: NodeListOf<HTMLLinkElement> =
document.head.querySelectorAll("link[data-turbopack-chunk-id]");
initialCssChunkLinks.forEach((link) => {
const chunkId = link.dataset.turbopackChunkId!;

onChunkUpdate(chunkId, (update) => {
switch (update.type) {
case "restart": {
console.info(`Reloading CSS chunk \`${chunkId}\``);
link.replaceWith(link);
break;
}
case "partial":
throw new Error(`partial CSS chunk updates are not supported`);
default:
throw new Error(`unknown update type \`${update}\``);
}
});
});
}
106 changes: 106 additions & 0 deletions crates/next-core/js/src/dev/websocket.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
// Adapted from https://github.com/vercel/next.js/blob/canary/packages/next/client/dev/error-overlay/websocket.ts

let source: WebSocket;
const eventCallbacks: ((event: WebsocketEvent) => void)[] = [];

// TODO: add timeout again
// let lastActivity = Date.now()

function getSocketProtocol(assetPrefix: string): string {
let protocol = location.protocol;

try {
// assetPrefix is a url
protocol = new URL(assetPrefix).protocol;
} catch (_) {}

return protocol === "http:" ? "ws" : "wss";
}

type WebsocketEvent =
| {
type: "connected";
}
| {
type: "message";
message: MessageEvent;
};

export function addEventListener(cb: (event: WebsocketEvent) => void) {
eventCallbacks.push(cb);
}

export function sendMessage(data: any) {
if (!source || source.readyState !== source.OPEN) return;
return source.send(data);
}

export type HMROptions = {
path: string;
assetPrefix: string;
timeout?: number;
log?: boolean;
};

export function connectHMR(options: HMROptions) {
const { timeout = 5 * 1000 } = options;

function init() {
if (source) source.close();

function handleOnline() {
eventCallbacks.forEach((cb) => {
cb({
type: "connected",
});
});

if (options.log) console.log("[HMR] connected");
// lastActivity = Date.now()
}

function handleMessage(event: MessageEvent) {
// lastActivity = Date.now()

eventCallbacks.forEach((cb) => {
cb({
type: "message",
message: event,
});
});
}

// let timer: NodeJS.Timeout

function handleDisconnect() {
// clearInterval(timer)
source.close();
setTimeout(init, timeout);
}

// timer = setInterval(function() {
// if (Date.now() - lastActivity > timeout) {
// handleDisconnect()
// }
// }, timeout / 2)

const { hostname, port } = location;
const protocol = getSocketProtocol(options.assetPrefix || "");
const assetPrefix = options.assetPrefix.replace(/^\/+/, "");

let url = `${protocol}://${hostname}:${port}${
assetPrefix ? `/${assetPrefix}` : ""
}`;

if (assetPrefix.startsWith("http")) {
url = `${protocol}://${assetPrefix.split("://")[1]}`;
}

source = new window.WebSocket(`${url}${options.path}`);
source.onopen = handleOnline;
source.onerror = handleDisconnect;
source.onmessage = handleMessage;
}

init();
}
4 changes: 4 additions & 0 deletions crates/next-core/js/src/entry/index.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
// this file is just here to make typescript happy about the wrapped/virtual assets (import ".")

declare var Anything: any;
export = Anything;
Original file line number Diff line number Diff line change
@@ -1,20 +1,27 @@
import "@vercel/turbopack-next/internal/shims";

import { initialize, hydrate } from "next/dist/client";
import { initializeHMR } from "@vercel/turbopack-next/dev/client";

import * as _app from "@vercel/turbopack-next/pages/_app";
import * as page from ".";

(async () => {
console.debug("Initializing Next.js");

initializeHMR({
assetPrefix: "",
});

await initialize({
webpackHMR: {
// Expected when `process.env.NODE_ENV === 'development'`
onUnrecoverableError() {},
},
});

__NEXT_P.push(["/_app", () => _app]);
__NEXT_P.push([window.__NEXT_DATA__.page, () => page]);
window.__NEXT_P.push(["/_app", () => _app]);
window.__NEXT_P.push([window.__NEXT_DATA__.page, () => page]);

console.debug("Hydrating the page");

Expand Down
Loading

0 comments on commit ff7c02a

Please sign in to comment.