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.
- package/README.md +72 -0
- package/package.json +59 -0
- package/src/api/auth.ts +76 -0
- package/src/api/index.ts +10 -0
- package/src/api/resource.ts +177 -0
- package/src/api/route.ts +301 -0
- package/src/app/index.ts +6 -0
- package/src/app/loader.ts +218 -0
- package/src/auth/auth.ts +247 -0
- package/src/auth/index.ts +2 -0
- package/src/cli/args.ts +40 -0
- package/src/cli/bin.ts +12 -0
- package/src/cli/commands/add-api.ts +32 -0
- package/src/cli/commands/add-doctype.ts +43 -0
- package/src/cli/commands/add-page.ts +33 -0
- package/src/cli/commands/add-user.ts +96 -0
- package/src/cli/commands/dev.ts +71 -0
- package/src/cli/commands/drop-site.ts +27 -0
- package/src/cli/commands/init.ts +98 -0
- package/src/cli/commands/migrate.ts +110 -0
- package/src/cli/commands/new-site.ts +61 -0
- package/src/cli/commands/routes.ts +56 -0
- package/src/cli/commands/use.ts +30 -0
- package/src/cli/index.ts +73 -0
- package/src/cli/log.ts +13 -0
- package/src/cli/scaffold/templates.ts +189 -0
- package/src/context.ts +162 -0
- package/src/core/doctype/migration/migration.ts +17 -0
- package/src/core/doctype/role/role.ts +7 -0
- package/src/core/doctype/session/session.ts +16 -0
- package/src/core/doctype/user/user.controller.ts +11 -0
- package/src/core/doctype/user/user.ts +22 -0
- package/src/core/doctype/user_role/user_role.ts +9 -0
- package/src/core/doctypes.ts +25 -0
- package/src/core/index.ts +1 -0
- package/src/database/database.ts +359 -0
- package/src/database/filters.ts +131 -0
- package/src/database/index.ts +30 -0
- package/src/database/query-builder.ts +1118 -0
- package/src/database/schema.ts +188 -0
- package/src/doctype/define.ts +45 -0
- package/src/doctype/discovery.ts +57 -0
- package/src/doctype/field.ts +160 -0
- package/src/doctype/index.ts +20 -0
- package/src/doctype/layout.ts +62 -0
- package/src/doctype/query-builder-stub.ts +16 -0
- package/src/doctype/registry.ts +106 -0
- package/src/doctype/types.ts +407 -0
- package/src/document/document.ts +593 -0
- package/src/document/index.ts +6 -0
- package/src/document/naming.ts +56 -0
- package/src/errors.ts +53 -0
- package/src/frappe.d.ts +128 -0
- package/src/globals.ts +72 -0
- package/src/index.ts +112 -0
- package/src/migrations/index.ts +11 -0
- package/src/migrations/runner.ts +256 -0
- package/src/permissions/index.ts +265 -0
- package/src/response.ts +100 -0
- package/src/server.ts +210 -0
- package/src/site.ts +126 -0
- package/src/ssr/handler.ts +56 -0
- package/src/ssr/index.ts +11 -0
- package/src/ssr/page-loader.ts +200 -0
- package/src/ssr/renderer.ts +94 -0
- 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
|
+
}
|
package/src/cli/index.ts
ADDED
|
@@ -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
|
+
}
|