@timber-js/app 0.2.0-alpha.136 → 0.2.0-alpha.138

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.
@@ -1 +1 @@
1
- {"version":3,"file":"pipeline-phases.d.ts","sourceRoot":"","sources":["../../src/server/pipeline-phases.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAoBH,OAAO,EAA2C,KAAK,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAEpG,OAAO,EAAqB,KAAK,YAAY,EAAE,MAAM,uBAAuB,CAAC;AAC7E,OAAO,KAAK,EAAE,mBAAmB,EAAE,cAAc,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAKrF,UAAU,aAAa;IACrB,iBAAiB,EAAE,MAAM,CAAC;IAC1B,YAAY,CAAC,EAAE,mBAAmB,CAAC;CACpC;AAwBD;;;;;GAKG;AACH,wBAAsB,aAAa,CACjC,MAAM,EAAE,cAAc,EACtB,QAAQ,EAAE,aAAa,EACvB,GAAG,EAAE,OAAO,EACZ,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,MAAM,GACX,OAAO,CAAC,YAAY,CAAC,CAavB;AAID;;;;GAIG;AACH,wBAAsB,kBAAkB,CACtC,MAAM,EAAE,cAAc,EACtB,GAAG,EAAE,OAAO,EACZ,KAAK,EAAE,UAAU,EACjB,eAAe,EAAE,OAAO,EACxB,oBAAoB,EAAE,OAAO,EAC7B,aAAa,EAAE,aAAa,GAC3B,OAAO,CAAC,YAAY,CAAC,CA6DvB;AAID;;;GAGG;AACH,wBAAsB,cAAc,CAClC,MAAM,EAAE,cAAc,EACtB,GAAG,EAAE,OAAO,EACZ,KAAK,EAAE,UAAU,EACjB,eAAe,EAAE,OAAO,EACxB,oBAAoB,EAAE,OAAO,EAC7B,EAAE,iBAAiB,EAAE,YAAY,EAAE,EAAE,aAAa,GACjD,OAAO,CAAC,YAAY,CAAC,CAkBvB;AAID;;;;;;;;;;;;;;GAcG;AACH,wBAAsB,aAAa,CACjC,MAAM,EAAE,cAAc,EACtB,GAAG,EAAE,OAAO,EACZ,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,MAAM,EACZ,eAAe,CAAC,EAAE,OAAO,GACxB,OAAO,CAAC,QAAQ,CAAC,CAwRnB"}
1
+ {"version":3,"file":"pipeline-phases.d.ts","sourceRoot":"","sources":["../../src/server/pipeline-phases.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAoBH,OAAO,EAA2C,KAAK,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAEpG,OAAO,EAAqB,KAAK,YAAY,EAAE,MAAM,uBAAuB,CAAC;AAC7E,OAAO,KAAK,EAAE,mBAAmB,EAAE,cAAc,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAKrF,UAAU,aAAa;IACrB,iBAAiB,EAAE,MAAM,CAAC;IAC1B,YAAY,CAAC,EAAE,mBAAmB,CAAC;CACpC;AAwBD;;;;;GAKG;AACH,wBAAsB,aAAa,CACjC,MAAM,EAAE,cAAc,EACtB,QAAQ,EAAE,aAAa,EACvB,GAAG,EAAE,OAAO,EACZ,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,MAAM,GACX,OAAO,CAAC,YAAY,CAAC,CAmBvB;AAID;;;;GAIG;AACH,wBAAsB,kBAAkB,CACtC,MAAM,EAAE,cAAc,EACtB,GAAG,EAAE,OAAO,EACZ,KAAK,EAAE,UAAU,EACjB,eAAe,EAAE,OAAO,EACxB,oBAAoB,EAAE,OAAO,EAC7B,aAAa,EAAE,aAAa,GAC3B,OAAO,CAAC,YAAY,CAAC,CA6DvB;AAID;;;GAGG;AACH,wBAAsB,cAAc,CAClC,MAAM,EAAE,cAAc,EACtB,GAAG,EAAE,OAAO,EACZ,KAAK,EAAE,UAAU,EACjB,eAAe,EAAE,OAAO,EACxB,oBAAoB,EAAE,OAAO,EAC7B,EAAE,iBAAiB,EAAE,YAAY,EAAE,EAAE,aAAa,GACjD,OAAO,CAAC,YAAY,CAAC,CAkBvB;AAID;;;;;;;;;;;;;;GAcG;AACH,wBAAsB,aAAa,CACjC,MAAM,EAAE,cAAc,EACtB,GAAG,EAAE,OAAO,EACZ,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,MAAM,EACZ,eAAe,CAAC,EAAE,OAAO,GACxB,OAAO,CAAC,QAAQ,CAAC,CAqSnB"}
@@ -1 +1 @@
1
- {"version":3,"file":"route-handler.d.ts","sourceRoot":"","sources":["../../src/server/route-handler.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAK/C,+DAA+D;AAC/D,MAAM,MAAM,UAAU,GAAG,KAAK,GAAG,MAAM,GAAG,KAAK,GAAG,OAAO,GAAG,QAAQ,GAAG,MAAM,GAAG,SAAS,CAAC;AAE1F,2DAA2D;AAC3D,MAAM,MAAM,YAAY,GAAG,CAAC,GAAG,EAAE,YAAY,KAAK,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAC;AAE/E,wEAAwE;AACxE,MAAM,MAAM,WAAW,GAAG;KACvB,CAAC,IAAI,UAAU,CAAC,CAAC,EAAE,YAAY;CACjC,CAAC;AAOF;;;;;;;GAOG;AACH,wBAAgB,qBAAqB,CAAC,GAAG,EAAE,WAAW,GAAG,UAAU,EAAE,CAyBpE;AAID;;;;;GAKG;AACH,wBAAsB,kBAAkB,CAAC,GAAG,EAAE,WAAW,EAAE,GAAG,EAAE,YAAY,GAAG,OAAO,CAAC,QAAQ,CAAC,CAyC/F"}
1
+ {"version":3,"file":"route-handler.d.ts","sourceRoot":"","sources":["../../src/server/route-handler.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAM/C,+DAA+D;AAC/D,MAAM,MAAM,UAAU,GAAG,KAAK,GAAG,MAAM,GAAG,KAAK,GAAG,OAAO,GAAG,QAAQ,GAAG,MAAM,GAAG,SAAS,CAAC;AAE1F,2DAA2D;AAC3D,MAAM,MAAM,YAAY,GAAG,CAAC,GAAG,EAAE,YAAY,KAAK,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAC;AAE/E,wEAAwE;AACxE,MAAM,MAAM,WAAW,GAAG;KACvB,CAAC,IAAI,UAAU,CAAC,CAAC,EAAE,YAAY;CACjC,CAAC;AAOF;;;;;;;GAOG;AACH,wBAAgB,qBAAqB,CAAC,GAAG,EAAE,WAAW,GAAG,UAAU,EAAE,CAyBpE;AAID;;;;;GAKG;AACH,wBAAsB,kBAAkB,CAAC,GAAG,EAAE,WAAW,EAAE,GAAG,EAAE,YAAY,GAAG,OAAO,CAAC,QAAQ,CAAC,CAyC/F"}
@@ -1 +1 @@
1
- {"version":3,"file":"api-handler.d.ts","sourceRoot":"","sources":["../../../src/server/rsc-entry/api-handler.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAGH,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,qBAAqB,CAAC;AAO/D;;;;;;;;;GASG;AACH,wBAAsB,cAAc,CAClC,GAAG,EAAE,OAAO,EACZ,QAAQ,EAAE,mBAAmB,EAAE,EAC/B,eAAe,EAAE,OAAO,GACvB,OAAO,CAAC,QAAQ,CAAC,CAqDnB"}
1
+ {"version":3,"file":"api-handler.d.ts","sourceRoot":"","sources":["../../../src/server/rsc-entry/api-handler.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAGH,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,qBAAqB,CAAC;AAO/D;;;;;;;;;GASG;AACH,wBAAsB,cAAc,CAClC,GAAG,EAAE,OAAO,EACZ,QAAQ,EAAE,mBAAmB,EAAE,EAC/B,eAAe,EAAE,OAAO,GACvB,OAAO,CAAC,QAAQ,CAAC,CAsDnB"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@timber-js/app",
3
- "version": "0.2.0-alpha.136",
3
+ "version": "0.2.0-alpha.138",
4
4
  "description": "Vite-native React framework built for Servers and Serverless Platforms — correct HTTP semantics, real status codes, pages that work without JavaScript",
5
5
  "keywords": [
6
6
  "cloudflare-workers",
@@ -114,11 +114,6 @@
114
114
  "publishConfig": {
115
115
  "access": "public"
116
116
  },
117
- "scripts": {
118
- "build": "vite build --config vite.lib.config.ts && tsc --emitDeclarationOnly --project tsconfig.json --outDir dist",
119
- "typecheck": "tsgo --noEmit",
120
- "prepublishOnly": "pnpm run build"
121
- },
122
117
  "dependencies": {
123
118
  "@opentelemetry/api": "^1.9.1",
124
119
  "@opentelemetry/context-async-hooks": "^2.7.1",
@@ -162,5 +157,9 @@
162
157
  },
163
158
  "engines": {
164
159
  "node": ">=22.12.0"
160
+ },
161
+ "scripts": {
162
+ "build": "vite build --config vite.lib.config.ts && tsc --emitDeclarationOnly --project tsconfig.json --outDir dist",
163
+ "typecheck": "tsgo --noEmit"
165
164
  }
166
- }
165
+ }
package/src/cli.ts CHANGED
File without changes
@@ -165,6 +165,12 @@ export function setupNavigationApi(callbacks: NavigationApiCallbacks): Navigatio
165
165
  // Skip download requests
166
166
  if (event.downloadRequest) return;
167
167
 
168
+ // Skip blob: URLs — these are almost always downloads or object-URL
169
+ // navigations initiated by the host page (e.g., generated files, PDFs).
170
+ // The RSC pipeline cannot handle them, and intercepting would break
171
+ // the download/open behavior the host page expects.
172
+ if (event.destination.url.startsWith('blob:')) return;
173
+
168
174
  // Skip hash-only changes — let the browser handle scroll-to-anchor
169
175
  if (event.hashChange) return;
170
176
 
@@ -88,6 +88,12 @@ export async function runProxyPhase(
88
88
  );
89
89
  return { kind: 'response', phase: 'proxy', response };
90
90
  } catch (error) {
91
+ if (error instanceof RedirectSignal) {
92
+ return { kind: 'redirect', phase: 'proxy', signal: error };
93
+ }
94
+ if (error instanceof DenySignal) {
95
+ return { kind: 'deny', phase: 'proxy', signal: error };
96
+ }
91
97
  return { kind: 'error', phase: 'proxy', error };
92
98
  }
93
99
  }
@@ -313,6 +319,19 @@ export async function handleRequest(
313
319
  headers: { 'Content-Type': `${contentType}; charset=utf-8` },
314
320
  });
315
321
  } catch (error) {
322
+ // Control-flow signals from the metadata handler (e.g. a sitemap that
323
+ // calls deny(404) for an unknown tenant, or a dynamic icon that
324
+ // redirects to a CDN-hosted asset) translate to proper HTTP responses
325
+ // instead of being logged as 500s.
326
+ if (error instanceof RedirectSignal) {
327
+ return new Response(null, {
328
+ status: error.status,
329
+ headers: { Location: error.location },
330
+ });
331
+ }
332
+ if (error instanceof DenySignal) {
333
+ return new Response(null, { status: error.status });
334
+ }
316
335
  logRenderError({ method, path, error });
317
336
  if (config.onPipelineError && error instanceof Error)
318
337
  config.onPipelineError(error, 'metadata-route');
@@ -10,6 +10,7 @@
10
10
 
11
11
  import type { RouteContext } from './types.js';
12
12
  import { logRouteError } from './logger.js';
13
+ import { DenySignal, RedirectSignal } from './primitives.js';
13
14
 
14
15
  // ─── Types ───────────────────────────────────────────────────────────────
15
16
 
@@ -123,6 +124,12 @@ async function runHandler(handler: RouteHandler, ctx: RouteContext): Promise<Res
123
124
  const res = await handler(ctx);
124
125
  return mergeResponseHeaders(res, ctx.headers);
125
126
  } catch (error) {
127
+ // Control-flow signals — propagate to the API route dispatcher so deny()
128
+ // gets the JSON status-file chain and redirect() gets a proper 3xx with
129
+ // Location. Not errors; don't log them.
130
+ if (error instanceof DenySignal || error instanceof RedirectSignal) {
131
+ throw error;
132
+ }
126
133
  logRouteError({ method: ctx.req.method, path: new URL(ctx.req.url).pathname, error });
127
134
  return new Response(null, { status: 500 });
128
135
  }
@@ -61,13 +61,8 @@ export async function handleApiRoute(
61
61
  }
62
62
  );
63
63
  } catch (error) {
64
- if (error instanceof DenySignal) {
65
- return renderApiDeny(error, segments, responseHeaders);
66
- }
67
- if (error instanceof RedirectSignal) {
68
- responseHeaders.set('Location', error.location);
69
- return new Response(null, { status: error.status, headers: responseHeaders });
70
- }
64
+ const signalResponse = await translateApiSignal(error, req, segments, responseHeaders);
65
+ if (signalResponse) return signalResponse;
71
66
  throw error;
72
67
  }
73
68
  }
@@ -80,7 +75,44 @@ export async function handleApiRoute(
80
75
  // the request-context ALS that the pipeline already populated.
81
76
  const routeMod = await loadModule<RouteModule>(leaf.route!);
82
77
  const ctx: RouteContext = { req, headers: responseHeaders };
83
- return handleRouteRequest(routeMod, ctx);
78
+ try {
79
+ return await handleRouteRequest(routeMod, ctx);
80
+ } catch (error) {
81
+ const signalResponse = await translateApiSignal(error, req, segments, responseHeaders);
82
+ if (signalResponse) return signalResponse;
83
+ throw error;
84
+ }
85
+ }
86
+
87
+ /**
88
+ * Translate a control-flow signal thrown from access.ts or a route.ts
89
+ * method handler into the proper API response: DenySignal → JSON status-file
90
+ * chain via `renderApiDeny`, RedirectSignal → bare 3xx with Location header.
91
+ *
92
+ * For HEAD requests, strip the deny body to preserve HEAD semantics — the
93
+ * normal HEAD path in `handleRouteRequest` strips bodies after the handler
94
+ * returns, but signals bypass that codepath.
95
+ *
96
+ * Returns `null` for non-signal errors so the caller knows to rethrow.
97
+ */
98
+ async function translateApiSignal(
99
+ error: unknown,
100
+ req: Request,
101
+ segments: ManifestSegmentNode[],
102
+ responseHeaders: Headers
103
+ ): Promise<Response | null> {
104
+ if (error instanceof DenySignal) {
105
+ const res = await renderApiDeny(error, segments, responseHeaders);
106
+ if (req.method.toUpperCase() === 'HEAD') {
107
+ return new Response(null, { status: res.status, headers: res.headers });
108
+ }
109
+ return res;
110
+ }
111
+ if (error instanceof RedirectSignal) {
112
+ responseHeaders.set('Location', error.location);
113
+ return new Response(null, { status: error.status, headers: responseHeaders });
114
+ }
115
+ return null;
84
116
  }
85
117
 
86
118
  /**