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

feat(remix-dev/vite): add Vite plugin adapter API #8514

Merged
merged 22 commits into from
Jan 18, 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
5 changes: 5 additions & 0 deletions .changeset/ninety-baboons-leave.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@remix-run/dev": patch
---

Vite: Add `adapter` option to support modifying the build output and/or development environment for different hosting providers.
4 changes: 4 additions & 0 deletions docs/future/vite.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,10 @@ The following subset of Remix config options are supported:

The Vite plugin also accepts the following additional options:

#### adapter

A function for adapting the build output and/or development environment for different hosting providers.

#### serverBuildDirectory

The path to the server build directory, relative to the project root. Defaults to `"build/server"`.
Expand Down
115 changes: 115 additions & 0 deletions integration/vite-adapter-test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
import * as fs from "node:fs";
import * as path from "node:path";
import { test, expect } from "@playwright/test";
import { normalizePath } from "vite";
import getPort from "get-port";

import {
createProject,
viteDev,
viteBuild,
VITE_CONFIG,
} from "./helpers/vite.js";

test.describe(async () => {
let port: number;
let cwd: string;
let stop: () => void;

function pathStartsWithCwd(pathname: string) {
return normalizePath(pathname).startsWith(normalizePath(cwd));
}

function pathRelativeToCwd(pathname: string) {
return normalizePath(path.relative(cwd, pathname));
}

test.beforeAll(async () => {
port = await getPort();
cwd = await createProject({
"vite.config.ts": await VITE_CONFIG({
port,
pluginOptions: `
{
adapter: async ({ remixConfig }) => ({
unstable_serverBundles(...args) {
// This lets us assert that user options are passed to adapter options hook
return remixConfig.unstable_serverBundles?.(...args) + "--adapter-options";
},
async buildEnd(args) {
let fs = await import("node:fs/promises");
await fs.writeFile("BUILD_END_ARGS.json", JSON.stringify(args, null, 2), "utf-8");
}
}),

unstable_serverBundles() {
return "user-options";
}
},
`,
}),
});
stop = await viteDev({ cwd, port });
});
test.afterAll(() => stop());

test("Vite / adapter / unstable_serverBundles and buildEnd hooks", async () => {
let { status } = viteBuild({ cwd });
expect(status).toBe(0);

expect(
Object.keys(
JSON.parse(
fs.readFileSync(path.join(cwd, "build/server/bundles.json"), "utf8")
).serverBundles
)
).toEqual(["user-options--adapter-options"]);

let buildEndArgs: any = JSON.parse(
fs.readFileSync(path.join(cwd, "BUILD_END_ARGS.json"), "utf8")
);

// Before rewriting to relative paths, assert that paths are absolute within cwd
expect(pathStartsWithCwd(buildEndArgs.serverBuildDirectory)).toBe(true);
expect(pathStartsWithCwd(buildEndArgs.assetsBuildDirectory)).toBe(true);

// Rewrite path args to be relative and normalized for snapshot test
buildEndArgs.serverBuildDirectory = pathRelativeToCwd(
buildEndArgs.serverBuildDirectory
);
buildEndArgs.assetsBuildDirectory = pathRelativeToCwd(
buildEndArgs.assetsBuildDirectory
);

expect(buildEndArgs).toEqual({
assetsBuildDirectory: "build/client",
serverBuildDirectory: "build/server",
serverBuildFile: "index.js",
unstable_serverBundlesManifest: {
routeIdToServerBundleId: {
"routes/_index": "user-options--adapter-options",
},
routes: {
root: {
file: "app/root.tsx",
id: "root",
path: "",
},
"routes/_index": {
file: "app/routes/_index.tsx",
id: "routes/_index",
index: true,
parentId: "root",
},
},
serverBundles: {
"user-options--adapter-options": {
file: "build/server/user-options--adapter-options/index.js",
id: "user-options--adapter-options",
},
},
},
unstable_ssr: true,
});
});
});
5 changes: 4 additions & 1 deletion packages/remix-dev/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,8 @@ export * as cli from "./cli/index";

export type { Manifest as AssetsManifest } from "./manifest";
export { getDependenciesToBundle } from "./dependencies";
export type { Unstable_ServerBundlesManifest } from "./vite";
export type {
Unstable_ServerBundlesManifest,
Unstable_VitePluginAdapter,
} from "./vite";
export { unstable_vitePlugin } from "./vite";
57 changes: 35 additions & 22 deletions packages/remix-dev/vite/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@ import fse from "fs-extra";
import colors from "picocolors";

import {
type ResolvedRemixVitePluginConfig,
type ResolvedVitePluginConfig,
type ServerBuildConfig,
type ServerBundlesManifest,
configRouteToBranchRoute,
} from "./plugin";
import type { ConfigRoute, RouteManifest } from "../config/routes";
Expand All @@ -32,15 +33,15 @@ async function extractConfig({
"production" // default NODE_ENV
);

let pluginConfig = viteConfig[
let remixConfig = viteConfig[
"__remixPluginResolvedConfig" as keyof typeof viteConfig
] as ResolvedRemixVitePluginConfig | undefined;
if (!pluginConfig) {
] as ResolvedVitePluginConfig | undefined;
if (!remixConfig) {
console.error(colors.red("Remix Vite plugin not found in Vite config"));
process.exit(1);
}

return { pluginConfig, viteConfig };
return { remixConfig, viteConfig };
}

function getAddressableRoutes(routes: RouteManifest): ConfigRoute[] {
Expand Down Expand Up @@ -83,25 +84,14 @@ function getRouteBranch(routes: RouteManifest, routeId: string) {
return branch.reverse();
}

export type ServerBundlesManifest = {
serverBundles: {
[serverBundleId: string]: {
id: string;
file: string;
};
};
routeIdToServerBundleId: Record<string, string>;
routes: RouteManifest;
};

async function getServerBuilds({
routes,
serverBuildDirectory,
serverBuildFile,
serverBundles,
rootDirectory,
appDirectory,
}: ResolvedRemixVitePluginConfig): Promise<{
}: ResolvedVitePluginConfig): Promise<{
serverBuilds: ServerBuildConfig[];
serverBundlesManifest?: ServerBundlesManifest;
}> {
Expand Down Expand Up @@ -178,7 +168,7 @@ async function getServerBuilds({

async function cleanServerBuildDirectory(
viteConfig: Vite.ResolvedConfig,
{ rootDirectory, serverBuildDirectory }: ResolvedRemixVitePluginConfig
{ rootDirectory, serverBuildDirectory }: ResolvedVitePluginConfig
) {
let isWithinRoot = () => {
let relativePath = path.relative(rootDirectory, serverBuildDirectory);
Expand Down Expand Up @@ -219,7 +209,7 @@ export async function build(
// so it can be accessed synchronously via `importViteEsmSync`
await preloadViteEsm();

let { pluginConfig, viteConfig } = await extractConfig({
let { remixConfig, viteConfig } = await extractConfig({
configFile,
mode,
root,
Expand Down Expand Up @@ -247,23 +237,46 @@ export async function build(
// output directories, we need to clean the root server build directory
// ourselves rather than relying on Vite to do it, otherwise you can end up
// with stale server bundle directories in your build output
await cleanServerBuildDirectory(viteConfig, pluginConfig);
await cleanServerBuildDirectory(viteConfig, remixConfig);

// Run the Vite client build first
await viteBuild();

// Then run Vite SSR builds in parallel
let { serverBuilds, serverBundlesManifest } = await getServerBuilds(
pluginConfig
remixConfig
);

await Promise.all(serverBuilds.map(viteBuild));

if (serverBundlesManifest) {
await fse.writeFile(
path.join(pluginConfig.serverBuildDirectory, "bundles.json"),
path.join(remixConfig.serverBuildDirectory, "bundles.json"),
JSON.stringify(serverBundlesManifest, null, 2),
"utf-8"
);
}

let {
isSpaMode,
assetsBuildDirectory,
serverBuildDirectory,
serverBuildFile,
} = remixConfig;

// Should this already be absolute on the resolved config object?
// In the meantime, we make it absolute before passing to adapter hooks
serverBuildDirectory = path.resolve(root, serverBuildDirectory);

await remixConfig.adapter?.buildEnd?.({
// Since this is public API, these properties need to mirror the options
// passed to the Remix plugin. This means we need to translate our internal
// names back to their original public counterparts. It's probably worth
// aligning these internally so we don't need this translation layer.
assetsBuildDirectory,
serverBuildDirectory,
serverBuildFile,
unstable_serverBundlesManifest: serverBundlesManifest,
unstable_ssr: !isSpaMode,
});
}
5 changes: 4 additions & 1 deletion packages/remix-dev/vite/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@
// don't need to have Vite installed as a peer dependency. Only types should
// be imported at the top level.
import type { RemixVitePlugin } from "./plugin";
export type { ServerBundlesManifest as Unstable_ServerBundlesManifest } from "./build";
export type {
ServerBundlesManifest as Unstable_ServerBundlesManifest,
VitePluginAdapter as Unstable_VitePluginAdapter,
} from "./plugin";

export const unstable_vitePlugin: RemixVitePlugin = (...args) => {
// eslint-disable-next-line @typescript-eslint/consistent-type-imports
Expand Down
Loading