diff --git a/.changeset/ninety-turtles-explode.md b/.changeset/ninety-turtles-explode.md new file mode 100644 index 00000000000..f25f4541fd0 --- /dev/null +++ b/.changeset/ninety-turtles-explode.md @@ -0,0 +1,45 @@ +--- +"@remix-run/dev": patch +"@remix-run/react": patch +--- + +Vite: Replace with + +**This is a breaking change for projects using the unstable Vite plugin.** + +The `` component has a confusing name as it now also supports HMR and HDR. +Additionally, it provides an bespoke client-side runtime that is obsoleted by Vite. +To get our Vite plugin working, we were doing some compiler magic to swap out the +implementation of ``. +This was always meant as a temporary measure. + +Now we have a better solution in the form of a new `` component specifically +designed with Vite's HMR capabilities in mind. + +The `` component will cease to provide HMR and HDR capabilities in Vite, +so you'll need to replace `` with `` in your app. + +The `` component should be placed in the `` of your app so that it +can be loaded before any other scripts as required by React Fast Refresh. + +```diff + import { +- LiveReload, ++ DevScripts, + Outlet, + } + + export default function App() { + return ( + + ++ + + +- + + + + ) + } +``` diff --git a/docs/future/vite.md b/docs/future/vite.md index 51040a0f88a..8034d4f12fb 100644 --- a/docs/future/vite.md +++ b/docs/future/vite.md @@ -243,6 +243,43 @@ export default defineConfig({ }); ``` +#### HMR & HDR + +The new `` component enables development-specific features like HMR and HDR. +`` automatically removes itself in production, just like the old `` component. +But unlike ``, it works with Vite's out-of-the-box HMR capabilities. + + + +The `` component should be placed in the `` of your app so that it +can be loaded before any other scripts as required by [React Fast Refresh][react-fast-refresh]. + + + +👉 **Replace `` with ``** + +```diff + import { +- LiveReload, ++ DevScripts, + Outlet, + } + + export default function App() { + return ( + + ++ + + +- + + + + ) + } +``` + #### TypeScript integration Vite handles imports for all sorts of different file types, sometimes in ways that differ from the existing Remix compiler, so let's reference Vite's types from `vite/client` instead of the obsolete types from `@remix-run/dev`. @@ -1149,3 +1186,4 @@ We're definitely late to the Vite party, but we're excited to be here now! [wrangler-getbindingsproxy]: https://github.com/cloudflare/workers-sdk/pull/4523 [remix-config-server]: https://remix.run/docs/en/main/file-conventions/remix-config#server [cloudflare-vite-and-wrangler]: #vite--wrangler +[react-fast-refresh]: https://github.com/facebook/react/issues/16604#issuecomment-528663101 diff --git a/integration/helpers/vite-template/app/root.tsx b/integration/helpers/vite-template/app/root.tsx index 1d6916394c3..44aba0660dc 100644 --- a/integration/helpers/vite-template/app/root.tsx +++ b/integration/helpers/vite-template/app/root.tsx @@ -1,6 +1,6 @@ import { + DevScripts, Links, - LiveReload, Meta, Outlet, Scripts, @@ -15,12 +15,12 @@ export default function App() { + - ); diff --git a/integration/vite-css-test.ts b/integration/vite-css-test.ts index 1a5c21a082c..a8c3a448402 100644 --- a/integration/vite-css-test.ts +++ b/integration/vite-css-test.ts @@ -34,7 +34,7 @@ const files = { }); `, "app/root.tsx": ` - import { Links, Meta, Outlet, Scripts, LiveReload } from "@remix-run/react"; + import { Links, Meta, Outlet, Scripts, DevScripts } from "@remix-run/react"; export default function Root() { return ( @@ -42,11 +42,11 @@ const files = { + - ); diff --git a/integration/vite-dev-test.ts b/integration/vite-dev-test.ts index ba9e17e508b..acf5bedf336 100644 --- a/integration/vite-dev-test.ts +++ b/integration/vite-dev-test.ts @@ -40,7 +40,7 @@ test.describe("Vite dev", () => { }); `, "app/root.tsx": js` - import { Links, Meta, Outlet, Scripts, LiveReload } from "@remix-run/react"; + import { Links, Meta, Outlet, Scripts, DevScripts } from "@remix-run/react"; export default function Root() { return ( @@ -48,6 +48,7 @@ test.describe("Vite dev", () => { +
@@ -55,7 +56,6 @@ test.describe("Vite dev", () => {
- ); @@ -330,7 +330,7 @@ test.describe("Vite dev", () => { await expect(hmrStatus).toHaveText("HMR updated: yes"); await expect(input).toHaveValue("stateful"); - // check LiveReload script has nonce + // check DevScripts script has nonce await expect(page.locator(`script[nonce="1234"]`)).toBeAttached(); // Ensure no errors after HMR diff --git a/integration/vite-server-bundles-test.ts b/integration/vite-server-bundles-test.ts index 316ec571be7..b593511dea4 100644 --- a/integration/vite-server-bundles-test.ts +++ b/integration/vite-server-bundles-test.ts @@ -74,7 +74,7 @@ const TEST_ROUTES = [ const files = { "app/root.tsx": ` ${ROUTE_FILE_COMMENT} - import { Links, Meta, Outlet, Scripts, LiveReload } from "@remix-run/react"; + import { Links, Meta, Outlet, Scripts, DevScripts } from "@remix-run/react"; export default function Root() { return ( @@ -82,11 +82,11 @@ const files = { + - ); diff --git a/packages/remix-dev/vite/plugin.ts b/packages/remix-dev/vite/plugin.ts index a23ca54e989..1d1ec1bcdc5 100644 --- a/packages/remix-dev/vite/plugin.ts +++ b/packages/remix-dev/vite/plugin.ts @@ -37,7 +37,6 @@ import { getStylesForUrl, isCssModulesFile } from "./styles"; import * as VirtualModule from "./vmod"; import { resolveFileUrl } from "./resolve-file-url"; import { removeExports } from "./remove-exports"; -import { replaceImportSpecifier } from "./replace-import-specifier"; import { importViteEsmSync, preloadViteEsm } from "./import-vite-esm-sync"; const supportedRemixEsbuildConfigKeys = [ @@ -227,7 +226,6 @@ export type RemixPluginContext = RemixPluginSsrBuildContext & { let serverBuildId = VirtualModule.id("server-build"); let serverManifestId = VirtualModule.id("server-manifest"); let browserManifestId = VirtualModule.id("browser-manifest"); -let remixReactProxyId = VirtualModule.id("remix-react-proxy"); let hmrRuntimeId = VirtualModule.id("hmr-runtime"); let injectHmrRuntimeId = VirtualModule.id("inject-hmr-runtime"); @@ -1280,54 +1278,6 @@ export const remixVitePlugin: RemixVitePlugin = (remixUserConfig = {}) => { }; }, }, - { - name: "remix-remix-react-proxy", - enforce: "post", // Ensure we're operating on the transformed code to support MDX etc. - resolveId(id) { - if (id === remixReactProxyId) { - return VirtualModule.resolve(remixReactProxyId); - } - }, - transform(code, id) { - // Don't transform the proxy itself, otherwise it will import itself - if (id === VirtualModule.resolve(remixReactProxyId)) { - return; - } - - let hasLiveReloadHints = - code.includes("LiveReload") && code.includes("@remix-run/react"); - - // Don't transform files that don't need the proxy - if (!hasLiveReloadHints) { - return; - } - - // Rewrite imports to use the proxy - return replaceImportSpecifier({ - code, - specifier: "@remix-run/react", - replaceWith: remixReactProxyId, - }); - }, - load(id) { - if (id === VirtualModule.resolve(remixReactProxyId)) { - // TODO: ensure react refresh is initialized before `` - return [ - 'import { createElement } from "react";', - 'export * from "@remix-run/react";', - `export const LiveReload = ${ - viteCommand !== "serve" - } ? () => null : `, - '({ nonce = undefined }) => createElement("script", {', - " nonce,", - " dangerouslySetInnerHTML: { ", - " __html: `window.__remixLiveReloadEnabled = true`", - " }", - "});", - ].join("\n"); - } - }, - }, { name: "remix-inject-hmr-runtime", enforce: "pre", @@ -1504,7 +1454,7 @@ const inWebWorker = typeof WorkerGlobalScope !== 'undefined' && self instanceof let prevRefreshReg; let prevRefreshSig; -if (import.meta.hot && !inWebWorker && window.__remixLiveReloadEnabled) { +if (import.meta.hot && !inWebWorker && window.__remixHmrEnabled) { if (!window.__vite_plugin_react_preamble_installed__) { throw new Error( "Remix Vite plugin can't detect preamble. Something is wrong." @@ -1520,7 +1470,7 @@ if (import.meta.hot && !inWebWorker && window.__remixLiveReloadEnabled) { }`.replace(/\n+/g, ""); const REACT_REFRESH_FOOTER = ` -if (import.meta.hot && !inWebWorker && window.__remixLiveReloadEnabled) { +if (import.meta.hot && !inWebWorker && window.__remixHmrEnabled) { window.$RefreshReg$ = prevRefreshReg; window.$RefreshSig$ = prevRefreshSig; RefreshRuntime.__hmr_import(import.meta.url).then((currentExports) => { diff --git a/packages/remix-dev/vite/replace-import-specifier.ts b/packages/remix-dev/vite/replace-import-specifier.ts deleted file mode 100644 index 8f1fd59c07c..00000000000 --- a/packages/remix-dev/vite/replace-import-specifier.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { parse, traverse, generate } from "./babel"; - -export const replaceImportSpecifier = ({ - code, - specifier, - replaceWith, -}: { - code: string; - specifier: string; - replaceWith: string; -}) => { - let ast = parse(code, { sourceType: "module" }); - - traverse(ast, { - ImportDeclaration(path) { - if (path.node.source.value === specifier) { - path.node.source.value = replaceWith; - } - }, - }); - - return { - code: generate(ast, { retainLines: true }).code, - map: null, - }; -}; diff --git a/packages/remix-react/components.tsx b/packages/remix-react/components.tsx index f9604f7655e..65e7fcb880c 100644 --- a/packages/remix-react/components.tsx +++ b/packages/remix-react/components.tsx @@ -1060,6 +1060,18 @@ export function useFetcher( return useFetcherRR(opts); } +export const DevScripts = + process.env.NODE_ENV === "production" + ? () => null + : ({ nonce }: { nonce?: undefined }) => ( +