Skip to content

pcattori/vite-env-only

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

82 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

ci workflow

vite-env-only

Minimal Vite plugin for for isolating server-only and client-only code.

Install

npm install -D vite-env-only

Setup

// vite.config.ts
import { defineConfig } from "vite"
import envOnly from "vite-env-only"

export default defineConfig({
  plugins: [envOnly()],
})

Options

denyImports

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.

{
  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"],
      },
    }),
  ],
})

denyFiles

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.

{
  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\.(\w+)?$/,
          // Deny all files nested within a `.server` directory
          /(^|\/).server\//,
          // Deny a specific file
          "src/secrets.ts",
        ],
      },
    }),
  ],
})

Macros

serverOnly$

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"

clientOnly$

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

Dead-code elimination

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()

Type safety

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

Why?

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.

Prior art

Thanks to these project for exploring environment isolation and conventions for transpilation: