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',
		}),
	],
});

Under the hood, besides root and outDir the plugin sets a few more Vite options so everything fits together: base to '' (assets are served straight from the document root), build.manifest to true (so Nette Assets can map the hashed filenames), and build.assetsDir to '' (compiled files land directly in outDir, without a static/ subfolder). You can override any of them.

The default outDir (www/assets) requires the www/ directory to already exist. If it doesn't, the plugin stops with the error “The output directory … does not exist”.

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 in production you can only load files that Vite makes available – either through its manifest, or by copying them unchanged from the public folder:

  1. Entry points defined in entry (including modules they dynamically import) and assets referenced from JavaScript or CSS (images, fonts, …) – all of these are recorded in the manifest
  2. Files from the assets/public/ directory – these are not in the manifest; they're copied as-is and {asset} locates them through a filesystem fallback

You cannot load using {asset} arbitrary files from assets/ – if a 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.

While the dev server is running, the plugin writes a small signal file www/assets/.vite/nette.json containing its URL. Nette Assets on the PHP side reads this file and switches to loading from the dev server when both:

  1. the Vite dev server is running (the signal file exists), and
  2. your Nette application is in debug mode.

The result:

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

No configuration needed – it just works! To disable the detection or set the dev server URL manually, see the devServer option.

The signal file lives at www/assets/.vite/nette.json (right next to the production manifest.json). You can rename it with the plugin's infoFile option, which defaults to .vite/nette.json. If Nette doesn't pick up the running dev server, check that this file exists and points to the right URL.

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://.

Docker Development

When you run Vite inside a Docker container, two things need attention: the browser on your machine must be able to reach the dev server, and Vite must detect file changes across the container boundary.

First, publish the Vite port from the container and bind the dev server to all interfaces, so it is reachable from outside the container:

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

	plugins: [
		nette(),
	],
	server: {
		host: '0.0.0.0',      // listen on all interfaces (required in a container)
		port: 5173,           // must match the published port
		strictPort: true,     // fail rather than pick another port
		watch: {
			usePolling: true, // enable if file changes aren't detected on mounted volumes
		},
	},
});

The plugin writes the dev server URL into nette.json for the PHP side. Because host: '0.0.0.0' is not usable by a browser (it redirects to localhost for every asset), the plugin automatically rewrites it to localhost in that URL, so assets load correctly.

If you open the application on a custom domain instead of localhost, set the plugin's host option to that domain:

	plugins: [
		nette({ host: 'myapp.local' }),  // the same domain as your PHP app
	],

The plugin then uses this host for the dev server URL, adds it to the CORS origins, and whitelists it in Vite's allowedHosts – so it works without any manual CORS setup.

For more complex setups – for example when Vite runs behind a reverse proxy where the public host, port, and protocol all differ from the internal address – set Vite's server.origin to the full public URL. The plugin respects it and writes it into nette.json as-is, instead of deriving the URL from the local socket:

	server: {
		origin: 'https://myapp.local:8443',  // the public URL where the browser reaches Vite
	},

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. The extension option applies to references without an extension (e.g. {asset 'images/og-image'} finds og-image.webp first):

assets:
	mapping:
		default:
			type: vite
			path: assets
			extension: [webp, jpg, png]  # for extension-less references
			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 (as with any file, main.ts must be an entry point):

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

Watch out for the entry paths in rollupOptions.input above: Rollup resolves relative paths against the current working directory (your project root), not against root: 'assets'. So a bare 'app.js' won't exist at build time. Either use the plugin's entry option (which resolves entry paths relative to root), or write the paths relative to the project root, e.g. 'assets/app.js'.

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

version: 1.x