@foxmayn/fft
v1.0.6
Published
Foxmayn Frappe Types — build, watch, typecheck, and generate TypeScript for Frappe apps
Downloads
650
Maintainers
Readme
@foxmayn/fft
Foxmayn Frappe Types — build, watch, typecheck, and DocType codegen for Frappe/ERPNext apps, plus the Frappe client type surface.
A single, self-contained devDependency for the client-side TypeScript of a Frappe app:
- ⚡ Build / watch / typecheck via the esbuild JS API — one context per tier, not one OS process per file.
- 🧬 DB-accurate DocType codegen from a live site — including Custom Fields and Property-Setter-overridden Select options that static DocType JSON misses.
- 📘 Vendored Frappe client + global types — no third-party Frappe type package required.
pnpm add -D @foxmayn/fftContents
- What it does
- Requirements
- Installation
- CLI
- How builds work
- Shared bundle mode
- Type generation (
fft generate) - Frappe globals & client types
fft.config.mjsreference- Programmatic API
- Development
What it does
| Problem | Solution |
|---------|----------|
| One spawned npx esbuild process per file in watch mode (dozens of long-lived watchers) | One esbuild JS-API context per build tier |
| __bundle_entry__.ts temp files written into the source tree | Virtual esbuild entry points — zero temp files |
| Shared public/ts modules inlined into every DocType bundle | sharedPublicNamespace — one shared IIFE, slim DocType shims |
| No build script → stale .js shipped by CI | fft build — a real one-shot compile, wired to bench build |
| Generated DocType types fail tsc on quotes/HTML in field metadata | Sanitizer escapes inner quotes in unions and strips HTML in comments |
| Frappe globals & client types scattered across apps or pulled from a third party | Centralized and owned in types/globals/ + types/client/ |
Requirements
- Node ≥ 18
- TypeScript ≥ 5 — an optional peer dependency; only needed for
fft typecheckandfft watch --typecheck. It is resolved from the consumer app'snode_modules/.bin/tsc, falling back to a globaltsc. esbuildships as a direct dependency — nothing else to install.fft generateadditionally needs a workingbenchwith the target site reachable (it runs the bench's own Python).
Installation
pnpm add -D @foxmayn/fft
# or, for a local checkout:
pnpm add -D "@foxmayn/fft@file:../../foxmayn_frappe_types"package.json scripts (in your Frappe app):
{
"scripts": {
"build": "fft build",
"watch": "fft watch",
"watch:tc": "fft watch --typecheck",
"typecheck": "fft typecheck"
}
}tsconfig.json — add @foxmayn/fft to types[] to load the Frappe globals:
{
"compilerOptions": {
"types": ["jquery", "bootstrap", "@foxmayn/fft"]
}
}To import named types directly in source files:
import type { Frappe } from "@foxmayn/fft/client/frappe";
import type { FrappeForm } from "@foxmayn/fft/client/frappe/core";CLI
fft <command> [options]
Commands:
build One-shot compile of all TypeScript targets
watch Watch mode (esbuild contexts, optional tsc)
typecheck Run tsc --noEmit
generate Generate DocType TypeScript interfaces from a live site
Options:
--app <path> App root directory (default: cwd)
--typecheck Also run tsc --watch alongside esbuild (watch only)
--sourcemap Emit source maps (build / watch)
--tsconfig <path> tsconfig to use (typecheck; overrides config.tsconfig)
--site <name> Frappe site name (generate only, required)
--doctypes <list> Comma-separated DocType names (generate only, required)
--out <path> Output directory for generated types (default: <app>/types/generated)
--bench <path> Bench root (generate only; auto-detected if omitted)
--version Print version and exit
--help, -h Show usageCLI flags take precedence over fft.config.mjs for the keys they share (e.g. --sourcemap).
How builds work
FFT discovers the Frappe app structure from --app (defaults to cwd) — at both the app root and one subdirectory deep, matching the standard <app>/<module>/ layout:
<app>/<module>/doctype/<name>/ts/*.ts→<app>/<module>/doctype/<name>/<name>.js— IIFE, bundled, withfrappeandjqueryexternal.<app>/<module>/public/ts/*.ts→<app>/<module>/public/js/*.js— ESM, transpile-only (not bundled).
.d.ts files are ignored. No configuration is required for a standard layout — output paths are derived from input location. DocType entry points are virtual (an esbuild plugin synthesizes a module that imports every .ts in the doctype's ts/ folder), so nothing is ever written into your source tree.
fft watch runs the same contexts incrementally and keeps the process alive until Ctrl-C; add --typecheck to also spawn tsc --noEmit --watch.
Shared bundle mode (sharedPublicNamespace)
By default, any shared public/ts helper that a DocType imports is inlined into that DocType's bundle — duplicated across every DocType that uses it. Enable shared mode in fft.config.mjs:
// fft.config.mjs
export default {
sharedPublicNamespace: "myapp",
}FFT then builds a single shared IIFE — public/js/myapp.shared.js — that exposes the runtime exports of every module in the app's primary public/ts directory on frappe.myapp.<module>. Each DocType bundle is rewritten to read from that namespace instead of inlining the code.
Effect: DocType bundles shrink dramatically (the shared code is loaded once instead of N times). Side-effect-only public/ts files (no runtime exports) are skipped. Shared mode currently covers a single public/ts directory — modules under additional public/ts folders are still inlined.
Wire the shared bundle into your app's hooks.py:
app_include_js = [
"/assets/myapp/js/myapp.shared.js",
# ...other app-level JS
]The shared bundle must load before any DocType script. ERPNext/Frappe always load app_include_js before doctype_js, so the order holds — but verify per app during rollout.
Type generation (fft generate)
Generates TypeScript interfaces from a live Frappe site, including Custom Fields and Property-Setter-overridden Select options (which static DocType JSON misses).
fft generate --site acme.localhost --doctypes "Sales Invoice,Customer" --out myapp/types/generatedFFT auto-detects the bench root by walking up from --app until it finds a directory containing both sites/ and apps/ (robust to cross-user symlinks in env/bin/). Override with --bench. The schema-dump subprocess runs with its working directory set to <bench>/sites/ so Frappe's relative log paths resolve.
The pipeline:
- Runs the shipped
dump_schema.pyvia the bench's own Python interpreter. - For each
Selectfield, resolves options fromProperty Setterfirst (matching Frappe's actual runtime behaviour —get_field_options()ignores Property Setters), falling back to the DocField definition. - Maps each
fieldtypeto a TypeScript type (Check→0 | 1,Selectwith ≥2 options → string-literal union (elsestring),Link/dates/text →string, numeric →number,Table*→any[], anything unrecognized →any), withname: stringalways first. - Emits sanitized
.ts— inner double-quotes in union members are escaped, and HTML/*/in field-label comments is neutralized so output passestscclean.
⚠️ Never hand-edit files under
types/generated/— regenerate them instead.
Frappe globals & client types
@foxmayn/fft ships types/globals/index.d.ts, loaded automatically when you add "@foxmayn/fft" to tsconfig.json types[]. It provides:
- The full Frappe client type surface, vendored under
types/client/(frappe,cur_frm,FrappeForm,FrappeDoc,frappe.db,frappe.model,frappe.ui,frappe.datetime, …). flt(value, precision?)— Frappe float parser.format_currency(value, currency?, precision?)— Frappe currency formatter.locals—frappe.localsin-memory document store.__(text, replacements?)— translation helper (accepts bothRecordandArrayreplacements).
Coverage is intentionally partial and owned by this package — extend it from your app via interface merging in any .d.ts included by your tsconfig.json:
// myapp/types/global.d.ts
interface FrappeForm<T = Record<string, any>> {
_old_doc?: T;
}
interface Frappe {
format(value: any, opts: { fieldtype: "Currency"; options: "currency" }, extra?: { inline: true }): string;
}fft.config.mjs reference
Optional. Place it in the app root (next to package.json). All keys are optional; a missing config file — or one that throws while loading — falls back to defaults silently. Values are not type-validated: they're forwarded as-is to esbuild/tsc, so a wrong-typed value won't be swapped for a default.
// fft.config.mjs
export default {
// Banner prepended to every emitted .js file.
banner: "// Copyright (c) 2026, Acme — see license.txt",
// Module specifiers left external in DocType (IIFE) bundles.
external: ["frappe", "jquery"],
// Enable shared bundle mode — exposes public/ts on frappe.<namespace>.
// Omit (or null) to inline shared code per DocType.
sharedPublicNamespace: "myapp",
// Emit source maps for build/watch.
sourcemap: true,
// tsconfig used by `typecheck` and `watch --typecheck`.
tsconfig: "tsconfig.json",
}| Key | Type | Default | Applies to |
|-----|------|---------|------------|
| banner | string | // Generated by @foxmayn/fft — do not edit | build, watch |
| external | string[] | ["frappe", "jquery"] | build, watch (DocType tier) |
| sharedPublicNamespace | string | null (off) | build, watch |
| sourcemap | boolean | false | build, watch |
| tsconfig | string | "tsconfig.json" | typecheck, watch --typecheck |
Programmatic API
const { build, watch, generate, typecheck, discoverApp, loadConfig } = require('@foxmayn/fft')
await build({ app: '/path/to/myapp' })
await generate({ app: '/path/to/myapp', site: 'mysite.localhost', doctypes: 'Sales Invoice', out: 'types/generated' })
const { doctypes, publicTargets } = discoverApp('/path/to/myapp')The exported functions take the same flag object the CLI builds from its arguments.
Development
npm install # esbuild + vitest
npm test # vitest — 23 tests across discover, build, shared-externals, generate
node bin/fft.js --help
npx vitest run test/discover.test.js # a single test file
npx vitest run -t "escapes inner quotes" # a single test by nameTests run against the fixture app under test/fixtures/sample_app/ — no live bench is required. There is no build step for the package itself; it ships plain CommonJS lib//bin/ and hand-written .d.ts under types/.
See PLAN.md for the original design rationale and CLAUDE.md for an architecture map.
License
MIT © Foxmayn
