@pylonsync/functions 0.3.292 → 0.3.293
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/dist/define.d.ts +195 -0
- package/dist/index.d.ts +25 -0
- package/dist/member.d.ts +8 -0
- package/dist/runtime.d.ts +19 -0
- package/dist/slugify.d.ts +49 -0
- package/dist/ssr-client-boundary.d.ts +136 -0
- package/dist/ssr-client-bundler.d.ts +79 -0
- package/dist/ssr-fonts.d.ts +94 -0
- package/dist/ssr-form-runtime.d.ts +33 -0
- package/dist/ssr-runtime.d.ts +419 -0
- package/dist/testing.d.ts +31 -0
- package/dist/types.d.ts +561 -0
- package/dist/validators.d.ts +74 -0
- package/package.json +15 -7
- package/src/ssr-client-bundler.ts +25 -0
- package/src/ssr-fonts.test.ts +303 -0
- package/src/ssr-fonts.ts +633 -0
- package/src/ssr-runtime.ts +34 -2
package/dist/define.d.ts
ADDED
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Function definition constructors.
|
|
3
|
+
*
|
|
4
|
+
* These are the primary API for defining server-side functions.
|
|
5
|
+
*
|
|
6
|
+
* Each constructor (query / mutation / action) has two overloads:
|
|
7
|
+
*
|
|
8
|
+
* 1. `auth` omitted, or `auth: "user" | "admin"` → the framework
|
|
9
|
+
* enforces a real signed-in user, so the handler's
|
|
10
|
+
* `ctx.auth.userId` is narrowed from `string | null` to
|
|
11
|
+
* `string`. The redundant `if (!ctx.auth.userId) throw …`
|
|
12
|
+
* check disappears from app code.
|
|
13
|
+
*
|
|
14
|
+
* 2. `auth: "public" | "guest"` → the function is reachable by
|
|
15
|
+
* anonymous callers, so `ctx.auth.userId` stays `string | null`
|
|
16
|
+
* and the handler must check it manually if relevant.
|
|
17
|
+
*
|
|
18
|
+
* The framework gate happens BEFORE the handler runs (in Rust,
|
|
19
|
+
* inside the router). A handler can't accidentally leak data by
|
|
20
|
+
* forgetting an auth check — when `auth: "user"` is in effect,
|
|
21
|
+
* an anonymous request never reaches the handler at all.
|
|
22
|
+
*/
|
|
23
|
+
import type { FnDefinition, QueryCtx, MutationCtx, ActionCtx, Validator } from "./types";
|
|
24
|
+
interface CommonDef {
|
|
25
|
+
args?: Record<string, Validator>;
|
|
26
|
+
/**
|
|
27
|
+
* When true, the function is callable only via `ctx.runQuery()` /
|
|
28
|
+
* `ctx.runMutation()` / `ctx.runAction()` from another function
|
|
29
|
+
* — never via the public `/api/fn/<name>` HTTP endpoint. The
|
|
30
|
+
* router refuses external calls with `404 FN_NOT_FOUND` so
|
|
31
|
+
* probing can't even confirm the name exists.
|
|
32
|
+
*
|
|
33
|
+
* Use for helper functions that are safe inside trusted
|
|
34
|
+
* wrappers but unsafe if any caller could invoke them directly
|
|
35
|
+
* (e.g. they trust args without re-checking caller authority).
|
|
36
|
+
*
|
|
37
|
+
* Internal functions inherit the wrapping handler's auth — the
|
|
38
|
+
* `auth` field has no effect when `internal: true`. The router
|
|
39
|
+
* isn't reachable anyway, and the caller has already passed its
|
|
40
|
+
* own gate.
|
|
41
|
+
*/
|
|
42
|
+
internal?: boolean;
|
|
43
|
+
/**
|
|
44
|
+
* Max wall-clock SECONDS this function may run before the runtime
|
|
45
|
+
* recycles its worker as wedged. Defaults to `PYLON_FN_CALL_TIMEOUT`
|
|
46
|
+
* (30s). Raise it for legitimately long-running work — heavy renders,
|
|
47
|
+
* big batch jobs, slow external calls — so the call isn't force-killed
|
|
48
|
+
* mid-flight. This also lifts the runtime's wedge backstop for the
|
|
49
|
+
* worker while such a call is in flight, so a busy-but-progressing
|
|
50
|
+
* worker (e.g. one doing synchronous canvas/image work that blocks the
|
|
51
|
+
* event loop) isn't respawned out from under the work.
|
|
52
|
+
*
|
|
53
|
+
* Keep it as small as the work honestly needs: a genuinely stuck call
|
|
54
|
+
* still ties up its worker until this deadline. Prefer offloading very
|
|
55
|
+
* heavy CPU work to a dedicated service over setting a huge timeout.
|
|
56
|
+
*/
|
|
57
|
+
timeout?: number;
|
|
58
|
+
}
|
|
59
|
+
interface QueryDefRequired<TArgs, TReturn> extends CommonDef {
|
|
60
|
+
/** Defaults to `"user"`. See [`AuthMode`] for the full surface. */
|
|
61
|
+
auth?: "user" | "admin";
|
|
62
|
+
handler: (ctx: QueryCtx<"required">, args: TArgs) => Promise<TReturn>;
|
|
63
|
+
}
|
|
64
|
+
interface QueryDefOptional<TArgs, TReturn> extends CommonDef {
|
|
65
|
+
auth: "public" | "guest";
|
|
66
|
+
handler: (ctx: QueryCtx<"optional">, args: TArgs) => Promise<TReturn>;
|
|
67
|
+
}
|
|
68
|
+
interface MutationDefRequired<TArgs, TReturn> extends CommonDef {
|
|
69
|
+
auth?: "user" | "admin";
|
|
70
|
+
handler: (ctx: MutationCtx<"required">, args: TArgs) => Promise<TReturn>;
|
|
71
|
+
}
|
|
72
|
+
interface MutationDefOptional<TArgs, TReturn> extends CommonDef {
|
|
73
|
+
auth: "public" | "guest";
|
|
74
|
+
handler: (ctx: MutationCtx<"optional">, args: TArgs) => Promise<TReturn>;
|
|
75
|
+
}
|
|
76
|
+
interface ActionDefRequired<TArgs, TReturn> extends CommonDef {
|
|
77
|
+
auth?: "user" | "admin";
|
|
78
|
+
handler: (ctx: ActionCtx<"required">, args: TArgs) => Promise<TReturn>;
|
|
79
|
+
}
|
|
80
|
+
interface ActionDefOptional<TArgs, TReturn> extends CommonDef {
|
|
81
|
+
auth: "public" | "guest";
|
|
82
|
+
handler: (ctx: ActionCtx<"optional">, args: TArgs) => Promise<TReturn>;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Define a read-only query function.
|
|
86
|
+
*
|
|
87
|
+
* Queries use the read pool — they never block writes and can run
|
|
88
|
+
* concurrently. They cannot modify data.
|
|
89
|
+
*
|
|
90
|
+
* @example
|
|
91
|
+
* ```typescript
|
|
92
|
+
* export default query({
|
|
93
|
+
* // auth: "user" is the default — ctx.auth.userId is `string`,
|
|
94
|
+
* // not `string | null`, inside the handler.
|
|
95
|
+
* args: { auctionId: v.string() },
|
|
96
|
+
* async handler(ctx, args) {
|
|
97
|
+
* return ctx.db.query("Lot", {
|
|
98
|
+
* auctionId: args.auctionId,
|
|
99
|
+
* authorId: ctx.auth.userId,
|
|
100
|
+
* });
|
|
101
|
+
* },
|
|
102
|
+
* });
|
|
103
|
+
* ```
|
|
104
|
+
*
|
|
105
|
+
* @example
|
|
106
|
+
* ```typescript
|
|
107
|
+
* // Explicitly public — landing-page count, never auth-shaped.
|
|
108
|
+
* export default query({
|
|
109
|
+
* auth: "public",
|
|
110
|
+
* async handler(ctx) {
|
|
111
|
+
* return ctx.db.query("PublicPost", { published: true });
|
|
112
|
+
* },
|
|
113
|
+
* });
|
|
114
|
+
* ```
|
|
115
|
+
*/
|
|
116
|
+
export declare function query<TArgs = Record<string, unknown>, TReturn = unknown>(def: QueryDefRequired<TArgs, TReturn>): FnDefinition<TArgs, TReturn>;
|
|
117
|
+
export declare function query<TArgs = Record<string, unknown>, TReturn = unknown>(def: QueryDefOptional<TArgs, TReturn>): FnDefinition<TArgs, TReturn>;
|
|
118
|
+
/**
|
|
119
|
+
* Define a transactional mutation function.
|
|
120
|
+
*
|
|
121
|
+
* The entire handler IS the transaction. If it returns, all writes commit
|
|
122
|
+
* atomically. If it throws, all writes roll back — including scheduled
|
|
123
|
+
* functions.
|
|
124
|
+
*
|
|
125
|
+
* Mutations can stream data to the client via `ctx.stream.write()`.
|
|
126
|
+
* Stream chunks are sent immediately; DB writes commit at the end.
|
|
127
|
+
*
|
|
128
|
+
* @example
|
|
129
|
+
* ```typescript
|
|
130
|
+
* export default mutation({
|
|
131
|
+
* args: { lotId: v.string(), amount: v.number() },
|
|
132
|
+
* async handler(ctx, args) {
|
|
133
|
+
* // ctx.auth.userId is `string` (not nullable) — the runtime
|
|
134
|
+
* // already enforced `auth: "user"` (default) before reaching here.
|
|
135
|
+
* const lot = await ctx.db.get("Lot", args.lotId);
|
|
136
|
+
* if (!lot) throw ctx.error("NOT_FOUND", "Lot not found");
|
|
137
|
+
* await ctx.db.insert("Bid", {
|
|
138
|
+
* lotId: args.lotId,
|
|
139
|
+
* amount: args.amount,
|
|
140
|
+
* bidderId: ctx.auth.userId,
|
|
141
|
+
* });
|
|
142
|
+
* return { accepted: true };
|
|
143
|
+
* },
|
|
144
|
+
* });
|
|
145
|
+
* ```
|
|
146
|
+
*/
|
|
147
|
+
export declare function mutation<TArgs = Record<string, unknown>, TReturn = unknown>(def: MutationDefRequired<TArgs, TReturn>): FnDefinition<TArgs, TReturn>;
|
|
148
|
+
export declare function mutation<TArgs = Record<string, unknown>, TReturn = unknown>(def: MutationDefOptional<TArgs, TReturn>): FnDefinition<TArgs, TReturn>;
|
|
149
|
+
/**
|
|
150
|
+
* Define an action function (external I/O allowed).
|
|
151
|
+
*
|
|
152
|
+
* Actions can call external APIs (fetch, email, Stripe, etc.) but cannot
|
|
153
|
+
* access the database directly. Use `ctx.runQuery()` and `ctx.runMutation()`
|
|
154
|
+
* for DB access — each runs in its own transaction.
|
|
155
|
+
*
|
|
156
|
+
* Actions are NOT automatically retried because they may have side effects.
|
|
157
|
+
*
|
|
158
|
+
* **Important:** policies don't gate actions — `auth: "user"` (the
|
|
159
|
+
* default) is the only thing protecting an action from anonymous calls.
|
|
160
|
+
* An action that charges Stripe, hits a private API, or reads a
|
|
161
|
+
* secret will respond happily to anonymous POSTs unless this gate
|
|
162
|
+
* fires. Pick `auth: "public"` explicitly only when the endpoint is
|
|
163
|
+
* meant to be open (a webhook receiver, a public form submit, …).
|
|
164
|
+
*
|
|
165
|
+
* @example
|
|
166
|
+
* ```typescript
|
|
167
|
+
* export default action({
|
|
168
|
+
* args: { lotId: v.string() },
|
|
169
|
+
* async handler(ctx, args) {
|
|
170
|
+
* const lot = await ctx.runQuery("lotDetails", { lotId: args.lotId });
|
|
171
|
+
* await fetch("https://api.sendgrid.com/...", { ... });
|
|
172
|
+
* await ctx.runMutation("markNotified", { lotId: args.lotId });
|
|
173
|
+
* },
|
|
174
|
+
* });
|
|
175
|
+
* ```
|
|
176
|
+
*
|
|
177
|
+
* @example
|
|
178
|
+
* ```typescript
|
|
179
|
+
* // GitHub webhook receiver — public because GitHub doesn't sign
|
|
180
|
+
* // in, the handler verifies the signature itself, then optionally
|
|
181
|
+
* // elevates.
|
|
182
|
+
* export default action({
|
|
183
|
+
* auth: "public",
|
|
184
|
+
* async handler(ctx) {
|
|
185
|
+
* const ok = verifyGithubSignature(secret, ctx.request!.rawBody, sig);
|
|
186
|
+
* if (!ok) throw ctx.error("INVALID_SIGNATURE", "bad sig");
|
|
187
|
+
* await ctx.auth.elevate({ admin: true, reason: "github webhook hmac" });
|
|
188
|
+
* // …
|
|
189
|
+
* },
|
|
190
|
+
* });
|
|
191
|
+
* ```
|
|
192
|
+
*/
|
|
193
|
+
export declare function action<TArgs = Record<string, unknown>, TReturn = unknown>(def: ActionDefRequired<TArgs, TReturn>): FnDefinition<TArgs, TReturn>;
|
|
194
|
+
export declare function action<TArgs = Record<string, unknown>, TReturn = unknown>(def: ActionDefOptional<TArgs, TReturn>): FnDefinition<TArgs, TReturn>;
|
|
195
|
+
export {};
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @pylonsync/functions — TypeScript function definitions for pylon.
|
|
3
|
+
*
|
|
4
|
+
* This is the developer-facing API. App developers import from here
|
|
5
|
+
* to define queries, mutations, and actions.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```typescript
|
|
9
|
+
* import { mutation, v } from "@pylonsync/functions";
|
|
10
|
+
*
|
|
11
|
+
* export default mutation({
|
|
12
|
+
* args: { lotId: v.string(), amount: v.number() },
|
|
13
|
+
* async handler(ctx, args) {
|
|
14
|
+
* const lot = await ctx.db.get("Lot", args.lotId);
|
|
15
|
+
* // ...
|
|
16
|
+
* },
|
|
17
|
+
* });
|
|
18
|
+
* ```
|
|
19
|
+
*/
|
|
20
|
+
export { query, mutation, action } from "./define";
|
|
21
|
+
export { v } from "./validators";
|
|
22
|
+
export { resetDb, installTestIsolation } from "./testing";
|
|
23
|
+
export { slugifyName, availableSlug } from "./slugify";
|
|
24
|
+
export type { SsrResponse, SsrCookieOptions, SsrMetadata, Sitemap, SitemapEntry, Robots, RobotsRule, } from "./ssr-runtime";
|
|
25
|
+
export type { QueryCtx, MutationCtx, ActionCtx, DbReader, DbWriter, Stream, Scheduler, AuthInfo, AuthMode, AuthRequirement, FnDefinition, RequireMember, RequireMemberOptions, MemberRow, } from "./types";
|
package/dist/member.d.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { RequireMember } from "./types";
|
|
2
|
+
/**
|
|
3
|
+
* Build `ctx.requireMember(orgId, opts)` bound to a membership `read`. Errors
|
|
4
|
+
* carry `.code` (UNAUTHENTICATED / MISSING_ORG / FORBIDDEN) the same way
|
|
5
|
+
* `ctx.error` does — self-contained so it also works on the query ctx, which
|
|
6
|
+
* has no `ctx.error`.
|
|
7
|
+
*/
|
|
8
|
+
export declare function makeRequireMember(userId: string | null | undefined, read: (entity: string, filter: Record<string, unknown>) => Promise<any[]>): RequireMember;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Function runtime — the Bun process that loads and executes TypeScript functions.
|
|
3
|
+
*
|
|
4
|
+
* Protocol: NDJSON over stdin/stdout.
|
|
5
|
+
*
|
|
6
|
+
* Usage:
|
|
7
|
+
* bun run packages/functions/src/runtime.ts ./functions
|
|
8
|
+
*
|
|
9
|
+
* Design:
|
|
10
|
+
* - A single reader consumes lines from stdin and dispatches by message type.
|
|
11
|
+
* - Incoming `call` messages launch a handler.
|
|
12
|
+
* - Incoming `result` messages resolve a pending RPC keyed by call_id.
|
|
13
|
+
* - Each call's handler has at most ONE outstanding RPC at a time (it awaits
|
|
14
|
+
* each ctx.db / ctx.scheduler / ctx.runMutation call), so the map never
|
|
15
|
+
* needs to queue multiple RPCs per call_id.
|
|
16
|
+
*/
|
|
17
|
+
import type { DbReader, DbWriter } from "./types";
|
|
18
|
+
export declare function buildDbReader(callId: string): DbReader;
|
|
19
|
+
export declare function buildDbWriter(callId: string): DbWriter;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { DbReader, DbWriter } from "./types";
|
|
2
|
+
/**
|
|
3
|
+
* Pure: turn a human name into a URL-safe base slug. Doesn't check
|
|
4
|
+
* uniqueness — combine with `availableSlug` for that.
|
|
5
|
+
*
|
|
6
|
+
* Rules:
|
|
7
|
+
* - lowercase
|
|
8
|
+
* - non-alphanumeric runs become single dashes
|
|
9
|
+
* - trim leading/trailing dashes
|
|
10
|
+
* - max 40 chars (so a numeric suffix can land within typical
|
|
11
|
+
* 50-char schema constraints)
|
|
12
|
+
* - empty result (e.g. emoji-only name) → `fallback`
|
|
13
|
+
*
|
|
14
|
+
* @param name The user-typed display name to slugify.
|
|
15
|
+
* @param fallback Replacement when the slug strips to empty. Default: "x".
|
|
16
|
+
*/
|
|
17
|
+
export declare function slugifyName(name: string, fallback?: string): string;
|
|
18
|
+
/**
|
|
19
|
+
* Find the first slug that's available in the named entity's `field`
|
|
20
|
+
* column. Tries `base`, then `base-2`, `base-3`, …, then a 6-char
|
|
21
|
+
* random suffix as a final fallback.
|
|
22
|
+
*
|
|
23
|
+
* Reserved slugs (operational subdomain names, brand-protected words)
|
|
24
|
+
* can be passed via `reserved`; matches are skipped.
|
|
25
|
+
*
|
|
26
|
+
* Caller still wraps the eventual insert in try/catch — TOCTOU is
|
|
27
|
+
* always possible with this kind of "check then write" probe, and
|
|
28
|
+
* the framework's UNIQUE index is the actual source of truth. The
|
|
29
|
+
* helper just minimises the number of avoidable retries.
|
|
30
|
+
*
|
|
31
|
+
* @example
|
|
32
|
+
* ```ts
|
|
33
|
+
* const slug = await availableSlug(ctx.db, {
|
|
34
|
+
* entity: "Workspace",
|
|
35
|
+
* field: "slug",
|
|
36
|
+
* name: args.name,
|
|
37
|
+
* reserved: RESERVED_SUBDOMAINS,
|
|
38
|
+
* });
|
|
39
|
+
* await ctx.db.insert("Workspace", { slug, ... });
|
|
40
|
+
* ```
|
|
41
|
+
*/
|
|
42
|
+
export declare function availableSlug(db: DbReader | DbWriter, options: {
|
|
43
|
+
entity: string;
|
|
44
|
+
field?: string;
|
|
45
|
+
name: string;
|
|
46
|
+
reserved?: ReadonlySet<string> | readonly string[];
|
|
47
|
+
/** Max numeric suffix to try before falling back to random. Default: 50. */
|
|
48
|
+
maxNumericTries?: number;
|
|
49
|
+
}): Promise<string>;
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolve the nearest boundary module (`<dir>/not-found` or `<dir>/error`)
|
|
3
|
+
* for a route by walking its component path up to the app root, returning the
|
|
4
|
+
* first manifest route key that exists. Nearest ancestor wins — the same model
|
|
5
|
+
* the server's `findBoundary` uses, but driven off the client build manifest's
|
|
6
|
+
* route keys so the client runtime needs no extra server round-trip.
|
|
7
|
+
*
|
|
8
|
+
* `component` is a cwd-relative path with "/" separators and no extension
|
|
9
|
+
* (e.g. "web/app/dashboard/orgs/[slug]/page"); `routeKeys` is
|
|
10
|
+
* `Object.keys(manifest.routes)`.
|
|
11
|
+
*/
|
|
12
|
+
export declare function nearestBoundaryComponent(component: string, fileName: "not-found" | "error", routeKeys: Iterable<string>): string | null;
|
|
13
|
+
/** Runtime internals the boundary needs, injected so the module stays
|
|
14
|
+
* browser-safe AND unit-testable. */
|
|
15
|
+
export interface BoundaryDeps {
|
|
16
|
+
/** Resolve the client build manifest (`{ routes: {...} }`). */
|
|
17
|
+
loadManifest: () => Promise<{
|
|
18
|
+
routes?: Record<string, unknown>;
|
|
19
|
+
} | null>;
|
|
20
|
+
/** Dynamically load a route entry → its Page + Layout chain. */
|
|
21
|
+
loadRouteEntry: (component: string) => Promise<{
|
|
22
|
+
Page: any;
|
|
23
|
+
Layouts: any[];
|
|
24
|
+
}>;
|
|
25
|
+
/** Client-side navigation — used by an error boundary's reset(). */
|
|
26
|
+
navigate: (href: string, opts?: {
|
|
27
|
+
replace?: boolean;
|
|
28
|
+
}) => void;
|
|
29
|
+
/** Wrap a Page in its Layout chain with props (the runtime's buildTree). */
|
|
30
|
+
buildTree: (Page: any, Layouts: any[], props: any) => any;
|
|
31
|
+
/** The props the active page was rendered with (auth, serverData, …). */
|
|
32
|
+
getPageProps: () => any;
|
|
33
|
+
/** Current same-origin href, for an error boundary's reset() re-navigation. */
|
|
34
|
+
getResetHref: () => string;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Build the client error / not-found boundary against the runtime's internals.
|
|
38
|
+
* Returns `withBoundary(tree, component, navEpoch)` — the transparent root
|
|
39
|
+
* wrapper the runtime puts around every page — plus `PylonBoundary` for tests.
|
|
40
|
+
*
|
|
41
|
+
* Behavior: a `notFound()` (digest "PYLON_NOT_FOUND") or any error thrown
|
|
42
|
+
* during a descendant's render is caught; the nearest not-found.tsx / error.tsx
|
|
43
|
+
* for the active route is resolved from the manifest, loaded, and rendered in
|
|
44
|
+
* its own layout chain WITH the page's props (so an auth-guarding layout sees
|
|
45
|
+
* the real auth and doesn't redirect away). Resets on navigation via a changing
|
|
46
|
+
* `navEpoch` prop — not a key — so layouts keep their state across nav.
|
|
47
|
+
*/
|
|
48
|
+
export declare function createPylonBoundary(deps: BoundaryDeps): {
|
|
49
|
+
withBoundary: (tree: any, component: string, navEpoch: number) => import("react").CElement<any, {
|
|
50
|
+
componentDidUpdate(prev: any): void;
|
|
51
|
+
componentDidCatch(err: any): void;
|
|
52
|
+
render(): any;
|
|
53
|
+
context: unknown;
|
|
54
|
+
setState<K extends "err">(state: {
|
|
55
|
+
err: any;
|
|
56
|
+
} | ((prevState: Readonly<{
|
|
57
|
+
err: any;
|
|
58
|
+
}>, props: Readonly<any>) => {
|
|
59
|
+
err: any;
|
|
60
|
+
} | Pick<{
|
|
61
|
+
err: any;
|
|
62
|
+
}, K> | null) | Pick<{
|
|
63
|
+
err: any;
|
|
64
|
+
}, K> | null, callback?: (() => void) | undefined): void;
|
|
65
|
+
forceUpdate(callback?: (() => void) | undefined): void;
|
|
66
|
+
readonly props: Readonly<any>;
|
|
67
|
+
state: Readonly<{
|
|
68
|
+
err: any;
|
|
69
|
+
}>;
|
|
70
|
+
componentDidMount?(): void;
|
|
71
|
+
shouldComponentUpdate?(nextProps: Readonly<any>, nextState: Readonly<{
|
|
72
|
+
err: any;
|
|
73
|
+
}>, nextContext: any): boolean;
|
|
74
|
+
componentWillUnmount?(): void;
|
|
75
|
+
getSnapshotBeforeUpdate?(prevProps: Readonly<any>, prevState: Readonly<{
|
|
76
|
+
err: any;
|
|
77
|
+
}>): any;
|
|
78
|
+
componentWillMount?(): void;
|
|
79
|
+
UNSAFE_componentWillMount?(): void;
|
|
80
|
+
componentWillReceiveProps?(nextProps: Readonly<any>, nextContext: any): void;
|
|
81
|
+
UNSAFE_componentWillReceiveProps?(nextProps: Readonly<any>, nextContext: any): void;
|
|
82
|
+
componentWillUpdate?(nextProps: Readonly<any>, nextState: Readonly<{
|
|
83
|
+
err: any;
|
|
84
|
+
}>, nextContext: any): void;
|
|
85
|
+
UNSAFE_componentWillUpdate?(nextProps: Readonly<any>, nextState: Readonly<{
|
|
86
|
+
err: any;
|
|
87
|
+
}>, nextContext: any): void;
|
|
88
|
+
}>;
|
|
89
|
+
PylonBoundary: {
|
|
90
|
+
new (p: any): {
|
|
91
|
+
componentDidUpdate(prev: any): void;
|
|
92
|
+
componentDidCatch(err: any): void;
|
|
93
|
+
render(): any;
|
|
94
|
+
context: unknown;
|
|
95
|
+
setState<K extends "err">(state: {
|
|
96
|
+
err: any;
|
|
97
|
+
} | ((prevState: Readonly<{
|
|
98
|
+
err: any;
|
|
99
|
+
}>, props: Readonly<any>) => {
|
|
100
|
+
err: any;
|
|
101
|
+
} | Pick<{
|
|
102
|
+
err: any;
|
|
103
|
+
}, K> | null) | Pick<{
|
|
104
|
+
err: any;
|
|
105
|
+
}, K> | null, callback?: (() => void) | undefined): void;
|
|
106
|
+
forceUpdate(callback?: (() => void) | undefined): void;
|
|
107
|
+
readonly props: Readonly<any>;
|
|
108
|
+
state: Readonly<{
|
|
109
|
+
err: any;
|
|
110
|
+
}>;
|
|
111
|
+
componentDidMount?(): void;
|
|
112
|
+
shouldComponentUpdate?(nextProps: Readonly<any>, nextState: Readonly<{
|
|
113
|
+
err: any;
|
|
114
|
+
}>, nextContext: any): boolean;
|
|
115
|
+
componentWillUnmount?(): void;
|
|
116
|
+
getSnapshotBeforeUpdate?(prevProps: Readonly<any>, prevState: Readonly<{
|
|
117
|
+
err: any;
|
|
118
|
+
}>): any;
|
|
119
|
+
componentWillMount?(): void;
|
|
120
|
+
UNSAFE_componentWillMount?(): void;
|
|
121
|
+
componentWillReceiveProps?(nextProps: Readonly<any>, nextContext: any): void;
|
|
122
|
+
UNSAFE_componentWillReceiveProps?(nextProps: Readonly<any>, nextContext: any): void;
|
|
123
|
+
componentWillUpdate?(nextProps: Readonly<any>, nextState: Readonly<{
|
|
124
|
+
err: any;
|
|
125
|
+
}>, nextContext: any): void;
|
|
126
|
+
UNSAFE_componentWillUpdate?(nextProps: Readonly<any>, nextState: Readonly<{
|
|
127
|
+
err: any;
|
|
128
|
+
}>, nextContext: any): void;
|
|
129
|
+
};
|
|
130
|
+
getDerivedStateFromError(err: any): {
|
|
131
|
+
err: any;
|
|
132
|
+
};
|
|
133
|
+
contextType?: import("react").Context<any> | undefined;
|
|
134
|
+
propTypes?: any;
|
|
135
|
+
};
|
|
136
|
+
};
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { type ManifestFonts } from "./ssr-fonts";
|
|
2
|
+
type Send = (msg: Record<string, unknown>) => void;
|
|
3
|
+
interface BundleClientMessage {
|
|
4
|
+
type: "bundle_client";
|
|
5
|
+
call_id: string;
|
|
6
|
+
/**
|
|
7
|
+
* Project-relative directory holding the route tree
|
|
8
|
+
* (`<app_dir>/**/page.tsx`). Defaults to `"app"` when the host
|
|
9
|
+
* doesn't send it (older hosts, or the default single-`app/`
|
|
10
|
+
* layout). The full-stack app that namespaces its frontend under a
|
|
11
|
+
* subdir — e.g. `web/app` via `discoverAppRoutes({appDir:"web/app"})`
|
|
12
|
+
* — sends the same dir here so the client bundler and the SSR
|
|
13
|
+
* manifest agree on where the routes live.
|
|
14
|
+
*/
|
|
15
|
+
app_dir?: string;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Manifest schema. One entry per route, indexed by the same
|
|
19
|
+
* project-relative component path the SSR side passes through.
|
|
20
|
+
* `file` is the entry chunk, `imports` is the transitive set of
|
|
21
|
+
* shared chunks the browser needs to load BEFORE the entry runs
|
|
22
|
+
* — that's the modulepreload list.
|
|
23
|
+
*
|
|
24
|
+
* Paths in the manifest are relative to `.pylon/client-build/` so
|
|
25
|
+
* the Rust host can serve them under `/_pylon/build/<path>` with
|
|
26
|
+
* no rewriting.
|
|
27
|
+
*/
|
|
28
|
+
export interface PylonBundleManifest {
|
|
29
|
+
/** Build identity — bumps every successful build. */
|
|
30
|
+
build_id: string;
|
|
31
|
+
/** Output root, relative to cwd (always `.pylon/client-build`). */
|
|
32
|
+
outdir: string;
|
|
33
|
+
/** Public URL prefix the Rust host serves chunks under. */
|
|
34
|
+
public_prefix: string;
|
|
35
|
+
/** routeComponentPath → file + imports for that route. */
|
|
36
|
+
routes: Record<string, {
|
|
37
|
+
/** Per-route entry file, relative to outdir. */
|
|
38
|
+
file: string;
|
|
39
|
+
/** Transitive shared chunks to modulepreload, relative to outdir. */
|
|
40
|
+
imports: string[];
|
|
41
|
+
/** CSS chunks (Phase 1.5f). */
|
|
42
|
+
css: string[];
|
|
43
|
+
}>;
|
|
44
|
+
/** Self-hosted fonts (next/font parity): structured `@font-face`s + the
|
|
45
|
+
* `:root` CSS variables + the woff2 files to preload. Global (route-
|
|
46
|
+
* independent); rendered into every SSR `<head>` against `public_prefix`.
|
|
47
|
+
* Absent when the app declares no `font({...})`. */
|
|
48
|
+
fonts?: ManifestFonts;
|
|
49
|
+
}
|
|
50
|
+
/** Result of an in-process build — same shape the protocol returns. */
|
|
51
|
+
export interface BuildOutput {
|
|
52
|
+
manifestPath: string;
|
|
53
|
+
outdir: string;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Run the bundler in-process and return the manifest path + outdir.
|
|
57
|
+
* Used from `handleBundleClient` (protocol RPC path from Rust) AND
|
|
58
|
+
* from `getManifest` (in-process SSR path).
|
|
59
|
+
*/
|
|
60
|
+
export declare function buildClientBundle(appDirRel?: string): Promise<BuildOutput>;
|
|
61
|
+
/**
|
|
62
|
+
* Return the bundle manifest. If a fresh manifest exists on disk,
|
|
63
|
+
* use it (caching parse output across requests). Otherwise build
|
|
64
|
+
* the bundle in-process (deduped by `buildClientBundle`) and read.
|
|
65
|
+
*
|
|
66
|
+
* Called from `ssr-runtime.ts` per-request, so the disk-stat fast
|
|
67
|
+
* path matters. Bun's `fs.statSync` is a ~5µs syscall; cheap enough
|
|
68
|
+
* that we don't gate it on a flag.
|
|
69
|
+
*/
|
|
70
|
+
export declare function getManifest(): Promise<PylonBundleManifest>;
|
|
71
|
+
/**
|
|
72
|
+
* Protocol entry. Builds + responds. Rust calls this via the
|
|
73
|
+
* `bundle_client` RPC; on success the response carries both
|
|
74
|
+
* the manifest path (so Rust can load it if it wants, today it
|
|
75
|
+
* doesn't) and the outdir (so `/_pylon/build/<rel>` serves the
|
|
76
|
+
* right tree).
|
|
77
|
+
*/
|
|
78
|
+
export declare function handleBundleClient(msg: BundleClientMessage, send: Send): Promise<void>;
|
|
79
|
+
export {};
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/** One `@font-face` rule. A real face carries `src` (woff2 files); a generated
|
|
2
|
+
* fallback face carries `local` + the size-adjust/override metrics instead. */
|
|
3
|
+
export interface FontFace {
|
|
4
|
+
/** `font-family` — the real family, or `"<Family> Fallback"` for the
|
|
5
|
+
* size-adjusted fallback face. */
|
|
6
|
+
family: string;
|
|
7
|
+
/** outdir-relative woff2 filenames (real face). Empty for a fallback face. */
|
|
8
|
+
src: string[];
|
|
9
|
+
/** `local("...")` source for a size-adjusted fallback face, e.g. `"Arial"`. */
|
|
10
|
+
local?: string;
|
|
11
|
+
weight?: string;
|
|
12
|
+
style?: string;
|
|
13
|
+
display?: string;
|
|
14
|
+
unicodeRange?: string;
|
|
15
|
+
/** `size-adjust` percentage (fallback face only), e.g. `"107.40%"`. */
|
|
16
|
+
sizeAdjust?: string;
|
|
17
|
+
/** `ascent-override` percentage (fallback face only). */
|
|
18
|
+
ascentOverride?: string;
|
|
19
|
+
/** `descent-override` percentage (fallback face only). */
|
|
20
|
+
descentOverride?: string;
|
|
21
|
+
/** `line-gap-override` percentage (fallback face only). */
|
|
22
|
+
lineGapOverride?: string;
|
|
23
|
+
}
|
|
24
|
+
/** The bundle manifest's `fonts` block: structured faces + CSS variables +
|
|
25
|
+
* the woff2 files to `<link rel=preload>`. Rendered into every SSR `<head>`. */
|
|
26
|
+
export interface ManifestFonts {
|
|
27
|
+
faces: FontFace[];
|
|
28
|
+
/** CSS custom property → font-family stack, e.g.
|
|
29
|
+
* `{"--font-sans": '"Geist", "Geist Fallback", sans-serif'}`. */
|
|
30
|
+
variables: Record<string, string>;
|
|
31
|
+
/** outdir-relative woff2 filenames to preload. */
|
|
32
|
+
preload: string[];
|
|
33
|
+
}
|
|
34
|
+
interface SfntMetrics {
|
|
35
|
+
unitsPerEm: number;
|
|
36
|
+
ascent: number;
|
|
37
|
+
descent: number;
|
|
38
|
+
lineGap: number;
|
|
39
|
+
xAvgCharWidth: number;
|
|
40
|
+
}
|
|
41
|
+
/** A `ManifestFont` as it appears in `pylon.manifest.json` (snake_case wire
|
|
42
|
+
* shape — see the SDK `font()` helper). */
|
|
43
|
+
interface ManifestFontInput {
|
|
44
|
+
family: string;
|
|
45
|
+
variable: string;
|
|
46
|
+
weights?: string[];
|
|
47
|
+
styles?: string[];
|
|
48
|
+
subsets?: string[];
|
|
49
|
+
display?: string;
|
|
50
|
+
preload?: boolean;
|
|
51
|
+
fallback?: string[];
|
|
52
|
+
adjust_font_fallback?: boolean;
|
|
53
|
+
}
|
|
54
|
+
/** Decode the line-box metrics from a font buffer (woff2 or raw sfnt). Returns
|
|
55
|
+
* null when the format/tables can't be read — callers degrade gracefully. */
|
|
56
|
+
export declare function decodeFontMetrics(buf: Uint8Array): SfntMetrics | null;
|
|
57
|
+
/** Compute the size-adjusted fallback `@font-face` for a family. The web font's
|
|
58
|
+
* metrics, divided by the size-adjust factor, become the overrides — so the
|
|
59
|
+
* local fallback occupies the same line box + average advance as the web font
|
|
60
|
+
* will, eliminating the swap-in shift. */
|
|
61
|
+
export declare function computeFallbackFace(metrics: SfntMetrics, family: string, category: "sans-serif" | "serif"): FontFace;
|
|
62
|
+
/** Render the structured faces + `:root` variables into a CSS string. woff2 URLs
|
|
63
|
+
* are resolved against `prefix` (the live `public_prefix`) so they match
|
|
64
|
+
* wherever the assets are served (same-origin or CDN). */
|
|
65
|
+
export declare function renderFontFaceCss(fonts: ManifestFonts, prefix: string): string;
|
|
66
|
+
interface FontSpec {
|
|
67
|
+
family: string;
|
|
68
|
+
weights: string[];
|
|
69
|
+
styles: string[];
|
|
70
|
+
subsets: string[];
|
|
71
|
+
display: string;
|
|
72
|
+
}
|
|
73
|
+
interface FaceBlock {
|
|
74
|
+
subset: string;
|
|
75
|
+
style: string;
|
|
76
|
+
weight: string;
|
|
77
|
+
unicodeRange: string;
|
|
78
|
+
url: string;
|
|
79
|
+
}
|
|
80
|
+
/** Build the Google Fonts CSS2 API URL for a spec. */
|
|
81
|
+
export declare function buildGoogleFontsUrl(spec: FontSpec): string;
|
|
82
|
+
/** Parse a Google Fonts CSS2 response into per-subset face blocks, keeping only
|
|
83
|
+
* the requested subsets. Each `@font-face` is preceded by a subset comment in
|
|
84
|
+
* the API output. */
|
|
85
|
+
export declare function parseGoogleFontsCss(css: string, subsets: string[]): FaceBlock[];
|
|
86
|
+
/** Build all declared fonts: fetch + self-host + metrics + structured faces.
|
|
87
|
+
* Network/parse failures for a single family degrade to a variable-only entry
|
|
88
|
+
* (so `var(--font-x)` still resolves to the fallback stack) rather than killing
|
|
89
|
+
* the build. Returns null when nothing was produced. */
|
|
90
|
+
export declare function buildFonts(fs: any, path: any, cwd: string, outdir: string, fonts: ManifestFontInput[]): Promise<ManifestFonts | null>;
|
|
91
|
+
/** Read the `fonts` array from the app's `pylon.manifest.json` (written next to
|
|
92
|
+
* app.ts). Returns [] when absent/unreadable. */
|
|
93
|
+
export declare function readManifestFonts(fs: any, path: any, cwd: string): ManifestFontInput[];
|
|
94
|
+
export {};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/** Matches HandleFormMessage in crates/functions/src/protocol.rs. */
|
|
2
|
+
export interface HandleFormMessage {
|
|
3
|
+
type: "handle_form";
|
|
4
|
+
call_id: string;
|
|
5
|
+
component: string;
|
|
6
|
+
route_path: string;
|
|
7
|
+
method: string;
|
|
8
|
+
url: string;
|
|
9
|
+
params: Record<string, string>;
|
|
10
|
+
search_params: Record<string, string>;
|
|
11
|
+
form: Record<string, string | string[]>;
|
|
12
|
+
headers: Record<string, string>;
|
|
13
|
+
cookies: Record<string, string>;
|
|
14
|
+
auth: {
|
|
15
|
+
user_id: string | null;
|
|
16
|
+
is_admin: boolean;
|
|
17
|
+
tenant_id: string | null;
|
|
18
|
+
roles: string[];
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
type Send = (msg: Record<string, unknown>) => void;
|
|
22
|
+
/** Parsed form fields. Mirrors URLSearchParams get/getAll/has semantics. */
|
|
23
|
+
export interface FormFields {
|
|
24
|
+
/** First value for `name`, or null. */
|
|
25
|
+
get(name: string): string | null;
|
|
26
|
+
/** All values for `name` (empty array if none). */
|
|
27
|
+
getAll(name: string): string[];
|
|
28
|
+
has(name: string): boolean;
|
|
29
|
+
/** Raw map: name → value | values. */
|
|
30
|
+
readonly fields: Record<string, string | string[]>;
|
|
31
|
+
}
|
|
32
|
+
export declare function handleForm(msg: HandleFormMessage, send: Send): Promise<void>;
|
|
33
|
+
export {};
|