@fragno-dev/core 0.1.2 → 0.1.4
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/.turbo/turbo-build.log +40 -31
- package/CHANGELOG.md +15 -0
- package/LICENSE.md +16 -0
- package/dist/api/api.d.ts +1 -1
- package/dist/api/fragment-builder.d.ts +2 -2
- package/dist/api/fragment-instantiation.d.ts +2 -2
- package/dist/api/fragment-instantiation.js +3 -2
- package/dist/{api-jKNXmz2B.d.ts → api-BX90b4-D.d.ts} +2 -2
- package/dist/{api-jKNXmz2B.d.ts.map → api-BX90b4-D.d.ts.map} +1 -1
- package/dist/client/client.d.ts +2 -2
- package/dist/client/client.js +4 -3
- package/dist/client/client.svelte.d.ts +2 -2
- package/dist/client/client.svelte.js +4 -3
- package/dist/client/client.svelte.js.map +1 -1
- package/dist/client/react.d.ts +2 -2
- package/dist/client/react.js +4 -3
- package/dist/client/react.js.map +1 -1
- package/dist/client/solid.d.ts +2 -2
- package/dist/client/solid.js +4 -3
- package/dist/client/solid.js.map +1 -1
- package/dist/client/vanilla.d.ts +2 -2
- package/dist/client/vanilla.js +4 -3
- package/dist/client/vanilla.js.map +1 -1
- package/dist/client/vue.d.ts +2 -2
- package/dist/client/vue.js +4 -3
- package/dist/client/vue.js.map +1 -1
- package/dist/{client-CzWq6IlK.js → client-C6LChM0Y.js} +4 -3
- package/dist/{client-CzWq6IlK.js.map → client-C6LChM0Y.js.map} +1 -1
- package/dist/{fragment-builder-B3JXWiZB.d.ts → fragment-builder-BZr2JkuW.d.ts} +35 -35
- package/dist/fragment-builder-BZr2JkuW.d.ts.map +1 -0
- package/dist/fragment-builder-DOnCVBqc.js.map +1 -1
- package/dist/{fragment-instantiation-D1q7pltx.js → fragment-instantiation-D74OQjbn.js} +4 -3
- package/dist/fragment-instantiation-D74OQjbn.js.map +1 -0
- package/dist/integrations/react-ssr.js +1 -1
- package/dist/mod.d.ts +2 -2
- package/dist/mod.js +3 -2
- package/dist/route-CTxjMtGZ.js +10 -0
- package/dist/route-CTxjMtGZ.js.map +1 -0
- package/dist/{route-DbBZ3Ep9.js → route-D1MZR6JL.js} +2 -10
- package/dist/route-D1MZR6JL.js.map +1 -0
- package/dist/{ssr-CamRrMc0.js → ssr-BByDVfFD.js} +1 -1
- package/dist/{ssr-CamRrMc0.js.map → ssr-BByDVfFD.js.map} +1 -1
- package/dist/test/test.d.ts +112 -0
- package/dist/test/test.d.ts.map +1 -0
- package/dist/test/test.js +155 -0
- package/dist/test/test.js.map +1 -0
- package/package.json +18 -24
- package/src/api/fragment-builder.ts +0 -1
- package/src/api/request-middleware.test.ts +251 -0
- package/src/api/request-middleware.ts +1 -1
- package/src/test/test.test.ts +504 -0
- package/src/test/test.ts +379 -0
- package/tsdown.config.ts +1 -0
- package/dist/fragment-builder-B3JXWiZB.d.ts.map +0 -1
- package/dist/fragment-instantiation-D1q7pltx.js.map +0 -1
- package/dist/route-DbBZ3Ep9.js.map +0 -1
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import "../api-DngJDcmO.js";
|
|
2
|
+
import { a as RequestOutputContext, o as RequestInputContext, r as resolveRouteFactories } from "../route-D1MZR6JL.js";
|
|
3
|
+
|
|
4
|
+
//#region src/test/test.ts
|
|
5
|
+
/**
|
|
6
|
+
* Parse a Response object into a TestResponse discriminated union
|
|
7
|
+
*/
|
|
8
|
+
async function parseResponse(response) {
|
|
9
|
+
const status = response.status;
|
|
10
|
+
const headers = response.headers;
|
|
11
|
+
if ((headers.get("content-type") || "").includes("application/x-ndjson")) return {
|
|
12
|
+
type: "jsonStream",
|
|
13
|
+
status,
|
|
14
|
+
headers,
|
|
15
|
+
stream: parseNDJSONStream(response)
|
|
16
|
+
};
|
|
17
|
+
const text = await response.text();
|
|
18
|
+
if (!text || text === "null") return {
|
|
19
|
+
type: "empty",
|
|
20
|
+
status,
|
|
21
|
+
headers
|
|
22
|
+
};
|
|
23
|
+
const data = JSON.parse(text);
|
|
24
|
+
if (data && typeof data === "object" && "message" in data && "code" in data) return {
|
|
25
|
+
type: "error",
|
|
26
|
+
status,
|
|
27
|
+
headers,
|
|
28
|
+
error: {
|
|
29
|
+
message: data.message,
|
|
30
|
+
code: data.code
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
return {
|
|
34
|
+
type: "json",
|
|
35
|
+
status,
|
|
36
|
+
headers,
|
|
37
|
+
data
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Parse an NDJSON stream into an async generator
|
|
42
|
+
*/
|
|
43
|
+
async function* parseNDJSONStream(response) {
|
|
44
|
+
if (!response.body) return;
|
|
45
|
+
const reader = response.body.getReader();
|
|
46
|
+
const decoder = new TextDecoder();
|
|
47
|
+
let buffer = "";
|
|
48
|
+
try {
|
|
49
|
+
while (true) {
|
|
50
|
+
const { done, value } = await reader.read();
|
|
51
|
+
if (done) break;
|
|
52
|
+
buffer += decoder.decode(value, { stream: true });
|
|
53
|
+
const lines = buffer.split("\n");
|
|
54
|
+
buffer = lines.pop() || "";
|
|
55
|
+
for (const line of lines) if (line.trim()) yield JSON.parse(line);
|
|
56
|
+
}
|
|
57
|
+
if (buffer.trim()) yield JSON.parse(buffer);
|
|
58
|
+
} finally {
|
|
59
|
+
reader.releaseLock();
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Create a fragment instance for testing with optional dependency and service overrides
|
|
64
|
+
*
|
|
65
|
+
* @param fragmentBuilder - The fragment builder with definition and required options
|
|
66
|
+
* @param options - Configuration and optional overrides for deps/services
|
|
67
|
+
* @returns A fragment test instance with a type-safe handler method
|
|
68
|
+
*
|
|
69
|
+
* @example
|
|
70
|
+
* ```typescript
|
|
71
|
+
* const fragment = createFragmentForTest(chatnoDefinition, {
|
|
72
|
+
* config: { openaiApiKey: "test-key" },
|
|
73
|
+
* options: { mountRoute: "/api/chatno" },
|
|
74
|
+
* services: {
|
|
75
|
+
* generateStreamMessages: mockGenerator
|
|
76
|
+
* }
|
|
77
|
+
* });
|
|
78
|
+
*
|
|
79
|
+
* // Initialize routes with fragment's config/deps/services
|
|
80
|
+
* const [route] = fragment.initRoutes(routes);
|
|
81
|
+
*
|
|
82
|
+
* // Or override specific config/deps/services for certain routes
|
|
83
|
+
* const [route] = fragment.initRoutes(routes, {
|
|
84
|
+
* services: { mockService: mockImplementation }
|
|
85
|
+
* });
|
|
86
|
+
*
|
|
87
|
+
* const response = await fragment.handler(route, {
|
|
88
|
+
* pathParams: { id: "123" },
|
|
89
|
+
* body: { message: "Hello" }
|
|
90
|
+
* });
|
|
91
|
+
*
|
|
92
|
+
* if (response.type === 'json') {
|
|
93
|
+
* expect(response.data).toEqual({...});
|
|
94
|
+
* }
|
|
95
|
+
* ```
|
|
96
|
+
*/
|
|
97
|
+
function createFragmentForTest(fragmentBuilder, options) {
|
|
98
|
+
const { config, options: fragmentOptions = {}, deps: depsOverride, services: servicesOverride, additionalContext: additionalContextOverride } = options;
|
|
99
|
+
const definition = fragmentBuilder.definition;
|
|
100
|
+
const deps = {
|
|
101
|
+
...definition.dependencies ? definition.dependencies(config, fragmentOptions) : {},
|
|
102
|
+
...depsOverride
|
|
103
|
+
};
|
|
104
|
+
const services = {
|
|
105
|
+
...definition.services ? definition.services(config, fragmentOptions, deps) : {},
|
|
106
|
+
...servicesOverride
|
|
107
|
+
};
|
|
108
|
+
return {
|
|
109
|
+
config,
|
|
110
|
+
deps,
|
|
111
|
+
services,
|
|
112
|
+
additionalContext: {
|
|
113
|
+
...definition.additionalContext,
|
|
114
|
+
...fragmentOptions,
|
|
115
|
+
...additionalContextOverride
|
|
116
|
+
},
|
|
117
|
+
initRoutes: (routesOrFactories, overrides) => {
|
|
118
|
+
return resolveRouteFactories({
|
|
119
|
+
config: {
|
|
120
|
+
...config,
|
|
121
|
+
...overrides?.config
|
|
122
|
+
},
|
|
123
|
+
deps: {
|
|
124
|
+
...deps,
|
|
125
|
+
...overrides?.deps
|
|
126
|
+
},
|
|
127
|
+
services: {
|
|
128
|
+
...services,
|
|
129
|
+
...overrides?.services
|
|
130
|
+
}
|
|
131
|
+
}, routesOrFactories);
|
|
132
|
+
},
|
|
133
|
+
handler: async (route, inputOptions) => {
|
|
134
|
+
const { pathParams = {}, body, query, headers } = inputOptions || {};
|
|
135
|
+
const searchParams = query instanceof URLSearchParams ? query : query ? new URLSearchParams(query) : new URLSearchParams();
|
|
136
|
+
const requestHeaders = headers instanceof Headers ? headers : headers ? new Headers(headers) : new Headers();
|
|
137
|
+
const inputContext = new RequestInputContext({
|
|
138
|
+
path: route.path,
|
|
139
|
+
method: route.method,
|
|
140
|
+
pathParams,
|
|
141
|
+
searchParams,
|
|
142
|
+
headers: requestHeaders,
|
|
143
|
+
body,
|
|
144
|
+
inputSchema: route.inputSchema,
|
|
145
|
+
shouldValidateInput: false
|
|
146
|
+
});
|
|
147
|
+
const outputContext = new RequestOutputContext(route.outputSchema);
|
|
148
|
+
return parseResponse(await route.handler(inputContext, outputContext));
|
|
149
|
+
}
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
//#endregion
|
|
154
|
+
export { createFragmentForTest };
|
|
155
|
+
//# sourceMappingURL=test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"test.js","names":[],"sources":["../../src/test/test.ts"],"sourcesContent":["import type { StandardSchemaV1 } from \"@standard-schema/spec\";\nimport type { FragmentDefinition } from \"../api/fragment-builder\";\nimport type { FragnoRouteConfig, HTTPMethod } from \"../api/api\";\nimport type { ExtractPathParams } from \"../api/internal/path\";\nimport { RequestInputContext } from \"../api/request-input-context\";\nimport { RequestOutputContext } from \"../api/request-output-context\";\nimport type { AnyRouteOrFactory, FlattenRouteFactories } from \"../api/route\";\nimport { resolveRouteFactories } from \"../api/route\";\nimport type { FragnoPublicConfig } from \"../api/fragment-instantiation\";\n\n/**\n * Discriminated union representing all possible test response types\n */\nexport type TestResponse<T> =\n | {\n type: \"empty\";\n status: number;\n headers: Headers;\n }\n | {\n type: \"error\";\n status: number;\n headers: Headers;\n error: { message: string; code: string };\n }\n | {\n type: \"json\";\n status: number;\n headers: Headers;\n data: T;\n }\n | {\n type: \"jsonStream\";\n status: number;\n headers: Headers;\n stream: AsyncGenerator<T>;\n };\n\n/**\n * Parse a Response object into a TestResponse discriminated union\n */\nasync function parseResponse<T>(response: Response): Promise<TestResponse<T>> {\n const status = response.status;\n const headers = response.headers;\n const contentType = headers.get(\"content-type\") || \"\";\n\n // Check for streaming response\n if (contentType.includes(\"application/x-ndjson\")) {\n return {\n type: \"jsonStream\",\n status,\n headers,\n stream: parseNDJSONStream<T>(response),\n };\n }\n\n // Parse JSON body\n const text = await response.text();\n\n // Empty response\n if (!text || text === \"null\") {\n return {\n type: \"empty\",\n status,\n headers,\n };\n }\n\n const data = JSON.parse(text);\n\n // Error response (has message and code)\n if (data && typeof data === \"object\" && \"message\" in data && \"code\" in data) {\n return {\n type: \"error\",\n status,\n headers,\n error: { message: data.message, code: data.code },\n };\n }\n\n // JSON response\n return {\n type: \"json\",\n status,\n headers,\n data: data as T,\n };\n}\n\n/**\n * Parse an NDJSON stream into an async generator\n */\nasync function* parseNDJSONStream<T>(response: Response): AsyncGenerator<T> {\n if (!response.body) {\n return;\n }\n\n const reader = response.body.getReader();\n const decoder = new TextDecoder();\n let buffer = \"\";\n\n try {\n while (true) {\n const { done, value } = await reader.read();\n\n if (done) {\n break;\n }\n\n buffer += decoder.decode(value, { stream: true });\n const lines = buffer.split(\"\\n\");\n\n // Keep the last incomplete line in the buffer\n buffer = lines.pop() || \"\";\n\n for (const line of lines) {\n if (line.trim()) {\n yield JSON.parse(line) as T;\n }\n }\n }\n\n // Process any remaining data in the buffer\n if (buffer.trim()) {\n yield JSON.parse(buffer) as T;\n }\n } finally {\n reader.releaseLock();\n }\n}\n\n/**\n * Options for creating a test fragment\n */\nexport interface CreateFragmentForTestOptions<\n TConfig,\n TDeps,\n TServices,\n TAdditionalContext extends Record<string, unknown>,\n TOptions extends FragnoPublicConfig,\n> {\n config: TConfig;\n options?: Partial<TOptions>;\n deps?: Partial<TDeps>;\n services?: Partial<TServices>;\n additionalContext?: Partial<TAdditionalContext>;\n}\n\n/**\n * Options for calling a route handler\n */\nexport interface RouteHandlerInputOptions<\n TPath extends string,\n TInputSchema extends StandardSchemaV1 | undefined,\n> {\n pathParams?: ExtractPathParams<TPath>;\n body?: TInputSchema extends StandardSchemaV1 ? StandardSchemaV1.InferInput<TInputSchema> : never;\n query?: URLSearchParams | Record<string, string>;\n headers?: Headers | Record<string, string>;\n}\n\n/**\n * Options for overriding config/deps/services when initializing routes\n */\nexport interface InitRoutesOverrides<TConfig, TDeps, TServices> {\n config?: Partial<TConfig>;\n deps?: Partial<TDeps>;\n services?: Partial<TServices>;\n}\n\n/**\n * Fragment test instance with type-safe handler method\n */\nexport interface FragmentForTest<\n TConfig,\n TDeps,\n TServices,\n TAdditionalContext extends Record<string, unknown>,\n TOptions extends FragnoPublicConfig,\n> {\n config: TConfig;\n deps: TDeps;\n services: TServices;\n additionalContext: TAdditionalContext & TOptions;\n handler: <\n TMethod extends HTTPMethod,\n TPath extends string,\n TInputSchema extends StandardSchemaV1 | undefined,\n TOutputSchema extends StandardSchemaV1 | undefined,\n TErrorCode extends string,\n TQueryParameters extends string,\n >(\n route: FragnoRouteConfig<\n TMethod,\n TPath,\n TInputSchema,\n TOutputSchema,\n TErrorCode,\n TQueryParameters\n >,\n inputOptions?: RouteHandlerInputOptions<TPath, TInputSchema>,\n ) => Promise<\n TestResponse<\n TOutputSchema extends StandardSchemaV1 ? StandardSchemaV1.InferOutput<TOutputSchema> : unknown\n >\n >;\n initRoutes: <const TRoutesOrFactories extends readonly AnyRouteOrFactory[]>(\n routesOrFactories: TRoutesOrFactories,\n overrides?: InitRoutesOverrides<TConfig, TDeps, TServices>,\n ) => FlattenRouteFactories<TRoutesOrFactories>;\n}\n\n/**\n * Create a fragment instance for testing with optional dependency and service overrides\n *\n * @param fragmentBuilder - The fragment builder with definition and required options\n * @param options - Configuration and optional overrides for deps/services\n * @returns A fragment test instance with a type-safe handler method\n *\n * @example\n * ```typescript\n * const fragment = createFragmentForTest(chatnoDefinition, {\n * config: { openaiApiKey: \"test-key\" },\n * options: { mountRoute: \"/api/chatno\" },\n * services: {\n * generateStreamMessages: mockGenerator\n * }\n * });\n *\n * // Initialize routes with fragment's config/deps/services\n * const [route] = fragment.initRoutes(routes);\n *\n * // Or override specific config/deps/services for certain routes\n * const [route] = fragment.initRoutes(routes, {\n * services: { mockService: mockImplementation }\n * });\n *\n * const response = await fragment.handler(route, {\n * pathParams: { id: \"123\" },\n * body: { message: \"Hello\" }\n * });\n *\n * if (response.type === 'json') {\n * expect(response.data).toEqual({...});\n * }\n * ```\n */\nexport function createFragmentForTest<\n TConfig,\n TDeps,\n TServices extends Record<string, unknown>,\n TAdditionalContext extends Record<string, unknown>,\n TOptions extends FragnoPublicConfig,\n>(\n fragmentBuilder: {\n definition: FragmentDefinition<TConfig, TDeps, TServices, TAdditionalContext>;\n $requiredOptions: TOptions;\n },\n options: CreateFragmentForTestOptions<TConfig, TDeps, TServices, TAdditionalContext, TOptions>,\n): FragmentForTest<TConfig, TDeps, TServices, TAdditionalContext, TOptions> {\n const {\n config,\n options: fragmentOptions = {} as TOptions,\n deps: depsOverride,\n services: servicesOverride,\n additionalContext: additionalContextOverride,\n } = options;\n\n // Create deps from definition or use empty object\n const definition = fragmentBuilder.definition;\n const baseDeps = definition.dependencies\n ? definition.dependencies(config, fragmentOptions)\n : ({} as TDeps);\n\n // Merge deps with overrides\n const deps = { ...baseDeps, ...depsOverride } as TDeps;\n\n // Create services from definition or use empty object\n const baseServices = definition.services\n ? definition.services(config, fragmentOptions, deps)\n : ({} as TServices);\n\n // Merge services with overrides\n const services = { ...baseServices, ...servicesOverride } as TServices;\n\n // Merge additional context with options\n const additionalContext = {\n ...definition.additionalContext,\n ...fragmentOptions,\n ...additionalContextOverride,\n } as TAdditionalContext & TOptions;\n\n return {\n config,\n deps,\n services,\n additionalContext,\n initRoutes: <const TRoutesOrFactories extends readonly AnyRouteOrFactory[]>(\n routesOrFactories: TRoutesOrFactories,\n overrides?: InitRoutesOverrides<TConfig, TDeps, TServices>,\n ): FlattenRouteFactories<TRoutesOrFactories> => {\n // Merge overrides with base config/deps/services\n const routeContext = {\n config: { ...config, ...overrides?.config } as TConfig,\n deps: { ...deps, ...overrides?.deps } as TDeps,\n services: { ...services, ...overrides?.services } as TServices,\n };\n return resolveRouteFactories(routeContext, routesOrFactories);\n },\n handler: async <\n TMethod extends HTTPMethod,\n TPath extends string,\n TInputSchema extends StandardSchemaV1 | undefined,\n TOutputSchema extends StandardSchemaV1 | undefined,\n TErrorCode extends string,\n TQueryParameters extends string,\n >(\n route: FragnoRouteConfig<\n TMethod,\n TPath,\n TInputSchema,\n TOutputSchema,\n TErrorCode,\n TQueryParameters\n >,\n inputOptions?: RouteHandlerInputOptions<TPath, TInputSchema>,\n ): Promise<\n TestResponse<\n TOutputSchema extends StandardSchemaV1\n ? StandardSchemaV1.InferOutput<TOutputSchema>\n : unknown\n >\n > => {\n const {\n pathParams = {} as ExtractPathParams<TPath>,\n body,\n query,\n headers,\n } = inputOptions || {};\n\n // Convert query to URLSearchParams if needed\n const searchParams =\n query instanceof URLSearchParams\n ? query\n : query\n ? new URLSearchParams(query)\n : new URLSearchParams();\n\n // Convert headers to Headers if needed\n const requestHeaders =\n headers instanceof Headers ? headers : headers ? new Headers(headers) : new Headers();\n\n // Construct RequestInputContext\n const inputContext = new RequestInputContext<TPath, TInputSchema>({\n path: route.path,\n method: route.method,\n pathParams,\n searchParams,\n headers: requestHeaders,\n body,\n inputSchema: route.inputSchema,\n shouldValidateInput: false, // Skip validation in tests\n });\n\n // Construct RequestOutputContext\n const outputContext = new RequestOutputContext(route.outputSchema);\n\n // Call the route handler\n const response = await route.handler(inputContext, outputContext);\n\n // Parse and return the response\n return parseResponse<\n TOutputSchema extends StandardSchemaV1\n ? StandardSchemaV1.InferOutput<TOutputSchema>\n : unknown\n >(response);\n },\n };\n}\n"],"mappings":";;;;;;;AAyCA,eAAe,cAAiB,UAA8C;CAC5E,MAAM,SAAS,SAAS;CACxB,MAAM,UAAU,SAAS;AAIzB,MAHoB,QAAQ,IAAI,eAAe,IAAI,IAGnC,SAAS,uBAAuB,CAC9C,QAAO;EACL,MAAM;EACN;EACA;EACA,QAAQ,kBAAqB,SAAS;EACvC;CAIH,MAAM,OAAO,MAAM,SAAS,MAAM;AAGlC,KAAI,CAAC,QAAQ,SAAS,OACpB,QAAO;EACL,MAAM;EACN;EACA;EACD;CAGH,MAAM,OAAO,KAAK,MAAM,KAAK;AAG7B,KAAI,QAAQ,OAAO,SAAS,YAAY,aAAa,QAAQ,UAAU,KACrE,QAAO;EACL,MAAM;EACN;EACA;EACA,OAAO;GAAE,SAAS,KAAK;GAAS,MAAM,KAAK;GAAM;EAClD;AAIH,QAAO;EACL,MAAM;EACN;EACA;EACM;EACP;;;;;AAMH,gBAAgB,kBAAqB,UAAuC;AAC1E,KAAI,CAAC,SAAS,KACZ;CAGF,MAAM,SAAS,SAAS,KAAK,WAAW;CACxC,MAAM,UAAU,IAAI,aAAa;CACjC,IAAI,SAAS;AAEb,KAAI;AACF,SAAO,MAAM;GACX,MAAM,EAAE,MAAM,UAAU,MAAM,OAAO,MAAM;AAE3C,OAAI,KACF;AAGF,aAAU,QAAQ,OAAO,OAAO,EAAE,QAAQ,MAAM,CAAC;GACjD,MAAM,QAAQ,OAAO,MAAM,KAAK;AAGhC,YAAS,MAAM,KAAK,IAAI;AAExB,QAAK,MAAM,QAAQ,MACjB,KAAI,KAAK,MAAM,CACb,OAAM,KAAK,MAAM,KAAK;;AAM5B,MAAI,OAAO,MAAM,CACf,OAAM,KAAK,MAAM,OAAO;WAElB;AACR,SAAO,aAAa;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAwHxB,SAAgB,sBAOd,iBAIA,SAC0E;CAC1E,MAAM,EACJ,QACA,SAAS,kBAAkB,EAAE,EAC7B,MAAM,cACN,UAAU,kBACV,mBAAmB,8BACjB;CAGJ,MAAM,aAAa,gBAAgB;CAMnC,MAAM,OAAO;EAAE,GALE,WAAW,eACxB,WAAW,aAAa,QAAQ,gBAAgB,GAC/C,EAAE;EAGqB,GAAG;EAAc;CAQ7C,MAAM,WAAW;EAAE,GALE,WAAW,WAC5B,WAAW,SAAS,QAAQ,iBAAiB,KAAK,GACjD,EAAE;EAG6B,GAAG;EAAkB;AASzD,QAAO;EACL;EACA;EACA;EACA,mBAVwB;GACxB,GAAG,WAAW;GACd,GAAG;GACH,GAAG;GACJ;EAOC,aACE,mBACA,cAC8C;AAO9C,UAAO,sBALc;IACnB,QAAQ;KAAE,GAAG;KAAQ,GAAG,WAAW;KAAQ;IAC3C,MAAM;KAAE,GAAG;KAAM,GAAG,WAAW;KAAM;IACrC,UAAU;KAAE,GAAG;KAAU,GAAG,WAAW;KAAU;IAClD,EAC0C,kBAAkB;;EAE/D,SAAS,OAQP,OAQA,iBAOG;GACH,MAAM,EACJ,aAAa,EAAE,EACf,MACA,OACA,YACE,gBAAgB,EAAE;GAGtB,MAAM,eACJ,iBAAiB,kBACb,QACA,QACE,IAAI,gBAAgB,MAAM,GAC1B,IAAI,iBAAiB;GAG7B,MAAM,iBACJ,mBAAmB,UAAU,UAAU,UAAU,IAAI,QAAQ,QAAQ,GAAG,IAAI,SAAS;GAGvF,MAAM,eAAe,IAAI,oBAAyC;IAChE,MAAM,MAAM;IACZ,QAAQ,MAAM;IACd;IACA;IACA,SAAS;IACT;IACA,aAAa,MAAM;IACnB,qBAAqB;IACtB,CAAC;GAGF,MAAM,gBAAgB,IAAI,qBAAqB,MAAM,aAAa;AAMlE,UAAO,cAHU,MAAM,MAAM,QAAQ,cAAc,cAAc,CAOtD;;EAEd"}
|
package/package.json
CHANGED
|
@@ -1,69 +1,63 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fragno-dev/core",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
4
4
|
"exports": {
|
|
5
5
|
".": {
|
|
6
|
-
"bun": "./src/mod.ts",
|
|
7
6
|
"development": "./src/mod.ts",
|
|
8
7
|
"types": "./dist/mod.d.ts",
|
|
9
8
|
"default": "./dist/mod.js"
|
|
10
9
|
},
|
|
11
10
|
"./api": {
|
|
12
|
-
"bun": "./src/api/api.ts",
|
|
13
11
|
"development": "./src/api/api.ts",
|
|
14
12
|
"types": "./dist/api/api.d.ts",
|
|
15
13
|
"default": "./dist/api/api.js"
|
|
16
14
|
},
|
|
15
|
+
"./test": {
|
|
16
|
+
"development": "./src/test/test.ts",
|
|
17
|
+
"types": "./dist/test/test.d.ts",
|
|
18
|
+
"default": "./dist/test/test.js"
|
|
19
|
+
},
|
|
17
20
|
"./api/fragment-builder": {
|
|
18
|
-
"bun": "./src/api/fragment-builder.ts",
|
|
19
21
|
"development": "./src/api/fragment-builder.ts",
|
|
20
22
|
"types": "./dist/api/fragment-builder.d.ts",
|
|
21
23
|
"default": "./dist/api/fragment-builder.js"
|
|
22
24
|
},
|
|
23
25
|
"./api/fragment-instantiation": {
|
|
24
|
-
"bun": "./src/api/fragment-instantiation.ts",
|
|
25
26
|
"development": "./src/api/fragment-instantiation.ts",
|
|
26
27
|
"types": "./dist/api/fragment-instantiation.d.ts",
|
|
27
28
|
"default": "./dist/api/fragment-instantiation.js"
|
|
28
29
|
},
|
|
29
30
|
"./client": {
|
|
30
|
-
"bun": "./src/client/client.ts",
|
|
31
31
|
"development": "./src/client/client.ts",
|
|
32
32
|
"types": "./dist/client/client.d.ts",
|
|
33
33
|
"default": "./dist/client/client.js"
|
|
34
34
|
},
|
|
35
35
|
"./react": {
|
|
36
|
-
"bun": "./src/client/react.ts",
|
|
37
36
|
"development": "./src/client/react.ts",
|
|
38
37
|
"types": "./dist/client/react.d.ts",
|
|
39
38
|
"default": "./dist/client/react.js"
|
|
40
39
|
},
|
|
41
40
|
"./vanilla": {
|
|
42
|
-
"bun": "./src/client/vanilla.ts",
|
|
43
41
|
"development": "./src/client/vanilla.ts",
|
|
44
42
|
"types": "./dist/client/vanilla.d.ts",
|
|
45
43
|
"default": "./dist/client/vanilla.js"
|
|
46
44
|
},
|
|
47
45
|
"./vue": {
|
|
48
|
-
"bun": "./src/client/vue.ts",
|
|
49
46
|
"development": "./src/client/vue.ts",
|
|
50
47
|
"types": "./dist/client/vue.d.ts",
|
|
51
48
|
"default": "./dist/client/vue.js"
|
|
52
49
|
},
|
|
53
50
|
"./svelte": {
|
|
54
|
-
"bun": "./src/client/client.svelte.ts",
|
|
55
51
|
"development": "./src/client/client.svelte.ts",
|
|
56
52
|
"types": "./dist/client/client.svelte.d.ts",
|
|
57
53
|
"default": "./dist/client/client.svelte.js"
|
|
58
54
|
},
|
|
59
55
|
"./solid": {
|
|
60
|
-
"bun": "./src/client/solid.ts",
|
|
61
56
|
"development": "./src/client/solid.ts",
|
|
62
57
|
"types": "./dist/client/solid.d.ts",
|
|
63
58
|
"default": "./dist/client/solid.js"
|
|
64
59
|
},
|
|
65
60
|
"./react-ssr": {
|
|
66
|
-
"bun": "./src/integrations/react-ssr.ts",
|
|
67
61
|
"development": "./src/integrations/react-ssr.ts",
|
|
68
62
|
"types": "./dist/integrations/react-ssr.d.ts",
|
|
69
63
|
"default": "./dist/integrations/react-ssr.js"
|
|
@@ -73,13 +67,6 @@
|
|
|
73
67
|
"main": "./dist/mod.js",
|
|
74
68
|
"module": "./dist/mod.js",
|
|
75
69
|
"types": "./dist/mod.d.ts",
|
|
76
|
-
"scripts": {
|
|
77
|
-
"build": "tsdown",
|
|
78
|
-
"build:watch": "tsdown --watch",
|
|
79
|
-
"types:check": "tsc --noEmit",
|
|
80
|
-
"test": "vitest run",
|
|
81
|
-
"test:watch": "vitest --watch"
|
|
82
|
-
},
|
|
83
70
|
"type": "module",
|
|
84
71
|
"dependencies": {
|
|
85
72
|
"@nanostores/query": "^0.3.4",
|
|
@@ -89,8 +76,6 @@
|
|
|
89
76
|
"rou3": "^0.7.3"
|
|
90
77
|
},
|
|
91
78
|
"devDependencies": {
|
|
92
|
-
"@fragno-private/typescript-config": "0.0.1",
|
|
93
|
-
"@fragno-private/vitest-config": "0.0.0",
|
|
94
79
|
"@sveltejs/vite-plugin-svelte": "^6.2.0",
|
|
95
80
|
"@testing-library/react": "^16.3.0",
|
|
96
81
|
"@testing-library/svelte": "^5.2.8",
|
|
@@ -106,7 +91,9 @@
|
|
|
106
91
|
"solid-js": "^1.9.3",
|
|
107
92
|
"vitest": "^3.2.4",
|
|
108
93
|
"vue": "^3",
|
|
109
|
-
"zod": "^4.0.5"
|
|
94
|
+
"zod": "^4.0.5",
|
|
95
|
+
"@fragno-private/typescript-config": "0.0.1",
|
|
96
|
+
"@fragno-private/vitest-config": "0.0.0"
|
|
110
97
|
},
|
|
111
98
|
"peerDependencies": {
|
|
112
99
|
"react": "^19.0.0",
|
|
@@ -134,5 +121,12 @@
|
|
|
134
121
|
"directory": "packages/fragno"
|
|
135
122
|
},
|
|
136
123
|
"homepage": "https://fragno.dev",
|
|
137
|
-
"license": "MIT"
|
|
138
|
-
|
|
124
|
+
"license": "MIT",
|
|
125
|
+
"scripts": {
|
|
126
|
+
"build": "tsdown",
|
|
127
|
+
"build:watch": "tsdown --watch",
|
|
128
|
+
"types:check": "tsc --noEmit",
|
|
129
|
+
"test": "vitest run",
|
|
130
|
+
"test:watch": "vitest --watch"
|
|
131
|
+
}
|
|
132
|
+
}
|
|
@@ -628,4 +628,255 @@ describe("Request Middleware", () => {
|
|
|
628
628
|
custom: "middleware-value",
|
|
629
629
|
});
|
|
630
630
|
});
|
|
631
|
+
|
|
632
|
+
test("ifMatchesRoute properly awaits async handlers", async () => {
|
|
633
|
+
const fragment = defineFragment("test-lib");
|
|
634
|
+
|
|
635
|
+
const routes = [
|
|
636
|
+
defineRoute({
|
|
637
|
+
method: "POST",
|
|
638
|
+
path: "/users",
|
|
639
|
+
inputSchema: z.object({ name: z.string() }),
|
|
640
|
+
outputSchema: z.object({ id: z.number(), name: z.string(), verified: z.boolean() }),
|
|
641
|
+
handler: async ({ input }, { json }) => {
|
|
642
|
+
const body = await input.valid();
|
|
643
|
+
return json({
|
|
644
|
+
id: 1,
|
|
645
|
+
name: body.name,
|
|
646
|
+
verified: false,
|
|
647
|
+
});
|
|
648
|
+
},
|
|
649
|
+
}),
|
|
650
|
+
] as const;
|
|
651
|
+
|
|
652
|
+
let asyncOperationCompleted = false;
|
|
653
|
+
|
|
654
|
+
const instance = createFragment(fragment, {}, routes, {
|
|
655
|
+
mountRoute: "/api",
|
|
656
|
+
}).withMiddleware(async ({ ifMatchesRoute }) => {
|
|
657
|
+
const result = await ifMatchesRoute("POST", "/users", async ({ input }) => {
|
|
658
|
+
// Simulate async operation (e.g., database lookup)
|
|
659
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
660
|
+
const body = await input.valid();
|
|
661
|
+
|
|
662
|
+
// Verify the async operation completed
|
|
663
|
+
asyncOperationCompleted = true;
|
|
664
|
+
|
|
665
|
+
// Check if user exists
|
|
666
|
+
if (body.name === "existing-user") {
|
|
667
|
+
return new Response(
|
|
668
|
+
JSON.stringify({
|
|
669
|
+
message: "User already exists",
|
|
670
|
+
code: "USER_EXISTS",
|
|
671
|
+
}),
|
|
672
|
+
{ status: 409, headers: { "Content-Type": "application/json" } },
|
|
673
|
+
);
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
return undefined;
|
|
677
|
+
});
|
|
678
|
+
|
|
679
|
+
return result;
|
|
680
|
+
});
|
|
681
|
+
|
|
682
|
+
// Test with existing user
|
|
683
|
+
const existingUserReq = new Request("http://localhost/api/users", {
|
|
684
|
+
method: "POST",
|
|
685
|
+
headers: { "Content-Type": "application/json" },
|
|
686
|
+
body: JSON.stringify({ name: "existing-user" }),
|
|
687
|
+
});
|
|
688
|
+
|
|
689
|
+
const existingUserRes = await instance.handler(existingUserReq);
|
|
690
|
+
expect(asyncOperationCompleted).toBe(true);
|
|
691
|
+
expect(existingUserRes.status).toBe(409);
|
|
692
|
+
expect(await existingUserRes.json()).toEqual({
|
|
693
|
+
message: "User already exists",
|
|
694
|
+
code: "USER_EXISTS",
|
|
695
|
+
});
|
|
696
|
+
|
|
697
|
+
// Reset flag
|
|
698
|
+
asyncOperationCompleted = false;
|
|
699
|
+
|
|
700
|
+
// Test with new user
|
|
701
|
+
const newUserReq = new Request("http://localhost/api/users", {
|
|
702
|
+
method: "POST",
|
|
703
|
+
headers: { "Content-Type": "application/json" },
|
|
704
|
+
body: JSON.stringify({ name: "new-user" }),
|
|
705
|
+
});
|
|
706
|
+
|
|
707
|
+
const newUserRes = await instance.handler(newUserReq);
|
|
708
|
+
expect(asyncOperationCompleted).toBe(true);
|
|
709
|
+
expect(newUserRes.status).toBe(200);
|
|
710
|
+
expect(await newUserRes.json()).toEqual({
|
|
711
|
+
id: 1,
|
|
712
|
+
name: "new-user",
|
|
713
|
+
verified: false,
|
|
714
|
+
});
|
|
715
|
+
});
|
|
716
|
+
|
|
717
|
+
test("ifMatchesRoute handles async errors properly", async () => {
|
|
718
|
+
const fragment = defineFragment("test-lib");
|
|
719
|
+
|
|
720
|
+
const routes = [
|
|
721
|
+
defineRoute({
|
|
722
|
+
method: "GET",
|
|
723
|
+
path: "/data",
|
|
724
|
+
outputSchema: z.object({ data: z.string() }),
|
|
725
|
+
handler: async (_, { json }) => {
|
|
726
|
+
return json({ data: "test" });
|
|
727
|
+
},
|
|
728
|
+
}),
|
|
729
|
+
] as const;
|
|
730
|
+
|
|
731
|
+
const instance = createFragment(fragment, {}, routes, {
|
|
732
|
+
mountRoute: "/api",
|
|
733
|
+
}).withMiddleware(async ({ ifMatchesRoute }) => {
|
|
734
|
+
const result = await ifMatchesRoute("GET", "/data", async (_, { error }) => {
|
|
735
|
+
// Simulate async operation that fails
|
|
736
|
+
await new Promise((resolve) => setTimeout(resolve, 5));
|
|
737
|
+
|
|
738
|
+
// Simulate an error condition (e.g., database unavailable)
|
|
739
|
+
return error(
|
|
740
|
+
{
|
|
741
|
+
message: "Service temporarily unavailable",
|
|
742
|
+
code: "SERVICE_UNAVAILABLE",
|
|
743
|
+
},
|
|
744
|
+
503,
|
|
745
|
+
);
|
|
746
|
+
});
|
|
747
|
+
|
|
748
|
+
return result;
|
|
749
|
+
});
|
|
750
|
+
|
|
751
|
+
const req = new Request("http://localhost/api/data", {
|
|
752
|
+
method: "GET",
|
|
753
|
+
});
|
|
754
|
+
|
|
755
|
+
const res = await instance.handler(req);
|
|
756
|
+
expect(res.status).toBe(503);
|
|
757
|
+
expect(await res.json()).toEqual({
|
|
758
|
+
message: "Service temporarily unavailable",
|
|
759
|
+
code: "SERVICE_UNAVAILABLE",
|
|
760
|
+
});
|
|
761
|
+
});
|
|
762
|
+
|
|
763
|
+
test("ifMatchesRoute with async body modification", async () => {
|
|
764
|
+
const fragment = defineFragment("test-lib");
|
|
765
|
+
|
|
766
|
+
const routes = [
|
|
767
|
+
defineRoute({
|
|
768
|
+
method: "POST",
|
|
769
|
+
path: "/posts",
|
|
770
|
+
inputSchema: z.object({ title: z.string(), content: z.string() }),
|
|
771
|
+
outputSchema: z.object({
|
|
772
|
+
title: z.string(),
|
|
773
|
+
content: z.string(),
|
|
774
|
+
slug: z.string(),
|
|
775
|
+
createdAt: z.number(),
|
|
776
|
+
}),
|
|
777
|
+
handler: async ({ input }, { json }) => {
|
|
778
|
+
const body = await input.valid();
|
|
779
|
+
return json({
|
|
780
|
+
title: body.title,
|
|
781
|
+
content: body.content,
|
|
782
|
+
slug: body.title.toLowerCase().replace(/\s+/g, "-"),
|
|
783
|
+
createdAt: Date.now(),
|
|
784
|
+
});
|
|
785
|
+
},
|
|
786
|
+
}),
|
|
787
|
+
] as const;
|
|
788
|
+
|
|
789
|
+
const instance = createFragment(fragment, {}, routes, {
|
|
790
|
+
mountRoute: "/api",
|
|
791
|
+
}).withMiddleware(async ({ ifMatchesRoute, requestState }) => {
|
|
792
|
+
const result = await ifMatchesRoute("POST", "/posts", async ({ input }) => {
|
|
793
|
+
// Simulate async operation to enrich the body (e.g., fetching user data)
|
|
794
|
+
await new Promise((resolve) => setTimeout(resolve, 5));
|
|
795
|
+
|
|
796
|
+
const body = await input.valid();
|
|
797
|
+
|
|
798
|
+
// Enrich the body with additional data
|
|
799
|
+
requestState.setBody({
|
|
800
|
+
...body,
|
|
801
|
+
content: `${body.content}\n\n[Enhanced by middleware]`,
|
|
802
|
+
});
|
|
803
|
+
|
|
804
|
+
return undefined;
|
|
805
|
+
});
|
|
806
|
+
|
|
807
|
+
return result;
|
|
808
|
+
});
|
|
809
|
+
|
|
810
|
+
const req = new Request("http://localhost/api/posts", {
|
|
811
|
+
method: "POST",
|
|
812
|
+
headers: { "Content-Type": "application/json" },
|
|
813
|
+
body: JSON.stringify({
|
|
814
|
+
title: "Test Post",
|
|
815
|
+
content: "Original content",
|
|
816
|
+
}),
|
|
817
|
+
});
|
|
818
|
+
|
|
819
|
+
const res = await instance.handler(req);
|
|
820
|
+
expect(res.status).toBe(200);
|
|
821
|
+
|
|
822
|
+
const responseBody = await res.json();
|
|
823
|
+
expect(responseBody).toMatchObject({
|
|
824
|
+
title: "Test Post",
|
|
825
|
+
content: "Original content\n\n[Enhanced by middleware]",
|
|
826
|
+
slug: "test-post",
|
|
827
|
+
});
|
|
828
|
+
expect(responseBody.createdAt).toBeTypeOf("number");
|
|
829
|
+
});
|
|
830
|
+
|
|
831
|
+
test("multiple async operations in ifMatchesRoute complete in order", async () => {
|
|
832
|
+
const fragment = defineFragment("test-lib");
|
|
833
|
+
|
|
834
|
+
const routes = [
|
|
835
|
+
defineRoute({
|
|
836
|
+
method: "GET",
|
|
837
|
+
path: "/status",
|
|
838
|
+
outputSchema: z.object({ message: z.string() }),
|
|
839
|
+
handler: async (_, { json }) => {
|
|
840
|
+
return json({ message: "OK" });
|
|
841
|
+
},
|
|
842
|
+
}),
|
|
843
|
+
] as const;
|
|
844
|
+
|
|
845
|
+
const executionOrder: string[] = [];
|
|
846
|
+
|
|
847
|
+
const instance = createFragment(fragment, {}, routes, {
|
|
848
|
+
mountRoute: "/api",
|
|
849
|
+
}).withMiddleware(async ({ ifMatchesRoute }) => {
|
|
850
|
+
executionOrder.push("start");
|
|
851
|
+
|
|
852
|
+
const result = await ifMatchesRoute("GET", "/status", async () => {
|
|
853
|
+
executionOrder.push("before-first-async");
|
|
854
|
+
await new Promise((resolve) => setTimeout(resolve, 5));
|
|
855
|
+
executionOrder.push("after-first-async");
|
|
856
|
+
|
|
857
|
+
await new Promise((resolve) => setTimeout(resolve, 5));
|
|
858
|
+
executionOrder.push("after-second-async");
|
|
859
|
+
|
|
860
|
+
return undefined;
|
|
861
|
+
});
|
|
862
|
+
|
|
863
|
+
executionOrder.push("end");
|
|
864
|
+
|
|
865
|
+
return result;
|
|
866
|
+
});
|
|
867
|
+
|
|
868
|
+
const req = new Request("http://localhost/api/status", {
|
|
869
|
+
method: "GET",
|
|
870
|
+
});
|
|
871
|
+
|
|
872
|
+
const res = await instance.handler(req);
|
|
873
|
+
expect(res.status).toBe(200);
|
|
874
|
+
expect(executionOrder).toEqual([
|
|
875
|
+
"start",
|
|
876
|
+
"before-first-async",
|
|
877
|
+
"after-first-async",
|
|
878
|
+
"after-second-async",
|
|
879
|
+
"end",
|
|
880
|
+
]);
|
|
881
|
+
});
|
|
631
882
|
});
|
|
@@ -143,6 +143,6 @@ export class RequestMiddlewareInputContext<const TRoutes extends readonly AnyFra
|
|
|
143
143
|
const outputContext = new RequestOutputContext(this.#route.outputSchema);
|
|
144
144
|
|
|
145
145
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
146
|
-
return (handler as any)(inputContext, outputContext);
|
|
146
|
+
return await (handler as any)(inputContext, outputContext);
|
|
147
147
|
};
|
|
148
148
|
}
|