@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 +1 -0
- package/package.json +1 -1
- package/src/adapter.ts +30 -8
- package/src/dispatch.ts +3 -1
- package/src/failures.ts +20 -0
- package/src/index.ts +1 -1
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
package/src/adapter.ts
CHANGED
|
@@ -54,22 +54,43 @@ export interface NextAdapterOptions {
|
|
|
54
54
|
capabilities?: Partial<typeof nextCapabilities>
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
-
|
|
58
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
86
|
-
if (
|
|
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'
|