| ← Back to JavaScript Development | ← Back to Table of Contents |
10.4 Asset Management
This document explains how KMP manages, compiles, and serves static assets (CSS, JavaScript, images, fonts).
Overview
KMP uses Laravel Mix (webpack-based) for asset compilation and versioning. All source files live in assets/ and compile to webroot/. Key features:
- Dynamic controller discovery: Automatically finds all
*-controller.jsfiles inassets/js/andplugins/ - Service file bundling: Collects
*-service.jsfiles fromassets/js/services/ - Vendor extraction: Core libraries (Bootstrap, Stimulus, Turbo) extracted to
core.js - Asset versioning: Cache busting via
mix-manifest.json - Source maps: Generated for development debugging
- Plain CSS: No SCSS/Sass preprocessing — uses plain CSS files
Directory Structure
Source Assets
assets/
├── css/
│ ├── app.css # Main application styles
│ ├── signin.css # Sign-in page styles
│ ├── cover.css # Cover/landing page styles
│ ├── dashboard.css # Dashboard styles
│ ├── bootstrap-icons.css # Bootstrap Icons font CSS
│ └── bootstrap.css # Bootstrap CSS
├── js/
│ ├── index.js # Application entry point
│ ├── KMP_utils.js # Shared utility functions
│ ├── timezone-utils.js # Client-side timezone utilities
│ ├── controllers/ # Stimulus controllers (~60 files)
│ │ └── *-controller.js
│ └── services/ # Service modules
│ ├── offline-queue-service.js
│ └── rsvp-cache-service.js
Compiled Output
webroot/
├── css/
│ ├── app.css # Compiled main CSS
│ ├── signin.css # Compiled sign-in CSS
│ ├── cover.css # Compiled cover CSS
│ ├── dashboard.css # Compiled dashboard CSS
│ ├── waivers.css # Compiled Waivers plugin CSS
│ └── waiver-upload.css # Compiled waiver upload CSS
├── js/
│ ├── index.js # Main entry point bundle
│ ├── controllers.js # All controllers + services bundle
│ ├── core.js # Extracted vendor libraries
│ └── manifest.js # Webpack runtime manifest
├── fonts/ # FontAwesome webfonts (copied)
└── mix-manifest.json # Versioned asset mapping
webpack.mix.js Configuration
The full build configuration in app/webpack.mix.js:
const mix = require('laravel-mix');
const webpack = require('webpack');
const fs = require('fs');
const path = require('path');
// Dynamic file discovery — recursively finds files matching a suffix
function getJsFilesFromDir(startPath, skiplist, filter, callback) {
if (!fs.existsSync(startPath)) return;
const files = fs.readdirSync(startPath);
files.forEach(file => {
const filename = path.join(startPath, file);
if (skiplist.some((skip) => filename.includes(skip))) return;
const stat = fs.lstatSync(filename);
if (stat.isDirectory()) {
getJsFilesFromDir(filename, skipList, filter, callback);
} else if (filename.endsWith(filter)) {
callback(filename);
}
});
}
// Collect all controller files from app and plugins
const files = [];
const skipList = ['node_modules', 'webroot'];
getJsFilesFromDir('./assets/js', skipList, '-controller.js', (filename) => {
files.push(filename);
});
getJsFilesFromDir('./plugins', skipList, '-controller.js', (filename) => {
files.push(filename);
});
// Collect service files
const serviceFiles = [];
getJsFilesFromDir('./assets/js/services', skipList, '-service.js', (filename) => {
serviceFiles.push(filename);
});
const allJsFiles = [...files, ...serviceFiles];
mix.setPublicPath('./webroot')
.js(allJsFiles, 'webroot/js/controllers.js') // Bundle all controllers + services
.js('assets/js/index.js', 'webroot/js') // Main entry point → index.js
.extract([ // Vendor libs → core.js
'bootstrap', 'popper.js',
'@hotwired/turbo', '@hotwired/stimulus',
'@hotwired/stimulus-webpack-helpers'
], 'webroot/js/core.js')
.webpackConfig({
devtool: "source-map",
optimization: { runtimeChunk: true }, // → manifest.js
plugins: [
new webpack.ProvidePlugin({ 'bootstrap': 'bootstrap' }),
],
module: {
rules: [{
test: /\.(woff|woff2|eot|ttf|otf|svg)$/,
type: 'asset/resource',
generator: { filename: 'fonts/[name][ext]' }
}]
}
})
.css('assets/css/app.css', 'webroot/css')
.css('assets/css/signin.css', 'webroot/css')
.css('assets/css/cover.css', 'webroot/css')
.css('assets/css/dashboard.css', 'webroot/css')
.css('plugins/Waivers/assets/css/waivers.css', 'webroot/css/waivers.css')
.css('plugins/Waivers/assets/css/waiver-upload.css', 'webroot/css/waiver-upload.css')
.copyDirectory('node_modules/@fortawesome/fontawesome-free/webfonts', 'webroot/fonts')
.version()
.sourceMaps();
Build Output Summary
| Output File | Contents |
|---|---|
webroot/js/index.js |
Main entry point (Stimulus init, Turbo, Bootstrap tooltips) |
webroot/js/controllers.js |
All *-controller.js files from app + plugins, plus service files |
webroot/js/core.js |
Extracted vendor libraries (Bootstrap, Stimulus, Turbo, Popper) |
webroot/js/manifest.js |
Webpack runtime chunk |
webroot/css/app.css |
Main application styles |
webroot/css/signin.css |
Sign-in page styles |
webroot/css/cover.css |
Cover page styles |
webroot/css/dashboard.css |
Dashboard styles |
webroot/css/waivers.css |
Waivers plugin styles |
webroot/css/waiver-upload.css |
Waiver upload styles |
webroot/fonts/ |
FontAwesome webfonts |
NPM Scripts
Defined in app/package.json:
{
"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",
"docs:js": "jsdoc -c jsdoc.config.json",
"test": "npm run test:js && npm run test:ui",
"test:js": "jest",
"test:js:watch": "jest --watch",
"test:js:coverage": "jest --coverage",
"test:ui": "bddgen && bash ../reset_dev_database.sh && playwright test",
"test:security": "bash ../security-checker.sh"
}
}
| Command | Purpose |
|---|---|
npm run dev |
One-time development build (unminified, source maps) |
npm run watch |
Watch mode — auto-recompile on file changes |
npm run watch-poll |
Watch with polling (for Docker/VM environments) |
npm run hot |
Hot module replacement |
npm run prod |
Production build (minified, versioned) |
npm run docs:js |
Generate JSDoc documentation |
npm run test:js |
Run Jest unit tests |
npm run test:ui |
Run Playwright UI tests |
Asset Integration with CakePHP
AssetMix Helper
KMP uses the AssetMix helper (not the standard Html helper) to load versioned assets. This reads mix-manifest.json to map logical names to versioned filenames.
<!-- In layout templates -->
<?= $this->AssetMix->css('app') ?>
<?= $this->AssetMix->script('manifest') ?>
<?= $this->AssetMix->script('core') ?>
<?= $this->AssetMix->script('controllers') ?>
<?= $this->AssetMix->script('index') ?>
The AssetMix helper is loaded in AppView:
// src/View/AppView.php
$this->loadHelper('AssetMix.AssetMix');
Asset Loading Order
Scripts must be loaded in this order for correct initialization:
manifest.js— Webpack runtimecore.js— Vendor libraries (Bootstrap, Stimulus, Turbo)controllers.js— All Stimulus controllers (register onwindow.Controllers)index.js— Application entry point (starts Stimulus, registers controllers)
JavaScript Assets
KMP_utils
Shared utility functions available globally as window.KMP_utils:
// assets/js/KMP_utils.js
export default {
// Extract URL query parameter by name
urlParam(name) {
var results = new RegExp('[\?&]' + name + '=([^&#]*)').exec(window.location.href);
return results ? decodeURIComponent(results[1]) : null;
},
// Sanitize HTML string to prevent XSS (entity encoding)
sanitizeString(str) {
const map = {
'&': '&', '<': '<', '>': '>',
'"': '"', "'": ''', "/": '/',
};
const reg = /[&<>"'/]/ig;
return str.replace(reg, (match) => (map[match]));
},
// URL-encode special characters for safe URL construction
sanitizeUrl(str) {
const map = {
'<': '%3C', '>': '%3E', '"': '%22', "'": '%27', ' ': '%20',
};
const reg = /[<>"' ]/ig;
return str.replace(reg, (match) => (map[match]));
}
};
Controller Registration Pattern
All controllers use the window.Controllers global registry:
import { Controller } from "@hotwired/stimulus";
class MyFeatureController extends Controller {
static targets = ["input", "output"]
static values = { url: String }
connect() { /* ... */ }
disconnect() { /* ... */ }
}
if (!window.Controllers) {
window.Controllers = {};
}
window.Controllers["my-feature"] = MyFeatureController;
CSS Assets
KMP uses plain CSS — no SCSS/Sass preprocessing. Bootstrap is included as a pre-built CSS file.
Source CSS Files
| File | Purpose |
|---|---|
assets/css/app.css |
Main application styles |
assets/css/signin.css |
Sign-in page layout |
assets/css/cover.css |
Cover/landing page |
assets/css/dashboard.css |
Dashboard layout |
assets/css/bootstrap-icons.css |
Bootstrap Icons font |
assets/css/bootstrap.css |
Bootstrap framework |
Plugin CSS
Only the Waivers plugin CSS is compiled via webpack.mix.js. Other plugins must manually add their CSS to the Mix configuration:
// To add plugin CSS, add to webpack.mix.js:
.css('plugins/YourPlugin/assets/css/styles.css', 'webroot/css/your-plugin.css')
Fonts
FontAwesome webfonts are copied from node_modules to webroot/fonts/ during the build. Bootstrap Icons are loaded via CSS font-face from assets/css/.
Troubleshooting
Assets Not Loading
- Run
cd app && npm install && npm run dev - Check
webroot/mix-manifest.jsonexists and has correct mappings - Verify
AssetMixhelper is loaded in AppView - Clear browser cache (Ctrl+F5)
New Controller Not Appearing
- Verify filename ends with
-controller.js - Verify file is in
assets/js/controllers/orplugins/*/assets/js/controllers/ - Verify it registers on
window.Controllers - Rebuild:
npm run dev
Module Not Found Errors
- Run
npm installin theapp/directory - Verify import paths match actual file locations
- Check
node_modules/exists