@fragno-dev/core 0.1.6 → 0.1.8

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 (85) hide show
  1. package/.turbo/turbo-build.log +46 -54
  2. package/CHANGELOG.md +12 -0
  3. package/dist/api/api.d.ts +2 -2
  4. package/dist/api/api.js +3 -2
  5. package/dist/api/fragment-builder.d.ts +2 -4
  6. package/dist/api/fragment-builder.js +1 -1
  7. package/dist/api/fragment-instantiation.d.ts +2 -4
  8. package/dist/api/fragment-instantiation.js +3 -5
  9. package/dist/api/route.d.ts +2 -3
  10. package/dist/api/route.js +1 -1
  11. package/dist/api-BFrUCIsF.d.ts +963 -0
  12. package/dist/api-BFrUCIsF.d.ts.map +1 -0
  13. package/dist/client/client.d.ts +1 -3
  14. package/dist/client/client.js +4 -5
  15. package/dist/client/client.svelte.d.ts +2 -3
  16. package/dist/client/client.svelte.d.ts.map +1 -1
  17. package/dist/client/client.svelte.js +4 -5
  18. package/dist/client/client.svelte.js.map +1 -1
  19. package/dist/client/react.d.ts +2 -3
  20. package/dist/client/react.d.ts.map +1 -1
  21. package/dist/client/react.js +4 -5
  22. package/dist/client/react.js.map +1 -1
  23. package/dist/client/solid.d.ts +2 -3
  24. package/dist/client/solid.d.ts.map +1 -1
  25. package/dist/client/solid.js +4 -5
  26. package/dist/client/solid.js.map +1 -1
  27. package/dist/client/vanilla.d.ts +2 -3
  28. package/dist/client/vanilla.d.ts.map +1 -1
  29. package/dist/client/vanilla.js +4 -5
  30. package/dist/client/vanilla.js.map +1 -1
  31. package/dist/client/vue.d.ts +2 -3
  32. package/dist/client/vue.d.ts.map +1 -1
  33. package/dist/client/vue.js +8 -9
  34. package/dist/client/vue.js.map +1 -1
  35. package/dist/{client-DJfCJiHK.js → client-DAFHcKqA.js} +15 -8
  36. package/dist/client-DAFHcKqA.js.map +1 -0
  37. package/dist/fragment-builder-Boh2vNHq.js +108 -0
  38. package/dist/fragment-builder-Boh2vNHq.js.map +1 -0
  39. package/dist/fragment-instantiation-DUT-HLl1.js +898 -0
  40. package/dist/fragment-instantiation-DUT-HLl1.js.map +1 -0
  41. package/dist/integrations/react-ssr.js +1 -1
  42. package/dist/mod.d.ts +2 -4
  43. package/dist/mod.js +4 -6
  44. package/dist/{route-C5Uryylh.js → route-C4CyNHkC.js} +8 -3
  45. package/dist/route-C4CyNHkC.js.map +1 -0
  46. package/dist/{ssr-BByDVfFD.js → ssr-kyKI7pqH.js} +1 -1
  47. package/dist/{ssr-BByDVfFD.js.map → ssr-kyKI7pqH.js.map} +1 -1
  48. package/dist/test/test.d.ts +6 -7
  49. package/dist/test/test.d.ts.map +1 -1
  50. package/dist/test/test.js +9 -7
  51. package/dist/test/test.js.map +1 -1
  52. package/package.json +1 -1
  53. package/src/api/api.ts +45 -6
  54. package/src/api/fragment-builder.ts +463 -25
  55. package/src/api/fragment-instantiation.test.ts +249 -7
  56. package/src/api/fragment-instantiation.ts +283 -16
  57. package/src/api/fragment-services.test.ts +462 -0
  58. package/src/api/fragment.test.ts +65 -17
  59. package/src/api/internal/path-type.test.ts +7 -7
  60. package/src/api/internal/path.ts +1 -1
  61. package/src/api/request-middleware.test.ts +6 -3
  62. package/src/api/route.test.ts +111 -1
  63. package/src/api/route.ts +323 -14
  64. package/src/client/client-types.test.ts +4 -4
  65. package/src/client/client.test.ts +77 -0
  66. package/src/client/client.ts +31 -12
  67. package/src/mod.ts +11 -1
  68. package/src/test/test.test.ts +20 -15
  69. package/src/test/test.ts +48 -9
  70. package/dist/api-CoCkNi6h.d.ts +0 -377
  71. package/dist/api-CoCkNi6h.d.ts.map +0 -1
  72. package/dist/api-DngJDcmO.js +0 -54
  73. package/dist/api-DngJDcmO.js.map +0 -1
  74. package/dist/client-DJfCJiHK.js.map +0 -1
  75. package/dist/fragment-builder-8-tiECi5.d.ts +0 -408
  76. package/dist/fragment-builder-8-tiECi5.d.ts.map +0 -1
  77. package/dist/fragment-builder-DOnCVBqc.js +0 -47
  78. package/dist/fragment-builder-DOnCVBqc.js.map +0 -1
  79. package/dist/fragment-instantiation-C4wvwl6V.js +0 -446
  80. package/dist/fragment-instantiation-C4wvwl6V.js.map +0 -1
  81. package/dist/request-output-context-CdIjwmEN.js +0 -320
  82. package/dist/request-output-context-CdIjwmEN.js.map +0 -1
  83. package/dist/route-C5Uryylh.js.map +0 -1
  84. package/dist/route-mGLYSUvD.d.ts +0 -26
  85. package/dist/route-mGLYSUvD.d.ts.map +0 -1
@@ -1,6 +1,6 @@
1
1
  import { nanoquery, type FetcherStore, type MutatorStore } from "@nanostores/query";
2
2
  import type { StandardSchemaV1 } from "@standard-schema/spec";
3
- import { task, type ReadableAtom, type Store } from "nanostores";
3
+ import { computed, task, type ReadableAtom, type Store } from "nanostores";
4
4
  import type { FragnoRouteConfig, HTTPMethod, NonGetHTTPMethod } from "../api/api";
5
5
  import {
6
6
  buildPath,
@@ -252,11 +252,11 @@ export type FragnoClientHookData<
252
252
  >;
253
253
  query(args?: {
254
254
  path?: MaybeExtractPathParamsOrWiden<TPath, string>;
255
- query?: Record<TQueryParameters, string>;
255
+ query?: Record<TQueryParameters, string | undefined>;
256
256
  }): Promise<StandardSchemaV1.InferOutput<TOutputSchema>>;
257
257
  store(args?: {
258
258
  path?: MaybeExtractPathParamsOrWiden<TPath, string | ReadableAtom<string>>;
259
- query?: Record<TQueryParameters, string | ReadableAtom<string>>;
259
+ query?: Record<TQueryParameters, string | undefined | ReadableAtom<string | undefined>>;
260
260
  }): FetcherStore<StandardSchemaV1.InferOutput<TOutputSchema>, FragnoClientError<TErrorCode>>;
261
261
  [GET_HOOK_SYMBOL]: true;
262
262
  } & {
@@ -287,14 +287,14 @@ export type FragnoClientMutatorData<
287
287
  mutateQuery(args?: {
288
288
  body?: InferOr<TInputSchema, undefined>;
289
289
  path?: MaybeExtractPathParamsOrWiden<TPath, string>;
290
- query?: Record<TQueryParameters, string>;
290
+ query?: Record<TQueryParameters, string | undefined>;
291
291
  }): Promise<InferOr<TOutputSchema, undefined>>;
292
292
 
293
293
  mutatorStore: MutatorStore<
294
294
  {
295
295
  body?: InferOr<TInputSchema, undefined>;
296
296
  path?: MaybeExtractPathParamsOrWiden<TPath, string | ReadableAtom<string>>;
297
- query?: Record<TQueryParameters, string | ReadableAtom<string>>;
297
+ query?: Record<TQueryParameters, string | undefined | ReadableAtom<string | undefined>>;
298
298
  },
299
299
  InferOr<TOutputSchema, undefined>,
300
300
  FragnoClientError<TErrorCode>
@@ -313,7 +313,7 @@ export function buildUrl<TPath extends string>(
313
313
  },
314
314
  params: {
315
315
  pathParams?: Record<string, string | ReadableAtom<string>>;
316
- queryParams?: Record<string, string | ReadableAtom<string>>;
316
+ queryParams?: Record<string, string | undefined | ReadableAtom<string | undefined>>;
317
317
  },
318
318
  ): string {
319
319
  const { baseUrl = "", mountRoute, path } = config;
@@ -322,7 +322,12 @@ export function buildUrl<TPath extends string>(
322
322
  const normalizedPathParams = unwrapObject(pathParams) as ExtractPathParams<TPath, string>;
323
323
  const normalizedQueryParams = unwrapObject(queryParams) ?? {};
324
324
 
325
- const searchParams = new URLSearchParams(normalizedQueryParams);
325
+ // Filter out undefined values to prevent URLSearchParams from converting them to string "undefined"
326
+ const filteredQueryParams = Object.fromEntries(
327
+ Object.entries(normalizedQueryParams).filter(([_, value]) => value !== undefined),
328
+ ) as Record<string, string>;
329
+
330
+ const searchParams = new URLSearchParams(filteredQueryParams);
326
331
  const builtPath = buildPath(path, normalizedPathParams ?? {});
327
332
  const search = searchParams.toString() ? `?${searchParams.toString()}` : "";
328
333
  return `${baseUrl}${mountRoute}${builtPath}${search}`;
@@ -333,6 +338,7 @@ export function buildUrl<TPath extends string>(
333
338
  *
334
339
  * The returned array is always: path, pathParams (In order they appear in the path), queryParams (In alphabetical order)
335
340
  * Missing pathParams are replaced with "<missing>".
341
+ * Atoms with undefined values are wrapped in computed atoms that map undefined to "" to avoid nanoquery treating the key as incomplete.
336
342
  * @param path
337
343
  * @param params
338
344
  * @returns
@@ -342,7 +348,7 @@ export function getCacheKey<TMethod extends HTTPMethod, TPath extends string>(
342
348
  path: TPath,
343
349
  params?: {
344
350
  pathParams?: Record<string, string | ReadableAtom<string>>;
345
- queryParams?: Record<string, string | ReadableAtom<string>>;
351
+ queryParams?: Record<string, string | undefined | ReadableAtom<string | undefined>>;
346
352
  },
347
353
  ): (string | ReadableAtom<string>)[] {
348
354
  if (!params) {
@@ -357,7 +363,15 @@ export function getCacheKey<TMethod extends HTTPMethod, TPath extends string>(
357
363
  const queryParamValues = queryParams
358
364
  ? Object.keys(queryParams)
359
365
  .sort()
360
- .map((key) => queryParams[key])
366
+ .map((key) => {
367
+ const value = queryParams[key];
368
+ // If it's an atom, wrap it to convert undefined to ""
369
+ if (value && typeof value === "object" && "get" in value) {
370
+ return computed(value as ReadableAtom<string | undefined>, (v) => v ?? "");
371
+ }
372
+ // Plain string value (or undefined)
373
+ return value ?? "";
374
+ })
361
375
  : [];
362
376
 
363
377
  return [method, path, ...pathParamValues, ...queryParamValues];
@@ -668,14 +682,19 @@ export class ClientBuilder<
668
682
 
669
683
  async function callServerSideHandler(params: {
670
684
  pathParams?: Record<string, string | ReadableAtom<string>>;
671
- queryParams?: Record<string, string | ReadableAtom<string>>;
685
+ queryParams?: Record<string, string | undefined | ReadableAtom<string | undefined>>;
672
686
  }): Promise<Response> {
673
687
  const { pathParams, queryParams } = params ?? {};
674
688
 
675
689
  const normalizedPathParams = unwrapObject(pathParams) as ExtractPathParams<TPath, string>;
676
690
  const normalizedQueryParams = unwrapObject(queryParams) ?? {};
677
691
 
678
- const searchParams = new URLSearchParams(normalizedQueryParams);
692
+ // Filter out undefined values to prevent URLSearchParams from converting them to string "undefined"
693
+ const filteredQueryParams = Object.fromEntries(
694
+ Object.entries(normalizedQueryParams).filter(([_, value]) => value !== undefined),
695
+ ) as Record<string, string>;
696
+
697
+ const searchParams = new URLSearchParams(filteredQueryParams);
679
698
 
680
699
  const result = await route.handler(
681
700
  RequestInputContext.fromSSRContext({
@@ -692,7 +711,7 @@ export class ClientBuilder<
692
711
 
693
712
  async function executeQuery(params?: {
694
713
  pathParams?: Record<string, string | ReadableAtom<string>>;
695
- queryParams?: Record<string, string | ReadableAtom<string>>;
714
+ queryParams?: Record<string, string | undefined | ReadableAtom<string | undefined>>;
696
715
  }): Promise<Response> {
697
716
  const { pathParams, queryParams } = params ?? {};
698
717
 
package/src/mod.ts CHANGED
@@ -1,7 +1,14 @@
1
- export { defineFragment, FragmentBuilder, type FragmentDefinition } from "./api/fragment-builder";
1
+ export {
2
+ defineFragment,
3
+ FragmentBuilder,
4
+ type FragmentDefinition,
5
+ type RouteHandler,
6
+ } from "./api/fragment-builder";
2
7
 
3
8
  export {
4
9
  createFragment,
10
+ instantiateFragment,
11
+ FragmentInstantiationBuilder,
5
12
  type FragnoFragmentSharedConfig,
6
13
  type FragnoPublicConfig,
7
14
  type FragnoPublicClientConfig,
@@ -18,3 +25,6 @@ export {
18
25
  type AnyRouteOrFactory,
19
26
  type FlattenRouteFactories,
20
27
  } from "./api/route";
28
+
29
+ export { RequestInputContext } from "./api/request-input-context";
30
+ export { RequestOutputContext } from "./api/request-output-context";
@@ -1,4 +1,4 @@
1
- import { describe, it, expect, expectTypeOf } from "vitest";
1
+ import { describe, it, expect, expectTypeOf, assert } from "vitest";
2
2
  import { createFragmentForTest } from "./test";
3
3
  import { defineFragment } from "../api/fragment-builder";
4
4
  import { defineRoute, defineRoutes } from "../api/route";
@@ -46,9 +46,11 @@ describe("createFragmentForTest", () => {
46
46
  .withDependencies(({ config }) => ({
47
47
  client: { apiKey: config.apiKey },
48
48
  }))
49
- .withServices(({ deps }) => ({
50
- getApiKey: () => deps.client.apiKey,
51
- }));
49
+ .providesService(({ deps, defineService }) =>
50
+ defineService({
51
+ getApiKey: () => deps.client.apiKey,
52
+ }),
53
+ );
52
54
 
53
55
  const testFragment = createFragmentForTest(fragment, [], {
54
56
  config: { apiKey: "test-key" },
@@ -62,9 +64,11 @@ describe("createFragmentForTest", () => {
62
64
  .withDependencies(({ config }) => ({
63
65
  client: { apiKey: config.apiKey },
64
66
  }))
65
- .withServices(({ deps }) => ({
66
- getApiKey: () => deps.client.apiKey,
67
- }));
67
+ .providesService(({ deps, defineService }) =>
68
+ defineService({
69
+ getApiKey: () => deps.client.apiKey,
70
+ }),
71
+ );
68
72
 
69
73
  const testFragment = createFragmentForTest(fragment, [], {
70
74
  config: { apiKey: "test-key" },
@@ -81,9 +85,11 @@ describe("createFragmentForTest", () => {
81
85
 
82
86
  const fragment = defineFragment<Config>("test")
83
87
  .withDependencies(() => ({ dep: "value" }))
84
- .withServices(({ config }) => ({
85
- multiply: (x: number) => x * config.multiplier,
86
- }));
88
+ .providesService(({ config, defineService }) =>
89
+ defineService({
90
+ multiply: (x: number) => x * config.multiplier,
91
+ }),
92
+ );
87
93
 
88
94
  const routeFactory = defineRoutes<Config, Deps, Services>().create(({ services }) => [
89
95
  defineRoute({
@@ -233,7 +239,7 @@ describe("fragment.callRoute", () => {
233
239
  });
234
240
 
235
241
  it("should handle route factory created with defineRoutes", async () => {
236
- const fragment = defineFragment<{ apiKey: string }>("test").withServices(() => ({
242
+ const fragment = defineFragment<{ apiKey: string }>("test").providesService(() => ({
237
243
  getGreeting: (name: string) => `Hello, ${name}!`,
238
244
  getCount: () => 42,
239
245
  }));
@@ -270,10 +276,9 @@ describe("fragment.callRoute", () => {
270
276
  pathParams: { name: "World" },
271
277
  });
272
278
 
273
- expect(greetingResponse.type).toBe("json");
274
- if (greetingResponse.type === "json") {
275
- expect(greetingResponse.data).toEqual({ message: "Hello, World!" });
276
- }
279
+ console.log(greetingResponse);
280
+ assert(greetingResponse.type === "json");
281
+ expect(greetingResponse.data).toEqual({ message: "Hello, World!" });
277
282
 
278
283
  // Test second route
279
284
  const countResponse = await testFragment.callRoute("GET", "/count");
package/src/test/test.ts CHANGED
@@ -23,12 +23,14 @@ export interface CreateFragmentForTestOptions<
23
23
  TServices,
24
24
  TAdditionalContext extends Record<string, unknown>,
25
25
  TOptions extends FragnoPublicConfig,
26
+ TRequiredInterfaces extends Record<string, unknown> = {},
26
27
  > {
27
28
  config: TConfig;
28
29
  options?: Partial<TOptions>;
29
30
  deps?: Partial<TDeps>;
30
31
  services?: Partial<TServices>;
31
32
  additionalContext?: Partial<TAdditionalContext>;
33
+ interfaceImplementations?: TRequiredInterfaces;
32
34
  }
33
35
 
34
36
  /**
@@ -108,18 +110,34 @@ export function createFragmentForTest<
108
110
  TServices extends Record<string, unknown>,
109
111
  TAdditionalContext extends Record<string, unknown>,
110
112
  TOptions extends FragnoPublicConfig,
113
+ TRequiredInterfaces extends Record<string, unknown>,
114
+ TProvidedInterfaces extends Record<string, unknown>,
111
115
  const TRoutesOrFactories extends readonly AnyRouteOrFactory[],
112
116
  >(
113
117
  fragmentBuilder: {
114
- definition: FragmentDefinition<TConfig, TDeps, TServices, TAdditionalContext>;
118
+ definition: FragmentDefinition<
119
+ TConfig,
120
+ TDeps,
121
+ TServices,
122
+ TAdditionalContext,
123
+ TRequiredInterfaces,
124
+ TProvidedInterfaces
125
+ >;
115
126
  $requiredOptions: TOptions;
116
127
  },
117
128
  routesOrFactories: TRoutesOrFactories,
118
- options: CreateFragmentForTestOptions<TConfig, TDeps, TServices, TAdditionalContext, TOptions>,
129
+ options: CreateFragmentForTestOptions<
130
+ TConfig,
131
+ TDeps,
132
+ TServices,
133
+ TAdditionalContext,
134
+ TOptions,
135
+ TRequiredInterfaces
136
+ >,
119
137
  ): FragmentForTest<
120
138
  TConfig,
121
- TDeps,
122
- TServices,
139
+ TDeps & TRequiredInterfaces,
140
+ TServices & TProvidedInterfaces,
123
141
  TAdditionalContext,
124
142
  TOptions,
125
143
  FlattenRouteFactories<TRoutesOrFactories>
@@ -130,6 +148,7 @@ export function createFragmentForTest<
130
148
  deps: depsOverride,
131
149
  services: servicesOverride,
132
150
  additionalContext: additionalContextOverride,
151
+ interfaceImplementations,
133
152
  } = options;
134
153
 
135
154
  // Create deps from definition or use empty object
@@ -138,16 +157,30 @@ export function createFragmentForTest<
138
157
  ? definition.dependencies(config, fragmentOptions)
139
158
  : ({} as TDeps);
140
159
 
141
- // Merge deps with overrides
142
- const deps = { ...baseDeps, ...depsOverride } as TDeps;
160
+ // Merge deps with overrides and interface implementations
161
+ const deps = {
162
+ ...baseDeps,
163
+ ...interfaceImplementations,
164
+ ...depsOverride,
165
+ } as TDeps & TRequiredInterfaces;
143
166
 
144
167
  // Create services from definition or use empty object
145
168
  const baseServices = definition.services
146
169
  ? definition.services(config, fragmentOptions, deps)
147
170
  : ({} as TServices);
148
171
 
149
- // Merge services with overrides
150
- const services = { ...baseServices, ...servicesOverride } as TServices;
172
+ // Handle providedServices - can be either a factory function or a direct object
173
+ const providedServicesResolved =
174
+ typeof definition.providedServices === "function"
175
+ ? definition.providedServices(config, fragmentOptions, deps)
176
+ : definition.providedServices;
177
+
178
+ // Merge services with provided services and overrides
179
+ const services = {
180
+ ...baseServices,
181
+ ...providedServicesResolved,
182
+ ...servicesOverride,
183
+ } as TServices & TProvidedInterfaces;
151
184
 
152
185
  // Merge additional context with options
153
186
  const additionalContext = {
@@ -157,7 +190,13 @@ export function createFragmentForTest<
157
190
  } as TAdditionalContext & TOptions;
158
191
 
159
192
  // Create the actual fragment using createFragment
160
- const fragment = createFragment(fragmentBuilder, config, routesOrFactories, fragmentOptions);
193
+ const fragment = createFragment(
194
+ fragmentBuilder,
195
+ config,
196
+ routesOrFactories,
197
+ fragmentOptions,
198
+ interfaceImplementations,
199
+ );
161
200
 
162
201
  return {
163
202
  config,