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,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
|
+
}
|
package/src/ssr/index.ts
ADDED
|
@@ -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, "&")
|
|
91
|
+
.replace(/</g, "<")
|
|
92
|
+
.replace(/>/g, ">")
|
|
93
|
+
.replace(/"/g, """)
|
|
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 }
|