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): change build output paths #8077

Merged
merged 8 commits into from
Nov 22, 2023
Merged
Show file tree
Hide file tree
Changes from 5 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
7 changes: 7 additions & 0 deletions .changeset/fast-needles-sort.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"@remix-run/dev": patch
---

Change Vite build output paths to fix a conflict between how Vite and the Remix compiler each manage the `public` directory.

The server is now compiled into `build/server` rather than `build`, and the client is now compiled into `build/client` rather than `public`. For more information on the changes and guidance on how to migrate your project, refer to the updated [Remix Vite documentation](https://remix.run/docs/en/main/future/vite).
5 changes: 0 additions & 5 deletions .changeset/proud-otters-cheat.md

This file was deleted.

52 changes: 45 additions & 7 deletions docs/future/vite.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,21 @@ export default defineConfig({

All other bundling-related options are now [configured with Vite][vite-config]. This means you have much greater control over the bundling process.

## New build output paths

There is a notable difference with the way Vite manages the `public` directory between Vite and the existing Remix compiler. During the build, Vite copies files from the `public` directory into `build/client`, whereas the Remix compiler left the `public` directory untouched and used a subdirectory (`public/build`) as the client build directory.

As a result, the build output paths are now different when using Vite:

- The server is now compiled into `build/server` by default.
- The client is now compiled into `build/client` by default.

This means that the following configuration defaults have now been changed:

- [assetsBuildDirectory][assets-build-directory] defaults to `"build/client"` rather than `"public/build"`
- [publicPath][public-path] defaults to `"/"` rather than `"/build/"`
- [serverBuildPath][server-build-path] defaults to `"build/server/index.js"` rather than `"build/index.js"`

## Additional features & plugins

One of the reasons that Remix is moving to Vite is, so you have less to learn when adopting Remix.
Expand Down Expand Up @@ -132,21 +147,25 @@ Vite handles imports for all sorts of different file types, sometimes in ways th
If you were using `remix-serve` in development (or `remix dev` without the `-c` flag), you'll need to switch to the new minimal dev server.
It comes built-in with the Remix Vite plugin and will take over when you run `vite dev`.

👉 **Update your `dev` and `build` scripts**
You'll also need to update to the new build output paths, which are `build/server` for the server and `build/client` for client assets.

👉 **Update your `dev`, `build` and `start` scripts**

```json filename=package.json lines=[3-4]
{
"scripts": {
"build": "vite build && vite build --ssr",
"dev": "vite dev",
"start": "remix-serve ./build/index.js"
"start": "remix-serve ./build/server/index.js"
}
}
```

#### Migrating from a custom server

If you were using a custom server in development, you'll need to edit your custom server to use Vite's `connect` middleware.
If you were using a custom server in development, you'll need to update your server code to reference the new build output paths, which are `build/server` for the server build and `build/client` for client assets.

You'll also need to edit your custom server to use Vite's `connect` middleware.
This will delegate asset requests and initial render requests to Vite during development, letting you benefit from Vite's excellent DX even with a custom server.

Remix exposes APIs for exactly this purpose:
Expand Down Expand Up @@ -185,22 +204,22 @@ if (vite) {
app.use(vite.middlewares);
} else {
app.use(
"/build",
express.static("public/build", {
"/assets",
express.static("build/client/assets", {
immutable: true,
maxAge: "1y",
})
);
}
app.use(express.static("public", { maxAge: "1h" }));
app.use(express.static("build/client", { maxAge: "1h" }));

// handle SSR requests
app.all(
"*",
createRequestHandler({
build: vite
? () => unstable_loadViteServerBuild(vite)
: await import("./build/index.js"),
: await import("./build/server/index.js"),
})
);

Expand Down Expand Up @@ -232,6 +251,24 @@ node --loader tsm ./server.ts

Just remember that there might be some noticeable slowdown for initial server startup if you do this.

#### Migrate references to build output paths

When using the existing Remix compiler's default options, the server was compiled into `build` and the client was compiled into `public/build`. Due to differences with the way Vite typically works with its `public` directory compared to the existing Remix compiler, these output paths have changed.

👉 **Update references to build output paths**

- The server is now compiled into `build/server` by default.
- The client is now compiled into `build/client` by default.

For example, to update the Dockerfile from the [Blues Stack][blues-stack]:

```diff filename=Dockerfile
-COPY --from=build /myapp/build /myapp/build
-COPY --from=build /myapp/public /myapp/public
+COPY --from=build /myapp/server/build /myapp/server/build
+COPY --from=build /myapp/client/build /myapp/client/build
```

#### Configure path aliases

The Remix compiler leverages the `paths` option in your `tsconfig.json` to resolve path aliases. This is commonly used in the Remix community to define `~` as an alias for the `app` directory.
Expand Down Expand Up @@ -655,3 +692,4 @@ We're definitely late to the Vite party, but we're excited to be here now!
[vite-plugin-cjs-interop]: https://github.com/cyco130/vite-plugin-cjs-interop
[ssr-no-external]: https://vitejs.dev/config/ssr-options.html#ssr-noexternal
[server-dependencies-to-bundle]: https://remix.run/docs/en/main/file-conventions/remix-config#serverdependenciestobundle
[blues-stack]: https://github.com/remix-run/blues-stack
23 changes: 20 additions & 3 deletions integration/helpers/create-fixture.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,9 +46,13 @@ export function json(value: JsonObject) {

export async function createFixture(init: FixtureInit, mode?: ServerMode) {
installGlobals();
let compiler = init.compiler ?? "remix";
let projectDir = await createFixtureProject(init, mode);
let buildPath = url.pathToFileURL(
path.join(projectDir, "build/index.js")
path.join(
projectDir,
compiler === "vite" ? "build/server/index.js" : "build/index.js"
)
).href;
let app: ServerBuild = await import(buildPath);
let handler = createRequestHandler(app, mode || ServerMode.Production);
Expand Down Expand Up @@ -98,6 +102,7 @@ export async function createFixture(init: FixtureInit, mode?: ServerMode) {
return {
projectDir,
build: app,
compiler,
requestDocument,
requestData,
postDocument,
Expand All @@ -118,7 +123,12 @@ export async function createAppFixture(fixture: Fixture, mode?: ServerMode) {
let nodebin = process.argv[0];
let serveProcess = spawn(
nodebin,
["node_modules/@remix-run/serve/dist/cli.js", "build/index.js"],
[
"node_modules/@remix-run/serve/dist/cli.js",
fixture.compiler === "vite"
? "server/build/index.js"
: "build/index.js",
],
{
env: {
NODE_ENV: mode || "production",
Expand Down Expand Up @@ -171,7 +181,14 @@ export async function createAppFixture(fixture: Fixture, mode?: ServerMode) {
return new Promise(async (accept) => {
let port = await getPort();
let app = express();
app.use(express.static(path.join(fixture.projectDir, "public")));
app.use(
express.static(
path.join(
fixture.projectDir,
fixture.compiler === "vite" ? "build/client" : "public"
)
)
);

app.all(
"*",
Expand Down
12 changes: 6 additions & 6 deletions integration/vite-build-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -242,12 +242,12 @@ test.describe("Vite build", () => {
appFixture.close();
});

test("server code is removed from client assets", async () => {
let publicBuildDir = path.join(fixture.projectDir, "public/build");
test("server code is removed from client build", async () => {
let clientBuildDir = path.join(fixture.projectDir, "build/client");

// detect client asset files
let assetFiles = glob.sync("**/*.@(js|jsx|ts|tsx)", {
cwd: publicBuildDir,
cwd: clientBuildDir,
absolute: true,
});

Expand Down Expand Up @@ -307,12 +307,12 @@ test.describe("Vite build", () => {

// verify asset files are emitted and served correctly
await page.getByRole("link", { name: "url1" }).click();
await page.waitForURL("**/build/assets/test1-*.txt");
await page.waitForURL("**/assets/test1-*.txt");
await page.getByText("test1").click();
await page.goBack();

await page.getByRole("link", { name: "url2" }).click();
await page.waitForURL("**/build/assets/test2-*.txt");
await page.waitForURL("**/assets/test2-*.txt");
await page.getByText("test2").click();
});

Expand All @@ -333,7 +333,7 @@ test.describe("Vite build", () => {

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"),
cwd: path.join(fixture.projectDir, "build/server/assets"),
});
let [asset, ...rest] = assetFiles;
expect(rest).toEqual([]); // Provide more useful test output if this fails
Expand Down
28 changes: 15 additions & 13 deletions packages/remix-dev/vite/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -255,12 +255,20 @@ export const remixVitePlugin: RemixVitePlugin = (options = {}) => {

let resolvePluginConfig =
async (): Promise<ResolvedRemixVitePluginConfig> => {
let defaults: Partial<RemixVitePluginOptions> = {
serverBuildPath: "build/server/index.js",
markdalgleish marked this conversation as resolved.
Show resolved Hide resolved
assetsBuildDirectory: "build/client",
publicPath: "/",
Copy link
Contributor

@hi-ogawa hi-ogawa Nov 22, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice change!
With this change of default publicPath from /build to /, I feel it became closer to what base vite config does (base default value is also / https://vitejs.dev/config/shared-options.html#base).

I think remix still doesn't support custom basename in general, but I was wondering if it makes sense to deprecate publicPath remix config and favor base option from user vite config at some point (which also somehow works on dev too hopefully).

For the reference:

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's a really good point! I'll keep this in mind when looking at custom base support.

};

let config = {
...defaults,
...pick(options, supportedRemixConfigKeys), // Avoid leaking any config options that the Vite plugin doesn't support
};

let rootDirectory =
viteUserConfig.root ?? process.env.REMIX_ROOT ?? process.cwd();

// Avoid leaking any config options that the Vite plugin doesn't support
let config = pick(options, supportedRemixConfigKeys);

// Only select the Remix config options that the Vite plugin uses
let {
appDirectory,
Expand Down Expand Up @@ -385,8 +393,8 @@ export const remixVitePlugin: RemixVitePlugin = (options = {}) => {

let fingerprintedValues = { entry, routes };
let version = getHash(JSON.stringify(fingerprintedValues), 8);
let manifestFilename = `manifest-${version}.js`;
let url = `${pluginConfig.publicPath}${manifestFilename}`;
let manifestPath = `assets/manifest-${version}.js`;
let url = `${pluginConfig.publicPath}${manifestPath}`;
let nonFingerprintedValues = { url, version };

let manifest: Manifest = {
Expand All @@ -395,7 +403,7 @@ export const remixVitePlugin: RemixVitePlugin = (options = {}) => {
};

await writeFileSafe(
path.join(pluginConfig.assetsBuildDirectory, manifestFilename),
path.join(pluginConfig.assetsBuildDirectory, manifestPath),
`window.__remixManifest=${JSON.stringify(manifest)};`
);

Expand Down Expand Up @@ -516,13 +524,6 @@ export const remixVitePlugin: RemixVitePlugin = (options = {}) => {
base: pluginConfig.publicPath,
build: {
...viteUserConfig.build,
// By convention Remix builds into a subdirectory within the
// public directory ("public/build" by default) so we don't want
// to copy the contents of the public directory around. This also
// ensures that we don't get caught in an infinite loop when
// `assetsBuildDirectory` is nested multiple levels deep within
// the public directory, e.g. "public/custom-base-dir/build"
copyPublicDir: false,
...(!isSsrBuild
? {
manifest: true,
Expand All @@ -545,6 +546,7 @@ export const remixVitePlugin: RemixVitePlugin = (options = {}) => {
// regardless of "ssrEmitAssets" option, so we also need to
// keep these JS files have to be kept as-is.
ssrEmitAssets: true,
copyPublicDir: false, // Assets in the public directory are only used by the client
manifest: true, // We need the manifest to detect SSR-only assets
outDir: path.dirname(pluginConfig.serverBuildPath),
rollupOptions: {
Expand Down
1 change: 0 additions & 1 deletion templates/unstable-vite-express/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,4 @@ node_modules

/.cache
/build
/public/build
.env
10 changes: 5 additions & 5 deletions templates/unstable-vite-express/server.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import express from "express";

installGlobals();

let vite =
const vite =
process.env.NODE_ENV === "production"
? undefined
: await unstable_createViteServer();
Expand All @@ -20,19 +20,19 @@ if (vite) {
app.use(vite.middlewares);
} else {
app.use(
"/build",
express.static("public/build", { immutable: true, maxAge: "1y" })
"/assets",
express.static("build/client/assets", { immutable: true, maxAge: "1y" })
);
}
app.use(express.static("public", { maxAge: "1h" }));
app.use(express.static("build/client", { maxAge: "1h" }));

// handle SSR requests
app.all(
"*",
createRequestHandler({
build: vite
? () => unstable_loadViteServerBuild(vite)
: await import("./build/index.js"),
: await import("./build/server/index.js"),
})
);

Expand Down
1 change: 0 additions & 1 deletion templates/unstable-vite/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,4 @@ node_modules

/.cache
/build
/public/build
.env
2 changes: 1 addition & 1 deletion templates/unstable-vite/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"scripts": {
"dev": "vite dev",
"build": "vite build && vite build --ssr",
"start": "remix-serve ./build/index.js",
"start": "remix-serve ./build/server/index.js",
"typecheck": "tsc"
},
"dependencies": {
Expand Down