diff --git a/.changeset/ninety-baboons-leave.md b/.changeset/ninety-baboons-leave.md index 3610cdaf01b..24273e1bc4f 100644 --- a/.changeset/ninety-baboons-leave.md +++ b/.changeset/ninety-baboons-leave.md @@ -2,4 +2,4 @@ "@remix-run/dev": patch --- -Vite: Add `adapter` option to support modifying the build output and/or development environment for different hosting providers. +Vite: Add `presets` option to ease integration with different platforms and tools. diff --git a/.changeset/quiet-adults-drum.md b/.changeset/quiet-adults-drum.md new file mode 100644 index 00000000000..a559de674f8 --- /dev/null +++ b/.changeset/quiet-adults-drum.md @@ -0,0 +1,5 @@ +--- +"@remix-run/dev": patch +--- + +Vite: Add `buildEnd` hook diff --git a/docs/future/vite.md b/docs/future/vite.md index 19a740ad3f9..0a51b0f1882 100644 --- a/docs/future/vite.md +++ b/docs/future/vite.md @@ -58,17 +58,23 @@ The following subset of Remix config options are supported: The Vite plugin also accepts the following additional options: -#### adapter +#### buildDirectory -A function for adapting the build output and/or development environment for different hosting providers. +The path to the build directory, relative to the project root. Defaults to +`"build"`. -#### buildDirectory +#### buildEnd -The path to the build directory, relative to the project root. Defaults to `"build"`. +A function that is called after the full Remix build is complete. #### manifest -Whether to write a `.remix/manifest.json` file to the build directory. Defaults to `false`. +Whether to write a `.remix/manifest.json` file to the build directory. Defaults +to `false`. + +#### presets + +An array of Remix config presets to ease integration with different platforms and tools. #### serverBuildFile @@ -116,19 +122,19 @@ wrangler pages dev ./build/client ``` While Vite provides a better development experience, Wrangler provides closer emulation of the Cloudflare environment by running your server code in [Cloudflare's `workerd` runtime][cloudflare-workerd] instead of Node. -To simulate the Cloudflare environment in Vite, Wrangler provides [Node proxies for resource bindings][wrangler-getbindingsproxy] which are automatically available when using the Remix Cloudflare adapter: +To simulate the Cloudflare environment in Vite, Wrangler provides [Node proxies for resource bindings][wrangler-getbindingsproxy] which are automatically available when using the Remix Cloudflare preset: ```ts filename=vite.config.ts lines=[3,10] import { unstable_vitePlugin as remix, - unstable_vitePluginAdapterCloudflare as cloudflare, + unstable_vitePluginPresetCloudflare as cloudflare, } from "@remix-run/dev"; import { defineConfig } from "vite"; export default defineConfig({ plugins: [ remix({ - adapter: cloudflare(), + presets: [cloudflare()], }), ], }); @@ -443,19 +449,25 @@ The Remix Vite plugin only officially supports [Cloudflare Pages][cloudflare-pag -👉 **Add the Cloudflare adapter to your Vite config** +👉 **In your Vite config, add `"workerd"` and `"worker"` to Vite's +`ssr.resolve.externalConditions` option and add the Cloudflare Remix preset** -```ts filename=vite.config.ts lines=[3,10] +```ts filename=vite.config.ts lines=[3,8-12,15] import { unstable_vitePlugin as remix, - unstable_vitePluginAdapterCloudflare as cloudflare, + unstable_vitePluginPresetCloudflare as cloudflare, } from "@remix-run/dev"; import { defineConfig } from "vite"; export default defineConfig({ + ssr: { + resolve: { + externalConditions: ["workerd", "worker"], + }, + }, plugins: [ remix({ - adapter: cloudflare(), + presets: [cloudflare()], }), ], }); diff --git a/integration/helpers/vite.ts b/integration/helpers/vite.ts index 9e75a69bf38..0f2581a9b72 100644 --- a/integration/helpers/vite.ts +++ b/integration/helpers/vite.ts @@ -18,6 +18,7 @@ export const VITE_CONFIG = async (args: { pluginOptions?: string; vitePlugins?: string; viteManifest?: boolean; + viteSsrResolveExternalConditions?: string[]; }) => { let hmrPort = await getPort(); return String.raw` @@ -25,6 +26,13 @@ export const VITE_CONFIG = async (args: { import { unstable_vitePlugin as remix } from "@remix-run/dev"; export default defineConfig({ + ssr: { + resolve: { + externalConditions: ${JSON.stringify( + args.viteSsrResolveExternalConditions ?? [] + )}, + }, + }, server: { port: ${args.port}, strictPort: true, diff --git a/integration/vite-adapter-test.ts b/integration/vite-adapter-test.ts deleted file mode 100644 index 8c2073789e2..00000000000 --- a/integration/vite-adapter-test.ts +++ /dev/null @@ -1,125 +0,0 @@ -import * as fs from "node:fs"; -import * as path from "node:path"; -import { test, expect } from "@playwright/test"; -import { normalizePath } from "vite"; -import getPort from "get-port"; - -import { - createProject, - viteDev, - viteBuild, - VITE_CONFIG, -} from "./helpers/vite.js"; - -test.describe(async () => { - let port: number; - let cwd: string; - let stop: () => void; - - function pathStartsWithCwd(pathname: string) { - return normalizePath(pathname).startsWith(normalizePath(cwd)); - } - - function relativeToCwd(pathname: string) { - return normalizePath(path.relative(cwd, pathname)); - } - - test.beforeAll(async () => { - port = await getPort(); - cwd = await createProject({ - "vite.config.ts": await VITE_CONFIG({ - port, - pluginOptions: ` - { - adapter: async ({ remixConfig }) => ({ - serverBundles(...args) { - // This lets us assert that user options are passed to adapter options hook - return remixConfig.serverBundles?.(...args) + "--adapter-options"; - }, - async buildEnd(args) { - let fs = await import("node:fs/promises"); - await fs.writeFile( - "BUILD_END_ARGS.json", - JSON.stringify( - args, - function replacer(key, value) { - return typeof value === "function" - ? value.toString() - : value; - }, - 2, - ), - "utf-8"); - } - }), - - serverBundles() { - return "user-options"; - } - }, - `, - }), - }); - stop = await viteDev({ cwd, port }); - }); - test.afterAll(() => stop()); - - test("Vite / adapter / serverBundles and buildEnd hooks", async () => { - let { status } = viteBuild({ cwd }); - expect(status).toBe(0); - - let buildEndArgs: any = JSON.parse( - fs.readFileSync(path.join(cwd, "BUILD_END_ARGS.json"), "utf8") - ); - let { remixConfig } = buildEndArgs; - - // Before rewriting to relative paths, assert that paths are absolute within cwd - expect(pathStartsWithCwd(remixConfig.buildDirectory)).toBe(true); - - // Rewrite path args to be relative and normalized for snapshot test - remixConfig.buildDirectory = relativeToCwd(remixConfig.buildDirectory); - - expect(Object.keys(buildEndArgs)).toEqual(["buildManifest", "remixConfig"]); - - // Smoke test the resolved config - expect(Object.keys(buildEndArgs.remixConfig)).toEqual([ - "adapter", - "appDirectory", - "buildDirectory", - "future", - "manifest", - "publicPath", - "routes", - "serverBuildFile", - "serverBundles", - "serverModuleFormat", - "unstable_ssr", - ]); - - // Ensure we get a valid build manifest - expect(buildEndArgs.buildManifest).toEqual({ - routeIdToServerBundleId: { - "routes/_index": "user-options--adapter-options", - }, - routes: { - root: { - file: "app/root.tsx", - id: "root", - path: "", - }, - "routes/_index": { - file: "app/routes/_index.tsx", - id: "routes/_index", - index: true, - parentId: "root", - }, - }, - serverBundles: { - "user-options--adapter-options": { - file: "build/server/user-options--adapter-options/index.js", - id: "user-options--adapter-options", - }, - }, - }); - }); -}); diff --git a/integration/vite-cloudflare-test.ts b/integration/vite-cloudflare-test.ts index deff4241247..ea636bb03b7 100644 --- a/integration/vite-cloudflare-test.ts +++ b/integration/vite-cloudflare-test.ts @@ -51,7 +51,8 @@ test.describe("Vite / cloudflare", async () => { ), "vite.config.ts": await VITE_CONFIG({ port, - pluginOptions: `{ adapter: (await import("@remix-run/dev")).unstable_vitePluginAdapterCloudflare() }`, + viteSsrResolveExternalConditions: ["workerd", "worker"], + pluginOptions: `{ presets: [(await import("@remix-run/dev")).unstable_vitePluginPresetCloudflare()] }`, }), "functions/[[page]].ts": ` import { createPagesFunctionHandler } from "@remix-run/cloudflare-pages"; diff --git a/integration/vite-presets-test.ts b/integration/vite-presets-test.ts new file mode 100644 index 00000000000..bc6568c8e89 --- /dev/null +++ b/integration/vite-presets-test.ts @@ -0,0 +1,200 @@ +import fs from "node:fs/promises"; +import * as path from "node:path"; +import URL from "node:url"; +import { test, expect } from "@playwright/test"; +import { normalizePath } from "vite"; +import getPort from "get-port"; + +import { + createProject, + viteDev, + viteBuild, + VITE_CONFIG, +} from "./helpers/vite.js"; + +const js = String.raw; + +test.describe(async () => { + let port: number; + let cwd: string; + let stop: () => void; + + function pathStartsWithCwd(pathname: string) { + return normalizePath(pathname).startsWith(normalizePath(cwd)); + } + + function relativeToCwd(pathname: string) { + return normalizePath(path.relative(cwd, pathname)); + } + + test.beforeAll(async () => { + port = await getPort(); + cwd = await createProject({ + "vite.config.ts": await VITE_CONFIG({ + port, + pluginOptions: js` + { + presets: [ + // Ensure preset config takes lower precedence than user config + { + name: "test-preset", + remixConfig: async () => ({ + appDirectory: "INCORRECT_APP_DIR", // This is overridden by the user config further down this file + }), + }, + { + name: "test-preset", + remixConfigResolved: async ({ remixConfig }) => { + if (remixConfig.appDirectory.includes("INCORRECT_APP_DIR")) { + throw new Error("Remix preset config wasn't overridden with user config"); + } + } + }, + + // Ensure config presets are merged in the correct order + { + name: "test-preset", + remixConfig: async () => ({ + buildDirectory: "INCORRECT_BUILD_DIR", + }), + }, + { + name: "test-preset", + remixConfig: async () => ({ + buildDirectory: "build", + }), + }, + + // Ensure remixConfigResolved is called with a frozen Remix config + { + name: "test-preset", + remixConfigResolved: async ({ remixConfig }) => { + let isDeepFrozen = (obj: any) => + Object.isFrozen(obj) && + Object.keys(obj).every( + prop => typeof obj[prop] !== 'object' || isDeepFrozen(obj[prop]) + ); + + let fs = await import("node:fs/promises"); + await fs.writeFile("PRESET_REMIX_CONFIG_RESOLVED_META.json", JSON.stringify({ + remixConfigFrozen: isDeepFrozen(remixConfig), + }), "utf-8"); + } + }, + + // Ensure presets can set serverBundles option (this is critical for Vercel support) + { + name: "test-preset", + remixConfig: async () => ({ + serverBundles() { + return "preset-server-bundle-id"; + }, + }), + }, + + // Ensure presets can set buildEnd option (this is critical for Vercel support) + { + name: "test-preset", + remixConfig: async () => ({ + async buildEnd(buildEndArgs) { + let fs = await import("node:fs/promises"); + let serializeJs = (await import("serialize-javascript")).default; + + await fs.writeFile( + "BUILD_END_ARGS.js", + "export default " + serializeJs(buildEndArgs, { space: 2, unsafe: true }), + "utf-8" + ); + }, + }), + }, + ], + + // Ensure user config takes precedence over preset config + appDirectory: "app", + }, + `, + }), + }); + stop = await viteDev({ cwd, port }); + }); + test.afterAll(() => stop()); + + test("Vite / presets", async () => { + let { status, stderr } = viteBuild({ cwd }); + expect(stderr.toString()).toBeFalsy(); + expect(status).toBe(0); + + let buildEndArgs: any = ( + await import(URL.pathToFileURL(path.join(cwd, "BUILD_END_ARGS.js")).href) + ).default; + let { remixConfig } = buildEndArgs; + + // Before rewriting to relative paths, assert that paths are absolute within cwd + expect(pathStartsWithCwd(remixConfig.buildDirectory)).toBe(true); + + // Rewrite path args to be relative and normalized for snapshot test + remixConfig.buildDirectory = relativeToCwd(remixConfig.buildDirectory); + + // Ensure preset configs are merged in correct order, resulting in the correct build directory + expect(remixConfig.buildDirectory).toBe("build"); + + // Ensure preset config takes lower precedence than user config + expect(remixConfig.serverModuleFormat).toBe("esm"); + + // Ensure `remixConfigResolved` is called with a frozen Remix config + expect( + JSON.parse( + await fs.readFile( + path.join(cwd, "PRESET_REMIX_CONFIG_RESOLVED_META.json"), + "utf-8" + ) + ) + ).toEqual({ + remixConfigFrozen: true, + }); + + expect(Object.keys(buildEndArgs)).toEqual(["buildManifest", "remixConfig"]); + + // Smoke test the resolved config + expect(Object.keys(buildEndArgs.remixConfig)).toEqual([ + "appDirectory", + "buildDirectory", + "buildEnd", + "future", + "manifest", + "publicPath", + "routes", + "serverBuildFile", + "serverBundles", + "serverModuleFormat", + "unstable_ssr", + ]); + + // Ensure we get a valid build manifest + expect(buildEndArgs.buildManifest).toEqual({ + routeIdToServerBundleId: { + "routes/_index": "preset-server-bundle-id", + }, + routes: { + root: { + file: "app/root.tsx", + id: "root", + path: "", + }, + "routes/_index": { + file: "app/routes/_index.tsx", + id: "routes/_index", + index: true, + parentId: "root", + }, + }, + serverBundles: { + "preset-server-bundle-id": { + file: "build/server/preset-server-bundle-id/index.js", + id: "preset-server-bundle-id", + }, + }, + }); + }); +}); diff --git a/packages/remix-dev/index.ts b/packages/remix-dev/index.ts index db014d6db65..72bc165736d 100644 --- a/packages/remix-dev/index.ts +++ b/packages/remix-dev/index.ts @@ -6,11 +6,8 @@ export * as cli from "./cli/index"; export type { Manifest as AssetsManifest } from "./manifest"; export { getDependenciesToBundle } from "./dependencies"; -export type { - Unstable_BuildManifest, - Unstable_VitePluginAdapter, -} from "./vite"; +export type { Unstable_BuildManifest, Unstable_VitePluginPreset } from "./vite"; export { unstable_vitePlugin, - unstable_vitePluginAdapterCloudflare, + unstable_vitePluginPresetCloudflare, } from "./vite"; diff --git a/packages/remix-dev/vite/adapters/cloudflare.ts b/packages/remix-dev/vite/adapters/cloudflare.ts deleted file mode 100644 index 88983bf471c..00000000000 --- a/packages/remix-dev/vite/adapters/cloudflare.ts +++ /dev/null @@ -1,13 +0,0 @@ -export const adapter = () => async () => { - let { getBindingsProxy } = await import("wrangler"); - let { bindings } = await getBindingsProxy(); - let loadContext = bindings && { env: bindings }; - let viteConfig = { - ssr: { - resolve: { - externalConditions: ["workerd", "worker"], - }, - }, - }; - return { viteConfig, loadContext }; -}; diff --git a/packages/remix-dev/vite/build.ts b/packages/remix-dev/vite/build.ts index e2e5b2e60c2..e70c9956a26 100644 --- a/packages/remix-dev/vite/build.ts +++ b/packages/remix-dev/vite/build.ts @@ -354,7 +354,7 @@ export async function build( ); } - await remixConfig.adapter?.buildEnd?.({ + await remixConfig.buildEnd?.({ buildManifest, remixConfig, }); diff --git a/packages/remix-dev/vite/index.ts b/packages/remix-dev/vite/index.ts index 332ab2816b0..e906c55f0b0 100644 --- a/packages/remix-dev/vite/index.ts +++ b/packages/remix-dev/vite/index.ts @@ -4,7 +4,7 @@ import type { RemixVitePlugin } from "./plugin"; export type { BuildManifest as Unstable_BuildManifest, - VitePluginAdapter as Unstable_VitePluginAdapter, + VitePluginPreset as Unstable_VitePluginPreset, } from "./plugin"; export const unstable_vitePlugin: RemixVitePlugin = (...args) => { @@ -13,4 +13,4 @@ export const unstable_vitePlugin: RemixVitePlugin = (...args) => { return remixVitePlugin(...args); }; -export { adapter as unstable_vitePluginAdapterCloudflare } from "./adapters/cloudflare"; +export { preset as unstable_vitePluginPresetCloudflare } from "./presets/cloudflare"; diff --git a/packages/remix-dev/vite/plugin.ts b/packages/remix-dev/vite/plugin.ts index 181871d43a6..7c5daa4b5ad 100644 --- a/packages/remix-dev/vite/plugin.ts +++ b/packages/remix-dev/vite/plugin.ts @@ -115,51 +115,47 @@ export type ServerBundlesBuildManifest = BaseBuildManifest & { export type BuildManifest = DefaultBuildManifest | ServerBundlesBuildManifest; -const adapterRemixConfigOverrideKeys = [ - "serverBundles", +const excludedRemixConfigPresetKeys = [ + "presets", ] as const satisfies ReadonlyArray; -type AdapterRemixConfigOverrideKey = - typeof adapterRemixConfigOverrideKeys[number]; +type ExcludedRemixConfigPresetKey = + typeof excludedRemixConfigPresetKeys[number]; -type AdapterRemixConfigOverrides = Pick< - VitePluginConfig, - AdapterRemixConfigOverrideKey ->; +type RemixConfigPreset = Omit; -type AdapterConfig = AdapterRemixConfigOverrides & { - loadContext?: Record; - buildEnd?: BuildEndHook; - viteConfig: Vite.UserConfig; +export type VitePluginPreset = { + name: string; + remixConfig?: () => RemixConfigPreset | Promise; + remixConfigResolved?: (args: { + remixConfig: ResolvedVitePluginConfig; + }) => void | Promise; }; -type Adapter = Omit; - -export type VitePluginAdapter = (args: { - remixConfig: VitePluginConfig; - viteConfig: Vite.UserConfig; -}) => AdapterConfig | Promise; - export type VitePluginConfig = RemixEsbuildUserConfigJsdocOverrides & Omit< SupportedRemixEsbuildUserConfig, keyof RemixEsbuildUserConfigJsdocOverrides > & { - /** - * A function for adapting the build output and/or development environment - * for different hosting providers. - */ - adapter?: VitePluginAdapter; /** * The path to the build directory, relative to the project. Defaults to * `"build"`. */ buildDirectory?: string; /** - * Whether to write a `"manifest.json"` file to the build directory. + * A function that is called after the full Remix build is complete. + */ + buildEnd?: BuildEndHook; + /** + * Whether to write a `"manifest.json"` file to the build directory.` * Defaults to `false`. */ manifest?: boolean; + /** + * An array of Remix config presets to ease integration with other platforms + * and tools. + */ + presets?: Array; /** * The file name of the server build output. This file * should end in a `.js` extension and should be deployed to your server. @@ -186,17 +182,19 @@ type BuildEndHook = (args: { buildManifest: BuildManifest | undefined; }) => void | Promise; -export type ResolvedVitePluginConfig = Pick< - ResolvedRemixEsbuildConfig, - "appDirectory" | "future" | "publicPath" | "routes" | "serverModuleFormat" -> & { - adapter?: Adapter; - buildDirectory: string; - manifest: boolean; - serverBuildFile: string; - serverBundles?: ServerBundlesFunction; - unstable_ssr: boolean; -}; +export type ResolvedVitePluginConfig = Readonly< + Pick< + ResolvedRemixEsbuildConfig, + "appDirectory" | "future" | "publicPath" | "routes" | "serverModuleFormat" + > & { + buildDirectory: string; + buildEnd?: BuildEndHook; + manifest: boolean; + serverBuildFile: string; + serverBundles?: ServerBundlesFunction; + unstable_ssr: boolean; + } +>; export type ServerBundleBuildConfig = { routes: RouteManifest; @@ -431,6 +429,98 @@ export let getServerBuildDirectory = (ctx: RemixPluginContext) => let getClientBuildDirectory = (remixConfig: ResolvedVitePluginConfig) => path.join(remixConfig.buildDirectory, "client"); +let mergeRemixConfig = (...configs: VitePluginConfig[]): VitePluginConfig => { + let reducer = ( + configA: VitePluginConfig, + configB: VitePluginConfig + ): VitePluginConfig => { + let mergeRequired = (key: keyof VitePluginConfig) => + configA[key] !== undefined && configB[key] !== undefined; + + return { + ...configA, + ...configB, + ...(mergeRequired("buildEnd") + ? { + buildEnd: async (...args) => { + await Promise.all([ + configA.buildEnd?.(...args), + configB.buildEnd?.(...args), + ]); + }, + } + : {}), + ...(mergeRequired("future") + ? { + future: { + ...configA.future, + ...configB.future, + }, + } + : {}), + ...(mergeRequired("ignoredRouteFiles") + ? { + ignoredRouteFiles: Array.from( + new Set([ + ...(configA.ignoredRouteFiles ?? []), + ...(configB.ignoredRouteFiles ?? []), + ]) + ), + } + : {}), + ...(mergeRequired("presets") + ? { + presets: [...(configA.presets ?? []), ...(configB.presets ?? [])], + } + : {}), + ...(mergeRequired("routes") + ? { + routes: async (...args) => { + let [routesA, routesB] = await Promise.all([ + configA.routes?.(...args), + configB.routes?.(...args), + ]); + + return { + ...routesA, + ...routesB, + }; + }, + } + : {}), + }; + }; + + return configs.reduce(reducer, {}); +}; + +let remixDevLoadContext: Record | undefined; + +export let setRemixDevLoadContext = (loadContext: Record) => { + remixDevLoadContext = loadContext; +}; + +// Inlined from https://github.com/jsdf/deep-freeze +let deepFreeze = (o: any) => { + Object.freeze(o); + let oIsFunction = typeof o === "function"; + let hasOwnProp = Object.prototype.hasOwnProperty; + Object.getOwnPropertyNames(o).forEach(function (prop) { + if ( + hasOwnProp.call(o, prop) && + (oIsFunction + ? prop !== "caller" && prop !== "callee" && prop !== "arguments" + : true) && + o[prop] !== null && + (typeof o[prop] === "object" || typeof o[prop] === "function") && + !Object.isFrozen(o[prop]) + ) { + deepFreeze(o[prop]); + } + }); + return o; +}; + export type RemixVitePlugin = (config?: VitePluginConfig) => Vite.Plugin[]; export const remixVitePlugin: RemixVitePlugin = (remixUserConfig = {}) => { let viteCommand: Vite.ResolvedConfig["command"]; @@ -448,6 +538,31 @@ export const remixVitePlugin: RemixVitePlugin = (remixUserConfig = {}) => { /** Mutates `ctx` as a side-effect */ let updateRemixPluginContext = async (): Promise => { + let remixConfigPresets: VitePluginConfig[] = ( + await Promise.all( + (remixUserConfig.presets ?? []).map(async (preset) => { + if (!preset.name) { + throw new Error( + "Remix presets must have a `name` property defined." + ); + } + + if (!preset.remixConfig) { + return null; + } + + let remixConfigPreset: VitePluginConfig = omit( + await preset.remixConfig(), + excludedRemixConfigPresetKeys + ); + + return remixConfigPreset; + }) + ) + ).filter(function isNotNull(value: T | null): value is T { + return value !== null; + }); + let defaults = { buildDirectory: "build", manifest: false, @@ -456,29 +571,15 @@ export const remixVitePlugin: RemixVitePlugin = (remixUserConfig = {}) => { unstable_ssr: true, } as const satisfies Partial; - let adapterConfig = remixUserConfig.adapter - ? await remixUserConfig.adapter({ - // We only pass in the plugin config that the user defined. We don't - // know the final resolved config until the adapter has been resolved. - remixConfig: remixUserConfig, - viteConfig: viteUserConfig, - }) - : undefined; - let adapter: Adapter | undefined = - adapterConfig && omit(adapterConfig, adapterRemixConfigOverrideKeys); - let adapterRemixConfigOverrides: AdapterRemixConfigOverrides | undefined = - adapterConfig && pick(adapterConfig, adapterRemixConfigOverrideKeys); - let resolvedRemixUserConfig = { - ...defaults, - ...remixUserConfig, - ...(adapterRemixConfigOverrides ?? {}), - } satisfies VitePluginConfig; + ...defaults, // Default values should be completely overridden by user/preset config, not merged + ...mergeRemixConfig(...remixConfigPresets, remixUserConfig), + }; let rootDirectory = viteUserConfig.root ?? process.env.REMIX_ROOT ?? process.cwd(); - let { manifest, unstable_ssr } = resolvedRemixUserConfig; + let { buildEnd, manifest, unstable_ssr } = resolvedRemixUserConfig; let isSpaMode = !unstable_ssr; // Only select the Remix esbuild config options that the Vite plugin uses @@ -523,10 +624,10 @@ export const remixVitePlugin: RemixVitePlugin = (remixUserConfig = {}) => { routes = serverBundleBuildConfig.routes; } - let remixConfig: ResolvedVitePluginConfig = { - adapter, + let remixConfig: ResolvedVitePluginConfig = deepFreeze({ appDirectory, buildDirectory, + buildEnd, future, manifest, publicPath, @@ -535,7 +636,11 @@ export const remixVitePlugin: RemixVitePlugin = (remixUserConfig = {}) => { serverBundles, serverModuleFormat, unstable_ssr, - }; + }); + + for (let preset of remixUserConfig.presets ?? []) { + await preset.remixConfigResolved?.({ remixConfig }); + } let viteManifestEnabled = viteUserConfig.build?.manifest === true; @@ -759,7 +864,7 @@ export const remixVitePlugin: RemixVitePlugin = (remixUserConfig = {}) => { ) ); - let defaults = { + return { __remixPluginContext: ctx, appType: "custom", optimizeDeps: { @@ -844,10 +949,6 @@ export const remixVitePlugin: RemixVitePlugin = (remixUserConfig = {}) => { }, }), }; - return vite.mergeConfig( - defaults, - ctx.remixConfig.adapter?.viteConfig ?? {} - ); }, async configResolved(resolvedViteConfig) { await initEsModuleLexer; @@ -1031,8 +1132,7 @@ export const remixVitePlugin: RemixVitePlugin = (remixUserConfig = {}) => { nodeRes ) => { let req = fromNodeRequest(nodeReq); - let { adapter } = ctx.remixConfig; - let res = await handler(req, adapter?.loadContext); + let res = await handler(req, remixDevLoadContext); await toNodeRequest(res, nodeRes); }; await nodeHandler(req, res); diff --git a/packages/remix-dev/vite/presets/cloudflare.ts b/packages/remix-dev/vite/presets/cloudflare.ts new file mode 100644 index 00000000000..29b978c63e8 --- /dev/null +++ b/packages/remix-dev/vite/presets/cloudflare.ts @@ -0,0 +1,14 @@ +import { type VitePluginPreset, setRemixDevLoadContext } from "../plugin"; + +export const preset: () => VitePluginPreset = () => ({ + name: "cloudflare", + remixConfig: async () => { + let { getBindingsProxy } = await import("wrangler"); + let { bindings } = await getBindingsProxy(); + let loadContext = bindings && { env: bindings }; + + setRemixDevLoadContext(loadContext); + + return {}; + }, +}); diff --git a/templates/unstable-vite-cloudflare/vite.config.ts b/templates/unstable-vite-cloudflare/vite.config.ts index ebb089b505a..64a80932aca 100644 --- a/templates/unstable-vite-cloudflare/vite.config.ts +++ b/templates/unstable-vite-cloudflare/vite.config.ts @@ -1,14 +1,19 @@ import { unstable_vitePlugin as remix, - unstable_vitePluginAdapterCloudflare as cloudflare, + unstable_vitePluginPresetCloudflare as cloudflare, } from "@remix-run/dev"; import { defineConfig } from "vite"; import tsconfigPaths from "vite-tsconfig-paths"; export default defineConfig({ + ssr: { + resolve: { + externalConditions: ["workerd", "worker"], + }, + }, plugins: [ remix({ - adapter: cloudflare(), + presets: [cloudflare()], }), tsconfigPaths(), ],