--- 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:** 1. `bench build` scans each app's `public/` directory recursively 2. Files matching `*.bundle.*` are treated as entry points 3. esbuild compiles, bundles, and optionally minifies each entry point 4. Output goes to `assets/dist/[app]/js/` or `assets/dist/[app]/css/` 5. 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. ```json { "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 ```javascript // myapp/public/js/myapp.bundle.js import { createApp } from "vue"; import MyComponent from "./components/MyComponent.vue"; // ES6 imports are resolved by esbuild import "../css/myapp.bundle.scss"; // npm packages (installed via yarn) can be imported directly 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) ```python # hooks.py — loads in /app (Desk) app_include_js = "myapp.bundle.js" app_include_css = "myapp.bundle.css" # Multiple files app_include_js = ["myapp.bundle.js", "extra.bundle.js"] app_include_css = ["myapp.bundle.css", "extra.bundle.css"] ``` ### Portal Assets (Public Website) ```python # hooks.py — loads on web pages (portal) web_include_js = "myapp-web.bundle.js" web_include_css = "myapp-web.bundle.css" ``` ### Page-Specific Assets ```python # hooks.py — loads on specific Desk pages page_js = {"page_name": "public/js/custom_page.js"} ``` ### Web Form Assets (Standard Web Forms Only) ```python # hooks.py — loads on specific Web Forms 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 ```html {{ include_script("myapp.bundle.js") }} {{ include_style("myapp.bundle.css") }} ``` ### Lazy Loading in Desk ```javascript // Load asset on demand (returns Promise) frappe.require("myapp.bundle.js", () => { // Asset loaded, initialize component myapp.init(); }); // Multiple assets frappe.require(["widget.bundle.js", "widget.bundle.css"], () => { // Both loaded }); ``` --- ## SCSS/CSS Compilation ### SCSS Bundle Example ```scss // myapp/public/css/myapp.bundle.scss // Import Frappe variables (available in all apps) @import "frappe/public/scss/variables"; // Import partials (NOT bundles — no .bundle. in name) @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+] ```bash # Auto-rebuild on file changes 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 ```bash # Via config bench set-config -g live_reload true # Via environment variable 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 ```javascript // myapp/public/js/mypage.bundle.js 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 ```python # Create a Page DocType or use www/ for web pages # The bundle loads via hooks.py or include_script() ``` ### npm Dependencies ```bash # Install from app directory 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: ```bash 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: ```bash 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: ```bash 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 1. **Build with production flag**: `bench build --production` 2. **Verify bundle sizes**: Check `assets/dist/` for unexpectedly large files 3. **Use lazy loading**: Split rarely-used features into separate bundles loaded via `frappe.require()` 4. **Minimize hook includes**: Only include essential assets in `app_include_js/css` 5. **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 | File | Contents | |------|----------| | [examples.md](references/examples.md) | Complete build configuration examples | | [anti-patterns.md](references/anti-patterns.md) | Common build mistakes and fixes |