From df0d4a0960e94a99190e6a8ecd603b118115bed8 Mon Sep 17 00:00:00 2001 From: Pedro Cattori Date: Wed, 25 Jan 2023 15:41:46 -0500 Subject: [PATCH 01/66] wip --- packages/remix-dev/compiler/compileBrowser.ts | 57 ++++++++++++++++++- packages/remix-dev/compiler/remixCompiler.ts | 9 ++- packages/remix-dev/compiler/watch.ts | 18 ++++-- packages/remix-dev/devServer2.ts | 8 ++- packages/remix-dev/liveReload.ts | 6 +- 5 files changed, 85 insertions(+), 13 deletions(-) diff --git a/packages/remix-dev/compiler/compileBrowser.ts b/packages/remix-dev/compiler/compileBrowser.ts index 826310843e3..10f413d0fed 100644 --- a/packages/remix-dev/compiler/compileBrowser.ts +++ b/packages/remix-dev/compiler/compileBrowser.ts @@ -31,7 +31,9 @@ import invariant from "../invariant"; export type BrowserCompiler = { // produce ./public/build/ - compile: (manifestChannel: WriteChannel) => Promise; + compile: ( + manifestChannel: WriteChannel + ) => Promise; dispose: () => void; }; @@ -173,7 +175,7 @@ export const createBrowserCompiler = ( ): BrowserCompiler => { let appCompiler: esbuild.BuildIncremental; let cssCompiler: esbuild.BuildIncremental; - + let prevMetafile: esbuild.Metafile; let compile = async (manifestChannel: WriteChannel) => { let appBuildTask = async () => { appCompiler = await (!appCompiler @@ -188,6 +190,7 @@ export const createBrowserCompiler = ( appCompiler.metafile, "Expected app compiler metafile to be defined. This is likely a bug in Remix. Please open an issue at https://github.com/remix-run/remix/issues/new" ); + return appCompiler.metafile; }; let cssBuildTask = async () => { @@ -270,7 +273,11 @@ export const createBrowserCompiler = ( return cssBundlePath; }; - let [cssBundlePath] = await Promise.all([cssBuildTask(), appBuildTask()]); + let [cssBundlePath, metafile] = await Promise.all([ + cssBuildTask(), + appBuildTask(), + ]); + // TODO map outputs to public paths let manifest = await createAssetsManifest({ config: remixConfig, @@ -279,6 +286,50 @@ export const createBrowserCompiler = ( }); manifestChannel.write(manifest); await writeAssetsManifest(remixConfig, manifest); + + if (prevMetafile !== undefined) { + let create_input2output = ( + metafile: esbuild.Metafile + ): Record => { + let inputs = new Set(Object.keys(metafile.inputs)); + let input2output: Record = {}; + for (let [outputPath, output] of Object.entries(metafile.outputs)) { + for (let x of Object.keys(output.inputs)) { + if (inputs.has(x)) { + input2output[x] = outputPath; + } + } + } + return input2output; + }; + let updates = []; + let prev_i2o = create_input2output(prevMetafile); + let i2o = create_input2output(metafile); + console.log( + JSON.stringify( + { + prev_i2o, + i2o, + }, + undefined, + 2 + ) + ); + for (let input of Object.keys(metafile.inputs)) { + let prev_o = prev_i2o[input]; + let o = i2o[input]; + if (prev_o !== o) { + updates.push({ + // TODO replace /public/build with /build, but respect remix config settings for those + id: input, + url: "", + }); + } + } + prevMetafile = metafile; + return updates; + } + prevMetafile = metafile; }; return { diff --git a/packages/remix-dev/compiler/remixCompiler.ts b/packages/remix-dev/compiler/remixCompiler.ts index 4b7250cc87e..5c10f63cd87 100644 --- a/packages/remix-dev/compiler/remixCompiler.ts +++ b/packages/remix-dev/compiler/remixCompiler.ts @@ -28,15 +28,18 @@ export const compile = async ( options: { onCompileFailure?: OnCompileFailure; } = {} -): Promise => { +): Promise< + { assetsManifest?: AssetsManifest; hmrUpdates: unknown } | undefined +> => { try { let assetsManifestChannel = createChannel(); let browserPromise = compiler.browser.compile(assetsManifestChannel); let serverPromise = compiler.server.compile(assetsManifestChannel); - await Promise.all([browserPromise, serverPromise]); - return assetsManifestChannel.read(); + let [hmrUpdates] = await Promise.all([browserPromise, serverPromise]); + return { assetsManifest: await assetsManifestChannel.read(), hmrUpdates }; } catch (error: unknown) { options.onCompileFailure?.(error as Error); + return undefined; } }; diff --git a/packages/remix-dev/compiler/watch.ts b/packages/remix-dev/compiler/watch.ts index f8bad392d21..43d7165692f 100644 --- a/packages/remix-dev/compiler/watch.ts +++ b/packages/remix-dev/compiler/watch.ts @@ -23,7 +23,11 @@ function isEntryPoint(config: RemixConfig, file: string): boolean { export type WatchOptions = Partial & { reloadConfig?(root: string): Promise; onRebuildStart?(): void; - onRebuildFinish?(durationMs: number, assetsManifest?: AssetsManifest): void; + onRebuildFinish?( + durationMs: number, + assetsManifest?: AssetsManifest, + hmrEvent?: unknown + ): void; onFileCreated?(file: string): void; onFileChanged?(file: string): void; onFileDeleted?(file: string): void; @@ -77,15 +81,19 @@ export async function watch( } compiler = createRemixCompiler(config, options); - let assetsManifest = await compile(compiler, { onCompileFailure }); - onRebuildFinish?.(Date.now() - start, assetsManifest); + let { assetsManifest, hmrUpdates } = + (await compile(compiler, { onCompileFailure })) ?? {}; + onRebuildFinish?.(Date.now() - start, assetsManifest, hmrUpdates); }, 500); let rebuild = debounce(async () => { onRebuildStart?.(); let start = Date.now(); - let assetsManifest = await compile(compiler, { onCompileFailure }); - onRebuildFinish?.(Date.now() - start, assetsManifest); + let { assetsManifest, hmrUpdates } = + (await compile(compiler, { + onCompileFailure, + })) ?? {}; + onRebuildFinish?.(Date.now() - start, assetsManifest, hmrUpdates); }, 100); let toWatch = [config.appDirectory]; diff --git a/packages/remix-dev/devServer2.ts b/packages/remix-dev/devServer2.ts index 43772ead8f6..48f907b71e8 100644 --- a/packages/remix-dev/devServer2.ts +++ b/packages/remix-dev/devServer2.ts @@ -120,10 +120,16 @@ export let serve = async ( clean(config); socket.log("Rebuilding..."); }, - onRebuildFinish: async (durationMs, assetsManifest) => { + onRebuildFinish: async (durationMs, assetsManifest, hmrUpdates) => { if (!assetsManifest) return; socket.log(`Rebuilt in ${prettyMs(durationMs)}`); + console.log({ hmrUpdates }); + if (hmrUpdates) { + socket.hmr(hmrUpdates); + return; + } + info(`Waiting for ${appServerOrigin}...`); let start = Date.now(); await waitForAppServer(assetsManifest.version); diff --git a/packages/remix-dev/liveReload.ts b/packages/remix-dev/liveReload.ts index 81c16a34758..6905739df24 100644 --- a/packages/remix-dev/liveReload.ts +++ b/packages/remix-dev/liveReload.ts @@ -23,5 +23,9 @@ export let serve = (options: { port: number }) => { broadcast({ type: "LOG", message: _message }); }; - return { reload, log, close: wss.close }; + let hmr = (event: unknown) => { + log(`[HMR] sending event: ${JSON.stringify(event)}`); + }; + + return { reload, hmr, log, close: wss.close }; }; From 6a9a5ff9c8b3c53a35b7684ee4e52cf42e5202d6 Mon Sep 17 00:00:00 2001 From: Pedro Cattori Date: Wed, 25 Jan 2023 16:07:09 -0500 Subject: [PATCH 02/66] wip --- packages/remix-dev/compiler/compileBrowser.ts | 25 ++-- .../remix-dev/compiler/plugins/hmrPlugin.ts | 107 ++++++++++++++++++ 2 files changed, 120 insertions(+), 12 deletions(-) create mode 100644 packages/remix-dev/compiler/plugins/hmrPlugin.ts diff --git a/packages/remix-dev/compiler/compileBrowser.ts b/packages/remix-dev/compiler/compileBrowser.ts index 10f413d0fed..703110af24f 100644 --- a/packages/remix-dev/compiler/compileBrowser.ts +++ b/packages/remix-dev/compiler/compileBrowser.ts @@ -125,6 +125,15 @@ const createEsbuildConfig = ( NodeModulesPolyfillPlugin(), ].filter(isNotNull); + if (mode === "development") { + // TODO do this for all deps in package.json + entryPoints["react"] = "react"; + entryPoints["react-dom"] = "react-dom"; + entryPoints["react-dom/client"] = "react-dom/client"; + entryPoints["react-refresh/runtime"] = "react-refresh/runtime"; + entryPoints["remix:hmr"] = "remix:hmr"; + } + return { entryPoints, outdir: config.assetsBuildDirectory, @@ -305,24 +314,16 @@ export const createBrowserCompiler = ( let updates = []; let prev_i2o = create_input2output(prevMetafile); let i2o = create_input2output(metafile); - console.log( - JSON.stringify( - { - prev_i2o, - i2o, - }, - undefined, - 2 - ) - ); for (let input of Object.keys(metafile.inputs)) { let prev_o = prev_i2o[input]; let o = i2o[input]; if (prev_o !== o) { + let url = + remixConfig.publicPath + + path.relative(remixConfig.assetsBuildDirectory, path.resolve(o)); updates.push({ - // TODO replace /public/build with /build, but respect remix config settings for those id: input, - url: "", + url, }); } } diff --git a/packages/remix-dev/compiler/plugins/hmrPlugin.ts b/packages/remix-dev/compiler/plugins/hmrPlugin.ts new file mode 100644 index 00000000000..f0c4df1f54b --- /dev/null +++ b/packages/remix-dev/compiler/plugins/hmrPlugin.ts @@ -0,0 +1,107 @@ +import type { Plugin } from "esbuild"; + +export let hmrPlugin = (): Plugin => { + return { + name: "remix-hmr", + setup: (build) => { + build.onResolve({ filter: /^remix:hmr$/ }, (args) => { + return { + namespace: "remix-hmr", + path: args.path, + }; + }); + build.onLoad({ filter: /.*/, namespace: "remix-hmr" }, () => { + let contents = ` + if (!window.__hmr__) { + window.__hmr__ = { + contexts: {}, + }; + + const socketURL = new URL( + "/__hmr__", + window.location.href.replace(/^http(s)?:/, "ws$1:") + ); + const socket = (window.__hmr__.socket = new WebSocket(socketURL.href)); + socket.addEventListener("message", async (event) => { + const payload = JSON.parse(event.data); + + switch (payload?.type) { + case "reload": + window.location.reload(); + break; + case "hmr": + if (!payload.updates?.length) return; + + let anyAccepted = false; + for (const update of payload.updates) { + if (window.__hmr__.contexts[update.id]) { + const accepted = window.__hmr__.contexts[update.id].emit( + await import(update.url + "?t=" + Date.now()) + ); + if (accepted) { + console.log("[HMR] Updated accepted by", update.id); + anyAccepted = true; + } + } + } + + if (!anyAccepted) { + console.log("[HMR] Updated rejected, reloading..."); + window.location.reload(); + } + break; + } + }); +} + +export function createHotContext(id: string): ImportMetaHot { + let callback: undefined | ((mod: ModuleNamespace) => void); + let disposed = false; + + const hot = { + accept: (cb) => { + if (disposed) { + throw new Error("import.meta.hot.accept() called after dispose()"); + } + if (callback) { + throw new Error("import.meta.hot.accept() already called"); + } + callback = cb; + }, + dispose: () => { + disposed = true; + callback = undefined; + }, + emit(self: ModuleNamespace) { + if (disposed) { + throw new Error("import.meta.hot.emit() called after dispose()"); + } + + if (callback) { + callback(self); + return true; + } + return false; + }, + }; + + if (window.__hmr__.contexts[id]) { + window.__hmr__.contexts[id].dispose(); + window.__hmr__.contexts[id] = undefined; + } + window.__hmr__.contexts[id] = hot; + + return hot; +} + +declare global { + interface Window { + __hmr__: any; + } +} + `; + return { loader: "ts", contents }; + }); + }, + }; +}; From 44c1fbb95375fb77bcc7e96ee9cfb6ef1f3c2d4a Mon Sep 17 00:00:00 2001 From: Pedro Cattori Date: Wed, 25 Jan 2023 16:23:38 -0500 Subject: [PATCH 03/66] wip --- packages/remix-dev/compiler/compileBrowser.ts | 2 ++ packages/remix-dev/package.json | 2 ++ packages/remix-react/components.tsx | 10 +++++++ yarn.lock | 29 +++++++++++++++++++ 4 files changed, 43 insertions(+) diff --git a/packages/remix-dev/compiler/compileBrowser.ts b/packages/remix-dev/compiler/compileBrowser.ts index 703110af24f..a0b15520e9f 100644 --- a/packages/remix-dev/compiler/compileBrowser.ts +++ b/packages/remix-dev/compiler/compileBrowser.ts @@ -28,6 +28,7 @@ import { } from "./plugins/cssBundleEntryModulePlugin"; import { writeFileSafe } from "./utils/fs"; import invariant from "../invariant"; +import { hmrPlugin } from "./plugins/hmrPlugin"; export type BrowserCompiler = { // produce ./public/build/ @@ -132,6 +133,7 @@ const createEsbuildConfig = ( entryPoints["react-dom/client"] = "react-dom/client"; entryPoints["react-refresh/runtime"] = "react-refresh/runtime"; entryPoints["remix:hmr"] = "remix:hmr"; + plugins.push(hmrPlugin()); } return { diff --git a/packages/remix-dev/package.json b/packages/remix-dev/package.json index 5ab257046d1..1c9e613a4c7 100644 --- a/packages/remix-dev/package.json +++ b/packages/remix-dev/package.json @@ -44,6 +44,7 @@ "fs-extra": "^10.0.0", "get-port": "^5.1.1", "gunzip-maybe": "^1.4.2", + "http-proxy": "^1.18.1", "inquirer": "^8.2.1", "jsesc": "3.0.2", "json5": "^2.2.1", @@ -60,6 +61,7 @@ "prettier": "2.7.1", "pretty-ms": "^7.0.1", "proxy-agent": "^5.0.0", + "react-refresh": "^0.14.0", "recast": "^0.21.5", "remark-frontmatter": "4.0.1", "remark-mdx-frontmatter": "^1.0.1", diff --git a/packages/remix-react/components.tsx b/packages/remix-react/components.tsx index 272119db822..05d47b137c2 100644 --- a/packages/remix-react/components.tsx +++ b/packages/remix-react/components.tsx @@ -902,6 +902,7 @@ export function Scripts(props: ScriptProps) { ? `__remixContext.a=${deferredScripts.length};` : ""); + // TODO: change to be dynamic imports for routes let routeModulesScript = !isStatic ? " " : `${matches @@ -1547,6 +1548,15 @@ function convertRouterFetcherToRemixFetcher( return fetcher; } +export let Hmr = () => { + let [hydrated, setHydrated] = React.useState(false); + React.useEffect(() => { + setHydrated(true); + }, []); + if (!hydrated) return null; + return