agentsclimarketplace

Frappe frontend

Skill prilk-consulting/frappe-agent-kit/skills/frappe-frontend

Production-grade Frappe Framework & ERPNext skills, agents, and scaffolding commands for Claude Code

Install
npx -y skills add prilk-consulting/frappe-agent-kit --skill frappe-frontend

Assembled from the repository path, not quoted from the project. Check it against their README if it does not work.

One thing to look at

  • 0 stars0 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.

What its author says it does

Copied from the file, not written here

Modern SPA frontends for Frappe apps. Use for React with frappe-react-sdk, Vue 3 with frappe-ui, frappe-js-sdk, Vite build configuration, the dev/prod boot dance, Socket.io realtime, and asset-path pitfalls that 404 in production. Not for desk form scripts (use frappe-dev) or server-rendered portal pages (use frappe-portal).

SKILL.md

8.0 KB, ~2.1k tokens by cl100k_base, as published. Nobody here has run it

Modern Frontend Development for Frappe

Usage

Use this skill when:

  • Building a React or Vue SPA inside a Frappe app (Raven/CRM-style)
  • Wiring Vite builds into bench build and Frappe routing
  • Debugging prod asset 404s, dev boot failures, or silent realtime listeners
  • Choosing between frappe-react-sdk, frappe-ui, and frappe-js-sdk

Stack options

StackLibrariesReference apps
Reactfrappe-react-sdk, SWR, Tailwind, Radix/shadcnRaven
Vue 3frappe-ui, Pinia, TailwindCRM, Helpdesk

frappe-js-sdk (any framework)

import { FrappeApp } from 'frappe-js-sdk'
const frappe = new FrappeApp('https://site.example.com')
await frappe.auth.loginWithUsernamePassword({ username, password })
await frappe.db.getDoc('Customer', 'X')
await frappe.db.getDocList('Customer', { fields: ['name'], filters: [['disabled', '=', 0]], limit: 20 })
await frappe.call.post('module.method', { param: 'value' })
await frappe.file.uploadFile(file, { doctype: 'Customer', docname: 'X' })

frappe-react-sdk (React)

<FrappeProvider socketPort={import.meta.env.VITE_SOCKET_PORT || '9000'}>
  <App />
</FrappeProvider>

// SWR-backed hooks — call mutate() to refetch after writes
const { data, error, isLoading, mutate } = useFrappeGetCall<{ message: Row[] }>(
    'myapp.api.get_list', { status: 'Open', limit: 50 })
const rows = data?.message ?? []

const { data: doc, mutate } = useFrappeGetDoc<Shape>(doctype, name)
const { data: list } = useFrappeGetDocList<Shape>('Customer', {
    fields: ['name'], filters: [['disabled', '=', 0]],
    orderBy: { field: 'modified', order: 'desc' }, limit: 50 })

const { updateDoc } = useFrappeUpdateDoc()
await updateDoc('Customer', name, { territory: 'US' }); await mutate()

useFrappeEventListener('myapp_progress', d => setProgress(d.percent))
useFrappeDocumentEventListener(doctype, name, () => mutate())

Error helper — every server error has { exception, exc_type, _server_messages, message }:

const msg = err?.exception || err?.message || 'Unexpected error'

React app scaffold (canonical)

apps/myapp/frontend/
├── index.html               # Jinja: {{ csrf_token }}, {{ boot }}
├── package.json             # dev / build / copy-html-entry scripts
├── vite.config.ts           # base, proxyOptions, manualChunks
├── proxyOptions.ts          # reads common_site_config.json
└── src/
    ├── main.tsx             # dev/prod boot dance + FrappeProvider mount
    ├── App.tsx              # BrowserRouter basename="/myroute"
    └── components/ui/       # shadcn/ui wrappers (Radix + tailwind-merge)

The dev/prod boot dance

In prod, Frappe injects boot into window.frappe.boot via the Jinja template. In dev, the template literally contains {{ boot }} — you must fetch it before mounting:

<!-- index.html -->
<script>
  window.csrf_token = '{{ csrf_token }}'
  if (!window.frappe) window.frappe = {}
  frappe.boot = JSON.parse({{ boot }})
</script>
// src/main.tsx
const mount = () => createRoot(document.getElementById('root')!).render(
    <FrappeProvider socketPort={import.meta.env.VITE_SOCKET_PORT || '9000'}>
        <App />
    </FrappeProvider>)

if (import.meta.env.DEV) {
    fetch('/api/method/myapp.www.myroute.get_context_for_dev', { method: 'POST' })
        .then(r => r.json()).then(v => { window.frappe.boot = JSON.parse(v.message); mount() })
        .catch(() => { window.location.href = '/login?redirect-to=/myroute' })
} else { mount() }
# myapp/www/myroute.py
@frappe.whitelist()
def get_context_for_dev():
    return json.dumps(frappe.sessions.get())

Build chain — three paths MUST agree

--base=/assets/<app>/<name>/ in the build script · BrowserRouter basename="/<route>" · website_route_rules from_route. Any drift → assets 404 in prod.

// package.json
"scripts": {
  "dev": "vite",
  "build": "vite build --base=/assets/myapp/myname/ && yarn copy-html-entry",
  "copy-html-entry": "cp ../myapp/public/myname/index.html ../myapp/www/myroute.html"
}
# hooks.py
website_route_rules = [{"from_route": "/myroute/<path:app_path>", "to_route": "myroute"}]
add_to_apps_screen = [{"name": "myapp", "logo": "/assets/myapp/images/logo.svg",
                        "title": "My App", "route": "/myroute"}]

The <path:app_path> catch-all gives client-side routing the whole subtree.

Vite config (canonical)

// vite.config.ts
export default defineConfig({
  plugins: [react()],
  server: { port: 8080, proxy: proxyOptions },
  resolve: { alias: { '@': path.resolve(__dirname, './src') } },
  build: {
    outDir: '../myapp/public/myname',
    emptyOutDir: true, target: 'es2015',
    rollupOptions: { output: { manualChunks: {
      vendor: ['react', 'react-dom', 'react-router-dom'],
      frappe: ['frappe-react-sdk'],
    }}},
  },
})
// proxyOptions.ts — derives upstream port from bench config; multi-site safe
const config = JSON.parse(fs.readFileSync('../../../sites/common_site_config.json', 'utf-8'))
const port = config.webserver_port || 8000
export default {
  '^/(app|api|assets|files|private)': {
    target: `http://127.0.0.1:${port}`,
    ws: true, changeOrigin: true,
    router: (req: any) => `http://${req.headers.host?.split(':')[0]}:${port}`,
  },
}

frappe-ui (Vue 3)

import frappeui from 'frappe-ui/vite'
export default defineConfig({
  plugins: [
    frappeui({
      frappeProxy: true,
      lucideIcons: true,
      jinjaBootData: true,
      buildConfig: { indexHtmlPath: '../myapp/www/app.html', emptyOutDir: true },
    }),
    vue(),
  ],
})
import { FrappeUI, setConfig, frappeRequest } from 'frappe-ui'
setConfig('resourceFetcher', frappeRequest)
app.use(FrappeUI).use(pinia)
const data = await frappeRequest({ method: 'frappe.client.get_list', body: { doctype, fields: ['name'] } })

Wiring the build into bench

Add a root package.json build script so bench build regenerates the bundle, then gitignore the build output (public/<bundle>/ and www/<route>.html):

// apps/myapp/package.json (root)
{ "scripts": { "build": "cd frontend && yarn install && yarn build" } }

Only commit built assets when the app has no root build wiring (and then document why).

Frontend gotchas

  • Base path mismatch--base / basename / website_route_rules drift → /assets/*.js 404 in prod.
  • Dev boot timing — In dev, window.frappe.boot is undefined until the get_context_for_dev fetch resolves; mount AFTER it resolves.
  • Socket port mismatchFrappeProvider socketPort must match the bench's socketio_port. Mismatch fails silently — handlers never fire.
  • Proxy site routingproxyOptions.ts must derive the host from request headers. Hardcoding localhost:8000 breaks multi-site dev.
  • copy-html-entry is mandatory — Vite writes to public/; Frappe serves the entry from www/<route>.html. Skip the copy → route 404 after build.
  • mutate() after writes, not refetch hooks — the SDK is SWR-backed; call mutate() to revalidate one query, globalMutate(matcher) to invalidate many.
  • Realtime needs user=frappe.publish_realtime(event, data, user=target) to target one user; useFrappeEventListener only fires for events bound to frappe.session.user.
  • CSRF tokenfrappe-react-sdk reads window.csrf_token automatically. Don't pass it manually.
  • Validation stays server-side — SPA-side checks are UX hints; every rule must also exist in the DocType controller, because the REST API bypasses your React forms entirely.

What ships with it

Read from the repository

Just SKILL.md. No reference files, no scripts.

Keep looking

Skills are one crate of 326,970. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.