@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
|
@@ -0,0 +1,419 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Is the runtime in dev mode? MUST match the Rust host's `is_dev_mode()`
|
|
3
|
+
* (crates/runtime/src/frontend.rs): `PYLON_DEV_MODE` is on ONLY for the exact
|
|
4
|
+
* strings "1" or "true" (case-insensitive). A bare `if (process.env.PYLON_DEV_MODE)`
|
|
5
|
+
* is WRONG — the string "false"/"0" is truthy in JS, so an explicit
|
|
6
|
+
* `PYLON_DEV_MODE=false` on a PROD machine would wrongly enable dev behavior
|
|
7
|
+
* (e.g. the live-reload `<script>` was being injected into prod pages, whose
|
|
8
|
+
* EventSource then 404-retried `/_pylon/dev/live` forever).
|
|
9
|
+
*/
|
|
10
|
+
export declare function isDevMode(): boolean;
|
|
11
|
+
/**
|
|
12
|
+
* The message payload the host sends. Matches RenderRouteMessage in
|
|
13
|
+
* crates/functions/src/protocol.rs.
|
|
14
|
+
*/
|
|
15
|
+
export interface RenderRouteMessage {
|
|
16
|
+
type: "render_route";
|
|
17
|
+
call_id: string;
|
|
18
|
+
/**
|
|
19
|
+
* Project-relative module path (e.g. "app/hello/page"). The
|
|
20
|
+
* adapter joins cwd + this + the right extension (.tsx → .ts).
|
|
21
|
+
*/
|
|
22
|
+
component: string;
|
|
23
|
+
/**
|
|
24
|
+
* Project-relative module paths for the layout chain, walked
|
|
25
|
+
* root → leaf. Each layout's default export wraps the next as
|
|
26
|
+
* `children`. Absent / empty when no layouts apply.
|
|
27
|
+
*
|
|
28
|
+
* Example:
|
|
29
|
+
* layouts: ["app/layout", "app/blog/layout"]
|
|
30
|
+
* component: "app/blog/[slug]/page"
|
|
31
|
+
*
|
|
32
|
+
* Resolves to:
|
|
33
|
+
* <RootLayout>
|
|
34
|
+
* <BlogLayout>
|
|
35
|
+
* <Page {...props} />
|
|
36
|
+
* </BlogLayout>
|
|
37
|
+
* </RootLayout>
|
|
38
|
+
*/
|
|
39
|
+
layouts?: string[];
|
|
40
|
+
/** The matched route pattern (e.g. `/blog/:slug`). */
|
|
41
|
+
route_path: string;
|
|
42
|
+
/** The incoming URL path (e.g. `/blog/hello-world`). */
|
|
43
|
+
url: string;
|
|
44
|
+
/** Dynamic-segment matches keyed by name (e.g. `{slug: "hello-world"}`). */
|
|
45
|
+
params: Record<string, string>;
|
|
46
|
+
/** Parsed query string. */
|
|
47
|
+
search_params: Record<string, string>;
|
|
48
|
+
/** Lowercased header names → values. */
|
|
49
|
+
headers: Record<string, string>;
|
|
50
|
+
/** Parsed cookies. */
|
|
51
|
+
cookies: Record<string, string>;
|
|
52
|
+
/** Pylon auth context. */
|
|
53
|
+
auth: {
|
|
54
|
+
user_id: string | null;
|
|
55
|
+
is_admin: boolean;
|
|
56
|
+
tenant_id: string | null;
|
|
57
|
+
roles: string[];
|
|
58
|
+
};
|
|
59
|
+
/**
|
|
60
|
+
* Initial HTTP status the response controller starts at (default 200).
|
|
61
|
+
* The host sets this to 404 when dispatching a `not-found.tsx` render
|
|
62
|
+
* for an unmatched URL, so the boundary streams at 404 without the
|
|
63
|
+
* component having to call `response.setStatus`. A page can still
|
|
64
|
+
* override it via `response.setStatus`.
|
|
65
|
+
*/
|
|
66
|
+
initial_status?: number;
|
|
67
|
+
}
|
|
68
|
+
type Send = (msg: Record<string, unknown>) => void;
|
|
69
|
+
/**
|
|
70
|
+
* Control-flow signal a page or layout throws to short-circuit the
|
|
71
|
+
* render: `response.redirect(url)` / `response.notFound()`. The adapter
|
|
72
|
+
* catches it and turns it into a 3xx + Location or a 404 instead of a
|
|
73
|
+
* normal 200 body. Extends Error so React's stream rejects cleanly when
|
|
74
|
+
* it's thrown during the shell render. (Throw it OUTSIDE an error
|
|
75
|
+
* boundary — an enclosing boundary would swallow the signal.)
|
|
76
|
+
*/
|
|
77
|
+
export declare class PylonRouteControl extends Error {
|
|
78
|
+
kind: "redirect" | "notFound";
|
|
79
|
+
url?: string;
|
|
80
|
+
redirectStatus?: number;
|
|
81
|
+
constructor(kind: "redirect" | "notFound");
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Normalize a thrown value into a route-control signal: either the framework's
|
|
85
|
+
* own `PylonRouteControl` (`response.redirect()` / `response.notFound()`) or a
|
|
86
|
+
* branded `NotFoundError` thrown by `@pylonsync/react`'s `notFound()` from a
|
|
87
|
+
* page/layout render. Returns `null` for an ordinary error so the caller falls
|
|
88
|
+
* through to its real error-handling path.
|
|
89
|
+
*/
|
|
90
|
+
export declare function asRouteControl(err: unknown): PylonRouteControl | null;
|
|
91
|
+
export interface SsrCookieOptions {
|
|
92
|
+
path?: string;
|
|
93
|
+
domain?: string;
|
|
94
|
+
maxAge?: number;
|
|
95
|
+
expires?: Date | string;
|
|
96
|
+
/** Defaults to true (secure default). Pass false for a client-readable cookie. */
|
|
97
|
+
httpOnly?: boolean;
|
|
98
|
+
secure?: boolean;
|
|
99
|
+
/** Defaults to "lax". */
|
|
100
|
+
sameSite?: "strict" | "lax" | "none";
|
|
101
|
+
}
|
|
102
|
+
export interface ResponseState {
|
|
103
|
+
status: number;
|
|
104
|
+
headers: Record<string, string>;
|
|
105
|
+
cookies: string[];
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* The per-render `response` controller handed to every page + layout in
|
|
109
|
+
* props. Pylon already has a backend for data/mutations, so SSR's job is
|
|
110
|
+
* just the response envelope: status, redirects, 404, and the occasional
|
|
111
|
+
* Set-Cookie.
|
|
112
|
+
*
|
|
113
|
+
* IMPORTANT — call these during the SYNCHRONOUS shell render (the
|
|
114
|
+
* component body, before any `await` / Suspense boundary). The HTTP head
|
|
115
|
+
* is committed when the shell is ready; status/headers/cookies set from a
|
|
116
|
+
* suspended subtree that streams in later are lost, and a redirect()/
|
|
117
|
+
* notFound() thrown below a Suspense boundary is caught by React's error
|
|
118
|
+
* handling rather than turned into a 3xx/404 (same constraint as Next's
|
|
119
|
+
* streaming SSR). Per render, not shared across requests.
|
|
120
|
+
*/
|
|
121
|
+
export interface SsrResponse {
|
|
122
|
+
/** Set the HTTP status (100–599). Default 200. */
|
|
123
|
+
setStatus(code: number): void;
|
|
124
|
+
/** Set a response header (name must be a token; value CR/LF/NUL-free). */
|
|
125
|
+
setHeader(name: string, value: string): void;
|
|
126
|
+
/** Append a Set-Cookie. Defaults: HttpOnly + SameSite=Lax. */
|
|
127
|
+
setCookie(name: string, value: string, opts?: SsrCookieOptions): void;
|
|
128
|
+
/** Throw to send a 3xx (default 307) + Location, no body. Shell-render only. */
|
|
129
|
+
redirect(url: string, status?: number): never;
|
|
130
|
+
/**
|
|
131
|
+
* Throw to send a 404. Renders the nearest `not-found.tsx` (walking up
|
|
132
|
+
* from the page's directory, wrapped in the route's layout chain), or a
|
|
133
|
+
* minimal framework body if none is defined. Shell-render only — a throw
|
|
134
|
+
* below a Suspense boundary is swallowed by React.
|
|
135
|
+
*/
|
|
136
|
+
notFound(): never;
|
|
137
|
+
}
|
|
138
|
+
export declare function makeResponseController(state: ResponseState, defaultRedirectStatus?: number): SsrResponse;
|
|
139
|
+
/**
|
|
140
|
+
* Merge page-set headers + cookies into the response_start header map.
|
|
141
|
+
* Cookies are newline-joined under `set-cookie`; the host splits them
|
|
142
|
+
* into one `Set-Cookie` header each (newline is forbidden inside a
|
|
143
|
+
* cookie, so it can't be turned into header injection).
|
|
144
|
+
*/
|
|
145
|
+
export declare function finalizeHeaders(state: ResponseState, extra?: Record<string, string>): Record<string, string>;
|
|
146
|
+
/**
|
|
147
|
+
* Phase 1 SSR handler. Resolves the component, renders it via
|
|
148
|
+
* react-dom/server.renderToReadableStream, pumps chunks back to the
|
|
149
|
+
* host as base64-encoded NDJSON.
|
|
150
|
+
*
|
|
151
|
+
* Errors fall back to a type:"error" frame so the host can return a
|
|
152
|
+
* 500 with the error body. Mid-stream errors (after the first chunk
|
|
153
|
+
* has flushed) are uncatchable here — React's `onError` would have
|
|
154
|
+
* to feed into a separate signal, deferred to Phase 1.5.
|
|
155
|
+
*/
|
|
156
|
+
/**
|
|
157
|
+
* Page SEO metadata. A page exports `export const metadata = {...}`
|
|
158
|
+
* (static) or `export async function generateMetadata(props)` (dynamic,
|
|
159
|
+
* e.g. param-derived titles). Kept flat — no deep nesting beyond og/twitter.
|
|
160
|
+
*
|
|
161
|
+
* React 19 hoists the resulting <title>/<meta>/<link> into <head>. A page
|
|
162
|
+
* `title` overrides a layout's static `<title>` (both render; the browser
|
|
163
|
+
* uses the last, which is the page's). React does NOT dedupe arbitrary
|
|
164
|
+
* `<meta>`, so set `description`/OG in EITHER the layout OR page metadata,
|
|
165
|
+
* not both, to avoid duplicate tags.
|
|
166
|
+
*/
|
|
167
|
+
/** A single OpenGraph image (for `openGraph.images` — multiple images). */
|
|
168
|
+
export interface OgImage {
|
|
169
|
+
url: string;
|
|
170
|
+
secureUrl?: string;
|
|
171
|
+
type?: string;
|
|
172
|
+
width?: number;
|
|
173
|
+
height?: number;
|
|
174
|
+
alt?: string;
|
|
175
|
+
}
|
|
176
|
+
export interface SsrMetadata {
|
|
177
|
+
title?: string;
|
|
178
|
+
description?: string;
|
|
179
|
+
keywords?: string | string[];
|
|
180
|
+
canonical?: string;
|
|
181
|
+
robots?: string;
|
|
182
|
+
/** `<meta name="author">` — one tag per author. */
|
|
183
|
+
authors?: string | string[];
|
|
184
|
+
/** `<meta name="theme-color">` — browser UI tint for the page. */
|
|
185
|
+
themeColor?: string;
|
|
186
|
+
openGraph?: {
|
|
187
|
+
title?: string;
|
|
188
|
+
description?: string;
|
|
189
|
+
image?: string;
|
|
190
|
+
/** `og:image:secure_url` — set automatically to the https image URL. */
|
|
191
|
+
imageSecureUrl?: string;
|
|
192
|
+
/** `og:image:type` (e.g. "image/png"). */
|
|
193
|
+
imageType?: string;
|
|
194
|
+
/** `og:image:width` / `og:image:height` in pixels. */
|
|
195
|
+
imageWidth?: number;
|
|
196
|
+
imageHeight?: number;
|
|
197
|
+
imageAlt?: string;
|
|
198
|
+
/** Additional images beyond the primary `image` (each emits its own
|
|
199
|
+
* `og:image` + dimensions). Provide absolute URLs. */
|
|
200
|
+
images?: OgImage[];
|
|
201
|
+
url?: string;
|
|
202
|
+
type?: string;
|
|
203
|
+
/** `og:locale` (e.g. "en_US"). */
|
|
204
|
+
locale?: string;
|
|
205
|
+
/** `og:site_name` — the brand the page belongs to (e.g. "Pylon").
|
|
206
|
+
* Discord and other unfurlers show this above the title. */
|
|
207
|
+
siteName?: string;
|
|
208
|
+
/** `article:*` tags for `og:type=article` pages. */
|
|
209
|
+
article?: {
|
|
210
|
+
author?: string | string[];
|
|
211
|
+
publishedTime?: string;
|
|
212
|
+
modifiedTime?: string;
|
|
213
|
+
section?: string;
|
|
214
|
+
tags?: string | string[];
|
|
215
|
+
};
|
|
216
|
+
};
|
|
217
|
+
twitter?: {
|
|
218
|
+
card?: string;
|
|
219
|
+
title?: string;
|
|
220
|
+
description?: string;
|
|
221
|
+
image?: string;
|
|
222
|
+
/** `twitter:site` / `twitter:creator` — @handles. */
|
|
223
|
+
site?: string;
|
|
224
|
+
creator?: string;
|
|
225
|
+
/** `twitter:image:alt` — alt text for the card image. */
|
|
226
|
+
imageAlt?: string;
|
|
227
|
+
};
|
|
228
|
+
/** `<link rel="icon">` / `<link rel="apple-touch-icon">`. Auto-wired
|
|
229
|
+
* from the app/icon.* + app/apple-icon.* + app/favicon.ico file
|
|
230
|
+
* conventions, or set explicitly. */
|
|
231
|
+
icons?: {
|
|
232
|
+
icon?: {
|
|
233
|
+
url: string;
|
|
234
|
+
type?: string;
|
|
235
|
+
sizes?: string;
|
|
236
|
+
};
|
|
237
|
+
apple?: {
|
|
238
|
+
url: string;
|
|
239
|
+
type?: string;
|
|
240
|
+
sizes?: string;
|
|
241
|
+
};
|
|
242
|
+
};
|
|
243
|
+
/** Alternate URLs. `languages` emits `<link rel="alternate" hreflang>`
|
|
244
|
+
* per locale; `canonical` is an alias for the top-level `canonical`. */
|
|
245
|
+
alternates?: {
|
|
246
|
+
canonical?: string;
|
|
247
|
+
languages?: Record<string, string>;
|
|
248
|
+
};
|
|
249
|
+
/** Structured data, emitted as `<script type="application/ld+json">`.
|
|
250
|
+
* One object or an array (each item gets its own script). Serialized with
|
|
251
|
+
* `<` escaped so values can't break out of the script element. */
|
|
252
|
+
jsonLd?: Record<string, unknown> | Record<string, unknown>[];
|
|
253
|
+
}
|
|
254
|
+
/**
|
|
255
|
+
* Build a React fragment of <title>/<meta>/<link> from a page's metadata.
|
|
256
|
+
* React 19 auto-hoists these into <head> wherever they render, and the
|
|
257
|
+
* host's </head> splice preserves them. React escapes all text/attrs, so
|
|
258
|
+
* there's no manual XSS handling. Returns null when there's nothing to emit.
|
|
259
|
+
*/
|
|
260
|
+
export declare function renderMetadata(React: any, m: SsrMetadata | undefined): any;
|
|
261
|
+
/** Import a project-relative module, trying each common extension. */
|
|
262
|
+
export declare function importModule(cwd: string, relPath: string): Promise<any>;
|
|
263
|
+
/** Pure origin resolution (exported for tests).
|
|
264
|
+
*
|
|
265
|
+
* SECURITY: the request `Host` (and `X-Forwarded-Proto`) is attacker-
|
|
266
|
+
* controlled. It's only trusted to build the absolute origin baked into
|
|
267
|
+
* `og:image` / canonical URLs when it's in the allowlist — the configured
|
|
268
|
+
* public/canonical host, an explicit `PYLON_TRUSTED_HOSTS` entry, or
|
|
269
|
+
* loopback. An untrusted (or absent) Host falls back to the configured
|
|
270
|
+
* public origin. Without this, `Host: evil.com` on a cacheable
|
|
271
|
+
* (force-static / `revalidate`) render bakes `https://evil.com/_pylon/og…`
|
|
272
|
+
* into the HTML, which is then teed into the shared ISR/CDN cache and
|
|
273
|
+
* served to every subsequent visitor (cache poisoning). */
|
|
274
|
+
export declare function resolveOrigin(opts: {
|
|
275
|
+
host?: string;
|
|
276
|
+
forwardedProto?: string;
|
|
277
|
+
publicUrl?: string;
|
|
278
|
+
canonicalHost?: string;
|
|
279
|
+
trustedHostsCsv?: string;
|
|
280
|
+
}): string;
|
|
281
|
+
/** SECURITY: validate a `response.redirect()` target to prevent OPEN REDIRECTS.
|
|
282
|
+
* Mirrors the OAuth-layer `validate_trusted_redirect` (crates/auth). A relative
|
|
283
|
+
* same-site path is always allowed; an absolute URL only when it's http(s) to a
|
|
284
|
+
* trusted host — the app's public/canonical origin, a `PYLON_TRUSTED_HOSTS`
|
|
285
|
+
* entry, or loopback. Everything else is rejected: protocol-relative
|
|
286
|
+
* `//evil.com`, backslash tricks (`/\evil.com` — browsers normalize `\`→`/`),
|
|
287
|
+
* other-origin absolutes, and `javascript:`/`data:` schemes. So the natural
|
|
288
|
+
* `response.redirect(searchParams.get("next"))` can't be turned into an
|
|
289
|
+
* off-site redirect by attacker-supplied input. Exported for tests. */
|
|
290
|
+
export declare function isSafeRedirect(url: string, opts: {
|
|
291
|
+
publicUrl?: string;
|
|
292
|
+
canonicalHost?: string;
|
|
293
|
+
trustedHostsCsv?: string;
|
|
294
|
+
}): boolean;
|
|
295
|
+
/** Merge auto-discovered favicons (icon.* / apple-icon.* / favicon.ico)
|
|
296
|
+
* into a page's metadata. Explicit `metadata.icons.*` wins. */
|
|
297
|
+
export declare function applyAutoIcons(component: string, metadata: SsrMetadata | undefined): SsrMetadata | undefined;
|
|
298
|
+
export declare function applyAutoSocialImages(component: string, headers: Record<string, string> | undefined, metadata: SsrMetadata | undefined): SsrMetadata | undefined;
|
|
299
|
+
/**
|
|
300
|
+
* Build the hydration tail appended after React's stream EOFs: the
|
|
301
|
+
* `__PYLON_DATA__` JSON blob (props + ssrData) + the per-route entry
|
|
302
|
+
* `<script>` that hydrates it, + (dev) the live-reload snippet. Shared by the
|
|
303
|
+
* page render AND the now-hydrated boundary render (#279) so a boundary
|
|
304
|
+
* hydrates through the EXACT same path as a page.
|
|
305
|
+
*
|
|
306
|
+
* `kind` marks an error/not-found boundary so the client knows whether to
|
|
307
|
+
* synthesize a `reset()`. For an error boundary, `errorForClient` is the SAFE
|
|
308
|
+
* projection ({message, digest}) — the raw `Error` (and its stack) is NEVER
|
|
309
|
+
* serialized (the dev overlay owns dev stacks; preserves the #270 posture).
|
|
310
|
+
*/
|
|
311
|
+
export declare function buildHydrationTail(args: {
|
|
312
|
+
component: string;
|
|
313
|
+
layouts: string[];
|
|
314
|
+
props: any;
|
|
315
|
+
ssrData: Record<string, any>;
|
|
316
|
+
manifestRoute: {
|
|
317
|
+
file: string;
|
|
318
|
+
imports: string[];
|
|
319
|
+
css: string[];
|
|
320
|
+
} | null;
|
|
321
|
+
publicPrefix: string;
|
|
322
|
+
manifestErr: string | null;
|
|
323
|
+
kind?: "error" | "not-found";
|
|
324
|
+
errorForClient?: {
|
|
325
|
+
message: string;
|
|
326
|
+
digest?: string;
|
|
327
|
+
};
|
|
328
|
+
}): string;
|
|
329
|
+
/**
|
|
330
|
+
* A short, non-reversible correlation id for an error — surfaced to the
|
|
331
|
+
* client error boundary as `error.digest` (matching server logs) WITHOUT
|
|
332
|
+
* carrying any stack content. FNV-1a over message+stack, 8 hex chars.
|
|
333
|
+
*/
|
|
334
|
+
export declare function errorDigest(err: any): string;
|
|
335
|
+
/**
|
|
336
|
+
* #278: does this route STREAM (vs buffer the whole document)? Streaming is
|
|
337
|
+
* opt-in: a `loading.tsx` (route-level Suspense) or `export const streaming =
|
|
338
|
+
* true` (inner-boundary). Pure for testing.
|
|
339
|
+
*/
|
|
340
|
+
export declare function computeWantsStream(hasLoading: boolean, mod: any): boolean;
|
|
341
|
+
/**
|
|
342
|
+
* #277: how long an opt-in page stays cacheable, in seconds — or null if it
|
|
343
|
+
* never opted in. `export const revalidate = N` (N>0) → N; `dynamic:
|
|
344
|
+
* "force-static"` → a year (only a deploy invalidates); else null. Pure.
|
|
345
|
+
*/
|
|
346
|
+
export declare function computeRevalidateSecs(mod: any): number | null;
|
|
347
|
+
/**
|
|
348
|
+
* #277 cache verdict — the security-critical predicate, extracted pure so the
|
|
349
|
+
* leak class (a personalized/streaming render marked cacheable) is a TEST, not
|
|
350
|
+
* a mental walkthrough. INVARIANT: result ⟹ !wantsStream (a streaming render
|
|
351
|
+
* commits its head before auth/cookies/status are final, so it can never be
|
|
352
|
+
* cached). Fail-closed: every condition must hold.
|
|
353
|
+
*/
|
|
354
|
+
export declare function computeCacheVerdict(args: {
|
|
355
|
+
revalidateSecs: number | null;
|
|
356
|
+
forceDynamic: boolean;
|
|
357
|
+
authTouched: boolean;
|
|
358
|
+
cookieCount: number;
|
|
359
|
+
strictPolicies: boolean;
|
|
360
|
+
wantsStream: boolean;
|
|
361
|
+
status: number;
|
|
362
|
+
}): boolean;
|
|
363
|
+
/**
|
|
364
|
+
* #278: diff the response head committed at `response_start` against the final
|
|
365
|
+
* state after EOF, to catch a late response.* mutation from a suspended subtree
|
|
366
|
+
* that the already-sent head couldn't carry. Returns the dropped pieces, or
|
|
367
|
+
* null if nothing was lost. Pure.
|
|
368
|
+
*/
|
|
369
|
+
export declare function diffCommittedResponse(snapshot: {
|
|
370
|
+
status: number;
|
|
371
|
+
cookies: string[];
|
|
372
|
+
headerKeys: string[];
|
|
373
|
+
}, final: {
|
|
374
|
+
status: number;
|
|
375
|
+
cookies: string[];
|
|
376
|
+
headers: Record<string, string>;
|
|
377
|
+
}): {
|
|
378
|
+
droppedCookies: string[];
|
|
379
|
+
statusChanged: boolean;
|
|
380
|
+
newHeaderKeys: string[];
|
|
381
|
+
} | null;
|
|
382
|
+
/** One URL entry in a sitemap (mirrors Next's `MetadataRoute.Sitemap[number]`). */
|
|
383
|
+
export interface SitemapEntry {
|
|
384
|
+
url: string;
|
|
385
|
+
lastModified?: string | Date;
|
|
386
|
+
changeFrequency?: "always" | "hourly" | "daily" | "weekly" | "monthly" | "yearly" | "never";
|
|
387
|
+
priority?: number;
|
|
388
|
+
/** hreflang alternates, e.g. `{ languages: { "en-US": "https://…/en" } }`. */
|
|
389
|
+
alternates?: {
|
|
390
|
+
languages?: Record<string, string>;
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
/** Return type of a default export in `app/sitemap.ts`. */
|
|
394
|
+
export type Sitemap = SitemapEntry[];
|
|
395
|
+
export interface RobotsRule {
|
|
396
|
+
userAgent?: string | string[];
|
|
397
|
+
allow?: string | string[];
|
|
398
|
+
disallow?: string | string[];
|
|
399
|
+
crawlDelay?: number;
|
|
400
|
+
}
|
|
401
|
+
/** Return type of a default export in `app/robots.ts`. */
|
|
402
|
+
export interface Robots {
|
|
403
|
+
rules: RobotsRule | RobotsRule[];
|
|
404
|
+
sitemap?: string | string[];
|
|
405
|
+
host?: string;
|
|
406
|
+
}
|
|
407
|
+
/** Serialize sitemap entries to a sitemaps.org 0.9 XML document. */
|
|
408
|
+
export declare function serializeSitemap(entries: Sitemap | undefined): string;
|
|
409
|
+
/** Serialize a robots config to robots.txt text. */
|
|
410
|
+
export declare function serializeRobots(robots: Robots | undefined): string;
|
|
411
|
+
/**
|
|
412
|
+
* Import app/sitemap or app/robots, run its default export, serialize the
|
|
413
|
+
* result, and stream it back with the right content-type. Errors surface as a
|
|
414
|
+
* 500 with a short plain-text message (so a broken sitemap doesn't wedge the
|
|
415
|
+
* runner). 1-hour cache — sitemaps/robots change rarely; tune via a CDN.
|
|
416
|
+
*/
|
|
417
|
+
export declare function handleDataRoute(msg: RenderRouteMessage, kind: "sitemap" | "robots", send: Send): Promise<void>;
|
|
418
|
+
export declare function handleRenderRoute(msg: RenderRouteMessage, send: Send): Promise<void>;
|
|
419
|
+
export {};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Test-side helpers for `pylon test`.
|
|
3
|
+
*
|
|
4
|
+
* The CLI already starts each test FILE with a fresh in-memory database
|
|
5
|
+
* (`PYLON_IN_MEMORY=1`), so tests across files never cross-contaminate.
|
|
6
|
+
* This module covers the finer-grained case: isolating individual
|
|
7
|
+
* `test(...)` blocks within a single file.
|
|
8
|
+
*
|
|
9
|
+
* Two integration patterns are supported:
|
|
10
|
+
*
|
|
11
|
+
* 1. **Manual** — call `resetDb()` from a `beforeEach` hook.
|
|
12
|
+
* 2. **Automatic** — call `installTestIsolation()` at the top of the file
|
|
13
|
+
* and every `test()` block runs with a reset store.
|
|
14
|
+
*
|
|
15
|
+
* Both require the test file to run under `pylon test` (not raw
|
|
16
|
+
* `bun test`), because resetDb talks to the server via HTTP.
|
|
17
|
+
*/
|
|
18
|
+
/**
|
|
19
|
+
* Reset the in-memory database to empty. Returns when the server confirms.
|
|
20
|
+
*
|
|
21
|
+
* Only works when the server is running in in-memory dev mode — production
|
|
22
|
+
* deployments refuse this call. Safe to no-op when the reset endpoint is
|
|
23
|
+
* unreachable so tests using this helper still work under raw `bun test`
|
|
24
|
+
* (they just won't reset between cases).
|
|
25
|
+
*/
|
|
26
|
+
export declare function resetDb(baseUrl?: string): Promise<void>;
|
|
27
|
+
/**
|
|
28
|
+
* Bun-friendly `beforeEach(resetDb)` installer. Looks up Bun's global
|
|
29
|
+
* `beforeEach` via `globalThis`; no-ops under other runners.
|
|
30
|
+
*/
|
|
31
|
+
export declare function installTestIsolation(baseUrl?: string): void;
|