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,56 @@
1
+ /**
2
+ * HTTP handler — matches a request URL against loaded pages, invokes the
3
+ * page's `getContext()` if present, and returns either a full SSR'd HTML
4
+ * response (initial page load) or a JSON payload (SPA navigation).
5
+ */
6
+
7
+ import { response } from "../response"
8
+ import type { LoadedPage } from "./page-loader"
9
+ import { renderPage } from "./renderer"
10
+
11
+ export interface PageMatcher {
12
+ find(pathname: string): LoadedPage | null
13
+ readonly pages: LoadedPage[]
14
+ }
15
+
16
+ /** Phase 2 router: pathname equality against a map of static routes. */
17
+ export function createPageMatcher(pages: LoadedPage[]): PageMatcher {
18
+ const byPath = new Map(pages.map((p) => [p.routePath, p]))
19
+ return {
20
+ pages,
21
+ find: (pathname) => {
22
+ const normalized = pathname.length > 1 ? pathname.replace(/\/$/, "") : pathname
23
+ return byPath.get(normalized) ?? null
24
+ },
25
+ }
26
+ }
27
+
28
+ /**
29
+ * Attempt to handle a request as a page. Returns `null` if no page matches,
30
+ * so the caller can fall through to API routing / 404.
31
+ */
32
+ export async function handlePageRequest(
33
+ req: Request,
34
+ url: URL,
35
+ matcher: PageMatcher,
36
+ opts: { title?: string } = {},
37
+ ): Promise<Response | null> {
38
+ if (req.method !== "GET") return null
39
+
40
+ const page = matcher.find(url.pathname)
41
+ if (!page) return null
42
+
43
+ const query = Object.fromEntries(url.searchParams.entries())
44
+ const contextData = page.getContext ? await page.getContext({ params: {}, query }) : null
45
+
46
+ // SPA navigation hint → return JSON only.
47
+ if (req.headers.get("accept")?.includes("application/json")) {
48
+ return response.ok(contextData)
49
+ }
50
+
51
+ const html = await renderPage(page, contextData, { title: opts.title })
52
+ return new Response(html, {
53
+ status: 200,
54
+ headers: { "Content-Type": "text/html; charset=utf-8" },
55
+ })
56
+ }
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Server-side rendering — Vue SFC → HTML with server-side data fetching.
3
+ */
4
+
5
+ export { loadPages, relPathToRoute } from "./page-loader"
6
+ export type { LoadedPage, GetContextFn, GetContextArgs } from "./page-loader"
7
+ export { renderPage, renderComponent, FRAPPE_CONTEXT_KEY } from "./renderer"
8
+ export type { RenderOptions } from "./renderer"
9
+ export { useContext } from "./use-context"
10
+ export { handlePageRequest, createPageMatcher } from "./handler"
11
+ export type { PageMatcher } from "./handler"
@@ -0,0 +1,200 @@
1
+ /**
2
+ * Page loader — discovers `.vue` files under `<moduleDir>/pages/` and
3
+ * compiles each one into a server-renderable Vue component, plus an
4
+ * optional server-only `getContext()` function from `<script context>`.
5
+ *
6
+ * Phase 2 scope: static routes only (no `[name].vue`).
7
+ * pages/index.vue → /
8
+ * pages/about.vue → /about
9
+ * pages/portal/index.vue → /portal
10
+ * pages/portal/faq.vue → /portal/faq
11
+ *
12
+ * Pipeline for each SFC:
13
+ * 1. parse SFC → descriptor
14
+ * 2. write `<script context>` content to a cache .ts → import → getContext()
15
+ * 3. compile `<script setup>` via compileScript (SSR mode)
16
+ * 4. compile `<template>` via compileTemplate (SSR mode)
17
+ * 5. write a generated .mjs module combining setup + ssrRender
18
+ * 6. dynamic import → Component
19
+ */
20
+
21
+ import { createHash } from "node:crypto"
22
+ import { existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from "node:fs"
23
+ import { join } from "node:path"
24
+
25
+ import { compileScript, compileTemplate, parse as parseSFC } from "@vue/compiler-sfc"
26
+ import type { Component } from "vue"
27
+
28
+ // ─── Types ────────────────────────────────────────────────
29
+
30
+ export interface LoadedPage {
31
+ /** URL path this page is mounted at — e.g. "/", "/portal", "/portal/faq". */
32
+ routePath: string
33
+
34
+ /** Absolute source file path. */
35
+ sourcePath: string
36
+
37
+ /** Compiled SSR Vue component. */
38
+ component: Component
39
+
40
+ /** Server-only data fetcher from `<script context>`, if present. */
41
+ getContext?: GetContextFn
42
+ }
43
+
44
+ export type GetContextFn = (args: GetContextArgs) => Promise<unknown> | unknown
45
+
46
+ export interface GetContextArgs {
47
+ params: Record<string, string | string[]>
48
+ query: Record<string, string>
49
+ }
50
+
51
+ // ─── Public API ───────────────────────────────────────────
52
+
53
+ /**
54
+ * Scan the pages directory and load every `.vue` file it contains.
55
+ */
56
+ export async function loadPages(moduleDir: string): Promise<LoadedPage[]> {
57
+ const pagesDir = join(moduleDir, "pages")
58
+ if (!existsSync(pagesDir)) return []
59
+
60
+ const cacheDir = join(moduleDir, ".frappe-cache", "pages")
61
+ mkdirSync(cacheDir, { recursive: true })
62
+
63
+ const pages: LoadedPage[] = []
64
+ for (const relPath of walkVueFiles(pagesDir)) {
65
+ pages.push(await loadPage(join(pagesDir, relPath), relPath, cacheDir))
66
+ }
67
+
68
+ return pages.sort((a, b) => a.routePath.localeCompare(b.routePath))
69
+ }
70
+
71
+ /**
72
+ * Convert a pages-relative path into a URL route path.
73
+ * "index.vue" → "/"
74
+ * "about.vue" → "/about"
75
+ * "portal/index.vue" → "/portal"
76
+ * "portal/faq.vue" → "/portal/faq"
77
+ */
78
+ export function relPathToRoute(relPath: string): string {
79
+ const withoutExt = relPath.replace(/\.vue$/, "")
80
+ const parts = withoutExt.split("/").filter((p) => p !== "index")
81
+ return parts.length === 0 ? "/" : "/" + parts.join("/")
82
+ }
83
+
84
+ // ─── Discovery ────────────────────────────────────────────
85
+
86
+ function* walkVueFiles(root: string, prefix = ""): Generator<string> {
87
+ for (const entry of readdirSync(root).sort()) {
88
+ const abs = join(root, entry)
89
+ const rel = prefix ? `${prefix}/${entry}` : entry
90
+ if (statSync(abs).isDirectory()) yield* walkVueFiles(abs, rel)
91
+ else if (entry.endsWith(".vue")) yield rel
92
+ }
93
+ }
94
+
95
+ // ─── Per-page compilation ────────────────────────────────
96
+
97
+ async function loadPage(absPath: string, relPath: string, cacheDir: string): Promise<LoadedPage> {
98
+ const source = readFileSync(absPath, "utf-8")
99
+ const { descriptor } = parseSFC(source, { filename: absPath })
100
+
101
+ // Hash the source content so edits produce a new cache file path —
102
+ // Bun caches module imports by path, so a changing id is the cheapest
103
+ // way to force a re-import.
104
+ const id = createHash("md5").update(absPath).update(source).digest("hex").slice(0, 10)
105
+ const baseName = relPath.replace(/[\\/]/g, "__").replace(/\.vue$/, "")
106
+
107
+ const getContext = await extractGetContext(descriptor, baseName, id, cacheDir)
108
+ const component = await compilePage(descriptor, absPath, baseName, id, cacheDir)
109
+
110
+ return { routePath: relPathToRoute(relPath), sourcePath: absPath, component, getContext }
111
+ }
112
+
113
+ // ─── <script context> extraction ──────────────────────────
114
+
115
+ /**
116
+ * Detect and extract a `<script context>` block. The SFC parser treats a
117
+ * bare `<script>` as `descriptor.script`, so we identify it by the
118
+ * `context` attribute. When found we move it out of the descriptor so it
119
+ * does not confuse `compileScript` (which otherwise fails when the
120
+ * context script's `lang` differs from scriptSetup's).
121
+ */
122
+ async function extractGetContext(
123
+ // oxlint-disable-next-line typescript/no-explicit-any
124
+ descriptor: any,
125
+ baseName: string,
126
+ id: string,
127
+ cacheDir: string,
128
+ ): Promise<GetContextFn | undefined> {
129
+ const isContextScript = Boolean(descriptor.script?.attrs?.context)
130
+ const block = isContextScript
131
+ ? descriptor.script
132
+ : descriptor.customBlocks.find(
133
+ // oxlint-disable-next-line typescript/no-explicit-any
134
+ (b: any) => b.type === "context" || (b.type === "script" && b.attrs?.context),
135
+ )
136
+ if (!block) return undefined
137
+
138
+ if (isContextScript) descriptor.script = null
139
+
140
+ const file = join(cacheDir, `${baseName}.${id}.context.ts`)
141
+ writeFileSync(file, block.content)
142
+ const mod = (await import(file)) as { default?: GetContextFn }
143
+ return typeof mod.default === "function" ? mod.default : undefined
144
+ }
145
+
146
+ // ─── SFC → component compilation ──────────────────────────
147
+
148
+ async function compilePage(
149
+ // oxlint-disable-next-line typescript/no-explicit-any
150
+ descriptor: any,
151
+ absPath: string,
152
+ baseName: string,
153
+ id: string,
154
+ cacheDir: string,
155
+ ): Promise<Component> {
156
+ const hasScriptSetup = Boolean(descriptor.scriptSetup || descriptor.script)
157
+ const templateSource = descriptor.template?.content ?? ""
158
+
159
+ // Compile <script setup> (SSR-friendly). compileScript produces the
160
+ // component's bindingMetadata — the template compiler needs it to know
161
+ // which identifiers are setup bindings vs globals.
162
+ let scriptCode = "const __sfc__ = {}"
163
+ // oxlint-disable-next-line typescript/no-explicit-any
164
+ let bindingMetadata: any = undefined
165
+ if (hasScriptSetup) {
166
+ const compiled = compileScript(descriptor, {
167
+ id,
168
+ inlineTemplate: false,
169
+ genDefaultAs: "__sfc__",
170
+ })
171
+ scriptCode = compiled.content
172
+ bindingMetadata = compiled.bindings
173
+ }
174
+
175
+ // Compile <template> to an SSR render function.
176
+ const templateResult = compileTemplate({
177
+ source: templateSource,
178
+ filename: absPath,
179
+ id,
180
+ ssr: true,
181
+ ssrCssVars: [],
182
+ compilerOptions: { bindingMetadata },
183
+ })
184
+
185
+ const moduleSource = [
186
+ scriptCode,
187
+ templateResult.code,
188
+ `__sfc__.ssrRender = ssrRender`,
189
+ `__sfc__.__file = ${JSON.stringify(absPath)}`,
190
+ `export default __sfc__`,
191
+ ].join("\n\n")
192
+
193
+ // Write as .ts so Bun can transpile any TypeScript that survived
194
+ // compileScript (its output preserves TS syntax from lang="ts" blocks).
195
+ const outFile = join(cacheDir, `${baseName}.${id}.page.ts`)
196
+ writeFileSync(outFile, moduleSource)
197
+
198
+ const mod = (await import(outFile)) as { default: Component }
199
+ return mod.default
200
+ }
@@ -0,0 +1,94 @@
1
+ /**
2
+ * SSR renderer — takes a loaded page + its server-side context data and
3
+ * produces a full HTML document for the first render. The page's reactive
4
+ * data is embedded as `window.__FRAPPE_CONTEXT__` for future client hydration.
5
+ */
6
+
7
+ import { renderToString } from "@vue/server-renderer"
8
+ import { createSSRApp, ref } from "vue"
9
+ import type { Component } from "vue"
10
+
11
+ import type { LoadedPage } from "./page-loader"
12
+
13
+ export interface RenderOptions {
14
+ /** Page title rendered into `<head>`. */
15
+ title?: string
16
+ /** Stylesheet URLs to include. */
17
+ stylesheets?: string[]
18
+ }
19
+
20
+ /**
21
+ * Render a single page to a full HTML document.
22
+ *
23
+ * The server context data is injected into the root component's `setup()`
24
+ * via a provide/inject pair that `useContext()` reads from. This keeps
25
+ * the page component pure — it accepts context through the composable,
26
+ * not via props.
27
+ */
28
+ export async function renderPage(
29
+ page: LoadedPage,
30
+ contextData: unknown,
31
+ opts: RenderOptions = {},
32
+ ): Promise<string> {
33
+ const appHtml = await renderComponent(page.component, contextData)
34
+ return htmlShell({
35
+ appHtml,
36
+ contextJson: JSON.stringify(contextData ?? null),
37
+ title: opts.title ?? "Frappe",
38
+ stylesheets: opts.stylesheets ?? [],
39
+ })
40
+ }
41
+
42
+ /**
43
+ * Render just the component to an HTML string, with context provided.
44
+ */
45
+ export async function renderComponent(component: Component, contextData: unknown): Promise<string> {
46
+ const app = createSSRApp(component)
47
+ app.provide(FRAPPE_CONTEXT_KEY, ref(contextData))
48
+ return renderToString(app)
49
+ }
50
+
51
+ // ─── useContext() composable ──────────────────────────────
52
+
53
+ /**
54
+ * Key shared between the renderer (provide) and `useContext()` (inject).
55
+ * Kept as a plain string so pages don't have to import a Symbol.
56
+ */
57
+ export const FRAPPE_CONTEXT_KEY = "__frappe_context__"
58
+
59
+ // ─── HTML shell ───────────────────────────────────────────
60
+
61
+ interface ShellOptions {
62
+ appHtml: string
63
+ contextJson: string
64
+ title: string
65
+ stylesheets: string[]
66
+ }
67
+
68
+ function htmlShell({ appHtml, contextJson, title, stylesheets }: ShellOptions): string {
69
+ const styleTags = stylesheets
70
+ .map((href) => `<link rel="stylesheet" href="${escapeHtml(href)}">`)
71
+ .join("\n ")
72
+ return `<!DOCTYPE html>
73
+ <html lang="en">
74
+ <head>
75
+ <meta charset="UTF-8">
76
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
77
+ <title>${escapeHtml(title)}</title>
78
+ ${styleTags}
79
+ </head>
80
+ <body>
81
+ <div id="app">${appHtml}</div>
82
+ <script>window.__FRAPPE_CONTEXT__ = ${contextJson};</script>
83
+ </body>
84
+ </html>
85
+ `
86
+ }
87
+
88
+ function escapeHtml(s: string): string {
89
+ return s
90
+ .replace(/&/g, "&amp;")
91
+ .replace(/</g, "&lt;")
92
+ .replace(/>/g, "&gt;")
93
+ .replace(/"/g, "&quot;")
94
+ }
@@ -0,0 +1,41 @@
1
+ /**
2
+ * `useContext()` composable — reads the server-side data injected by the
3
+ * SSR renderer. On the client, falls back to `window.__FRAPPE_CONTEXT__`.
4
+ *
5
+ * Page components declare their context shape as a generic type parameter:
6
+ *
7
+ * const { user, invoices } = useContext<{ user: User; invoices: Invoice[] }>()
8
+ *
9
+ * During SSR the data is synchronously available. On the client after
10
+ * hydration, the same data has already been serialized into the HTML.
11
+ */
12
+
13
+ import { inject, ref } from "vue"
14
+ import type { Ref } from "vue"
15
+
16
+ import { FRAPPE_CONTEXT_KEY } from "./renderer"
17
+
18
+ // oxlint-disable-next-line typescript/no-explicit-any
19
+ type AnyRecord = Record<string, any>
20
+
21
+ /**
22
+ * Return the page's server-provided context as a plain object. Destructuring
23
+ * the result gives you each field as a plain value — reactivity is not
24
+ * needed for the Phase 2 static-render loop.
25
+ */
26
+ export function useContext<T extends AnyRecord = AnyRecord>(): T {
27
+ // SSR: read from the provide/inject chain.
28
+ const injected = inject<Ref<T> | undefined>(FRAPPE_CONTEXT_KEY, undefined)
29
+ if (injected) return injected.value
30
+
31
+ // Client: read from the embedded global.
32
+ if (typeof globalThis !== "undefined") {
33
+ const g = globalThis as { __FRAPPE_CONTEXT__?: T }
34
+ if (g.__FRAPPE_CONTEXT__) return g.__FRAPPE_CONTEXT__
35
+ }
36
+
37
+ return {} as T
38
+ }
39
+
40
+ // Re-export a Vue-less no-op ref for modules that need a sentinel.
41
+ export { ref }