From 1bcce40a6232c39a97e9bd99c7f6046ca541eb51 Mon Sep 17 00:00:00 2001 From: Pedro Cattori Date: Wed, 3 May 2023 19:43:18 -0400 Subject: [PATCH] wip --- packages/remix-dev/compiler/compiler.ts | 18 +++- packages/remix-dev/compiler/hmr/compiler.ts | 98 +++++++++++++++++++++ 2 files changed, 115 insertions(+), 1 deletion(-) create mode 100644 packages/remix-dev/compiler/hmr/compiler.ts diff --git a/packages/remix-dev/compiler/compiler.ts b/packages/remix-dev/compiler/compiler.ts index 83fb44b9969..d924e07e620 100644 --- a/packages/remix-dev/compiler/compiler.ts +++ b/packages/remix-dev/compiler/compiler.ts @@ -1,10 +1,12 @@ import * as path from "path"; +import type esbuild from "esbuild"; import type { Context } from "./context"; import * as CSS from "./css"; import * as JS from "./js"; import * as Server from "./server"; import * as Channel from "../channel"; +import * as HMR from "./hmr/compiler"; import type { Manifest } from "../manifest"; import { create as createManifest, write as writeManifest } from "./manifest"; import { err, ok } from "../result"; @@ -31,7 +33,11 @@ export let create = async (ctx: Context): Promise => { css: await CSS.createCompiler(ctx), js: await JS.createCompiler(ctx, channels), server: await Server.createCompiler(ctx, channels), + hmr: undefined as esbuild.BuildContext | undefined, }; + if (ctx.options.mode === "development") { + subcompiler.hmr = await HMR.create(ctx); + } let cancel = async () => { // resolve channels with error so that downstream tasks don't hang waiting for results from upstream tasks channels.cssBundleHref.err(); @@ -66,6 +72,7 @@ export let create = async (ctx: Context): Promise => { css: subcompiler.css.compile().then(ok, errCancel), js: subcompiler.js.compile().then(ok, errCancel), server: subcompiler.server.compile().then(ok, errCancel), + hmr: subcompiler.hmr?.rebuild().then(ok, errCancel), }; // keep track of manually written artifacts @@ -115,6 +122,13 @@ export let create = async (ctx: Context): Promise => { // artifacts/server writes.server = Server.write(ctx.config, server.value); + if (ctx.options.mode === "development" && tasks.hmr) { + let hmr = await tasks.hmr; + if (!hmr.ok) throw hmr.error; + console.log("HMR_RESULTS:"); + console.log(hmr.value.metafile?.outputs); + } + await Promise.all(Object.values(writes)); return manifest; }; @@ -122,7 +136,9 @@ export let create = async (ctx: Context): Promise => { compile, cancel, dispose: async () => { - await Promise.all(Object.values(subcompiler).map((sub) => sub.dispose())); + await Promise.all( + Object.values(subcompiler).map((sub) => sub?.dispose()) + ); }, }; }; diff --git a/packages/remix-dev/compiler/hmr/compiler.ts b/packages/remix-dev/compiler/hmr/compiler.ts new file mode 100644 index 00000000000..d059f739889 --- /dev/null +++ b/packages/remix-dev/compiler/hmr/compiler.ts @@ -0,0 +1,98 @@ +import * as path from "node:path"; +import esbuild from "esbuild"; + +import type { Context } from "../context"; +import { externalPlugin } from "../plugins/external"; +import { emptyModulesPlugin } from "../plugins/emptyModules"; +import { createMatchPath } from "../utils/tsconfig"; +import invariant from "../../invariant"; +import { getRouteModuleExports } from "../utils/routeExports"; + +function isBareModuleId(id: string): boolean { + return !id.startsWith("node:") && !id.startsWith(".") && !path.isAbsolute(id); +} + +type Route = Context["config"]["routes"][string]; + +export let create = async (ctx: Context) => { + let entryPoints: Record = {}; + for (let id of Object.keys(ctx.config.routes)) { + entryPoints[id] = ctx.config.routes[id].file + "?loader"; + } + let compiler = await esbuild.context({ + bundle: true, + entryPoints: entryPoints, + treeShaking: true, + metafile: true, + outdir: "blah", + write: false, + entryNames: "[dir]/[name]-[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 }), + 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 }; + }); + }, + }, + ], + }); + return compiler; + // let result = await compiler.rebuild(); + // console.log(result.metafile.outputs); +};