Skip to content

Commit c691ae1

Browse files
authored
feat(dev): unstable dev server improvements (#6133)
1 parent f09ff88 commit c691ae1

File tree

13 files changed

+282
-255
lines changed

13 files changed

+282
-255
lines changed

integration/hmr-test.ts

Lines changed: 39 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,21 @@ import getPort, { makeRange } from "get-port";
77

88
import { createFixtureProject, css, js, json } from "./helpers/create-fixture";
99

10-
let fixture = (options: { port: number; appServerPort: number }) => ({
10+
test.setTimeout(120_000);
11+
12+
let fixture = (options: {
13+
appServerPort: number;
14+
httpPort: number;
15+
webSocketPort: number;
16+
}) => ({
1117
files: {
1218
"remix.config.js": js`
1319
module.exports = {
1420
tailwind: true,
1521
future: {
1622
unstable_dev: {
17-
port: ${options.port},
18-
appServerPort: ${options.appServerPort},
23+
httpPort: ${options.httpPort},
24+
webSocketPort: ${options.webSocketPort},
1925
},
2026
v2_routeConvention: true,
2127
v2_errorBoundary: true,
@@ -28,8 +34,7 @@ let fixture = (options: { port: number; appServerPort: number }) => ({
2834
private: true,
2935
sideEffects: false,
3036
scripts: {
31-
"dev:remix": `cross-env NODE_ENV=development node ./node_modules/@remix-run/dev/dist/cli.js dev`,
32-
"dev:app": `cross-env NODE_ENV=development nodemon --watch build/ ./server.js`,
37+
dev: `cross-env NODE_ENV=development node ./node_modules/@remix-run/dev/dist/cli.js dev -c "node ./server.js"`,
3338
},
3439
dependencies: {
3540
"@remix-run/css-bundle": "0.0.0-local-version",
@@ -38,7 +43,6 @@ let fixture = (options: { port: number; appServerPort: number }) => ({
3843
"cross-env": "0.0.0-local-version",
3944
express: "0.0.0-local-version",
4045
isbot: "0.0.0-local-version",
41-
nodemon: "0.0.0-local-version",
4246
react: "0.0.0-local-version",
4347
"react-dom": "0.0.0-local-version",
4448
tailwindcss: "0.0.0-local-version",
@@ -58,6 +62,7 @@ let fixture = (options: { port: number; appServerPort: number }) => ({
5862
let path = require("path");
5963
let express = require("express");
6064
let { createRequestHandler } = require("@remix-run/express");
65+
let { ping } = require("@remix-run/dev");
6166
6267
const app = express();
6368
app.use(express.static("public", { immutable: true, maxAge: "1y" }));
@@ -75,8 +80,11 @@ let fixture = (options: { port: number; appServerPort: number }) => ({
7580
7681
let port = ${options.appServerPort};
7782
app.listen(port, () => {
78-
require(BUILD_DIR);
83+
let build = require(BUILD_DIR);
7984
console.log('✅ app ready: http://localhost:' + port);
85+
if (process.env.NODE_ENV === 'development') {
86+
ping(build);
87+
}
8088
});
8189
`,
8290

@@ -204,43 +212,34 @@ let bufferize = (stream: Readable): (() => string) => {
204212
return () => buffer;
205213
};
206214

215+
let HMR_TIMEOUT_MS = 10_000;
216+
207217
test("HMR", async ({ page }) => {
208218
// uncomment for debugging
209219
// page.on("console", (msg) => console.log(msg.text()));
210220
page.on("pageerror", (err) => console.log(err.message));
211221

212-
let appServerPort = await getPort({ port: makeRange(3080, 3089) });
213-
let port = await getPort({ port: makeRange(3090, 3099) });
214-
let projectDir = await createFixtureProject(fixture({ port, appServerPort }));
222+
let portRange = makeRange(3080, 3099);
223+
let appServerPort = await getPort({ port: portRange });
224+
let httpPort = await getPort({ port: portRange });
225+
let webSocketPort = await getPort({ port: portRange });
226+
let projectDir = await createFixtureProject(
227+
fixture({ appServerPort, httpPort, webSocketPort })
228+
);
215229

216230
// spin up dev server
217-
let dev = execa("npm", ["run", "dev:remix"], { cwd: projectDir });
231+
let dev = execa("npm", ["run", "dev"], { cwd: projectDir });
218232
let devStdout = bufferize(dev.stdout!);
219233
let devStderr = bufferize(dev.stderr!);
220234
await wait(
221235
() => {
222236
let stderr = devStderr();
223237
if (stderr.length > 0) throw Error(stderr);
224-
return /💿 Built in /.test(devStdout());
238+
return / app ready: /.test(devStdout());
225239
},
226240
{ timeoutMs: 10_000 }
227241
);
228242

229-
// spin up app server
230-
let app = execa("npm", ["run", "dev:app"], { cwd: projectDir });
231-
let appStdout = bufferize(app.stdout!);
232-
let appStderr = bufferize(app.stderr!);
233-
await wait(
234-
() => {
235-
let stderr = appStderr();
236-
if (stderr.length > 0) throw Error(stderr);
237-
return / app ready: /.test(appStdout());
238-
},
239-
{
240-
timeoutMs: 10_000,
241-
}
242-
);
243-
244243
try {
245244
await page.goto(`http://localhost:${appServerPort}`, {
246245
waitUntil: "networkidle",
@@ -290,7 +289,7 @@ test("HMR", async ({ page }) => {
290289
// detect HMR'd content and style changes
291290
await page.waitForLoadState("networkidle");
292291
let h1 = page.getByText("Changed");
293-
await h1.waitFor({ timeout: 2000 });
292+
await h1.waitFor({ timeout: HMR_TIMEOUT_MS });
294293
expect(h1).toHaveCSS("color", "rgb(255, 255, 255)");
295294
expect(h1).toHaveCSS("background-color", "rgb(0, 0, 0)");
296295

@@ -301,7 +300,7 @@ test("HMR", async ({ page }) => {
301300
// undo change
302301
fs.writeFileSync(indexPath, originalIndex);
303302
fs.writeFileSync(cssModulePath, originalCssModule);
304-
await page.getByText("Index Title").waitFor({ timeout: 2000 });
303+
await page.getByText("Index Title").waitFor({ timeout: HMR_TIMEOUT_MS });
305304
expect(await page.getByLabel("Root Input").inputValue()).toBe("asdfasdf");
306305
await page.waitForSelector(`#root-counter:has-text("inc 1")`);
307306

@@ -322,7 +321,7 @@ test("HMR", async ({ page }) => {
322321
}
323322
`;
324323
fs.writeFileSync(indexPath, withLoader1);
325-
await page.getByText("Hello, world").waitFor({ timeout: 2000 });
324+
await page.getByText("Hello, world").waitFor({ timeout: HMR_TIMEOUT_MS });
326325
expect(await page.getByLabel("Root Input").inputValue()).toBe("asdfasdf");
327326
await page.waitForSelector(`#root-counter:has-text("inc 1")`);
328327

@@ -344,7 +343,7 @@ test("HMR", async ({ page }) => {
344343
}
345344
`;
346345
fs.writeFileSync(indexPath, withLoader2);
347-
await page.getByText("Hello, planet").waitFor({ timeout: 2000 });
346+
await page.getByText("Hello, planet").waitFor({ timeout: HMR_TIMEOUT_MS });
348347
expect(await page.getByLabel("Root Input").inputValue()).toBe("asdfasdf");
349348
await page.waitForSelector(`#root-counter:has-text("inc 1")`);
350349

@@ -388,10 +387,16 @@ test("HMR", async ({ page }) => {
388387
aboutCounter = await page.waitForSelector(
389388
`#about-counter:has-text("inc 0")`
390389
);
390+
} catch (e) {
391+
console.log("stdout begin -----------------------");
392+
console.log(devStdout());
393+
console.log("stdout end -------------------------");
394+
395+
console.log("stderr begin -----------------------");
396+
console.log(devStderr());
397+
console.log("stderr end -------------------------");
398+
throw e;
391399
} finally {
392400
dev.kill();
393-
app.kill();
394-
console.log(devStderr());
395-
console.log(appStderr());
396401
}
397402
});

packages/remix-dev/channel.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1+
import type { Result } from "./result";
2+
13
type Resolve<V> = (value: V | PromiseLike<V>) => void;
24
type Reject = (reason?: any) => void;
35

4-
type Result<V, E = unknown> = { ok: true; value: V } | { ok: false; error: E };
5-
66
export type Type<V, E = unknown> = {
77
ok: (value: V) => void;
88
err: (reason?: any) => void;

packages/remix-dev/cli/commands.ts

Lines changed: 44 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import * as path from "path";
22
import { execSync } from "child_process";
3+
import inspector from "inspector";
34
import * as fse from "fs-extra";
5+
import getPort, { makeRange } from "get-port";
46
import ora from "ora";
57
import prettyMs from "pretty-ms";
68
import * as esbuild from "esbuild";
@@ -206,17 +208,52 @@ export async function watch(
206208

207209
export async function dev(
208210
remixRoot: string,
209-
flags: { port?: number; appServerPort?: number } = {}
211+
flags: {
212+
debug?: boolean;
213+
port?: number; // TODO: remove for v2
214+
command?: string;
215+
httpPort?: number;
216+
restart?: boolean;
217+
websocketPort?: number;
218+
} = {}
210219
) {
220+
if (process.env.NODE_ENV && process.env.NODE_ENV !== "development") {
221+
console.warn(
222+
`Expected NODE_ENV to be 'development' but got ${process.env.NODE_ENV}`
223+
);
224+
}
225+
if (flags.debug) inspector.open();
226+
211227
let config = await readConfig(remixRoot);
212228

213-
if (config.future.unstable_dev !== false) {
214-
await devServer_unstable.serve(config, flags);
229+
if (config.future.unstable_dev === false) {
230+
await devServer.serve(config, flags.port);
215231
return await new Promise(() => {});
216232
}
217233

218-
await devServer.serve(config, flags.port);
219-
return await new Promise(() => {});
234+
let { unstable_dev } = config.future;
235+
236+
let command =
237+
flags.command ?? (unstable_dev === true ? undefined : unstable_dev.command);
238+
let httpPort =
239+
flags.httpPort ??
240+
(unstable_dev === true ? undefined : unstable_dev.httpPort) ??
241+
(await findPort());
242+
let websocketPort =
243+
flags.websocketPort ??
244+
(unstable_dev === true ? undefined : unstable_dev.websocketPort) ??
245+
(await findPort());
246+
let restart =
247+
flags.restart ??
248+
(unstable_dev === true ? undefined : unstable_dev.restart) ??
249+
true;
250+
251+
await devServer_unstable.serve(config, {
252+
command,
253+
httpPort,
254+
websocketPort,
255+
restart,
256+
});
220257
}
221258

222259
export async function codemod(
@@ -442,3 +479,5 @@ let parseMode = (
442479
console.error(`Unrecognized mode: ${mode}`);
443480
process.exit(1);
444481
};
482+
483+
let findPort = async () => getPort({ port: makeRange(3001, 3100) });

packages/remix-dev/cli/run.ts

Lines changed: 21 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import * as path from "path";
22
import os from "os";
33
import arg from "arg";
4-
import inspector from "inspector";
54
import inquirer from "inquirer";
65
import semver from "semver";
76
import fse from "fs-extra";
@@ -137,20 +136,6 @@ const npxInterop = {
137136
pnpm: "pnpm exec",
138137
};
139138

140-
async function dev(
141-
projectDir: string,
142-
flags: { debug?: boolean; port?: number; appServerPort?: number }
143-
) {
144-
if (process.env.NODE_ENV && process.env.NODE_ENV !== "development") {
145-
console.warn(
146-
`NODE_ENV=${process.env.NODE_ENV} overwritten to 'development'`
147-
);
148-
}
149-
150-
if (flags.debug) inspector.open();
151-
await commands.dev(projectDir, flags);
152-
}
153-
154139
/**
155140
* Programmatic interface for running the Remix CLI with the given command line
156141
* arguments.
@@ -166,12 +151,14 @@ export async function run(argv: string[] = process.argv.slice(2)) {
166151

167152
let args = arg(
168153
{
169-
"--app-server-port": Number,
154+
"--command": String,
155+
"-c": "--command",
170156
"--debug": Boolean,
171157
"--no-delete": Boolean,
172158
"--dry": Boolean,
173159
"--force": Boolean,
174160
"--help": Boolean,
161+
"--http-port": Number,
175162
"-h": "--help",
176163
"--install": Boolean,
177164
"--no-install": Boolean,
@@ -181,13 +168,16 @@ export async function run(argv: string[] = process.argv.slice(2)) {
181168
"--port": Number,
182169
"-p": "--port",
183170
"--remix-version": String,
171+
"--restart": Boolean,
172+
"--no-restart": Boolean,
184173
"--sourcemap": Boolean,
185174
"--template": String,
186175
"--token": String,
187176
"--typescript": Boolean,
188177
"--no-typescript": Boolean,
189178
"--version": Boolean,
190179
"-v": "--version",
180+
"--websocket-port": Number,
191181
},
192182
{
193183
argv,
@@ -212,6 +202,15 @@ export async function run(argv: string[] = process.argv.slice(2)) {
212202
return;
213203
}
214204

205+
if (flags["http-port"]) {
206+
flags.httpPort = flags["http-port"];
207+
delete flags["http-port"];
208+
}
209+
if (flags["websocket-port"]) {
210+
flags.websocketPort = flags["websocket-port"];
211+
delete flags["websocket-port"];
212+
}
213+
215214
if (args["--no-delete"]) {
216215
flags.delete = false;
217216
}
@@ -222,6 +221,10 @@ export async function run(argv: string[] = process.argv.slice(2)) {
222221
flags.interactive = false;
223222
}
224223
flags.interactive = flags.interactive ?? require.main === module;
224+
if (args["--no-restart"]) {
225+
flags.restart = false;
226+
delete flags["no-restart"];
227+
}
225228
if (args["--no-typescript"]) {
226229
flags.typescript = false;
227230
}
@@ -498,10 +501,10 @@ export async function run(argv: string[] = process.argv.slice(2)) {
498501
break;
499502
}
500503
case "dev":
501-
await dev(input[1], flags);
504+
await commands.dev(input[1], flags);
502505
break;
503506
default:
504507
// `remix ./my-project` is shorthand for `remix dev ./my-project`
505-
await dev(input[0], flags);
508+
await commands.dev(input[0], flags);
506509
}
507510
}

0 commit comments

Comments
 (0)