Vite Integration

Modern JavaScript applications require sophisticated build tools. Nette Assets provides first-class integration with Vite, the next-generation frontend build tool. Get lightning-fast development with Hot Module Replacement (HMR) and optimized production builds with zero configuration hassle.

  • Zero configuration – automatic bridge between Vite and PHP templates
  • Complete dependency management – one tag handles all assets
  • Hot Module Replacement – instant JavaScript and CSS updates
  • Optimized production builds – code splitting and tree shaking

Nette Assets integrates seamlessly with Vite, so you get all these benefits while writing your templates as usual.

Setting Up Vite

Let's set up Vite step by step. Don't worry if you're new to build tools – we'll explain everything!

Step 1: Install Vite

First, install Vite and the Nette plugin in your project:

npm install -D vite @nette/vite-plugin

This installs Vite and a special plugin that helps Vite work perfectly with Nette.

Step 2: Project Structure

The standard approach is to place source asset files in an assets/ folder in your project root, and the compiled versions in www/assets/:

web-project/
├── assets/                   ← source files (SCSS, TypeScript, source images)
│   ├── public/               ← static files (copied as-is)
│   │   └── favicon.ico
│   ├── images/
│   │   └── logo.png
│   ├── app.js                ← main entry point
│   └── style.css             ← your styles
└── www/                      ← public directory (document root)
	├── assets/               ← compiled files will go here
	└── index.php

The assets/ folder contains your source files – the code you write. Vite will process these files and put the compiled versions in www/assets/.

Step 3: Configure Vite

Create a vite.config.ts file in your project root. This file tells Vite where to find your source files and where to put the compiled ones.

The Nette Vite plugin comes with smart defaults that make configuration simple. It assumes your front-end source files are in the assets/ directory (option root) and compiled files go to www/assets/ (option outDir). You only need to specify the entry point:

import { defineConfig } from 'vite';
import nette from '@nette/vite-plugin';

export default defineConfig({
	plugins: [
		nette({
			entry: 'app.js',
		}),
	],
});

If you want to specify another directory name to build your assets, you will need to change a few options:

export default defineConfig({
	root: 'assets', // root directory of source assets

	build: {
		outDir: '../www/assets',  // where compiled files go
	},

	// ... other config ...
});

The outDir path is considered relative to root, which is why there's ../ at the beginning.

Step 4: Configure Nette

Tell Nette Assets about Vite in your common.neon:

assets:
	mapping:
		default:
			type: vite      # tells Nette to use the ViteMapper
			path: assets

Step 5: Add scripts

Add these scripts to your package.json:

{
	"scripts": {
		"dev": "vite",
		"build": "vite build"
	}
}

Now you can:

  • npm run dev – start development server with hot reloading
  • npm run build – create optimized production files

Entry Points

An entry point is the main file where your application starts. From this file, you import other files (CSS, JavaScript modules, images), creating a dependency tree. Vite follows these imports and bundles everything together.

Example entry point assets/app.js:

// Import styles
import './style.css'

// Import JavaScript modules
import netteForms from 'nette-forms';
import naja from 'naja';

// Initialize your application
netteForms.initOnLoad();
naja.initialize();

In the template you can insert an entry point as follows:

{asset 'app.js'}

Nette Assets automatically generates all necessary HTML tags – JavaScript, CSS, and any other dependencies.

Multiple Entry Points

Larger applications often need separate entry points:

export default defineConfig({
	plugins: [
		nette({
			entry: [
				'app.js',      // public pages
				'admin.js',    // admin panel
			],
		}),
	],
});

Use them in different templates:

{* In public pages *}
{asset 'app.js'}

{* In admin panel *}
{asset 'admin.js'}

Important: Source vs Compiled Files

It's crucial to understand that on production you can only load:

  1. Entry points defined in entry
  2. Files from the assets/public/ directory

You cannot load using {asset} arbitrary files from assets/ – only assets referenced by JavaScript or CSS files. If your file is not referenced anywhere it will not be compiled. If you want to make Vite aware of other assets, you can move them to the public folder.

Please note that by default, Vite will inline all assets smaller than 4KB, so you will not be able to reference these files directly. (See Vite documentation).

{* ✓ This works - it's an entry point *}
{asset 'app.js'}

{* ✓ This works - it's in assets/public/ *}
{asset 'favicon.ico'}

{* ✗ This won't work - random file in assets/ *}
{asset 'components/button.js'}

Development Mode

Development mode is completely optional but provides significant benefits when enabled. The main advantage is Hot Module Replacement (HMR) – see changes instantly without losing application state, making the development experience much smoother and faster.

Vite is a modern build tool that makes development incredibly fast. Unlike traditional bundlers, Vite serves your code directly to the browser during development, which means instant server start no matter how large your project and lightning-fast updates.

Starting Development Server

Run the development server:

npm run dev

You'll see:

  ➜  Local:   http://localhost:5173/
  ➜  Network: use --host to expose

Keep this terminal open while developing.

The Nette Vite plugin automatically detects when:

  1. Vite dev server is running
  2. Your Nette application is in debug mode

When both conditions are met, Nette Assets loads files from the Vite dev server instead of the compiled directory:

{asset 'app.js'}
{* In development: <script src="http://localhost:5173/app.js" type="module"></script> *}
{* In production: <script src="/assets/app-4a8f9c7.js" type="module"></script> *}

No configuration needed – it just works!

Working on Different Domains

If your development server runs on something other than localhost (like myapp.local), you might encounter CORS (Cross-Origin Resource Sharing) issues. CORS is a security feature in web browsers that blocks requests between different domains by default. When your PHP application runs on myapp.local but Vite runs on localhost:5173, the browser sees these as different domains and blocks the requests.

You have two options to solve this:

Option 1: Configure CORS

The simplest solution is to allow cross-origin requests from your PHP application:

export default defineConfig({
	// ... other config ...

	server: {
		cors: {
			origin: 'http://myapp.local',  // your PHP app URL
		},
	},
});

Option 2: Run Vite on your domain

The other solution is to make Vite run on the same domain as your PHP application.

export default defineConfig({
	// ... other config ...

	server: {
		host: 'myapp.local',  // same as your PHP app
	},
});

Actually, even in this case, you need to configure CORS because the dev server runs on the same hostname but on a different port. However, in this case, CORS is automatically configured by the Nette Vite plugin.

HTTPS Development

If you develop on HTTPS, you need certificates for your Vite development server. The easiest way is using a plugin that generates certificates automatically:

npm install -D vite-plugin-mkcert

Here's how to configure it in vite.config.ts:

import mkcert from 'vite-plugin-mkcert';

export default defineConfig({
	// ... other config ...

	plugins: [
		mkcert(),  // generates certificates automatically and enables https
		nette(),
	],
});

Note that if you're using the CORS configuration (Option 1 from above), you need to update the origin URL to use https:// instead of http://.

Production Builds

Create optimized production files:

npm run build

Vite will:

  • Minify all JavaScript and CSS
  • Split code into optimal chunks
  • Generate hashed filenames for cache-busting
  • Create a manifest file for Nette Assets

Example output:

www/assets/
├── app-4f3a2b1c.js       # Your main JavaScript (minified)
├── app-7d8e9f2a.css      # Extracted CSS (minified)
├── vendor-8c4b5e6d.js    # Shared dependencies
└── .vite/
	└── manifest.json     # Mapping for Nette Assets

The hashed filenames ensure browsers always load the latest version.

Public Folder

Files in assets/public/ directory are copied to the output without processing:

assets/
├── public/
│   ├── favicon.ico
│   ├── robots.txt
│   └── images/
│       └── og-image.jpg
├── app.js
└── style.css

Reference them normally:

{* These files are copied as-is *}
<link rel="icon" href={asset 'favicon.ico'}>
<meta property="og:image" content={asset 'images/og-image.jpg'}>

For public files, you can use FilesystemMapper features:

assets:
	mapping:
		default:
			type: vite
			path: assets
			extension: [webp, jpg, png]  # Try WebP first
			versioning: true             # Add cache-busting

In the vite.config.ts configuration you can change the public folder using the publicDir option.

Dynamic Imports

Vite automatically splits code for optimal loading. Dynamic imports allow you to load code only when it's actually needed, reducing the initial bundle size:

// Load heavy components on demand
button.addEventListener('click', async () => {
	let { Chart } = await import('./components/chart.js')
	new Chart(data)
})

Dynamic imports create separate chunks that are loaded only when needed. This is called “code splitting” and it's one of Vite's most powerful features. When you use dynamic imports, Vite automatically creates separate JavaScript files for each dynamically imported module.

The {asset 'app.js'} tag does not automatically preload these dynamic chunks. This is intentional behavior – we don't want to download code that might never be used. The chunks are downloaded only when the dynamic import is executed.

However, if you know that certain dynamic imports are critical and will be needed soon, you can preload them:

{* Main entry point *}
{asset 'app.js'}

{* Preload critical dynamic imports *}
{preload 'components/chart.js'}

This tells the browser to download the chart component in the background, so it's ready immediately when needed.

TypeScript Support

TypeScript works out of the box:

// assets/main.ts
interface User {
	name: string
	email: string
}

export function greetUser(user: User): void {
	console.log(`Hello, ${user.name}!`)
}

Reference TypeScript files normally:

{asset 'main.ts'}

For full TypeScript support, install it:

npm install -D typescript

Additional Vite Configuration

Here are some useful Vite configuration options with detailed explanations:

export default defineConfig({
	// Root directory containing source assets
	root: 'assets',

	// Folder whose contents are copied to output directory as-is
	// Default: 'public' (relative to 'root')
	publicDir: 'public',

	build: {
		// Where to put compiled files (relative to 'root')
		outDir: '../www/assets',

		// Empty output directory before building?
		// Useful to remove old files from previous builds
		emptyOutDir: true,

		// Subdirectory within outDir for generated chunks and assets
		// This helps organize the output structure
		assetsDir: 'static',

		rollupOptions: {
			// Entry point(s) - can be a single file or array of files
			// Each entry point becomes a separate bundle
			input: [
				'app.js',      // main application
				'admin.js',    // admin panel
			],
		},
	},

	server: {
		// Host to bind the dev server to
		// Use '0.0.0.0' to expose to network
		host: 'localhost',

		// Port for the dev server
		port: 5173,

		// CORS configuration for cross-origin requests
		cors: {
			origin: 'http://myapp.local',
		},
	},

	css: {
		// Enable CSS source maps in development
		devSourcemap: true,
	},

	plugins: [
		nette(),
	],
});

That's it! You now have a modern build system integrated with Nette Assets.

version: 1.0