| name | frappe-ops-frontend-build |
| description | Use when configuring frontend asset bundling, migrating from build.json (v14) to esbuild (v15+), or troubleshooting SCSS/CSS compilation. Prevents build failures from mixing v14 and v15 build systems and misconfigured asset pipelines. Covers esbuild configuration (v15+), build.json (v14), asset bundling, SCSS compilation, bundle.js setup, bench build flags. Keywords: esbuild, build.json, frontend build, SCSS, CSS, asset bundling, bench build, bundle.js, webpack, build error, assets not loading, CSS not updating, JS not compiling, bench build fails..
|
| license | MIT |
| compatibility | Claude Code, Claude.ai Projects, Claude API. Frappe v14-v16. |
| metadata | {"author":"OpenAEC-Foundation","version":"2.0"} |
Frontend Build System
Complete reference for Frappe's frontend asset bundling pipeline, from build configuration to production optimization.
Versions: v14 (build.json) / v15+ (esbuild)
Quick Reference: Build Commands
| Task | Command |
|---|
| Build all apps | bench build |
| Build specific app | bench build --app myapp |
| Build multiple apps | bench build --apps frappe,erpnext |
| Production build (minified) | bench build --production |
| Force rebuild | bench build --force |
| Watch mode (auto-rebuild) | bench watch |
| Hard link assets | bench build --hard-link |
Decision Tree: Build System Selection
Which build system?
├── Frappe v14?
│ └── build.json — Concatenation-based bundling
├── Frappe v15+?
│ └── esbuild — ES module bundling with *.bundle.* convention
└── Migrating v14 → v15?
└── Replace build.json with *.bundle.* files in public/
Build Pipeline Overview
v15+ (esbuild): Current System
The v15+ build system uses esbuild for fast ES module bundling. It automatically discovers bundle entry points by scanning the public/ directory for files matching *.bundle.{js|ts|css|scss|sass|less|styl}.
How it works:
bench build scans each app's public/ directory recursively
- Files matching
*.bundle.* are treated as entry points
- esbuild compiles, bundles, and optionally minifies each entry point
- Output goes to
assets/dist/[app]/js/ or assets/dist/[app]/css/
- Filenames include content hashes for cache-busting:
main.bundle.HASH.js
Supported file types:
.js — ES6 modules with import/export
.ts — TypeScript
.vue — Vue single-file components
.css — Standard CSS
.scss / .sass — SASS/SCSS stylesheets
.less — Less stylesheets
.styl — Stylus stylesheets
v14 (build.json): Legacy System
The v14 system uses build.json in the app root to define concatenation rules.
{
"js/myapp.min.js": [
"public/js/file1.js",
"public/js/file2.js"
],
"css/myapp.min.css": [
"public/css/style1.css",
"public/css/style2.css"
]
}
NEVER use build.json in v15+ — it is ignored by the esbuild pipeline.
Bundle Entry Points [v15+]
Creating a Bundle
Place files in your app's public/ directory with the .bundle. naming convention:
myapp/
└── public/
├── js/
│ └── myapp.bundle.js # → dist/myapp/js/myapp.bundle.HASH.js
├── css/
│ └── myapp.bundle.scss # → dist/myapp/css/myapp.bundle.HASH.css
└── components/
└── widget.bundle.js # → dist/myapp/js/widget.bundle.HASH.js
Bundle File Content
import { createApp } from "vue";
import MyComponent from "./components/MyComponent.vue";
import "../css/myapp.bundle.scss";
import dayjs from "dayjs";
createApp(MyComponent).mount("#myapp-root");
Output Mapping
| Input | Output |
|---|
public/js/main.bundle.js | assets/dist/[app]/js/main.bundle.[hash].js |
public/css/style.bundle.scss | assets/dist/[app]/css/style.bundle.[hash].css |
public/deep/nested/file.bundle.ts | assets/dist/[app]/js/file.bundle.[hash].js |
hooks.py Asset Inclusion
Desk Assets (Backend Interface)
app_include_js = "myapp.bundle.js"
app_include_css = "myapp.bundle.css"
app_include_js = ["myapp.bundle.js", "extra.bundle.js"]
app_include_css = ["myapp.bundle.css", "extra.bundle.css"]
Portal Assets (Public Website)
web_include_js = "myapp-web.bundle.js"
web_include_css = "myapp-web.bundle.css"
Page-Specific Assets
page_js = {"page_name": "public/js/custom_page.js"}
Web Form Assets (Standard Web Forms Only)
webform_include_js = {"ToDo": "public/js/custom_todo.js"}
webform_include_css = {"ToDo": "public/css/custom_todo.css"}
Critical Rules
- ALWAYS use the bundle filename (not the full path) in hooks.py for v15+
- NEVER include the hash in hooks.py — Frappe resolves the hashed filename automatically
- ALWAYS rebuild after changing hooks.py:
bench build --app myapp
- Multiple apps can define the same hooks — assets accumulate across all installed apps
Including Assets in Templates
Jinja Helpers
{{ include_script("myapp.bundle.js") }}
{{ include_style("myapp.bundle.css") }}
<script src="{{ bundled_asset('myapp.bundle.js') }}"></script>
Lazy Loading in Desk
frappe.require("myapp.bundle.js", () => {
myapp.init();
});
frappe.require(["widget.bundle.js", "widget.bundle.css"], () => {
});
SCSS/CSS Compilation
SCSS Bundle Example
@import "frappe/public/scss/variables";
@import "./components/header";
@import "./components/sidebar";
.myapp-container {
padding: var(--padding-lg);
background: var(--bg-color);
}
Partial Files
Partials (files starting with _ or without .bundle. in the name) are NOT compiled as entry points. They are only included via @import:
public/css/
├── myapp.bundle.scss # Entry point — compiled
├── _variables.scss # Partial — imported only
└── components/
├── _header.scss # Partial — imported only
└── _sidebar.scss # Partial — imported only
Development Workflow
Watch Mode [v15+]
bench watch
- Watches all apps'
public/ directories for changes
- Rebuilds only affected bundles (incremental)
- Desk auto-reloads when assets change (if
live_reload is enabled)
Enabling Live Reload
bench set-config -g live_reload true
export LIVE_RELOAD=1
Development vs Production Build
| Feature | Development (bench build) | Production (bench build --production) |
|---|
| Minification | No | Yes |
| Source maps | Yes | No |
| Bundle size | Larger | Optimized |
| Build speed | Fast | Slower |
Frappe UI (Vue.js) Custom Pages [v15+]
Setting Up a Vue Page
import { createApp } from "vue";
import { FrappeUI } from "frappe-ui";
import App from "./App.vue";
const app = createApp(App);
app.use(FrappeUI);
app.mount("#myapp-page");
Registering the Page
npm Dependencies
cd apps/myapp
yarn add vue frappe-ui dayjs
Dependencies are resolved by esbuild from node_modules/ during build.
Common Build Errors and Fixes
Error: "Could not resolve module"
ERROR: Could not resolve "some-package"
Fix: Install the missing npm package:
cd apps/myapp && yarn add some-package
Error: "No bundle entry points found"
Fix: Ensure files use the *.bundle.* naming convention and are in the public/ directory.
Error: Stale Assets After Deployment
Fix: Force rebuild with cache clear:
bench build --force
bench clear-cache
Error: CSS Not Updating
Fix: Check that SCSS files import correctly and the entry point has .bundle. in the name:
bench build --app myapp --force
Error: "build.json" Ignored in v15
Fix: Migrate to *.bundle.* entry points. build.json is a v14-only feature.
Asset Optimization for Production
Pre-Deployment Checklist
- Build with production flag:
bench build --production
- Verify bundle sizes: Check
assets/dist/ for unexpectedly large files
- Use lazy loading: Split rarely-used features into separate bundles loaded via
frappe.require()
- Minimize hook includes: Only include essential assets in
app_include_js/css
- Use CSS variables: Leverage Frappe's built-in CSS custom properties instead of duplicating styles
Bundle Splitting Strategy
public/
├── js/
│ ├── myapp.bundle.js # Core — loaded on every page via hooks
│ ├── report-widget.bundle.js # Lazy — loaded only on report pages
│ └── chart-tools.bundle.js # Lazy — loaded only when charts needed
└── css/
├── myapp.bundle.scss # Core — loaded on every page via hooks
└── print.bundle.scss # Lazy — loaded only for print views
Version Differences
| Feature | v14 | v15+ |
|---|
| Build system | build.json | esbuild |
| Entry point convention | Defined in JSON | *.bundle.* auto-discovery |
| TypeScript support | No | Yes |
| Vue SFC support | No | Yes |
| SCSS compilation | Via build pipeline | Via esbuild |
| Watch mode | bench watch | bench watch (faster) |
| Live reload | Manual | Automatic (configurable) |
| Source maps | Limited | Full support |
| Tree shaking | No | Yes |
| npm imports | Requires manual bundling | Direct ES6 imports |
Reference Files