@timber-js/app 0.1.12 → 0.1.14
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/client/error-boundary.d.ts +7 -0
- package/dist/client/error-boundary.d.ts.map +1 -1
- package/dist/client/error-boundary.js +4 -7
- package/dist/client/error-boundary.js.map +1 -1
- package/dist/client/index.js +12 -6
- package/dist/client/index.js.map +1 -1
- package/dist/client/use-router.d.ts.map +1 -1
- package/dist/index.js +27 -4
- package/dist/index.js.map +1 -1
- package/dist/plugins/shims.d.ts.map +1 -1
- package/dist/server/slot-resolver.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/client/error-boundary.tsx +21 -20
- package/src/client/use-router.ts +15 -3
- package/src/plugins/shims.ts +31 -4
- package/src/server/slot-resolver.ts +24 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"shims.d.ts","sourceRoot":"","sources":["../../src/plugins/shims.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,MAAM,CAAC;AAGnC,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AA6DhD;;;;GAIG;AACH,wBAAgB,WAAW,CAAC,IAAI,EAAE,aAAa,GAAG,MAAM,
|
|
1
|
+
{"version":3,"file":"shims.d.ts","sourceRoot":"","sources":["../../src/plugins/shims.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,MAAM,CAAC;AAGnC,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AA6DhD;;;;GAIG;AACH,wBAAgB,WAAW,CAAC,IAAI,EAAE,aAAa,GAAG,MAAM,CAkIvD"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"slot-resolver.d.ts","sourceRoot":"","sources":["../../src/server/slot-resolver.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAMH,OAAO,KAAK,EAAE,mBAAmB,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAErE,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,oBAAoB,CAAC;AAE9D,KAAK,eAAe,GAAG,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,KAAK,KAAK,CAAC,YAAY,CAAC;AAElE;;;;;;;;;;GAUG;AACH,wBAAsB,kBAAkB,CACtC,QAAQ,EAAE,mBAAmB,EAC7B,KAAK,EAAE,UAAU,EACjB,aAAa,EAAE,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC,CAAC,EACzD,CAAC,EAAE,eAAe,EAClB,YAAY,CAAC,EAAE,mBAAmB,GACjC,OAAO,CAAC,KAAK,CAAC,YAAY,GAAG,IAAI,CAAC,
|
|
1
|
+
{"version":3,"file":"slot-resolver.d.ts","sourceRoot":"","sources":["../../src/server/slot-resolver.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAMH,OAAO,KAAK,EAAE,mBAAmB,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAErE,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,oBAAoB,CAAC;AAE9D,KAAK,eAAe,GAAG,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,KAAK,KAAK,CAAC,YAAY,CAAC;AAElE;;;;;;;;;;GAUG;AACH,wBAAsB,kBAAkB,CACtC,QAAQ,EAAE,mBAAmB,EAC7B,KAAK,EAAE,UAAU,EACjB,aAAa,EAAE,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC,CAAC,EACzD,CAAC,EAAE,eAAe,EAClB,YAAY,CAAC,EAAE,mBAAmB,GACjC,OAAO,CAAC,KAAK,CAAC,YAAY,GAAG,IAAI,CAAC,CAkKpC"}
|
package/package.json
CHANGED
|
@@ -71,6 +71,13 @@ export interface TimberErrorBoundaryProps {
|
|
|
71
71
|
* 400 = any 4xx, 500 = any 5xx, specific number = exact match.
|
|
72
72
|
*/
|
|
73
73
|
status?: number;
|
|
74
|
+
/**
|
|
75
|
+
* When true, catching a DenySignal sets _denyHandledByBoundary on the
|
|
76
|
+
* nav context to prevent page-level deny promotion. Only slot catch-all
|
|
77
|
+
* boundaries should set this — segment boundaries (403.tsx, 4xx.tsx,
|
|
78
|
+
* error.tsx) must NOT, otherwise normal page denies get swallowed.
|
|
79
|
+
*/
|
|
80
|
+
isSlotBoundary?: boolean;
|
|
74
81
|
children: ReactNode;
|
|
75
82
|
}
|
|
76
83
|
|
|
@@ -99,26 +106,6 @@ export class TimberErrorBoundary extends Component<
|
|
|
99
106
|
return { hasError: false, error: null };
|
|
100
107
|
}
|
|
101
108
|
|
|
102
|
-
// Report DenySignal handling to prevent page-level promotion.
|
|
103
|
-
// When a slot's error boundary catches a DenySignal, the RSC onError
|
|
104
|
-
// callback has already tracked it globally. Setting this flag tells
|
|
105
|
-
// the RSC entry not to promote the denial to page-level (which would
|
|
106
|
-
// replace the entire SSR response). See LOCAL-298.
|
|
107
|
-
const digest = (error as { digest?: string }).digest;
|
|
108
|
-
if (typeof digest === 'string') {
|
|
109
|
-
try {
|
|
110
|
-
const parsed = JSON.parse(digest);
|
|
111
|
-
if (parsed?.type === 'deny') {
|
|
112
|
-
const ssrData = getSsrData();
|
|
113
|
-
if (ssrData?._navContext) {
|
|
114
|
-
ssrData._navContext._denyHandledByBoundary = true;
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
} catch {
|
|
118
|
-
// Not a JSON digest — ignore
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
|
|
122
109
|
return { hasError: true, error };
|
|
123
110
|
}
|
|
124
111
|
|
|
@@ -161,6 +148,20 @@ export class TimberErrorBoundary extends Component<
|
|
|
161
148
|
}
|
|
162
149
|
}
|
|
163
150
|
|
|
151
|
+
// Report DenySignal handling to prevent page-level promotion — but only
|
|
152
|
+
// for slot boundaries. Segment boundaries (403.tsx, 4xx.tsx, error.tsx)
|
|
153
|
+
// must NOT set this flag, otherwise normal page/hold-window denies get
|
|
154
|
+
// swallowed as 200 with boundary HTML instead of the intended 4xx.
|
|
155
|
+
// Runs here in render() (not getDerivedStateFromError) so the status
|
|
156
|
+
// filter has already been applied — non-matching boundaries re-threw above.
|
|
157
|
+
// See LOCAL-298.
|
|
158
|
+
if (parsed?.type === 'deny' && this.props.isSlotBoundary) {
|
|
159
|
+
const ssrData = getSsrData();
|
|
160
|
+
if (ssrData?._navContext) {
|
|
161
|
+
ssrData._navContext._denyHandledByBoundary = true;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
164
165
|
// Render the fallback component with the right props shape.
|
|
165
166
|
if (parsed?.type === 'deny') {
|
|
166
167
|
return createElement(this.props.fallbackComponent as never, {
|
package/src/client/use-router.ts
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
* AppRouterInstance shape that ecosystem libraries expect.
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
+
import { startTransition } from 'react';
|
|
12
13
|
import { getRouter } from './router-ref.js';
|
|
13
14
|
|
|
14
15
|
export interface AppRouterInstance {
|
|
@@ -56,13 +57,24 @@ export function useRouter(): AppRouterInstance {
|
|
|
56
57
|
|
|
57
58
|
return {
|
|
58
59
|
push(href: string, options?: { scroll?: boolean }) {
|
|
59
|
-
|
|
60
|
+
// Wrap in startTransition so React 19 tracks the async navigation.
|
|
61
|
+
// React 19's startTransition accepts async callbacks — it keeps
|
|
62
|
+
// isPending=true until the returned promise resolves. This means
|
|
63
|
+
// useTransition's isPending reflects the full RSC fetch + render
|
|
64
|
+
// lifecycle when wrapping router.push() in startTransition.
|
|
65
|
+
startTransition(async () => {
|
|
66
|
+
await router.navigate(href, { scroll: options?.scroll });
|
|
67
|
+
});
|
|
60
68
|
},
|
|
61
69
|
replace(href: string, options?: { scroll?: boolean }) {
|
|
62
|
-
|
|
70
|
+
startTransition(async () => {
|
|
71
|
+
await router.navigate(href, { scroll: options?.scroll, replace: true });
|
|
72
|
+
});
|
|
63
73
|
},
|
|
64
74
|
refresh() {
|
|
65
|
-
|
|
75
|
+
startTransition(async () => {
|
|
76
|
+
await router.refresh();
|
|
77
|
+
});
|
|
66
78
|
},
|
|
67
79
|
back() {
|
|
68
80
|
window.history.back();
|
package/src/plugins/shims.ts
CHANGED
|
@@ -177,11 +177,38 @@ export function timberShims(_ctx: PluginContext): Plugin {
|
|
|
177
177
|
// Server modules must never be bundled into the browser — if this
|
|
178
178
|
// module is reached, there is a broken import chain that needs fixing.
|
|
179
179
|
if (id === '\0timber:server-empty') {
|
|
180
|
-
|
|
181
|
-
|
|
180
|
+
// Export named stubs instead of throwing at evaluation time.
|
|
181
|
+
// Throwing at eval breaks the module system — the browser can't
|
|
182
|
+
// resolve named imports like `import { notFound } from '...'`.
|
|
183
|
+
// Instead, each stub throws at call time with a clear message.
|
|
184
|
+
return `
|
|
185
|
+
const msg = "[timber] @timber-js/app/server was imported from client code. " +
|
|
182
186
|
"Server modules (headers, cookies, redirect, deny, etc.) cannot be used in client components. " +
|
|
183
|
-
"If you need these APIs, move the import to a server component or middleware."
|
|
184
|
-
)
|
|
187
|
+
"If you need these APIs, move the import to a server component or middleware.";
|
|
188
|
+
function stub() { throw new Error(msg); }
|
|
189
|
+
export const headers = stub;
|
|
190
|
+
export const cookies = stub;
|
|
191
|
+
export const searchParams = stub;
|
|
192
|
+
export const deny = stub;
|
|
193
|
+
export const notFound = stub;
|
|
194
|
+
export const redirect = stub;
|
|
195
|
+
export const permanentRedirect = stub;
|
|
196
|
+
export const redirectExternal = stub;
|
|
197
|
+
export const waitUntil = stub;
|
|
198
|
+
export const RenderError = stub;
|
|
199
|
+
export const RedirectType = {};
|
|
200
|
+
export const DenySignal = stub;
|
|
201
|
+
export const RedirectSignal = stub;
|
|
202
|
+
export const createPipeline = stub;
|
|
203
|
+
export const revalidatePath = stub;
|
|
204
|
+
export const revalidateTag = stub;
|
|
205
|
+
export const createActionClient = stub;
|
|
206
|
+
export const ActionError = stub;
|
|
207
|
+
export const validated = stub;
|
|
208
|
+
export const getFormFlash = stub;
|
|
209
|
+
export const parseFormData = stub;
|
|
210
|
+
export const coerce = stub;
|
|
211
|
+
`;
|
|
185
212
|
}
|
|
186
213
|
},
|
|
187
214
|
};
|
|
@@ -186,6 +186,7 @@ export async function resolveSlotElement(
|
|
|
186
186
|
// See design/02-rendering-pipeline.md §"Slot Access Failure = Graceful Degradation"
|
|
187
187
|
element = h(TimberErrorBoundary, {
|
|
188
188
|
fallbackComponent: SlotErrorFallback,
|
|
189
|
+
isSlotBoundary: true,
|
|
189
190
|
children: element,
|
|
190
191
|
});
|
|
191
192
|
|
|
@@ -257,6 +258,12 @@ function findSlotMatch(slotNode: ManifestSegmentNode, match: RouteMatch): SlotMa
|
|
|
257
258
|
if (slotNode.page) {
|
|
258
259
|
return { page: slotNode.page, chain: [slotNode] };
|
|
259
260
|
}
|
|
261
|
+
// Check for optional-catch-all child — [[...slug]] matches zero segments
|
|
262
|
+
for (const child of slotNode.children ?? []) {
|
|
263
|
+
if (child.segmentType === 'optional-catch-all' && child.page) {
|
|
264
|
+
return { page: child.page, chain: [slotNode, child] };
|
|
265
|
+
}
|
|
266
|
+
}
|
|
260
267
|
return null;
|
|
261
268
|
}
|
|
262
269
|
|
|
@@ -280,11 +287,27 @@ function findSlotMatch(slotNode: ManifestSegmentNode, match: RouteMatch): SlotMa
|
|
|
280
287
|
// Try dynamic segments if no static match
|
|
281
288
|
if (!found) {
|
|
282
289
|
for (const child of directChildren) {
|
|
283
|
-
if (child.segmentType === 'dynamic'
|
|
290
|
+
if (child.segmentType === 'dynamic') {
|
|
291
|
+
found = child;
|
|
292
|
+
break;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// Try catch-all segments — these consume ALL remaining URL segments,
|
|
298
|
+
// so we break out of the outer loop immediately.
|
|
299
|
+
if (!found) {
|
|
300
|
+
for (const child of directChildren) {
|
|
301
|
+
if (child.segmentType === 'catch-all' || child.segmentType === 'optional-catch-all') {
|
|
284
302
|
found = child;
|
|
285
303
|
break;
|
|
286
304
|
}
|
|
287
305
|
}
|
|
306
|
+
if (found) {
|
|
307
|
+
chain.push(found);
|
|
308
|
+
currentNode = found;
|
|
309
|
+
break;
|
|
310
|
+
}
|
|
288
311
|
}
|
|
289
312
|
|
|
290
313
|
// Try group children (transparent)
|