Important: This documentation covers Yarn 1 (Classic).
For Yarn 2+ docs and migration guide, see yarnpkg.com.

Package detail

vite-env-only

pcattori170.6kMIT3.0.3TypeScript support: included

Vite plugins for isolating server-only and client-only code

vite-plugin, env, only, client, server, macro, deny, imports

readme

ci workflow

vite-env-only

Vite plugins for isolating server-only and client-only code

Install

npm install -D vite-env-only

Deny Imports

Prevents specific packages and files from being included in the client or server bundle by throwing an error at build-time when a matching import would have been included.

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

export default defineConfig({
  plugins: [
    denyImports({
      client: {
        specifiers: ["fs-extra", /^node:/, "@prisma/*"],
        files: ["**/.server/*", "**/*.server.*"],
      },
      server: {
        specifiers: ["jquery"],
      },
    }),
  ],
})
{
  client?: {
    specifiers?: Array<string | RegExp>,
    files?: Array<string | RegExp>
  },
  server?: {
    specifiers?: Array<string | RegExp>,
    files?: Array<string | RegExp>
  }
}

specifiers

Matching is performed against the raw import specifier in the source code. Match patterns can be:

  • String literal for exact matches
  • Globs via micromatch
  • RegExps

files

Matching is performed against the resolved and normalized root-relative file path. Match patterns can be:

  • String literal for exact matches
  • Globs via micromatch
  • RegExps

Macros

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

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

All macros can be imported within your app code from "vite-env-only/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/macros"

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/macros"

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/macros"
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/macros"

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/macros"

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:

changelog

vite-env-only

3.0.3

Patch Changes

3.0.2

Patch Changes

  • 69d739d: Better dead code elimination

    Upgrading to babel-dead-code-eliminiation@1.0.5 as it contains fixes for:

    • Object destructuring
    • Array destructuring
    • Function expressions
    • Arrow function expressions

3.0.1

Patch Changes

  • 02c683a: Allow call expressions in macro identifier validation

    Previously, the code had duplicated checks for allowing macro within import specifiers. This was always meant to be a check for import specifiers and a check for a call expression.

3.0.0

Major Changes

  • a46e247: Rather than ship as a monolithic plugin, we've split up vite-env-only into two separate plugins: envOnlyMacros and denyImports. These are both named exports of vite-env-only; the default export has been removed. This makes it easy to tell if you app is relying on macros, import denial, or both.

    Additionally, we've changed the macros themselves to come from vite-env-only/macros to more clearly separate vite-env-only plugins (for use in your vite.config.ts) and vite-env-only macros (for use in your app code).

    Migrating macros

    👉 In your vite.config.ts, replace the default import with the envOnlyMacros named import:

    -import envOnly from "vite-env-only"
    +import { envOnlyMacros } from "vite-env-only"
    
    export default {
      plugins: [
    -    envOnly(),
    +    envOnlyMacros(),
      ]
    }

    👉 In your app code, replace your macro imports to use the new /macros export:

    -import { serverOnly$ } from "vite-env-only"
    +import { serverOnly$ } from "vite-env-only/macros"

    Migrating denyImports + denyFiles

    The new denyImports plugin replaces the old denyImports and denyFiles options. Both of these options denied imports:

    • denyImports denied imports with specific import specifiers
    • denyFiles denied imports that resolved to specific files

    Additionally, neither of these options had anything to do with macros. But there wasn't a way to configure vite-env-only for import denial without also implicitly setting up its macros.

    The new denyImports named export is a new plugin replaces these options.

    The specifiers option replaces the old denyImports option. Matching is performed against the raw import specifier in the source code.

    The files option replaces the old denyFiles option. Matching is performed against the resolved and normalized root-relative file path.

    {
      client?: {
        specifiers?: Array<string | RegExp>,
        files?: Array<string | RegExp>
      },
      server?: {
        specifiers?: Array<string | RegExp>,
        files?: Array<string | RegExp>
      }
    }

    👉 In your vite.config.ts, replace the envOnly plugin with the denyImports plugin.

    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:/, "@prisma/*"],
            server: ["jquery"],
          },
          denyFiles: {
            client: ["**/.server/*", "**/*.server.*"],
          },
        }),
      ],
    })

    Should now be written as:

    // vite.config.ts
    import { defineConfig } from "vite"
    import { denyImports } from "vite-env-only"
    
    export default defineConfig({
      plugins: [
        denyImports({
          client: {
            specifiers: ["fs-extra", /^node:/, "@prisma/*"],
            files: ["**/.server/*", "**/*.server.*"],
          },
          server: {
            specifiers: ["jquery"],
          },
        }),
      ],
    })

    🚨 Macros are not enabled by the denyImports plugin. 🚨 If you also wanted to use macros, be sure to explicitly add the envOnlyMacros plugin to your vite.config.ts.

2.4.1

Patch Changes

  • 4f87331: Use default import from micromatch to fix ESM build error

2.4.0

Minor Changes

  • 25a324d: Allow globs for denyImports and denyFiles

    Using micromatch for pattern matching