Skip to main content
Version: CANARY 🚧

Front-End Assets

Introduction​

Bow comes preconfigured with a modern Frontend environment based on Vite, including:

This setup provides instant reloading (HMR) and ultra-fast compilation.

Installation​

npm install

Commands​

CommandDescription
npm run devStarts the Vite development server with HMR
npm run buildCompiles and optimizes for production

Development​

npm run dev

Starts the Vite server with hot reloading. Changes are reflected instantly without reloading the page.

Production​

npm run build

Compiles and minifies the assets into the public/ folder.

File Structure​

assets/
β”œβ”€β”€ js/
β”‚ β”œβ”€β”€ app.js # Main entry point
β”‚ β”œβ”€β”€ Example.jsx # React component
β”‚ └── Example.vue # Vue component
β”œβ”€β”€ sass/
β”‚ β”œβ”€β”€ app.scss # Main styles
β”‚ └── variables.scss # SCSS variables
└── css/
└── app.css # CSS/Tailwind

Vite Configuration​

vite.config.js
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
import react from '@vitejs/plugin-react';
import path from 'path';
import tailwindcss from '@tailwindcss/vite';

export default defineConfig({
plugins: [
vue(),
react(),
tailwindcss(),
],
root: path.resolve(__dirname, 'assets'),
build: {
outDir: path.resolve(__dirname, 'public'),
emptyOutDir: true,
rollupOptions: {
input: {
app: path.resolve(__dirname, 'assets/js/app.js'),
},
output: {
entryFileNames: 'js/[name].js',
assetFileNames: (assetInfo) => {
if (/\.(css|scss)$/.test(assetInfo.name)) {
return 'css/[name]-[hash][extname]';
}
if (/\.(png|jpe?g|gif|svg|webp)$/.test(assetInfo.name)) {
return 'img/[name]-[hash][extname]';
}
return '[ext]/[name]-[hash][extname]';
},
},
},
},
});

Tailwind CSS​

Configuration​

tailwind.config.js
import defaultTheme from "tailwindcss/defaultTheme";

export default {
content: [
"./templates/**/*.{blade,tintin}.php",
"./templates/**/*.twig",
"./templates/**/*.{js,jsx,ts,tsx,vue}",
"./assets/js/**/*.{js,jsx,ts,tsx,vue}",
],
theme: {
extend: {
fontFamily: {
sans: ["Figtree", ...defaultTheme.fontFamily.sans],
},
},
},
plugins: [],
};

Usage​

assets/css/app.css
@tailwind base;
@tailwind components;
@tailwind utilities;

/* Custom styles */
.btn-primary {
@apply bg-blue-500 text-white px-4 py-2 rounded hover:bg-blue-600;
}

React Components​

assets/js/Example.jsx
import React, { useState } from 'react';

export default function Example() {
const [count, setCount] = useState(0);

return (
<div className="p-4">
<h1 className="text-2xl font-bold">Compteur: {count}</h1>
<button
className="btn-primary mt-4"
onClick={() => setCount(count + 1)}
>
IncrΓ©menter
</button>
</div>
);
}

Vue Components​

assets/js/Example.vue
<script setup>
import { ref } from 'vue';

const count = ref(0);
</script>

<template>
<div class="p-4">
<h1 class="text-2xl font-bold">Compteur: {{ count }}</h1>
<button class="btn-primary mt-4" @click="count++">
IncrΓ©menter
</button>
</div>
</template>

Template Integration​

With Tintin​

templates/app.tintin.php
<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" href="/css/app.css">
</head>
<body>
<div id="app"></div>
<script type="module" src="/js/app.js"></script>
</body>
</html>

The asset helper​

The asset() helper simply builds a URL to a static file in the public/ folder. It is not aware of the cache hash added by Vite in production β€” reserve it for files whose names do not change (favicon, static images, etc.).

<link rel="stylesheet" href="{{ asset('css/app.css') }}">
<script src="{{ asset('js/app.js') }}"></script>

The vite helper (compiled assets)​

In production, npm run build adds a hash to each file name (app-CBsLkjX9.js) for cache-busting, and records the mapping from logical name to actual file in a manifest. The vite() helper reads this manifest and returns the URL of the corresponding compiled file:

templates/layouts/default.tintin.php
<link rel="stylesheet" href="{{ vite('css/app.css') }}">
<script type="module" src="{{ vite('js/app.js') }}"></script>

Signature:

vite(string $file, bool $absolute = false): string
  • $file: the entry key in the manifest (e.g. js/app.js). Each referenced file must be declared as an entry (input) in vite.config.js in order to appear in the manifest.
  • $absolute: if true, the URL is prefixed with the application URL (via url()) instead of being relative.
  • Throws an Exception if the entry does not exist in the manifest.

The helper looks for the manifest, in order, at the following locations:

public/build/.vite/manifest.json
public/build/manifest.json
public/.vite/manifest.json
manifest.json (project root)
Served path

vite() returns URLs prefixed with /build/ (e.g. /build/js/app-CBsLkjX9.js). The compiled files must therefore be served under /build/: make sure the build output (outDir in vite.config.js) and the manifest location are consistent with this prefix.

Development vs production

During npm run dev, assets are served by the Vite server with HMR. The vite() helper is only useful after a npm run build, when the manifest exists. Remember to run the build before testing in production mode.

Environment Variables​

Vite exposes variables prefixed with VITE_:

.env.json
{
"VITE_APP_NAME": "Mon App",
"VITE_API_URL": "https://api.example.com"
}
// Access in the code
const appName = import.meta.env.VITE_APP_NAME;
Documentation

For more information, see the Vite documentation.

Is something missing?

If you run into problems with the documentation or have suggestions to improve the documentation or the project in general, please open an issue for us, or send a tweet mentioning the Twitter account @bowframework or directly on github.