frappebun 0.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. package/README.md +72 -0
  2. package/package.json +59 -0
  3. package/src/api/auth.ts +76 -0
  4. package/src/api/index.ts +10 -0
  5. package/src/api/resource.ts +177 -0
  6. package/src/api/route.ts +301 -0
  7. package/src/app/index.ts +6 -0
  8. package/src/app/loader.ts +218 -0
  9. package/src/auth/auth.ts +247 -0
  10. package/src/auth/index.ts +2 -0
  11. package/src/cli/args.ts +40 -0
  12. package/src/cli/bin.ts +12 -0
  13. package/src/cli/commands/add-api.ts +32 -0
  14. package/src/cli/commands/add-doctype.ts +43 -0
  15. package/src/cli/commands/add-page.ts +33 -0
  16. package/src/cli/commands/add-user.ts +96 -0
  17. package/src/cli/commands/dev.ts +71 -0
  18. package/src/cli/commands/drop-site.ts +27 -0
  19. package/src/cli/commands/init.ts +98 -0
  20. package/src/cli/commands/migrate.ts +110 -0
  21. package/src/cli/commands/new-site.ts +61 -0
  22. package/src/cli/commands/routes.ts +56 -0
  23. package/src/cli/commands/use.ts +30 -0
  24. package/src/cli/index.ts +73 -0
  25. package/src/cli/log.ts +13 -0
  26. package/src/cli/scaffold/templates.ts +189 -0
  27. package/src/context.ts +162 -0
  28. package/src/core/doctype/migration/migration.ts +17 -0
  29. package/src/core/doctype/role/role.ts +7 -0
  30. package/src/core/doctype/session/session.ts +16 -0
  31. package/src/core/doctype/user/user.controller.ts +11 -0
  32. package/src/core/doctype/user/user.ts +22 -0
  33. package/src/core/doctype/user_role/user_role.ts +9 -0
  34. package/src/core/doctypes.ts +25 -0
  35. package/src/core/index.ts +1 -0
  36. package/src/database/database.ts +359 -0
  37. package/src/database/filters.ts +131 -0
  38. package/src/database/index.ts +30 -0
  39. package/src/database/query-builder.ts +1118 -0
  40. package/src/database/schema.ts +188 -0
  41. package/src/doctype/define.ts +45 -0
  42. package/src/doctype/discovery.ts +57 -0
  43. package/src/doctype/field.ts +160 -0
  44. package/src/doctype/index.ts +20 -0
  45. package/src/doctype/layout.ts +62 -0
  46. package/src/doctype/query-builder-stub.ts +16 -0
  47. package/src/doctype/registry.ts +106 -0
  48. package/src/doctype/types.ts +407 -0
  49. package/src/document/document.ts +593 -0
  50. package/src/document/index.ts +6 -0
  51. package/src/document/naming.ts +56 -0
  52. package/src/errors.ts +53 -0
  53. package/src/frappe.d.ts +128 -0
  54. package/src/globals.ts +72 -0
  55. package/src/index.ts +112 -0
  56. package/src/migrations/index.ts +11 -0
  57. package/src/migrations/runner.ts +256 -0
  58. package/src/permissions/index.ts +265 -0
  59. package/src/response.ts +100 -0
  60. package/src/server.ts +210 -0
  61. package/src/site.ts +126 -0
  62. package/src/ssr/handler.ts +56 -0
  63. package/src/ssr/index.ts +11 -0
  64. package/src/ssr/page-loader.ts +200 -0
  65. package/src/ssr/renderer.ts +94 -0
  66. package/src/ssr/use-context.ts +41 -0
@@ -0,0 +1,98 @@
1
+ /**
2
+ * `frappe init <name>` — scaffold a complete, runnable project.
3
+ *
4
+ * Creates:
5
+ * myapp/
6
+ * package.json
7
+ * tsconfig.json
8
+ * .gitignore
9
+ * myapp/ ← app module
10
+ * pages/index.vue
11
+ * doctype/
12
+ * api/
13
+ * sites/<name>.localhost/site_config.json
14
+ * sites/common_site_config.json
15
+ *
16
+ * Does NOT run `bun install`, `git init`, or `frappe migrate` — that is
17
+ * the caller's responsibility and keeps this command trivially testable.
18
+ */
19
+
20
+ import { existsSync, mkdirSync, writeFileSync } from "node:fs"
21
+ import { join } from "node:path"
22
+
23
+ import type { CliArgs } from "../args"
24
+ import { log } from "../log"
25
+ import {
26
+ commonSiteConfigTpl,
27
+ frappeEnvTpl,
28
+ gitignoreTpl,
29
+ packageJsonTpl,
30
+ siteConfigTpl,
31
+ tsconfigTpl,
32
+ vscodeLaunchTpl,
33
+ welcomePageTpl,
34
+ } from "../scaffold/templates"
35
+
36
+ export interface InitOptions {
37
+ /** Absolute project root to create. */
38
+ root: string
39
+ /** App name — used as both package name and app module directory name. */
40
+ name: string
41
+ /** Default site name seeded in common_site_config.json. */
42
+ site?: string
43
+ }
44
+
45
+ /** Scaffold a new project at `root`. Safe to call from tests. */
46
+ export function scaffoldProject(opts: InitOptions): string[] {
47
+ const { root, name } = opts
48
+ const site = opts.site ?? `${name}.localhost`
49
+
50
+ if (existsSync(root)) throw new Error(`Directory already exists: ${root}`)
51
+
52
+ const written: string[] = []
53
+ const write = (relPath: string, content: string) => {
54
+ const abs = join(root, relPath)
55
+ mkdirSync(join(abs, ".."), { recursive: true })
56
+ writeFileSync(abs, content)
57
+ written.push(relPath)
58
+ }
59
+
60
+ // Root config
61
+ write("package.json", packageJsonTpl(name))
62
+ write("tsconfig.json", tsconfigTpl())
63
+ write(".gitignore", gitignoreTpl())
64
+ write(".vscode/launch.json", vscodeLaunchTpl())
65
+ write("frappe-env.d.ts", frappeEnvTpl())
66
+
67
+ // App module scaffolding
68
+ write(`${name}/pages/index.vue`, welcomePageTpl())
69
+ mkdirSync(join(root, name, "doctype"), { recursive: true })
70
+ mkdirSync(join(root, name, "api"), { recursive: true })
71
+
72
+ // Sites
73
+ write(`sites/${site}/site_config.json`, siteConfigTpl())
74
+ write("sites/common_site_config.json", commonSiteConfigTpl(site))
75
+
76
+ return written
77
+ }
78
+
79
+ export async function run(args: CliArgs): Promise<void> {
80
+ const name = args.positional[0]
81
+ if (!name) {
82
+ log.error("Usage: frappe init <name>")
83
+ process.exit(1)
84
+ }
85
+
86
+ const root = join(process.cwd(), name)
87
+ log.step(`Creating ${name}...`)
88
+
89
+ const written = scaffoldProject({ root, name })
90
+ for (const file of written) log.success(`${name}/${file}`)
91
+
92
+ log.blank()
93
+ log.info(`Done! Next steps:`)
94
+ log.info(` cd ${name}`)
95
+ log.info(` bun install`)
96
+ log.info(` frappe dev`)
97
+ log.blank()
98
+ }
@@ -0,0 +1,110 @@
1
+ /**
2
+ * `frappe migrate` — load the app, sync schema, run migrations.
3
+ *
4
+ * Phase 1: before-sync migrations
5
+ * Phase 2: schema sync
6
+ * Phase 3: after-sync migrations
7
+ */
8
+
9
+ import { existsSync, readdirSync, statSync } from "node:fs"
10
+ import { join } from "node:path"
11
+
12
+ import { loadApp, findProjectRoot } from "../../app/loader"
13
+ import { registerCoreDocTypes } from "../../core/doctypes"
14
+ import { FrappeDatabase } from "../../database/database"
15
+ import { installGlobals } from "../../globals"
16
+ import { runMigrations } from "../../migrations/runner"
17
+ import { loadSiteConfig } from "../../site"
18
+ import { flagString, type CliArgs } from "../args"
19
+ import { log } from "../log"
20
+
21
+ export interface MigrateOptions {
22
+ root: string
23
+ sitesDir: string
24
+ /** Migrate only this site. If omitted, migrate all sites. */
25
+ site?: string
26
+ /** Suppress per-migration log lines — `frappe dev` uses this for its own output. */
27
+ quiet?: boolean
28
+ }
29
+
30
+ export interface SiteMigrationResult {
31
+ site: string
32
+ /** Number of migrations that ran. */
33
+ ran: number
34
+ failed: boolean
35
+ error?: string
36
+ }
37
+
38
+ // ─── Core ─────────────────────────────────────────────────
39
+
40
+ export function discoverSites(sitesDir: string): string[] {
41
+ if (!existsSync(sitesDir)) return []
42
+ return readdirSync(sitesDir).filter((e) => {
43
+ if (e.startsWith(".") || e === "common_site_config.json") return false
44
+ return (
45
+ statSync(join(sitesDir, e)).isDirectory() && existsSync(join(sitesDir, e, "site_config.json"))
46
+ )
47
+ })
48
+ }
49
+
50
+ export async function migrate(opts: MigrateOptions): Promise<SiteMigrationResult[]> {
51
+ installGlobals()
52
+ await registerCoreDocTypes()
53
+
54
+ const app = await loadApp(opts.root)
55
+ const sites = opts.site ? [opts.site] : discoverSites(opts.sitesDir)
56
+ const results: SiteMigrationResult[] = []
57
+
58
+ for (const site of sites) {
59
+ const dbPath = join(opts.sitesDir, site, "site.db")
60
+ loadSiteConfig(site, opts.sitesDir) // validates site exists
61
+
62
+ const db = new FrappeDatabase(dbPath)
63
+ try {
64
+ if (!opts.quiet) {
65
+ log.info(``)
66
+ log.info(` Migrating ${site}...`)
67
+ }
68
+
69
+ const result = await runMigrations(db, app.moduleDir, app.name)
70
+ const ran = result.before.length + result.after.length
71
+
72
+ if (!opts.quiet) {
73
+ for (const r of result.before) log.success(`[before] ${r.id}`)
74
+ for (const r of result.after) log.success(`[after] ${r.id}`)
75
+ log.info(` ${ran} migration(s) executed`)
76
+ }
77
+
78
+ results.push({ site, ran, failed: false })
79
+ } catch (error) {
80
+ const msg = error instanceof Error ? error.message : String(error)
81
+ if (!opts.quiet) log.error(` ${site}: ${msg}`)
82
+ results.push({ site, ran: 0, failed: true, error: msg })
83
+ } finally {
84
+ db.close()
85
+ }
86
+ }
87
+
88
+ return results
89
+ }
90
+
91
+ // ─── CLI entry ────────────────────────────────────────────
92
+
93
+ export async function run(args: CliArgs): Promise<void> {
94
+ const root = findProjectRoot()
95
+ const sitesDir = join(root, "sites")
96
+ const site = flagString(args, "site")
97
+
98
+ const results = await migrate({ root, sitesDir, site })
99
+
100
+ log.info(``)
101
+ for (const r of results) {
102
+ if (r.failed) {
103
+ log.error(` ${r.site} — ${r.error}`)
104
+ } else {
105
+ const label = r.ran > 0 ? `${r.ran} migration(s) applied` : "schema up to date"
106
+ log.success(` ${r.site} — ${label}`)
107
+ }
108
+ }
109
+ log.blank()
110
+ }
@@ -0,0 +1,61 @@
1
+ /**
2
+ * `frappe new-site <name>` — create a site directory, write its config,
3
+ * initialise an empty SQLite DB, and migrate core DocTypes into it.
4
+ */
5
+
6
+ import { existsSync, mkdirSync, writeFileSync } from "node:fs"
7
+ import { join } from "node:path"
8
+
9
+ import { registerCoreDocTypes } from "../../core/doctypes"
10
+ import { FrappeDatabase } from "../../database/database"
11
+ import { flagString, type CliArgs } from "../args"
12
+ import { log } from "../log"
13
+
14
+ export interface NewSiteOptions {
15
+ /** Absolute path to the sites/ directory. */
16
+ sitesDir: string
17
+ /** Site name (used as both directory name and hostname). */
18
+ name: string
19
+ /** Database type — Phase 2 only supports sqlite. */
20
+ dbType?: "sqlite"
21
+ }
22
+
23
+ /**
24
+ * Create a new site directory with config + migrated database.
25
+ * Returns the site's DB instance (caller closes it).
26
+ */
27
+ export async function createSite(opts: NewSiteOptions): Promise<FrappeDatabase> {
28
+ const siteDir = join(opts.sitesDir, opts.name)
29
+ if (existsSync(siteDir)) throw new Error(`Site already exists: ${opts.name}`)
30
+
31
+ mkdirSync(siteDir, { recursive: true })
32
+ writeFileSync(
33
+ join(siteDir, "site_config.json"),
34
+ `${JSON.stringify({ db_type: opts.dbType ?? "sqlite" }, null, 2)}\n`,
35
+ )
36
+
37
+ await registerCoreDocTypes()
38
+
39
+ const db = new FrappeDatabase(join(siteDir, "site.db"))
40
+ db.migrate()
41
+ return db
42
+ }
43
+
44
+ export async function run(args: CliArgs): Promise<void> {
45
+ const name = args.positional[0]
46
+ if (!name) {
47
+ log.error("Usage: frappe new-site <name>")
48
+ process.exit(1)
49
+ }
50
+
51
+ const sitesDir = flagString(args, "sites", join(process.cwd(), "sites"))!
52
+ log.step(`Creating site ${name}...`)
53
+
54
+ const db = await createSite({ sitesDir, name })
55
+ db.close()
56
+
57
+ log.success(`sites/${name}/site_config.json`)
58
+ log.success("Database initialised")
59
+ log.success("Core tables migrated")
60
+ log.blank()
61
+ }
@@ -0,0 +1,56 @@
1
+ /**
2
+ * `frappe routes` — print all registered API routes and page handlers.
3
+ *
4
+ * Loads the app (registering DocTypes and routes) then prints a formatted
5
+ * table to stdout. Useful for debugging "why is my endpoint 404?".
6
+ */
7
+
8
+ import { routeRegistry } from "../../api/route"
9
+ import { findProjectRoot, loadApp } from "../../app/loader"
10
+ import { registerCoreDocTypes } from "../../core/doctypes"
11
+ import { installGlobals } from "../../globals"
12
+ import { loadPages } from "../../ssr/page-loader"
13
+ import type { CliArgs } from "../args"
14
+ import { log } from "../log"
15
+
16
+ export async function run(_args: CliArgs): Promise<void> {
17
+ installGlobals()
18
+ await registerCoreDocTypes()
19
+
20
+ const root = findProjectRoot()
21
+ const app = await loadApp(root)
22
+ const pages = await loadPages(app.moduleDir)
23
+
24
+ // ── API routes ────────────────────────────────────────
25
+ const routes = routeRegistry.list()
26
+ log.info("")
27
+ log.info(` API routes (${routes.length})`)
28
+ log.info(` ${"─".repeat(60)}`)
29
+
30
+ if (routes.length === 0) {
31
+ log.info(" (none)")
32
+ } else {
33
+ const methodWidth = 6
34
+ for (const r of routes) {
35
+ const method = r.method.padEnd(methodWidth)
36
+ const auth = r.auth === "public" ? " public" : r.auth === "guest-only" ? " guest" : ""
37
+ log.info(` ${method} ${r.path}${auth}`)
38
+ }
39
+ }
40
+
41
+ // ── Pages ─────────────────────────────────────────────
42
+ log.info("")
43
+ log.info(` Pages (${pages.length})`)
44
+ log.info(` ${"─".repeat(60)}`)
45
+
46
+ if (pages.length === 0) {
47
+ log.info(" (none)")
48
+ } else {
49
+ for (const p of pages) {
50
+ const ctx = p.getContext ? " + getContext" : ""
51
+ log.info(` GET ${p.routePath}${ctx}`)
52
+ }
53
+ }
54
+
55
+ log.info("")
56
+ }
@@ -0,0 +1,30 @@
1
+ /**
2
+ * `frappe use <site>` — set the default site in common_site_config.json.
3
+ */
4
+
5
+ import { existsSync, readFileSync, writeFileSync } from "node:fs"
6
+ import { join } from "node:path"
7
+
8
+ import { findProjectRoot } from "../../app/loader"
9
+ import type { CliArgs } from "../args"
10
+ import { log } from "../log"
11
+
12
+ export function setDefaultSite(sitesDir: string, site: string): void {
13
+ const configPath = join(sitesDir, "common_site_config.json")
14
+ const existing = existsSync(configPath)
15
+ ? (JSON.parse(readFileSync(configPath, "utf-8")) as Record<string, unknown>)
16
+ : {}
17
+ existing["default_site"] = site
18
+ writeFileSync(configPath, `${JSON.stringify(existing, null, 2)}\n`)
19
+ }
20
+
21
+ export async function run(args: CliArgs): Promise<void> {
22
+ const site = args.positional[0]
23
+ if (!site) {
24
+ log.error("Usage: frappe use <site>")
25
+ process.exit(1)
26
+ }
27
+ const sitesDir = join(findProjectRoot(), "sites")
28
+ setDefaultSite(sitesDir, site)
29
+ log.success(`default_site = ${site}`)
30
+ }
@@ -0,0 +1,73 @@
1
+ /**
2
+ * CLI entry point. Parses argv, dispatches to a lazy-loaded command module.
3
+ *
4
+ * Commands are dynamically imported so `frappe --help` stays instant and
5
+ * one broken command can never take down the rest of the CLI.
6
+ */
7
+
8
+ import { parseArgs } from "./args"
9
+ import { log } from "./log"
10
+
11
+ type CommandModule = { run: (args: ReturnType<typeof parseArgs>) => Promise<void> }
12
+
13
+ const commands: Record<string, () => Promise<CommandModule>> = {
14
+ "init": () => import("./commands/init"),
15
+ "dev": () => import("./commands/dev"),
16
+ "migrate": () => import("./commands/migrate"),
17
+ "routes": () => import("./commands/routes"),
18
+ "new-site": () => import("./commands/new-site"),
19
+ "drop-site": () => import("./commands/drop-site"),
20
+ "use": () => import("./commands/use"),
21
+ "add-doctype": () => import("./commands/add-doctype"),
22
+ "add-api": () => import("./commands/add-api"),
23
+ "add-page": () => import("./commands/add-page"),
24
+ "add-user": () => import("./commands/add-user"),
25
+ }
26
+
27
+ export async function main(argv: string[] = process.argv.slice(2)): Promise<void> {
28
+ const [command, ...rest] = argv
29
+ const loader = command ? commands[command] : undefined
30
+
31
+ if (!command || command === "--help" || command === "-h") {
32
+ printHelp()
33
+ process.exit(command ? 0 : 0)
34
+ }
35
+
36
+ if (!loader) {
37
+ log.error(`Unknown command: ${command}`)
38
+ printHelp()
39
+ process.exit(1)
40
+ }
41
+
42
+ const mod = await loader()
43
+ await mod.run(parseArgs(rest))
44
+ }
45
+
46
+ function printHelp(): void {
47
+ log.info("")
48
+ log.info(" frappe — Frappe Framework CLI")
49
+ log.info("")
50
+ log.info(" Usage: frappe <command> [options]")
51
+ log.info("")
52
+ log.info(" Commands:")
53
+ log.info(" init <name> Scaffold a new Frappe project")
54
+ log.info(" dev Start the development server")
55
+ log.info(" routes List all API routes and pages")
56
+ log.info(" migrate Sync schemas & run migrations")
57
+ log.info(" new-site <name> Create a new site")
58
+ log.info(" drop-site <name> Delete a site")
59
+ log.info(" use <site> Set the default site")
60
+ log.info(" add-doctype <Name> Scaffold a new DocType")
61
+ log.info(" add-api <name> Scaffold a new API route file")
62
+ log.info(" add-page <path> Scaffold a new SSR page")
63
+ log.info(" add-user <email> Create a user on the default site")
64
+ log.info("")
65
+ }
66
+
67
+ // When executed directly (e.g. via a shim `#!/usr/bin/env bun`).
68
+ if (import.meta.main) {
69
+ main().catch((err: unknown) => {
70
+ log.error(err instanceof Error ? err.message : String(err))
71
+ process.exit(1)
72
+ })
73
+ }
package/src/cli/log.ts ADDED
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Minimal logger for CLI output — plain text, no color deps.
3
+ * Functions are tiny and self-contained so tests can spy on them.
4
+ */
5
+
6
+ export const log = {
7
+ info: (msg: string) => console.log(msg),
8
+ success: (msg: string) => console.log(` ✓ ${msg}`),
9
+ warn: (msg: string) => console.warn(` ! ${msg}`),
10
+ error: (msg: string) => console.error(` ✗ ${msg}`),
11
+ step: (msg: string) => console.log(`\n ${msg}\n`),
12
+ blank: () => console.log(""),
13
+ }
@@ -0,0 +1,189 @@
1
+ /**
2
+ * Inline scaffold templates for `frappe init` and `frappe add-*` commands.
3
+ *
4
+ * Kept as functions so we can interpolate the app name cleanly without
5
+ * pulling in a template library.
6
+ */
7
+
8
+ export function packageJsonTpl(name: string): string {
9
+ return `${JSON.stringify(
10
+ {
11
+ name,
12
+ version: "0.0.1",
13
+ private: true,
14
+ frappe: { title: titleCase(name) },
15
+ scripts: {
16
+ dev: "frappe dev",
17
+ test: "frappe test",
18
+ build: "frappe build",
19
+ typecheck: "tsc --noEmit",
20
+ },
21
+ dependencies: { frappebun: "^0.0.1", vue: "^3.5.0" },
22
+ devDependencies: { "@types/bun": "latest", "typescript": "^5.0.0" },
23
+ },
24
+ null,
25
+ 2,
26
+ )}\n`
27
+ }
28
+
29
+ export function vscodeLaunchTpl(): string {
30
+ return `{
31
+ "version": "0.2.0",
32
+ "configurations": [
33
+ {
34
+ "type": "bun",
35
+ "request": "launch",
36
+ "name": "frappe dev",
37
+ "program": "\${workspaceFolder}/node_modules/frappebun/src/cli/bin.ts",
38
+ "args": ["dev"],
39
+ "cwd": "\${workspaceFolder}",
40
+ "stopOnEntry": false,
41
+ "watchMode": false
42
+ }
43
+ ]
44
+ }\n`
45
+ }
46
+
47
+ export function frappeEnvTpl(): string {
48
+ return `/// <reference path="./node_modules/frappebun/src/frappe.d.ts" />\n`
49
+ }
50
+
51
+ export function tsconfigTpl(): string {
52
+ return `{
53
+ "compilerOptions": {
54
+ "lib": ["ESNext", "DOM"],
55
+ "target": "ESNext",
56
+ "module": "Preserve",
57
+ "moduleDetection": "force",
58
+ "moduleResolution": "bundler",
59
+ "allowImportingTsExtensions": true,
60
+ "verbatimModuleSyntax": true,
61
+ "noEmit": true,
62
+ "strict": true,
63
+ "skipLibCheck": true,
64
+ "noFallthroughCasesInSwitch": true,
65
+ "noUncheckedIndexedAccess": true,
66
+ // picks up @types/* packages AND frappebun's ambient globals
67
+ "typeRoots": ["./node_modules/@types", "./node_modules"]
68
+ }
69
+ }\n`
70
+ }
71
+
72
+ export function gitignoreTpl(): string {
73
+ return [
74
+ "node_modules/",
75
+ "sites/*/site.db",
76
+ "sites/*/site.db-*",
77
+ "*.sqlite",
78
+ "*.sqlite-shm",
79
+ "*.sqlite-wal",
80
+ ".frappe-cache/",
81
+ "",
82
+ ].join("\n")
83
+ }
84
+
85
+ export function siteConfigTpl(): string {
86
+ return `${JSON.stringify({ db_type: "sqlite" }, null, 2)}\n`
87
+ }
88
+
89
+ export function commonSiteConfigTpl(defaultSite: string): string {
90
+ return `${JSON.stringify({ default_site: defaultSite }, null, 2)}\n`
91
+ }
92
+
93
+ export function welcomePageTpl(): string {
94
+ return `<script context>
95
+ export default async function getContext() {
96
+ const userCount = await frappe.db.count("User")
97
+ return {
98
+ siteName: frappe.site,
99
+ userCount,
100
+ }
101
+ }
102
+ </script>
103
+
104
+ <script setup lang="ts">
105
+ const { siteName, userCount } = useContext<{
106
+ siteName: string
107
+ userCount: number
108
+ }>()
109
+ </script>
110
+
111
+ <template>
112
+ <main style="font-family: system-ui; max-width: 40rem; margin: 4rem auto; padding: 0 1rem">
113
+ <h1>Welcome to {{ siteName }}</h1>
114
+ <p>Your Frappe app is running — {{ userCount }} user(s) registered.</p>
115
+ <p>
116
+ Next steps: run
117
+ <code>frappe add-doctype "My First DocType"</code>
118
+ </p>
119
+ </main>
120
+ </template>
121
+ `
122
+ }
123
+
124
+ export function doctypeSchemaTpl(name: string): string {
125
+ return `export default doctype(${JSON.stringify(name)}, {
126
+ naming: "autoincrement",
127
+ fields: {
128
+ title: field.data(),
129
+ },
130
+ })
131
+ `
132
+ }
133
+
134
+ export function doctypeControllerTpl(name: string): string {
135
+ return `export default controller(${JSON.stringify(name)}, {
136
+ // validate(doc) { },
137
+ // beforeInsert(doc) { },
138
+ // afterInsert(doc) { },
139
+ // beforeSave(doc) { },
140
+ // afterSave(doc) { },
141
+ // onTrash(doc) { },
142
+ })
143
+ `
144
+ }
145
+
146
+ export function apiRouteTpl(): string {
147
+ return `export const ping = route(async () => {
148
+ return { message: "pong" }
149
+ })
150
+ `
151
+ }
152
+
153
+ export function pageTpl(): string {
154
+ return `<script context>
155
+ export default async function getContext() {
156
+ return { now: new Date().toISOString() }
157
+ }
158
+ </script>
159
+
160
+ <script setup lang="ts">
161
+ const { now } = useContext<{ now: string }>()
162
+ </script>
163
+
164
+ <template>
165
+ <main>
166
+ <h1>New page</h1>
167
+ <p>Rendered at {{ now }}</p>
168
+ </main>
169
+ </template>
170
+ `
171
+ }
172
+
173
+ // ─── Helpers ──────────────────────────────────────────────
174
+
175
+ function titleCase(s: string): string {
176
+ return s
177
+ .split(/[-_\s]+/)
178
+ .map((w) => w.charAt(0).toUpperCase() + w.slice(1))
179
+ .join(" ")
180
+ }
181
+
182
+ /** Lowercase, underscore-separated slug used for DocType directory names. */
183
+ export function slugify(s: string): string {
184
+ return s
185
+ .trim()
186
+ .toLowerCase()
187
+ .replace(/[^a-z0-9]+/g, "_")
188
+ .replace(/^_+|_+$/g, "")
189
+ }