Skip to content

Commit 2251663

Browse files
Refactor build logic to emulate Vite Environment API (remix-run#12807)
1 parent c979bcb commit 2251663

File tree

3 files changed

+497
-367
lines changed

3 files changed

+497
-367
lines changed

packages/react-router-dev/config/config.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@ import isEqual from "lodash/isEqual";
1717
import {
1818
type RouteManifest,
1919
type RouteManifestEntry,
20-
type RouteConfig,
2120
setAppDirectory,
2221
validateRouteConfig,
2322
configRoutesToRouteManifest,
@@ -72,7 +71,7 @@ type DefaultBuildManifest = BaseBuildManifest & {
7271
routeIdToServerBundleId?: never;
7372
};
7473

75-
export type ServerBundlesBuildManifest = BaseBuildManifest & {
74+
type ServerBundlesBuildManifest = BaseBuildManifest & {
7675
serverBundles: {
7776
[serverBundleId: string]: {
7877
id: string;

packages/react-router-dev/vite/build.ts

Lines changed: 57 additions & 186 deletions
Original file line numberDiff line numberDiff line change
@@ -5,172 +5,17 @@ import colors from "picocolors";
55

66
import {
77
type ReactRouterPluginContext,
8-
type ServerBundleBuildConfig,
8+
type EnvironmentName,
9+
type EnvironmentBuildContext,
10+
type EnvironmentOptionsResolvers,
911
resolveViteConfig,
1012
extractPluginContext,
11-
getServerBuildDirectory,
13+
getBuildManifest,
14+
getEnvironmentOptionsResolvers,
1215
} from "./plugin";
13-
import {
14-
type BuildManifest,
15-
type ServerBundlesBuildManifest,
16-
configRouteToBranchRoute,
17-
} from "../config/config";
18-
import type { RouteManifestEntry, RouteManifest } from "../config/routes";
1916
import invariant from "../invariant";
2017
import { preloadVite, getVite } from "./vite";
2118

22-
function getAddressableRoutes(routes: RouteManifest): RouteManifestEntry[] {
23-
let nonAddressableIds = new Set<string>();
24-
25-
for (let id in routes) {
26-
let route = routes[id];
27-
28-
// We omit the parent route of index routes since the index route takes ownership of its parent's path
29-
if (route.index) {
30-
invariant(
31-
route.parentId,
32-
`Expected index route "${route.id}" to have "parentId" set`
33-
);
34-
nonAddressableIds.add(route.parentId);
35-
}
36-
37-
// We omit pathless routes since they can only be addressed via descendant routes
38-
if (typeof route.path !== "string" && !route.index) {
39-
nonAddressableIds.add(id);
40-
}
41-
}
42-
43-
return Object.values(routes).filter(
44-
(route) => !nonAddressableIds.has(route.id)
45-
);
46-
}
47-
48-
function getRouteBranch(routes: RouteManifest, routeId: string) {
49-
let branch: RouteManifestEntry[] = [];
50-
let currentRouteId: string | undefined = routeId;
51-
52-
while (currentRouteId) {
53-
let route: RouteManifestEntry = routes[currentRouteId];
54-
invariant(route, `Missing route for ${currentRouteId}`);
55-
branch.push(route);
56-
currentRouteId = route.parentId;
57-
}
58-
59-
return branch.reverse();
60-
}
61-
62-
type ReactRouterClientBuildArgs = {
63-
ssr: false;
64-
serverBundleBuildConfig?: never;
65-
};
66-
67-
type ReactRouterServerBuildArgs = {
68-
ssr: true;
69-
serverBundleBuildConfig?: ServerBundleBuildConfig;
70-
};
71-
72-
type ReactRouterBuildArgs =
73-
| ReactRouterClientBuildArgs
74-
| ReactRouterServerBuildArgs;
75-
76-
async function getServerBuilds(ctx: ReactRouterPluginContext): Promise<{
77-
serverBuilds: ReactRouterServerBuildArgs[];
78-
buildManifest: BuildManifest;
79-
}> {
80-
let { rootDirectory } = ctx;
81-
const { routes, serverBuildFile, serverBundles, appDirectory } =
82-
ctx.reactRouterConfig;
83-
let serverBuildDirectory = getServerBuildDirectory(ctx);
84-
if (!serverBundles) {
85-
return {
86-
serverBuilds: [{ ssr: true }],
87-
buildManifest: { routes },
88-
};
89-
}
90-
91-
let { normalizePath } = await import("vite");
92-
93-
let resolvedAppDirectory = path.resolve(rootDirectory, appDirectory);
94-
let rootRelativeRoutes = Object.fromEntries(
95-
Object.entries(routes).map(([id, route]) => {
96-
let filePath = path.join(resolvedAppDirectory, route.file);
97-
let rootRelativeFilePath = normalizePath(
98-
path.relative(rootDirectory, filePath)
99-
);
100-
return [id, { ...route, file: rootRelativeFilePath }];
101-
})
102-
);
103-
104-
let buildManifest: ServerBundlesBuildManifest = {
105-
serverBundles: {},
106-
routeIdToServerBundleId: {},
107-
routes: rootRelativeRoutes,
108-
};
109-
110-
let serverBundleBuildConfigById = new Map<string, ServerBundleBuildConfig>();
111-
112-
await Promise.all(
113-
getAddressableRoutes(routes).map(async (route) => {
114-
let branch = getRouteBranch(routes, route.id);
115-
let serverBundleId = await serverBundles({
116-
branch: branch.map((route) =>
117-
configRouteToBranchRoute({
118-
...route,
119-
// Ensure absolute paths are passed to the serverBundles function
120-
file: path.join(resolvedAppDirectory, route.file),
121-
})
122-
),
123-
});
124-
if (typeof serverBundleId !== "string") {
125-
throw new Error(`The "serverBundles" function must return a string`);
126-
}
127-
if (!/^[a-zA-Z0-9-_]+$/.test(serverBundleId)) {
128-
throw new Error(
129-
`The "serverBundles" function must only return strings containing alphanumeric characters, hyphens and underscores.`
130-
);
131-
}
132-
buildManifest.routeIdToServerBundleId[route.id] = serverBundleId;
133-
134-
let relativeServerBundleDirectory = path.relative(
135-
rootDirectory,
136-
path.join(serverBuildDirectory, serverBundleId)
137-
);
138-
let serverBuildConfig = serverBundleBuildConfigById.get(serverBundleId);
139-
if (!serverBuildConfig) {
140-
buildManifest.serverBundles[serverBundleId] = {
141-
id: serverBundleId,
142-
file: normalizePath(
143-
path.join(relativeServerBundleDirectory, serverBuildFile)
144-
),
145-
};
146-
serverBuildConfig = {
147-
routes: {},
148-
serverBundleId,
149-
};
150-
serverBundleBuildConfigById.set(serverBundleId, serverBuildConfig);
151-
}
152-
for (let route of branch) {
153-
serverBuildConfig.routes[route.id] = route;
154-
}
155-
})
156-
);
157-
158-
let serverBuilds = Array.from(serverBundleBuildConfigById.values()).map(
159-
(serverBundleBuildConfig): ReactRouterServerBuildArgs => {
160-
let serverBuild: ReactRouterServerBuildArgs = {
161-
ssr: true,
162-
serverBundleBuildConfig,
163-
};
164-
return serverBuild;
165-
}
166-
);
167-
168-
return {
169-
serverBuilds,
170-
buildManifest,
171-
};
172-
}
173-
17419
async function cleanBuildDirectory(
17520
viteConfig: Vite.ResolvedConfig,
17621
ctx: ReactRouterPluginContext
@@ -187,22 +32,23 @@ async function cleanBuildDirectory(
18732
}
18833

18934
function getViteManifestPaths(
190-
ctx: ReactRouterPluginContext,
191-
serverBuilds: Array<ReactRouterServerBuildArgs>
35+
environmentOptionsResolvers: EnvironmentOptionsResolvers
19236
) {
193-
let buildRelative = (pathname: string) =>
194-
path.resolve(ctx.reactRouterConfig.buildDirectory, pathname);
195-
196-
let viteManifestPaths: Array<string> = [
197-
"client/.vite/manifest.json",
198-
...serverBuilds.map(({ serverBundleBuildConfig }) => {
199-
let serverBundleId = serverBundleBuildConfig?.serverBundleId;
200-
let serverBundlePath = serverBundleId ? serverBundleId + "/" : "";
201-
return `server/${serverBundlePath}.vite/manifest.json`;
202-
}),
203-
].map((srcPath) => buildRelative(srcPath));
204-
205-
return viteManifestPaths;
37+
return Object.entries(environmentOptionsResolvers).map(
38+
([environmentName, resolveOptions]) => {
39+
invariant(
40+
resolveOptions,
41+
`Expected build environment options resolver for ${environmentName}`
42+
);
43+
let options = resolveOptions({
44+
viteCommand: "build",
45+
viteUserConfig: {},
46+
});
47+
let outDir = options.build.outDir;
48+
invariant(outDir, `Expected build.outDir for ${environmentName}`);
49+
return path.join(outDir, ".vite/manifest.json");
50+
}
51+
);
20652
}
20753

20854
export interface ViteBuildOptions {
@@ -253,10 +99,23 @@ export async function build(
25399

254100
let vite = getVite();
255101

256-
async function viteBuild({
257-
ssr,
258-
serverBundleBuildConfig,
259-
}: ReactRouterBuildArgs) {
102+
async function viteBuild(
103+
environmentOptionsResolvers: EnvironmentOptionsResolvers,
104+
environmentName: EnvironmentName
105+
) {
106+
let ssr = environmentName !== "client";
107+
108+
let resolveOptions = environmentOptionsResolvers[environmentName];
109+
invariant(
110+
resolveOptions,
111+
`Missing environment options resolver for ${environmentName}`
112+
);
113+
114+
let environmentBuildContext: EnvironmentBuildContext = {
115+
name: environmentName,
116+
resolveOptions,
117+
};
118+
260119
await vite.build({
261120
root,
262121
mode,
@@ -271,22 +130,34 @@ export async function build(
271130
optimizeDeps: { force },
272131
clearScreen,
273132
logLevel,
274-
...(serverBundleBuildConfig
275-
? { __reactRouterServerBundleBuildConfig: serverBundleBuildConfig }
276-
: {}),
133+
...{ __reactRouterEnvironmentBuildContext: environmentBuildContext },
277134
});
278135
}
279136

280137
await cleanBuildDirectory(viteConfig, ctx);
281138

139+
let buildManifest = await getBuildManifest(ctx);
140+
let environmentOptionsResolvers = await getEnvironmentOptionsResolvers(
141+
ctx,
142+
buildManifest
143+
);
144+
282145
// Run the Vite client build first
283-
await viteBuild({ ssr: false });
146+
await viteBuild(environmentOptionsResolvers, "client");
284147

285148
// Then run Vite SSR builds in parallel
286-
let { serverBuilds, buildManifest } = await getServerBuilds(ctx);
287-
await Promise.all(serverBuilds.map(viteBuild));
149+
let serverEnvironmentNames = (
150+
Object.keys(
151+
environmentOptionsResolvers
152+
) as (keyof typeof environmentOptionsResolvers)[]
153+
).filter((environmentName) => environmentName !== "client");
154+
await Promise.all(
155+
serverEnvironmentNames.map((environmentName) =>
156+
viteBuild(environmentOptionsResolvers, environmentName)
157+
)
158+
);
288159

289-
let viteManifestPaths = getViteManifestPaths(ctx, serverBuilds);
160+
let viteManifestPaths = getViteManifestPaths(environmentOptionsResolvers);
290161
await Promise.all(
291162
viteManifestPaths.map(async (viteManifestPath) => {
292163
let manifestExists = await fse.pathExists(viteManifestPath);

0 commit comments

Comments
 (0)