
Frappe Ops Frontend Build
- 23 installs
- 159 repo stars
- Updated July 8, 2026
- openaec-foundation/frappe_claude_skill_package
Helps with frontend development tasks.
About
frappe-ops-frontend-build is a Claude Code skill for frontend development. It helps solo builders move faster with AI-assisted development.
- frappe-ops-frontend-build
- Frontend Development
- AI-coding skill
Frappe Ops Frontend Build by the numbers
- 23 all-time installs (skills.sh)
- Ranked #1,515 of 2,245 Frontend Development skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/openaec-foundation/frappe_claude_skill_package --skill frappe-ops-frontend-buildAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 23 |
|---|---|
| repo stars | ★ 159 |
| Last updated | July 8, 2026 |
| Repository | openaec-foundation/frappe_claude_skill_package ↗ |
What it does
Helps with frontend development tasks.
Files
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.
{
"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.jsBundle File Content
// 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)
# 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)
# hooks.py — loads on web pages (portal)
web_include_js = "myapp-web.bundle.js"
web_include_css = "myapp-web.bundle.css"Page-Specific Assets
# hooks.py — loads on specific Desk pages
page_js = {"page_name": "public/js/custom_page.js"}Web Form Assets (Standard Web Forms Only)
# 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
<!-- Include script with correct hash -->
{{ include_script("myapp.bundle.js") }}
<!-- Include stylesheet with correct hash -->
{{ include_style("myapp.bundle.css") }}
<!-- Get path string only (no HTML tag) -->
<script src="{{ bundled_asset('myapp.bundle.js') }}"></script>Lazy Loading in Desk
// 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
// 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+]
# 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_reloadis enabled)
Enabling Live Reload
# Via config
bench set-config -g live_reload true
# Via environment variable
export LIVE_RELOAD=1Development 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
// 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
# Create a Page DocType or use www/ for web pages
# The bundle loads via hooks.py or include_script()npm Dependencies
# Install from app directory
cd apps/myapp
yarn add vue frappe-ui dayjsDependencies 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-packageError: "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-cacheError: CSS Not Updating
Fix: Check that SCSS files import correctly and the entry point has .bundle. in the name:
bench build --app myapp --forceError: "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 | Complete build configuration examples |
| anti-patterns.md | Common build mistakes and fixes |
Frontend Build Anti-Patterns
Anti-Pattern 1: Using build.json in v15+
// WRONG in v15+:
{
"js/myapp.min.js": ["public/js/file1.js", "public/js/file2.js"]
}Why it breaks: v15+ ignores build.json entirely. The esbuild pipeline only discovers *.bundle.* files.
Correct approach: Create bundle entry points using the *.bundle.* convention:
// public/js/myapp.bundle.js
import "./file1.js";
import "./file2.js";---
Anti-Pattern 2: Including Full Paths in hooks.py (v15+)
# WRONG in v15+:
app_include_js = "assets/myapp/js/myapp.min.js"
app_include_css = "assets/myapp/css/myapp.min.css"Why it breaks: v15+ uses hashed filenames. The full path will not resolve.
Correct approach: Use the bundle name only — Frappe resolves the hashed path automatically:
app_include_js = "myapp.bundle.js"
app_include_css = "myapp.bundle.css"---
Anti-Pattern 3: Including Hash in hooks.py
# WRONG:
app_include_js = "myapp.bundle.abc123.js"Why it breaks: The hash changes on every build. Hardcoding it means assets break after the next build.
Correct approach: NEVER include the hash. Use the base bundle name:
app_include_js = "myapp.bundle.js"---
Anti-Pattern 4: Forgetting to Rebuild After hooks.py Changes
# Changed hooks.py but didn't rebuild
app_include_js = "new-feature.bundle.js" # Added this line
# But forgot: bench build --app myappWhy it breaks: hooks.py changes require a rebuild to register new asset mappings.
Correct approach: ALWAYS run bench build --app myapp after changing hooks.py.
---
Anti-Pattern 5: Naming Partials with .bundle.
public/css/
├── myapp.bundle.scss
└── _helpers.bundle.scss # WRONG — this becomes a separate entry pointWhy it breaks: Any file with .bundle. in the name is treated as an entry point and compiled separately.
Correct approach: Partials MUST NOT have .bundle. in their name:
public/css/
├── myapp.bundle.scss # Entry point
└── _helpers.scss # Partial — imported only---
Anti-Pattern 6: Loading All Assets on Every Page
# WRONG — huge bundle loaded on every Desk page
app_include_js = [
"myapp.bundle.js",
"charts.bundle.js",
"reports.bundle.js",
"dashboard.bundle.js",
"print-tools.bundle.js"
]Why it breaks: Every page load downloads all these bundles, even when not needed.
Correct approach: Only include essential assets in hooks. Lazy-load the rest:
app_include_js = "myapp.bundle.js" # Core only// Lazy load when needed
frappe.require("charts.bundle.js", () => {
renderChart();
});---
Anti-Pattern 7: Not Using --production for Deployment
# WRONG for production:
bench buildWhy it breaks: Development builds include source maps and are not minified, resulting in larger downloads and slower page loads.
Correct approach: ALWAYS use --production for deployment:
bench build --production---
Anti-Pattern 8: Editing Files in assets/dist/
# WRONG: Editing compiled output directly
vim sites/assets/dist/myapp/js/myapp.bundle.abc123.jsWhy it breaks: Changes are overwritten on the next bench build. The dist/ directory is auto-generated.
Correct approach: ALWAYS edit source files in apps/myapp/public/ and rebuild.
---
Anti-Pattern 9: Mixing ES6 and CommonJS Imports
// WRONG — mixing module systems
const dayjs = require("dayjs"); // CommonJS
import { Chart } from "chart.js"; // ES6Why it breaks: esbuild handles both but mixing can cause unexpected bundling behavior and duplicate dependencies.
Correct approach: ALWAYS use ES6 imports consistently:
import dayjs from "dayjs";
import { Chart } from "chart.js";---
Anti-Pattern 10: Not Clearing Cache After Production Build
bench build --production
# Forgot: bench clear-cache
# Users still see old assets due to cached asset pathsWhy it breaks: Frappe caches asset URL mappings. Old hashed filenames are served until cache is cleared.
Correct approach: ALWAYS clear cache after production builds:
bench build --production
bench clear-cacheFrontend Build Examples
Example 1: Minimal App with Custom JS and CSS
Directory Structure
myapp/
├── hooks.py
└── public/
├── js/
│ └── myapp.bundle.js
└── css/
└── myapp.bundle.scsshooks.py
app_include_js = "myapp.bundle.js"
app_include_css = "myapp.bundle.css"myapp.bundle.js
// Simple Desk customization
frappe.provide("myapp");
myapp.init = function() {
console.log("MyApp loaded");
};
// Auto-initialize
$(document).ready(() => myapp.init());myapp.bundle.scss
.myapp-highlight {
background-color: var(--yellow-100);
padding: var(--padding-sm);
border-radius: var(--border-radius);
}Build
bench build --app myapp---
Example 2: Vue Component Page [v15+]
Directory Structure
myapp/
├── hooks.py
└── public/
├── js/
│ ├── myapp.bundle.js # Core bundle (hooks)
│ └── dashboard.bundle.js # Lazy-loaded Vue dashboard
└── css/
└── myapp.bundle.scssdashboard.bundle.js
import { createApp } from "vue";
import Dashboard from "./components/Dashboard.vue";
// Mount when called
window.myapp_dashboard = {
mount(el) {
const app = createApp(Dashboard);
app.mount(el);
return app;
}
};Loading the Dashboard Lazily
// In a Page or Client Script
frappe.require("dashboard.bundle.js", () => {
const container = document.getElementById("dashboard-container");
window.myapp_dashboard.mount(container);
});---
Example 3: Migrating from build.json (v14) to esbuild (v15+)
Before (v14 — build.json)
{
"js/myapp.min.js": [
"public/js/utils.js",
"public/js/forms.js",
"public/js/reports.js"
],
"css/myapp.min.css": [
"public/css/base.css",
"public/css/forms.css"
]
}After (v15+ — bundle files)
// public/js/myapp.bundle.js
import "./utils.js";
import "./forms.js";
import "./reports.js";// public/css/myapp.bundle.scss
@import "./base";
@import "./forms";Update hooks.py
# Before (v14):
app_include_js = "assets/myapp/js/myapp.min.js"
app_include_css = "assets/myapp/css/myapp.min.css"
# After (v15+):
app_include_js = "myapp.bundle.js"
app_include_css = "myapp.bundle.css"Delete build.json
rm apps/myapp/build.json
bench build --app myapp---
Example 4: Portal (Website) Assets
hooks.py
# Web-only assets (load on public pages, NOT in Desk)
web_include_js = "myapp-web.bundle.js"
web_include_css = "myapp-web.bundle.css"public/js/myapp-web.bundle.js
// Portal-specific JavaScript
document.addEventListener("DOMContentLoaded", () => {
// Initialize web components
document.querySelectorAll(".myapp-accordion").forEach(el => {
el.addEventListener("click", toggleAccordion);
});
});
function toggleAccordion(e) {
e.currentTarget.classList.toggle("active");
}---
Example 5: Using npm Packages
# Install dependencies
cd apps/myapp
yarn add chart.js dayjs// public/js/charts.bundle.js
import { Chart } from "chart.js/auto";
import dayjs from "dayjs";
export function renderChart(canvas, data) {
return new Chart(canvas, {
type: "bar",
data: {
labels: data.map(d => dayjs(d.date).format("MMM DD")),
datasets: [{
label: "Revenue",
data: data.map(d => d.amount)
}]
}
});
}---
Example 6: SCSS with Frappe Variables
// public/css/myapp.bundle.scss
// Use Frappe's built-in CSS variables (no import needed for CSS vars)
.myapp-card {
background: var(--card-bg);
border: 1px solid var(--border-color);
border-radius: var(--border-radius-lg);
padding: var(--padding-lg);
margin-bottom: var(--margin-md);
&__title {
font-size: var(--text-lg);
font-weight: var(--weight-semibold);
color: var(--heading-color);
}
&__body {
color: var(--text-color);
font-size: var(--text-base);
}
&--highlighted {
border-color: var(--primary);
box-shadow: var(--shadow-sm);
}
}
// Dark mode support (automatic via Frappe's theme system)
// CSS variables automatically switch values in dark mode---
Example 7: Production Build and Deploy
# 1. Build for production (minified, no source maps)
bench build --production
# 2. Verify output
ls -la sites/assets/dist/myapp/js/
# myapp.bundle.abc123.js (minified)
ls -la sites/assets/dist/myapp/css/
# myapp.bundle.def456.css (minified)
# 3. Clear cache to serve new assets
bench clear-cache
# 4. Restart to pick up changes
sudo bench restart