This tutorial offer to use pnpm as Node.js package manager but you can use npm.

From vitejs.dev

Remove old css and js

rm -rf resources/css
rm -rf resources/js
.gitignore
public/build

Remove Node.js from package.json

pnpm remove axios lodash postcss

Install vite

Add vite with typescript.

pnpm add @types/node dotenv typescript vite -D

package.json scripts

Update package.json in scripts.

{
"scripts": {
"dev": "vite --config vite.config.ts --port 3100 --host",
"build": "vite build --config vite.config.ts"
}
}

Configuration files

touch tsconfig.json
touch vite.config.ts
{
"compilerOptions": {
"target": "esnext",
"module": "esnext",
"moduleResolution": "node",
"strict": true,
"jsx": "preserve",
"sourceMap": true,
"resolveJsonModule": true,
"esModuleInterop": true,
"skipLibCheck": true,
"noImplicitAny": false,
"baseUrl": ".",
"lib": ["esnext", "dom"],
"types": ["vite/client"],
"typeRoots": ["./node_modules/@types", "resources/**/*.d.ts"],
"plugins": [],
"paths": {
"@/*": ["./*"],
"~": ["./"],
"~/*": ["./*"],
"~/app": ["resources"],
"~/app/*": ["resources/*"]
}
},
"include": ["resources/**/*.ts", "resources/**/*.d.ts", "resources/**/*.vue"],
"exclude": ["node_modules"]
}
import { defineConfig } from 'vite'
import type { PluginOption } from 'vite'
import Dotenv from 'dotenv'
Dotenv.config()
/**
* Enable full reload for blade file
*/
const bladePlugin = (): PluginOption => ({
name: 'vite:laravel',
handleHotUpdate({ file, server }) {
if (file.endsWith('.blade.php')) {
server.ws.send({
type: 'full-reload',
path: '*',
})
}
},
})
// https://vitejs.dev/config/
export default defineConfig({
server: {
hmr: {
host: process.env.VITE_DEV_SERVER_HOST,
},
},
base: '',
root: 'resources',
publicDir: 'static',
build: {
outDir: '../public/build',
emptyOutDir: true,
manifest: true,
rollupOptions: {
input: '/app.ts',
},
},
cacheDir: '../node_modules/.vite',
// views config
resolve: {
alias: {
'~/app': './',
},
},
plugins: [bladePlugin()],
})

In .env

.env
VITE_DEV_SERVER_HOST=localhost

Laravel Vite

Create configuration files.

touch config/vite.php
mkdir -p app/Facades/ ; touch app/Facades/ViteManifest.php
mkdir -p app/Support/ ; touch app/Support/LaravelViteManifest.php

Add vite.php

<?php
return [
'dev_server' => env('VITE_DEV_SERVER', 'local' === env('APP_ENV')),
'dev_server_host' => env('VITE_DEV_SERVER_HOST', '127.0.0.1'),
];

Add ViteManifest.php

<?php
namespace App\Facades;
use Illuminate\Support\Facades\Facade;
class ViteManifest extends Facade
{
/**
* Get the registered name of the component.
*/
protected static function getFacadeAccessor(): string
{
return 'laravel-vite-manifest';
}
}

Add LaravelViteManifest.php

<?php
namespace App\Support;
use Illuminate\Support\Facades\Config;
use Illuminate\Support\Facades\File;
class LaravelViteManifest
{
private $manifestCache = [];
public function embed(?string $name = 'views', ?string $entry = 'app.ts', ?int $port = 3100): string
{
if (Config::get('vite.dev_server')) {
$host = Config::get('vite.dev_server_host');
return $this->jsImports(
"http://{$host}:{$port}/{$entry}"
);
}
if ($assets = $this->productionAssets($name, $entry)) {
return $this->jsImports($assets)
.$this->jsPreloadImports($name, $entry)
.$this->cssImports($name, $entry);
}
return '';
}
private function getManifest(string $name): array
{
if (! empty($this->manifestCache[$name])) {
return $this->manifestCache[$name];
}
$manifest = public_path("build/manifest.json");
if (File::exists($manifest)) {
$this->manifestCache[$name] = json_decode(File::get($manifest), true);
}
return $this->manifestCache[$name] ?? [];
}
private function jsImports(string $url): string
{
return "<script type=\"module\" crossorigin src=\"{$url}\"></script>";
}
private function jsPreloadImports(string $name, string $entry): string
{
$res = '';
foreach ($this->preloadUrls($name, $entry) as $url) {
$res .= "<link rel=\"modulepreload\" href=\"{$url}\">";
}
return $res;
}
private function preloadUrls(string $name, string $entry): array
{
$urls = [];
$manifest = $this->getManifest($name);
if (! empty($manifest[$entry]['imports'])) {
foreach ($manifest[$entry]['imports'] as $imports) {
$urls[] = asset("build/".$manifest[$imports]['file']);
}
}
return $urls;
}
private function cssImports(string $name, string $entry): string
{
$tags = '';
foreach ($this->cssUrls($name, $entry) as $url) {
$tags .= "<link rel=\"stylesheet\" href=\"{$url}\">";
}
return $tags;
}
private function cssUrls(string $name, string $entry): array
{
$urls = [];
$manifest = $this->getManifest($name);
if (! empty($manifest[$entry]['css'])) {
foreach ($manifest[$entry]['css'] as $file) {
$urls[] = asset("build/{$file}");
}
}
return $urls;
}
private function productionAssets(string $name, string $entry): string
{
$manifest = $this->getManifest($name);
if (! isset($manifest[$entry])) {
return '';
}
return asset("build/".$manifest[$entry]['file']);
}
}

Add AppServiceProvider.php

<?php
namespace App\Providers;
use App\Support\LaravelViteManifest;
use Illuminate\Support\Facades\Blade;
use Illuminate\Support\ServiceProvider;
class AppServiceProvider extends ServiceProvider
{
/**
* Register any application services.
*/
public function register()
{
$this->app->singleton('laravel-vite-manifest', function () {
return new LaravelViteManifest();
});
}
/**
* Bootstrap any application services.
*/
public function boot()
{
Blade::directive('vite', function ($expression) {
return '{!! App\Facades\ViteManifest::embed('.$expression.') !!}';
});
}
}

Blade

Create files

Remove current Blade view.

rm resources/views/welcome.blade.php

Create new Blade files.

mkdir -p public/assets
mkdir -p resources/views/components ; touch resources/views/components/app.blade.php
mkdir -p resources/views/pages ; touch resources/views/pages/index.blade.php
touch resources/app.css
touch resources/app.ts
touch resources/global.d.ts

Create new app component.

<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="csrf-token" content="{{ csrf_token() }}">
<title>
{{ config('app.name') }}
</title>
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png">
<link rel="manifest" href="/site.webmanifest">
@vite()
@stack('styles')
</head>
<body class="{{ config('app.env') === 'local' ? 'debug-screens' : '' }}">
{{ $slot }}
@stack('scripts')
</body>
</html>

Create a new page.

<x-app>
<div>Welcome</div>
</x-app>

TypeScript & configuration

Create an app.css

/* your style */

Create app.ts

import './app.css'

Create TypeScript global.d.ts interface.

/**
* From https://bobbyhadz.com/blog/typescript-make-types-global
*/
declare global {}
export {}

Routes

Create route to serve page.

php artisan make:controller MainController
<?php
namespace App\Http\Controllers;
class MainController extends Controller
{
public function index()
{
return view('pages.index');
}
}
<?php
use App\Http\Controllers\MainController;
use Illuminate\Support\Facades\Route;
- Route::get('/', function () {
- return view('welcome');
- });
+ Route::get('/', [MainController::class, 'index'])->name('welcome');

Serve app

In one hand, keep serve.

php artisan serve

In other hand, keep dev.

pnpm dev