Minimal Vite plugin for for isolating server-only and client-only code.
npm install -D vite-env-only
// vite.config.ts
import { defineConfig } from "vite"
import envOnly from "vite-env-only"
export default defineConfig({
plugins: [envOnly()],
})
Configures validation of import specifiers that should not be present on the client or server. Validation is performed against the raw import specifier in the source code. Uses micromatch for pattern matching globs.
{
denyImports?: {
client?: Array<string | RegExp>,
server?: Array<string | RegExp>
}
}
For example:
// vite.config.ts
import { defineConfig } from "vite"
import envOnly from "vite-env-only"
export default defineConfig({
plugins: [
envOnly({
denyImports: {
client: ["fs-extra", /^node:/],
},
}),
],
})
Configures validation of files that should not be present on the client or server. Validation is performed against the resolved and normalized root-relative file path. Uses micromatch for pattern matching globs.
{
denyFiles?: {
client?: Array<string | RegExp>,
server?: Array<string | RegExp>
}
}
For example:
// vite.config.ts
import { defineConfig } from "vite"
import envOnly from "vite-env-only"
export default defineConfig({
plugins: [
envOnly({
denyFiles: {
client: [
// Deny all files with a `.server` suffix
"**/*.server.*",
// Deny all files nested within a `.server` directory
"**/.server/**/*",
// Deny a specific file
"src/secrets.ts",
],
},
}),
],
})
Marks an expression as server-only and replaces it with undefined
on the client.
Keeps the expression as-is on the server.
For example:
import { serverOnly$ } from "vite-env-only"
export const message = serverOnly$("i only exist on the server")
On the client this produces:
export const message = undefined
On the server this produces:
export const message = "i only exist on the server"
Marks an expression as client-only and replaces it with undefined
on the server.
Keeps the expression as-is on the client.
For example:
import { clientOnly$ } from "vite-env-only"
export const message = clientOnly$("i only exist on the client")
On the client this produces:
export const message = "i only exist on the client"
On the server this produces:
export const message = undefined
This plugin eliminates any identifiers that become unreferenced as a result of macro replacement.
For example, given the following usage of serverOnly$
:
import { serverOnly$ } from "vite-env-only"
import { readFile } from "node:fs"
function readConfig() {
return JSON.parse(readFile.sync("./config.json", "utf-8"))
}
export const serverConfig = serverOnly$(readConfig())
On the client this produces:
export const serverConfig = undefined
On the server this produces:
import { readFile } from "node:fs"
function readConfig() {
return JSON.parse(readFile.sync("./config.json", "utf-8"))
}
export const serverConfig = readConfig()
The macro types capture the fact that values can be undefined
depending on the environment.
For example:
import { serverOnly$ } from "vite-env-only"
export const API_KEY = serverOnly$("secret")
// ^? string | undefined
If you want to opt out of strict type safety, you can use a non-null assertion (!
):
import { serverOnly$ } from "vite-env-only"
export const API_KEY = serverOnly$("secret")!
// ^? string
Vite already provides import.meta.env.SSR
which works in a similar way to these macros in production.
However, in development Vite neither replaces import.meta.env.SSR
nor performs dead-code elimination as Vite considers these steps to be optimizations.
In general, its a bad idea to rely on optimizations for correctness. In contrast, these macros treat code replacement and dead-code elimination as part of their feature set.
Additionally, these macros use function calls to mark expressions as server-only or client-only. That means they can guarantee that code within the function call never ends up in the wrong environment while only transforming a single AST node type: function call expressions.
import.meta.env.SSR
is instead a special identifier which can show up in many different AST node types: if
statements, ternaries, switch
statements, etc.
This makes it far more challenging to guarantee that dead-code completely eliminated.
Thanks to these project for exploring environment isolation and conventions for transpilation: