| ← 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.