Skip to content
Merged
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
152 changes: 152 additions & 0 deletions docs/latest/advanced/vite.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,158 @@ changes to components, islands, and CSS are reflected in the browser instantly
without a full page reload. This is powered by Prefresh, Preact's fast refresh
implementation.

## Migrating from the Builder to Vite

If your Fresh 2 project was created with `--builder` (or predates the Vite
plugin), it uses the legacy [`Builder`](/docs/advanced/builder) class wired up
in `dev.ts`. Migrating to Vite is mostly a matter of swapping `dev.ts` for a
`vite.config.ts`, moving CSS into the module graph, and updating `deno.json`.

### 1. Update `deno.json`

Add the Vite plugin and `vite` itself to your imports, drop the Builder-only
Tailwind packages (if any), and point `compilerOptions.types` at Vite's client
types so HMR and asset imports type-check:

```diff deno.json
{
"nodeModulesDir": "manual",
"tasks": {
- "dev": "deno run -A --watch=static/,routes/ dev.ts",
- "build": "deno run -A dev.ts build",
+ "dev": "vite",
+ "build": "vite build",
"start": "deno serve -A _fresh/server.js"
},
"imports": {
"fresh": "jsr:@fresh/core@^2",
"preact": "npm:preact@^10",
"@preact/signals": "npm:@preact/signals@^2",
+ "@fresh/plugin-vite": "jsr:@fresh/plugin-vite@^1",
+ "vite": "npm:vite@^7",
+ "@types/babel__core": "npm:@types/babel__core@^7"
},
"compilerOptions": {
"jsx": "precompile",
"jsxImportSource": "preact",
+ "types": ["vite/client"]
}
}
```

If you were using `@fresh/plugin-tailwind` / `@fresh/plugin-tailwindcss-v3`,
remove those imports — Vite has a first-party Tailwind plugin (see step 4).

### 2. Replace `dev.ts` with `vite.config.ts`

Delete `dev.ts` and create a `vite.config.ts` at the project root:

```ts vite.config.ts
import { defineConfig } from "vite";
import { fresh } from "@fresh/plugin-vite";

export default defineConfig({
plugins: [fresh()],
});
```

If you passed options to `new Builder({ ... })` (custom `serverEntry`,
`islandDir`, `routeDir`, `staticDir`, `ignore`), pass the equivalent options to
`fresh({ ... })` — the names match. See [Configuration](#configuration) above.

Any `builder.registerIsland("jsr:@scope/pkg/Island.tsx")` calls become
`fresh({ islandSpecifiers: ["jsr:@scope/pkg/Island.tsx"] })`.

### 3. Add a `client.ts` entry

The Builder discovered CSS by scanning `static/`. Vite needs CSS to be part of
the module graph so it can hash, bundle, and hot-reload it. Move your stylesheet
out of `static/` and import it from a new `client.ts` file:

```diff Project structure
<project root>
- ├── static/styles.css
+ ├── assets/styles.css
+ ├── client.ts
├── vite.config.ts
└── main.ts
```

```ts client.ts
// Import CSS files here for hot module reloading to work.
import "./assets/styles.css";
```

Then remove the manual `<link>` from your app wrapper — Vite injects the
stylesheet for you:

```diff routes/_app.tsx
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>My App</title>
- <link rel="stylesheet" href="/styles.css" />
</head>
```

Static assets that are not part of the JS/CSS graph (favicons, images served by
URL, robots.txt, …) stay in `static/`.

### 4. Switch the Tailwind plugin (if applicable)

Replace the Builder-side Tailwind plugin with the official Vite plugin:

```diff deno.json
"imports": {
- "@fresh/plugin-tailwind": "jsr:@fresh/plugin-tailwind@^1",
- "@tailwindcss/postcss": "npm:@tailwindcss/postcss@^4",
- "postcss": "npm:postcss@^8",
+ "@tailwindcss/vite": "npm:@tailwindcss/vite@^4",
"tailwindcss": "npm:tailwindcss@^4"
}
```

```ts vite.config.ts
import { defineConfig } from "vite";
import { fresh } from "@fresh/plugin-vite";
import tailwindcss from "@tailwindcss/vite";

export default defineConfig({
plugins: [fresh(), tailwindcss()],
});
```

Make sure your stylesheet starts with `@import "tailwindcss";` and is imported
from `client.ts`.

### 5. Verify

Run `deno install` to pull in the new npm packages, then:

```sh Terminal
deno task dev # starts Vite with HMR
deno task build # writes _fresh/server.js and client assets
deno task start # deno serve -A _fresh/server.js
```

The output layout under `_fresh/` is the same as the Builder produced, so
deployment configuration (Deno Deploy, Docker, `deno compile`) does not need to
change.

### Checklist

- [ ] `dev.ts` removed, `vite.config.ts` added
- [ ] `client.ts` created and imports your CSS
- [ ] Stylesheet moved out of `static/` and the `<link>` removed from `_app.tsx`
- [ ] `deno.json` tasks point at `vite` / `vite build`
- [ ] `@fresh/plugin-vite`, `vite`, and `@types/babel__core` in `imports`
- [ ] `"vite/client"` in `compilerOptions.types`
- [ ] Tailwind (if used) switched to `@tailwindcss/vite`

> [info]: If you get stuck, run `deno run -Ar jsr:@fresh/init` in a scratch
> directory and diff the generated project against yours — the generator is the
> source of truth for a working Vite-based Fresh setup.

## Debugging

To debug Vite resolution issues, run Vite with the `--debug` flag:
Expand Down
Loading