Skip to content

Commit 9efdaea

Browse files
authored
fix(dev): gracefully handle hdr errors (#6467)
1 parent c90c16e commit 9efdaea

File tree

5 files changed

+141
-32
lines changed

5 files changed

+141
-32
lines changed

.changeset/strong-dolphins-tell.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@remix-run/dev": patch
3+
---
4+
5+
fix dev server crashes caused by ungraceful hdr error handling

integration/hmr-log-test.ts

+43
Original file line numberDiff line numberDiff line change
@@ -463,6 +463,49 @@ whatsup
463463
fs.writeFileSync(mdxPath, originalMdx);
464464
await page.waitForSelector(`#crazy`);
465465
expect(dataRequests).toBe(5);
466+
467+
// dev server doesn't crash when rebuild fails
468+
await page.click(`a[href="/"]`);
469+
await page.getByText("Hello, planet").waitFor({ timeout: HMR_TIMEOUT_MS });
470+
await page.waitForLoadState("networkidle");
471+
472+
expect(devStderr()).toBe("");
473+
let withSyntaxError = `
474+
import { useLoaderData } from "@remix-run/react";
475+
export function shouldRevalidate(args) {
476+
return true;
477+
}
478+
eport efault functio Index() {
479+
const t = useLoaderData();
480+
return (
481+
<mai>
482+
<h1>With Syntax Error</h1>
483+
</main>
484+
)
485+
}
486+
`;
487+
fs.writeFileSync(indexPath, withSyntaxError);
488+
await wait(() => devStderr().includes('Expected ";" but found "efault"'), {
489+
timeoutMs: HMR_TIMEOUT_MS,
490+
});
491+
492+
let withFix = `
493+
import { useLoaderData } from "@remix-run/react";
494+
export function shouldRevalidate(args) {
495+
return true;
496+
}
497+
export default function Index() {
498+
// const t = useLoaderData();
499+
return (
500+
<main>
501+
<h1>With Fix</h1>
502+
</main>
503+
)
504+
}
505+
`;
506+
fs.writeFileSync(indexPath, withFix);
507+
await page.waitForLoadState("networkidle");
508+
await page.getByText("With Fix").waitFor({ timeout: HMR_TIMEOUT_MS });
466509
} catch (e) {
467510
console.log("stdout begin -----------------------");
468511
console.log(devStdout());

integration/hmr-test.ts

+43
Original file line numberDiff line numberDiff line change
@@ -462,6 +462,49 @@ whatsup
462462
fs.writeFileSync(mdxPath, originalMdx);
463463
await page.waitForSelector(`#crazy`);
464464
expect(dataRequests).toBe(5);
465+
466+
// dev server doesn't crash when rebuild fails
467+
await page.click(`a[href="/"]`);
468+
await page.getByText("Hello, planet").waitFor({ timeout: HMR_TIMEOUT_MS });
469+
await page.waitForLoadState("networkidle");
470+
471+
expect(devStderr()).toBe("");
472+
let withSyntaxError = `
473+
import { useLoaderData } from "@remix-run/react";
474+
export function shouldRevalidate(args) {
475+
return true;
476+
}
477+
eport efault functio Index() {
478+
const t = useLoaderData();
479+
return (
480+
<mai>
481+
<h1>With Syntax Error</h1>
482+
</main>
483+
)
484+
}
485+
`;
486+
fs.writeFileSync(indexPath, withSyntaxError);
487+
await wait(() => devStderr().includes('Expected ";" but found "efault"'), {
488+
timeoutMs: HMR_TIMEOUT_MS,
489+
});
490+
491+
let withFix = `
492+
import { useLoaderData } from "@remix-run/react";
493+
export function shouldRevalidate(args) {
494+
return true;
495+
}
496+
export default function Index() {
497+
// const t = useLoaderData();
498+
return (
499+
<main>
500+
<h1>With Fix</h1>
501+
</main>
502+
)
503+
}
504+
`;
505+
fs.writeFileSync(indexPath, withFix);
506+
await page.waitForLoadState("networkidle");
507+
await page.getByText("With Fix").waitFor({ timeout: HMR_TIMEOUT_MS });
465508
} catch (e) {
466509
console.log("stdout begin -----------------------");
467510
console.log(devStdout());

packages/remix-dev/devServer_unstable/hdr.ts

+13-10
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,9 @@ function isBareModuleId(id: string): boolean {
1616

1717
type Route = Context["config"]["routes"][string];
1818

19-
export let detectLoaderChanges = async (ctx: Context) => {
19+
export let detectLoaderChanges = async (
20+
ctx: Context
21+
): Promise<Record<string, string>> => {
2022
let entryPoints: Record<string, string> = {};
2123
for (let id of Object.keys(ctx.config.routes)) {
2224
entryPoints[id] = ctx.config.routes[id].file + "?loader";
@@ -30,6 +32,7 @@ export let detectLoaderChanges = async (ctx: Context) => {
3032
write: false,
3133
entryNames: "[hash]",
3234
loader: loaders,
35+
logLevel: "silent",
3336
plugins: [
3437
{
3538
name: "hmr-loader",
@@ -98,13 +101,13 @@ export let detectLoaderChanges = async (ctx: Context) => {
98101
};
99102

100103
let { metafile } = await esbuild.build(options);
101-
let entries = Object.entries(metafile!.outputs).map(
102-
([hashjs, { entryPoint }]) => {
103-
let file = entryPoint
104-
?.replace(/^hmr-loader:/, "")
105-
?.replace(/\?loader$/, "");
106-
return [file, hashjs.replace(/\.js$/, "")];
107-
}
108-
);
109-
return Object.fromEntries(entries);
104+
105+
let entries: Record<string, string> = {};
106+
for (let [hashjs, { entryPoint }] of Object.entries(metafile!.outputs)) {
107+
if (entryPoint === undefined) continue;
108+
let file = entryPoint.replace(/^hmr-loader:/, "").replace(/\?loader$/, "");
109+
entries[file] = hashjs.replace(/\.js$/, "");
110+
}
111+
112+
return entries;
110113
};

packages/remix-dev/devServer_unstable/index.ts

+37-22
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ import * as HMR from "./hmr";
1515
import { warnOnce } from "../warnOnce";
1616
import { detectPackageManager } from "../cli/detectPackageManager";
1717
import * as HDR from "./hdr";
18+
import type { Result } from "../result";
19+
import { err, ok } from "../result";
1820

1921
type Origin = {
2022
scheme: string;
@@ -59,7 +61,7 @@ export let serve = async (
5961
manifest?: Manifest;
6062
prevManifest?: Manifest;
6163
appReady?: Channel.Type<void>;
62-
hdr?: Promise<Record<string, string>>;
64+
loaderChanges?: Promise<Result<Record<string, string>>>;
6365
prevLoaderHashes?: Record<string, string>;
6466
} = {};
6567

@@ -122,57 +124,70 @@ export let serve = async (
122124
},
123125
{
124126
onBuildStart: async (ctx) => {
127+
// stop listening for previous manifest
125128
state.appReady?.err();
129+
126130
clean(ctx.config);
127131
websocket.log(state.prevManifest ? "Rebuilding..." : "Building...");
128132

129-
state.hdr = HDR.detectLoaderChanges(ctx);
133+
state.loaderChanges = HDR.detectLoaderChanges(ctx).then(ok, err);
130134
},
131135
onBuildManifest: (manifest: Manifest) => {
132136
state.manifest = manifest;
137+
state.appReady = Channel.create();
133138
},
134139
onBuildFinish: async (ctx, durationMs, succeeded) => {
135140
if (!succeeded) return;
136-
137141
websocket.log(
138142
(state.prevManifest ? "Rebuilt" : "Built") +
139143
` in ${prettyMs(durationMs)}`
140144
);
141-
state.appReady = Channel.create();
142145

143-
let start = Date.now();
144-
console.log(`Waiting for app server (${state.manifest?.version})`);
145-
if (
146-
options.command &&
147-
(state.appServer === undefined || options.restart)
148-
) {
149-
await kill(state.appServer);
150-
state.appServer = startAppServer(options.command);
151-
}
152-
let { ok } = await state.appReady.result;
153-
// result not ok -> new build started before this one finished. do not process outdated manifest
154-
let loaderHashes = await state.hdr;
155-
if (ok) {
146+
// accumulate new state, but only update state after updates are processed
147+
let newState: typeof state = { prevManifest: state.manifest };
148+
try {
149+
console.log(`Waiting for app server (${state.manifest?.version})`);
150+
let start = Date.now();
151+
if (
152+
options.command &&
153+
(state.appServer === undefined || options.restart)
154+
) {
155+
await kill(state.appServer);
156+
state.appServer = startAppServer(options.command);
157+
}
158+
let appReady = await state.appReady!.result;
159+
if (!appReady.ok) return;
156160
console.log(`App server took ${prettyMs(Date.now() - start)}`);
157-
if (state.manifest && loaderHashes && state.prevManifest) {
161+
162+
// HMR + HDR
163+
let loaderChanges = await state.loaderChanges!;
164+
if (loaderChanges.ok) {
165+
newState.prevLoaderHashes = loaderChanges.value;
166+
}
167+
if (loaderChanges?.ok && state.manifest && state.prevManifest) {
158168
let updates = HMR.updates(
159169
ctx.config,
160170
state.manifest,
161171
state.prevManifest,
162-
loaderHashes,
172+
loaderChanges.value,
163173
state.prevLoaderHashes
164174
);
165175
websocket.hmr(state.manifest, updates);
166176

167177
let hdr = updates.some((u) => u.revalidate);
168178
console.log("> HMR" + (hdr ? " + HDR" : ""));
169-
} else if (state.prevManifest !== undefined) {
179+
return;
180+
}
181+
182+
// Live Reload
183+
if (state.prevManifest !== undefined) {
170184
websocket.reload();
171185
console.log("> Live reload");
172186
}
187+
} finally {
188+
// commit accumulated state
189+
Object.assign(state, newState);
173190
}
174-
state.prevManifest = state.manifest;
175-
state.prevLoaderHashes = loaderHashes;
176191
},
177192
onFileCreated: (file) =>
178193
websocket.log(`File created: ${relativePath(file)}`),

0 commit comments

Comments
 (0)