@timber-js/app 0.2.0-alpha.84 → 0.2.0-alpha.85

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 (58) hide show
  1. package/LICENSE +8 -0
  2. package/dist/_chunks/{actions-YHRCboUO.js → actions-DLnUaR65.js} +2 -2
  3. package/dist/_chunks/{actions-YHRCboUO.js.map → actions-DLnUaR65.js.map} +1 -1
  4. package/dist/_chunks/{chunk-DYhsFzuS.js → chunk-BYIpzuS7.js} +7 -1
  5. package/dist/_chunks/{define-cookie-C9pquwOg.js → define-cookie-BowvzoP0.js} +4 -4
  6. package/dist/_chunks/{define-cookie-C9pquwOg.js.map → define-cookie-BowvzoP0.js.map} +1 -1
  7. package/dist/_chunks/{request-context-Dl0hXED3.js → request-context-CK5tZqIP.js} +2 -2
  8. package/dist/_chunks/{request-context-Dl0hXED3.js.map → request-context-CK5tZqIP.js.map} +1 -1
  9. package/dist/client/form.d.ts +4 -1
  10. package/dist/client/form.d.ts.map +1 -1
  11. package/dist/client/index.js +2 -2
  12. package/dist/client/index.js.map +1 -1
  13. package/dist/config-validation.d.ts +51 -0
  14. package/dist/config-validation.d.ts.map +1 -0
  15. package/dist/cookies/index.js +1 -1
  16. package/dist/index.d.ts.map +1 -1
  17. package/dist/index.js +1168 -51
  18. package/dist/index.js.map +1 -1
  19. package/dist/plugins/dev-404-page.d.ts +56 -0
  20. package/dist/plugins/dev-404-page.d.ts.map +1 -0
  21. package/dist/plugins/dev-error-overlay.d.ts +14 -11
  22. package/dist/plugins/dev-error-overlay.d.ts.map +1 -1
  23. package/dist/plugins/dev-error-page.d.ts +58 -0
  24. package/dist/plugins/dev-error-page.d.ts.map +1 -0
  25. package/dist/plugins/dev-server.d.ts.map +1 -1
  26. package/dist/plugins/dev-terminal-error.d.ts +28 -0
  27. package/dist/plugins/dev-terminal-error.d.ts.map +1 -0
  28. package/dist/plugins/entries.d.ts.map +1 -1
  29. package/dist/plugins/fonts.d.ts +4 -0
  30. package/dist/plugins/fonts.d.ts.map +1 -1
  31. package/dist/plugins/routing.d.ts.map +1 -1
  32. package/dist/routing/convention-lint.d.ts +41 -0
  33. package/dist/routing/convention-lint.d.ts.map +1 -0
  34. package/dist/server/action-client.d.ts +13 -5
  35. package/dist/server/action-client.d.ts.map +1 -1
  36. package/dist/server/fallback-error.d.ts +9 -5
  37. package/dist/server/fallback-error.d.ts.map +1 -1
  38. package/dist/server/index.js +2 -2
  39. package/dist/server/index.js.map +1 -1
  40. package/dist/server/internal.js +2 -2
  41. package/dist/server/rsc-entry/index.d.ts.map +1 -1
  42. package/package.json +6 -7
  43. package/src/cli.ts +0 -0
  44. package/src/client/form.tsx +10 -5
  45. package/src/config-validation.ts +299 -0
  46. package/src/index.ts +17 -0
  47. package/src/plugins/dev-404-page.ts +418 -0
  48. package/src/plugins/dev-error-overlay.ts +165 -54
  49. package/src/plugins/dev-error-page.ts +536 -0
  50. package/src/plugins/dev-server.ts +63 -10
  51. package/src/plugins/dev-terminal-error.ts +217 -0
  52. package/src/plugins/entries.ts +3 -0
  53. package/src/plugins/fonts.ts +3 -2
  54. package/src/plugins/routing.ts +37 -5
  55. package/src/routing/convention-lint.ts +356 -0
  56. package/src/server/action-client.ts +17 -9
  57. package/src/server/fallback-error.ts +39 -88
  58. package/src/server/rsc-entry/index.ts +34 -2
@@ -140,13 +140,13 @@ export interface ActionBuilder<TCtx> {
140
140
  /** Define the action body without input validation. */
141
141
  action<TData>(
142
142
  fn: (ctx: ActionContext<TCtx, undefined>) => Promise<TData>
143
- ): ActionFn<undefined, TData>;
143
+ ): ActionFn<TData, undefined>;
144
144
  }
145
145
 
146
146
  /** Builder after .schema() has been called. */
147
147
  export interface ActionBuilderWithSchema<TCtx, TInput> {
148
148
  /** Define the action body with validated input. */
149
- action<TData>(fn: (ctx: ActionContext<TCtx, TInput>) => Promise<TData>): ActionFn<TInput, TData>;
149
+ action<TData>(fn: (ctx: ActionContext<TCtx, TInput>) => Promise<TData>): ActionFn<TData, TInput>;
150
150
  }
151
151
 
152
152
  /**
@@ -169,10 +169,18 @@ export interface ActionBuilderWithSchema<TCtx, TInput> {
169
169
  export type InputHint<T> =
170
170
  T extends Record<string, unknown> ? { [K in keyof T]: string | undefined } : T;
171
171
 
172
- export type ActionFn<TInput = unknown, TData = unknown> = {
172
+ /**
173
+ * ActionFn — the callable returned by `createActionClient().action()`.
174
+ *
175
+ * Generic order: `<TData, TInput>` — TData first for backward compatibility.
176
+ * Previously ActionFn had a single `<TData>` generic, so existing code like
177
+ * `ActionFn<MyResult>` must still work with TData in the first position.
178
+ * See TIM-797.
179
+ */
180
+ export type ActionFn<TData = unknown, TInput = unknown> = {
173
181
  /** <form action={fn}> compatibility — React discards the return value. */
174
182
  (formData: FormData): void;
175
- /** Direct call: action(input) — optional when TInput is undefined (no-schema actions). */
183
+ /** Direct call: action(input) — optional when TInput is undefined/unknown (no-schema actions). */
176
184
  (
177
185
  ...args: undefined extends TInput ? [input?: TInput] : [input: TInput]
178
186
  ): Promise<ActionResult<TData>>;
@@ -310,7 +318,7 @@ export function createActionClient<TCtx = Record<string, never>>(
310
318
  function buildAction<TInput, TData>(
311
319
  schema: ActionSchema<TInput> | undefined,
312
320
  fn: (ctx: ActionContext<TCtx, TInput>) => Promise<TData>
313
- ): ActionFn<TInput, TData> {
321
+ ): ActionFn<TData, TInput> {
314
322
  async function actionHandler(...args: unknown[]): Promise<ActionResult<TData>> {
315
323
  try {
316
324
  // Run middleware
@@ -401,7 +409,7 @@ export function createActionClient<TCtx = Record<string, never>>(
401
409
  }
402
410
  }
403
411
 
404
- return actionHandler as ActionFn<TInput, TData>;
412
+ return actionHandler as ActionFn<TData, TInput>;
405
413
  }
406
414
 
407
415
  return {
@@ -409,14 +417,14 @@ export function createActionClient<TCtx = Record<string, never>>(
409
417
  return {
410
418
  action<TData>(
411
419
  fn: (ctx: ActionContext<TCtx, TInput>) => Promise<TData>
412
- ): ActionFn<TInput, TData> {
420
+ ): ActionFn<TData, TInput> {
413
421
  return buildAction(schema, fn);
414
422
  },
415
423
  };
416
424
  },
417
425
  action<TData>(
418
426
  fn: (ctx: ActionContext<TCtx, undefined>) => Promise<TData>
419
- ): ActionFn<undefined, TData> {
427
+ ): ActionFn<TData, undefined> {
420
428
  return buildAction(undefined, fn as (ctx: ActionContext<TCtx, unknown>) => Promise<TData>);
421
429
  },
422
430
  };
@@ -445,7 +453,7 @@ export function createActionClient<TCtx = Record<string, never>>(
445
453
  export function validated<TInput, TData>(
446
454
  schema: ActionSchema<TInput>,
447
455
  handler: (input: TInput) => Promise<TData>
448
- ): ActionFn<TInput, TData> {
456
+ ): ActionFn<TData, TInput> {
449
457
  return createActionClient()
450
458
  .schema(schema)
451
459
  .action(async ({ input }) => handler(input));
@@ -31,10 +31,11 @@ export async function renderFallbackError(
31
31
  isDev: boolean,
32
32
  rootSegment: ManifestSegmentNode,
33
33
  clientBootstrap: ClientBootstrapConfig,
34
- globalError?: GlobalErrorFile
34
+ globalError?: GlobalErrorFile,
35
+ projectRoot?: string
35
36
  ): Promise<Response> {
36
37
  if (isDev) {
37
- return renderDevErrorPage(error);
38
+ return renderDevErrorPage(error, projectRoot);
38
39
  }
39
40
  // Lazy import to avoid loading error-renderer in the pipeline module
40
41
  const { renderErrorPage } = await import('./rsc-entry/error-renderer.js');
@@ -75,89 +76,35 @@ export async function renderFallbackError(
75
76
  }
76
77
 
77
78
  /**
78
- * Render a dev-mode 500 error page with error message and stack trace.
79
+ * Render a dev-mode 500 error page with error details, source context,
80
+ * classified stack trace, and copy button.
79
81
  *
80
- * Returns an HTML Response that displays the error in a styled page.
81
- * The Vite HMR client script is included so the error overlay still fires.
82
+ * Dynamically imports the shared template from `plugins/dev-error-page.ts`
83
+ * so it is NOT pulled into production server bundles. The Vite client script
84
+ * is injected so the error overlay fires when the HMR WebSocket connects.
85
+ *
86
+ * Dev-only — the dynamic import has zero production cost.
82
87
  */
83
- export function renderDevErrorPage(error: unknown): Response {
88
+ export async function renderDevErrorPage(error: unknown, projectRoot?: string): Promise<Response> {
84
89
  const err = error instanceof Error ? error : new Error(String(error));
85
- const title = err.name || 'Error';
86
- const message = escapeHtml(err.message);
87
- const stack = err.stack ? escapeHtml(err.stack) : '';
90
+ const root = projectRoot ?? process.cwd();
88
91
 
89
- const html = `<!DOCTYPE html>
90
- <html lang="en">
91
- <head>
92
- <meta charset="utf-8">
93
- <meta name="viewport" content="width=device-width, initial-scale=1">
94
- <title>500 ${escapeHtml(title)}</title>
95
- <script type="module" src="/@vite/client"></script>
96
- <style>
97
- *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
98
- body {
99
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
100
- background: #1a1a2e;
101
- color: #e0e0e0;
102
- padding: 2rem;
103
- line-height: 1.6;
104
- }
105
- .container { max-width: 800px; margin: 0 auto; }
106
- .badge {
107
- display: inline-block;
108
- background: #e74c3c;
109
- color: white;
110
- font-size: 0.75rem;
111
- font-weight: 700;
112
- padding: 0.2rem 0.6rem;
113
- border-radius: 4px;
114
- text-transform: uppercase;
115
- letter-spacing: 0.05em;
116
- margin-bottom: 1rem;
117
- }
118
- h1 {
119
- font-size: 1.5rem;
120
- color: #ff6b6b;
121
- margin-bottom: 0.5rem;
122
- word-break: break-word;
123
- }
124
- .message {
125
- font-size: 1.1rem;
126
- color: #ccc;
127
- margin-bottom: 1.5rem;
128
- word-break: break-word;
129
- }
130
- .stack-container {
131
- background: #16213e;
132
- border: 1px solid #2a2a4a;
133
- border-radius: 8px;
134
- padding: 1rem;
135
- overflow-x: auto;
136
- }
137
- .stack {
138
- font-family: 'SF Mono', 'Fira Code', 'Fira Mono', Menlo, Consolas, monospace;
139
- font-size: 0.8rem;
140
- color: #a0a0c0;
141
- white-space: pre-wrap;
142
- word-break: break-all;
143
- }
144
- .hint {
145
- margin-top: 1.5rem;
146
- font-size: 0.85rem;
147
- color: #666;
148
- }
149
- </style>
150
- </head>
151
- <body>
152
- <div class="container">
153
- <span class="badge">500 Internal Server Error</span>
154
- <h1>${escapeHtml(title)}</h1>
155
- <p class="message">${message}</p>
156
- ${stack ? `<div class="stack-container"><pre class="stack">${stack}</pre></div>` : ''}
157
- <p class="hint">This error page is only shown in development.</p>
158
- </div>
159
- </body>
160
- </html>`;
92
+ let html: string;
93
+ try {
94
+ // Dynamic import — keeps dev-error-page.ts and its transitive deps
95
+ // (dev-error-overlay.ts, @jridgewell/trace-mapping) out of production bundles.
96
+ const { generateDevErrorPage } = await import('../plugins/dev-error-page.js');
97
+ html = generateDevErrorPage(err, 'render', root);
98
+ // Inject Vite client script so the error overlay fires when HMR connects.
99
+ html = html.replace(
100
+ '</head>',
101
+ ' <script type="module" src="/@vite/client"></script>\n</head>'
102
+ );
103
+ } catch {
104
+ // If the shared template fails (e.g., circular dep, missing module),
105
+ // fall back to a minimal error page.
106
+ html = minimalErrorPage(err);
107
+ }
161
108
 
162
109
  return new Response(html, {
163
110
  status: 500,
@@ -165,11 +112,15 @@ export function renderDevErrorPage(error: unknown): Response {
165
112
  });
166
113
  }
167
114
 
168
- function escapeHtml(str: string): string {
169
- return str
170
- .replace(/&/g, '&amp;')
171
- .replace(/</g, '&lt;')
172
- .replace(/>/g, '&gt;')
173
- .replace(/"/g, '&quot;')
174
- .replace(/'/g, '&#x27;');
115
+ /**
116
+ * Minimal fallback error page — used only if the shared template fails to load.
117
+ */
118
+ function minimalErrorPage(err: Error): string {
119
+ const esc = (s: string) => s.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
120
+ return `<!DOCTYPE html><html><head><meta charset="utf-8"><title>500</title>
121
+ <script type="module" src="/@vite/client"></script></head>
122
+ <body style="font-family:system-ui;padding:2rem;background:#1a1a2e;color:#e0e0e0">
123
+ <h1 style="color:#ff6b6b">500 Internal Server Error</h1>
124
+ <pre style="color:#a0a0c0;white-space:pre-wrap">${esc(err.stack ?? err.message)}</pre>
125
+ </body></html>`;
175
126
  }
@@ -247,7 +247,34 @@ async function createRequestHandler(manifest: typeof routeManifest, runtimeConfi
247
247
  );
248
248
  },
249
249
  renderNoMatch: async (req: Request, responseHeaders: Headers) => {
250
- return renderNoMatchPage(req, manifest.root, responseHeaders, clientBootstrap);
250
+ const response = await renderNoMatchPage(
251
+ req,
252
+ manifest.root,
253
+ responseHeaders,
254
+ clientBootstrap
255
+ );
256
+
257
+ // In dev mode, if the pipeline returned a bare 404 (no body — meaning
258
+ // no user-defined 404.tsx was found), replace it with a helpful dev
259
+ // page that lists available routes and suggests similar paths.
260
+ // Only for GET requests that accept HTML — HEAD/API/RSC clients
261
+ // should receive the bare 404 without an HTML body (TIM-793).
262
+ const acceptsHtml = (req.headers.get('accept') ?? '').includes('text/html');
263
+ if (isDev && !response.body && req.method === 'GET' && acceptsHtml) {
264
+ const { generateDev404Page, collectRoutes } = await import('../../plugins/dev-404-page.js');
265
+ const routes = collectRoutes(manifest.root);
266
+ const pathname = new URL(req.url).pathname;
267
+ const html = generateDev404Page(pathname, routes);
268
+ return new Response(html, {
269
+ status: 404,
270
+ headers: {
271
+ ...Object.fromEntries(responseHeaders.entries()),
272
+ 'content-type': 'text/html; charset=utf-8',
273
+ },
274
+ });
275
+ }
276
+
277
+ return response;
251
278
  },
252
279
  interceptionRewrites: manifest.interceptionRewrites,
253
280
  // Slow request threshold from timber.config.ts. Default 3000ms, 0 to disable.
@@ -276,7 +303,12 @@ async function createRequestHandler(manifest: typeof routeManifest, runtimeConfi
276
303
  isDev,
277
304
  manifest.root,
278
305
  clientBootstrap,
279
- manifest.globalError
306
+ manifest.globalError,
307
+ // Project root for dev error page frame classification.
308
+ // Not in runtimeConfig (TIM-787: leaked to client bundles).
309
+ // manifest.root is the resolved Vite root — correct even when
310
+ // CWD differs from project root (e.g., monorepo custom root) (TIM-807).
311
+ isDev ? manifest.root : undefined
280
312
  ),
281
313
  // Auto-generated sitemap handler — enabled when sitemap.enabled is true
282
314
  // and no user-authored sitemap exists at the app root.