Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(remix-dev/vite): keep code-split JS files in SSR build #8042

Merged
merged 16 commits into from
Nov 19, 2023
Merged
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
Loading