@timber-js/app 0.1.1 → 0.1.3

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 (143) hide show
  1. package/dist/index.d.ts.map +1 -1
  2. package/dist/index.js +11 -7
  3. package/dist/index.js.map +1 -1
  4. package/dist/plugins/dev-server.d.ts.map +1 -1
  5. package/dist/plugins/entries.d.ts.map +1 -1
  6. package/package.json +5 -4
  7. package/src/adapters/cloudflare.ts +325 -0
  8. package/src/adapters/nitro.ts +366 -0
  9. package/src/adapters/types.ts +63 -0
  10. package/src/cache/index.ts +91 -0
  11. package/src/cache/redis-handler.ts +91 -0
  12. package/src/cache/register-cached-function.ts +99 -0
  13. package/src/cache/singleflight.ts +26 -0
  14. package/src/cache/stable-stringify.ts +21 -0
  15. package/src/cache/timber-cache.ts +116 -0
  16. package/src/cli.ts +201 -0
  17. package/src/client/browser-entry.ts +663 -0
  18. package/src/client/error-boundary.tsx +209 -0
  19. package/src/client/form.tsx +200 -0
  20. package/src/client/head.ts +61 -0
  21. package/src/client/history.ts +46 -0
  22. package/src/client/index.ts +60 -0
  23. package/src/client/link-navigate-interceptor.tsx +62 -0
  24. package/src/client/link-status-provider.tsx +40 -0
  25. package/src/client/link.tsx +310 -0
  26. package/src/client/nuqs-adapter.tsx +117 -0
  27. package/src/client/router-ref.ts +25 -0
  28. package/src/client/router.ts +563 -0
  29. package/src/client/segment-cache.ts +194 -0
  30. package/src/client/segment-context.ts +57 -0
  31. package/src/client/ssr-data.ts +95 -0
  32. package/src/client/types.ts +4 -0
  33. package/src/client/unload-guard.ts +34 -0
  34. package/src/client/use-cookie.ts +122 -0
  35. package/src/client/use-link-status.ts +46 -0
  36. package/src/client/use-navigation-pending.ts +47 -0
  37. package/src/client/use-params.ts +71 -0
  38. package/src/client/use-pathname.ts +43 -0
  39. package/src/client/use-query-states.ts +133 -0
  40. package/src/client/use-router.ts +77 -0
  41. package/src/client/use-search-params.ts +74 -0
  42. package/src/client/use-selected-layout-segment.ts +110 -0
  43. package/src/content/index.ts +13 -0
  44. package/src/cookies/define-cookie.ts +137 -0
  45. package/src/cookies/index.ts +9 -0
  46. package/src/fonts/ast.ts +359 -0
  47. package/src/fonts/css.ts +68 -0
  48. package/src/fonts/fallbacks.ts +248 -0
  49. package/src/fonts/google.ts +332 -0
  50. package/src/fonts/local.ts +177 -0
  51. package/src/fonts/types.ts +88 -0
  52. package/src/index.ts +420 -0
  53. package/src/plugins/adapter-build.ts +118 -0
  54. package/src/plugins/build-manifest.ts +323 -0
  55. package/src/plugins/build-report.ts +353 -0
  56. package/src/plugins/cache-transform.ts +199 -0
  57. package/src/plugins/chunks.ts +90 -0
  58. package/src/plugins/content.ts +136 -0
  59. package/src/plugins/dev-error-overlay.ts +230 -0
  60. package/src/plugins/dev-logs.ts +280 -0
  61. package/src/plugins/dev-server.ts +391 -0
  62. package/src/plugins/dynamic-transform.ts +161 -0
  63. package/src/plugins/entries.ts +214 -0
  64. package/src/plugins/fonts.ts +581 -0
  65. package/src/plugins/mdx.ts +179 -0
  66. package/src/plugins/react-prod.ts +56 -0
  67. package/src/plugins/routing.ts +419 -0
  68. package/src/plugins/server-action-exports.ts +220 -0
  69. package/src/plugins/server-bundle.ts +113 -0
  70. package/src/plugins/shims.ts +168 -0
  71. package/src/plugins/static-build.ts +207 -0
  72. package/src/routing/codegen.ts +396 -0
  73. package/src/routing/index.ts +14 -0
  74. package/src/routing/interception.ts +173 -0
  75. package/src/routing/scanner.ts +487 -0
  76. package/src/routing/status-file-lint.ts +114 -0
  77. package/src/routing/types.ts +100 -0
  78. package/src/search-params/analyze.ts +192 -0
  79. package/src/search-params/codecs.ts +153 -0
  80. package/src/search-params/create.ts +314 -0
  81. package/src/search-params/index.ts +23 -0
  82. package/src/search-params/registry.ts +31 -0
  83. package/src/server/access-gate.tsx +142 -0
  84. package/src/server/action-client.ts +473 -0
  85. package/src/server/action-handler.ts +325 -0
  86. package/src/server/actions.ts +236 -0
  87. package/src/server/asset-headers.ts +81 -0
  88. package/src/server/body-limits.ts +102 -0
  89. package/src/server/build-manifest.ts +234 -0
  90. package/src/server/canonicalize.ts +90 -0
  91. package/src/server/client-module-map.ts +58 -0
  92. package/src/server/csrf.ts +79 -0
  93. package/src/server/deny-renderer.ts +302 -0
  94. package/src/server/dev-logger.ts +419 -0
  95. package/src/server/dev-span-processor.ts +78 -0
  96. package/src/server/dev-warnings.ts +282 -0
  97. package/src/server/early-hints-sender.ts +55 -0
  98. package/src/server/early-hints.ts +142 -0
  99. package/src/server/error-boundary-wrapper.ts +69 -0
  100. package/src/server/error-formatter.ts +184 -0
  101. package/src/server/flush.ts +182 -0
  102. package/src/server/form-data.ts +176 -0
  103. package/src/server/form-flash.ts +93 -0
  104. package/src/server/html-injectors.ts +445 -0
  105. package/src/server/index.ts +222 -0
  106. package/src/server/instrumentation.ts +136 -0
  107. package/src/server/logger.ts +145 -0
  108. package/src/server/manifest-status-resolver.ts +215 -0
  109. package/src/server/metadata-render.ts +527 -0
  110. package/src/server/metadata-routes.ts +189 -0
  111. package/src/server/metadata.ts +263 -0
  112. package/src/server/middleware-runner.ts +32 -0
  113. package/src/server/nuqs-ssr-provider.tsx +63 -0
  114. package/src/server/pipeline.ts +555 -0
  115. package/src/server/prerender.ts +139 -0
  116. package/src/server/primitives.ts +264 -0
  117. package/src/server/proxy.ts +43 -0
  118. package/src/server/request-context.ts +554 -0
  119. package/src/server/route-element-builder.ts +395 -0
  120. package/src/server/route-handler.ts +153 -0
  121. package/src/server/route-matcher.ts +316 -0
  122. package/src/server/rsc-entry/api-handler.ts +112 -0
  123. package/src/server/rsc-entry/error-renderer.ts +177 -0
  124. package/src/server/rsc-entry/helpers.ts +147 -0
  125. package/src/server/rsc-entry/index.ts +688 -0
  126. package/src/server/rsc-entry/ssr-bridge.ts +18 -0
  127. package/src/server/slot-resolver.ts +359 -0
  128. package/src/server/ssr-entry.ts +161 -0
  129. package/src/server/ssr-render.ts +200 -0
  130. package/src/server/status-code-resolver.ts +282 -0
  131. package/src/server/tracing.ts +281 -0
  132. package/src/server/tree-builder.ts +354 -0
  133. package/src/server/types.ts +150 -0
  134. package/src/shims/font-google.ts +67 -0
  135. package/src/shims/headers.ts +11 -0
  136. package/src/shims/image.ts +48 -0
  137. package/src/shims/link.ts +9 -0
  138. package/src/shims/navigation-client.ts +52 -0
  139. package/src/shims/navigation.ts +31 -0
  140. package/src/shims/server-only-noop.js +5 -0
  141. package/src/utils/directive-parser.ts +529 -0
  142. package/src/utils/format.ts +10 -0
  143. package/src/utils/startup-timer.ts +102 -0
@@ -0,0 +1,263 @@
1
+ /**
2
+ * Metadata resolution for timber.js.
3
+ *
4
+ * Resolves metadata from a segment chain (layouts + page), applies title
5
+ * templates, shallow-merges entries, and produces head element descriptors.
6
+ *
7
+ * Resolution happens inside the render pass — React.cache is active,
8
+ * metadata is outside Suspense, and the flush point guarantees completeness.
9
+ *
10
+ * Rendering (Metadata → HeadElement[]) is in metadata-render.ts.
11
+ *
12
+ * See design/16-metadata.md
13
+ */
14
+
15
+ import type { Metadata } from './types.js';
16
+
17
+ // Re-export renderMetadataToElements from the rendering module so existing
18
+ // consumers (route-element-builder, tests) can keep importing from here.
19
+ export { renderMetadataToElements } from './metadata-render.js';
20
+
21
+ // ─── Types ───────────────────────────────────────────────────────────────────
22
+
23
+ /** A single metadata entry from a layout or page module. */
24
+ export interface SegmentMetadataEntry {
25
+ /** The resolved metadata object (from static or async `metadata` export). */
26
+ metadata: Metadata;
27
+ /** Whether this entry is from the page (leaf) module. */
28
+ isPage: boolean;
29
+ }
30
+
31
+ /** Options for resolveMetadata. */
32
+ export interface ResolveMetadataOptions {
33
+ /**
34
+ * When true, the page's metadata is discarded (simulating a render error)
35
+ * and `<meta name="robots" content="noindex">` is injected.
36
+ */
37
+ errorState?: boolean;
38
+ }
39
+
40
+ /** A rendered head element descriptor. */
41
+ export interface HeadElement {
42
+ tag: 'title' | 'meta' | 'link';
43
+ content?: string;
44
+ attrs?: Record<string, string>;
45
+ }
46
+
47
+ // ─── Title Resolution ────────────────────────────────────────────────────────
48
+
49
+ /**
50
+ * Resolve a title value with an optional template.
51
+ *
52
+ * - string → apply template if present
53
+ * - { absolute: '...' } → use as-is, skip template
54
+ * - { default: '...' } → use as fallback (no template applied)
55
+ * - undefined → undefined
56
+ */
57
+ export function resolveTitle(
58
+ title: Metadata['title'],
59
+ template: string | undefined
60
+ ): string | undefined {
61
+ if (title === undefined || title === null) {
62
+ return undefined;
63
+ }
64
+
65
+ if (typeof title === 'string') {
66
+ return template ? template.replace('%s', title) : title;
67
+ }
68
+
69
+ // Object form
70
+ if (title.absolute !== undefined) {
71
+ return title.absolute;
72
+ }
73
+
74
+ if (title.default !== undefined) {
75
+ return title.default;
76
+ }
77
+
78
+ return undefined;
79
+ }
80
+
81
+ // ─── Metadata Resolution ─────────────────────────────────────────────────────
82
+
83
+ /**
84
+ * Resolve metadata from a segment chain.
85
+ *
86
+ * Processes entries from root layout to page (in segment order).
87
+ * The merge algorithm:
88
+ * 1. Shallow-merge all keys except title (later wins)
89
+ * 2. Track the most recent title template
90
+ * 3. Resolve the final title using the template
91
+ *
92
+ * In error state, the page entry is dropped and noindex is injected.
93
+ *
94
+ * See design/16-metadata.md §"Merge Algorithm"
95
+ */
96
+ export function resolveMetadata(
97
+ entries: SegmentMetadataEntry[],
98
+ options: ResolveMetadataOptions = {}
99
+ ): Metadata {
100
+ const { errorState = false } = options;
101
+
102
+ const merged: Metadata = {};
103
+ let titleTemplate: string | undefined;
104
+ let lastDefault: string | undefined;
105
+ let rawTitle: Metadata['title'];
106
+
107
+ for (const { metadata, isPage } of entries) {
108
+ // In error state, skip the page's metadata entirely
109
+ if (errorState && isPage) {
110
+ continue;
111
+ }
112
+
113
+ // Track title template
114
+ if (metadata.title !== undefined && typeof metadata.title === 'object') {
115
+ if (metadata.title.template !== undefined) {
116
+ titleTemplate = metadata.title.template;
117
+ }
118
+ if (metadata.title.default !== undefined) {
119
+ lastDefault = metadata.title.default;
120
+ }
121
+ }
122
+
123
+ // Shallow-merge all keys except title
124
+ for (const key of Object.keys(metadata) as Array<keyof Metadata>) {
125
+ if (key === 'title') continue;
126
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
127
+ (merged as any)[key] = metadata[key];
128
+ }
129
+
130
+ // Track raw title (will be resolved after the loop)
131
+ if (metadata.title !== undefined) {
132
+ rawTitle = metadata.title;
133
+ }
134
+ }
135
+
136
+ // In error state, we lost page title — use the most recent default
137
+ if (errorState) {
138
+ rawTitle = lastDefault !== undefined ? { default: lastDefault } : rawTitle;
139
+ // Don't apply template in error state
140
+ titleTemplate = undefined;
141
+ }
142
+
143
+ // Resolve the final title
144
+ const resolvedTitle = resolveTitle(rawTitle, titleTemplate);
145
+ if (resolvedTitle !== undefined) {
146
+ merged.title = resolvedTitle;
147
+ }
148
+
149
+ // Error state: inject noindex, overriding any user robots
150
+ if (errorState) {
151
+ merged.robots = 'noindex';
152
+ }
153
+
154
+ return merged;
155
+ }
156
+
157
+ // ─── URL Resolution ──────────────────────────────────────────────────────────
158
+
159
+ /**
160
+ * Check if a string is an absolute URL.
161
+ */
162
+ function isAbsoluteUrl(url: string): boolean {
163
+ return url.startsWith('http://') || url.startsWith('https://') || url.startsWith('//');
164
+ }
165
+
166
+ /**
167
+ * Resolve a relative URL against a base URL.
168
+ */
169
+ function resolveUrl(url: string, base: URL): string {
170
+ if (isAbsoluteUrl(url)) return url;
171
+ return new URL(url, base).toString();
172
+ }
173
+
174
+ /**
175
+ * Resolve relative URLs in metadata fields against metadataBase.
176
+ *
177
+ * Returns a new metadata object with URLs resolved. Absolute URLs are not modified.
178
+ * If metadataBase is not set, returns the metadata unchanged.
179
+ */
180
+ export function resolveMetadataUrls(metadata: Metadata): Metadata {
181
+ const base = metadata.metadataBase;
182
+ if (!base) return metadata;
183
+
184
+ const result = { ...metadata };
185
+
186
+ // Resolve openGraph images
187
+ if (result.openGraph) {
188
+ result.openGraph = { ...result.openGraph };
189
+ if (typeof result.openGraph.images === 'string') {
190
+ result.openGraph.images = resolveUrl(result.openGraph.images, base);
191
+ } else if (Array.isArray(result.openGraph.images)) {
192
+ result.openGraph.images = result.openGraph.images.map((img) => ({
193
+ ...img,
194
+ url: resolveUrl(img.url, base),
195
+ }));
196
+ } else if (result.openGraph.images) {
197
+ // Single object: { url, width?, height?, alt? }
198
+ result.openGraph.images = {
199
+ ...result.openGraph.images,
200
+ url: resolveUrl(result.openGraph.images.url, base),
201
+ };
202
+ }
203
+ if (result.openGraph.url && !isAbsoluteUrl(result.openGraph.url)) {
204
+ result.openGraph.url = resolveUrl(result.openGraph.url, base);
205
+ }
206
+ }
207
+
208
+ // Resolve twitter images
209
+ if (result.twitter) {
210
+ result.twitter = { ...result.twitter };
211
+ if (typeof result.twitter.images === 'string') {
212
+ result.twitter.images = resolveUrl(result.twitter.images, base);
213
+ } else if (Array.isArray(result.twitter.images)) {
214
+ // Resolve each image URL, preserving the union type structure
215
+ const resolved = result.twitter.images.map((img) =>
216
+ typeof img === 'string' ? resolveUrl(img, base) : { ...img, url: resolveUrl(img.url, base) }
217
+ );
218
+ // If all entries are strings, assign as string[]; otherwise as object[]
219
+ const allStrings = resolved.every((r) => typeof r === 'string');
220
+ result.twitter.images = allStrings
221
+ ? (resolved as string[])
222
+ : (resolved as Array<{ url: string; alt?: string; width?: number; height?: number }>);
223
+ } else if (result.twitter.images) {
224
+ // Single object: { url, alt?, width?, height? }
225
+ result.twitter.images = {
226
+ ...result.twitter.images,
227
+ url: resolveUrl(result.twitter.images.url, base),
228
+ };
229
+ }
230
+ }
231
+
232
+ // Resolve alternates
233
+ if (result.alternates) {
234
+ result.alternates = { ...result.alternates };
235
+ if (result.alternates.canonical && !isAbsoluteUrl(result.alternates.canonical)) {
236
+ result.alternates.canonical = resolveUrl(result.alternates.canonical, base);
237
+ }
238
+ if (result.alternates.languages) {
239
+ const langs: Record<string, string> = {};
240
+ for (const [lang, url] of Object.entries(result.alternates.languages)) {
241
+ langs[lang] = isAbsoluteUrl(url) ? url : resolveUrl(url, base);
242
+ }
243
+ result.alternates.languages = langs;
244
+ }
245
+ }
246
+
247
+ // Resolve icon URLs
248
+ if (result.icons) {
249
+ result.icons = { ...result.icons };
250
+ if (typeof result.icons.icon === 'string') {
251
+ result.icons.icon = resolveUrl(result.icons.icon, base);
252
+ } else if (Array.isArray(result.icons.icon)) {
253
+ result.icons.icon = result.icons.icon.map((i) => ({ ...i, url: resolveUrl(i.url, base) }));
254
+ }
255
+ if (typeof result.icons.apple === 'string') {
256
+ result.icons.apple = resolveUrl(result.icons.apple, base);
257
+ } else if (Array.isArray(result.icons.apple)) {
258
+ result.icons.apple = result.icons.apple.map((i) => ({ ...i, url: resolveUrl(i.url, base) }));
259
+ }
260
+ }
261
+
262
+ return result;
263
+ }
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Middleware runner — executes a route's middleware.ts before rendering.
3
+ *
4
+ * Only the leaf route's middleware runs. There is no middleware chain.
5
+ * Middleware does NOT have next() — it either short-circuits with a Response
6
+ * or returns void to continue to access checks + render.
7
+ *
8
+ * See design/07-routing.md §"middleware.ts"
9
+ */
10
+
11
+ import type { MiddlewareContext } from './types.js';
12
+
13
+ /** Signature of a middleware.ts default export. */
14
+ export type MiddlewareFn = (ctx: MiddlewareContext) => Response | void | Promise<Response | void>;
15
+
16
+ /**
17
+ * Run a route's middleware function.
18
+ *
19
+ * @param middlewareFn - The default export from the route's middleware.ts
20
+ * @param ctx - The middleware context (req, params, headers, requestHeaders, searchParams)
21
+ * @returns A Response if middleware short-circuited, or undefined to continue
22
+ */
23
+ export async function runMiddleware(
24
+ middlewareFn: MiddlewareFn,
25
+ ctx: MiddlewareContext
26
+ ): Promise<Response | undefined> {
27
+ const result = await middlewareFn(ctx);
28
+ if (result instanceof Response) {
29
+ return result;
30
+ }
31
+ return undefined;
32
+ }
@@ -0,0 +1,63 @@
1
+ /**
2
+ * Server-side nuqs adapter provider for SSR rendering.
3
+ *
4
+ * During SSR, `'use client'` components that call nuqs hooks (useQueryStates,
5
+ * useQueryState) need the nuqs adapter context. The client adapter
6
+ * (TimberNuqsAdapter) relies on `window` and React hooks that are
7
+ * client-only. This provider supplies a static, SSR-safe adapter
8
+ * that feeds the current request's search params into the nuqs context.
9
+ *
10
+ * The returned component wraps the React tree so that nuqs hooks render
11
+ * the correct initial values during server-side rendering. On the client,
12
+ * TimberNuqsAdapter (injected by browser-entry.ts) takes over.
13
+ *
14
+ * Design doc: design/23-search-params.md §"Custom Adapter"
15
+ */
16
+
17
+ import { createElement, type ReactNode } from 'react';
18
+ import {
19
+ unstable_createAdapterProvider as createAdapterProvider,
20
+ type unstable_AdapterInterface as AdapterInterface,
21
+ } from 'nuqs/adapters/custom';
22
+
23
+ // ─── SSR Adapter ──────────────────────────────────────────────────
24
+
25
+ /**
26
+ * Create a nuqs adapter provider for SSR that serves a static snapshot
27
+ * of search params. The `updateUrl` is a no-op because URL updates
28
+ * cannot happen on the server.
29
+ */
30
+ function makeNuqsSsrAdapter(searchParams: URLSearchParams) {
31
+ function useNuqsSsrAdapter(_watchKeys: string[]): AdapterInterface {
32
+ return {
33
+ searchParams,
34
+ updateUrl: () => {},
35
+ getSearchParamsSnapshot: () => searchParams,
36
+ };
37
+ }
38
+
39
+ return createAdapterProvider(useNuqsSsrAdapter);
40
+ }
41
+
42
+ // ─── Provider Component ───────────────────────────────────────────
43
+
44
+ /**
45
+ * Wrap the SSR element tree with a nuqs adapter context.
46
+ *
47
+ * Called by ssr-entry.ts before passing the element to renderSsrStream.
48
+ * Takes the NavContext search params and provides them to nuqs hooks
49
+ * running during SSR so they render with the correct initial values.
50
+ *
51
+ * @param searchParamsRecord - The request's search params as a plain record
52
+ * @param children - The React element tree to wrap
53
+ */
54
+ export function withNuqsSsrAdapter(
55
+ searchParamsRecord: Record<string, string>,
56
+ children: ReactNode
57
+ ): ReactNode {
58
+ const searchParams = new URLSearchParams(searchParamsRecord);
59
+ const Provider = makeNuqsSsrAdapter(searchParams);
60
+ // AdapterProvider types require children in the props object (not as 3rd arg)
61
+ // eslint-disable-next-line react/no-children-prop
62
+ return createElement(Provider, { defaultOptions: { shallow: false, scroll: true }, children });
63
+ }