diff --git a/.changeset/afraid-coats-count.md b/.changeset/afraid-coats-count.md new file mode 100644 index 00000000000..b6c06cc7ce8 --- /dev/null +++ b/.changeset/afraid-coats-count.md @@ -0,0 +1,6 @@ +--- +"@remix-run/dev": patch +"@remix-run/react": patch +--- + +cross-module loader change detection for hdr diff --git a/integration/hmr-log-test.ts b/integration/hmr-log-test.ts index b873ab5253e..cdb02556ed4 100644 --- a/integration/hmr-log-test.ts +++ b/integration/hmr-log-test.ts @@ -125,6 +125,11 @@ let fixture = (options: { ...cssBundleHref ? [{ rel: "stylesheet", href: cssBundleHref }] : [], ]; + // dummy loader to make sure that HDR is granular + export const loader = () => { + return null; + }; + export default function Root() { return ( @@ -141,6 +146,7 @@ let fixture = (options: { @@ -179,6 +185,18 @@ let fixture = (options: { ) } `, + "app/routes/mdx.mdx": `import { useLoaderData } from '@remix-run/react' +export const loader = () => "crazy" +export const Component = () => { + const data = useLoaderData() + return

{data}

+} + +# heyo +whatsup + + +`, "app/components/counter.tsx": js` import * as React from "react"; @@ -272,6 +290,8 @@ test("HMR", async ({ page }) => { let originalCounter = fs.readFileSync(counterPath, "utf8"); let cssModulePath = path.join(projectDir, "app", "styles.module.css"); let originalCssModule = fs.readFileSync(cssModulePath, "utf8"); + let mdxPath = path.join(projectDir, "app", "routes", "mdx.mdx"); + let originalMdx = fs.readFileSync(mdxPath, "utf8"); // make content and style changed to index route let newCssModule = ` @@ -419,9 +439,30 @@ test("HMR", async ({ page }) => { `#about-counter:has-text("inc 0")` ); - // This should not have triggered any revalidation but our detection is - // failing for x-module changes for route module imports - // expect(dataRequests).toBe(2); + expect(dataRequests).toBe(2); + + // mdx + await page.click(`a[href="/mdx"]`); + await page.waitForSelector(`#crazy`); + let mdx = `import { useLoaderData } from '@remix-run/react' +export const loader = () => "hot" +export const Component = () => { + const data = useLoaderData() + return

{data}

+} + +# heyo +whatsup + + +`; + fs.writeFileSync(mdxPath, mdx); + await page.waitForSelector(`#hot`); + expect(dataRequests).toBe(4); + + fs.writeFileSync(mdxPath, originalMdx); + await page.waitForSelector(`#crazy`); + expect(dataRequests).toBe(5); } catch (e) { console.log("stdout begin -----------------------"); console.log(devStdout()); diff --git a/integration/hmr-test.ts b/integration/hmr-test.ts index 1f54f4b6319..b3c418906e7 100644 --- a/integration/hmr-test.ts +++ b/integration/hmr-test.ts @@ -125,6 +125,11 @@ let fixture = (options: { ...cssBundleHref ? [{ rel: "stylesheet", href: cssBundleHref }] : [], ]; + // dummy loader to make sure that HDR is granular + export const loader = () => { + return null; + }; + export default function Root() { return ( @@ -141,6 +146,7 @@ let fixture = (options: { @@ -179,7 +185,18 @@ let fixture = (options: { ) } `, - + "app/routes/mdx.mdx": `import { useLoaderData } from '@remix-run/react' +export const loader = () => "crazy" +export const Component = () => { + const data = useLoaderData() + return

{data}

+} + +# heyo +whatsup + + +`, "app/components/counter.tsx": js` import * as React from "react"; export default function Counter({ id }) { @@ -272,6 +289,8 @@ test("HMR", async ({ page }) => { let originalCounter = fs.readFileSync(counterPath, "utf8"); let cssModulePath = path.join(projectDir, "app", "styles.module.css"); let originalCssModule = fs.readFileSync(cssModulePath, "utf8"); + let mdxPath = path.join(projectDir, "app", "routes", "mdx.mdx"); + let originalMdx = fs.readFileSync(mdxPath, "utf8"); // make content and style changed to index route let newCssModule = ` @@ -419,9 +438,30 @@ test("HMR", async ({ page }) => { `#about-counter:has-text("inc 0")` ); - // This should not have triggered any revalidation but our detection is - // failing for x-module changes for route module imports - // expect(dataRequests).toBe(2); + expect(dataRequests).toBe(2); + + // mdx + await page.click(`a[href="/mdx"]`); + await page.waitForSelector(`#crazy`); + let mdx = `import { useLoaderData } from '@remix-run/react' +export const loader = () => "hot" +export const Component = () => { + const data = useLoaderData() + return

{data}

+} + +# heyo +whatsup + + +`; + fs.writeFileSync(mdxPath, mdx); + await page.waitForSelector(`#hot`); + expect(dataRequests).toBe(4); + + fs.writeFileSync(mdxPath, originalMdx); + await page.waitForSelector(`#crazy`); + expect(dataRequests).toBe(5); } catch (e) { console.log("stdout begin -----------------------"); console.log(devStdout()); diff --git a/packages/remix-dev/__tests__/cli-test.ts b/packages/remix-dev/__tests__/cli-test.ts index ba57c100664..0ce72093343 100644 --- a/packages/remix-dev/__tests__/cli-test.ts +++ b/packages/remix-dev/__tests__/cli-test.ts @@ -122,7 +122,7 @@ describe("remix CLI", () => { --http-host HTTP(S) host for the dev server. Default: localhost --http-port HTTP(S) port for the dev server. Default: any open port --no-restart Do not restart the app server when rebuilds occur. - --websocket-port Websocket port for the dev server. Default: any open port + --websocket-port WebSocket port for the dev server. Default: any open port \`init\` Options: --no-delete Skip deleting the \`remix.init\` script \`routes\` Options: diff --git a/packages/remix-dev/cli/commands.ts b/packages/remix-dev/cli/commands.ts index 252449b9b75..200a7a81dc6 100644 --- a/packages/remix-dev/cli/commands.ts +++ b/packages/remix-dev/cli/commands.ts @@ -183,7 +183,7 @@ export async function build( host: dev.httpHost, port: dev.httpPort, }; - options.devWebsocketPort = dev.websocketPort; + options.devWebSocketPort = dev.webSocketPort; } fse.emptyDirSync(config.assetsBuildDirectory); @@ -475,7 +475,7 @@ type DevBuildFlags = { httpScheme: string; httpHost: string; httpPort: number; - websocketPort: number; + webSocketPort: number; }; let resolveDevBuild = async ( config: RemixConfig, @@ -500,16 +500,16 @@ let resolveDevBuild = async ( (dev === true ? undefined : dev.httpPort) ?? (await findPort()); // prettier-ignore - let websocketPort = - flags.websocketPort ?? - (dev === true ? undefined : dev.websocketPort) ?? + let webSocketPort = + flags.webSocketPort ?? + (dev === true ? undefined : dev.webSocketPort) ?? (await findPort()); return { httpScheme, httpHost, httpPort, - websocketPort, + webSocketPort, }; }; @@ -524,7 +524,7 @@ let resolveDevServe = async ( let dev = config.future.unstable_dev; if (dev === false) throw Error("Cannot resolve dev options"); - let { httpScheme, httpHost, httpPort, websocketPort } = await resolveDevBuild( + let { httpScheme, httpHost, httpPort, webSocketPort } = await resolveDevBuild( config, flags ); @@ -561,7 +561,7 @@ let resolveDevServe = async ( httpScheme, httpHost, httpPort, - websocketPort, + webSocketPort, restart, }; }; diff --git a/packages/remix-dev/cli/run.ts b/packages/remix-dev/cli/run.ts index c4448f122ca..524b1bdc844 100644 --- a/packages/remix-dev/cli/run.ts +++ b/packages/remix-dev/cli/run.ts @@ -48,7 +48,7 @@ ${colors.logoBlue("R")} ${colors.logoGreen("E")} ${colors.logoYellow( --http-host HTTP(S) host for the dev server. Default: localhost --http-port HTTP(S) port for the dev server. Default: any open port --no-restart Do not restart the app server when rebuilds occur. - --websocket-port Websocket port for the dev server. Default: any open port + --websocket-port WebSocket port for the dev server. Default: any open port \`init\` Options: --no-delete Skip deleting the \`remix.init\` script \`routes\` Options: @@ -226,7 +226,7 @@ export async function run(argv: string[] = process.argv.slice(2)) { delete flags["http-port"]; } if (flags["websocket-port"]) { - flags.websocketPort = flags["websocket-port"]; + flags.webSocketPort = flags["websocket-port"]; delete flags["websocket-port"]; } 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/js/compiler.ts b/packages/remix-dev/compiler/js/compiler.ts index e2bed448f27..d55e9ff98f8 100644 --- a/packages/remix-dev/compiler/js/compiler.ts +++ b/packages/remix-dev/compiler/js/compiler.ts @@ -72,7 +72,6 @@ const getExternals = (remixConfig: RemixConfig): string[] => { const createEsbuildConfig = ( ctx: Context, - onLoader: (filename: string, code: string) => void, channels: { cssBundleHref: Channel.Type } ): esbuild.BuildOptions => { let entryPoints: Record = { @@ -142,7 +141,7 @@ const createEsbuildConfig = ( absoluteCssUrlsPlugin(), externalPlugin(/^https?:\/\//, { sideEffects: false }), ctx.config.future.unstable_dev - ? browserRouteModulesPlugin_v2(ctx, routeModulePaths, onLoader) + ? browserRouteModulesPlugin_v2(ctx, routeModulePaths) : browserRouteModulesPlugin(ctx, /\?browser$/), mdxPlugin(ctx), emptyModulesPlugin(ctx, /\.server(\.[jt]sx?)?$/), @@ -237,19 +236,12 @@ export const create = async ( ctx: Context, channels: { cssBundleHref: Channel.Type } ): Promise => { - let hmrRoutes: Record = {}; - let onLoader = (filename: string, code: string) => { - let key = path.relative(ctx.config.rootDirectory, filename); - hmrRoutes[key] = { loaderHash: code }; - }; - let compiler = await esbuild.context({ - ...createEsbuildConfig(ctx, onLoader, channels), + ...createEsbuildConfig(ctx, channels), metafile: true, }); let compile = async () => { - hmrRoutes = {}; let { metafile } = await compiler.rebuild(); let hmr: Manifest["hmr"] | undefined = undefined; @@ -266,7 +258,6 @@ export const create = async ( ); hmr = { runtime: hmrRuntime, - routes: hmrRoutes, timestamp: Date.now(), }; } diff --git a/packages/remix-dev/compiler/js/plugins/routes_unstable.ts b/packages/remix-dev/compiler/js/plugins/routes_unstable.ts index 1fbfcb40526..7640a0dd953 100644 --- a/packages/remix-dev/compiler/js/plugins/routes_unstable.ts +++ b/packages/remix-dev/compiler/js/plugins/routes_unstable.ts @@ -12,7 +12,7 @@ import { processMDX } from "../../plugins/mdx"; const serverOnlyExports = new Set(["action", "loader"]); -let removeServerExports = (onLoader: (loader: string) => void) => +let removeServerExports = () => Transform.create(({ types: t }) => { return { visitor: { @@ -38,20 +38,12 @@ let removeServerExports = (onLoader: (loader: string) => void) => if (t.isFunctionDeclaration(node.declaration)) { let name = node.declaration.id?.name; if (!name) return; - if (name === "loader") { - let { code } = generate(node); - onLoader(code); - } if (serverOnlyExports.has(name)) return path.remove(); } if (t.isVariableDeclaration(node.declaration)) { let declarations = node.declaration.declarations.filter((d) => { let name = t.isIdentifier(d.id) ? d.id.name : undefined; if (!name) return false; - if (name === "loader") { - let { code } = generate(node); - onLoader(code); - } return !serverOnlyExports.has(name); }); if (declarations.length === 0) return path.remove(); @@ -72,8 +64,7 @@ let removeServerExports = (onLoader: (loader: string) => void) => */ export function browserRouteModulesPlugin( { config, options }: Context, - routeModulePaths: Map, - onLoader: (filename: string, code: string) => void + routeModulePaths: Map ): esbuild.Plugin { return { name: "browser-route-modules", @@ -119,9 +110,7 @@ export function browserRouteModulesPlugin( return mdxResult; } - let transform = removeServerExports((loader: string) => - onLoader(routeFile, loader) - ); + let transform = removeServerExports(); mdxResult.contents = transform( mdxResult.contents, // Trick babel into allowing JSX syntax. @@ -134,10 +123,7 @@ export function browserRouteModulesPlugin( let value = cache.get(file); if (!value || value.sourceCode !== sourceCode) { - let extractedLoader; - let transform = removeServerExports( - (loader: string) => (extractedLoader = loader) - ); + let transform = removeServerExports(); let contents = transform(sourceCode, routeFile); if (options.mode === "development" && config.future.unstable_dev) { @@ -153,7 +139,6 @@ export function browserRouteModulesPlugin( } value = { sourceCode, - extractedLoader, output: { contents, loader: getLoaderForFile(routeFile), @@ -163,10 +148,6 @@ export function browserRouteModulesPlugin( cache.set(file, value); } - if (value.extractedLoader) { - onLoader(routeFile, value.extractedLoader); - } - return value.output; } ); diff --git a/packages/remix-dev/compiler/manifest.ts b/packages/remix-dev/compiler/manifest.ts index 412574fc8a3..0f1de86f321 100644 --- a/packages/remix-dev/compiler/manifest.ts +++ b/packages/remix-dev/compiler/manifest.ts @@ -93,9 +93,7 @@ export async function create({ invariant(entry, `Missing output for entry point`); optimizeRoutes(routes, entry.imports); - let version = getHash( - JSON.stringify({ entry, routes, hmrRoutes: hmr?.routes }) - ).slice(0, 8); + let version = getHash(JSON.stringify({ entry, routes })).slice(0, 8); return { version, entry, routes, cssBundleHref, hmr }; } diff --git a/packages/remix-dev/compiler/options.ts b/packages/remix-dev/compiler/options.ts index d2f9dc979ec..165b89cbab0 100644 --- a/packages/remix-dev/compiler/options.ts +++ b/packages/remix-dev/compiler/options.ts @@ -11,5 +11,5 @@ export type Options = { host: string; port: number; }; - devWebsocketPort?: number; + devWebSocketPort?: number; }; diff --git a/packages/remix-dev/compiler/server/plugins/entry.ts b/packages/remix-dev/compiler/server/plugins/entry.ts index 76b16114d5e..3d64193e3b4 100644 --- a/packages/remix-dev/compiler/server/plugins/entry.ts +++ b/packages/remix-dev/compiler/server/plugins/entry.ts @@ -51,9 +51,9 @@ ${Object.keys(config.routes) export const publicPath = ${JSON.stringify(config.publicPath)}; export const entry = { module: entryServer }; ${ - options.devWebsocketPort + options.devWebSocketPort ? `export const dev = ${JSON.stringify({ - websocketPort: options.devWebsocketPort, + websocketPort: options.devWebSocketPort, })}` : "" } 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/config.ts b/packages/remix-dev/config.ts index 8f427f9dc80..a795c10cc75 100644 --- a/packages/remix-dev/config.ts +++ b/packages/remix-dev/config.ts @@ -43,7 +43,7 @@ type Dev = { httpScheme?: string; httpHost?: string; httpPort?: number; - websocketPort?: number; + webSocketPort?: number; restart?: boolean; publicDirectory?: string; }; diff --git a/packages/remix-dev/devServer_unstable/hdr.ts b/packages/remix-dev/devServer_unstable/hdr.ts new file mode 100644 index 00000000000..f391e3b3c1d --- /dev/null +++ b/packages/remix-dev/devServer_unstable/hdr.ts @@ -0,0 +1,108 @@ +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"; +import { mdxPlugin } from "../compiler/plugins/mdx"; + +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 }), + mdxPlugin(ctx), + 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..6f88ab803b5 100644 --- a/packages/remix-dev/devServer_unstable/hmr.ts +++ b/packages/remix-dev/devServer_unstable/hmr.ts @@ -5,6 +5,7 @@ import { type Manifest } from "../manifest"; export type Update = { id: string; + routeId: string; url: string; revalidate: boolean; reason: string; @@ -16,12 +17,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; @@ -35,6 +34,7 @@ export let updates = ( if (!prevRoute) { updates.push({ id: moduleId, + routeId: route.id, url: route.module, revalidate: true, reason: "Route added", @@ -43,11 +43,12 @@ 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, + routeId: route.id, url: route.module, revalidate: true, reason: "Loader changed", @@ -62,6 +63,7 @@ export let updates = ( if (diffModule || xorImports.size > 0) { updates.push({ id: moduleId, + routeId: route.id, url: route.module, revalidate: false, reason: "Component changed", diff --git a/packages/remix-dev/devServer_unstable/index.ts b/packages/remix-dev/devServer_unstable/index.ts index 05a427229ea..75e8c7fa2c5 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; @@ -41,12 +42,12 @@ export let serve = async ( httpScheme: string; httpHost: string; httpPort: number; - websocketPort: number; + webSocketPort: number; restart: boolean; } ) => { await loadEnv(initialConfig.rootDirectory); - let websocket = Socket.serve({ port: options.websocketPort }); + let websocket = Socket.serve({ port: options.webSocketPort }); let httpOrigin: Origin = { scheme: options.httpScheme, host: options.httpHost, @@ -58,6 +59,8 @@ export let serve = async ( manifest?: Manifest; prevManifest?: Manifest; appReady?: Channel.Type; + hdr?: Promise>; + prevLoaderHashes?: Record; } = {}; let bin = await detectBin(); @@ -111,14 +114,16 @@ export let serve = async ( sourcemap: true, onWarning: warnOnce, devHttpOrigin: httpOrigin, - devWebsocketPort: options.websocketPort, + devWebSocketPort: options.webSocketPort, }, }, { - 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)}`), diff --git a/packages/remix-dev/manifest.ts b/packages/remix-dev/manifest.ts index 48bd043590e..12391a827ef 100644 --- a/packages/remix-dev/manifest.ts +++ b/packages/remix-dev/manifest.ts @@ -24,6 +24,5 @@ export type Manifest = { hmr?: { timestamp: number; runtime: string; - routes: Record; }; }; diff --git a/packages/remix-react/browser.tsx b/packages/remix-react/browser.tsx index 4453d171d58..c81a6ed80fc 100644 --- a/packages/remix-react/browser.tsx +++ b/packages/remix-react/browser.tsx @@ -55,7 +55,7 @@ if (import.meta && import.meta.hot) { needsRevalidation, }: { assetsManifest: EntryContext["manifest"]; - needsRevalidation: boolean; + needsRevalidation: Set; }) => { let routeIds = [ ...new Set( diff --git a/packages/remix-react/components.tsx b/packages/remix-react/components.tsx index c4077f16109..89fa1b7e9ab 100644 --- a/packages/remix-react/components.tsx +++ b/packages/remix-react/components.tsx @@ -1759,12 +1759,12 @@ export const LiveReload = } if (!event.updates || !event.updates.length) return; let updateAccepted = false; - let needsRevalidation = false; + let needsRevalidation = new Set(); for (let update of event.updates) { console.log("[HMR] " + update.reason + " [" + update.id +"]") if (update.revalidate) { - needsRevalidation = true; - console.log("[HMR] Revalidating [" + update.id + "]"); + needsRevalidation.add(update.routeId); + console.log("[HMR] Revalidating [" + update.routeId + "]"); } let imported = await import(update.url + '?t=' + event.assetsManifest.hmr.timestamp); if (window.__hmr__.contexts[update.id]) { diff --git a/packages/remix-react/routes.tsx b/packages/remix-react/routes.tsx index 6ac78f8244f..3e49768e995 100644 --- a/packages/remix-react/routes.tsx +++ b/packages/remix-react/routes.tsx @@ -105,7 +105,7 @@ export function createServerRoutes( } export function createClientRoutesWithHMRRevalidationOptOut( - needsRevalidation: boolean, + needsRevalidation: Set, manifest: RouteManifest, routeModulesCache: RouteModules, future: FutureConfig @@ -129,7 +129,7 @@ export function createClientRoutes( string, Omit[] > = groupRoutesByParentId(manifest), - needsRevalidation: boolean | undefined = undefined + needsRevalidation?: Set ): DataRouteObject[] { return (routesByParentId[parentId] || []).map((route) => { let hasErrorBoundary = @@ -163,7 +163,9 @@ export function createClientRoutes( manifest, routeModulesCache, future, - route.id + route.id, + routesByParentId, + needsRevalidation ); if (children.length > 0) dataRoute.children = children; return dataRoute; @@ -173,7 +175,7 @@ export function createClientRoutes( function createShouldRevalidate( route: EntryRoute, routeModules: RouteModules, - needsRevalidation: boolean | undefined + needsRevalidation?: Set ): ShouldRevalidateFunction { let handledRevalidation = false; return function (arg) { @@ -183,9 +185,9 @@ function createShouldRevalidate( // When an HMR / HDR update happens we opt out of all user-defined // revalidation logic and the do as the dev server tells us the first // time router.revalidate() is called. - if (typeof needsRevalidation === "boolean" && !handledRevalidation) { + if (needsRevalidation !== undefined && !handledRevalidation) { handledRevalidation = true; - return needsRevalidation; + return needsRevalidation.has(route.id); } if (module.shouldRevalidate) {