Setting up Laravel Nova with Vite and Typescript
It comes a time when you might need to build your own Laravel Nova components, whether it is a custom field, a tool or a card. And if you are conveniently satisfied with Tailwindcss as I am, you’ll find that using it with Nova isn’t that straightforward.
Nova ships with compiled Tailwindcss styles built with a custom configuration, so if a directive was not used anywhere in Nova’s source components, for instance backdrop-blur-sm
, you won't be able to use as it is not already available in the compiled styles.
Another fun thing you might explore, is hooking up Vite instead of Mix. As of today, Nova ships natively with Laravel Mix, and all the stubs it provides have Mix built in as a bundler tool. Vite has been growing lately, and was integrated natively into Laravel in a recent version.
Here we go !
Replacing Laravel Mix with Vite
To get started, our first step is to replace Laravel Mix with Vite.
💡 If you want a quickstart template, you may want to check out https://github.com/oneduo/laravel-nova-vite-typescript-skeleton
You can start by removing the following files:
1rm webpack.mix.js nova.mix.js
We won’t be removing NPM dependencies for the time being, you may do that if you want.
After that, you want to install Vite and its dependencies:
1npm i -D vite \2@vitejs/plugin-vue \3@inertiajs/inertia \4@inertiajs/inertia-vue3 \5typescript \6ts-loader \7vue \8vuex
This command will install Vite, required dependencies for Inertia, Typescript and Vue to get us started.
Let’s now create a new Vite config:
1touch vite.config.ts
And we can provide this sample configuration:
1import vue from "@vitejs/plugin-vue" 2import { resolve } from "path" 3import { defineConfig } from "vite" 4 5export default defineConfig({ 6 plugins: [vue()], 7 8 root: "resources", 9 10 build: {11 outDir: resolve(__dirname, "dist"),12 emptyOutDir: true,13 manifest: true,14 target: "es2018",15 minify: true,16 lib: {17 entry: resolve(__dirname, "resources/js/tool.ts"),18 name: "tool",19 formats: ["umd"],20 fileName: "js/tool",21 },22 rollupOptions: {23 input: resolve(__dirname, "resources/js/tool.ts"),24 external: ["vue", "Nova", "LaravelNova"],25 output: {26 globals: {27 vue: "Vue",28 nova: "Nova",29 "laravel-nova": "LaravelNova",30 },31 32 assetFileNames: "css/tool.css",33 },34 },35 },36 37 optimizeDeps: {38 include: ["vue", "@inertiajs/inertia", "@inertiajs/inertia-vue3", "axios"],39 },40})
Let’s take a look into this configuration.
1// This provides Vite with the required Vue plugin2plugins: [vue()],
1// Since we are in a custom Nova component, we need to set the root 2// Typically you'd have a directory structure similar to this : 3/* 4 app/ 5 config/ 6 database/ 7 nova-components/ 8 ├─ MyTool/ 9 │ ├─ resources/10 │ │ ├─ js/11 │ │ │ ├─ components/12 │ │ │ ├─ tool.ts13 │ │ ├─ css/14 │ │ │ ├─ tool.css15 │ ├─ vite.config.ts16 routes/17*/18root: "resources",
1build: { 2 // This sets the output dir to nova-components/MyTool/dist 3 outDir: resolve(__dirname, "dist"), 4 5 // Clears the outDir on every build 6 emptyOutDir: true, 7 8 // Generates a manifest.json we can use later on 9 manifest: true,10 11 // Build target12 target: "es2018",13 14 // Minify build15 minify: true,16 17 // Sets Vite to library mode 18 // https://vitejs.dev/guide/build.html#library-mode19 lib: {20 entry: resolve(__dirname, "resources/js/tool.ts"),21 name: "tool",22 formats: ["umd"], // We only need UMD format23 fileName: "js/tool", // Built file is going to be name js/tool.umd.cjs24 },25 rollupOptions: {26 input: resolve(__dirname, "resources/js/tool.ts"),27 28 // Define external dependencies and globally available objects29 external: ["vue", "Nova", "LaravelNova"],30 output: {31 globals: {32 vue: "Vue",33 nova: "Nova",34 "laravel-nova": "LaravelNova",35 },36 37 // Set the file name for compiled CSS38 assetFileNames: "css/tool.css",39 },40 },41},
And finally, you need to add a tsconfig.json
file:
1{ 2 "compilerOptions": { 3 "module": "commonjs", 4 "target": "es5", 5 "noImplicitAny": true, 6 "sourceMap": true, 7 "declaration": true, 8 "outDir": "dist" 9 },10 "include": [11 "resources/js"12 ]13}
Now, we are ready to add our Vite script to the package.json
file:
1{ 2 "name": "tool-template", 3 "private": true, 4 "type": "module", 5 "files": [ 6 "dist" 7 ], 8 "main": "./dist/js/tool.umd.cjs", 9 "scripts": {10 "dev": "vite build --watch",11 "build": "vite build",12 "prod": "npm run build",13 "production": "npm run build",14 "nova:install": "npm --prefix='../../vendor/laravel/nova' ci"15 }16}
Installing Tailwindcss
To get started with Tailwindcss, you need to run the following command
1npm i -D tailwindcss autoprefixer postcss
Add the follwoing in a postcss.config.cjs
file:
1module.exports = {2 plugins: [require("postcss-import"), require("tailwindcss")],3}
And then, add a tailwind.config.cjs
file:
1/** @type {import("tailwindcss").Config} */ 2module.exports = { 3 darkMode: "class", 4 content: ["./resources/**/*{js,ts,vue,blade.php}"], 5 theme: { 6 extend: {}, 7 }, 8 plugins: [], 9 important: ".tool-template", 10}
Since Nova ships with a built Tailwindcss stylesheet, we need to build our own, without breaking the existing or introducing conflicts.
Which explains the need of having an important: ".tool-template"
defined.
We can then add it to our top level Vue component. Typically, this can be the resources/js/pages/Tool.vue
1<script setup lang="ts"> 2</script> 3 4<template> 5 <div class="tool-template"> 6 <Head title="Tool Template" /> 7 8 <Heading class="mb-6">Tool Template</Heading> 9 10 <Card11 class="flex flex-col items-center justify-center"12 style="min-height: 300px"13 >14 <h1 class="dark:text-white text-4xl font-light mb-6">15 Nova x Typescript x Vite16 </h1>17 18 <p class="dark:text-white text-lg opacity-70">19 You can edit this tool's component at:20 <code21 class="ml-1 border border-gray-100 dark:border-gray-900 text-sm font-mono text-white bg-black rounded px-2 py-1"22 >23 /nova-components/ToolTemplate/resources/js/pages/Tool.vue24 </code>25 </p>26 </Card>27 </div>28</template>29 30 31<style>32/* Scoped Styles */33</style>
Setting up the service provider
Since we’re using Vite, let’s take full advantage of it and use the generated manifest file.
To get started, add the following dependency to your package:
1composer require innocenzi/laravel-vite
Once we got it installed, we need to head over to the service provider of our component, for instance your tool service provider, and change the boot method to the following:
1<?php 2 3declare(strict_types=1); 4 5namespace Oneduo\ToolTemplate; 6 7use Illuminate\Http\Request; 8use Innocenzi\Vite\Exceptions\NoSuchEntrypointException; 9use Innocenzi\Vite\Manifest;10use Laravel\Nova\Menu\MenuSection;11use Laravel\Nova\Nova;12use Laravel\Nova\Tool;13 14class ToolTemplate extends Tool15{16 /**17 * Perform any tasks that need to happen when the tool is booted.18 *19 * @throws NoSuchEntrypointException20 */21 public function boot(): void22 {23 $manifest = Manifest::read(__DIR__ . '/../dist/manifest.json');24 25 $script = $manifest->getEntry('js/tool.ts')->file;26 $style = $manifest->getChunk('style.css')->file;27 28 Nova::script('tool-template', __DIR__ . '/../dist/' . $script);29 Nova::style('tool-template', __DIR__ . '/../dist/' . $style);30 }