Skip to content

Commit

Permalink
wip: hdr from dev server working
Browse files Browse the repository at this point in the history
  • Loading branch information
pcattori committed May 4, 2023
1 parent 0b6bc2c commit fffc5f5
Show file tree
Hide file tree
Showing 5 changed files with 124 additions and 13 deletions.
1 change: 0 additions & 1 deletion packages/remix-dev/compiler/compiler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,6 @@ export let create = async (ctx: Context): Promise<Compiler> => {
}

// 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;
Expand Down
2 changes: 1 addition & 1 deletion packages/remix-dev/compiler/watch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand All @@ -67,6 +66,7 @@ export async function watch(
logThrown(thrown);
return;
}
onBuildStart?.(ctx);

compiler = await Compiler.create(ctx);
let manifest = await compile();
Expand Down
106 changes: 106 additions & 0 deletions packages/remix-dev/devServer_unstable/hdr.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
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";

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<string, string> = {};
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<string, Route> = 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 }),
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);
};
12 changes: 5 additions & 7 deletions packages/remix-dev/devServer_unstable/hmr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,10 @@ export type Update = {
export let updates = (
config: RemixConfig,
manifest: Manifest,
prevManifest: Manifest
prevManifest: Manifest,
hdr: Record<string, string>,
prevHdr?: Record<string, string>
): 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;
Expand All @@ -43,8 +41,8 @@ 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,
Expand Down
16 changes: 12 additions & 4 deletions packages/remix-dev/devServer_unstable/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -58,6 +59,8 @@ export let serve = async (
manifest?: Manifest;
prevManifest?: Manifest;
appReady?: Channel.Type<void>;
hdr?: Promise<Record<string, string>>;
prevLoaderHashes?: Record<string, string>;
} = {};

let bin = await detectBin();
Expand Down Expand Up @@ -115,10 +118,12 @@ export let serve = async (
},
},
{
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;
Expand All @@ -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);

Expand All @@ -162,6 +169,7 @@ export let serve = async (
}
}
state.prevManifest = state.manifest;
state.prevLoaderHashes = loaderHashes;
},
onFileCreated: (file) =>
websocket.log(`File created: ${relativePath(file)}`),
Expand Down

0 comments on commit fffc5f5

Please sign in to comment.