From 8c6879a5448b2cec19bec060578acaf8818f3aed Mon Sep 17 00:00:00 2001 From: Pedro Cattori Date: Mon, 26 Sep 2022 15:32:10 -0400 Subject: [PATCH 1/4] wip --- packages/remix-dev/compiler-esbuild/index.ts | 81 +++++++++++ .../serverAssetsManifestPlugin.ts | 37 +++++ .../remix-dev/compiler-interface/build.ts | 36 +++++ .../remix-dev/compiler-interface/channel.ts | 20 +++ .../remix-dev/compiler-interface/compiler.ts | 27 ++++ .../remix-dev/compiler-interface/index.ts | 8 ++ .../remix-dev/compiler-interface/watch.ts | 129 ++++++++++++++++++ 7 files changed, 338 insertions(+) create mode 100644 packages/remix-dev/compiler-esbuild/index.ts create mode 100644 packages/remix-dev/compiler-esbuild/serverAssetsManifestPlugin.ts create mode 100644 packages/remix-dev/compiler-interface/build.ts create mode 100644 packages/remix-dev/compiler-interface/channel.ts create mode 100644 packages/remix-dev/compiler-interface/compiler.ts create mode 100644 packages/remix-dev/compiler-interface/index.ts create mode 100644 packages/remix-dev/compiler-interface/watch.ts diff --git a/packages/remix-dev/compiler-esbuild/index.ts b/packages/remix-dev/compiler-esbuild/index.ts new file mode 100644 index 00000000000..f4b5f905a5c --- /dev/null +++ b/packages/remix-dev/compiler-esbuild/index.ts @@ -0,0 +1,81 @@ +import path from "path"; +import type { BuildIncremental, BuildOptions } from "esbuild"; +import esbuild from "esbuild"; + +import type { BrowserCompiler, ServerCompiler } from "../compiler-interface"; +import type { ReadChannel, WriteChannel } from "../compiler-interface/channel"; +import type { CreateCompiler } from "../compiler-interface/compiler"; +import type { AssetsManifest } from "../compiler/assets"; +import { createAssetsManifest } from "../compiler/assets"; +import { writeFileSafe } from "../compiler/utils/fs"; +import type { RemixConfig } from "../config"; +import { serverAssetsManifestPlugin } from "./serverAssetsManifestPlugin"; + +// browser compiler `build` should do a fresh build the first time its called, but reuse the compiler subsequently + +async function generateAssetsManifest( + config: RemixConfig, + metafile: esbuild.Metafile +): Promise { + let assetsManifest = await createAssetsManifest(config, metafile); + let filename = `manifest-${assetsManifest.version.toUpperCase()}.js`; + + assetsManifest.url = config.publicPath + filename; + + await writeFileSafe( + path.join(config.assetsBuildDirectory, filename), + `window.__remixManifest=${JSON.stringify(assetsManifest)};` + ); + + return assetsManifest; +} + +// TODO: change `config` to `getConfig`: `(RemixConfig) => BuildOptions` +export const createBrowserCompiler = + (config: BuildOptions): CreateCompiler => + (remixConfig) => { + let compiler: BuildIncremental | undefined = undefined; + let build = async (manifestChannel: WriteChannel) => { + if (compiler === undefined) { + compiler = await esbuild.build({ + ...config, + metafile: true, + incremental: true, + }); + let manifest = await generateAssetsManifest( + remixConfig, + compiler.metafile! + ); + manifestChannel.write(manifest); + } else { + let { metafile } = await compiler.rebuild(); + let manifest = await generateAssetsManifest(remixConfig, metafile!); + manifestChannel.write(manifest); + } + }; + return { + build, + dispose: () => compiler?.rebuild.dispose(), + }; + }; + +// TODO: change `config` to `getConfig`: `(RemixConfig) => BuildOptions` +export const createServerCompiler = + (config: BuildOptions): CreateCompiler => + (remixConfig) => { + let build = async (manifestChannel: ReadChannel) => { + let manifestPromise = manifestChannel.read(); + + await esbuild.build({ + ...config, + plugins: [ + ...(config.plugins ?? []), + serverAssetsManifestPlugin(manifestPromise), + ], + }); + }; + return { + build, + dispose: () => undefined, + }; + }; diff --git a/packages/remix-dev/compiler-esbuild/serverAssetsManifestPlugin.ts b/packages/remix-dev/compiler-esbuild/serverAssetsManifestPlugin.ts new file mode 100644 index 00000000000..181ef358760 --- /dev/null +++ b/packages/remix-dev/compiler-esbuild/serverAssetsManifestPlugin.ts @@ -0,0 +1,37 @@ +import type { Plugin } from "esbuild"; +import jsesc from "jsesc"; + +import type { AssetsManifest } from "../compiler/assets"; +import { assetsManifestVirtualModule } from "../compiler/virtualModules"; + +/** + * Creates a virtual module called `@remix-run/dev/assets-manifest` that exports + * the assets manifest. This is used in the server entry module to access the + * assets manifest in the server build. + */ +export function serverAssetsManifestPlugin( + manifestPromise: Promise +): Plugin { + let filter = assetsManifestVirtualModule.filter; + + return { + name: "server-assets-manifest", + setup(build) { + build.onResolve({ filter }, ({ path }) => { + return { + path, + namespace: "server-assets-manifest", + }; + }); + + build.onLoad({ filter }, async () => { + let manifest = await manifestPromise; + + return { + contents: `export default ${jsesc(manifest, { es6: true })};`, + loader: "js", + }; + }); + }, + }; +} diff --git a/packages/remix-dev/compiler-interface/build.ts b/packages/remix-dev/compiler-interface/build.ts new file mode 100644 index 00000000000..81fff67c1cc --- /dev/null +++ b/packages/remix-dev/compiler-interface/build.ts @@ -0,0 +1,36 @@ +import fs from "fs"; +import path from "path"; + +import type { AssetsManifest } from "../compiler/assets"; +import type { RemixConfig } from "../config"; +import { createChannel } from "./channel"; +import type { RemixCompiler } from "./compiler"; + +// TODO error handling for if browser/server builds fail (e.g. syntax error) +// enumerate different types of errors +// console.log hints for users if we know how to diagnose the error from experience +// consider throwing custom Remix-specific error type if its an error we know more stuff about + +/** + * Coordinate the hand-off of the asset manifest between the browser and server builds. + * Additionally, write the asset manifest to the file system. + */ +export const build = async ( + config: RemixConfig, + compiler: RemixCompiler +): Promise => { + let manifestChannel = createChannel(); + let browser = compiler.browser.build(manifestChannel); + + // write manifest + manifestChannel.read().then((manifest) => { + fs.mkdirSync(config.assetsBuildDirectory, { recursive: true }); + fs.writeFileSync( + path.resolve(config.assetsBuildDirectory, path.basename(manifest.url!)), + `window.__remixManifest=${JSON.stringify(manifest)};` + ); + }); + + let server = compiler.server.build(manifestChannel); + await Promise.all([browser, server]); +}; diff --git a/packages/remix-dev/compiler-interface/channel.ts b/packages/remix-dev/compiler-interface/channel.ts new file mode 100644 index 00000000000..e5f76a58d3e --- /dev/null +++ b/packages/remix-dev/compiler-interface/channel.ts @@ -0,0 +1,20 @@ +export interface WriteChannel { + write: (data: T) => void; +} +export interface ReadChannel { + read: () => Promise; +} +export type Channel = WriteChannel & ReadChannel; + +export const createChannel = (): Channel => { + let promiseResolve: ((value: T) => void) | undefined = undefined; + + let promise = new Promise((resolve) => { + promiseResolve = resolve; + }); + + return { + write: promiseResolve!, + read: async () => promise, + }; +}; diff --git a/packages/remix-dev/compiler-interface/compiler.ts b/packages/remix-dev/compiler-interface/compiler.ts new file mode 100644 index 00000000000..476137a501a --- /dev/null +++ b/packages/remix-dev/compiler-interface/compiler.ts @@ -0,0 +1,27 @@ +import type { AssetsManifest } from "../compiler/assets"; +import type { RemixConfig } from "../config"; +import type { ReadChannel, WriteChannel } from "./channel"; + +export interface BrowserCompiler { + // produce ./public/build/ + build: (manifestChannel: WriteChannel) => Promise; + dispose: () => void; +} +export interface ServerCompiler { + // produce ./build/index.js + build: (manifestChannel: ReadChannel) => Promise; + dispose: () => void; +} +export type CreateCompiler = ( + config: RemixConfig +) => T; + +export interface RemixCompiler { + browser: BrowserCompiler; + server: ServerCompiler; +} + +export const dispose = ({ browser, server }: RemixCompiler) => { + browser.dispose(); + server.dispose(); +}; diff --git a/packages/remix-dev/compiler-interface/index.ts b/packages/remix-dev/compiler-interface/index.ts new file mode 100644 index 00000000000..020ffbd1244 --- /dev/null +++ b/packages/remix-dev/compiler-interface/index.ts @@ -0,0 +1,8 @@ +export type { + BrowserCompiler, + ServerCompiler, + CreateCompiler, + RemixCompiler, +} from "./compiler"; +export { build } from "./build"; +export { watch } from "./watch"; diff --git a/packages/remix-dev/compiler-interface/watch.ts b/packages/remix-dev/compiler-interface/watch.ts new file mode 100644 index 00000000000..2d01fdfd491 --- /dev/null +++ b/packages/remix-dev/compiler-interface/watch.ts @@ -0,0 +1,129 @@ +import path from "path"; +import chokidar from "chokidar"; + +import type { + BrowserCompiler, + CreateCompiler, + ServerCompiler, +} from "./compiler"; +import { dispose } from "./compiler"; +import { build } from "./build"; +import type { RemixConfig } from "../config"; +import { readConfig } from "../config"; + +// TODO error handling for if browser/server builds fail (e.g. syntax error) +// on___Error callback +// preserve watcher for common cases, throw for exotic errors + +const reloadConfig = async (config: RemixConfig): Promise => { + try { + return readConfig(config.rootDirectory); + } catch (error) { + // onBuildFailure(error as Error); + console.error(error); + throw error; + } +}; + +export const watch = async ( + config: RemixConfig, + createCompiler: { + browser: CreateCompiler; + server: CreateCompiler; + }, + callbacks: { + onInitialBuild?: () => void; + onRebuildStart?: () => void; + onRebuildFinish?: (durationMs: number) => void; + onFileCreated?: (file: string) => void; + onFileChanged?: (file: string) => void; + onFileDeleted?: (file: string) => void; + } = {} +): Promise<() => Promise> => { + let compiler = { + browser: createCompiler.browser(config), + server: createCompiler.server(config), + }; + + // initial build + await build(config, compiler); + callbacks.onInitialBuild?.(); + + // TODO debounce + let restart = async () => { + callbacks.onRebuildStart?.(); + let start = Date.now(); + dispose(compiler); + + config = await reloadConfig(config); + compiler = { + browser: createCompiler.browser(config), + server: createCompiler.server(config), + }; + await build(config, compiler); + callbacks.onRebuildFinish?.(Date.now() - start); + }; + + // TODO debounce + let rebuild = async () => { + callbacks.onRebuildStart?.(); + let start = Date.now(); + await build(config, compiler); + callbacks.onRebuildFinish?.(Date.now() - start); + }; + + // watch files + let toWatch = [config.appDirectory]; + if (config.serverEntryPoint) { + toWatch.push(config.serverEntryPoint); + } + config.watchPaths.forEach((watchPath) => toWatch.push(watchPath)); + + // what if route modules are not on filesystem? + let watcher = chokidar + .watch(toWatch, { + persistent: true, + ignoreInitial: true, + awaitWriteFinish: { + stabilityThreshold: 100, + pollInterval: 100, + }, + }) + .on("error", (error) => console.error(error)) + .on("change", async (file) => { + callbacks.onFileChanged?.(file); + await rebuild(); + }) + .on("add", async (file) => { + callbacks.onFileCreated?.(file); + config = await reloadConfig(config); + if (isEntryPoint(config, file)) { + await restart(); + } else { + await rebuild(); + } + }) + .on("unlink", async (file) => { + callbacks.onFileDeleted?.(file); + if (isEntryPoint(config, file)) { + await restart(); + } else { + await rebuild(); + } + }); + + return async () => { + await watcher.close().catch(() => undefined); + dispose(compiler); + }; +}; + +function isEntryPoint(config: RemixConfig, file: string): boolean { + let appFile = path.relative(config.appDirectory, file); + let entryPoints = [ + config.entryClientFile, + config.entryServerFile, + ...Object.values(config.routes).map((route) => route.file), + ]; + return entryPoints.includes(appFile); +} From a8b08d8d67c7c106c5142a5ab019bebeb80e77c5 Mon Sep 17 00:00:00 2001 From: Pedro Cattori Date: Wed, 28 Sep 2022 17:58:12 -0400 Subject: [PATCH 2/4] wip2 --- .../compiler-esbuild/browser/config.ts | 91 ++++++++++++++ .../browser/create-compiler.ts | 59 +++++++++ packages/remix-dev/compiler-esbuild/index.ts | 81 ------------- .../compiler-esbuild/server/config.ts | 113 ++++++++++++++++++ .../server/create-compiler.ts | 25 ++++ .../plugins/assets-manifest.ts} | 6 +- .../remix-dev/compiler-interface/compiler.ts | 6 +- .../remix-dev/compiler-interface/index.ts | 1 + .../remix-dev/compiler-interface/options.ts | 15 +++ .../remix-dev/compiler-interface/watch.ts | 17 +-- 10 files changed, 322 insertions(+), 92 deletions(-) create mode 100644 packages/remix-dev/compiler-esbuild/browser/config.ts create mode 100644 packages/remix-dev/compiler-esbuild/browser/create-compiler.ts create mode 100644 packages/remix-dev/compiler-esbuild/server/config.ts create mode 100644 packages/remix-dev/compiler-esbuild/server/create-compiler.ts rename packages/remix-dev/compiler-esbuild/{serverAssetsManifestPlugin.ts => server/plugins/assets-manifest.ts} (82%) create mode 100644 packages/remix-dev/compiler-interface/options.ts diff --git a/packages/remix-dev/compiler-esbuild/browser/config.ts b/packages/remix-dev/compiler-esbuild/browser/config.ts new file mode 100644 index 00000000000..388ceb72a2f --- /dev/null +++ b/packages/remix-dev/compiler-esbuild/browser/config.ts @@ -0,0 +1,91 @@ +import path from "path"; +import type esbuild from "esbuild"; +import NodeModulesPolyfillPlugin from "@esbuild-plugins/node-modules-polyfill"; +import { pnpPlugin as yarnPnpPlugin } from "@yarnpkg/esbuild-plugin-pnp"; +import { builtinModules as nodeBuiltins } from "module"; + +import { loaders } from "../../compiler/loaders"; +import type { RemixConfig } from "../../config"; +import { urlImportsPlugin } from "../../compiler/plugins/urlImportsPlugin"; +import { mdxPlugin } from "../../compiler/plugins/mdx"; +import { browserRouteModulesPlugin } from "../../compiler/plugins/browserRouteModulesPlugin"; +import { emptyModulesPlugin } from "../../compiler/plugins/emptyModulesPlugin"; +import { getAppDependencies } from "../../compiler/dependencies"; +import type { Options } from "../../compiler-interface"; + +const getExternals = (remixConfig: RemixConfig): string[] => { + // For the browser build, exclude node built-ins that don't have a + // browser-safe alternative installed in node_modules. Nothing should + // *actually* be external in the browser build (we want to bundle all deps) so + // this is really just making sure we don't accidentally have any dependencies + // on node built-ins in browser bundles. + let dependencies = Object.keys(getAppDependencies(remixConfig)); + let fakeBuiltins = nodeBuiltins.filter((mod) => dependencies.includes(mod)); + + if (fakeBuiltins.length > 0) { + throw new Error( + `It appears you're using a module that is built in to node, but you installed it as a dependency which could cause problems. Please remove ${fakeBuiltins.join( + ", " + )} before continuing.` + ); + } + return nodeBuiltins.filter((mod) => !dependencies.includes(mod)); +}; + +export const createEsbuildConfig = ( + remixConfig: RemixConfig, + options: Options +): esbuild.BuildOptions | esbuild.BuildIncremental => { + let entryPoints: esbuild.BuildOptions["entryPoints"] = { + "entry.client": path.resolve( + remixConfig.appDirectory, + remixConfig.entryClientFile + ), + }; + for (let id of Object.keys(remixConfig.routes)) { + // All route entry points are virtual modules that will be loaded by the + // browserEntryPointsPlugin. This allows us to tree-shake server-only code + // that we don't want to run in the browser (i.e. action & loader). + entryPoints[id] = remixConfig.routes[id].file + "?browser"; + } + + return { + entryPoints, + outdir: remixConfig.assetsBuildDirectory, + platform: "browser", + format: "esm", + external: getExternals(remixConfig), + loader: loaders, + bundle: true, + logLevel: "silent", + splitting: true, + sourcemap: options.sourcemap, + // As pointed out by https://github.com/evanw/esbuild/issues/2440, when tsconfig is set to + // `undefined`, esbuild will keep looking for a tsconfig.json recursively up. This unwanted + // behavior can only be avoided by creating an empty tsconfig file in the root directory. + tsconfig: remixConfig.tsconfigPath, + mainFields: ["browser", "module", "main"], + treeShaking: true, + minify: options.mode === "production", + entryNames: "[dir]/[name]-[hash]", + chunkNames: "_shared/[name]-[hash]", + assetNames: "_assets/[name]-[hash]", + publicPath: remixConfig.publicPath, + define: { + "process.env.NODE_ENV": JSON.stringify(options.mode), + "process.env.REMIX_DEV_SERVER_WS_PORT": JSON.stringify( + remixConfig.devServerPort + ), + }, + jsx: "automatic", + jsxDev: options.mode !== "production", + plugins: [ + urlImportsPlugin(), + mdxPlugin(remixConfig), + browserRouteModulesPlugin(remixConfig, /\?browser$/), + emptyModulesPlugin(remixConfig, /\.server(\.[jt]sx?)?$/), + NodeModulesPolyfillPlugin(), + yarnPnpPlugin(), + ], + }; +}; diff --git a/packages/remix-dev/compiler-esbuild/browser/create-compiler.ts b/packages/remix-dev/compiler-esbuild/browser/create-compiler.ts new file mode 100644 index 00000000000..b63b2f53843 --- /dev/null +++ b/packages/remix-dev/compiler-esbuild/browser/create-compiler.ts @@ -0,0 +1,59 @@ +import path from "path"; +import type { BuildIncremental } from "esbuild"; +import esbuild from "esbuild"; + +import type { AssetsManifest } from "../../compiler/assets"; +import { createAssetsManifest } from "../../compiler/assets"; +import { writeFileSafe } from "../../compiler/utils/fs"; +import type { RemixConfig } from "../../config"; +import { createEsbuildConfig } from "./config"; +import type { BrowserCompiler } from "../../compiler-interface"; +import type { WriteChannel } from "../../compiler-interface/channel"; +import type { CreateCompiler } from "../../compiler-interface/compiler"; + +async function generateAssetsManifest( + config: RemixConfig, + metafile: esbuild.Metafile +): Promise { + let assetsManifest = await createAssetsManifest(config, metafile); + let filename = `manifest-${assetsManifest.version.toUpperCase()}.js`; + + assetsManifest.url = config.publicPath + filename; + + await writeFileSafe( + path.join(config.assetsBuildDirectory, filename), + `window.__remixManifest=${JSON.stringify(assetsManifest)};` + ); + + return assetsManifest; +} + +export const createBrowserCompiler: CreateCompiler = ( + remixConfig, + options +) => { + let compiler: BuildIncremental | undefined = undefined; + let esbuildConfig = createEsbuildConfig(remixConfig, options); + let build = async (manifestChannel: WriteChannel) => { + if (compiler === undefined) { + compiler = await esbuild.build({ + ...esbuildConfig, + metafile: true, + incremental: true, + }); + let manifest = await generateAssetsManifest( + remixConfig, + compiler.metafile! + ); + manifestChannel.write(manifest); + } else { + let { metafile } = await compiler.rebuild(); + let manifest = await generateAssetsManifest(remixConfig, metafile!); + manifestChannel.write(manifest); + } + }; + return { + build, + dispose: () => compiler?.rebuild.dispose(), + }; +}; diff --git a/packages/remix-dev/compiler-esbuild/index.ts b/packages/remix-dev/compiler-esbuild/index.ts index f4b5f905a5c..e69de29bb2d 100644 --- a/packages/remix-dev/compiler-esbuild/index.ts +++ b/packages/remix-dev/compiler-esbuild/index.ts @@ -1,81 +0,0 @@ -import path from "path"; -import type { BuildIncremental, BuildOptions } from "esbuild"; -import esbuild from "esbuild"; - -import type { BrowserCompiler, ServerCompiler } from "../compiler-interface"; -import type { ReadChannel, WriteChannel } from "../compiler-interface/channel"; -import type { CreateCompiler } from "../compiler-interface/compiler"; -import type { AssetsManifest } from "../compiler/assets"; -import { createAssetsManifest } from "../compiler/assets"; -import { writeFileSafe } from "../compiler/utils/fs"; -import type { RemixConfig } from "../config"; -import { serverAssetsManifestPlugin } from "./serverAssetsManifestPlugin"; - -// browser compiler `build` should do a fresh build the first time its called, but reuse the compiler subsequently - -async function generateAssetsManifest( - config: RemixConfig, - metafile: esbuild.Metafile -): Promise { - let assetsManifest = await createAssetsManifest(config, metafile); - let filename = `manifest-${assetsManifest.version.toUpperCase()}.js`; - - assetsManifest.url = config.publicPath + filename; - - await writeFileSafe( - path.join(config.assetsBuildDirectory, filename), - `window.__remixManifest=${JSON.stringify(assetsManifest)};` - ); - - return assetsManifest; -} - -// TODO: change `config` to `getConfig`: `(RemixConfig) => BuildOptions` -export const createBrowserCompiler = - (config: BuildOptions): CreateCompiler => - (remixConfig) => { - let compiler: BuildIncremental | undefined = undefined; - let build = async (manifestChannel: WriteChannel) => { - if (compiler === undefined) { - compiler = await esbuild.build({ - ...config, - metafile: true, - incremental: true, - }); - let manifest = await generateAssetsManifest( - remixConfig, - compiler.metafile! - ); - manifestChannel.write(manifest); - } else { - let { metafile } = await compiler.rebuild(); - let manifest = await generateAssetsManifest(remixConfig, metafile!); - manifestChannel.write(manifest); - } - }; - return { - build, - dispose: () => compiler?.rebuild.dispose(), - }; - }; - -// TODO: change `config` to `getConfig`: `(RemixConfig) => BuildOptions` -export const createServerCompiler = - (config: BuildOptions): CreateCompiler => - (remixConfig) => { - let build = async (manifestChannel: ReadChannel) => { - let manifestPromise = manifestChannel.read(); - - await esbuild.build({ - ...config, - plugins: [ - ...(config.plugins ?? []), - serverAssetsManifestPlugin(manifestPromise), - ], - }); - }; - return { - build, - dispose: () => undefined, - }; - }; diff --git a/packages/remix-dev/compiler-esbuild/server/config.ts b/packages/remix-dev/compiler-esbuild/server/config.ts new file mode 100644 index 00000000000..f0cf5ba08a8 --- /dev/null +++ b/packages/remix-dev/compiler-esbuild/server/config.ts @@ -0,0 +1,113 @@ +import type esbuild from "esbuild"; +import { pnpPlugin as yarnPnpPlugin } from "@yarnpkg/esbuild-plugin-pnp"; +import NodeModulesPolyfillPlugin from "@esbuild-plugins/node-modules-polyfill"; + +import type { Options } from "../../compiler-interface"; +import { loaders } from "../../compiler/loaders"; +import { emptyModulesPlugin } from "../../compiler/plugins/emptyModulesPlugin"; +import { mdxPlugin } from "../../compiler/plugins/mdx"; +import { serverBareModulesPlugin } from "../../compiler/plugins/serverBareModulesPlugin"; +import { serverEntryModulePlugin } from "../../compiler/plugins/serverEntryModulePlugin"; +import { serverRouteModulesPlugin } from "../../compiler/plugins/serverRouteModulesPlugin"; +import { urlImportsPlugin } from "../../compiler/plugins/urlImportsPlugin"; +import type { RemixConfig } from "../../config"; +import { assetsManifestPlugin } from "./plugins/assets-manifest"; +import type { ReadChannel } from "../../compiler-interface/channel"; +import type { AssetsManifest } from "../../compiler/assets"; + +export const createEsbuildConfig = ( + remixConfig: RemixConfig, + manifestChannel: ReadChannel, + options: Options +): esbuild.BuildOptions => { + let stdin: esbuild.StdinOptions | undefined; + let entryPoints: string[] | undefined; + + if (remixConfig.serverEntryPoint) { + entryPoints = [remixConfig.serverEntryPoint]; + } else { + stdin = { + contents: remixConfig.serverBuildTargetEntryModule, + resolveDir: remixConfig.rootDirectory, + loader: "ts", + }; + } + + let isCloudflareRuntime = ["cloudflare-pages", "cloudflare-workers"].includes( + remixConfig.serverBuildTarget ?? "" + ); + let isDenoRuntime = remixConfig.serverBuildTarget === "deno"; + + let plugins: esbuild.Plugin[] = [ + urlImportsPlugin(), + mdxPlugin(remixConfig), + emptyModulesPlugin(remixConfig, /\.client(\.[jt]sx?)?$/), + serverRouteModulesPlugin(remixConfig), + serverEntryModulePlugin(remixConfig), + assetsManifestPlugin(manifestChannel.read()), + serverBareModulesPlugin(remixConfig, options.onWarning), + yarnPnpPlugin(), + ]; + + if (remixConfig.serverPlatform !== "node") { + plugins.unshift(NodeModulesPolyfillPlugin()); + } + + return { + absWorkingDir: remixConfig.rootDirectory, + stdin, + entryPoints, + outfile: remixConfig.serverBuildPath, + write: false, + conditions: isCloudflareRuntime + ? ["worker"] + : isDenoRuntime + ? ["deno", "worker"] + : undefined, + platform: remixConfig.serverPlatform, + format: remixConfig.serverModuleFormat, + treeShaking: true, + // The type of dead code elimination we want to do depends on the + // minify syntax property: https://github.com/evanw/esbuild/issues/672#issuecomment-1029682369 + // Dev builds are leaving code that should be optimized away in the + // bundle causing server / testing code to be shipped to the browser. + // These are properly optimized away in prod builds today, and this + // PR makes dev mode behave closer to production in terms of dead + // code elimination / tree shaking is concerned. + minifySyntax: true, + minify: options.mode === "production" && isCloudflareRuntime, + mainFields: isCloudflareRuntime + ? ["browser", "module", "main"] + : remixConfig.serverModuleFormat === "esm" + ? ["module", "main"] + : ["main", "module"], + target: options.target, + loader: loaders, + bundle: true, + logLevel: "silent", + // As pointed out by https://github.com/evanw/esbuild/issues/2440, when tsconfig is set to + // `undefined`, esbuild will keep looking for a tsremixConfig.json recursively up. This unwanted + // behavior can only be avoided by creating an empty tsconfig file in the root directory. + tsconfig: remixConfig.tsconfigPath, + // TODO(pcattori): removed`incremental: options.incremental,` + sourcemap: options.sourcemap, // use linked (true) to fix up .map file + // The server build needs to know how to generate asset URLs for imports + // of CSS and other files. + assetNames: "_assets/[name]-[hash]", + publicPath: remixConfig.publicPath, + define: { + "process.env.NODE_ENV": JSON.stringify(options.mode), + "process.env.REMIX_DEV_SERVER_WS_PORT": JSON.stringify( + remixConfig.devServerPort + ), + }, + jsx: "automatic", + jsxDev: options.mode !== "production", + plugins, + }; + // TODO(pcattori): + // .then(async (build) => { + // await writeServerBuildResult(config, build.outputFiles); + // return build; + // } +}; diff --git a/packages/remix-dev/compiler-esbuild/server/create-compiler.ts b/packages/remix-dev/compiler-esbuild/server/create-compiler.ts new file mode 100644 index 00000000000..0be7909132e --- /dev/null +++ b/packages/remix-dev/compiler-esbuild/server/create-compiler.ts @@ -0,0 +1,25 @@ +import esbuild from "esbuild"; + +import type { Options, ServerCompiler } from "../../compiler-interface"; +import type { ReadChannel } from "../../compiler-interface/channel"; +import type { CreateCompiler } from "../../compiler-interface/compiler"; +import type { AssetsManifest } from "../../compiler/assets"; +import { createEsbuildConfig } from "./config"; + +export const createServerCompiler: CreateCompiler = ( + remixConfig, + options: Options +) => { + let build = async (manifestChannel: ReadChannel) => { + let esbuildConfig = createEsbuildConfig( + remixConfig, + manifestChannel, + options + ); + await esbuild.build(esbuildConfig); + }; + return { + build, + dispose: () => undefined, + }; +}; diff --git a/packages/remix-dev/compiler-esbuild/serverAssetsManifestPlugin.ts b/packages/remix-dev/compiler-esbuild/server/plugins/assets-manifest.ts similarity index 82% rename from packages/remix-dev/compiler-esbuild/serverAssetsManifestPlugin.ts rename to packages/remix-dev/compiler-esbuild/server/plugins/assets-manifest.ts index 181ef358760..fe8a2ce62ae 100644 --- a/packages/remix-dev/compiler-esbuild/serverAssetsManifestPlugin.ts +++ b/packages/remix-dev/compiler-esbuild/server/plugins/assets-manifest.ts @@ -1,15 +1,15 @@ import type { Plugin } from "esbuild"; import jsesc from "jsesc"; -import type { AssetsManifest } from "../compiler/assets"; -import { assetsManifestVirtualModule } from "../compiler/virtualModules"; +import type { AssetsManifest } from "../../../compiler/assets"; +import { assetsManifestVirtualModule } from "../../../compiler/virtualModules"; /** * Creates a virtual module called `@remix-run/dev/assets-manifest` that exports * the assets manifest. This is used in the server entry module to access the * assets manifest in the server build. */ -export function serverAssetsManifestPlugin( +export function assetsManifestPlugin( manifestPromise: Promise ): Plugin { let filter = assetsManifestVirtualModule.filter; diff --git a/packages/remix-dev/compiler-interface/compiler.ts b/packages/remix-dev/compiler-interface/compiler.ts index 476137a501a..f4a90ebde59 100644 --- a/packages/remix-dev/compiler-interface/compiler.ts +++ b/packages/remix-dev/compiler-interface/compiler.ts @@ -1,6 +1,9 @@ import type { AssetsManifest } from "../compiler/assets"; import type { RemixConfig } from "../config"; import type { ReadChannel, WriteChannel } from "./channel"; +import type { Options } from "./options"; + +// TODO explain that `build` will be incremental (i.e. reuse compiler) if run multiple times export interface BrowserCompiler { // produce ./public/build/ @@ -13,7 +16,8 @@ export interface ServerCompiler { dispose: () => void; } export type CreateCompiler = ( - config: RemixConfig + remixConfig: RemixConfig, + options: Options ) => T; export interface RemixCompiler { diff --git a/packages/remix-dev/compiler-interface/index.ts b/packages/remix-dev/compiler-interface/index.ts index 020ffbd1244..d4d8d717cbd 100644 --- a/packages/remix-dev/compiler-interface/index.ts +++ b/packages/remix-dev/compiler-interface/index.ts @@ -6,3 +6,4 @@ export type { } from "./compiler"; export { build } from "./build"; export { watch } from "./watch"; +export type { Options } from "./options"; diff --git a/packages/remix-dev/compiler-interface/options.ts b/packages/remix-dev/compiler-interface/options.ts new file mode 100644 index 00000000000..44cf6a21eeb --- /dev/null +++ b/packages/remix-dev/compiler-interface/options.ts @@ -0,0 +1,15 @@ +type Mode = "development" | "test" | "production"; + +type Target = + | "browser" // TODO: remove + | "server" // TODO: remove + | "cloudflare-workers" + | "node14"; + +export type Options = { + mode: Mode; + sourcemap: boolean; + target: Target; + onWarning?: (message: string, key: string) => void; + // onBuildFailure?(failure: Error | esbuild.BuildFailure): void; +}; diff --git a/packages/remix-dev/compiler-interface/watch.ts b/packages/remix-dev/compiler-interface/watch.ts index 2d01fdfd491..005ddad8d08 100644 --- a/packages/remix-dev/compiler-interface/watch.ts +++ b/packages/remix-dev/compiler-interface/watch.ts @@ -10,6 +10,7 @@ import { dispose } from "./compiler"; import { build } from "./build"; import type { RemixConfig } from "../config"; import { readConfig } from "../config"; +import type { Options } from "./options"; // TODO error handling for if browser/server builds fail (e.g. syntax error) // on___Error callback @@ -28,6 +29,7 @@ const reloadConfig = async (config: RemixConfig): Promise => { export const watch = async ( config: RemixConfig, createCompiler: { + options: Options; // TODO should options be curried into createCompiler.{browser,server} ? browser: CreateCompiler; server: CreateCompiler; }, @@ -40,10 +42,14 @@ export const watch = async ( onFileDeleted?: (file: string) => void; } = {} ): Promise<() => Promise> => { - let compiler = { - browser: createCompiler.browser(config), - server: createCompiler.server(config), + let createRemixCompiler = (remixConfig: RemixConfig) => { + let { browser, server, options } = createCompiler; + return { + browser: browser(remixConfig, options), + server: server(remixConfig, options), + }; }; + let compiler = createRemixCompiler(config); // initial build await build(config, compiler); @@ -56,10 +62,7 @@ export const watch = async ( dispose(compiler); config = await reloadConfig(config); - compiler = { - browser: createCompiler.browser(config), - server: createCompiler.server(config), - }; + compiler = createRemixCompiler(config); await build(config, compiler); callbacks.onRebuildFinish?.(Date.now() - start); }; From 82f901bc19b61c451c9deea972904eb9afd87863 Mon Sep 17 00:00:00 2001 From: Pedro Cattori Date: Mon, 3 Oct 2022 11:36:10 -0500 Subject: [PATCH 3/4] wip3 --- packages/remix-dev/compiler-esbuild/index.ts | 0 packages/remix-dev/compiler-kit/README.md | 50 +++++++++++++++++++ .../build.ts | 4 +- .../index.ts | 2 +- .../compiler.ts => compiler-kit/interface.ts} | 7 +-- .../options.ts | 0 .../utils}/channel.ts | 0 .../watch.ts | 9 +++- .../browser/config.ts | 2 +- .../browser/create-compiler.ts | 6 +-- packages/remix-dev/compiler2/index.ts | 2 + .../server/config.ts | 4 +- .../server/create-compiler.ts | 6 +-- .../server/plugins/assets-manifest.ts | 0 14 files changed, 72 insertions(+), 20 deletions(-) delete mode 100644 packages/remix-dev/compiler-esbuild/index.ts create mode 100644 packages/remix-dev/compiler-kit/README.md rename packages/remix-dev/{compiler-interface => compiler-kit}/build.ts (92%) rename packages/remix-dev/{compiler-interface => compiler-kit}/index.ts (89%) rename packages/remix-dev/{compiler-interface/compiler.ts => compiler-kit/interface.ts} (82%) rename packages/remix-dev/{compiler-interface => compiler-kit}/options.ts (100%) rename packages/remix-dev/{compiler-interface => compiler-kit/utils}/channel.ts (100%) rename packages/remix-dev/{compiler-interface => compiler-kit}/watch.ts (96%) rename packages/remix-dev/{compiler-esbuild => compiler2}/browser/config.ts (98%) rename packages/remix-dev/{compiler-esbuild => compiler2}/browser/create-compiler.ts (89%) create mode 100644 packages/remix-dev/compiler2/index.ts rename packages/remix-dev/{compiler-esbuild => compiler2}/server/config.ts (97%) rename packages/remix-dev/{compiler-esbuild => compiler2}/server/create-compiler.ts (72%) rename packages/remix-dev/{compiler-esbuild => compiler2}/server/plugins/assets-manifest.ts (100%) diff --git a/packages/remix-dev/compiler-esbuild/index.ts b/packages/remix-dev/compiler-esbuild/index.ts deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/packages/remix-dev/compiler-kit/README.md b/packages/remix-dev/compiler-kit/README.md new file mode 100644 index 00000000000..0aefd118b65 --- /dev/null +++ b/packages/remix-dev/compiler-kit/README.md @@ -0,0 +1,50 @@ +Q: who should be in charge of writing the assets manifest to disk? browser? server? build? +PC: I _think_ browser, since its written to `/public/build/` + + +channel -> [promise, resolve] + +# compiler-kit + +```ts +type Build = (remixConfig: RemixConfig, compiler: RemixCompiler) => Promise + +type Watch = (remixConfig: RemixConfig, createCompiler: { + options: Options, + browser: CreateCompiler, + server: CreateCompiler, +}) => Promise +``` + +notes: +- we want `compiler` as a param for `build` so that we can reuse compilers + - alternative would be for `build` to internally create a `RemixCompiler`... then we could't reuse compiler across builds. + +# compiler- + +```ts +type CreateBrowserCompiler = (remixConfig: RemixConfig, options: Options) => BrowserCompiler +type CreateServerCompiler = (remixConfig: RemixConfig, options: Options) => ServerCompiler +``` + +# dev-server + +```ts +type Serve = ( + config: RemixConfig, + createCompiler: { + options: Options; + browser: CreateCompiler; + server: CreateCompiler; + }, + port?: number +) => Promise +``` + +# Open questions + +Q1: who should be in charge of writing the assets manifest to disk? browser? server? build? +PC: I _think_ browser, since its written to `/public/build/` + + +Q2: channel -> [promise, resolve]? \ No newline at end of file diff --git a/packages/remix-dev/compiler-interface/build.ts b/packages/remix-dev/compiler-kit/build.ts similarity index 92% rename from packages/remix-dev/compiler-interface/build.ts rename to packages/remix-dev/compiler-kit/build.ts index 81fff67c1cc..5ffbd5f0602 100644 --- a/packages/remix-dev/compiler-interface/build.ts +++ b/packages/remix-dev/compiler-kit/build.ts @@ -3,8 +3,8 @@ import path from "path"; import type { AssetsManifest } from "../compiler/assets"; import type { RemixConfig } from "../config"; -import { createChannel } from "./channel"; -import type { RemixCompiler } from "./compiler"; +import { createChannel } from "./utils/channel"; +import type { RemixCompiler } from "./interface"; // TODO error handling for if browser/server builds fail (e.g. syntax error) // enumerate different types of errors diff --git a/packages/remix-dev/compiler-interface/index.ts b/packages/remix-dev/compiler-kit/index.ts similarity index 89% rename from packages/remix-dev/compiler-interface/index.ts rename to packages/remix-dev/compiler-kit/index.ts index d4d8d717cbd..a592a03c805 100644 --- a/packages/remix-dev/compiler-interface/index.ts +++ b/packages/remix-dev/compiler-kit/index.ts @@ -3,7 +3,7 @@ export type { ServerCompiler, CreateCompiler, RemixCompiler, -} from "./compiler"; +} from "./interface"; export { build } from "./build"; export { watch } from "./watch"; export type { Options } from "./options"; diff --git a/packages/remix-dev/compiler-interface/compiler.ts b/packages/remix-dev/compiler-kit/interface.ts similarity index 82% rename from packages/remix-dev/compiler-interface/compiler.ts rename to packages/remix-dev/compiler-kit/interface.ts index f4a90ebde59..1f95ede2e21 100644 --- a/packages/remix-dev/compiler-interface/compiler.ts +++ b/packages/remix-dev/compiler-kit/interface.ts @@ -1,6 +1,6 @@ import type { AssetsManifest } from "../compiler/assets"; import type { RemixConfig } from "../config"; -import type { ReadChannel, WriteChannel } from "./channel"; +import type { ReadChannel, WriteChannel } from "./utils/channel"; import type { Options } from "./options"; // TODO explain that `build` will be incremental (i.e. reuse compiler) if run multiple times @@ -24,8 +24,3 @@ export interface RemixCompiler { browser: BrowserCompiler; server: ServerCompiler; } - -export const dispose = ({ browser, server }: RemixCompiler) => { - browser.dispose(); - server.dispose(); -}; diff --git a/packages/remix-dev/compiler-interface/options.ts b/packages/remix-dev/compiler-kit/options.ts similarity index 100% rename from packages/remix-dev/compiler-interface/options.ts rename to packages/remix-dev/compiler-kit/options.ts diff --git a/packages/remix-dev/compiler-interface/channel.ts b/packages/remix-dev/compiler-kit/utils/channel.ts similarity index 100% rename from packages/remix-dev/compiler-interface/channel.ts rename to packages/remix-dev/compiler-kit/utils/channel.ts diff --git a/packages/remix-dev/compiler-interface/watch.ts b/packages/remix-dev/compiler-kit/watch.ts similarity index 96% rename from packages/remix-dev/compiler-interface/watch.ts rename to packages/remix-dev/compiler-kit/watch.ts index 005ddad8d08..9966d03669e 100644 --- a/packages/remix-dev/compiler-interface/watch.ts +++ b/packages/remix-dev/compiler-kit/watch.ts @@ -4,9 +4,9 @@ import chokidar from "chokidar"; import type { BrowserCompiler, CreateCompiler, + RemixCompiler, ServerCompiler, -} from "./compiler"; -import { dispose } from "./compiler"; +} from "./interface"; import { build } from "./build"; import type { RemixConfig } from "../config"; import { readConfig } from "../config"; @@ -26,6 +26,11 @@ const reloadConfig = async (config: RemixConfig): Promise => { } }; +const dispose = ({ browser, server }: RemixCompiler) => { + browser.dispose(); + server.dispose(); +}; + export const watch = async ( config: RemixConfig, createCompiler: { diff --git a/packages/remix-dev/compiler-esbuild/browser/config.ts b/packages/remix-dev/compiler2/browser/config.ts similarity index 98% rename from packages/remix-dev/compiler-esbuild/browser/config.ts rename to packages/remix-dev/compiler2/browser/config.ts index 388ceb72a2f..85f8504e270 100644 --- a/packages/remix-dev/compiler-esbuild/browser/config.ts +++ b/packages/remix-dev/compiler2/browser/config.ts @@ -11,7 +11,7 @@ import { mdxPlugin } from "../../compiler/plugins/mdx"; import { browserRouteModulesPlugin } from "../../compiler/plugins/browserRouteModulesPlugin"; import { emptyModulesPlugin } from "../../compiler/plugins/emptyModulesPlugin"; import { getAppDependencies } from "../../compiler/dependencies"; -import type { Options } from "../../compiler-interface"; +import type { Options } from "../../compiler-kit"; const getExternals = (remixConfig: RemixConfig): string[] => { // For the browser build, exclude node built-ins that don't have a diff --git a/packages/remix-dev/compiler-esbuild/browser/create-compiler.ts b/packages/remix-dev/compiler2/browser/create-compiler.ts similarity index 89% rename from packages/remix-dev/compiler-esbuild/browser/create-compiler.ts rename to packages/remix-dev/compiler2/browser/create-compiler.ts index b63b2f53843..7db18e113cb 100644 --- a/packages/remix-dev/compiler-esbuild/browser/create-compiler.ts +++ b/packages/remix-dev/compiler2/browser/create-compiler.ts @@ -7,9 +7,9 @@ import { createAssetsManifest } from "../../compiler/assets"; import { writeFileSafe } from "../../compiler/utils/fs"; import type { RemixConfig } from "../../config"; import { createEsbuildConfig } from "./config"; -import type { BrowserCompiler } from "../../compiler-interface"; -import type { WriteChannel } from "../../compiler-interface/channel"; -import type { CreateCompiler } from "../../compiler-interface/compiler"; +import type { BrowserCompiler } from "../../compiler-kit"; +import type { WriteChannel } from "../../compiler-kit/utils/channel"; +import type { CreateCompiler } from "../../compiler-kit/interface"; async function generateAssetsManifest( config: RemixConfig, diff --git a/packages/remix-dev/compiler2/index.ts b/packages/remix-dev/compiler2/index.ts new file mode 100644 index 00000000000..c2575720a6c --- /dev/null +++ b/packages/remix-dev/compiler2/index.ts @@ -0,0 +1,2 @@ +export { createBrowserCompiler } from "./browser/create-compiler"; +export { createServerCompiler } from "./server/create-compiler"; diff --git a/packages/remix-dev/compiler-esbuild/server/config.ts b/packages/remix-dev/compiler2/server/config.ts similarity index 97% rename from packages/remix-dev/compiler-esbuild/server/config.ts rename to packages/remix-dev/compiler2/server/config.ts index f0cf5ba08a8..12e803367ff 100644 --- a/packages/remix-dev/compiler-esbuild/server/config.ts +++ b/packages/remix-dev/compiler2/server/config.ts @@ -2,7 +2,7 @@ import type esbuild from "esbuild"; import { pnpPlugin as yarnPnpPlugin } from "@yarnpkg/esbuild-plugin-pnp"; import NodeModulesPolyfillPlugin from "@esbuild-plugins/node-modules-polyfill"; -import type { Options } from "../../compiler-interface"; +import type { Options } from "../../compiler-kit"; import { loaders } from "../../compiler/loaders"; import { emptyModulesPlugin } from "../../compiler/plugins/emptyModulesPlugin"; import { mdxPlugin } from "../../compiler/plugins/mdx"; @@ -12,7 +12,7 @@ import { serverRouteModulesPlugin } from "../../compiler/plugins/serverRouteModu import { urlImportsPlugin } from "../../compiler/plugins/urlImportsPlugin"; import type { RemixConfig } from "../../config"; import { assetsManifestPlugin } from "./plugins/assets-manifest"; -import type { ReadChannel } from "../../compiler-interface/channel"; +import type { ReadChannel } from "../../compiler-kit/utils/channel"; import type { AssetsManifest } from "../../compiler/assets"; export const createEsbuildConfig = ( diff --git a/packages/remix-dev/compiler-esbuild/server/create-compiler.ts b/packages/remix-dev/compiler2/server/create-compiler.ts similarity index 72% rename from packages/remix-dev/compiler-esbuild/server/create-compiler.ts rename to packages/remix-dev/compiler2/server/create-compiler.ts index 0be7909132e..d4cfca81807 100644 --- a/packages/remix-dev/compiler-esbuild/server/create-compiler.ts +++ b/packages/remix-dev/compiler2/server/create-compiler.ts @@ -1,8 +1,8 @@ import esbuild from "esbuild"; -import type { Options, ServerCompiler } from "../../compiler-interface"; -import type { ReadChannel } from "../../compiler-interface/channel"; -import type { CreateCompiler } from "../../compiler-interface/compiler"; +import type { Options, ServerCompiler } from "../../compiler-kit"; +import type { ReadChannel } from "../../compiler-kit/utils/channel"; +import type { CreateCompiler } from "../../compiler-kit/interface"; import type { AssetsManifest } from "../../compiler/assets"; import { createEsbuildConfig } from "./config"; diff --git a/packages/remix-dev/compiler-esbuild/server/plugins/assets-manifest.ts b/packages/remix-dev/compiler2/server/plugins/assets-manifest.ts similarity index 100% rename from packages/remix-dev/compiler-esbuild/server/plugins/assets-manifest.ts rename to packages/remix-dev/compiler2/server/plugins/assets-manifest.ts From 945a4cc21f84fcc0e3e6fad6e8b8989d6355e5a8 Mon Sep 17 00:00:00 2001 From: Pedro Cattori Date: Mon, 3 Oct 2022 11:36:22 -0500 Subject: [PATCH 4/4] decision docs --- decisions/0005-remix-compiler-interface.md | 64 +++++++++++++++++++ .../0006-webpack-compiler-for-migrations.md | 20 ++++++ 2 files changed, 84 insertions(+) create mode 100644 decisions/0005-remix-compiler-interface.md create mode 100644 decisions/0006-webpack-compiler-for-migrations.md diff --git a/decisions/0005-remix-compiler-interface.md b/decisions/0005-remix-compiler-interface.md new file mode 100644 index 00000000000..3a16fcc17c2 --- /dev/null +++ b/decisions/0005-remix-compiler-interface.md @@ -0,0 +1,64 @@ +# Remix Compiler interface + +Date: 2022-08-03 + +Status: proposed + +## Context + +The ethos of Remix is "use the platform", but many popular tools and frameworks +require compiler-level support for syntax not natively supported by the browser runtime or the server runtime. + +Some tools, like Typescript, are ubiquitous. +For these, we think it best to include them as part of the Remix compiler. + +But there are a myriad of different CSS frameworks, a handful of different package managers, etc... that require compiler-level integration to fully support their features. + +The status quo has been to assess each framework/tool and add plugins to our compiler to support each of the ones that we decide have enough upside to warrant the maintenance burden. +For example, here are PRs for [Yarn PnP support](https://github.com/remix-run/remix/pulls?q=pnp) and [CSS Modules support](https://github.com/remix-run/remix/pulls?q=%22css+modules%22). +But that places Remix releases on the critical path for support and bug fixes for each of these features! +We're already feeling some maintenance pressure from this and we don't users to have to wait for us to review and merge PRs for N different CSS frameworks before we get to the one they are using. + +But Remix shouldn't care how you prefer to do CSS nor what package manager you use. +At the end of the day, Remix wants to introduce as few things on top of the platform for excellent DX and UX. +For example, Remix has strong opinions on routing that enables awesome features like loaders fetching in parallel. +But Remix does not want to prescribe your package manager. + +## Considered + +### Remix as a {vite,esbuild,...} plugin + +Other web frameworks have instead ditched the idea of having their own compiler and instead provide plugins for popular compilers like Vite. +The idea is that users would invoke Vite (or their compiler of choice) directly. + +As a proof-of-concept, we prototyped Remix plugins for Webpack. +However, Remix requires the server build to have access to the assets manifest produced by the browser build at build-time. + +Coordinating the assets manifest handoff is not possible when invoking webpack directly. + +### Pluggable compiler + +Alternatively, we could open up our esbuild-based compiler for users to add their own esbuild plugins. + +In the short term, this sounds great, but we know how this goes in the long term. +Users will add plugins and we'll have to be extremely careful whenever we change our compiler not to conflict with _any_ plugins users may be using. + +## Decision + +- our compiler's interface is: + - porcelain/CLI: `build`, `watch` commands + - plumbing/Node API: + - `createBrowserCompiler`, `createServerCompiler` + - from `compiler-kit`: `build`, `watch` functions + +the remix build expects web standards: (#useThePlatform) +- e.g. css as stylesheets! + +for features that require compiler-support (e.g. custom CSS framework), +run a pre-remix build step. +example: [vanilla extract](https://github.com/remix-run/remix/pull/4173) + +## Consequences + +- future work: partial build/jit (via cache) +- future work: HMR \ No newline at end of file diff --git a/decisions/0006-webpack-compiler-for-migrations.md b/decisions/0006-webpack-compiler-for-migrations.md new file mode 100644 index 00000000000..dd663844dfa --- /dev/null +++ b/decisions/0006-webpack-compiler-for-migrations.md @@ -0,0 +1,20 @@ +# Webpack compiler for migrations + +Date: 2022-08-03 + +Status: proposed + +## Context + +Most exisiting React apps are built on top of Webpack and many of those use Create React App. +We'd love for users who maintain those apps to be able to migrate to Remix incrementally. +That means supporting everything that their current setup allows from Day 1, even if they will eventually transition off of a Webpack-enable feature towards a Remix feature. + +## Decision + +Remix will provide a first-party, Webpack-based, pluggable compiler. +This compiler is **NOT** meant to be an alternative to the standard Remix compiler, but just a stepping stone for those migrating to Remix. +Support will be prioritized to those using this compiler to migrate an existing Webpack-based app to Remix. +Support will be limited for anyone using this compiler for greenfield apps. + +## Consequences