npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@foxmayn/fft

v1.0.6

Published

Foxmayn Frappe Types — build, watch, typecheck, and generate TypeScript for Frappe apps

Downloads

650

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/fft

Contents


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 typecheck and fft watch --typecheck. It is resolved from the consumer app's node_modules/.bin/tsc, falling back to a global tsc.
  • esbuild ships as a direct dependency — nothing else to install.
  • fft generate additionally needs a working bench with 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 usage

CLI 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>.jsIIFE, bundled, with frappe and jquery external.
  • <app>/<module>/public/ts/*.ts<app>/<module>/public/js/*.jsESM, 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/generated

FFT 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:

  1. Runs the shipped dump_schema.py via the bench's own Python interpreter.
  2. For each Select field, resolves options from Property Setter first (matching Frappe's actual runtime behaviour — get_field_options() ignores Property Setters), falling back to the DocField definition.
  3. Maps each fieldtype to a TypeScript type (Check0 | 1, Select with ≥2 options → string-literal union (else string), Link/dates/text → string, numeric → number, Table*any[], anything unrecognized → any), with name: string always first.
  4. Emits sanitized .ts — inner double-quotes in union members are escaped, and HTML/*/ in field-label comments is neutralized so output passes tsc clean.

⚠️ 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.
  • localsfrappe.locals in-memory document store.
  • __(text, replacements?) — translation helper (accepts both Record and Array replacements).

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 name

Tests 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