Skip to the content.
← 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:

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:

  1. manifest.js — Webpack runtime
  2. core.js — Vendor libraries (Bootstrap, Stimulus, Turbo)
  3. controllers.js — All Stimulus controllers (register on window.Controllers)
  4. 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 = {
            '&': '&amp;', '<': '&lt;', '>': '&gt;',
            '"': '&quot;', "'": '&#x27;', "/": '&#x2F;',
        };
        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

  1. Run cd app && npm install && npm run dev
  2. Check webroot/mix-manifest.json exists and has correct mappings
  3. Verify AssetMix helper is loaded in AppView
  4. Clear browser cache (Ctrl+F5)

New Controller Not Appearing

  1. Verify filename ends with -controller.js
  2. Verify file is in assets/js/controllers/ or plugins/*/assets/js/controllers/
  3. Verify it registers on window.Controllers
  4. Rebuild: npm run dev

Module Not Found Errors

  1. Run npm install in the app/ directory
  2. Verify import paths match actual file locations
  3. Check node_modules/ exists

See Also