Skip to content

Commit

Permalink
fix(remix-dev/vite): keep code-split JS files in SSR build (#8042)
Browse files Browse the repository at this point in the history
Co-authored-by: Mark Dalgleish <mark.john.dalgleish@gmail.com>
  • Loading branch information
hi-ogawa and markdalgleish authored Nov 19, 2023
1 parent f7062e4 commit a86ccbe
Show file tree
Hide file tree
Showing 3 changed files with 87 additions and 21 deletions.
5 changes: 5 additions & 0 deletions .changeset/modern-pans-happen.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@remix-run/dev": patch
---

Ensure code-split JS files in the server build's assets directory aren't cleaned up after Vite build
46 changes: 46 additions & 0 deletions integration/vite-build-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,28 @@ test.describe("Vite build", () => {

"app/assets/test1.txt": "test1",
"app/assets/test2.txt": "test2",

"app/routes/ssr-code-split.tsx": js`
import { useLoaderData } from "@remix-run/react"
export const loader: LoaderFunction = async () => {
const lib = await import("../ssr-code-split-lib");
return lib.ssrCodeSplitTest();
};
export default function SsrCodeSplitRoute() {
const loaderData = useLoaderData();
return (
<div data-ssr-code-split>{loaderData}</div>
);
}
`,

"app/ssr-code-split-lib.ts": js`
export function ssrCodeSplitTest() {
return "ssrCodeSplitTest";
}
`,
},
});

Expand Down Expand Up @@ -294,6 +316,30 @@ test.describe("Vite build", () => {
await page.getByText("test2").click();
});

test("supports code-split JS from SSR build", async ({ page }) => {
let pageErrors: unknown[] = [];
page.on("pageerror", (error) => pageErrors.push(error));

let app = new PlaywrightFixture(appFixture, page);
await app.goto(`/ssr-code-split`);
expect(pageErrors).toEqual([]);

await expect(page.locator("[data-ssr-code-split]")).toHaveText(
"ssrCodeSplitTest"
);

expect(pageErrors).toEqual([]);
});

test("removes assets (other than code-split JS) and CSS files from SSR build", async () => {
let assetFiles = glob.sync("*", {
cwd: path.join(fixture.projectDir, "build/assets"),
});
let [asset, ...rest] = assetFiles;
expect(rest).toEqual([]); // Provide more useful test output if this fails
expect(asset).toMatch(/ssr-code-split-lib-.*\.js/);
});

test("supports code-split css", async ({ page }) => {
let pageErrors: unknown[] = [];
page.on("pageerror", (error) => pageErrors.push(error));
Expand Down
57 changes: 36 additions & 21 deletions packages/remix-dev/vite/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -542,7 +542,12 @@ export const remixVitePlugin: RemixVitePlugin = (options = {}) => {
},
}
: {
ssrEmitAssets: true, // We move SSR-only assets to client assets and clean the rest
// We move SSR-only assets to client assets. Note that the
// SSR build can also emit code-split JS files (e.g. by
// dynamic import) under the same assets directory
// regardless of "ssrEmitAssets" option, so we also need to
// keep these JS files have to be kept as-is.
ssrEmitAssets: true,
manifest: true, // We need the manifest to detect SSR-only assets
outDir: path.dirname(pluginConfig.serverBuildPath),
rollupOptions: {
Expand Down Expand Up @@ -756,7 +761,7 @@ export const remixVitePlugin: RemixVitePlugin = (options = {}) => {
},
writeBundle: {
// After the SSR build is finished, we inspect the Vite manifest for
// the SSR build and move all server assets to client assets directory
// the SSR build and move server-only assets to client assets directory
async handler() {
if (!ssrBuildContext.isSsrBuild) {
return;
Expand Down Expand Up @@ -785,20 +790,39 @@ export const remixVitePlugin: RemixVitePlugin = (options = {}) => {
)
);

let ssrOnlyAssetPaths = new Set(
Object.values(ssrViteManifest)
.flatMap((chunk) => chunk.assets ?? [])
// Only move assets that aren't in the client build
.filter((asset) => !clientAssetPaths.has(asset))
let ssrAssetPaths = new Set(
Object.values(ssrViteManifest).flatMap(
(chunk) => chunk.assets ?? []
)
);

let movedAssetPaths = await Promise.all(
Array.from(ssrOnlyAssetPaths).map(async (ssrAssetPath) => {
let src = path.join(serverBuildDir, ssrAssetPath);
// We only move assets that aren't in the client build, otherwise we
// remove them. These assets only exist because we explicitly set
// `ssrEmitAssets: true` in the SSR Vite config. These assets
// typically wouldn't exist by default, which is why we assume it's
// safe to remove them. We're aiming for a clean build output so that
// unnecessary assets don't get deployed alongside the server code.
let movedAssetPaths: string[] = [];
for (let ssrAssetPath of ssrAssetPaths) {
let src = path.join(serverBuildDir, ssrAssetPath);
if (!clientAssetPaths.has(ssrAssetPath)) {
let dest = path.join(assetsBuildDirectory, ssrAssetPath);
await fse.move(src, dest);
return dest;
})
movedAssetPaths.push(dest);
} else {
await fse.remove(src);
}
}

// We assume CSS files from the SSR build are unnecessary and remove
// them for the same reasons as above.
let ssrCssPaths = Object.values(ssrViteManifest).flatMap(
(chunk) => chunk.css ?? []
);
await Promise.all(
ssrCssPaths.map((cssPath) =>
fse.remove(path.join(serverBuildDir, cssPath))
)
);

let logger = resolvedViteConfig.logger;
Expand All @@ -817,15 +841,6 @@ export const remixVitePlugin: RemixVitePlugin = (options = {}) => {
].join("\n")
);
}

let ssrAssetsDir = path.join(
resolvedViteConfig.build.outDir,
resolvedViteConfig.build.assetsDir
);

if (fse.existsSync(ssrAssetsDir)) {
await fse.remove(ssrAssetsDir);
}
},
},
async buildEnd() {
Expand Down

0 comments on commit a86ccbe

Please sign in to comment.