@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.
Files changed (141) hide show
  1. package/dist/index.js +5 -2
  2. package/dist/index.js.map +1 -1
  3. package/dist/plugins/entries.d.ts.map +1 -1
  4. package/package.json +43 -58
  5. package/src/adapters/cloudflare.ts +325 -0
  6. package/src/adapters/nitro.ts +366 -0
  7. package/src/adapters/types.ts +63 -0
  8. package/src/cache/index.ts +91 -0
  9. package/src/cache/redis-handler.ts +91 -0
  10. package/src/cache/register-cached-function.ts +99 -0
  11. package/src/cache/singleflight.ts +26 -0
  12. package/src/cache/stable-stringify.ts +21 -0
  13. package/src/cache/timber-cache.ts +116 -0
  14. package/src/cli.ts +201 -0
  15. package/src/client/browser-entry.ts +663 -0
  16. package/src/client/error-boundary.tsx +209 -0
  17. package/src/client/form.tsx +200 -0
  18. package/src/client/head.ts +61 -0
  19. package/src/client/history.ts +46 -0
  20. package/src/client/index.ts +60 -0
  21. package/src/client/link-navigate-interceptor.tsx +62 -0
  22. package/src/client/link-status-provider.tsx +40 -0
  23. package/src/client/link.tsx +310 -0
  24. package/src/client/nuqs-adapter.tsx +117 -0
  25. package/src/client/router-ref.ts +25 -0
  26. package/src/client/router.ts +563 -0
  27. package/src/client/segment-cache.ts +194 -0
  28. package/src/client/segment-context.ts +57 -0
  29. package/src/client/ssr-data.ts +95 -0
  30. package/src/client/types.ts +4 -0
  31. package/src/client/unload-guard.ts +34 -0
  32. package/src/client/use-cookie.ts +122 -0
  33. package/src/client/use-link-status.ts +46 -0
  34. package/src/client/use-navigation-pending.ts +47 -0
  35. package/src/client/use-params.ts +71 -0
  36. package/src/client/use-pathname.ts +43 -0
  37. package/src/client/use-query-states.ts +133 -0
  38. package/src/client/use-router.ts +77 -0
  39. package/src/client/use-search-params.ts +74 -0
  40. package/src/client/use-selected-layout-segment.ts +110 -0
  41. package/src/content/index.ts +13 -0
  42. package/src/cookies/define-cookie.ts +137 -0
  43. package/src/cookies/index.ts +9 -0
  44. package/src/fonts/ast.ts +359 -0
  45. package/src/fonts/css.ts +68 -0
  46. package/src/fonts/fallbacks.ts +248 -0
  47. package/src/fonts/google.ts +332 -0
  48. package/src/fonts/local.ts +177 -0
  49. package/src/fonts/types.ts +88 -0
  50. package/src/index.ts +413 -0
  51. package/src/plugins/adapter-build.ts +118 -0
  52. package/src/plugins/build-manifest.ts +323 -0
  53. package/src/plugins/build-report.ts +353 -0
  54. package/src/plugins/cache-transform.ts +199 -0
  55. package/src/plugins/chunks.ts +90 -0
  56. package/src/plugins/content.ts +136 -0
  57. package/src/plugins/dev-error-overlay.ts +230 -0
  58. package/src/plugins/dev-logs.ts +280 -0
  59. package/src/plugins/dev-server.ts +389 -0
  60. package/src/plugins/dynamic-transform.ts +161 -0
  61. package/src/plugins/entries.ts +207 -0
  62. package/src/plugins/fonts.ts +581 -0
  63. package/src/plugins/mdx.ts +179 -0
  64. package/src/plugins/react-prod.ts +56 -0
  65. package/src/plugins/routing.ts +419 -0
  66. package/src/plugins/server-action-exports.ts +220 -0
  67. package/src/plugins/server-bundle.ts +113 -0
  68. package/src/plugins/shims.ts +168 -0
  69. package/src/plugins/static-build.ts +207 -0
  70. package/src/routing/codegen.ts +396 -0
  71. package/src/routing/index.ts +14 -0
  72. package/src/routing/interception.ts +173 -0
  73. package/src/routing/scanner.ts +487 -0
  74. package/src/routing/status-file-lint.ts +114 -0
  75. package/src/routing/types.ts +100 -0
  76. package/src/search-params/analyze.ts +192 -0
  77. package/src/search-params/codecs.ts +153 -0
  78. package/src/search-params/create.ts +314 -0
  79. package/src/search-params/index.ts +23 -0
  80. package/src/search-params/registry.ts +31 -0
  81. package/src/server/access-gate.tsx +142 -0
  82. package/src/server/action-client.ts +473 -0
  83. package/src/server/action-handler.ts +325 -0
  84. package/src/server/actions.ts +236 -0
  85. package/src/server/asset-headers.ts +81 -0
  86. package/src/server/body-limits.ts +102 -0
  87. package/src/server/build-manifest.ts +234 -0
  88. package/src/server/canonicalize.ts +90 -0
  89. package/src/server/client-module-map.ts +58 -0
  90. package/src/server/csrf.ts +79 -0
  91. package/src/server/deny-renderer.ts +302 -0
  92. package/src/server/dev-logger.ts +419 -0
  93. package/src/server/dev-span-processor.ts +78 -0
  94. package/src/server/dev-warnings.ts +282 -0
  95. package/src/server/early-hints-sender.ts +55 -0
  96. package/src/server/early-hints.ts +142 -0
  97. package/src/server/error-boundary-wrapper.ts +69 -0
  98. package/src/server/error-formatter.ts +184 -0
  99. package/src/server/flush.ts +182 -0
  100. package/src/server/form-data.ts +176 -0
  101. package/src/server/form-flash.ts +93 -0
  102. package/src/server/html-injectors.ts +445 -0
  103. package/src/server/index.ts +222 -0
  104. package/src/server/instrumentation.ts +136 -0
  105. package/src/server/logger.ts +145 -0
  106. package/src/server/manifest-status-resolver.ts +215 -0
  107. package/src/server/metadata-render.ts +527 -0
  108. package/src/server/metadata-routes.ts +189 -0
  109. package/src/server/metadata.ts +263 -0
  110. package/src/server/middleware-runner.ts +32 -0
  111. package/src/server/nuqs-ssr-provider.tsx +63 -0
  112. package/src/server/pipeline.ts +555 -0
  113. package/src/server/prerender.ts +139 -0
  114. package/src/server/primitives.ts +264 -0
  115. package/src/server/proxy.ts +43 -0
  116. package/src/server/request-context.ts +554 -0
  117. package/src/server/route-element-builder.ts +395 -0
  118. package/src/server/route-handler.ts +153 -0
  119. package/src/server/route-matcher.ts +316 -0
  120. package/src/server/rsc-entry/api-handler.ts +112 -0
  121. package/src/server/rsc-entry/error-renderer.ts +177 -0
  122. package/src/server/rsc-entry/helpers.ts +147 -0
  123. package/src/server/rsc-entry/index.ts +688 -0
  124. package/src/server/rsc-entry/ssr-bridge.ts +18 -0
  125. package/src/server/slot-resolver.ts +359 -0
  126. package/src/server/ssr-entry.ts +161 -0
  127. package/src/server/ssr-render.ts +200 -0
  128. package/src/server/status-code-resolver.ts +282 -0
  129. package/src/server/tracing.ts +281 -0
  130. package/src/server/tree-builder.ts +354 -0
  131. package/src/server/types.ts +150 -0
  132. package/src/shims/font-google.ts +67 -0
  133. package/src/shims/headers.ts +11 -0
  134. package/src/shims/image.ts +48 -0
  135. package/src/shims/link.ts +9 -0
  136. package/src/shims/navigation-client.ts +52 -0
  137. package/src/shims/navigation.ts +31 -0
  138. package/src/shims/server-only-noop.js +5 -0
  139. package/src/utils/directive-parser.ts +529 -0
  140. package/src/utils/format.ts +10 -0
  141. 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
+ }