vernok / vite-plugin
Vite asset resolver for October CMS themes and plugins (manifest + dev server).
Package info
github.com/hello-vernok/oc-vite-plugin
Type:october-plugin
pkg:composer/vernok/vite-plugin
Requires
- php: ^8.3
- october/rain: ^4.0
Requires (Dev)
- laravel/pint: ^1.29
- phpunit/phpunit: ^9.6
README
PHP asset resolver for October CMS themes and plugins. Built for agencies and developers who ship production October CMS projects and need predictable Vite integration on the server — dev-server URLs during local development, manifest-driven hashed assets in production.
This plugin reads .vite-dev.json and assets/.vite/manifest.json, then exposes Twig functions, global helpers, traits, and the vite: asset protocol for addJs / addCss.
Companion package (Node/Vite): @vernok/vite-plugin-october — install this in each theme or plugin repository for Vite config, entry autodiscovery, build output, and .vite-dev.json during local development. Vernok.Vite (this plugin) handles the PHP side only.
| Surface | Use when |
|---|---|
vite: + addJs / addCss |
Default for PHP — components, controllers, FormWidgets; no trait required |
vite_scripts() / vite_styles() |
Theme layouts and partials (Twig) |
HasViteAssets |
Optional addViteAssets() when your team prefers a named method |
vite_assets() |
Manual addJs / addCss loops (advanced, e.g. backend skin registration) |
Table of contents
- Overview
- Requirements
- Installation
- Configuration
- Quick start
- Registering assets with
addJsandaddCss - Twig and PHP helpers
- Scope resolution
- Development vs production
- Docker and multi-owner setups
- Testing
- Troubleshooting
- Known limitations
- Related packages
Overview
October CMS frontend assets are split across two packages:
| Package | Runs in | Responsibility |
|---|---|---|
@vernok/vite-plugin-october |
Plugin or theme repository (Node) | Vite config, entry autodiscovery, assets/ build output, .vite-dev.json during vite dev |
| Vernok.Vite (this plugin) | October CMS application (PHP) | Dev-server detection, manifest resolution, rendering assets in Twig and PHP |
Agencies benefit from shared conventions — the same folder layout, manifest location, and cache-busting behaviour in every client repository. Developers reference source entry paths (for example resources/modules/blog/entrypoint.ts) from Twig or PHP; the resolver maps those paths to dev-server URLs or hashed production files. You never hard-code manifest hashes in templates or PHP.
- Development: run
npm run devin the plugin or theme directory. The npm package writes.vite-dev.json. Vernok.Vite readsoriginand emits module scripts (including@vite/clientonce per origin per request). - Production: run
npm run build. Output lands underassets/withassets/.vite/manifest.json. Vernok.Vite resolves hashed filenames, shared chunks (imports), and CSS linked to each entry.
Node / Vite setup: this README covers PHP only. For
vite.config.ts, entry discovery, folder layout, DockerhostUrl, and build output paths, see the@vernok/vite-plugin-octoberdocumentation on npm.
Agency checklist
Before shipping a theme or plugin with Vite assets:
- October CMS app —
composer require vernok/vite-plugin:^0.0.5(Packagist) orphp artisan plugin:install Vernok.Vite - Your plugin/theme —
require: [Vernok.Vite]inplugin.yamlortheme.yaml - Asset repo —
npm i -D @vernok/vite-plugin-october viteand avite.config.tsusingdefinePluginConfig()ordefineThemeConfig() - Local dev —
npm run devfrom the theme/plugin directory (not the October root) - Deploy —
npm run buildsoassets/.vite/manifest.jsonexists; add.vite-dev.jsonto.gitignore
Requirements
| Dependency | Version |
|---|---|
| PHP | ^8.3 |
| October CMS | ^4.0 |
| Node.js | ≥ 24.11.1 (per theme/plugin repo; see npm package) |
| Vite | ^8.0 (per theme/plugin repo) |
@vernok/vite-plugin-october |
latest compatible release (per theme/plugin repo) |
Each theme or plugin with frontend assets needs its own package.json, vite.config.ts (or .js), and npm run dev / npm run build scripts. Run those commands from the theme or plugin directory — not from the October project root.
Installation
October CMS application
Via Composer (Packagist):
composer require vernok/vite-plugin:^0.0.5 php artisan october:up
Via October CLI or marketplace:
php artisan plugin:install Vernok.Vite
Declaring Vernok.Vite as a dependency
| You maintain | Declare in | Required? |
|---|---|---|
| October CMS plugin with Vite assets | plugin.yaml → require |
Yes |
| Same plugin via Composer | Plugin composer.json → require |
Yes — keep in sync with plugin.yaml |
| October CMS theme with Vite assets | theme.yaml → require |
Yes |
| Same theme via Composer | Theme composer.json → require |
Yes |
Plugin — plugin.yaml:
require: - Vernok.Vite
Plugin — composer.json (Composer-distributed plugins only):
{
"require": {
"vernok/vite-plugin": "^0.0.5"
}
}
Theme — theme.yaml:
require: - Vernok.Vite
If the plugin or theme lives only under plugins/ or themes/ in a project repo, plugin.yaml or theme.yaml is sufficient. Add the Composer entry when you publish through Packagist or a private registry.
Plugin or theme repository (Node)
npm i -D @vernok/vite-plugin-october vite
Use definePluginConfig() or defineThemeConfig() in vite.config.ts. Full setup, entry discovery rules, and output paths: @vernok/vite-plugin-october on npm.
Entry paths (what to pass to Twig / vite:)
Pass the source path as discovered by the npm package — the same string appears as a key in assets/.vite/manifest.json:
| Context | Typical entry path (examples) |
|---|---|
| Theme site bundle | resources/entrypoint.ts |
| Theme module | resources/modules/<name>/entrypoint.ts |
| Plugin module | resources/modules/<name>/entrypoint.ts |
| Plugin FormWidget | resources/formwidgets/<name>/entrypoint.ts |
$this->addJs('vite:resources/formwidgets/color-picker/entrypoint.ts');
{{ vite_scripts(['resources/modules/blog/entrypoint.ts'], 'plugin', 'Acme.Blog') }}
If a path 404s or the manifest reports "entry not found", open assets/.vite/manifest.json and copy the key exactly. Details on which folders are autodiscovered: npm package docs.
Configuration
Config file: plugins/vernok/vite/config/vite.php
October key prefix: vernok.vite::vite
| Variable | Default | Description |
|---|---|---|
VITE_DEV_SERVER_ENABLED |
null (auto) |
false forces production manifest resolution even when .vite-dev.json exists |
VITE_RESOLVER_DEBUG |
false |
When true and app.debug=true, writes resolver details to Log::debug |
| Scenario | Action |
|---|---|
| CI/staging must never use the dev server | VITE_DEV_SERVER_ENABLED=false |
| Wrong mode locally | Check for a stale .vite-dev.json; enable VITE_RESOLVER_DEBUG=true |
The dev-server origin is not configured in this plugin — it comes from .vite-dev.json (written by the npm package). For Docker, set hostUrl in the Vite project; see Docker and multi-owner setups.
Quick start
1. Build assets in the theme or plugin repo:
cd themes/my-theme # or plugins/vendor/plugin npm install npm run dev # local development — writes .vite-dev.json npm run build # production — writes assets/.vite/manifest.json
2. Theme layout (Twig):
{% styles %}
{{ vite_styles(['resources/entrypoint.ts']) }}
{% endstyles %}
{% scripts %}
{{ vite_scripts(['resources/entrypoint.ts']) }}
{% endscripts %}
3. PHP (recommended) — components, controllers, FormWidgets:
$this->addJs('vite:resources/modules/checkout/entrypoint.ts');
For TypeScript entrypoints that import SCSS, addJs() alone is sufficient — linked CSS from the manifest or dev-server module graph is emitted automatically. Use addCss('vite:…scss') only for dedicated stylesheet entrypoints not imported via JS.
Entry paths match the source paths under resources/ in your repository (as registered in the manifest), not hashed output filenames.
Components in theme partials
CMS components declared in a partial's front matter ([myComponent] in the INI section) run onRun() when the partial renders. Vite scope is registered automatically from the component namespace via component.run — no explicit vite:plugin:… token required.
| Location | onRun() timing |
{% styles %} in <head> |
{% scripts %} after partial |
|---|---|---|---|
| Page front matter | Before HTML (execPageCycle) |
Yes (CSS) | Yes (JS) |
| Theme partial | Mid-page when partial renders | No (head already rendered) | Yes (JS + pending CSS fallback) |
Place {% partial %} before {% scripts %} in your layout or page. CSS from partial-only components cannot appear in <head> {% styles %} because October renders the head before body partials; Vernok.Vite appends pending vite-CSS when {% scripts %} runs.
The {% component %} tag is only required when you want the component's default markup — asset registration via onRun() works from the INI alias alone.
Registering assets with addJs and addCss
The vite: prefix is the recommended way to load Vite bundles from PHP. Use it anywhere October's addJs() and addCss() are available — no trait, no vite_assets() loop, and no knowledge of manifest hashes or dev-server URLs.
$this->addJs('vite:resources/modules/admin/entrypoint.ts');
TypeScript module entrypoints:
$this->addJs('vite:resources/modules/admin/entrypoint.ts');
Dedicated SCSS entrypoint (not imported via JS):
$this->addCss('vite:resources/scss/admin.scss');
Do not register the same .ts entrypoint with both addJs and addCss — addJs is enough for bundled CSS.
Works on the frontend ({% scripts %} / {% styles %}) and in the backend layout <head>.
Where agencies use this
| Context | Typical location | Example |
|---|---|---|
| CMS component | onRun() |
$this->addJs('vite:resources/modules/shop/entrypoint.ts'); |
| Backend controller | __construct() or action |
$this->addJs('vite:resources/modules/reports/entrypoint.ts'); |
| FormWidget | loadAssets() |
$this->addJs('vite:resources/formwidgets/color-picker/entrypoint.ts'); |
| Global event listener | cms.page.init, etc. |
$controller->addJs('vite:resources/modules/analytics/entrypoint.ts'); |
Scope is inferred from the calling class namespace (plugin code from Vendor\Plugin\...). FormWidgets and backend widgets resolve to their owning plugin automatically.
Explicit scope in the token
Use when autodetection is ambiguous — shared partials, loading a theme bundle from a plugin, or cross-plugin references:
| Token | Resolves to |
|---|---|
vite:resources/entrypoint.ts |
Autodetect (plugin namespace or theme heuristics) |
vite:theme:resources/entrypoint.ts |
Active theme |
vite:plugin:Vendor.Plugin:resources/modules/foo/entrypoint.ts |
Named plugin |
$this->addJs('vite:theme:resources/entrypoint.ts'); $this->addJs('vite:plugin:Acme.Blog:resources/modules/blog/entrypoint.ts');
CSS behaviour
- Development: CSS imported by a JS entry loads through the Vite module graph.
addJs('vite:…entrypoint.ts')is sufficient for typical module bundles. - Production: CSS arrays on the manifest entry (and shared chunks via
imports) are emitted as<link>tags automatically. - Partial components: vite-CSS queued from
addCss()in a partial is appended at{% scripts %}when it could not be drained at{% styles %}.
Alternatives
| Approach | When to use |
|---|---|
vite: + addJs / addCss |
Default — least boilerplate |
HasViteAssets → addViteAssets() |
Team convention for a named enqueue method |
vite_assets() + manual loop |
Per-URL control over addJs attributes |
Twig vite_scripts() / vite_styles() |
Theme layouts |
Under the hood, vite: tokens are intercepted via system.assets.beforeAddAsset, then resolved on cms.assets.render (frontend) or backend.layout.extendHead (backend).
Twig and PHP helpers
Twig functions
| Function | Output |
|---|---|
vite_scripts($entries, $scope, $plugin) |
<script type="module"> tags (@vite/client in dev) |
vite_styles($entries, $scope, $plugin) |
<link rel="stylesheet"> tags |
vite($entries, $scope, $plugin, $type) |
Both JS and CSS (or filter with $type) |
vite_asset($path, $scope, $plugin) |
Public URL for a static asset (image, font) |
Theme — site bundle:
{{ vite_scripts(['resources/entrypoint.ts']) }}
{{ vite_styles(['resources/entrypoint.ts']) }}
<img src="{{ vite_asset('resources/images/logo.svg') }}" alt="">
Plugin — module entrypoint:
{{ vite_scripts(['resources/modules/blog/entrypoint.ts'], 'plugin', 'Acme.Blog') }}
Global PHP helpers
vite_scripts(['resources/entrypoint.ts']); vite_styles(['resources/entrypoint.ts']); vite_asset('resources/images/hero.webp'); $assets = vite_assets(['resources/modules/admin/entrypoint.ts'], 'plugin', 'Vendor.Plugin'); foreach ($assets['js'] as $js) { $controller->addJs($js['url'], array_filter(['type' => $js['type'] ?? null])); } foreach ($assets['css'] as $css) { $controller->addCss($css['url']); }
HasViteAssets trait (optional)
use Vernok\Vite\Classes\Traits\HasViteAssets; class BlogList extends ComponentBase { use HasViteAssets; public function onRun(): void { $this->addViteAssets(['resources/modules/blog/entrypoint.ts']); // Or explicit scope: // $this->addViteAssets([...], 'plugin', 'Acme.Blog'); } }
Event listeners
Prefer vite: on the controller when possible:
Event::listen('cms.page.init', function ($controller) { $controller->addJs('vite:resources/modules/analytics/entrypoint.ts'); });
Or wrap the host with Vernok\Vite\Classes\Assets\ViteAssetHostAdapter and call enqueue() for scope inference from the host class.
Scope resolution
ViteScopeResolver determines theme vs plugin and the Vendor.Plugin code:
- Explicit
$scopeand/or plugin code from the caller (Twig, helper, orvite:plugin:…token) - Namespace of the calling class (components, controllers, widgets)
- Theme heuristics (
resources/entrypoint.ts,resources/images/*,resources/fonts/*) - Call stack path (Twig partials under
themes/orplugins/) - Default: theme
When in doubt — especially in shared Twig partials — pass scope and plugin code explicitly.
Development vs production
| Mode | Signal | Asset source |
|---|---|---|
| Dev | Valid .vite-dev.json with origin in theme/plugin root |
Vite dev server (origin + /resources/...) |
| Build | No valid dev file, or VITE_DEV_SERVER_ENABLED=false |
assets/.vite/manifest.json |
Example .vite-dev.json (written while npm run dev runs):
{
"origin": "http://localhost:5173",
"pid": 12345,
"mode": "dev"
}
Invalid or incomplete JSON (missing origin) falls back to build mode.
Manifest locations
| Owner | Manifest | Public URL base |
|---|---|---|
Theme my-theme |
themes/my-theme/assets/.vite/manifest.json |
/themes/my-theme/assets/ |
Plugin Vendor.Plugin |
plugins/vendor/plugin/assets/.vite/manifest.json |
/plugins/vendor/plugin/assets/ |
Manifest keys are source paths (e.g. resources/entrypoint.ts). Values include file, optional css, and imports for shared chunks.
Add .vite-dev.json to .gitignore in each theme/plugin repo. Whether you commit built assets/ depends on your deployment workflow.
Docker and multi-owner setups
Multiple Vite dev servers: each theme or plugin with its own vite.config.ts writes its own .vite-dev.json and origin. A theme on port 5173 and a plugin on 5174 work simultaneously — pass the correct scope when registering entries.
PHP in Docker, Vite on the host — configure in the Vite project (not in Vernok.Vite):
import { defineThemeConfig, octoberTheme } from '@vernok/vite-plugin-october'; export default defineThemeConfig({ plugins: [ octoberTheme({ hostUrl: 'http://host.docker.internal:5173' }), ], });
The npm package writes that origin to .vite-dev.json; Vernok.Vite reads it automatically.
Testing
From the October CMS project root:
php artisan plugin:test Vernok.Vite
From this plugin directory:
composer test
composer pint:test
Troubleshooting
| Symptom | Likely cause | Action |
|---|---|---|
<!-- Vite: entry not found in manifest --> |
Path mismatch or missing build | Run npm run build; verify manifest key equals the path passed to Twig/vite: |
| Dev assets not loading | Missing or invalid .vite-dev.json |
Run npm run dev in the correct directory; confirm origin is set |
| Production works, dev does not | VITE_DEV_SERVER_ENABLED=false |
Remove or set to null in .env |
| Wrong plugin manifest | Autodetection chose theme | Use vite:plugin:Vendor.Plugin:… or explicit Twig scope |
Partial component + theme:… manifest comment |
Scope not registered (check component.run) |
Ensure component is in partial INI; {% scripts %} after partial |
Partial CSS not in <head> |
October render order | Expected — CSS appears at {% scripts %}; move {% styles %} after content for strict <head> CSS |
vite: appears literally in HTML |
Plugin not installed or cache | php artisan plugin:install Vernok.Vite; php artisan cache:clear |
| Stale dev mode after crash | Leftover .vite-dev.json |
Delete the file manually |
Enable debug logging:
APP_DEBUG=true VITE_RESOLVER_DEBUG=true
Inspect storage/logs for [Vernok.Vite] entries.
Known limitations
- Manifest path is fixed to
assets/.vite/manifest.jsonper theme or plugin. - A stale
.vite-dev.jsonafter a crashed dev server keeps dev mode active until removed. vite_asset()resolves only paths known to the manifest (or dev URLs for tracked source paths).- The
vite:protocol hooks October'sAssetMakerpipeline only — assets registered outsideaddJs/addCssare not intercepted.
License
MIT — see LICENSE.
Related packages
| Package | Role |
|---|---|
@vernok/vite-plugin-october |
Vite config, entry discovery, build output, .vite-dev.json lifecycle — required in every theme/plugin repo that uses Vite |
| Vernok.Vite (this plugin) | PHP resolver — Twig, helpers, traits, vite: protocol — install once in the October CMS application |
Typical workflow: @vernok/vite-plugin-october in each asset repository + Vernok.Vite in the October project. Read both READMEs — Node setup on npm, PHP integration here.