Skip to content

Commit

Permalink
feat(dev): new dev server via future flag
Browse files Browse the repository at this point in the history
  • Loading branch information
pcattori committed Jan 18, 2023
1 parent faed344 commit 630245b
Show file tree
Hide file tree
Showing 7 changed files with 179 additions and 5 deletions.
14 changes: 12 additions & 2 deletions packages/remix-dev/cli/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import * as esbuild from "esbuild";
import * as colors from "../colors";
import * as compiler from "../compiler";
import * as devServer from "../devServer";
import * as devServer2 from "../devServer2";
import type { RemixConfig } from "../config";
import { readConfig } from "../config";
import { formatRoutes, RoutesFormat, isRoutesFormat } from "../config/format";
Expand Down Expand Up @@ -194,10 +195,19 @@ export async function watch(
});
}

export async function dev(remixRoot: string, modeArg?: string, port?: number) {
export async function dev(
remixRoot: string,
modeArg?: string,
flags: { port?: number; appServerPort?: number } = {}
) {
let config = await readConfig(remixRoot);
let mode = compiler.parseMode(modeArg ?? "", "development");
return devServer.serve(config, mode, port);

if (config.future.unstable_dev !== false) {
return devServer2.serve(config, flags);
}

return devServer.serve(config, mode, flags.port);
}

export async function codemod(
Expand Down
6 changes: 4 additions & 2 deletions packages/remix-dev/cli/run.ts
Original file line number Diff line number Diff line change
Expand Up @@ -132,11 +132,12 @@ const npxInterop = {

async function dev(
projectDir: string,
flags: { debug?: boolean; port?: number }
flags: { debug?: boolean; port?: number; appServerPort?: number }
) {
if (!process.env.NODE_ENV) process.env.NODE_ENV = "development";

if (flags.debug) inspector.open();
await commands.dev(projectDir, process.env.NODE_ENV, flags.port);
await commands.dev(projectDir, process.env.NODE_ENV, flags);
}

/**
Expand All @@ -154,6 +155,7 @@ export async function run(argv: string[] = process.argv.slice(2)) {

let args = arg(
{
"--app-server-port": Number,
"--debug": Boolean,
"--no-delete": Boolean,
"--dry": Boolean,
Expand Down
11 changes: 10 additions & 1 deletion packages/remix-dev/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,17 @@ export type ServerBuildTarget =
export type ServerModuleFormat = "esm" | "cjs";
export type ServerPlatform = "node" | "neutral";

type Dev = {
port?: number;
appServerPort?: number;
remixRequestHandlerPath?: string;
rebuildPollIntervalMs?: number;
};

interface FutureConfig {
unstable_cssModules: boolean;
unstable_cssSideEffectImports: boolean;
unstable_dev: false | Dev;
unstable_vanillaExtract: boolean;
v2_errorBoundary: boolean;
v2_meta: boolean;
Expand Down Expand Up @@ -491,10 +499,11 @@ export async function readConfig(
writeConfigDefaults(tsconfigPath);
}

let future = {
let future: FutureConfig = {
unstable_cssModules: appConfig.future?.unstable_cssModules === true,
unstable_cssSideEffectImports:
appConfig.future?.unstable_cssSideEffectImports === true,
unstable_dev: appConfig.future?.unstable_dev ?? false,
unstable_vanillaExtract: appConfig.future?.unstable_vanillaExtract === true,
v2_errorBoundary: appConfig.future?.v2_errorBoundary === true,
v2_meta: appConfig.future?.v2_meta === true,
Expand Down
106 changes: 106 additions & 0 deletions packages/remix-dev/devServer2.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import getPort, { makeRange } from "get-port";
import os from "os";
import path from "node:path";
import prettyMs from "pretty-ms";
import fetch from "node-fetch";

import { type AssetsManifest } from "./assets-manifest";
import * as Compiler from "./compiler";
import { type RemixConfig } from "./config";
import { loadEnv } from "./env";
import * as LiveReload from "./liveReload";

let info = (message: string) => console.info(`💿 ${message}`);

let relativePath = (file: string) => path.relative(process.cwd(), file);

let sleep = (ms: number) => new Promise((r) => setTimeout(r, ms));

let getHost = () =>
process.env.HOST ??
Object.values(os.networkInterfaces())
.flat()
.find((ip) => String(ip?.family).includes("4") && !ip?.internal)?.address;

let findPort = async (portPreference?: number) =>
getPort({
port:
// prettier-ignore
portPreference ? Number(portPreference) :
process.env.PORT ? Number(process.env.PORT) :
makeRange(3001, 3100),
});

let fetchAssetsManifest = async (
origin: string,
remixRequestHandlerPath: string
): Promise<AssetsManifest | undefined> => {
try {
let url = origin + remixRequestHandlerPath + "/__REMIX_ASSETS_MANIFEST";
let res = await fetch(url);
let assetsManifest = (await res.json()) as AssetsManifest;
return assetsManifest;
} catch (error) {
return undefined;
}
};

export let serve = async (
config: RemixConfig,
flags: { port?: number; appServerPort?: number } = {}
) => {
await loadEnv(config.rootDirectory);

let { unstable_dev } = config.future;
if (unstable_dev === false)
throw Error("The new dev server requires 'unstable_dev' to be set");
let { remixRequestHandlerPath, rebuildPollIntervalMs } = unstable_dev;
let appServerPort = flags.appServerPort ?? unstable_dev.appServerPort ?? 3000;

let host = getHost();
let appServerOrigin = `http://${host ?? "localhost"}:${appServerPort}`;

let waitForAppServer = async (buildHash: string) => {
while (true) {
// TODO AbortController signal to cancel responses?
let assetsManifest = await fetchAssetsManifest(
appServerOrigin,
remixRequestHandlerPath ?? ""
);
if (assetsManifest?.version === buildHash) return;

await sleep(rebuildPollIntervalMs ?? 50);
}
};

// watch and live reload on rebuilds
let port = await findPort(flags.port ?? unstable_dev.port);
let socket = LiveReload.serve({ port });
let dispose = await Compiler.watch(config, {
mode: "development",
liveReloadPort: port,
onInitialBuild: (durationMs) => info(`Built in ${prettyMs(durationMs)}`),
onRebuildStart: () => socket.log("Rebuilding..."),
onRebuildFinish: async (durationMs, assetsManifest) => {
if (!assetsManifest) return;
socket.log(`Rebuilt in ${prettyMs(durationMs)}`);

info(`Waiting for ${appServerOrigin}...`);
let start = Date.now();
await waitForAppServer(assetsManifest.version);
info(`${appServerOrigin} ready in ${prettyMs(Date.now() - start)}`);

socket.reload();
},
onFileCreated: (file) => socket.log(`File created: ${relativePath(file)}`),
onFileChanged: (file) => socket.log(`File changed: ${relativePath(file)}`),
onFileDeleted: (file) => socket.log(`File deleted: ${relativePath(file)}`),
});

// TODO exit hook: clean up assetsBuildDirectory and serverBuildPath?

return async () => {
await dispose();
socket.close();
};
};
27 changes: 27 additions & 0 deletions packages/remix-dev/liveReload.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import WebSocket from "ws";

type Message = { type: "RELOAD" } | { type: "LOG"; message: string };

type Broadcast = (message: Message) => void;

export let serve = (options: { port: number }) => {
let wss = new WebSocket.Server({ port: options.port });

let broadcast: Broadcast = (message) => {
wss.clients.forEach((client) => {
if (client.readyState === WebSocket.OPEN) {
client.send(JSON.stringify(message));
}
});
};

let reload = () => broadcast({ type: "RELOAD" });

let log = (messageText: string) => {
let _message = `💿 ${messageText}`;
console.log(_message);
broadcast({ type: "LOG", message: _message });
};

return { reload, log, close: wss.close };
};
1 change: 1 addition & 0 deletions packages/remix-server-runtime/entry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export interface EntryContext {
export interface FutureConfig {
unstable_cssModules: true;
unstable_cssSideEffectImports: boolean;
unstable_dev: false | { remixRequestHandlerPath?: string };
unstable_vanillaExtract: boolean;
v2_errorBoundary: boolean;
v2_meta: boolean;
Expand Down
19 changes: 19 additions & 0 deletions packages/remix-server-runtime/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,25 @@ export const createRequestHandler: CreateRequestHandlerFunction = (

return async function requestHandler(request, loadContext = {}) {
let url = new URL(request.url);

// special __REMIX_ASSETS_MANIFEST endpoint for checking if app server serving up-to-date routes and assets
let { unstable_dev } = build.future;
if (
mode === "development" &&
unstable_dev !== false &&
url.pathname ===
(unstable_dev.remixRequestHandlerPath ?? "") +
"/__REMIX_ASSETS_MANIFEST"
) {
if (request.method !== "GET") {
return new Response("Method not allowed", { status: 405 });
}
return new Response(JSON.stringify(build.assets), {
status: 200,
headers: { "Content-Type": "application/json" },
});
}

let matches = matchServerRoutes(routes, url.pathname);

let response: Response;
Expand Down

0 comments on commit 630245b

Please sign in to comment.