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) {