@timber-js/app 0.2.0-alpha.3 → 0.2.0-alpha.30
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/_chunks/{als-registry-k-AtAQ9R.js → als-registry-B7DbZ2hS.js} +1 -1
- package/dist/_chunks/{als-registry-k-AtAQ9R.js.map → als-registry-B7DbZ2hS.js.map} +1 -1
- package/dist/_chunks/debug-B3Gypr3D.js +108 -0
- package/dist/_chunks/debug-B3Gypr3D.js.map +1 -0
- package/dist/_chunks/{format-DNt20Kt8.js → format-RyoGQL74.js} +3 -2
- package/dist/_chunks/format-RyoGQL74.js.map +1 -0
- package/dist/_chunks/{interception-DGDIjDbR.js → interception-BOoWmLUA.js} +2 -2
- package/dist/_chunks/{interception-DGDIjDbR.js.map → interception-BOoWmLUA.js.map} +1 -1
- package/dist/_chunks/{metadata-routes-CQCnF4VK.js → metadata-routes-Cjmvi3rQ.js} +1 -1
- package/dist/_chunks/{metadata-routes-CQCnF4VK.js.map → metadata-routes-Cjmvi3rQ.js.map} +1 -1
- package/dist/_chunks/{request-context-CRj2Zh1E.js → request-context-BQUC8PHn.js} +5 -4
- package/dist/_chunks/request-context-BQUC8PHn.js.map +1 -0
- package/dist/_chunks/{ssr-data-DLnbYpj1.js → ssr-data-MjmprTmO.js} +1 -1
- package/dist/_chunks/{ssr-data-DLnbYpj1.js.map → ssr-data-MjmprTmO.js.map} +1 -1
- package/dist/_chunks/{tracing-DF0G3FB7.js → tracing-CemImE6h.js} +17 -3
- package/dist/_chunks/{tracing-DF0G3FB7.js.map → tracing-CemImE6h.js.map} +1 -1
- package/dist/_chunks/{use-cookie-dDbpCTx-.js → use-cookie-DX-l1_5E.js} +2 -2
- package/dist/_chunks/{use-cookie-dDbpCTx-.js.map → use-cookie-DX-l1_5E.js.map} +1 -1
- package/dist/_chunks/{use-query-states-DAhgj8Gx.js → use-query-states-D5KaffOK.js} +1 -1
- package/dist/_chunks/{use-query-states-DAhgj8Gx.js.map → use-query-states-D5KaffOK.js.map} +1 -1
- package/dist/adapters/nitro.d.ts +17 -1
- package/dist/adapters/nitro.d.ts.map +1 -1
- package/dist/adapters/nitro.js +5 -5
- package/dist/adapters/nitro.js.map +1 -1
- package/dist/cache/fast-hash.d.ts +22 -0
- package/dist/cache/fast-hash.d.ts.map +1 -0
- package/dist/cache/index.js +52 -10
- package/dist/cache/index.js.map +1 -1
- package/dist/cache/register-cached-function.d.ts.map +1 -1
- package/dist/cache/timber-cache.d.ts.map +1 -1
- package/dist/client/error-boundary.js +1 -1
- package/dist/client/index.js +3 -3
- package/dist/client/index.js.map +1 -1
- package/dist/client/link.d.ts.map +1 -1
- package/dist/client/router.d.ts.map +1 -1
- package/dist/client/segment-context.d.ts +1 -1
- package/dist/client/segment-context.d.ts.map +1 -1
- package/dist/client/segment-merger.d.ts.map +1 -1
- package/dist/client/stale-reload.d.ts.map +1 -1
- package/dist/client/top-loader.d.ts.map +1 -1
- package/dist/client/transition-root.d.ts +1 -1
- package/dist/client/transition-root.d.ts.map +1 -1
- package/dist/cookies/index.js +4 -4
- package/dist/fonts/css.d.ts +1 -0
- package/dist/fonts/css.d.ts.map +1 -1
- package/dist/fonts/local.d.ts +4 -2
- package/dist/fonts/local.d.ts.map +1 -1
- package/dist/index.d.ts +28 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +249 -21
- package/dist/index.js.map +1 -1
- package/dist/plugins/build-report.d.ts +11 -1
- package/dist/plugins/build-report.d.ts.map +1 -1
- package/dist/plugins/entries.d.ts +7 -0
- package/dist/plugins/entries.d.ts.map +1 -1
- package/dist/plugins/fonts.d.ts +9 -1
- package/dist/plugins/fonts.d.ts.map +1 -1
- package/dist/plugins/mdx.d.ts +6 -0
- package/dist/plugins/mdx.d.ts.map +1 -1
- package/dist/plugins/server-bundle.d.ts.map +1 -1
- package/dist/routing/index.js +1 -1
- package/dist/rsc-runtime/ssr.d.ts +12 -0
- package/dist/rsc-runtime/ssr.d.ts.map +1 -1
- package/dist/search-params/index.js +1 -1
- package/dist/server/access-gate.d.ts.map +1 -1
- package/dist/server/action-client.d.ts.map +1 -1
- package/dist/server/debug.d.ts +82 -0
- package/dist/server/debug.d.ts.map +1 -0
- package/dist/server/deny-renderer.d.ts.map +1 -1
- package/dist/server/dev-warnings.d.ts.map +1 -1
- package/dist/server/html-injectors.d.ts.map +1 -1
- package/dist/server/index.js +32 -23
- package/dist/server/index.js.map +1 -1
- package/dist/server/logger.d.ts.map +1 -1
- package/dist/server/node-stream-transforms.d.ts +65 -0
- package/dist/server/node-stream-transforms.d.ts.map +1 -0
- package/dist/server/pipeline.d.ts +7 -4
- package/dist/server/pipeline.d.ts.map +1 -1
- package/dist/server/primitives.d.ts.map +1 -1
- package/dist/server/request-context.d.ts.map +1 -1
- package/dist/server/route-element-builder.d.ts.map +1 -1
- package/dist/server/rsc-entry/index.d.ts.map +1 -1
- package/dist/server/rsc-entry/rsc-payload.d.ts.map +1 -1
- package/dist/server/rsc-entry/rsc-stream.d.ts +6 -0
- package/dist/server/rsc-entry/rsc-stream.d.ts.map +1 -1
- package/dist/server/rsc-entry/ssr-renderer.d.ts.map +1 -1
- package/dist/server/rsc-prop-warnings.d.ts.map +1 -1
- package/dist/server/ssr-entry.d.ts.map +1 -1
- package/dist/server/ssr-render.d.ts +34 -21
- package/dist/server/ssr-render.d.ts.map +1 -1
- package/dist/server/tracing.d.ts +10 -0
- package/dist/server/tracing.d.ts.map +1 -1
- package/dist/server/waituntil-bridge.d.ts.map +1 -1
- package/dist/shims/image.d.ts +15 -15
- package/package.json +1 -1
- package/src/adapters/nitro.ts +31 -5
- package/src/cache/fast-hash.ts +34 -0
- package/src/cache/register-cached-function.ts +7 -3
- package/src/cache/timber-cache.ts +17 -10
- package/src/client/browser-entry.ts +10 -6
- package/src/client/link.tsx +14 -9
- package/src/client/router.ts +4 -6
- package/src/client/segment-context.ts +6 -1
- package/src/client/segment-merger.ts +2 -8
- package/src/client/stale-reload.ts +5 -7
- package/src/client/top-loader.tsx +8 -7
- package/src/client/transition-root.tsx +7 -1
- package/src/fonts/css.ts +2 -1
- package/src/fonts/local.ts +7 -3
- package/src/index.ts +35 -2
- package/src/plugins/build-report.ts +23 -3
- package/src/plugins/entries.ts +9 -4
- package/src/plugins/fonts.ts +171 -19
- package/src/plugins/mdx.ts +9 -5
- package/src/plugins/server-bundle.ts +4 -0
- package/src/rsc-runtime/ssr.ts +50 -0
- package/src/rsc-runtime/vendor-types.d.ts +7 -0
- package/src/server/access-gate.tsx +3 -2
- package/src/server/action-client.ts +15 -5
- package/src/server/debug.ts +137 -0
- package/src/server/deny-renderer.ts +3 -2
- package/src/server/dev-warnings.ts +2 -1
- package/src/server/html-injectors.ts +30 -10
- package/src/server/logger.ts +4 -3
- package/src/server/node-stream-transforms.ts +315 -0
- package/src/server/pipeline.ts +34 -20
- package/src/server/primitives.ts +2 -1
- package/src/server/request-context.ts +3 -2
- package/src/server/route-element-builder.ts +1 -6
- package/src/server/rsc-entry/index.ts +50 -7
- package/src/server/rsc-entry/rsc-payload.ts +42 -7
- package/src/server/rsc-entry/rsc-stream.ts +10 -5
- package/src/server/rsc-entry/ssr-renderer.ts +12 -5
- package/src/server/rsc-prop-warnings.ts +3 -1
- package/src/server/ssr-entry.ts +128 -8
- package/src/server/ssr-render.ts +168 -57
- package/src/server/tracing.ts +23 -0
- package/src/server/waituntil-bridge.ts +4 -1
- package/dist/_chunks/format-DNt20Kt8.js.map +0 -1
- package/dist/_chunks/request-context-CRj2Zh1E.js.map +0 -1
|
@@ -1,17 +1,23 @@
|
|
|
1
|
-
import { createHash } from 'node:crypto';
|
|
2
1
|
import type { CacheHandler, CacheOptions } from './index';
|
|
3
2
|
import { stableStringify } from './stable-stringify';
|
|
4
3
|
import { createSingleflight } from './singleflight';
|
|
5
|
-
import {
|
|
4
|
+
import { addSpanEventSync } from '#/server/tracing.js';
|
|
5
|
+
import { fnv1aHash } from './fast-hash.js';
|
|
6
6
|
|
|
7
7
|
const singleflight = createSingleflight();
|
|
8
8
|
|
|
9
9
|
/**
|
|
10
|
-
* Generate a
|
|
10
|
+
* Generate a cache key from function identity and serialized args.
|
|
11
|
+
*
|
|
12
|
+
* Uses FNV-1a (fast non-crypto hash) instead of SHA-256. Cache keys don't
|
|
13
|
+
* need collision resistance — they need speed. The fnId prefix provides
|
|
14
|
+
* namespace isolation; the hash covers the args.
|
|
15
|
+
*
|
|
16
|
+
* See TIM-370 for perf motivation.
|
|
11
17
|
*/
|
|
12
18
|
function defaultKeyGenerator(fnId: string, args: unknown[]): string {
|
|
13
19
|
const raw = fnId + ':' + stableStringify(args);
|
|
14
|
-
return
|
|
20
|
+
return fnId + ':' + fnv1aHash(raw);
|
|
15
21
|
}
|
|
16
22
|
|
|
17
23
|
/**
|
|
@@ -57,8 +63,9 @@ export function createCache<Fn extends (...args: any[]) => Promise<any>>(
|
|
|
57
63
|
const cached = await handler.get(key);
|
|
58
64
|
|
|
59
65
|
if (cached && !cached.stale) {
|
|
60
|
-
// Record as OTEL span event on enclosing span (not a child span)
|
|
61
|
-
|
|
66
|
+
// Record as OTEL span event on enclosing span (not a child span).
|
|
67
|
+
// Fire-and-forget — no microtask overhead on the cache hot path.
|
|
68
|
+
addSpanEventSync('timber.cache.hit', {
|
|
62
69
|
key,
|
|
63
70
|
duration_ms: Math.round(performance.now() - cacheStart),
|
|
64
71
|
});
|
|
@@ -66,8 +73,8 @@ export function createCache<Fn extends (...args: any[]) => Promise<any>>(
|
|
|
66
73
|
}
|
|
67
74
|
|
|
68
75
|
if (cached && cached.stale && opts.staleWhileRevalidate) {
|
|
69
|
-
// Record stale cache hit as OTEL span event
|
|
70
|
-
|
|
76
|
+
// Record stale cache hit as OTEL span event (fire-and-forget).
|
|
77
|
+
addSpanEventSync('timber.cache.hit', {
|
|
71
78
|
key,
|
|
72
79
|
duration_ms: Math.round(performance.now() - cacheStart),
|
|
73
80
|
stale: true,
|
|
@@ -95,8 +102,8 @@ export function createCache<Fn extends (...args: any[]) => Promise<any>>(
|
|
|
95
102
|
const tags = resolveTags(opts, args);
|
|
96
103
|
await handler.set(key, result, { ttl: opts.ttl, tags });
|
|
97
104
|
|
|
98
|
-
// Record cache miss as OTEL span event
|
|
99
|
-
|
|
105
|
+
// Record cache miss as OTEL span event (fire-and-forget).
|
|
106
|
+
addSpanEventSync('timber.cache.miss', {
|
|
100
107
|
key,
|
|
101
108
|
duration_ms: Math.round(performance.now() - cacheStart),
|
|
102
109
|
});
|
|
@@ -52,7 +52,11 @@ import { setupServerLogReplay, setupClientErrorForwarding } from './browser-dev.
|
|
|
52
52
|
// browser-links.ts removed — Link components own their click/hover handlers directly.
|
|
53
53
|
// See LOCAL-340.
|
|
54
54
|
import { TransitionRoot, transitionRender, navigateTransition } from './transition-root.js';
|
|
55
|
-
import {
|
|
55
|
+
import {
|
|
56
|
+
isStaleClientReference,
|
|
57
|
+
triggerStaleReload,
|
|
58
|
+
clearStaleReloadFlag,
|
|
59
|
+
} from './stale-reload.js';
|
|
56
60
|
|
|
57
61
|
// ─── Server Action Dispatch ──────────────────────────────────────
|
|
58
62
|
|
|
@@ -213,10 +217,7 @@ function findOverflowContainer(): HTMLElement | null {
|
|
|
213
217
|
if (!(el instanceof HTMLElement)) continue;
|
|
214
218
|
const style = getComputedStyle(el);
|
|
215
219
|
const overflowY = style.overflowY;
|
|
216
|
-
if (
|
|
217
|
-
(overflowY === 'auto' || overflowY === 'scroll') &&
|
|
218
|
-
el.scrollHeight > el.clientHeight
|
|
219
|
-
) {
|
|
220
|
+
if ((overflowY === 'auto' || overflowY === 'scroll') && el.scrollHeight > el.clientHeight) {
|
|
220
221
|
return el;
|
|
221
222
|
}
|
|
222
223
|
}
|
|
@@ -381,7 +382,10 @@ function bootstrap(runtimeConfig: typeof config): void {
|
|
|
381
382
|
element as React.ReactNode
|
|
382
383
|
);
|
|
383
384
|
const wrapped = createElement(TimberNuqsAdapter, null, withNav);
|
|
384
|
-
const rootElement = createElement(TransitionRoot, {
|
|
385
|
+
const rootElement = createElement(TransitionRoot, {
|
|
386
|
+
initial: wrapped,
|
|
387
|
+
topLoaderConfig: _config.topLoader,
|
|
388
|
+
});
|
|
385
389
|
_reactRoot = hydrateRoot(document, rootElement, {
|
|
386
390
|
// Suppress recoverable hydration errors from deny/error signals
|
|
387
391
|
// inside Suspense boundaries. The server already handled these
|
package/src/client/link.tsx
CHANGED
|
@@ -335,7 +335,11 @@ export function Link({
|
|
|
335
335
|
// Call onNavigate if provided — allows caller to cancel
|
|
336
336
|
if (onNavigate) {
|
|
337
337
|
let prevented = false;
|
|
338
|
-
onNavigate({
|
|
338
|
+
onNavigate({
|
|
339
|
+
preventDefault: () => {
|
|
340
|
+
prevented = true;
|
|
341
|
+
},
|
|
342
|
+
});
|
|
339
343
|
if (prevented) {
|
|
340
344
|
event.preventDefault();
|
|
341
345
|
return;
|
|
@@ -352,15 +356,16 @@ export function Link({
|
|
|
352
356
|
: userOnClick; // External links — just pass through user's onClick
|
|
353
357
|
|
|
354
358
|
// ─── Hover prefetch ──────────────────────────────────────────
|
|
355
|
-
const handleMouseEnter =
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
router
|
|
359
|
+
const handleMouseEnter =
|
|
360
|
+
internal && prefetch
|
|
361
|
+
? (event: ReactMouseEvent<HTMLAnchorElement>) => {
|
|
362
|
+
userOnMouseEnter?.(event);
|
|
363
|
+
const router = getRouterOrNull();
|
|
364
|
+
if (router) {
|
|
365
|
+
router.prefetch(resolvedHref);
|
|
366
|
+
}
|
|
361
367
|
}
|
|
362
|
-
|
|
363
|
-
: userOnMouseEnter;
|
|
368
|
+
: userOnMouseEnter;
|
|
364
369
|
|
|
365
370
|
return (
|
|
366
371
|
<a {...rest} href={resolvedHref} onClick={handleClick} onMouseEnter={handleMouseEnter}>
|
package/src/client/router.ts
CHANGED
|
@@ -7,11 +7,7 @@ import { HistoryStack } from './history';
|
|
|
7
7
|
import type { HeadElement } from './head';
|
|
8
8
|
import { setCurrentParams } from './use-params.js';
|
|
9
9
|
import { setNavigationState } from './navigation-context.js';
|
|
10
|
-
import {
|
|
11
|
-
SegmentElementCache,
|
|
12
|
-
cacheSegmentElements,
|
|
13
|
-
mergeSegmentTree,
|
|
14
|
-
} from './segment-merger.js';
|
|
10
|
+
import { SegmentElementCache, cacheSegmentElements, mergeSegmentTree } from './segment-merger.js';
|
|
15
11
|
import { fetchRscPayload, RedirectError } from './rsc-fetch.js';
|
|
16
12
|
import type { FetchResult } from './rsc-fetch.js';
|
|
17
13
|
|
|
@@ -421,7 +417,9 @@ export function createRouter(deps: RouterDeps): RouterInstance {
|
|
|
421
417
|
setPending(true, url);
|
|
422
418
|
try {
|
|
423
419
|
const headElements = await renderViaTransition(url, async () => {
|
|
424
|
-
const stateTree = segmentCache.serializeStateTree(
|
|
420
|
+
const stateTree = segmentCache.serializeStateTree(
|
|
421
|
+
segmentElementCache.getMergeablePaths()
|
|
422
|
+
);
|
|
425
423
|
const result = await fetchRscPayload(url, deps, stateTree);
|
|
426
424
|
updateSegmentCache(result.segmentInfo);
|
|
427
425
|
updateNavigationState(result.params, url);
|
|
@@ -52,7 +52,12 @@ interface SegmentProviderProps {
|
|
|
52
52
|
* Wraps each layout to provide segment position context.
|
|
53
53
|
* Injected by rsc-entry.ts during element tree construction.
|
|
54
54
|
*/
|
|
55
|
-
export function SegmentProvider({
|
|
55
|
+
export function SegmentProvider({
|
|
56
|
+
segments,
|
|
57
|
+
segmentId: _segmentId,
|
|
58
|
+
parallelRouteKeys,
|
|
59
|
+
children,
|
|
60
|
+
}: SegmentProviderProps) {
|
|
56
61
|
const value = useMemo(
|
|
57
62
|
() => ({ segments, parallelRouteKeys }),
|
|
58
63
|
// segments and parallelRouteKeys are static per layout — they don't change
|
|
@@ -186,10 +186,7 @@ function walkChildren(children: ReactNode, out: CachedSegmentEntry[]): void {
|
|
|
186
186
|
* Cache all segment subtrees from a fully-rendered RSC element tree.
|
|
187
187
|
* Call this after every full RSC payload render (navigate, refresh, hydration).
|
|
188
188
|
*/
|
|
189
|
-
export function cacheSegmentElements(
|
|
190
|
-
element: unknown,
|
|
191
|
-
cache: SegmentElementCache
|
|
192
|
-
): void {
|
|
189
|
+
export function cacheSegmentElements(element: unknown, cache: SegmentElementCache): void {
|
|
193
190
|
const segments = extractSegments(element);
|
|
194
191
|
for (const entry of segments) {
|
|
195
192
|
cache.set(entry.segmentPath, entry);
|
|
@@ -208,10 +205,7 @@ export function cacheSegmentElements(
|
|
|
208
205
|
*/
|
|
209
206
|
type TreePath = Array<{ element: ReactElement; childIndex: number }>;
|
|
210
207
|
|
|
211
|
-
function findSegmentProviderPath(
|
|
212
|
-
node: ReactElement,
|
|
213
|
-
targetPath?: string
|
|
214
|
-
): TreePath | null {
|
|
208
|
+
function findSegmentProviderPath(node: ReactElement, targetPath?: string): TreePath | null {
|
|
215
209
|
const children = (node.props as { children?: ReactNode }).children;
|
|
216
210
|
if (children == null) return null;
|
|
217
211
|
|
|
@@ -29,7 +29,7 @@ const RELOAD_FLAG_KEY = '__timber_stale_reload';
|
|
|
29
29
|
export function isStaleClientReference(error: unknown): boolean {
|
|
30
30
|
if (!(error instanceof Error)) return false;
|
|
31
31
|
const msg = error.message;
|
|
32
|
-
return msg.includes('Could not find the module');
|
|
32
|
+
return msg.includes('Could not find the module') || msg.includes('client reference not found');
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
/**
|
|
@@ -48,8 +48,8 @@ export function triggerStaleReload(): boolean {
|
|
|
48
48
|
if (sessionStorage.getItem(RELOAD_FLAG_KEY)) {
|
|
49
49
|
console.warn(
|
|
50
50
|
'[timber] Stale client reference detected again after reload. ' +
|
|
51
|
-
|
|
52
|
-
|
|
51
|
+
'Not reloading to prevent infinite loop. ' +
|
|
52
|
+
'This may indicate a deployment issue — try a hard refresh.'
|
|
53
53
|
);
|
|
54
54
|
return false;
|
|
55
55
|
}
|
|
@@ -59,7 +59,7 @@ export function triggerStaleReload(): boolean {
|
|
|
59
59
|
|
|
60
60
|
console.warn(
|
|
61
61
|
'[timber] Stale client reference detected — the server has been ' +
|
|
62
|
-
|
|
62
|
+
'redeployed with new bundles. Reloading to pick up the new version.'
|
|
63
63
|
);
|
|
64
64
|
|
|
65
65
|
window.location.reload();
|
|
@@ -67,9 +67,7 @@ export function triggerStaleReload(): boolean {
|
|
|
67
67
|
} catch {
|
|
68
68
|
// sessionStorage may be unavailable (private browsing, storage full, etc.)
|
|
69
69
|
// Fall back to reloading without loop protection
|
|
70
|
-
console.warn(
|
|
71
|
-
'[timber] Stale client reference detected. Reloading page.'
|
|
72
|
-
);
|
|
70
|
+
console.warn('[timber] Stale client reference detected. Reloading page.');
|
|
73
71
|
window.location.reload();
|
|
74
72
|
return true;
|
|
75
73
|
}
|
|
@@ -183,18 +183,19 @@ export function TopLoader({ config }: { config?: TopLoaderConfig }): React.React
|
|
|
183
183
|
};
|
|
184
184
|
|
|
185
185
|
// Clean up the finishing phase when the finish animation completes.
|
|
186
|
-
const handleAnimationEnd =
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
186
|
+
const handleAnimationEnd =
|
|
187
|
+
phase === 'finishing'
|
|
188
|
+
? (e: React.AnimationEvent) => {
|
|
189
|
+
if (e.animationName === FINISH_KEYFRAMES) {
|
|
190
|
+
setPhase('hidden');
|
|
191
|
+
}
|
|
190
192
|
}
|
|
191
|
-
|
|
192
|
-
: undefined;
|
|
193
|
+
: undefined;
|
|
193
194
|
|
|
194
195
|
return createElement(
|
|
195
196
|
'div',
|
|
196
197
|
{
|
|
197
|
-
style: containerStyle,
|
|
198
|
+
'style': containerStyle,
|
|
198
199
|
'aria-hidden': 'true',
|
|
199
200
|
'data-timber-top-loader': '',
|
|
200
201
|
},
|
|
@@ -62,7 +62,13 @@ let _navigateTransition:
|
|
|
62
62
|
* Non-navigation renders:
|
|
63
63
|
* transitionRender(newWrappedElement);
|
|
64
64
|
*/
|
|
65
|
-
export function TransitionRoot({
|
|
65
|
+
export function TransitionRoot({
|
|
66
|
+
initial,
|
|
67
|
+
topLoaderConfig,
|
|
68
|
+
}: {
|
|
69
|
+
initial: ReactNode;
|
|
70
|
+
topLoaderConfig?: TopLoaderConfig;
|
|
71
|
+
}): ReactNode {
|
|
66
72
|
const [element, setElement] = useState<ReactNode>(initial);
|
|
67
73
|
const [pendingUrl, setPendingUrl] = useState<string | null>(null);
|
|
68
74
|
const [, startTransition] = useTransition();
|
package/src/fonts/css.ts
CHANGED
|
@@ -39,6 +39,7 @@ export function generateFontFaces(descriptors: FontFaceDescriptor[]): string {
|
|
|
39
39
|
* ```css
|
|
40
40
|
* .timber-font-inter {
|
|
41
41
|
* --font-sans: 'Inter', 'Inter Fallback', system-ui, sans-serif;
|
|
42
|
+
* font-family: 'Inter', 'Inter Fallback', system-ui, sans-serif;
|
|
42
43
|
* }
|
|
43
44
|
* ```
|
|
44
45
|
*/
|
|
@@ -47,7 +48,7 @@ export function generateVariableClass(
|
|
|
47
48
|
variable: string,
|
|
48
49
|
fontFamily: string
|
|
49
50
|
): string {
|
|
50
|
-
return `.${className} {\n ${variable}: ${fontFamily};\n}`;
|
|
51
|
+
return `.${className} {\n ${variable}: ${fontFamily};\n font-family: ${fontFamily};\n}`;
|
|
51
52
|
}
|
|
52
53
|
|
|
53
54
|
/**
|
package/src/fonts/local.ts
CHANGED
|
@@ -100,18 +100,22 @@ export function generateFamilyName(sources: LocalFontSrc[]): string {
|
|
|
100
100
|
* Generate @font-face descriptors for local font sources.
|
|
101
101
|
*
|
|
102
102
|
* Each source entry produces one @font-face rule. The `src` descriptor
|
|
103
|
-
* uses a `url()` pointing to the
|
|
103
|
+
* uses a `url()` pointing to the served path under `/_timber/fonts/`.
|
|
104
|
+
* The `urlPrefix` defaults to `/_timber/fonts` — the path used by both
|
|
105
|
+
* the dev server middleware and the production build output.
|
|
104
106
|
*/
|
|
105
107
|
export function generateLocalFontFaces(
|
|
106
108
|
family: string,
|
|
107
109
|
sources: LocalFontSrc[],
|
|
108
|
-
display: string
|
|
110
|
+
display: string,
|
|
111
|
+
urlPrefix = '/_timber/fonts'
|
|
109
112
|
): FontFaceDescriptor[] {
|
|
110
113
|
return sources.map((entry) => {
|
|
111
114
|
const format = inferFontFormat(entry.path);
|
|
115
|
+
const basename = entry.path.split('/').pop() ?? entry.path;
|
|
112
116
|
return {
|
|
113
117
|
family,
|
|
114
|
-
src: `url('${
|
|
118
|
+
src: `url('${urlPrefix}/${basename}') format('${format}')`,
|
|
115
119
|
weight: entry.weight,
|
|
116
120
|
style: entry.style,
|
|
117
121
|
display,
|
package/src/index.ts
CHANGED
|
@@ -46,6 +46,18 @@ export interface ResolvedClientJavascript {
|
|
|
46
46
|
|
|
47
47
|
export interface TimberUserConfig {
|
|
48
48
|
output?: 'server' | 'static';
|
|
49
|
+
/**
|
|
50
|
+
* Enable timber debug logging in production builds.
|
|
51
|
+
*
|
|
52
|
+
* When `true`, timber's own diagnostics (dev warnings, verbose logging)
|
|
53
|
+
* are active even in production mode. React stays in production mode —
|
|
54
|
+
* only timber's logs are affected.
|
|
55
|
+
*
|
|
56
|
+
* Can also be enabled at runtime via the `TIMBER_DEBUG` environment variable.
|
|
57
|
+
*
|
|
58
|
+
* Default: `false`.
|
|
59
|
+
*/
|
|
60
|
+
debug?: boolean;
|
|
49
61
|
/**
|
|
50
62
|
* Control client-side JavaScript output.
|
|
51
63
|
*
|
|
@@ -95,6 +107,22 @@ export interface TimberUserConfig {
|
|
|
95
107
|
/** Array of signing secrets for key rotation. Index 0 signs; all verify. */
|
|
96
108
|
secrets?: string[];
|
|
97
109
|
};
|
|
110
|
+
/**
|
|
111
|
+
* Control Server-Timing header output.
|
|
112
|
+
*
|
|
113
|
+
* - `'detailed'` — per-phase breakdown (proxy, middleware, render). Useful
|
|
114
|
+
* for APM / network monitoring. Exposes phase names to clients.
|
|
115
|
+
* - `'total'` — single `total;dur=N` entry. Safe to expose, gives
|
|
116
|
+
* browser DevTools useful timing without internal details.
|
|
117
|
+
* - `false` — no Server-Timing header at all.
|
|
118
|
+
*
|
|
119
|
+
* Default: `'detailed'` in dev, `'total'` in production.
|
|
120
|
+
*
|
|
121
|
+
* This is separate from `debug` / `TIMBER_DEBUG` — it's an intentional
|
|
122
|
+
* decision to expose timing data to clients, not a side effect of debug
|
|
123
|
+
* logging.
|
|
124
|
+
*/
|
|
125
|
+
serverTiming?: 'detailed' | 'total' | false;
|
|
98
126
|
/**
|
|
99
127
|
* Override the app directory location. By default, timber auto-detects
|
|
100
128
|
* `app/` at the project root, falling back to `src/app/`.
|
|
@@ -400,8 +428,13 @@ export function timber(config?: TimberUserConfig): PluginOption[] {
|
|
|
400
428
|
ssr: 'virtual:timber-ssr-entry',
|
|
401
429
|
client: 'virtual:timber-browser-entry',
|
|
402
430
|
},
|
|
403
|
-
//
|
|
404
|
-
//
|
|
431
|
+
// Group all client reference wrappers into a single chunk instead of
|
|
432
|
+
// creating one tiny file per "use client" module. Without this, each
|
|
433
|
+
// server chunk's client references become a separate entry point,
|
|
434
|
+
// producing many sub-500B wrapper files (e.g., 30-byte re-exports).
|
|
435
|
+
// A single group eliminates 10+ unnecessary HTTP requests.
|
|
436
|
+
// See design/27-chunking-strategy.md and TIM-440.
|
|
437
|
+
clientChunks: () => 'client-refs',
|
|
405
438
|
});
|
|
406
439
|
}
|
|
407
440
|
);
|
|
@@ -80,7 +80,17 @@ interface RouteInfo {
|
|
|
80
80
|
entryFilePath: string | null;
|
|
81
81
|
}
|
|
82
82
|
|
|
83
|
-
/**
|
|
83
|
+
/**
|
|
84
|
+
* Walk the route tree and collect all leaf routes (pages + API endpoints).
|
|
85
|
+
*
|
|
86
|
+
* Parallel slots (`@artists`, `@shows`, etc.) are intentionally skipped —
|
|
87
|
+
* they render alongside the parent page at the same URL and are not
|
|
88
|
+
* separately URL-addressable. Their JS is captured in shared/layout chunks.
|
|
89
|
+
*
|
|
90
|
+
* After collection, entries are deduplicated by URL path so that overlapping
|
|
91
|
+
* route groups (e.g. `(browse)` and `(marketing)` both producing `/`) only
|
|
92
|
+
* appear once. The entry with the largest route-specific size wins.
|
|
93
|
+
*/
|
|
84
94
|
export function collectRoutes(tree: RouteTree): RouteInfo[] {
|
|
85
95
|
const routes: RouteInfo[] = [];
|
|
86
96
|
|
|
@@ -95,12 +105,22 @@ export function collectRoutes(tree: RouteTree): RouteInfo[] {
|
|
|
95
105
|
routes.push({ path, segments: currentChain, entryFilePath: node.route.filePath });
|
|
96
106
|
}
|
|
97
107
|
|
|
108
|
+
// Recurse into child segments only — skip parallel slots (node.slots)
|
|
98
109
|
for (const child of node.children) walk(child, currentChain);
|
|
99
|
-
for (const slot of node.slots.values()) walk(slot, currentChain);
|
|
100
110
|
}
|
|
101
111
|
|
|
102
112
|
walk(tree.root, []);
|
|
103
|
-
|
|
113
|
+
|
|
114
|
+
// Deduplicate entries with the same URL path (e.g. from overlapping route groups).
|
|
115
|
+
// Keep the entry with the longest segment chain (most specific match).
|
|
116
|
+
const seen = new Map<string, RouteInfo>();
|
|
117
|
+
for (const route of routes) {
|
|
118
|
+
const existing = seen.get(route.path);
|
|
119
|
+
if (!existing || route.segments.length > existing.segments.length) {
|
|
120
|
+
seen.set(route.path, route);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return Array.from(seen.values());
|
|
104
124
|
}
|
|
105
125
|
|
|
106
126
|
// ─── Report formatting ────────────────────────────────────────────────────
|
package/src/plugins/entries.ts
CHANGED
|
@@ -110,6 +110,8 @@ function generateConfigModule(ctx: PluginContext): string {
|
|
|
110
110
|
slowRequestMs: ctx.config.slowRequestMs ?? 3000,
|
|
111
111
|
cookieSecrets,
|
|
112
112
|
topLoader: ctx.config.topLoader,
|
|
113
|
+
debug: ctx.config.debug ?? false,
|
|
114
|
+
serverTiming: ctx.config.serverTiming,
|
|
113
115
|
};
|
|
114
116
|
|
|
115
117
|
return [
|
|
@@ -128,11 +130,14 @@ function generateConfigModule(ctx: PluginContext): string {
|
|
|
128
130
|
* Checks for instrumentation.ts, .js, and .mjs — matching the same
|
|
129
131
|
* extensions as timber.config.ts detection.
|
|
130
132
|
*/
|
|
131
|
-
function detectInstrumentationFile(root: string): string | null {
|
|
133
|
+
export function detectInstrumentationFile(root: string): string | null {
|
|
132
134
|
const extensions = ['.ts', '.js', '.mjs'];
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
135
|
+
const dirs = [root, resolve(root, 'src')];
|
|
136
|
+
for (const dir of dirs) {
|
|
137
|
+
for (const ext of extensions) {
|
|
138
|
+
const candidate = resolve(dir, `instrumentation${ext}`);
|
|
139
|
+
if (existsSync(candidate)) return candidate;
|
|
140
|
+
}
|
|
136
141
|
}
|
|
137
142
|
return null;
|
|
138
143
|
}
|