diff --git a/.changeset/itchy-rabbits-fly.md b/.changeset/itchy-rabbits-fly.md new file mode 100644 index 00000000000..c88e6e0f8be --- /dev/null +++ b/.changeset/itchy-rabbits-fly.md @@ -0,0 +1,5 @@ +--- +"@remix-run/dev": patch +--- + +Attach CSS from shared chunks to routes in Vite build diff --git a/integration/vite-build-test.ts b/integration/vite-build-test.ts index 3972460e382..df13c3d8513 100644 --- a/integration/vite-build-test.ts +++ b/integration/vite-build-test.ts @@ -107,7 +107,7 @@ test.describe("Vite build", () => { import { useEffect, useState } from "react"; import { json } from "@remix-run/node"; import { useLoaderData } from "@remix-run/react"; - + import { serverOnly1, serverOnly2 } from "../utils.server"; export const loader = () => { @@ -138,6 +138,32 @@ test.describe("Vite build", () => { `, + "app/routes/code-split1.tsx": js` + import { CodeSplitComponent } from "../code-split-component"; + + export default function CodeSplit1Route() { + return
; + } + `, + "app/routes/code-split2.tsx": js` + import { CodeSplitComponent } from "../code-split-component"; + + export default function CodeSplit2Route() { + return
; + } + `, + "app/code-split-component.tsx": js` + import classes from "./code-split.module.css"; + + export function CodeSplitComponent() { + return ok + } + `, + "app/code-split.module.css": js` + .test { + background-color: rgb(255, 170, 0); + } + `, }, }); @@ -206,4 +232,26 @@ test.describe("Vite build", () => { expect(pageErrors).toEqual([]); }); + + test("supports code-split css", async ({ page }) => { + let pageErrors: unknown[] = []; + page.on("pageerror", (error) => pageErrors.push(error)); + + let app = new PlaywrightFixture(appFixture, page); + await app.goto("/code-split1"); + expect( + await page + .locator("#code-split1 span") + .evaluate((e) => window.getComputedStyle(e).backgroundColor) + ).toBe("rgb(255, 170, 0)"); + + await app.goto("/code-split2"); + expect( + await page + .locator("#code-split2 span") + .evaluate((e) => window.getComputedStyle(e).backgroundColor) + ).toBe("rgb(255, 170, 0)"); + + expect(pageErrors).toEqual([]); + }); }); diff --git a/packages/remix-dev/vite/plugin.ts b/packages/remix-dev/vite/plugin.ts index c3ea928b56e..9f401b7b01c 100644 --- a/packages/remix-dev/vite/plugin.ts +++ b/packages/remix-dev/vite/plugin.ts @@ -7,6 +7,7 @@ import { type Connect, type Plugin as VitePlugin, type Manifest as ViteManifest, + type ManifestChunk, type ResolvedConfig as ResolvedViteConfig, type ViteDevServer, type UserConfig as ViteUserConfig, @@ -120,7 +121,7 @@ const getHash = (source: BinaryLike, maxLength?: number): string => { const resolveBuildAssetPaths = ( pluginConfig: ResolvedRemixVitePluginConfig, - manifest: ViteManifest, + viteManifest: ViteManifest, absoluteFilePath: string ): Manifest["entry"] & { css: string[] } => { let rootRelativeFilePath = path.relative( @@ -128,10 +129,10 @@ const resolveBuildAssetPaths = ( absoluteFilePath ); let manifestKey = normalizePath(rootRelativeFilePath); - let manifestEntry = manifest[manifestKey]; + let entryChunk = viteManifest[manifestKey]; - if (!manifestEntry) { - let knownManifestKeys = Object.keys(manifest) + if (!entryChunk) { + let knownManifestKeys = Object.keys(viteManifest) .map((key) => '"' + key + '"') .join(", "); throw new Error( @@ -139,19 +140,50 @@ const resolveBuildAssetPaths = ( ); } + let chunks = resolveDependantChunks(viteManifest, entryChunk); + return { - module: `${pluginConfig.publicPath}${manifestEntry.file}`, + module: `${pluginConfig.publicPath}${entryChunk.file}`, imports: - manifestEntry.imports?.map((imported) => { - return `${pluginConfig.publicPath}${manifest[imported].file}`; + dedupe(chunks.flatMap((e) => e.imports ?? [])).map((imported) => { + return `${pluginConfig.publicPath}${viteManifest[imported].file}`; }) ?? [], css: - manifestEntry.css?.map((href) => { + dedupe(chunks.flatMap((e) => e.css ?? [])).map((href) => { return `${pluginConfig.publicPath}${href}`; }) ?? [], }; }; +function resolveDependantChunks( + viteManifest: ViteManifest, + entryChunk: ManifestChunk +): ManifestChunk[] { + let chunks = new Set(); + + function walk(chunk: ManifestChunk) { + if (chunks.has(chunk)) { + return; + } + + if (chunk.imports) { + for (let importKey of chunk.imports) { + walk(viteManifest[importKey]); + } + } + + chunks.add(chunk); + } + + walk(entryChunk); + + return Array.from(chunks); +} + +function dedupe(array: T[]): T[] { + return [...new Set(array)]; +} + const writeFileSafe = async (file: string, contents: string): Promise => { await fs.mkdir(path.dirname(file), { recursive: true }); await fs.writeFile(file, contents);