From 7bdcb9ca060f1421a7a29531235271f00cbb1c3d Mon Sep 17 00:00:00 2001 From: Pedro Cattori Date: Thu, 4 May 2023 16:48:58 -0400 Subject: [PATCH] wip --- packages/remix-dev/compiler/compiler.ts | 1 - packages/remix-dev/compiler/watch.ts | 2 +- packages/remix-dev/devServer_unstable/hdr.ts | 106 ++++++++++++++++++ packages/remix-dev/devServer_unstable/hmr.ts | 12 +- .../remix-dev/devServer_unstable/index.ts | 16 ++- 5 files changed, 124 insertions(+), 13 deletions(-) create mode 100644 packages/remix-dev/devServer_unstable/hdr.ts diff --git a/packages/remix-dev/compiler/compiler.ts b/packages/remix-dev/compiler/compiler.ts index 83fb44b9969..ce3fb3009da 100644 --- a/packages/remix-dev/compiler/compiler.ts +++ b/packages/remix-dev/compiler/compiler.ts @@ -93,7 +93,6 @@ export let create = async (ctx: Context): Promise => { } // js compilation (implicitly writes artifacts/js) - // TODO: js task should not return metafile, but rather js assets let js = await tasks.js; if (!js.ok) throw error ?? js.error; let { metafile, hmr } = js.value; diff --git a/packages/remix-dev/compiler/watch.ts b/packages/remix-dev/compiler/watch.ts index d8723aebed5..0cede310af3 100644 --- a/packages/remix-dev/compiler/watch.ts +++ b/packages/remix-dev/compiler/watch.ts @@ -57,7 +57,6 @@ export async function watch( onBuildFinish?.(ctx, Date.now() - start, manifest !== undefined); let restart = debounce(async () => { - onBuildStart?.(ctx); let start = Date.now(); compiler.dispose(); @@ -67,6 +66,7 @@ export async function watch( logThrown(thrown); return; } + onBuildStart?.(ctx); compiler = await Compiler.create(ctx); let manifest = await compile(); diff --git a/packages/remix-dev/devServer_unstable/hdr.ts b/packages/remix-dev/devServer_unstable/hdr.ts new file mode 100644 index 00000000000..3ec3d40e90f --- /dev/null +++ b/packages/remix-dev/devServer_unstable/hdr.ts @@ -0,0 +1,106 @@ +import * as path from "node:path"; +import esbuild from "esbuild"; + +import type { Context } from "../compiler/context"; +import { emptyModulesPlugin } from "../compiler/plugins/emptyModules"; +import { externalPlugin } from "../compiler/plugins/external"; +import { getRouteModuleExports } from "../compiler/utils/routeExports"; +import { createMatchPath } from "../compiler/utils/tsconfig"; +import invariant from "../invariant"; + +function isBareModuleId(id: string): boolean { + return !id.startsWith("node:") && !id.startsWith(".") && !path.isAbsolute(id); +} + +type Route = Context["config"]["routes"][string]; + +export let detectLoaderChanges = async (ctx: Context) => { + let entryPoints: Record = {}; + for (let id of Object.keys(ctx.config.routes)) { + entryPoints[id] = ctx.config.routes[id].file + "?loader"; + } + let options: esbuild.BuildOptions = { + bundle: true, + entryPoints: entryPoints, + treeShaking: true, + metafile: true, + outdir: ".", + write: false, + entryNames: "[hash]", + plugins: [ + { + name: "hmr-loader", + setup(build) { + let routesByFile: Map = Object.keys( + ctx.config.routes + ).reduce((map, key) => { + let route = ctx.config.routes[key]; + map.set(route.file, route); + return map; + }, new Map()); + let filter = /\?loader$/; + build.onResolve({ filter }, (args) => { + return { path: args.path, namespace: "hmr-loader" }; + }); + build.onLoad({ filter, namespace: "hmr-loader" }, async (args) => { + let file = args.path.replace(filter, ""); + let route = routesByFile.get(file); + invariant(route, `Cannot get route by path: ${args.path}`); + let theExports = await getRouteModuleExports(ctx.config, route.id); + let contents = "module.exports = {};"; + if (theExports.includes("loader")) { + contents = `export { loader } from ${JSON.stringify( + `./${file}` + )};`; + } + return { + contents, + resolveDir: ctx.config.appDirectory, + loader: "js", + }; + }); + }, + }, + externalPlugin(/^node:.*/, { sideEffects: false }), + externalPlugin(/\.css$/, { sideEffects: false }), + externalPlugin(/^https?:\/\//, { sideEffects: false }), + emptyModulesPlugin(ctx, /\.client(\.[jt]sx?)?$/), + { + name: "hmr-bare-modules", + setup(build) { + let matchPath = ctx.config.tsconfigPath + ? createMatchPath(ctx.config.tsconfigPath) + : undefined; + function resolvePath(id: string) { + if (!matchPath) return id; + return ( + matchPath(id, undefined, undefined, [ + ".ts", + ".tsx", + ".js", + ".jsx", + ]) || id + ); + } + build.onResolve({ filter: /.*/ }, (args) => { + if (!isBareModuleId(resolvePath(args.path))) { + return undefined; + } + return { path: args.path, external: true }; + }); + }, + }, + ], + }; + + let { metafile } = await esbuild.build(options); + let entries = Object.entries(metafile!.outputs).map( + ([hashjs, { entryPoint }]) => { + let file = entryPoint + ?.replace(/^hmr-loader:/, "") + ?.replace(/\?loader$/, ""); + return [file, hashjs.replace(/\.js$/, "")]; + } + ); + return Object.fromEntries(entries); +}; diff --git a/packages/remix-dev/devServer_unstable/hmr.ts b/packages/remix-dev/devServer_unstable/hmr.ts index cc6a8f3aa4d..eed235112a8 100644 --- a/packages/remix-dev/devServer_unstable/hmr.ts +++ b/packages/remix-dev/devServer_unstable/hmr.ts @@ -16,12 +16,10 @@ export type Update = { export let updates = ( config: RemixConfig, manifest: Manifest, - prevManifest: Manifest + prevManifest: Manifest, + hdr: Record, + prevHdr?: Record ): Update[] => { - // TODO: probably want another map to correlate every input file to the - // routes that consume it - // ^check if route chunk hash changes when its dependencies change, even in different chunks - let updates: Update[] = []; for (let [routeId, route] of Object.entries(manifest.routes)) { let prevRoute = prevManifest.routes[routeId] as typeof route | undefined; @@ -43,8 +41,8 @@ export let updates = ( } // when loaders are diff - let loaderHash = manifest.hmr?.routes[moduleId]?.loaderHash; - let prevLoaderHash = prevManifest.hmr?.routes[moduleId]?.loaderHash; + let loaderHash = hdr[file]; + let prevLoaderHash = prevHdr?.[file]; if (loaderHash !== prevLoaderHash) { updates.push({ id: moduleId, diff --git a/packages/remix-dev/devServer_unstable/index.ts b/packages/remix-dev/devServer_unstable/index.ts index 05a427229ea..c8248ec8fb9 100644 --- a/packages/remix-dev/devServer_unstable/index.ts +++ b/packages/remix-dev/devServer_unstable/index.ts @@ -14,6 +14,7 @@ import * as Socket from "./socket"; import * as HMR from "./hmr"; import { warnOnce } from "../warnOnce"; import { detectPackageManager } from "../cli/detectPackageManager"; +import * as HDR from "./hdr"; type Origin = { scheme: string; @@ -58,6 +59,8 @@ export let serve = async ( manifest?: Manifest; prevManifest?: Manifest; appReady?: Channel.Type; + hdr?: Promise>; + prevLoaderHashes?: Record; } = {}; let bin = await detectBin(); @@ -115,10 +118,12 @@ export let serve = async ( }, }, { - onBuildStart: (ctx) => { + onBuildStart: async (ctx) => { state.appReady?.err(); clean(ctx.config); websocket.log(state.prevManifest ? "Rebuilding..." : "Building..."); + + state.hdr = HDR.detectLoaderChanges(ctx); }, onBuildManifest: (manifest: Manifest) => { state.manifest = manifest; @@ -143,14 +148,16 @@ export let serve = async ( } let { ok } = await state.appReady.result; // result not ok -> new build started before this one finished. do not process outdated manifest + let loaderHashes = await state.hdr; if (ok) { console.log(`App server took ${prettyMs(Date.now() - start)}`); - - if (state.manifest?.hmr && state.prevManifest) { + if (state.manifest && loaderHashes && state.prevManifest) { let updates = HMR.updates( ctx.config, state.manifest, - state.prevManifest + state.prevManifest, + loaderHashes, + state.prevLoaderHashes ); websocket.hmr(state.manifest, updates); @@ -162,6 +169,7 @@ export let serve = async ( } } state.prevManifest = state.manifest; + state.prevLoaderHashes = loaderHashes; }, onFileCreated: (file) => websocket.log(`File created: ${relativePath(file)}`),