Skip to content

Clean build manifest output #11573

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 2 commits into from
May 21, 2024
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
28 changes: 28 additions & 0 deletions .changeset/remove-manifest-option.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
---
"@react-router/dev": major
---

For Remix consumers migrating to React Router, the Vite plugin's `manifest` option has been removed.

The `manifest` option been superseded by the more powerful `buildEnd` hook since it's passed the `buildManifest` argument. You can still write the build manifest to disk if needed, but you'll most likely find it more convenient to write any logic depending on the build manifest within the `buildEnd` hook itself.

If you were using the `manifest` option, you can replace it with a `buildEnd` hook that writes the manifest to disk like this:

```js
import { vitePlugin as reactRouter } from "@react-router/dev";
import { writeFile } from "node:fs/promises";

export default {
plugins: [
reactRouter({
async buildEnd({ buildManifest }) {
await writeFile(
"build/manifest.json",
JSON.stringify(buildManifest, null, 2),
"utf-8"
);
},
}),
],
};
```
7 changes: 7 additions & 0 deletions .changeset/vite-manifest-location.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"@react-router/dev": major
---

For Remix consumers migrating to React Router, Vite manifests (i.e. `.vite/manifest.json`) are now written within each build subdirectory, e.g. `build/client/.vite/manifest.json` and `build/server/.vite/manifest.json` instead of `build/.vite/client-manifest.json` and `build/.vite/server-manifest.json`. This means that the build output is now much closer to what you'd expect from a typical Vite project.

Originally the Remix Vite plugin moved all Vite manifests to a root-level `build/.vite` directory to avoid accidentally serving them in production, particularly from the client build. This was later improved with additional logic that deleted these Vite manifest files at the end of the build process unless Vite's `build.manifest` had been enabled within the app's Vite config. This greatly reduced the risk of accidentally serving the Vite manifests in production since they're only present when explicitly asked for. As a result, we can now assume that consumers will know that they need to manage these additional files themselves, and React Router can safely generate a more standard Vite build output.
51 changes: 29 additions & 22 deletions integration/vite-manifests-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,11 @@ import dedent from "dedent";

import { createProject, build, viteConfig } from "./helpers/vite.js";

const js = String.raw;

function createRoute(path: string) {
return {
[`app/routes/${path}`]: `
[`app/routes/${path}`]: js`
export default function Route() {
return <p>Path: ${path}</p>;
}
Expand All @@ -23,7 +25,7 @@ const TEST_ROUTES = [
];

const files = {
"app/root.tsx": `
"app/root.tsx": js`
import { Links, Meta, Outlet, Scripts } from "react-router-dom";

export default function Root() {
Expand All @@ -49,36 +51,43 @@ test.describe(() => {

test.beforeAll(async () => {
cwd = await createProject({
"vite.config.ts": dedent`
"vite.config.ts": dedent(js`
import { vitePlugin as reactRouter } from "@react-router/dev";

export default {
build: { manifest: true },
plugins: [reactRouter({ manifest: true })],
plugins: [reactRouter({
buildEnd: async ({ buildManifest }) => {
let fs = await import("node:fs");
await fs.promises.writeFile(
"build/test-manifest.json",
JSON.stringify(buildManifest, null, 2),
"utf-8",
);
},
})],
}
`,
`),
...files,
});

build({ cwd });
});

test("Vite / manifests enabled / Vite manifests", () => {
let viteManifestFiles = fs.readdirSync(path.join(cwd, "build", ".vite"));
let viteManifestFilesClient = fs.readdirSync(
path.join(cwd, "build", "client", ".vite")
);
expect(viteManifestFilesClient).toEqual(["manifest.json"]);

expect(viteManifestFiles).toEqual([
"client-manifest.json",
"server-manifest.json",
]);
let viteManifestFilesServer = fs.readdirSync(
path.join(cwd, "build", "server", ".vite")
);
expect(viteManifestFilesServer).toEqual(["manifest.json"]);
});

test("Vite / manifests enabled / React Router build manifest", async () => {
let manifestPath = path.join(
cwd,
"build",
".react-router",
"manifest.json"
);
let manifestPath = path.join(cwd, "build", "test-manifest.json");
expect(JSON.parse(fs.readFileSync(manifestPath, "utf8"))).toEqual({
routes: {
root: {
Expand Down Expand Up @@ -122,12 +131,10 @@ test.describe(() => {
});

test("Vite / manifest disabled / Vite manifests", () => {
let manifestDir = path.join(cwd, "build", ".vite");
expect(fs.existsSync(manifestDir)).toBe(false);
});
let manifestDirClient = path.join(cwd, "build", "client", ".vite");
expect(fs.existsSync(manifestDirClient)).toBe(false);

test("Vite / manifest disabled / React Router build manifest doesn't exist", async () => {
let manifestDir = path.join(cwd, "build", ".react-router");
expect(fs.existsSync(manifestDir)).toBe(false);
let manifestDirServer = path.join(cwd, "build", "server", ".vite");
expect(fs.existsSync(manifestDirServer)).toBe(false);
});
});
2 changes: 2 additions & 0 deletions integration/vite-prerender-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,7 @@ test.describe("Prerendering", () => {

let clientDir = path.join(fixture.projectDir, "build", "client");
expect(fs.readdirSync(clientDir).sort()).toEqual([
".vite",
"_root.data",
"about",
"about.data",
Expand Down Expand Up @@ -176,6 +177,7 @@ test.describe("Prerendering", () => {

let clientDir = path.join(fixture.projectDir, "build", "client");
expect(fs.readdirSync(clientDir).sort()).toEqual([
".vite",
"_root.data",
"about",
"about.data",
Expand Down
1 change: 0 additions & 1 deletion integration/vite-presets-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -213,7 +213,6 @@ test("Vite / presets", async () => {
"buildDirectory",
"buildEnd",
"future",
"manifest",
"prerender",
"publicPath",
"routes",
Expand Down
46 changes: 26 additions & 20 deletions integration/vite-server-bundles-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ import {
viteConfig,
} from "./helpers/vite.js";

const js = String.raw;

const withBundleServer = async (
cwd: string,
serverBundle: string,
Expand All @@ -27,7 +29,7 @@ const ROUTE_FILE_COMMENT = "// THIS IS A ROUTE FILE";

function createRoute(path: string) {
return {
[`app/routes/${path}`]: `
[`app/routes/${path}`]: js`
${ROUTE_FILE_COMMENT}
import { Outlet } from "react-router-dom";
import { useState, useEffect } from "react";
Expand Down Expand Up @@ -73,7 +75,7 @@ const TEST_ROUTES = [
];

const files = {
"app/root.tsx": `
"app/root.tsx": js`
${ROUTE_FILE_COMMENT}
import { Links, Meta, Outlet, Scripts } from "react-router-dom";

Expand Down Expand Up @@ -118,14 +120,20 @@ test.describe(() => {
test.beforeAll(async () => {
port = await getPort();
cwd = await createProject({
"vite.config.ts": dedent`
"vite.config.ts": dedent(js`
import { vitePlugin as reactRouter } from "@react-router/dev";

export default {
${await viteConfig.server({ port })}
build: { manifest: true },
plugins: [reactRouter({
manifest: true,
buildEnd: async ({ buildManifest }) => {
let fs = await import("node:fs");
await fs.promises.writeFile(
"build/test-manifest.json",
JSON.stringify(buildManifest, null, 2)
);
},
serverBundles: async ({ branch }) => {
// Smoke test to ensure we can read the route files via 'route.file'
await Promise.all(branch.map(async (route) => {
Expand Down Expand Up @@ -158,7 +166,7 @@ test.describe(() => {
}
})]
}
`,
`),
...files,
});
});
Expand Down Expand Up @@ -306,24 +314,22 @@ test.describe(() => {
});

test("Vite / server bundles / build / Vite manifests", () => {
let viteManifestFiles = fs.readdirSync(path.join(cwd, "build", ".vite"));

expect(viteManifestFiles).toEqual([
"client-manifest.json",
"server-bundle-a-manifest.json",
"server-bundle-b-manifest.json",
"server-bundle-c-manifest.json",
"server-root-manifest.json",
]);
[
["client"],
["server", "bundle-a"],
["server", "bundle-b"],
["server", "bundle-c"],
["server", "root"],
].forEach((buildPaths) => {
let viteManifestFiles = fs.readdirSync(
path.join(cwd, "build", ...buildPaths, ".vite")
);
expect(viteManifestFiles).toEqual(["manifest.json"]);
});
});

test("Vite / server bundles / build / React Router build manifest", () => {
let manifestPath = path.join(
cwd,
"build",
".react-router",
"manifest.json"
);
let manifestPath = path.join(cwd, "build", "test-manifest.json");
expect(JSON.parse(fs.readFileSync(manifestPath, "utf8"))).toEqual({
serverBundles: {
"bundle-c": {
Expand Down
4 changes: 2 additions & 2 deletions integration/vite-spa-mode-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -964,10 +964,10 @@ test.describe("SPA Mode", () => {

test("only generates client Vite manifest", () => {
let viteManifestFiles = fs.readdirSync(
path.join(fixture.projectDir, "build", ".vite")
path.join(fixture.projectDir, "build", "client", ".vite")
);

expect(viteManifestFiles).toEqual(["client-manifest.json"]);
expect(viteManifestFiles).toEqual(["manifest.json"]);
});
});
});
13 changes: 0 additions & 13 deletions packages/remix-dev/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -137,11 +137,6 @@ export type VitePluginConfig = {
* A function that is called after the full React Router build is complete.
*/
buildEnd?: BuildEndHook;
/**
* Whether to write a `"manifest.json"` file to the build directory.`
* Defaults to `false`.
*/
manifest?: boolean;
/**
* An array of URLs to prerender to HTML files at build time. Can also be a
* function returning an array to dynamically generate URLs.
Expand Down Expand Up @@ -194,11 +189,6 @@ export type ResolvedVitePluginConfig = Readonly<{
* Enabled future flags
*/
future: FutureConfig;
/**
* Whether to write a `"manifest.json"` file to the build directory.`
* Defaults to `false`.
*/
manifest: boolean;
/**
* An array of URLs to prerender to HTML files at build time.
*/
Expand Down Expand Up @@ -363,7 +353,6 @@ export async function resolveReactRouterConfig({
let defaults = {
basename: "/",
buildDirectory: "build",
manifest: false,
serverBuildFile: "index.js",
serverModuleFormat: "esm",
ssr: true,
Expand All @@ -376,7 +365,6 @@ export async function resolveReactRouterConfig({
buildEnd,
future: userFuture,
ignoredRouteFiles,
manifest,
routes: userRoutesFunction,
prerender: prerenderConfig,
serverBuildFile,
Expand Down Expand Up @@ -464,7 +452,6 @@ export async function resolveReactRouterConfig({
buildDirectory,
buildEnd,
future,
manifest,
prerender,
publicPath,
routes,
Expand Down
48 changes: 10 additions & 38 deletions packages/remix-dev/vite/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -193,24 +193,14 @@ function getViteManifestPaths(
let buildRelative = (pathname: string) =>
path.resolve(ctx.reactRouterConfig.buildDirectory, pathname);

let viteManifestPaths: Array<{ srcPath: string; destPath: string }> = [
{
srcPath: "client/.vite/manifest.json",
destPath: ".vite/client-manifest.json",
},
let viteManifestPaths: Array<string> = [
"client/.vite/manifest.json",
...serverBuilds.map(({ serverBundleBuildConfig }) => {
let serverBundleId = serverBundleBuildConfig?.serverBundleId;
let serverBundlePath = serverBundleId ? serverBundleId + "/" : "";
let serverBundleSuffix = serverBundleId ? serverBundleId + "-" : "";
return {
srcPath: `server/${serverBundlePath}.vite/manifest.json`,
destPath: `.vite/server-${serverBundleSuffix}manifest.json`,
};
return `server/${serverBundlePath}.vite/manifest.json`;
}),
].map(({ srcPath, destPath }) => ({
srcPath: buildRelative(srcPath),
destPath: buildRelative(destPath),
}));
].map((srcPath) => buildRelative(srcPath));

return viteManifestPaths;
}
Expand Down Expand Up @@ -298,42 +288,24 @@ export async function build(

let viteManifestPaths = getViteManifestPaths(ctx, serverBuilds);
await Promise.all(
viteManifestPaths.map(async ({ srcPath, destPath }) => {
let manifestExists = await fse.pathExists(srcPath);
viteManifestPaths.map(async (viteManifestPath) => {
let manifestExists = await fse.pathExists(viteManifestPath);
if (!manifestExists) return;

// Move/delete original Vite manifest file
if (ctx.viteManifestEnabled) {
await fse.ensureDir(path.dirname(destPath));
await fse.move(srcPath, destPath);
} else {
await fse.remove(srcPath);
// Delete original Vite manifest file if consumer doesn't want it
if (!ctx.viteManifestEnabled) {
await fse.remove(viteManifestPath);
}

// Remove .vite dir if it's now empty
let viteDir = path.dirname(srcPath);
let viteDir = path.dirname(viteManifestPath);
let viteDirFiles = await fse.readdir(viteDir);
if (viteDirFiles.length === 0) {
await fse.remove(viteDir);
}
})
);

if (ctx.reactRouterConfig.manifest) {
await fse.ensureDir(
path.join(ctx.reactRouterConfig.buildDirectory, ".react-router")
);
await fse.writeFile(
path.join(
ctx.reactRouterConfig.buildDirectory,
".react-router",
"manifest.json"
),
JSON.stringify(buildManifest, null, 2),
"utf-8"
);
}

await reactRouterConfig.buildEnd?.({
buildManifest,
reactRouterConfig,
Expand Down
Loading