← Back to JavaScript Development ← Back to Table of Contents

10.4 Asset management

KMP’s only frontend build pipeline is Vite. CakePHP resolves its versioned output through App\View\Helper\ViteHelper.

Source and output

app/
  assets/js/
    index.js
    controllers-entry.js
    controllers/
    services/
  assets/css/
  plugins/*/assets/
  vite.config.js
  package.json
  package-lock.json
  webroot/
    .vite/manifest.json
    js/*-[hash].js
    css/*-[hash].css
    fonts/
    assets/

vite.config.js sets publicDir: false, writes to webroot, emits source maps and a manifest, and keeps emptyOutDir: false. A build therefore does not delete older hashed files; webroot/.vite/manifest.json is the authority for the current build.

Current entries

JavaScript:

Logical name Source
index assets/js/index.js
controllers assets/js/controllers-entry.js

CSS:

Logical name Source
app assets/css/app.css
signin assets/css/signin.css
cover assets/css/cover.css
dashboard assets/css/dashboard.css
waivers plugins/Waivers/assets/css/waivers.css
waiver-upload plugins/Waivers/assets/css/waiver-upload.css
workflow-designer assets/css/workflow-designer.css
error assets/css/error.css
gatherings_public assets/css/gatherings_public.css
drawflow node_modules/drawflow/dist/drawflow.min.css

The build groups Bootstrap, Popper, Turbo, and Stimulus into a shared core chunk when Rollup can do so. Do not depend on a particular chunk filename or count.

After each build, a Vite plugin copies Font Awesome webfonts to webroot/fonts and the PDF.js worker to webroot/js/pdf.worker.min.mjs. Those public paths are application contracts separate from manifest-resolved entries.

CakePHP integration

AppView loads the Vite helper. The default layout requests logical names:

<?= $this->Vite->css('app') ?>
<?= $this->Vite->script('controllers') ?>
<?= $this->Vite->script('index') ?>

The helper reads webroot/.vite/manifest.json and emits module scripts and stylesheets for the hashed files. It also exposes:

Method Purpose
css($name, $options = []) Render a manifest-backed stylesheet link
script($name, $options = []) Render a manifest-backed module script
getStyleUrl($name) Return a versioned stylesheet URL, for example for service-worker lists
getScriptUrl($name) Return a versioned script URL
ViteHelper::reset() Clear the helper’s cached manifest in tests

A missing/invalid manifest or unknown logical asset throws RuntimeException. Do not catch that in a template; build the assets.

Commands

Run from app/:

npm ci
npm run dev
npm run watch
npm run build
Script Actual behavior
npm run dev vite build --mode development; one development build
npm run watch vite build --watch
npm run build Production Vite build
npm run prod, npm run production Aliases of the production build

There is no development server in these scripts. The Docker application serves files from webroot.

Use npm ci for a clean lockfile-based install. Change dependencies with the normal npm workflow and commit both package.json and package-lock.json.

Add frontend code

Stimulus controller

Place a core controller at app/assets/js/controllers/{name}-controller.js, or a plugin controller at app/plugins/{Plugin}/assets/js/controllers/{name}-controller.js. The controllers entry discovers it automatically; no new build input is needed. The module must still register its identifier in window.Controllers.

Shared service

A module named *-service.js under app/assets/js/services is eagerly imported by controllers-entry.js. Prefer an ordinary imported module when eager side effects are unnecessary.

Stylesheet

Import feature styles into an existing CSS entry when they share its layout and lifecycle. Add a top-level Vite input only when a layout must load the stylesheet independently. Update the relevant layout to request its logical name with $this->Vite->css().

Other static assets

Import assets through an owning JavaScript/CSS module when possible. If code requires a stable public path, make that copy/output rule explicit in vite.config.js and document the contract. Do not assume automatic public-file copying; it is disabled.

CSS dependencies

assets/css/app.css imports Bootstrap, KMP dashboard/platform styles, EasyMDE, Font Awesome, Bootstrap Icon sizing, and a versioned Bootstrap Icons font stylesheet. Bootstrap’s JavaScript is imported once by assets/js/index.js.

Avoid adding duplicate CDN or template-level imports to authenticated layouts. The specialized public-event layout currently has its own Bootstrap Icons stylesheet include.

Plugins

Plugin JavaScript controllers are discovered from lowercase assets and the legacy uppercase Assets directory. New plugins should use lowercase assets. Plugin CSS is not discovered automatically; either import it from an existing entry or add an explicit Vite input and load it from the relevant layout/template.

Keep plugin source and generated behavior isolated. Do not hard-code plugin assets into core templates when plugin bootstrap, a registry, or a dedicated entry can own the integration.

Multi-tenant boundary

The compiled bundle is platform-wide and identical for every tenant. Tenant settings, branding, routes, and authorization are runtime server data. Never embed an environment secret, tenant database value, hostname allowlist, or tenant-specific generated file in a Vite bundle.

Use same-origin, CakePHP-generated URLs so the current request host continues to select the tenant. Service-worker or offline asset lists should use getScriptUrl() and getStyleUrl() for the current manifest.

Verification

For JavaScript/CSS/import changes:

cd app
npm run test:js
npm run dev

For vite.config.js, entry, output, or dependency changes, also run npm run build. Inspect webroot/.vite/manifest.json to confirm the expected source key and output; do not commit generated output unless the repository explicitly tracks that artifact.

Troubleshooting

“Vite manifest not found”

Run npm ci if needed, then npm run dev or npm run build. Confirm the application and build use the same app/webroot.

“Vite CSS/JS asset not found”

Check the logical name against vite.config.js and the manifest source keys. A source filename and a logical input key are not always identical (gatherings_public is a current example).

Controller is absent after a build

Confirm its filename ends with -controller.js, lives under a discovered core/plugin path, and registers itself. Then inspect the controllers entry in the manifest and browser console.

Old code still loads

The output directory is not emptied. Confirm the page references filenames from the current manifest, clear browser/service-worker caches when appropriate, and do not diagnose by choosing the newest-looking hash in webroot.