NexusCS

Vite

Build Tools
Fast, modern frontend build tool with native ESM, instant HMR, and optimized production builds. Vite provides a lightning-fast dev experience for modern web projects.
featured

Getting started

Installation

# Create new project with prompts
npm create vite@latest

# Create with specific template
npm create vite@latest my-app -- --template react

# Create with TypeScript
npm create vite@latest my-app -- --template react-ts

Available templates: vanilla, vanilla-ts, vue, vue-ts, react, react-ts, preact, preact-ts, lit, lit-ts, svelte, svelte-ts, solid, solid-ts, qwik, qwik-ts

Quick start

cd my-app
npm install              # Install dependencies
npm run dev              # Start dev server
npm run build            # Production build
npm run preview          # Preview prod build

Basic structure

my-app/
├── index.html           # Entry HTML
├── package.json
├── vite.config.js       # Config file
├── public/              # Static assets
└── src/
    ├── main.js          # App entry
    └── style.css

Dev server

CLI commands

vite                     # Start dev server
vite --host              # Expose to network
vite --port 3000         # Custom port
vite --open              # Auto-open browser
vite --https             # Enable HTTPS
vite --force             # Force re-optimize

Dev server config

// vite.config.js
export default {
  server: {
    port: 3000,
    host: true, // Listen on all addresses
    open: "/docs", // Auto-open path
    https: true,
    cors: true,
    proxy: {
      "/api": "http://localhost:4000",
    },
  },
};

Hot Module Replacement (HMR)

// Accept hot updates
if (import.meta.hot) {
  import.meta.hot.accept((newModule) => {
    // Handle module update
  });
}

// Dispose side effects
import.meta.hot.dispose((data) => {
  // Cleanup
});

// Self-accept
import.meta.hot.accept();

Configuration

Basic config

// vite.config.js
import { defineConfig } from 'vite'

export default defineConfig({
  root: './src',         // Project root
  base: '/my-app/',      // Public base path
  publicDir: 'public',   # Static assets dir
  build: {
    outDir: '../dist',   # Output directory
    assetsDir: 'assets'  # Assets subdirectory
  }
})

Config with TypeScript

// vite.config.ts
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";

export default defineConfig({
  plugins: [react()],
  resolve: {
    alias: {
      "@": "/src",
      "@components": "/src/components",
    },
  },
});

Conditional config

export default defineConfig(({ command, mode }) => {
  if (command === "serve") {
    // Dev config
    return {
      server: { port: 3000 },
    };
  } else {
    // Build config
    return {
      build: { minify: "esbuild" },
    };
  }
});

Asset handling

Import assets

// Import as URL
import imgUrl from "./img.png";

// Import as string (text files)
import txt from "./file.txt?raw";

// Import as Web Worker
import Worker from "./worker.js?worker";

// Import JSON
import data from "./data.json";

Static assets

// Files in /public are served at /
<img src="/logo.png" />;

// Import from src/ gets hashed URL
import logo from "./assets/logo.png";
<img src={logo} />;

URL suffixes

// Explicit URL import
import url from "./asset.png?url";

// Force raw string
import raw from "./file.txt?raw";

// Import as inline base64
import inline from "./data.svg?inline";

// Web Worker
import MyWorker from "./worker?worker";
const worker = new MyWorker();

CSS & preprocessors

Import CSS

// Import global CSS
import "./style.css";

// CSS modules
import styles from "./style.module.css";
<div className={styles.red} />;

// Import SCSS
import "./style.scss";

// Import Less
import "./style.less";

CSS Modules

/* style.module.css */
.red {
  color: red;
}
import styles from './style.module.css'

// Access as property
<div className={styles.red} />

// Multiple classes
<div className={`${styles.red} ${styles.bold}`} />

PostCSS

// vite.config.js
export default {
  css: {
    postcss: {
      plugins: [require("autoprefixer"), require("postcss-nested")],
    },
  },
};

Or create postcss.config.js:

module.exports = {
  plugins: {
    autoprefixer: {},
    "postcss-nested": {},
  },
};

Preprocessors

# Install preprocessor
npm install -D sass
npm install -D less
npm install -D stylus
// Auto-detected by file extension
import "./style.scss";
import "./style.less";
import "./style.styl";

Build

Production build

vite build               # Build for production
vite build --mode staging  # Custom mode
vite build --base=/app/    # Custom base path
vite build --outDir dist   # Custom output
vite build --watch         # Watch mode

Build config

export default {
  build: {
    target: 'es2015',    # Browser target
    outDir: 'dist',      # Output directory
    assetsDir: 'assets', # Assets subdirectory
    assetsInlineLimit: 4096,  # Inline < 4kb
    cssCodeSplit: true,  # Split CSS
    sourcemap: true,     # Generate sourcemaps
    minify: 'esbuild',   # esbuild | terser
    rollupOptions: {
      output: {
        manualChunks: {
          vendor: ['react', 'react-dom']
        }
      }
    }
  }
}

Preview build

vite preview             # Preview production build
vite preview --port 4173 # Custom port
vite preview --open      # Auto-open browser

Bundle analysis

// vite.config.js
import { visualizer } from "rollup-plugin-visualizer";

export default {
  plugins: [
    visualizer({
      open: true,
      gzipSize: true,
      brotliSize: true,
    }),
  ],
};

Plugins

Official plugins

# React with Fast Refresh
npm install -D @vitejs/plugin-react

# Vue 3 with SFC support
npm install -D @vitejs/plugin-vue

# Vue JSX
npm install -D @vitejs/plugin-vue-jsx

# Legacy browser support
npm install -D @vitejs/plugin-legacy

Using plugins

import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import legacy from "@vitejs/plugin-legacy";

export default defineConfig({
  plugins: [
    react({
      fastRefresh: true,
      jsxRuntime: "automatic",
    }),
    legacy({
      targets: ["defaults", "not IE 11"],
    }),
  ],
});

Common community plugins

// Image optimization
import imagemin from "vite-plugin-imagemin";

// SVG as React components
import svgr from "vite-plugin-svgr";

// PWA
import { VitePWA } from "vite-plugin-pwa";

export default {
  plugins: [imagemin(), svgr(), VitePWA({ registerType: "autoUpdate" })],
};

Environment variables

.env files

# .env - All environments
VITE_API_URL=https://api.example.com

# .env.development - Dev only
VITE_API_URL=http://localhost:3000

# .env.production - Prod only
VITE_API_URL=https://api.prod.com

# .env.local - Local overrides (gitignored)
VITE_SECRET_KEY=abc123

⚠️ Only VITE_ prefixed vars are exposed to client code

Access variables

// In application code
const apiUrl = import.meta.env.VITE_API_URL
const mode = import.meta.env.MODE
const isDev = import.meta.env.DEV
const isProd = import.meta.env.PROD

// TypeScript IntelliSense
/// <reference types="vite/client" />
interface ImportMetaEnv {
  readonly VITE_API_URL: string
}

Define variables

// vite.config.js
export default {
  define: {
    __APP_VERSION__: JSON.stringify("1.0.0"),
    __API_URL__: JSON.stringify(process.env.API_URL),
  },
};

SSR (Server-Side Rendering)

Basic SSR setup

// server.js
import express from "express";
import { createServer as createViteServer } from "vite";

const app = express();

const vite = await createViteServer({
  server: { middlewareMode: true },
});

app.use(vite.middlewares);

app.use("*", async (req, res) => {
  const url = req.originalUrl;

  // Load template
  let template = fs.readFileSync("index.html", "utf-8");
  template = await vite.transformIndexHtml(url, template);

  // Render app
  const { render } = await vite.ssrLoadModule("/src/entry-server.js");
  const appHtml = await render(url);

  const html = template.replace("<!--ssr-outlet-->", appHtml);
  res.status(200).set({ "Content-Type": "text/html" }).end(html);
});

app.listen(3000);

SSR config

// vite.config.js
export default {
  ssr: {
    noExternal: ['package-name'],  # Force bundle
    external: ['fsevents'],        # Exclude
    target: 'node'                 # node | webworker
  }
}

Build for SSR

# Build client
vite build --outDir dist/client

# Build server
vite build --ssr src/entry-server.js --outDir dist/server

Library mode

Build as library

// vite.config.js
import { resolve } from "path";

export default {
  build: {
    lib: {
      entry: resolve(__dirname, "lib/main.js"),
      name: "MyLib",
      fileName: (format) => `my-lib.${format}.js`,
      formats: ["es", "cjs", "umd", "iife"],
    },
    rollupOptions: {
      // Externalize deps that shouldn't be bundled
      external: ["vue", "react"],
      output: {
        globals: {
          vue: "Vue",
          react: "React",
        },
      },
    },
  },
};

Library package.json

{
  "name": "my-lib",
  "type": "module",
  "files": ["dist"],
  "main": "./dist/my-lib.cjs.js",
  "module": "./dist/my-lib.es.js",
  "exports": {
    ".": {
      "import": "./dist/my-lib.es.js",
      "require": "./dist/my-lib.cjs.js"
    }
  }
}

Performance optimization

Code splitting

// Dynamic imports
const module = await import("./module.js");

// React lazy loading
const Component = lazy(() => import("./Component"));

// Manual chunks
export default {
  build: {
    rollupOptions: {
      output: {
        manualChunks(id) {
          if (id.includes("node_modules")) {
            return "vendor";
          }
        },
      },
    },
  },
};

Dependency pre-bundling

export default {
  optimizeDeps: {
    include: ['linked-dep'],      # Force include
    exclude: ['your-package'],    # Don't pre-bundle
    entries: ['./src/main.js'],   # Entry points
    esbuildOptions: {
      target: 'es2020'
    }
  }
}

Build optimizations

export default {
  build: {
    minify: 'esbuild',        # Faster than terser
    cssCodeSplit: true,       # Split CSS by chunk
    assetsInlineLimit: 4096,  # Inline small assets
    chunkSizeWarningLimit: 1000,
    rollupOptions: {
      output: {
        compact: true,
        manualChunks: {
          'react-vendor': ['react', 'react-dom'],
          'ui-vendor': ['@mui/material']
        }
      }
    }
  }
}

Webpack migration tips

webpack.config.js → vite.config.js

Webpack Vite
entry build.rollupOptions.input
output.path build.outDir
output.publicPath base
resolve.alias resolve.alias
module.rules Native or plugins
plugins plugins (different ecosystem)
devServer.proxy server.proxy
mode mode (from CLI or config)
optimization.splitChunks build.rollupOptions.output

Common patterns

// webpack: require() → Vite: import
// ❌ const img = require('./img.png')
import img from './img.png'

// webpack: require.context → Vite: import.meta.glob
// ❌ require.context('./dir', true, /\.js$/)
const modules = import.meta.glob('./dir/**/*.js')

// webpack: process.env.* → Vite: import.meta.env.VITE_*
// ❌ const api = process.env.API_URL
const api = import.meta.env.VITE_API_URL

// webpack: DefinePlugin → Vite: define
// ❌ new webpack.DefinePlugin({ ... })
export default { define: { ... } }

Advanced patterns

Glob imports

// Eager import all modules
const modules = import.meta.glob("./dir/*.js", { eager: true });

// Lazy import with await
const modules = import.meta.glob("./dir/*.js");
const mod = await modules["./dir/foo.js"]();

// Import as raw string
const files = import.meta.glob("./dir/*.md", {
  as: "raw",
  eager: true,
});

// Custom queries
const urls = import.meta.glob("./dir/*.svg", {
  as: "url",
  eager: true,
});

Multi-page app

import { resolve } from "path";

export default {
  build: {
    rollupOptions: {
      input: {
        main: resolve(__dirname, "index.html"),
        nested: resolve(__dirname, "nested/index.html"),
        admin: resolve(__dirname, "admin/index.html"),
      },
    },
  },
};

Custom server middleware

export default {
  server: {
    middlewareMode: false,
  },
  plugins: [
    {
      name: "custom-middleware",
      configureServer(server) {
        server.middlewares.use((req, res, next) => {
          // Custom logic
          next();
        });
      },
    },
  ],
};

Debugging & troubleshooting

Debug mode

# Enable debug logs
DEBUG=vite:* vite

# Specific module
DEBUG=vite:deps vite

# Clear cache and restart
vite --force

Common issues

# "Unexpected token" errors
# Fix: Add to optimizeDeps.include

# Dependency pre-bundling issues
# Fix: Use --force flag or delete node_modules/.vite

# CORS errors in dev
# Fix: Configure server.cors in vite.config.js

# Large bundle size
# Fix: Use rollup-plugin-visualizer to analyze

Config resolution

// Log resolved config
export default defineConfig(({ command, mode }) => {
  console.log('Command:', command)  # serve | build
  console.log('Mode:', mode)        # development | production

  return {
    // config
  }
})

Gotchas

⚠️ Environment variables - Only VITE_ prefixed vars are exposed to client. Don't use for secrets!

⚠️ index.html is entry point - Unlike webpack, index.html is at project root and imports /src/main.js

⚠️ No require() - Use ESM import instead. Use import.meta.glob() instead of require.context()

⚠️ Public directory - Files in /public are served as-is at /. Don't import them in code, reference by absolute path

⚠️ JSON imports - Named imports work: import { field } from './data.json'

⚠️ CSS Modules - Must use .module.css extension, not configurable like webpack

⚠️ Dynamic imports with variables - Must use import.meta.glob() for dynamic patterns, can't use template literals in import()

⚠️ SSR external packages - Some packages need ssr.noExternal to work properly in SSR

⚠️ Base path - When deploying to subpath, must set base: '/my-app/' in config

Also see