diff --git a/.changeset/orange-lobsters-lay.md b/.changeset/orange-lobsters-lay.md new file mode 100644 index 0000000000..0e66f00606 --- /dev/null +++ b/.changeset/orange-lobsters-lay.md @@ -0,0 +1,5 @@ +--- +"@react-router/dev": patch +--- + +fix(dev): add param to route css so it is not deduped by react diff --git a/.github/workflows/close-no-repro-issues.yml b/.github/workflows/close-no-repro-issues.yml index f9a6e2e45f..efc77ff396 100644 --- a/.github/workflows/close-no-repro-issues.yml +++ b/.github/workflows/close-no-repro-issues.yml @@ -31,7 +31,7 @@ jobs: uses: pnpm/action-setup@v4.1.0 - name: ⎔ Setup node - uses: actions/setup-node@v5 + uses: actions/setup-node@v6 with: # required for --experimental-strip-types node-version: 22 diff --git a/.github/workflows/deduplicate-lock-file.yml b/.github/workflows/deduplicate-lock-file.yml index 51d6c758b4..3174fcfcbb 100644 --- a/.github/workflows/deduplicate-lock-file.yml +++ b/.github/workflows/deduplicate-lock-file.yml @@ -26,7 +26,7 @@ jobs: uses: pnpm/action-setup@v4 - name: ⎔ Setup node - uses: actions/setup-node@v5 + uses: actions/setup-node@v6 with: node-version-file: ".nvmrc" cache: "pnpm" diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 86ced6e59e..1e6391dfd8 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -35,7 +35,7 @@ jobs: uses: pnpm/action-setup@v4 - name: ⎔ Setup node - uses: actions/setup-node@v5 + uses: actions/setup-node@v6 with: node-version-file: ".nvmrc" cache: pnpm diff --git a/.github/workflows/format.yml b/.github/workflows/format.yml index 459c848d12..ff63cd21ee 100644 --- a/.github/workflows/format.yml +++ b/.github/workflows/format.yml @@ -25,7 +25,7 @@ jobs: uses: pnpm/action-setup@v4 - name: ⎔ Setup node - uses: actions/setup-node@v5 + uses: actions/setup-node@v6 with: node-version-file: ".nvmrc" cache: pnpm diff --git a/.github/workflows/release-experimental.yml b/.github/workflows/release-experimental.yml index 1b4624d7f4..373afa10ba 100644 --- a/.github/workflows/release-experimental.yml +++ b/.github/workflows/release-experimental.yml @@ -29,7 +29,7 @@ jobs: uses: pnpm/action-setup@v4 - name: ⎔ Setup node - uses: actions/setup-node@v5 + uses: actions/setup-node@v6 with: node-version-file: ".nvmrc" cache: "pnpm" diff --git a/.github/workflows/release-nightly.yml b/.github/workflows/release-nightly.yml index b394521418..be5ec510e8 100644 --- a/.github/workflows/release-nightly.yml +++ b/.github/workflows/release-nightly.yml @@ -40,7 +40,7 @@ jobs: uses: pnpm/action-setup@v4 - name: ⎔ Setup node - uses: actions/setup-node@v5 + uses: actions/setup-node@v6 with: node-version-file: ".nvmrc" cache: "pnpm" diff --git a/.github/workflows/release-stage-2-alpha.yml b/.github/workflows/release-stage-2-alpha.yml index 40157097f3..c82f906205 100644 --- a/.github/workflows/release-stage-2-alpha.yml +++ b/.github/workflows/release-stage-2-alpha.yml @@ -35,7 +35,7 @@ jobs: uses: pnpm/action-setup@v4 - name: ⎔ Setup node - uses: actions/setup-node@v5 + uses: actions/setup-node@v6 with: node-version-file: ".nvmrc" cache: "pnpm" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 366c8dcc9f..b88bb74292 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -35,7 +35,7 @@ jobs: uses: pnpm/action-setup@v4 - name: ⎔ Setup node - uses: actions/setup-node@v5 + uses: actions/setup-node@v6 with: node-version-file: ".nvmrc" cache: "pnpm" @@ -84,7 +84,7 @@ jobs: uses: pnpm/action-setup@v4 - name: ⎔ Setup node - uses: actions/setup-node@v5 + uses: actions/setup-node@v6 with: node-version-file: ".nvmrc" cache: "pnpm" diff --git a/.github/workflows/shared-build.yml b/.github/workflows/shared-build.yml index 345ec9c300..4a3617aa6c 100644 --- a/.github/workflows/shared-build.yml +++ b/.github/workflows/shared-build.yml @@ -17,7 +17,7 @@ jobs: uses: pnpm/action-setup@v4 - name: ⎔ Setup node - uses: actions/setup-node@v5 + uses: actions/setup-node@v6 with: node-version-file: ".nvmrc" cache: "pnpm" diff --git a/.github/workflows/shared-integration.yml b/.github/workflows/shared-integration.yml index 0454fc2cca..782507c3ac 100644 --- a/.github/workflows/shared-integration.yml +++ b/.github/workflows/shared-integration.yml @@ -44,7 +44,7 @@ jobs: uses: pnpm/action-setup@v4 - name: ⎔ Setup node ${{ matrix.node }} - uses: actions/setup-node@v5 + uses: actions/setup-node@v6 with: node-version: ${{ matrix.node }} cache: "pnpm" diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index d510f93a25..2bf0dedbe9 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -39,7 +39,7 @@ jobs: uses: pnpm/action-setup@v4 - name: ⎔ Setup node - uses: actions/setup-node@v5 + uses: actions/setup-node@v6 with: node-version: ${{ matrix.node }} cache: pnpm diff --git a/contributors.yml b/contributors.yml index 0b1dee6722..7f5bbd347a 100644 --- a/contributors.yml +++ b/contributors.yml @@ -190,6 +190,7 @@ - johnpangalos - jonkoops - joseph0926 +- joshuaellis - jplhomer - jrakotoharisoa - jrestall @@ -308,6 +309,7 @@ - pawelblaszczyk5 - pcattori - penx +- peterneave - petersendidit - phildl - phryneas diff --git a/docs/api/data-routers/createHashRouter.md b/docs/api/data-routers/createHashRouter.md index 528410ec37..307944a3fa 100644 --- a/docs/api/data-routers/createHashRouter.md +++ b/docs/api/data-routers/createHashRouter.md @@ -23,7 +23,7 @@ https://github.com/remix-run/react-router/blob/main/packages/react-router/lib/do [Reference Documentation ↗](https://api.reactrouter.com/v7/functions/react_router.createHashRouter.html) Create a new [data router](https://api.reactrouter.com/v7/interfaces/react_router.DataRouter.html) that manages the application -path via the URL [`hash`]https://developer.mozilla.org/en-US/docs/Web/API/URL/hash). +path via the URL [`hash`](https://developer.mozilla.org/en-US/docs/Web/API/URL/hash). ## Signature diff --git a/integration/css-lazy-loading-test.ts b/integration/css-lazy-loading-test.ts new file mode 100644 index 0000000000..852ecdc857 --- /dev/null +++ b/integration/css-lazy-loading-test.ts @@ -0,0 +1,183 @@ +import { test, expect } from "@playwright/test"; + +import { PlaywrightFixture } from "./helpers/playwright-fixture.js"; +import type { Fixture, AppFixture } from "./helpers/create-fixture.js"; +import { + createAppFixture, + createFixture, + css, + js, +} from "./helpers/create-fixture.js"; + +let fixture: Fixture; +let appFixture: AppFixture; + +test.beforeEach(async ({ context }) => { + await context.route(/\.data$/, async (route) => { + await new Promise((resolve) => setTimeout(resolve, 50)); + route.continue(); + }); +}); + +test.beforeAll(async () => { + fixture = await createFixture({ + files: { + "app/routes.ts": js` + import { type RouteConfig, index, route } from "@react-router/dev/routes"; + + export default [ + index("routes/home.tsx"), + route("company", "routes/layout.tsx", [ + route("books", "routes/books/route.tsx"), + route("publishers", "routes/publishers/route.tsx"), + ]), + ] satisfies RouteConfig; + `, + + "app/components/Icon.module.css": css` + .icon { + width: 20px; + height: 20px; + background-color: green; + } + `, + + "app/components/Icon.tsx": js` + import styles from "./Icon.module.css"; + + export const Icon = () => { + return
; + } + `, + + "app/components/LazyIcon.tsx": js` + import { lazy, Suspense } from "react"; + + const Icon = lazy(() => + import("../components/Icon").then((m) => ({ default: m.Icon })) + ); + + const LazyIcon = ({ show }: { show: boolean }) => { + if (!show) return null; + + return ( + Loading...
}> + + + ); + }; + + export { LazyIcon }; + `, + + "app/routes/home.tsx": js` + import { redirect } from "react-router"; + + export const loader = () => { + return redirect("/company/books"); + }; + `, + + "app/routes/layout.tsx": js` + import { Link, Outlet } from "react-router"; + + import { LazyIcon } from "../components/LazyIcon"; + import { useState, useEffect } from "react"; + + export default function Layout() { + const [hydrated, setHydrated] = useState(false); + const [show, setShow] = useState(false); + + useEffect(() => { + setShow(true); + },[]) + + return ( +
+

Layout

+ +
+ +
+
+ +
+
+ ); + } + `, + + "app/routes/books/route.tsx": js` + import { Icon } from "../../components/Icon"; + + export default function BooksRoute() { + return ( + <> +

Books

+
+ +
+ + ); + } + + `, + + "app/routes/publishers/route.tsx": js` + export default function PublishersRoute() { + return

Publishers

; + } + `, + }, + }); + + // This creates an interactive app using playwright. + appFixture = await createAppFixture(fixture); +}); + +test.afterAll(() => { + appFixture.close(); +}); + +test("should preserve the CSS from the lazy loaded component even when it's in the route css manifest", async ({ + page, +}) => { + let app = new PlaywrightFixture(appFixture, page); + await app.goto("/"); + + expect(await page.getByTestId("icon").all()).toHaveLength(1); + + // check the head for a link to the css that includes the word `Icon` + const links1 = await page.$$("link"); + let found1 = false; + for (const link of links1) { + const href = await link.getAttribute("href"); + if (href?.includes("Icon") && href.includes("css")) { + found1 = true; + } + } + + expect(found1).toBe(true); + + // wait for the loading to be gone before checking the lazy loaded component has resolved + await expect(page.getByText("Loading...")).toHaveCount(0); + expect(await page.getByTestId("icon").all()).toHaveLength(2); + + await app.clickLink("/company/publishers"); + + expect(await page.getByTestId("icon").all()).toHaveLength(1); + + const links2 = await page.$$("link"); + let found2 = false; + for (const link of links2) { + const href = await link.getAttribute("href"); + if (href?.includes("Icon") && href.includes("css")) { + found2 = true; + } + } + + expect(found2).toBe(true); +}); diff --git a/integration/vite-dot-server-test.ts b/integration/vite-dot-server-test.ts index 77e749d192..6e686d6b01 100644 --- a/integration/vite-dot-server-test.ts +++ b/integration/vite-dot-server-test.ts @@ -146,7 +146,7 @@ test.describe("Vite / route / server-only module referenced by client", () => { ` But other route exports in 'app/routes/_index.tsx' depend on '${specifier}'.`, - " See https://remix.run/docs/en/main/guides/vite#splitting-up-client-and-server-code", + " See https://reactrouter.com/explanation/code-splitting#removal-of-server-code", ].forEach(expect(stderr).toMatch); }); } @@ -206,7 +206,7 @@ test.describe("Vite / non-route / server-only module referenced by client", () = ` '${specifier}' imported by 'app/reexport-server-only.ts'`, - " See https://remix.run/docs/en/main/guides/vite#splitting-up-client-and-server-code", + " See https://reactrouter.com/explanation/code-splitting#removal-of-server-code", ].forEach(expect(stderr).toMatch); }); } diff --git a/packages/react-router-dev/vite/plugin.ts b/packages/react-router-dev/vite/plugin.ts index d409f808c0..a4afbd0260 100644 --- a/packages/react-router-dev/vite/plugin.ts +++ b/packages/react-router-dev/vite/plugin.ts @@ -394,7 +394,7 @@ const getReactRouterManifestBuildAssets = ( : null, chunks .flatMap((e) => e.css ?? []) - .map((href) => `${ctx.publicPath}${href}`), + .map((href) => `${ctx.publicPath}${href}#route=true`), ] .flat(1) .filter(isNonNullable), @@ -2084,7 +2084,7 @@ export const reactRouterVitePlugin: ReactRouterVitePlugin = () => { "", ` But other route exports in '${importerShort}' depend on '${id}'.`, "", - " See https://remix.run/docs/en/main/guides/vite#splitting-up-client-and-server-code", + " See https://reactrouter.com/explanation/code-splitting#removal-of-server-code", "", ].join("\n"), ); @@ -2096,7 +2096,7 @@ export const reactRouterVitePlugin: ReactRouterVitePlugin = () => { "", ` '${id}' imported by '${importerShort}'`, "", - " See https://remix.run/docs/en/main/guides/vite#splitting-up-client-and-server-code", + " See https://reactrouter.com/explanation/code-splitting#removal-of-server-code", "", ].join("\n"), ); diff --git a/packages/react-router-fs-routes/index.ts b/packages/react-router-fs-routes/index.ts index a68afc85e2..11d02ffb89 100644 --- a/packages/react-router-fs-routes/index.ts +++ b/packages/react-router-fs-routes/index.ts @@ -12,7 +12,7 @@ import { normalizeSlashes } from "./normalizeSlashes"; /** * Creates route config from the file system using a convention that matches * [Remix v2's route file - * naming](https://remix.run/docs/en/v2/file-conventions/routes-files), for use + * naming](https://v2.remix.run/docs/file-conventions/routes), for use * within `routes.ts`. */ export async function flatRoutes( diff --git a/packages/react-router-node/sessions/fileStorage.ts b/packages/react-router-node/sessions/fileStorage.ts index 3d2c30ae41..840d463a5f 100644 --- a/packages/react-router-node/sessions/fileStorage.ts +++ b/packages/react-router-node/sessions/fileStorage.ts @@ -26,7 +26,7 @@ interface FileSessionStorageOptions { * The advantage of using this instead of cookie session storage is that * files may contain much more data than cookies. * - * @see https://remix.run/utils/sessions#createfilesessionstorage-node + * @see https://api.reactrouter.com/v7/functions/_react_router_node.createFileSessionStorage */ export function createFileSessionStorage({ cookie, diff --git a/packages/react-router-remix-routes-option-adapter/index.ts b/packages/react-router-remix-routes-option-adapter/index.ts index 77f2582068..219b7a5780 100644 --- a/packages/react-router-remix-routes-option-adapter/index.ts +++ b/packages/react-router-remix-routes-option-adapter/index.ts @@ -11,7 +11,7 @@ export type { DefineRoutesFunction, DefineRouteFunction }; /** * Adapts routes defined using [Remix's `routes` config - * option](https://remix.run/docs/en/v2/file-conventions/vite-config#routes) to + * option](https://v2.remix.run/docs/file-conventions/vite-config#routes) to * React Router's config format, for use within `routes.ts`. */ export async function remixRoutesOptionAdapter( diff --git a/packages/react-router/lib/dom/lib.tsx b/packages/react-router/lib/dom/lib.tsx index ecbe52374e..92cda6b050 100644 --- a/packages/react-router/lib/dom/lib.tsx +++ b/packages/react-router/lib/dom/lib.tsx @@ -766,7 +766,7 @@ export function createBrowserRouter( /** * Create a new {@link DataRouter| data router} that manages the application - * path via the URL [`hash`]https://developer.mozilla.org/en-US/docs/Web/API/URL/hash). + * path via the URL [`hash`](https://developer.mozilla.org/en-US/docs/Web/API/URL/hash). * * @public * @category Data Routers diff --git a/packages/react-router/lib/dom/ssr/routeModules.ts b/packages/react-router/lib/dom/ssr/routeModules.ts index c06e8b617c..e8926d46b4 100644 --- a/packages/react-router/lib/dom/ssr/routeModules.ts +++ b/packages/react-router/lib/dom/ssr/routeModules.ts @@ -119,7 +119,7 @@ export type LayoutComponent = ComponentType<{ * A function that defines `` tags to be inserted into the `` of * the document on route transitions. * - * @see https://remix.run/route/meta + * @see https://reactrouter.com/start/framework/route-module#meta */ export interface LinksFunction { (): LinkDescriptor[]; @@ -267,7 +267,7 @@ export type RouteComponent = ComponentType<{}>; /** * An arbitrary object that is associated with a route. * - * @see https://remix.run/route/handle + * @see https://reactrouter.com/how-to/using-handle */ export type RouteHandle = unknown;