@fabrika/app 0.0.1

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.
@@ -0,0 +1,75 @@
1
+ // ─── Standard Schema v1 ──────────────────────────────────────────────
2
+ //
3
+ // The validator-agnostic contract (https://standardschema.dev) — a tiny spec authored by the
4
+ // zod / valibot / arktype maintainers. Any conforming validator exposes a `~standard` property, so
5
+ // @fabrika/app accepts ANY of them for `.input()` / `.output()` (not just zod). It is the modern,
6
+ // library-neutral successor to a bespoke `{ parse(input): T }` interface (which is really zod-only —
7
+ // valibot/arktype don't expose `.parse()`).
8
+ //
9
+ // Vendored as TYPES ONLY (the spec explicitly blesses copying it in rather than taking a dependency);
10
+ // erased at build, so validation libraries remain optional. Verbatim from the spec.
11
+
12
+ /** The Standard Schema interface. */
13
+ export interface StandardSchemaV1<Input = unknown, Output = Input> {
14
+ /** The Standard Schema properties. */
15
+ readonly '~standard': StandardSchemaV1.Props<Input, Output>
16
+ }
17
+
18
+ export declare namespace StandardSchemaV1 {
19
+ /** The Standard Schema properties interface. */
20
+ export interface Props<Input = unknown, Output = Input> {
21
+ /** The version number of the standard. */
22
+ readonly version: 1
23
+ /** The vendor name of the schema library. */
24
+ readonly vendor: string
25
+ /** Validates unknown input values. */
26
+ readonly validate: (value: unknown) => Result<Output> | Promise<Result<Output>>
27
+ /** Inferred types associated with the schema. */
28
+ readonly types?: Types<Input, Output> | undefined
29
+ }
30
+
31
+ /** The result interface of the validate function. */
32
+ export type Result<Output> = SuccessResult<Output> | FailureResult
33
+
34
+ /** The result interface if validation succeeds. */
35
+ export interface SuccessResult<Output> {
36
+ /** The typed output value. */
37
+ readonly value: Output
38
+ /** The non-existent issues. */
39
+ readonly issues?: undefined
40
+ }
41
+
42
+ /** The result interface if validation fails. */
43
+ export interface FailureResult {
44
+ /** The issues of failed validation. */
45
+ readonly issues: ReadonlyArray<Issue>
46
+ }
47
+
48
+ /** The issue interface of the failure output. */
49
+ export interface Issue {
50
+ /** The error message of the issue. */
51
+ readonly message: string
52
+ /** The path of the issue, if any. */
53
+ readonly path?: ReadonlyArray<PropertyKey | PathSegment> | undefined
54
+ }
55
+
56
+ /** The path segment interface of the issue. */
57
+ export interface PathSegment {
58
+ /** The key representing a path segment. */
59
+ readonly key: PropertyKey
60
+ }
61
+
62
+ /** The Standard Schema types interface. */
63
+ export interface Types<Input = unknown, Output = Input> {
64
+ /** The input type of the schema. */
65
+ readonly input: Input
66
+ /** The output type of the schema. */
67
+ readonly output: Output
68
+ }
69
+
70
+ /** Infers the input type of a Standard Schema. */
71
+ export type InferInput<Schema extends StandardSchemaV1> = NonNullable<Schema['~standard']['types']>['input']
72
+
73
+ /** Infers the output type of a Standard Schema. */
74
+ export type InferOutput<Schema extends StandardSchemaV1> = NonNullable<Schema['~standard']['types']>['output']
75
+ }
package/src/testing.ts ADDED
@@ -0,0 +1,119 @@
1
+ import type { FabrikaApp, RequestExecutionContext } from './app.js'
2
+ import { createBunHandler } from './bun.js'
3
+ import { type CloudflareExecutionContext, createCloudflareWorker } from './cloudflare.js'
4
+
5
+ export type AppTestRuntime = 'direct' | 'bun' | 'cloudflare'
6
+
7
+ export interface NormalizedAppResponse {
8
+ readonly status: number
9
+ readonly headers: ReadonlyArray<readonly [name: string, value: string]>
10
+ readonly body: string
11
+ }
12
+
13
+ export interface AppRuntimeResponses {
14
+ readonly direct: NormalizedAppResponse
15
+ readonly bun: NormalizedAppResponse
16
+ readonly cloudflare: NormalizedAppResponse
17
+ }
18
+
19
+ export interface AppRuntimeConformanceOptions<Env> {
20
+ readonly app: FabrikaApp<Env>
21
+ /** Create isolated runtime state so one execution cannot affect the next. */
22
+ readonly createEnv: () => Env | Promise<Env>
23
+ /** Create an equivalent fresh request for each runtime. */
24
+ readonly createRequest: () => Request
25
+ /** Optional sink for rejected background work. */
26
+ readonly onBackgroundError?: (runtime: AppTestRuntime, error: unknown) => void
27
+ }
28
+
29
+ interface TrackedExecution {
30
+ readonly context: RequestExecutionContext
31
+ drain(): Promise<void>
32
+ }
33
+
34
+ function trackedExecution(onBackgroundError: (error: unknown) => void): TrackedExecution {
35
+ const pending = new Set<Promise<void>>()
36
+ const waitUntil = (promise: Promise<unknown>): void => {
37
+ const tracked = promise.then(
38
+ () => undefined,
39
+ (error) => {
40
+ try {
41
+ onBackgroundError(error)
42
+ } catch {
43
+ // Reporting failures must not prevent the conformance run from draining.
44
+ }
45
+ },
46
+ )
47
+ pending.add(tracked)
48
+ void tracked.then(() => pending.delete(tracked))
49
+ }
50
+ return {
51
+ context: { waitUntil },
52
+ async drain() {
53
+ while (pending.size > 0) {
54
+ await Promise.all(pending)
55
+ }
56
+ },
57
+ }
58
+ }
59
+
60
+ /** Convert a Fetch response into a deterministic value suitable for equality checks. */
61
+ export async function normalizeAppResponse(response: Response): Promise<NormalizedAppResponse> {
62
+ const headers: Array<readonly [name: string, value: string]> = []
63
+ response.headers.forEach((value, name) => headers.push([name, value]))
64
+ headers.sort(([left], [right]) => left.localeCompare(right))
65
+ return {
66
+ status: response.status,
67
+ headers,
68
+ body: await response.text(),
69
+ }
70
+ }
71
+
72
+ async function runDirect<Env>(options: AppRuntimeConformanceOptions<Env>): Promise<NormalizedAppResponse> {
73
+ const execution = trackedExecution((error) => options.onBackgroundError?.('direct', error))
74
+ const env = await options.createEnv()
75
+ const request = options.createRequest()
76
+ const response = await options.app.fetch(request, env, execution.context)
77
+ await execution.drain()
78
+ return normalizeAppResponse(response)
79
+ }
80
+
81
+ async function runBun<Env>(options: AppRuntimeConformanceOptions<Env>): Promise<NormalizedAppResponse> {
82
+ const handler = createBunHandler(options.app, await options.createEnv(), {
83
+ onBackgroundError: (error) => options.onBackgroundError?.('bun', error),
84
+ })
85
+ const response = await handler.fetch(options.createRequest())
86
+ await handler.drain()
87
+ return normalizeAppResponse(response)
88
+ }
89
+
90
+ async function runCloudflare<Env>(options: AppRuntimeConformanceOptions<Env>): Promise<NormalizedAppResponse> {
91
+ const execution = trackedExecution((error) => options.onBackgroundError?.('cloudflare', error))
92
+ const context: CloudflareExecutionContext = {
93
+ waitUntil: execution.context.waitUntil,
94
+ passThroughOnException() {},
95
+ }
96
+ const env = await options.createEnv()
97
+ const request = options.createRequest()
98
+ const response = await createCloudflareWorker(options.app).fetch(request, env, context)
99
+ await execution.drain()
100
+ return normalizeAppResponse(response)
101
+ }
102
+
103
+ /** Execute one request through every supported HTTP adapter. */
104
+ export async function runAppRuntimeConformance<Env>(options: AppRuntimeConformanceOptions<Env>): Promise<AppRuntimeResponses> {
105
+ const direct = await runDirect(options)
106
+ const bun = await runBun(options)
107
+ const cloudflare = await runCloudflare(options)
108
+ return { direct, bun, cloudflare }
109
+ }
110
+
111
+ /** Assert adapter parity without coupling application packages to a specific test runner. */
112
+ export async function assertAppRuntimeConformance<Env>(options: AppRuntimeConformanceOptions<Env>): Promise<NormalizedAppResponse> {
113
+ const responses = await runAppRuntimeConformance(options)
114
+ const direct = JSON.stringify(responses.direct)
115
+ if (JSON.stringify(responses.bun) !== direct || JSON.stringify(responses.cloudflare) !== direct) {
116
+ throw new Error(`application runtime responses differ: ${JSON.stringify(responses)}`)
117
+ }
118
+ return responses.direct
119
+ }