Skip to content

fix(dev): improve server build asset handling #13547

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

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/gold-socks-attack.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@react-router/dev": patch
---

Add additional logging to `build` command output when cleaning assets from server build
5 changes: 5 additions & 0 deletions .changeset/rude-lobsters-design.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@react-router/dev": patch
---

Don't clean assets from server build when `build.ssrEmitAssets` has been enabled in Vite config
135 changes: 103 additions & 32 deletions packages/react-router-dev/vite/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1659,49 +1659,120 @@ export const reactRouterVitePlugin: ReactRouterVitePlugin = () => {
let ssrViteManifest = await loadViteManifest(serverBuildDirectory);
let ssrAssetPaths = getViteManifestAssetPaths(ssrViteManifest);

// 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.
// If the consumer has explicitly opted in to keeping the SSR build
// assets, we don't remove them. We only copy missing assets from the
// SSR to the client build.
let userSsrEmitAssets =
(ctx.reactRouterConfig.future.unstable_viteEnvironmentApi
? viteUserConfig.environments?.ssr?.build?.ssrEmitAssets ??
viteUserConfig.environments?.ssr?.build?.emitAssets
: null) ??
viteUserConfig.build?.ssrEmitAssets ??
false;

// We only move/copy assets that aren't in the client build, otherwise
// we remove them if the consumer hasn't explicitly enabled
// `ssrEmitAssets` in their Vite config. These assets only exist
// because we internally enable `ssrEmitAssets` within our plugin.
// These assets typically wouldn't exist by default, which is why we
// assume it's safe to remove them.
let movedAssetPaths: string[] = [];
let removedAssetPaths: string[] = [];
let copiedAssetPaths: string[] = [];
for (let ssrAssetPath of ssrAssetPaths) {
let src = path.join(serverBuildDirectory, ssrAssetPath);
let dest = path.join(clientBuildDirectory, ssrAssetPath);

if (!fse.existsSync(dest)) {
await fse.move(src, dest);
movedAssetPaths.push(dest);
} else {
await fse.remove(src);
if (!userSsrEmitAssets) {
if (!fse.existsSync(dest)) {
await fse.move(src, dest);
movedAssetPaths.push(dest);
} else {
await fse.remove(src);
removedAssetPaths.push(dest);
}
} else if (!fse.existsSync(dest)) {
await fse.copy(src, dest);
copiedAssetPaths.push(dest);
}
}

// We assume CSS assets from the SSR build are unnecessary and remove
// them for the same reasons as above.
let ssrCssPaths = Object.values(ssrViteManifest).flatMap(
(chunk) => chunk.css ?? []
);
if (!userSsrEmitAssets) {
// We assume CSS assets 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(async (cssPath) => {
let src = path.join(serverBuildDirectory, cssPath);
await fse.remove(src);
removedAssetPaths.push(src);
})
);
}

let cleanedAssetPaths = [...removedAssetPaths, ...movedAssetPaths];
let handledAssetPaths = [...cleanedAssetPaths, ...copiedAssetPaths];

// Clean empty asset directories
let cleanedAssetDirs = new Set(cleanedAssetPaths.map(path.dirname));
await Promise.all(
ssrCssPaths.map((cssPath) =>
fse.remove(path.join(serverBuildDirectory, cssPath))
)
Array.from(cleanedAssetDirs).map(async (dir) => {
try {
const files = await fse.readdir(dir);
if (files.length === 0) {
await fse.remove(dir);
}
} catch {}
})
);

if (movedAssetPaths.length) {
viteConfig.logger.info(
[
"",
`${colors.green("✓")} ${movedAssetPaths.length} asset${
movedAssetPaths.length > 1 ? "s" : ""
} moved from React Router server build to client assets.`,
...movedAssetPaths.map((movedAssetPath) =>
colors.dim(path.relative(ctx.rootDirectory, movedAssetPath))
),
"",
].join("\n")
);
// If we handled any assets, add some leading whitespace to
// our logs to make them more prominent
if (handledAssetPaths.length) {
viteConfig.logger.info("");
}

function logHandledAssets(paths: string[], message: string) {
invariant(viteConfig);
if (paths.length) {
viteConfig.logger.info(
[
`${colors.green("✓")} ${message}`,
...paths.map((assetPath) =>
colors.dim(path.relative(ctx.rootDirectory, assetPath))
),
].join("\n")
);
}
}

logHandledAssets(
removedAssetPaths,
`${removedAssetPaths.length} asset${
removedAssetPaths.length > 1 ? "s" : ""
} cleaned from React Router server build.`
);

logHandledAssets(
movedAssetPaths,
`${movedAssetPaths.length} asset${
movedAssetPaths.length > 1 ? "s" : ""
} moved from React Router server build to client assets.`
);

logHandledAssets(
copiedAssetPaths,
`${copiedAssetPaths.length} asset${
copiedAssetPaths.length > 1 ? "s" : ""
} copied from React Router server build to client assets.`
);

// If we handled any assets, add some leading whitespace to our logs
// to make them more prominent
if (handledAssetPaths.length) {
viteConfig.logger.info("");
}

// Set an environment variable we can look for in the handler to
Expand Down