@avelonjs/next 0.3.1 → 0.3.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.
package/README.md CHANGED
@@ -91,6 +91,7 @@ Writes become server actions in `framework/routing/actions.generated.ts`. `.api(
91
91
  | `dispatch` | `(controller, action, uri, formData) => Promise<NextNativeResponse>` | Server-action entry used by generated writes. |
92
92
  | `handleRoute` | `(controller, action, uri, request, params?) => Promise<NextNativeResponse>` | JSON route-handler entry used by `.api()` files. |
93
93
  | `reportFailure` | `(scope, uri, error) => void` | Writes a request failure and its cause chain to stderr. |
94
+ | `reportMappedFailure` | `(scope, uri, result) => void` | Writes a 5xx action result the kernel mapped; input failures stay unlogged. |
94
95
  | `middleware` | `(request, routes?) => Promise<NextNativeResponse>` | `proxy.ts` / `middleware.ts` bridge. Pass generated routes on the Node proxy. |
95
96
  | `MiddlewareRoute` | `interface` | Method, path, and middleware aliases for Edge auth. |
96
97
  | `setCookie` | `(name, value, options?) => void` | Queues a cookie write for the current Next request. |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@avelonjs/next",
3
- "version": "0.3.1",
3
+ "version": "0.3.3",
4
4
  "private": false,
5
5
  "description": "Next.js adapter for Avelon routing, server actions, and pages.",
6
6
  "license": "MIT",
package/src/adapter.ts CHANGED
@@ -54,22 +54,43 @@ export interface NextAdapterOptions {
54
54
  capabilities?: Partial<typeof nextCapabilities>
55
55
  }
56
56
 
57
- let boundAdapter: NextAdapter | undefined
58
- let runtimeBoot: (() => Promise<void>) | undefined
57
+ interface MountState {
58
+ adapter: NextAdapter | undefined
59
+ boot: (() => Promise<void>) | undefined
60
+ }
61
+
62
+ const MOUNT_STATE = Symbol.for('avelon.next.mount')
63
+
64
+ /**
65
+ * The mount lives on `globalThis` because Next compiles one build into several module graphs.
66
+ *
67
+ * `instrumentation.ts` is assigned its own webpack layer, so a module it imports is duplicated into
68
+ * the layer serving requests: two copies of this file, two sets of module-level bindings. Boot ran
69
+ * in the instrumentation copy and a server action read the request copy, which had never been
70
+ * mounted and held no boot function to mount it — `dispatch` answered `Next adapter has not been
71
+ * mounted.` while pages worked, because the root layout boots the copy they render in. Next shares
72
+ * its own cross-entry state the same way (`globalThis.__incrementalCache`).
73
+ */
74
+ function mountState(): MountState {
75
+ const host = globalThis as typeof globalThis & { [MOUNT_STATE]?: MountState }
76
+ host[MOUNT_STATE] ??= { adapter: undefined, boot: undefined }
77
+ return host[MOUNT_STATE]
78
+ }
59
79
 
60
80
  /** Returns the adapter last passed to {@link bindAdapter}. */
61
81
  export function getAdapter(): NextAdapter {
62
- if (boundAdapter === undefined) {
82
+ const adapter = mountState().adapter
83
+ if (adapter === undefined) {
63
84
  throw new Invalid('Next adapter has not been mounted.', {
64
85
  metadata: { fields: { adapter: ['Call NextAdapter.mount() before page() or dispatch().'] } },
65
86
  })
66
87
  }
67
- return boundAdapter
88
+ return adapter
68
89
  }
69
90
 
70
91
  /** Records the adapter generated code and runtime helpers dispatch through. */
71
92
  export function bindAdapter(adapter: NextAdapter | undefined): void {
72
- boundAdapter = adapter
93
+ mountState().adapter = adapter
73
94
  }
74
95
 
75
96
  /**
@@ -77,13 +98,14 @@ export function bindAdapter(adapter: NextAdapter | undefined): void {
77
98
  * is not yet attached. Next may render a page before the root layout finishes.
78
99
  */
79
100
  export function setRuntimeBoot(boot: (() => Promise<void>) | undefined): void {
80
- runtimeBoot = boot
101
+ mountState().boot = boot
81
102
  }
82
103
 
83
104
  /** Returns the bound adapter, booting the runtime first when necessary. */
84
105
  export async function ensureAdapter(): Promise<NextAdapter> {
85
- if (boundAdapter !== undefined) return boundAdapter
86
- if (runtimeBoot !== undefined) await runtimeBoot()
106
+ const state = mountState()
107
+ if (state.adapter !== undefined) return state.adapter
108
+ if (state.boot !== undefined) await state.boot()
87
109
  return getAdapter()
88
110
  }
89
111
 
package/src/dispatch.ts CHANGED
@@ -2,7 +2,7 @@ import { finishResponse, mapKernelException, type ControllerClass } from '@avelo
2
2
 
3
3
  import { ensureAdapter, findMountedRoute } from './adapter'
4
4
  import { readIncomingCookies, withCookies } from './cookies'
5
- import { reportFailure } from './failures'
5
+ import { reportFailure, reportMappedFailure } from './failures'
6
6
  import { isNextControlFlow, type NextNativeResponse } from './response'
7
7
 
8
8
  /**
@@ -51,6 +51,7 @@ export async function dispatch(
51
51
  const request = await adapter.toRequest({ kind: 'form', uri, formData, cookies })
52
52
  const route = findMountedRoute(controller, action, uri)
53
53
  const result = await kernel.dispatch(route, request)
54
+ reportMappedFailure('action', uri, result)
54
55
  const response = await adapter.toResponse(result)
55
56
  await finishResponse()
56
57
  return response
@@ -89,6 +90,7 @@ export async function handleRoute(
89
90
  const http = await adapter.toRequest({ kind: 'http', request, params })
90
91
  const route = findMountedRoute(controller, action, uri)
91
92
  const result = await kernel.dispatch(route, http)
93
+ reportMappedFailure('route', uri, result)
92
94
  const response = await adapter.toResponse(result)
93
95
  await finishResponse()
94
96
  return response
package/src/failures.ts CHANGED
@@ -16,6 +16,26 @@ export function reportFailure(scope: string, uri: string, error: unknown): void
16
16
  console.error(lines.join('\n'))
17
17
  }
18
18
 
19
+ /**
20
+ * Writes a service failure the kernel already turned into a result.
21
+ *
22
+ * `mapKernelException` answers `Unavailable` with a 503 action result, so the request completes and
23
+ * the reason reaches the form. Nothing reaches the log, which leaves an operator reading a vendor
24
+ * sentence in a browser and no record of the deployment that produced it. Input failures stay
25
+ * unlogged: a mistyped password is not an incident.
26
+ */
27
+ export function reportMappedFailure(
28
+ scope: string,
29
+ uri: string,
30
+ result: { type?: string; status?: number; errors?: Readonly<Record<string, readonly string[]>> },
31
+ ): void {
32
+ if (result.type !== 'action' || (result.status ?? 0) < 500) return
33
+ const messages = Object.entries(result.errors ?? {})
34
+ .map(([field, list]) => `${field}: ${list.join('; ')}`)
35
+ .join(' | ')
36
+ console.error(`[avelon] ${scope} ${uri} failed: ${result.status} ${messages}`)
37
+ }
38
+
19
39
  function describe(error: unknown): string {
20
40
  if (error instanceof AvelonError) {
21
41
  return `${error.name}(${error.code}) ${error.message} ${JSON.stringify(error.metadata)}`
package/src/index.ts CHANGED
@@ -11,7 +11,7 @@ export {
11
11
  type NextNativeRequest,
12
12
  } from './adapter'
13
13
  export { clearCookie, setCookie, type PendingCookie, type ViewRegistry } from './cookies'
14
- export { reportFailure } from './failures'
14
+ export { reportFailure, reportMappedFailure } from './failures'
15
15
  export { generateRouter } from './codegen'
16
16
  export { dispatch, handleRoute } from './dispatch'
17
17
  export { middleware, type MiddlewareRoute } from './middleware'