Part of Real-World CoreUI Migration: From Laravel 8 to Laravel 11
Why Migrate?
Laravel 11 dropped official Laravel Mix support. While Mix still works, the framework's asset tooling, documentation, and @vite() Blade directive assume Vite. The migration brings:
- Faster dev builds: Vite uses native ES modules — no bundling during development
- Hot Module Replacement: Instant CSS/JS updates without full page reload
- Tree-shaking: ships code actually used
- Simpler configuration: No Webpack knowledge required
Package.json Transformation
Laravel 8 (Mix):
{
"private": true,
"scripts": {
"dev": "npm run development",
"development": "mix",
"watch": "mix watch",
"watch-poll": "mix watch -- --watch-options-poll=1000",
"hot": "mix watch --hot",
"prod": "npm run production",
"production": "mix --production"
},
"devDependencies": {
"axios": "^0.21",
"laravel-mix": "^6.0",
"lodash": "^4.17.19",
"sass": "^1.32",
"sass-loader": "^11.0"
},
"dependencies": {
"@coreui/coreui": "^3.4.0",
"bs-custom-file-input": "^1.3.4"
}
}
Laravel 11 (Vite):
{
"private": true,
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build"
},
"devDependencies": {
"axios": "^1.7.4",
"laravel-vite-plugin": "^1.0",
"sass-embedded": "^1.93.2",
"vite": "^5.0",
"vite-plugin-static-copy": "^3.2.0"
},
"dependencies": {
"@coreui/coreui": "^5.1.2",
"bs-custom-file-input": "^1.3.4"
}
}
Key changes:
"type": "module"— required for Vite's ESM-based configurationlaravel-mix→laravel-vite-plugin+vitesass+sass-loader→sass-embedded(Vite handles compilation natively, no loader needed)vite-plugin-static-copy— added for copying TinyMCE assets that can't be bundled- Six build scripts → two:
npm run devandnpm run build - Removed:
lodash(no longer a default dependency in Laravel 11)
Configuration File: webpack.mix.js → vite.config.js
Laravel 8 — webpack.mix.js:
const mix = require('laravel-mix');
mix.js('resources/js/app.js', 'public/js')
.sass('resources/sass/app.scss', 'public/css')
.options({
processCssUrls: false
});
Laravel 11 — vite.config.js:
import { defineConfig } from 'vite';
import laravel from 'laravel-vite-plugin';
import { viteStaticCopy } from 'vite-plugin-static-copy';
import path from 'path';
export default defineConfig({
plugins: [
laravel({
input: [
'resources/js/admin.js',
'resources/sass/frontend.scss',
'resources/sass/extend.scss',
'resources/js/tinymce-config.js'
],
refresh: true, // Auto-reload on Blade/route changes
}),
// TinyMCE loads plugins dynamically — can't be bundled by Vite.
// Copy the entire tinymce directory to the build output.
viteStaticCopy({
targets: [
{
src: 'resources/tinymce/**/*',
dest: 'tinymce'
}
]
})
],
css: {
preprocessorOptions: {
scss: {
api: 'modern-compiler', // Use modern sass-embedded API
additionalData: ``,
silenceDeprecations: [ // Suppress known CoreUI SCSS warnings
'legacy-js-api',
'import',
'global-builtin',
'color-functions'
],
loadPaths: ['node_modules', 'node_modules/@coreui']
}
}
},
resolve: {
alias: {
// Preserve the `~` import prefix used in CoreUI admin SCSS
'~': path.resolve(__dirname, 'node_modules')
}
}
});
Critical Vite SCSS Configuration Details
The silenceDeprecations Problem
CoreUI 5's SCSS source still uses deprecated Sass features. Without silencing these, you get hundreds of warnings during compilation:
DEPRECATION WARNING: Using / for division is deprecated. DEPRECATION WARNING: Global built-in functions are deprecated. DEPRECATION WARNING: color.lighten() is deprecated. Use color.adjust() instead.
The silenceDeprecations array suppresses these without modifying CoreUI's source:
scss: {
silenceDeprecations: [
'legacy-js-api', // Old Sass JS API usage
'import', // @import vs @use
'global-builtin', // Global math.div(), etc.
'color-functions' // color.lighten(), etc.
]
}
The ~ Alias for Backward Compatibility
The admin SCSS files use ~@coreui/coreui/scss/... import syntax (common with Webpack/Mix). Vite doesn't understand ~ by default, so we add an alias:
resolve: {
alias: {
'~': path.resolve(__dirname, 'node_modules')
}
}
This allows the admin SCSS (which uses the full CoreUI import) to work unchanged:
// admin/_custom-coreui.scss — these ~-prefixed imports work via the Vite alias
@import "~@coreui/coreui/scss/functions";
@import "~@coreui/coreui/scss/variables";
@import "~@coreui/coreui/scss/mixins";
While the frontend SCSS (which was rewritten) uses explicit relative paths:
// default/_frontend-coreui.scss — explicit paths, no alias needed
@import "../../../node_modules/@coreui/coreui/scss/functions";
@import "../../../node_modules/@coreui/coreui/scss/variables";
Blade Template Updates: mix() → @vite()
Laravel 8 (Mix) — admin layout:
<head>
<!-- Mix generates versioned assets in public/js and public/css -->
<script src="{{ mix('js/app.js') }}" defer></script>
<link rel="stylesheet" href="{{ mix('css/app.css') }}">
</head>
Laravel 11 (Vite) — admin layout:
<head>
<!-- Vite generates assets in public/build/ with manifest.json -->
@vite(['resources/js/admin.js'])
<!-- Legacy pre-built CoreUI 3 JS bundle — NOT processed by Vite -->
<script src="{{ asset('js/app.js') }}" defer></script>
<!-- Static assets still use asset() -->
<link rel="stylesheet" href="{{ asset('css/admin.css') }}">
<link rel="stylesheet" href="{{ asset('css/fontawesome.css') }}">
@stack('styles')
</head>
<!-- ... -->
<body>
<!-- TinyMCE loaded from Vite's static copy output -->
<script src="{{ asset('build/tinymce/tinymce.min.js') }}"></script>
@vite(['resources/js/tinymce-config.js'])
@stack('scripts')
</body>
Laravel 11 (Vite) — frontend layout:
<head>
<!-- Legacy CoreUI 3 JS bundle for dropdown/navbar interactivity -->
<script src="{{ asset('js/app.js') }}" defer></script>
@stack('scripts')
<!-- Vite-compiled SCSS (CoreUI 5 selective imports + custom styles) -->
@vite(['resources/sass/frontend.scss', 'resources/sass/extend.scss'])
<link rel="stylesheet" href="{{ asset('css/fontawesome.css') }}">
@stack('styles')
</head>
Key differences explained:
mix('js/app.js')→@vite(['resources/js/admin.js'])— Vite references source files, not output pathsmix('css/app.css')→@vite(['resources/sass/frontend.scss'])— SCSS files referenced directly; Vite compiles them- Multiple
@vite()calls are valid — each generates both<script>and<link>tags as needed - The
@vite()directive automatically handles dev server URLs vs production manifest
The Dual-Asset Strategy
This project runs a hybrid approach — not everything goes through Vite:
public/
├── build/ ← Vite output (npm run build)
│ ├── assets/
│ │ ├── admin-{hash}.js
│ │ ├── frontend-{hash}.css
│ │ ├── extend-{hash}.css
│ │ └── tinymce-config-{hash}.js
│ ├── tinymce/ ← Static copy (not bundled)
│ └── manifest.json
├── css/
│ ├── admin.css ← Pre-built CoreUI 3 admin stylesheet
│ ├── fontawesome.css ← Static (not processed by Vite)
│ └── prism.css ← Static syntax highlighting
├── js/
│ ├── app.js ← Pre-built CoreUI 3 JS bundle (Bootstrap 4 JS)
│ └── prism.js ← Static syntax highlighting
Why not put everything through Vite?
The admin panel's public/js/app.js is a pre-built CoreUI 3 JavaScript bundle containing Bootstrap 4 JS components (dropdowns, modals, tooltips) and Perfect Scrollbar. Migrating this to Vite would require rewriting all admin Blade templates to use Bootstrap 5 data-bs-* attributes and CoreUI 5 JavaScript initialization. The cost/benefit didn't justify it — the admin panel works, the legacy bundle is ~200KB gzipped, and it can be migrated to CoreUI 5 JavaScript independently later.
SCSS Entry Point Architecture
The project uses separate SCSS entry points for frontend and admin, compiled through Vite:
resources/sass/
├── frontend.scss ← Vite entry: @import 'default/app'
├── extend.scss ← Vite entry: custom utility classes
├── app-bag.scss ← Legacy (pre-Vite era, not used)
├── fontawesome.scss ← Compiled separately, not via Vite
│
├── default/ ← Frontend theme
│ ├── app.scss ← Main: imports variables → coreui → components
│ ├── _variables.scss ← Theme colors ($primary, $bg-main, etc.)
│ ├── _custom-coreui.scss ← Toggle: full CoreUI vs selective imports
│ ├── _frontend-coreui.scss ← Selective CoreUI 5 imports (7-step order)
│ ├── _components.scss ← Custom component barrel file
│ └── components/
│ ├── _card.scss
│ ├── _article.scss
│ ├── _layout.scss ← Bootstrap 4→5 compatibility layer
│ ├── _navigation-overrides.scss ← CoreUI CSS variable overrides
│ ├── _other.scss
│ └── media.scss
│
└── admin/
└── _custom-coreui.scss ← Full CoreUI import (uses ~ alias)
The key design decision: Frontend pages load the CoreUI 5 modules they actually use (navbar, dropdown, badge, buttons, etc.) for minimal CSS. The admin panel loads the full CoreUI set because it uses every component.
Development Workflow Comparison
Laravel 8 (Mix):
npm run watch-poll # Recompiles on file changes (polling)
# ↑ Full Webpack rebuild on every change: ~3-8 seconds
# Output: public/js/app.js, public/css/app.css
Laravel 11 (Vite):
npm run dev # Starts Vite dev server on localhost:5173
# ↑ No bundling during development — serves ES modules directly
# CSS changes: instant (<50ms)
# JS changes: HMR, no page reload needed
npm run build # Production build with minification + hashing
# Output: public/build/assets/* with manifest.json
Production Build & Deployment
# Build production assets
npm run build
# Laravel reads public/build/manifest.json to resolve @vite() paths:
# {
# "resources/js/admin.js": {
# "file": "assets/admin-BkH4x7q2.js",
# "src": "resources/js/admin.js"
# },
# "resources/sass/frontend.scss": {
# "file": "assets/frontend-D3nK9p1m.css",
# "src": "resources/sass/frontend.scss"
# }
# }
The @vite() directive automatically resolves source paths to hashed production filenames via the manifest. No cache-busting configuration needed — it's built into Vite's output.