@timber-js/app 0.1.0 → 0.1.2
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/index.js +5 -2
- package/dist/index.js.map +1 -1
- package/dist/plugins/entries.d.ts.map +1 -1
- package/package.json +43 -58
- package/src/adapters/cloudflare.ts +325 -0
- package/src/adapters/nitro.ts +366 -0
- package/src/adapters/types.ts +63 -0
- package/src/cache/index.ts +91 -0
- package/src/cache/redis-handler.ts +91 -0
- package/src/cache/register-cached-function.ts +99 -0
- package/src/cache/singleflight.ts +26 -0
- package/src/cache/stable-stringify.ts +21 -0
- package/src/cache/timber-cache.ts +116 -0
- package/src/cli.ts +201 -0
- package/src/client/browser-entry.ts +663 -0
- package/src/client/error-boundary.tsx +209 -0
- package/src/client/form.tsx +200 -0
- package/src/client/head.ts +61 -0
- package/src/client/history.ts +46 -0
- package/src/client/index.ts +60 -0
- package/src/client/link-navigate-interceptor.tsx +62 -0
- package/src/client/link-status-provider.tsx +40 -0
- package/src/client/link.tsx +310 -0
- package/src/client/nuqs-adapter.tsx +117 -0
- package/src/client/router-ref.ts +25 -0
- package/src/client/router.ts +563 -0
- package/src/client/segment-cache.ts +194 -0
- package/src/client/segment-context.ts +57 -0
- package/src/client/ssr-data.ts +95 -0
- package/src/client/types.ts +4 -0
- package/src/client/unload-guard.ts +34 -0
- package/src/client/use-cookie.ts +122 -0
- package/src/client/use-link-status.ts +46 -0
- package/src/client/use-navigation-pending.ts +47 -0
- package/src/client/use-params.ts +71 -0
- package/src/client/use-pathname.ts +43 -0
- package/src/client/use-query-states.ts +133 -0
- package/src/client/use-router.ts +77 -0
- package/src/client/use-search-params.ts +74 -0
- package/src/client/use-selected-layout-segment.ts +110 -0
- package/src/content/index.ts +13 -0
- package/src/cookies/define-cookie.ts +137 -0
- package/src/cookies/index.ts +9 -0
- package/src/fonts/ast.ts +359 -0
- package/src/fonts/css.ts +68 -0
- package/src/fonts/fallbacks.ts +248 -0
- package/src/fonts/google.ts +332 -0
- package/src/fonts/local.ts +177 -0
- package/src/fonts/types.ts +88 -0
- package/src/index.ts +413 -0
- package/src/plugins/adapter-build.ts +118 -0
- package/src/plugins/build-manifest.ts +323 -0
- package/src/plugins/build-report.ts +353 -0
- package/src/plugins/cache-transform.ts +199 -0
- package/src/plugins/chunks.ts +90 -0
- package/src/plugins/content.ts +136 -0
- package/src/plugins/dev-error-overlay.ts +230 -0
- package/src/plugins/dev-logs.ts +280 -0
- package/src/plugins/dev-server.ts +389 -0
- package/src/plugins/dynamic-transform.ts +161 -0
- package/src/plugins/entries.ts +207 -0
- package/src/plugins/fonts.ts +581 -0
- package/src/plugins/mdx.ts +179 -0
- package/src/plugins/react-prod.ts +56 -0
- package/src/plugins/routing.ts +419 -0
- package/src/plugins/server-action-exports.ts +220 -0
- package/src/plugins/server-bundle.ts +113 -0
- package/src/plugins/shims.ts +168 -0
- package/src/plugins/static-build.ts +207 -0
- package/src/routing/codegen.ts +396 -0
- package/src/routing/index.ts +14 -0
- package/src/routing/interception.ts +173 -0
- package/src/routing/scanner.ts +487 -0
- package/src/routing/status-file-lint.ts +114 -0
- package/src/routing/types.ts +100 -0
- package/src/search-params/analyze.ts +192 -0
- package/src/search-params/codecs.ts +153 -0
- package/src/search-params/create.ts +314 -0
- package/src/search-params/index.ts +23 -0
- package/src/search-params/registry.ts +31 -0
- package/src/server/access-gate.tsx +142 -0
- package/src/server/action-client.ts +473 -0
- package/src/server/action-handler.ts +325 -0
- package/src/server/actions.ts +236 -0
- package/src/server/asset-headers.ts +81 -0
- package/src/server/body-limits.ts +102 -0
- package/src/server/build-manifest.ts +234 -0
- package/src/server/canonicalize.ts +90 -0
- package/src/server/client-module-map.ts +58 -0
- package/src/server/csrf.ts +79 -0
- package/src/server/deny-renderer.ts +302 -0
- package/src/server/dev-logger.ts +419 -0
- package/src/server/dev-span-processor.ts +78 -0
- package/src/server/dev-warnings.ts +282 -0
- package/src/server/early-hints-sender.ts +55 -0
- package/src/server/early-hints.ts +142 -0
- package/src/server/error-boundary-wrapper.ts +69 -0
- package/src/server/error-formatter.ts +184 -0
- package/src/server/flush.ts +182 -0
- package/src/server/form-data.ts +176 -0
- package/src/server/form-flash.ts +93 -0
- package/src/server/html-injectors.ts +445 -0
- package/src/server/index.ts +222 -0
- package/src/server/instrumentation.ts +136 -0
- package/src/server/logger.ts +145 -0
- package/src/server/manifest-status-resolver.ts +215 -0
- package/src/server/metadata-render.ts +527 -0
- package/src/server/metadata-routes.ts +189 -0
- package/src/server/metadata.ts +263 -0
- package/src/server/middleware-runner.ts +32 -0
- package/src/server/nuqs-ssr-provider.tsx +63 -0
- package/src/server/pipeline.ts +555 -0
- package/src/server/prerender.ts +139 -0
- package/src/server/primitives.ts +264 -0
- package/src/server/proxy.ts +43 -0
- package/src/server/request-context.ts +554 -0
- package/src/server/route-element-builder.ts +395 -0
- package/src/server/route-handler.ts +153 -0
- package/src/server/route-matcher.ts +316 -0
- package/src/server/rsc-entry/api-handler.ts +112 -0
- package/src/server/rsc-entry/error-renderer.ts +177 -0
- package/src/server/rsc-entry/helpers.ts +147 -0
- package/src/server/rsc-entry/index.ts +688 -0
- package/src/server/rsc-entry/ssr-bridge.ts +18 -0
- package/src/server/slot-resolver.ts +359 -0
- package/src/server/ssr-entry.ts +161 -0
- package/src/server/ssr-render.ts +200 -0
- package/src/server/status-code-resolver.ts +282 -0
- package/src/server/tracing.ts +281 -0
- package/src/server/tree-builder.ts +354 -0
- package/src/server/types.ts +150 -0
- package/src/shims/font-google.ts +67 -0
- package/src/shims/headers.ts +11 -0
- package/src/shims/image.ts +48 -0
- package/src/shims/link.ts +9 -0
- package/src/shims/navigation-client.ts +52 -0
- package/src/shims/navigation.ts +31 -0
- package/src/shims/server-only-noop.js +5 -0
- package/src/utils/directive-parser.ts +529 -0
- package/src/utils/format.ts +10 -0
- package/src/utils/startup-timer.ts +102 -0
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
// Server-side primitives: deny, redirect, redirectExternal, RenderError, waitUntil
|
|
2
|
+
//
|
|
3
|
+
// These are the core runtime signals that components, middleware, and access gates
|
|
4
|
+
// use to control request flow. See design/10-error-handling.md.
|
|
5
|
+
|
|
6
|
+
// ─── DenySignal ─────────────────────────────────────────────────────────────
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Render-phase signal thrown by `deny()`. Caught by the framework to produce
|
|
10
|
+
* the correct HTTP status code (segment context) or graceful degradation (slot context).
|
|
11
|
+
*/
|
|
12
|
+
export class DenySignal extends Error {
|
|
13
|
+
readonly status: number;
|
|
14
|
+
readonly data: unknown;
|
|
15
|
+
|
|
16
|
+
constructor(status: number, data?: unknown) {
|
|
17
|
+
super(`Access denied with status ${status}`);
|
|
18
|
+
this.name = 'DenySignal';
|
|
19
|
+
this.status = status;
|
|
20
|
+
this.data = data;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Extract the file that called deny() from the stack trace.
|
|
25
|
+
* Returns a short path (e.g. "app/auth/access.ts") or undefined if
|
|
26
|
+
* the stack can't be parsed. Dev-only — used for dev log output.
|
|
27
|
+
*/
|
|
28
|
+
get sourceFile(): string | undefined {
|
|
29
|
+
if (!this.stack) return undefined;
|
|
30
|
+
const frames = this.stack.split('\n');
|
|
31
|
+
// Skip the Error line and the deny() frame — the caller is the 3rd line.
|
|
32
|
+
// Stack format: " at FnName (file:line:col)" or " at file:line:col"
|
|
33
|
+
for (let i = 2; i < frames.length; i++) {
|
|
34
|
+
const frame = frames[i];
|
|
35
|
+
if (!frame) continue;
|
|
36
|
+
// Skip framework internals
|
|
37
|
+
if (frame.includes('primitives.ts') || frame.includes('node_modules')) continue;
|
|
38
|
+
// Extract file path from the frame
|
|
39
|
+
const match =
|
|
40
|
+
frame.match(/\(([^)]+?)(?::\d+:\d+)\)/) ?? frame.match(/at\s+([^\s]+?)(?::\d+:\d+)/);
|
|
41
|
+
if (match?.[1]) {
|
|
42
|
+
// Shorten to app-relative path
|
|
43
|
+
const full = match[1];
|
|
44
|
+
const appIdx = full.indexOf('/app/');
|
|
45
|
+
return appIdx >= 0 ? full.slice(appIdx + 1) : full;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return undefined;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Universal denial primitive. Throws a `DenySignal` that the framework catches.
|
|
54
|
+
*
|
|
55
|
+
* - In segment context (outside Suspense): produces HTTP status code
|
|
56
|
+
* - In slot context: graceful degradation → denied.tsx → default.tsx → null
|
|
57
|
+
* - Inside Suspense (hold window): promoted to pre-flush behavior
|
|
58
|
+
* - Inside Suspense (after flush): error boundary + noindex meta
|
|
59
|
+
*
|
|
60
|
+
* @param status - Any 4xx HTTP status code. Defaults to 403.
|
|
61
|
+
* @param data - Optional data passed as `dangerouslyPassData` prop to status-code files.
|
|
62
|
+
*/
|
|
63
|
+
export function deny(status: number = 403, data?: unknown): never {
|
|
64
|
+
if (status < 400 || status > 499) {
|
|
65
|
+
throw new Error(
|
|
66
|
+
`deny() requires a 4xx status code, got ${status}. ` +
|
|
67
|
+
'For 5xx errors, throw a RenderError instead.'
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
throw new DenySignal(status, data);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Convenience alias for `deny(404)`.
|
|
75
|
+
*
|
|
76
|
+
* Provided for Next.js API compatibility — libraries and user code that
|
|
77
|
+
* call `notFound()` from `next/navigation` get the same behavior as
|
|
78
|
+
* `deny(404)` in timber.
|
|
79
|
+
*/
|
|
80
|
+
export function notFound(): never {
|
|
81
|
+
throw new DenySignal(404);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Next.js redirect type discriminator.
|
|
86
|
+
*
|
|
87
|
+
* Provided for API compatibility with libraries that import `RedirectType`
|
|
88
|
+
* from `next/navigation`. In timber, `redirect()` always uses `replace`
|
|
89
|
+
* semantics (no history entry for the redirect itself).
|
|
90
|
+
*/
|
|
91
|
+
export const RedirectType = {
|
|
92
|
+
push: 'push',
|
|
93
|
+
replace: 'replace',
|
|
94
|
+
} as const;
|
|
95
|
+
|
|
96
|
+
// ─── RedirectSignal ─────────────────────────────────────────────────────────
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Render-phase signal thrown by `redirect()` and `redirectExternal()`.
|
|
100
|
+
* Caught by the framework to produce a 3xx response or client-side navigation.
|
|
101
|
+
*/
|
|
102
|
+
export class RedirectSignal extends Error {
|
|
103
|
+
readonly location: string;
|
|
104
|
+
readonly status: number;
|
|
105
|
+
|
|
106
|
+
constructor(location: string, status: number) {
|
|
107
|
+
super(`Redirect to ${location}`);
|
|
108
|
+
this.name = 'RedirectSignal';
|
|
109
|
+
this.location = location;
|
|
110
|
+
this.status = status;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** Pattern matching absolute URLs: http(s):// or protocol-relative // */
|
|
115
|
+
const ABSOLUTE_URL_RE = /^(?:[a-zA-Z][a-zA-Z\d+\-.]*:|\/\/)/;
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Redirect to a relative path. Rejects absolute and protocol-relative URLs.
|
|
119
|
+
* Use `redirectExternal()` for external redirects with an allow-list.
|
|
120
|
+
*
|
|
121
|
+
* @param path - Relative path (e.g. '/login', 'settings', '/login?returnTo=/dash')
|
|
122
|
+
* @param status - HTTP redirect status code (3xx). Defaults to 302.
|
|
123
|
+
*/
|
|
124
|
+
export function redirect(path: string, status: number = 302): never {
|
|
125
|
+
if (status < 300 || status > 399) {
|
|
126
|
+
throw new Error(`redirect() requires a 3xx status code, got ${status}.`);
|
|
127
|
+
}
|
|
128
|
+
if (ABSOLUTE_URL_RE.test(path)) {
|
|
129
|
+
throw new Error(
|
|
130
|
+
`redirect() only accepts relative URLs. Got absolute URL: "${path}". ` +
|
|
131
|
+
'Use redirectExternal(url, allowList) for external redirects.'
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
throw new RedirectSignal(path, status);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Permanent redirect to a relative path. Shorthand for `redirect(path, 308)`.
|
|
139
|
+
*
|
|
140
|
+
* Uses 308 (Permanent Redirect) which preserves the HTTP method — the browser
|
|
141
|
+
* will replay POST requests to the new location. This matches Next.js behavior.
|
|
142
|
+
*
|
|
143
|
+
* @param path - Relative path (e.g. '/new-page', '/dashboard')
|
|
144
|
+
*/
|
|
145
|
+
export function permanentRedirect(path: string): never {
|
|
146
|
+
redirect(path, 308);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Redirect to an external URL. The hostname must be in the provided allow-list.
|
|
151
|
+
*
|
|
152
|
+
* @param url - Absolute URL to redirect to.
|
|
153
|
+
* @param allowList - Array of allowed hostnames (e.g. ['example.com', 'auth.example.com']).
|
|
154
|
+
* @param status - HTTP redirect status code (3xx). Defaults to 302.
|
|
155
|
+
*/
|
|
156
|
+
export function redirectExternal(url: string, allowList: string[], status: number = 302): never {
|
|
157
|
+
if (status < 300 || status > 399) {
|
|
158
|
+
throw new Error(`redirectExternal() requires a 3xx status code, got ${status}.`);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
let hostname: string;
|
|
162
|
+
try {
|
|
163
|
+
hostname = new URL(url).hostname;
|
|
164
|
+
} catch {
|
|
165
|
+
throw new Error(`redirectExternal() received an invalid URL: "${url}"`);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (!allowList.includes(hostname)) {
|
|
169
|
+
throw new Error(
|
|
170
|
+
`redirectExternal() target "${hostname}" is not in the allow-list. ` +
|
|
171
|
+
`Allowed: [${allowList.join(', ')}]`
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
throw new RedirectSignal(url, status);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// ─── RenderError ────────────────────────────────────────────────────────────
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Typed digest that crosses the RSC → client boundary.
|
|
182
|
+
* The `code` identifies the error class; `data` carries JSON-serializable context.
|
|
183
|
+
*/
|
|
184
|
+
export interface RenderErrorDigest<TCode extends string = string, TData = unknown> {
|
|
185
|
+
code: TCode;
|
|
186
|
+
data: TData;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Typed throw for render-phase errors that carry structured context to error boundaries.
|
|
191
|
+
*
|
|
192
|
+
* The `digest` (code + data) is serialized into the RSC stream separately from the
|
|
193
|
+
* Error instance — only the digest crosses the RSC → client boundary.
|
|
194
|
+
*
|
|
195
|
+
* @example
|
|
196
|
+
* ```ts
|
|
197
|
+
* throw new RenderError('PRODUCT_NOT_FOUND', {
|
|
198
|
+
* title: 'Product not found',
|
|
199
|
+
* resourceId: params.id,
|
|
200
|
+
* })
|
|
201
|
+
* ```
|
|
202
|
+
*/
|
|
203
|
+
export class RenderError<TCode extends string = string, TData = unknown> extends Error {
|
|
204
|
+
readonly code: TCode;
|
|
205
|
+
readonly digest: RenderErrorDigest<TCode, TData>;
|
|
206
|
+
readonly status: number;
|
|
207
|
+
|
|
208
|
+
constructor(code: TCode, data: TData, options?: { status?: number }) {
|
|
209
|
+
super(`RenderError: ${code}`);
|
|
210
|
+
this.name = 'RenderError';
|
|
211
|
+
this.code = code;
|
|
212
|
+
this.digest = { code, data };
|
|
213
|
+
|
|
214
|
+
const status = options?.status ?? 500;
|
|
215
|
+
if (status < 400 || status > 599) {
|
|
216
|
+
throw new Error(`RenderError status must be 4xx or 5xx, got ${status}.`);
|
|
217
|
+
}
|
|
218
|
+
this.status = status;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// ─── waitUntil ──────────────────────────────────────────────────────────────
|
|
223
|
+
|
|
224
|
+
/** Minimal interface for adapters that support background work. */
|
|
225
|
+
export interface WaitUntilAdapter {
|
|
226
|
+
waitUntil?(promise: Promise<unknown>): void;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// Intentional per-app singleton — warn-once flag that persists for the
|
|
230
|
+
// lifetime of the process/isolate. Not per-request; do not migrate to ALS.
|
|
231
|
+
let _waitUntilWarned = false;
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Register a promise to be kept alive after the response is sent.
|
|
235
|
+
* Maps to `ctx.waitUntil()` on Cloudflare Workers and similar platforms.
|
|
236
|
+
*
|
|
237
|
+
* If the adapter does not support `waitUntil`, a warning is logged once
|
|
238
|
+
* and the promise is left to resolve (or reject) without being tracked.
|
|
239
|
+
*
|
|
240
|
+
* @param promise - The background work to keep alive.
|
|
241
|
+
* @param adapter - The platform adapter (injected by the framework at runtime).
|
|
242
|
+
*/
|
|
243
|
+
export function waitUntil(promise: Promise<unknown>, adapter: WaitUntilAdapter): void {
|
|
244
|
+
if (typeof adapter.waitUntil === 'function') {
|
|
245
|
+
adapter.waitUntil(promise);
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
if (!_waitUntilWarned) {
|
|
250
|
+
_waitUntilWarned = true;
|
|
251
|
+
console.warn(
|
|
252
|
+
'[timber] waitUntil() is not supported by the current adapter. ' +
|
|
253
|
+
'Background work will not be tracked. This warning is shown once.'
|
|
254
|
+
);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Reset the waitUntil warning state. Exported for testing only.
|
|
260
|
+
* @internal
|
|
261
|
+
*/
|
|
262
|
+
export function _resetWaitUntilWarning(): void {
|
|
263
|
+
_waitUntilWarned = false;
|
|
264
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Proxy runner — executes app/proxy.ts before route matching.
|
|
3
|
+
*
|
|
4
|
+
* Supports two forms:
|
|
5
|
+
* - Function: (req, next) => Promise<Response>
|
|
6
|
+
* - Array: middleware functions composed left-to-right
|
|
7
|
+
*
|
|
8
|
+
* See design/07-routing.md §"proxy.ts — Global Middleware"
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/** Signature for a single proxy middleware function. */
|
|
12
|
+
export type ProxyFn = (req: Request, next: () => Promise<Response>) => Response | Promise<Response>;
|
|
13
|
+
|
|
14
|
+
/** The proxy.ts default export — either a function or an array of functions. */
|
|
15
|
+
export type ProxyExport = ProxyFn | ProxyFn[];
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Run the proxy pipeline.
|
|
19
|
+
*
|
|
20
|
+
* @param proxyExport - The default export from proxy.ts (function or array)
|
|
21
|
+
* @param req - The incoming request
|
|
22
|
+
* @param next - The continuation that proceeds to route matching and rendering
|
|
23
|
+
* @returns The final response
|
|
24
|
+
*/
|
|
25
|
+
export async function runProxy(
|
|
26
|
+
proxyExport: ProxyExport,
|
|
27
|
+
req: Request,
|
|
28
|
+
next: () => Promise<Response>
|
|
29
|
+
): Promise<Response> {
|
|
30
|
+
const fns = Array.isArray(proxyExport) ? proxyExport : [proxyExport];
|
|
31
|
+
|
|
32
|
+
// Compose left-to-right: first item's next() calls the second, etc.
|
|
33
|
+
// The last item's next() calls the original `next` (route matching + render).
|
|
34
|
+
let i = fns.length;
|
|
35
|
+
let composed = next;
|
|
36
|
+
while (i--) {
|
|
37
|
+
const fn = fns[i]!;
|
|
38
|
+
const downstream = composed;
|
|
39
|
+
composed = () => Promise.resolve(fn(req, downstream));
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return composed();
|
|
43
|
+
}
|