@fragno-dev/core 0.1.8 → 0.1.9

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 (176) hide show
  1. package/.turbo/turbo-build.log +131 -56
  2. package/CHANGELOG.md +13 -0
  3. package/dist/api/api.d.ts +38 -2
  4. package/dist/api/api.d.ts.map +1 -0
  5. package/dist/api/api.js +9 -3
  6. package/dist/api/api.js.map +1 -0
  7. package/dist/api/bind-services.d.ts +6 -0
  8. package/dist/api/bind-services.d.ts.map +1 -0
  9. package/dist/api/bind-services.js +20 -0
  10. package/dist/api/bind-services.js.map +1 -0
  11. package/dist/api/error.d.ts +26 -0
  12. package/dist/api/error.d.ts.map +1 -0
  13. package/dist/api/error.js +48 -0
  14. package/dist/api/error.js.map +1 -0
  15. package/dist/api/fragment-definition-builder.d.ts +313 -0
  16. package/dist/api/fragment-definition-builder.d.ts.map +1 -0
  17. package/dist/api/fragment-definition-builder.js +326 -0
  18. package/dist/api/fragment-definition-builder.js.map +1 -0
  19. package/dist/api/fragment-instantiator.d.ts +216 -0
  20. package/dist/api/fragment-instantiator.d.ts.map +1 -0
  21. package/dist/api/fragment-instantiator.js +487 -0
  22. package/dist/api/fragment-instantiator.js.map +1 -0
  23. package/dist/api/fragno-response.d.ts +30 -0
  24. package/dist/api/fragno-response.d.ts.map +1 -0
  25. package/dist/api/fragno-response.js +73 -0
  26. package/dist/api/fragno-response.js.map +1 -0
  27. package/dist/api/internal/path.d.ts +50 -0
  28. package/dist/api/internal/path.d.ts.map +1 -0
  29. package/dist/api/internal/path.js +76 -0
  30. package/dist/api/internal/path.js.map +1 -0
  31. package/dist/api/internal/response-stream.d.ts +43 -0
  32. package/dist/api/internal/response-stream.d.ts.map +1 -0
  33. package/dist/api/internal/response-stream.js +81 -0
  34. package/dist/api/internal/response-stream.js.map +1 -0
  35. package/dist/api/internal/route.js +10 -0
  36. package/dist/api/internal/route.js.map +1 -0
  37. package/dist/api/mutable-request-state.d.ts +82 -0
  38. package/dist/api/mutable-request-state.d.ts.map +1 -0
  39. package/dist/api/mutable-request-state.js +97 -0
  40. package/dist/api/mutable-request-state.js.map +1 -0
  41. package/dist/api/request-context-storage.d.ts +42 -0
  42. package/dist/api/request-context-storage.d.ts.map +1 -0
  43. package/dist/api/request-context-storage.js +43 -0
  44. package/dist/api/request-context-storage.js.map +1 -0
  45. package/dist/api/request-input-context.d.ts +89 -0
  46. package/dist/api/request-input-context.d.ts.map +1 -0
  47. package/dist/api/request-input-context.js +118 -0
  48. package/dist/api/request-input-context.js.map +1 -0
  49. package/dist/api/request-middleware.d.ts +50 -0
  50. package/dist/api/request-middleware.d.ts.map +1 -0
  51. package/dist/api/request-middleware.js +83 -0
  52. package/dist/api/request-middleware.js.map +1 -0
  53. package/dist/api/request-output-context.d.ts +41 -0
  54. package/dist/api/request-output-context.d.ts.map +1 -0
  55. package/dist/api/request-output-context.js +119 -0
  56. package/dist/api/request-output-context.js.map +1 -0
  57. package/dist/api/route-handler-input-options.d.ts +21 -0
  58. package/dist/api/route-handler-input-options.d.ts.map +1 -0
  59. package/dist/api/route.d.ts +54 -2
  60. package/dist/api/route.d.ts.map +1 -0
  61. package/dist/api/route.js +29 -2
  62. package/dist/api/route.js.map +1 -0
  63. package/dist/api/shared-types.d.ts +47 -0
  64. package/dist/api/shared-types.d.ts.map +1 -0
  65. package/dist/api/shared-types.js +1 -0
  66. package/dist/client/client-error.d.ts +60 -0
  67. package/dist/client/client-error.d.ts.map +1 -0
  68. package/dist/client/client-error.js +92 -0
  69. package/dist/client/client-error.js.map +1 -0
  70. package/dist/client/client.d.ts +210 -2
  71. package/dist/client/client.d.ts.map +1 -0
  72. package/dist/client/client.js +397 -5
  73. package/dist/client/client.js.map +1 -0
  74. package/dist/client/client.svelte.d.ts +5 -2
  75. package/dist/client/client.svelte.d.ts.map +1 -1
  76. package/dist/client/client.svelte.js +1 -4
  77. package/dist/client/client.svelte.js.map +1 -1
  78. package/dist/client/internal/fetcher-merge.js +36 -0
  79. package/dist/client/internal/fetcher-merge.js.map +1 -0
  80. package/dist/client/internal/ndjson-streaming.js +139 -0
  81. package/dist/client/internal/ndjson-streaming.js.map +1 -0
  82. package/dist/client/react.d.ts +5 -2
  83. package/dist/client/react.d.ts.map +1 -1
  84. package/dist/client/react.js +3 -4
  85. package/dist/client/react.js.map +1 -1
  86. package/dist/client/solid.d.ts +5 -2
  87. package/dist/client/solid.d.ts.map +1 -1
  88. package/dist/client/solid.js +2 -4
  89. package/dist/client/solid.js.map +1 -1
  90. package/dist/client/vanilla.d.ts +5 -2
  91. package/dist/client/vanilla.d.ts.map +1 -1
  92. package/dist/client/vanilla.js +2 -42
  93. package/dist/client/vanilla.js.map +1 -1
  94. package/dist/client/vue.d.ts +5 -2
  95. package/dist/client/vue.d.ts.map +1 -1
  96. package/dist/client/vue.js +1 -4
  97. package/dist/client/vue.js.map +1 -1
  98. package/dist/http/http-status.d.ts +26 -0
  99. package/dist/http/http-status.d.ts.map +1 -0
  100. package/dist/integrations/react-ssr.js +1 -1
  101. package/dist/internal/symbols.d.ts +9 -0
  102. package/dist/internal/symbols.d.ts.map +1 -0
  103. package/dist/internal/symbols.js +10 -0
  104. package/dist/internal/symbols.js.map +1 -0
  105. package/dist/mod-client.d.ts +36 -0
  106. package/dist/mod-client.d.ts.map +1 -0
  107. package/dist/mod-client.js +21 -0
  108. package/dist/mod-client.js.map +1 -0
  109. package/dist/mod.d.ts +7 -2
  110. package/dist/mod.js +4 -4
  111. package/dist/request/request.d.ts +4 -0
  112. package/dist/request/request.js +5 -0
  113. package/dist/test/test.d.ts +62 -34
  114. package/dist/test/test.d.ts.map +1 -1
  115. package/dist/test/test.js +75 -42
  116. package/dist/test/test.js.map +1 -1
  117. package/dist/util/async.js +40 -0
  118. package/dist/util/async.js.map +1 -0
  119. package/dist/util/content-type.js +49 -0
  120. package/dist/util/content-type.js.map +1 -0
  121. package/dist/util/nanostores.js +31 -0
  122. package/dist/util/nanostores.js.map +1 -0
  123. package/dist/{ssr-kyKI7pqH.js → util/ssr.js} +2 -2
  124. package/dist/util/ssr.js.map +1 -0
  125. package/dist/util/types-util.d.ts +8 -0
  126. package/dist/util/types-util.d.ts.map +1 -0
  127. package/package.json +19 -12
  128. package/src/api/api.ts +1 -5
  129. package/src/api/bind-services.ts +42 -0
  130. package/src/api/fragment-definition-builder.extend.test.ts +810 -0
  131. package/src/api/fragment-definition-builder.test.ts +499 -0
  132. package/src/api/fragment-definition-builder.ts +1088 -0
  133. package/src/api/fragment-instantiator.test.ts +1488 -0
  134. package/src/api/fragment-instantiator.ts +1053 -0
  135. package/src/api/fragment-services.test.ts +454 -189
  136. package/src/api/request-context-storage.ts +64 -0
  137. package/src/api/request-middleware.test.ts +301 -228
  138. package/src/api/route.test.ts +12 -36
  139. package/src/api/route.ts +167 -155
  140. package/src/api/shared-types.ts +43 -0
  141. package/src/client/client-builder.test.ts +23 -23
  142. package/src/client/client.ssr.test.ts +3 -3
  143. package/src/client/client.svelte.test.ts +15 -15
  144. package/src/client/client.test.ts +22 -22
  145. package/src/client/client.ts +72 -12
  146. package/src/client/internal/fetcher-merge.ts +1 -1
  147. package/src/client/react.test.ts +2 -2
  148. package/src/client/solid.test.ts +2 -2
  149. package/src/client/vanilla.test.ts +2 -2
  150. package/src/client/vue.test.ts +2 -2
  151. package/src/internal/symbols.ts +5 -0
  152. package/src/mod-client.ts +59 -0
  153. package/src/mod.ts +22 -15
  154. package/src/request/request.ts +8 -0
  155. package/src/test/test.test.ts +189 -375
  156. package/src/test/test.ts +186 -152
  157. package/tsdown.config.ts +8 -5
  158. package/dist/api/fragment-builder.d.ts +0 -2
  159. package/dist/api/fragment-builder.js +0 -3
  160. package/dist/api/fragment-instantiation.d.ts +0 -2
  161. package/dist/api/fragment-instantiation.js +0 -4
  162. package/dist/api-BFrUCIsF.d.ts +0 -963
  163. package/dist/api-BFrUCIsF.d.ts.map +0 -1
  164. package/dist/client-DAFHcKqA.js +0 -782
  165. package/dist/client-DAFHcKqA.js.map +0 -1
  166. package/dist/fragment-builder-Boh2vNHq.js +0 -108
  167. package/dist/fragment-builder-Boh2vNHq.js.map +0 -1
  168. package/dist/fragment-instantiation-DUT-HLl1.js +0 -898
  169. package/dist/fragment-instantiation-DUT-HLl1.js.map +0 -1
  170. package/dist/route-C4CyNHkC.js +0 -26
  171. package/dist/route-C4CyNHkC.js.map +0 -1
  172. package/dist/ssr-kyKI7pqH.js.map +0 -1
  173. package/src/api/fragment-builder.ts +0 -518
  174. package/src/api/fragment-instantiation.test.ts +0 -702
  175. package/src/api/fragment-instantiation.ts +0 -766
  176. package/src/api/fragment.test.ts +0 -585
@@ -1,6 +1,6 @@
1
1
  import { describe, test, expect, expectTypeOf } from "vitest";
2
- import { defineFragment } from "./fragment-builder";
3
- import { createFragment, instantiateFragment } from "./fragment-instantiation";
2
+ import { defineFragment } from "./fragment-definition-builder";
3
+ import { instantiate } from "./fragment-instantiator";
4
4
 
5
5
  // Test service interface definitions
6
6
  interface IEmailService {
@@ -14,75 +14,69 @@ interface ILogger {
14
14
  describe("Fragment Service System", () => {
15
15
  describe("usesService", () => {
16
16
  test("should declare required service by default", () => {
17
- const fragment = defineFragment<{}>("test-fragment").usesService<"email", IEmailService>(
18
- "email",
19
- );
17
+ const definition = defineFragment("test-fragment")
18
+ .usesService<"email", IEmailService>("email")
19
+ .build();
20
20
 
21
- expect(fragment.definition.usedServices).toBeDefined();
22
- expect(fragment.definition.usedServices?.email).toEqual({ name: "email", required: true });
21
+ expect(definition.serviceDependencies).toBeDefined();
22
+ expect(definition.serviceDependencies?.email).toEqual({ name: "email", required: true });
23
23
  });
24
24
 
25
- test("should declare optional service with { optional: true }", () => {
26
- const fragment = defineFragment<{}>("test-fragment").usesService<"email", IEmailService>(
27
- "email",
28
- { optional: true },
29
- );
25
+ test("should declare optional service with usesOptionalService", () => {
26
+ const definition = defineFragment("test-fragment")
27
+ .usesOptionalService<"email", IEmailService>("email")
28
+ .build();
30
29
 
31
- expect(fragment.definition.usedServices).toBeDefined();
32
- expect(fragment.definition.usedServices?.email).toEqual({ name: "email", required: false });
30
+ expect(definition.serviceDependencies).toBeDefined();
31
+ expect(definition.serviceDependencies?.email).toEqual({ name: "email", required: false });
33
32
  });
34
33
 
35
34
  test("should support multiple required services", () => {
36
- const fragment = defineFragment<{}>("test-fragment")
35
+ const definition = defineFragment("test-fragment")
37
36
  .usesService<"email", IEmailService>("email")
38
- .usesService<"logger", ILogger>("logger");
37
+ .usesService<"logger", ILogger>("logger")
38
+ .build();
39
39
 
40
- expect(fragment.definition.usedServices?.email).toEqual({ name: "email", required: true });
41
- expect(fragment.definition.usedServices?.logger).toEqual({ name: "logger", required: true });
40
+ expect(definition.serviceDependencies?.email).toEqual({ name: "email", required: true });
41
+ expect(definition.serviceDependencies?.logger).toEqual({ name: "logger", required: true });
42
42
  });
43
43
 
44
44
  test("should support mixing required and optional services", () => {
45
- const fragment = defineFragment<{}>("test-fragment")
45
+ const definition = defineFragment("test-fragment")
46
46
  .usesService<"email", IEmailService>("email")
47
- .usesService<"logger", ILogger>("logger", { optional: true });
47
+ .usesOptionalService<"logger", ILogger>("logger")
48
+ .build();
48
49
 
49
- expect(fragment.definition.usedServices?.email).toEqual({ name: "email", required: true });
50
- expect(fragment.definition.usedServices?.logger).toEqual({ name: "logger", required: false });
50
+ expect(definition.serviceDependencies?.email).toEqual({ name: "email", required: true });
51
+ expect(definition.serviceDependencies?.logger).toEqual({ name: "logger", required: false });
51
52
  });
52
53
 
53
54
  test("should preserve other fragment properties", () => {
54
- const fragment = defineFragment<{ apiKey: string }>("test-fragment")
55
+ const definition = defineFragment<{ apiKey: string }>("test-fragment")
55
56
  .withDependencies(() => ({ dep: "value" }))
56
- .usesService<"email", IEmailService>("email");
57
+ .usesService<"email", IEmailService>("email")
58
+ .build();
57
59
 
58
- expect(fragment.definition.name).toBe("test-fragment");
59
- expect(fragment.definition.usedServices?.email).toBeDefined();
60
+ expect(definition.name).toBe("test-fragment");
61
+ expect(definition.serviceDependencies?.email).toBeDefined();
60
62
  });
61
63
 
62
64
  test("should have correct type inference for required service", () => {
63
- const fragment = defineFragment<{}>("test").usesService<"email", IEmailService>("email");
65
+ const definition = defineFragment("test")
66
+ .usesService<"email", IEmailService>("email")
67
+ .build();
64
68
 
65
- expectTypeOf(fragment).toMatchTypeOf<{
66
- definition: {
67
- usedServices?: {
68
- email: { name: string; required: boolean };
69
- };
70
- };
71
- }>();
69
+ expect(definition.serviceDependencies?.email).toBeDefined();
70
+ expect(definition.serviceDependencies?.email?.required).toBe(true);
72
71
  });
73
72
 
74
73
  test("should have correct type inference for optional service", () => {
75
- const fragment = defineFragment<{}>("test").usesService<"logger", ILogger>("logger", {
76
- optional: true,
77
- });
78
-
79
- expectTypeOf(fragment).toMatchTypeOf<{
80
- definition: {
81
- usedServices?: {
82
- logger: { name: string; required: boolean };
83
- };
84
- };
85
- }>();
74
+ const definition = defineFragment("test")
75
+ .usesOptionalService<"logger", ILogger>("logger")
76
+ .build();
77
+
78
+ expect(definition.serviceDependencies?.logger).toBeDefined();
79
+ expect(definition.serviceDependencies?.logger?.required).toBe(false);
86
80
  });
87
81
  });
88
82
 
@@ -92,12 +86,11 @@ describe("Fragment Service System", () => {
92
86
  sendEmail: async () => {},
93
87
  };
94
88
 
95
- const fragment = defineFragment<{}>("test-fragment").providesService(
96
- "email",
97
- ({ defineService }) => defineService(emailImpl),
98
- );
89
+ const definition = defineFragment("test-fragment")
90
+ .providesService("email", () => emailImpl)
91
+ .build();
99
92
 
100
- expect(fragment.definition.providedServices).toBeDefined();
93
+ expect(definition.namedServices).toBeDefined();
101
94
  });
102
95
 
103
96
  test("should support multiple provided services", () => {
@@ -109,20 +102,22 @@ describe("Fragment Service System", () => {
109
102
  log: () => {},
110
103
  };
111
104
 
112
- const _fragment = defineFragment<{}>("test-fragment")
113
- .providesService("email", ({ defineService }) => defineService(emailImpl))
114
- .providesService("logger", ({ defineService }) => defineService(loggerImpl));
105
+ const _definition = defineFragment("test-fragment")
106
+ .providesService("email", () => emailImpl)
107
+ .providesService("logger", () => loggerImpl)
108
+ .build();
115
109
  });
116
110
  });
117
111
 
118
112
  describe("Service metadata", () => {
119
113
  test("should store service metadata in definition", () => {
120
- const fragment = defineFragment<{}>("test")
114
+ const definition = defineFragment("test")
121
115
  .usesService<"email", IEmailService>("email")
122
- .usesService<"logger", ILogger>("logger", { optional: true });
116
+ .usesOptionalService<"logger", ILogger>("logger")
117
+ .build();
123
118
 
124
- expect(fragment.definition.usedServices?.email?.required).toBe(true);
125
- expect(fragment.definition.usedServices?.logger?.required).toBe(false);
119
+ expect(definition.serviceDependencies?.email?.required).toBe(true);
120
+ expect(definition.serviceDependencies?.logger?.required).toBe(false);
126
121
  });
127
122
 
128
123
  test("should store provided services in definition", () => {
@@ -130,139 +125,159 @@ describe("Fragment Service System", () => {
130
125
  sendEmail: async () => {},
131
126
  };
132
127
 
133
- const fragment = defineFragment<{}>("test").providesService("email", ({ defineService }) =>
134
- defineService(emailImpl),
135
- );
128
+ const definition = defineFragment("test")
129
+ .providesService("email", () => emailImpl)
130
+ .build();
136
131
 
137
- expect(typeof fragment.definition.providedServices).toBe("object");
132
+ expect(typeof definition.namedServices).toBe("object");
138
133
  });
139
134
 
140
135
  test("should allow fragments without any services", () => {
141
- const fragment = defineFragment<{}>("test");
136
+ const definition = defineFragment("test").build();
142
137
 
143
- expect(fragment.definition.usedServices).toBeUndefined();
144
- expect(fragment.definition.providedServices).toBeUndefined();
138
+ expect(definition.serviceDependencies).toBeUndefined();
139
+ expect(definition.namedServices).toBeUndefined();
145
140
  });
146
141
  });
147
142
 
148
143
  describe("Type safety", () => {
149
- test("Unnamed services should have correct types (using defineService)", () => {
150
- const fragment = defineFragment<{}>("test").providesService(({ defineService }) =>
151
- defineService({
144
+ test("Unnamed services should have correct types (using providesBaseService)", () => {
145
+ const definition = defineFragment("test")
146
+ .providesBaseService(() => ({
152
147
  sendEmail: async () => {},
153
- }),
154
- );
148
+ }))
149
+ .build();
155
150
 
156
- const instance = createFragment(fragment, {}, [], {});
151
+ const instance = instantiate(definition).withOptions({}).build();
157
152
  expect(instance.services.sendEmail).toBeDefined();
158
153
  expectTypeOf<typeof instance.services.sendEmail>().toExtend<() => Promise<void>>();
159
154
  });
160
155
 
161
- test("Named services should have correct types (using defineService)", () => {
162
- const fragment = defineFragment<{}>("test").providesService("email", ({ defineService }) =>
163
- defineService({
156
+ test("Named services should have correct types", () => {
157
+ const definition = defineFragment("test")
158
+ .providesService("email", () => ({
164
159
  sendEmail: async () => {},
165
- }),
166
- );
160
+ }))
161
+ .build();
167
162
 
168
- const instance = createFragment(fragment, {}, [], {});
163
+ const instance = instantiate(definition).withOptions({}).build();
169
164
  expect(instance.services.email.sendEmail).toBeDefined();
170
165
  expectTypeOf<typeof instance.services.email.sendEmail>().toExtend<() => Promise<void>>();
171
166
  });
172
167
 
173
- test("Unnamed services should have correct types (using object)", () => {
174
- const fragment = defineFragment<{}>("test").providesService({
175
- sendEmail: async () => {},
176
- });
168
+ test("Unnamed services should have correct types (using factory)", () => {
169
+ const definition = defineFragment("test")
170
+ .providesBaseService(() => ({
171
+ sendEmail: async () => {},
172
+ }))
173
+ .build();
177
174
 
178
- const instance = createFragment(fragment, {}, [], {});
175
+ const instance = instantiate(definition).withOptions({}).build();
179
176
  expect(instance.services.sendEmail).toBeDefined();
180
177
  expectTypeOf<typeof instance.services.sendEmail>().toExtend<() => Promise<void>>();
181
178
  });
182
179
 
183
180
  test("Unnamed services should have correct types (using callback with context)", () => {
184
- const fragment = defineFragment<{}>("test").providesService(({ defineService }) =>
185
- defineService({
181
+ const definition = defineFragment("test")
182
+ .providesBaseService(() => ({
186
183
  sendEmail: async () => {},
187
- }),
188
- );
184
+ }))
185
+ .build();
189
186
 
190
- const instance = createFragment(fragment, {}, [], {});
187
+ const instance = instantiate(definition).withOptions({}).build();
191
188
  expect(instance.services.sendEmail).toBeDefined();
192
189
  expectTypeOf<typeof instance.services.sendEmail>().toExtend<() => Promise<void>>();
193
190
  });
194
191
 
195
192
  test("Unnamed services should have correct types (using 0-arity factory)", () => {
196
- const fragment = defineFragment<{}>("test").providesService(() => ({
197
- sendEmail: async () => {},
198
- }));
193
+ const definition = defineFragment("test")
194
+ .providesBaseService(() => ({
195
+ sendEmail: async () => {},
196
+ }))
197
+ .build();
199
198
 
200
- const instance = createFragment(fragment, {}, [], {});
199
+ const instance = instantiate(definition).withOptions({}).build();
201
200
  expect(instance.services.sendEmail).toBeDefined();
202
201
  expectTypeOf<typeof instance.services.sendEmail>().toExtend<() => Promise<void>>();
203
202
  });
204
203
 
205
- test("Named services should have correct types (using object)", () => {
206
- const fragment = defineFragment<{}>("test").providesService("email", {
207
- sendEmail: async () => {},
208
- });
204
+ test("Named services should have correct types (using factory)", () => {
205
+ const definition = defineFragment("test")
206
+ .providesService("email", () => ({
207
+ sendEmail: async () => {},
208
+ }))
209
+ .build();
209
210
 
210
- const instance = createFragment(fragment, {}, [], {});
211
+ const instance = instantiate(definition).withOptions({}).build();
211
212
  expect(instance.services.email.sendEmail).toBeDefined();
212
213
  expectTypeOf<typeof instance.services.email.sendEmail>().toExtend<() => Promise<void>>();
213
214
  });
214
215
 
215
216
  test("usesService (required)", () => {
216
- const fragment = defineFragment<{}>("test").usesService<"email", IEmailService>("email");
217
+ const definition = defineFragment("test")
218
+ .usesService<"email", IEmailService>("email")
219
+ .providesBaseService(({ serviceDeps }) => ({
220
+ sendEmail: (to: string, subject: string, body: string) => {
221
+ return serviceDeps.email.sendEmail(to, subject, body);
222
+ },
223
+ }))
224
+ .build();
217
225
 
218
226
  const emailImpl: IEmailService = {
219
227
  sendEmail: async () => {},
220
228
  };
221
229
 
222
- const instance = createFragment(
223
- fragment,
224
- {},
225
- [],
226
- {},
227
- {
228
- email: emailImpl,
229
- },
230
- );
231
-
232
- expectTypeOf<typeof instance.services.email.sendEmail>().toExtend<
230
+ const instance = instantiate(definition)
231
+ .withOptions({})
232
+ .withServices({ email: emailImpl })
233
+ .build();
234
+
235
+ expect(instance.services.sendEmail).toBeDefined();
236
+ expectTypeOf<typeof instance.services.sendEmail>().toExtend<
233
237
  (to: string, subject: string, body: string) => void
234
238
  >();
235
239
  });
236
240
 
237
241
  test("usesService (required) - builder style", () => {
238
- const fragment = defineFragment<{}>("test").usesService<"email", IEmailService>("email");
242
+ const definition = defineFragment("test")
243
+ .usesService<"email", IEmailService>("email")
244
+ .providesBaseService(({ serviceDeps }) => ({
245
+ sendEmail: (to: string, subject: string, body: string) => {
246
+ return serviceDeps.email.sendEmail(to, subject, body);
247
+ },
248
+ }))
249
+ .build();
239
250
 
240
251
  const emailImpl: IEmailService = {
241
252
  sendEmail: async () => {},
242
253
  };
243
254
 
244
- const instance = instantiateFragment(fragment).withServices({ email: emailImpl }).build();
255
+ const instance = instantiate(definition)
256
+ .withServices({ email: emailImpl })
257
+ .withOptions({})
258
+ .build();
245
259
 
246
- expectTypeOf<typeof instance.services.email.sendEmail>().toExtend<
260
+ expect(instance.services.sendEmail).toBeDefined();
261
+ expectTypeOf<typeof instance.services.sendEmail>().toExtend<
247
262
  (to: string, subject: string, body: string) => void
248
263
  >();
249
264
  });
250
265
 
251
- test("usesService (optional)", () => {
252
- const fragment = defineFragment<{}>("test").usesService<"email", IEmailService>("email", {
253
- optional: true,
254
- });
266
+ test("usesOptionalService", () => {
267
+ const definition = defineFragment("test")
268
+ .usesOptionalService<"email", IEmailService>("email")
269
+ .providesBaseService(({ serviceDeps }) => ({
270
+ sendEmail: serviceDeps.email
271
+ ? (to: string, subject: string, body: string) =>
272
+ serviceDeps.email!.sendEmail(to, subject, body)
273
+ : undefined,
274
+ }))
275
+ .build();
255
276
 
256
- const instance = createFragment(fragment, {}, [], {});
257
- // For optional services, the service itself might be undefined
258
- expectTypeOf<typeof instance.services.email>().toExtend<IEmailService | undefined>();
277
+ const instance = instantiate(definition).withOptions({}).build();
259
278
 
260
- // If provided, the service should have the correct type
261
- if (instance.services.email) {
262
- expectTypeOf<typeof instance.services.email.sendEmail>().toExtend<
263
- (to: string, subject: string, body: string) => Promise<void>
264
- >();
265
- }
279
+ // For optional services, the wrapped service method might be undefined
280
+ expect(instance.services.sendEmail).toBeUndefined();
266
281
  });
267
282
 
268
283
  test("provided services should have correct types", () => {
@@ -270,54 +285,89 @@ describe("Fragment Service System", () => {
270
285
  sendEmail: async () => {},
271
286
  };
272
287
 
273
- const fragment = defineFragment<{}>("test").providesService("email", ({ defineService }) =>
274
- defineService(emailImpl),
275
- );
288
+ const definition = defineFragment("test")
289
+ .providesService("email", () => emailImpl)
290
+ .build();
276
291
 
277
- // providedServices stores an object with service names as keys and factory functions as values
278
- expect(fragment.definition.providedServices).toBeDefined();
279
- expect(typeof fragment.definition.providedServices).toBe("object");
292
+ // namedServices stores an object with service names as keys and factory functions as values
293
+ expect(definition.namedServices).toBeDefined();
294
+ expect(typeof definition.namedServices).toBe("object");
280
295
  });
281
296
 
282
297
  test("Named services should have correct types (using callback with context)", () => {
283
- const fragment = defineFragment<{}>("test").providesService("email", ({ defineService }) =>
284
- defineService({
298
+ const definition = defineFragment("test")
299
+ .providesService("email", () => ({
285
300
  sendEmail: async () => {},
286
- }),
287
- );
301
+ }))
302
+ .build();
288
303
 
289
- const instance = createFragment(fragment, {}, [], {});
304
+ const instance = instantiate(definition).withOptions({}).build();
290
305
  expect(instance.services.email.sendEmail).toBeDefined();
291
306
  expectTypeOf<typeof instance.services.email.sendEmail>().toExtend<() => Promise<void>>();
292
307
  });
293
308
 
294
309
  test("Named services should have correct types (using 0-arity factory)", () => {
295
- const fragment = defineFragment<{}>("test").providesService("email", () => ({
296
- sendEmail: async () => {},
297
- }));
310
+ const definition = defineFragment("test")
311
+ .providesService("email", () => ({
312
+ sendEmail: async () => {},
313
+ }))
314
+ .build();
298
315
 
299
- const instance = createFragment(fragment, {}, [], {});
316
+ const instance = instantiate(definition).withOptions({}).build();
300
317
  expect(instance.services.email.sendEmail).toBeDefined();
301
318
  expectTypeOf<typeof instance.services.email.sendEmail>().toExtend<() => Promise<void>>();
302
319
  });
320
+
321
+ test("Type mismatch when using a service", () => {
322
+ interface ExpectedService {
323
+ throwDice: () => 1 | 2 | 3 | 4 | 5 | 6;
324
+ }
325
+
326
+ const definition = defineFragment("test")
327
+ .usesService<"expected", ExpectedService>("expected")
328
+ .providesBaseService(({ serviceDeps }) => ({
329
+ throwDice: () => serviceDeps.expected.throwDice(),
330
+ }))
331
+ .build();
332
+
333
+ interface ActualService {
334
+ throwDice: () => number;
335
+ }
336
+
337
+ const actualService: ActualService = {
338
+ throwDice: () => 1,
339
+ };
340
+
341
+ const instance = instantiate(definition)
342
+ // @ts-expect-error - Type mismatch
343
+ .withServices({ expected: actualService })
344
+ .withOptions({})
345
+ .build();
346
+
347
+ // The wrapped service on the instance has the correct type based on the declared service
348
+ expect(instance.services.throwDice).toBeDefined();
349
+ expectTypeOf<typeof instance.services.throwDice>().toExtend<() => 1 | 2 | 3 | 4 | 5 | 6>();
350
+ });
303
351
  });
304
352
 
305
353
  describe("Error handling", () => {
306
354
  test("should throw error when required service is not provided", () => {
307
- const fragment = defineFragment<{}>("test").usesService<"email", IEmailService>("email");
355
+ const definition = defineFragment("test")
356
+ .usesService<"email", IEmailService>("email")
357
+ .build();
308
358
 
309
359
  expect(() => {
310
- createFragment(fragment, {}, [], {});
360
+ instantiate(definition).withOptions({}).build();
311
361
  }).toThrow("Fragment 'test' requires service 'email' but it was not provided");
312
362
  });
313
363
 
314
364
  test("should not throw when optional service is not provided", () => {
315
- const fragment = defineFragment<{}>("test").usesService<"email", IEmailService>("email", {
316
- optional: true,
317
- });
365
+ const definition = defineFragment("test")
366
+ .usesOptionalService<"email", IEmailService>("email")
367
+ .build();
318
368
 
319
369
  expect(() => {
320
- createFragment(fragment, {}, [], {});
370
+ instantiate(definition).withOptions({}).build();
321
371
  }).not.toThrow();
322
372
  });
323
373
  });
@@ -328,15 +378,19 @@ describe("Fragment Service System", () => {
328
378
  sendEmail: async () => {},
329
379
  };
330
380
 
331
- const fragment = defineFragment<{}>("test")
381
+ const definition = defineFragment("test")
332
382
  .usesService<"email", IEmailService>("email")
333
- .providesService(({ deps }) => ({
383
+ .providesBaseService(({ serviceDeps }) => ({
334
384
  sendWelcomeEmail: async (to: string) => {
335
- await deps.email.sendEmail(to, "Welcome", "Welcome to our service!");
385
+ await serviceDeps.email.sendEmail(to, "Welcome", "Welcome to our service!");
336
386
  },
337
- }));
387
+ }))
388
+ .build();
338
389
 
339
- const instance = createFragment(fragment, {}, [], {}, { email: emailImpl });
390
+ const instance = instantiate(definition)
391
+ .withOptions({})
392
+ .withServices({ email: emailImpl })
393
+ .build();
340
394
 
341
395
  expect(instance.services.sendWelcomeEmail).toBeDefined();
342
396
  expect(typeof instance.services.sendWelcomeEmail).toBe("function");
@@ -347,40 +401,53 @@ describe("Fragment Service System", () => {
347
401
  sendEmail: async () => {},
348
402
  };
349
403
 
350
- const fragment = defineFragment<{}>("test")
404
+ const definition = defineFragment("test")
351
405
  .usesService<"email", IEmailService>("email")
352
- .providesService(({ deps }) => ({
406
+ .providesBaseService(({ serviceDeps }) => ({
353
407
  sendWelcomeEmail: async (to: string) => {
354
- await deps.email.sendEmail(to, "Welcome", "Welcome to our service!");
408
+ await serviceDeps.email.sendEmail(to, "Welcome", "Welcome to our service!");
355
409
  },
356
- }));
410
+ }))
411
+ .build();
357
412
 
358
- const instance = instantiateFragment(fragment).withServices({ email: emailImpl }).build();
413
+ const instance = instantiate(definition)
414
+ .withServices({ email: emailImpl })
415
+ .withOptions({})
416
+ .build();
359
417
 
360
418
  expect(instance.services.sendWelcomeEmail).toBeDefined();
361
419
  expect(typeof instance.services.sendWelcomeEmail).toBe("function");
362
420
  });
363
421
 
364
422
  test("provided service can access config", () => {
365
- const fragment = defineFragment<{ apiKey: string }>("test").providesService(({ config }) => ({
366
- getApiKey: () => config.apiKey,
367
- }));
423
+ const definition = defineFragment<{ apiKey: string }>("test")
424
+ .providesBaseService(({ config }) => ({
425
+ getApiKey: () => config.apiKey,
426
+ }))
427
+ .build();
368
428
 
369
- const instance = createFragment(fragment, { apiKey: "test-key" }, [], {});
429
+ const instance = instantiate(definition)
430
+ .withConfig({ apiKey: "test-key" })
431
+ .withOptions({})
432
+ .build();
370
433
 
371
434
  expect(instance.services.getApiKey()).toBe("test-key");
372
435
  });
373
436
 
374
437
  test("provided service can access deps from withDependencies", () => {
375
- const fragment = defineFragment<{ apiKey: string }>("test")
438
+ const definition = defineFragment<{ apiKey: string }>("test")
376
439
  .withDependencies(({ config }) => ({
377
440
  client: { key: config.apiKey },
378
441
  }))
379
- .providesService(({ deps }) => ({
442
+ .providesBaseService(({ deps }) => ({
380
443
  getClient: () => deps.client,
381
- }));
444
+ }))
445
+ .build();
382
446
 
383
- const instance = createFragment(fragment, { apiKey: "test-key" }, [], {});
447
+ const instance = instantiate(definition)
448
+ .withConfig({ apiKey: "test-key" })
449
+ .withOptions({})
450
+ .build();
384
451
 
385
452
  expect(instance.services.getClient()).toEqual({ key: "test-key" });
386
453
  });
@@ -388,29 +455,31 @@ describe("Fragment Service System", () => {
388
455
 
389
456
  describe("Service chaining and multiple services", () => {
390
457
  test("should support chaining multiple provided services", () => {
391
- const fragment = defineFragment<{}>("test")
392
- .providesService("email", {
458
+ const definition = defineFragment("test")
459
+ .providesService("email", () => ({
393
460
  sendEmail: async () => {},
394
- })
395
- .providesService("logger", {
461
+ }))
462
+ .providesService("logger", () => ({
396
463
  log: () => {},
397
- });
464
+ }))
465
+ .build();
398
466
 
399
- const instance = createFragment(fragment, {}, [], {});
467
+ const instance = instantiate(definition).withOptions({}).build();
400
468
  expect(instance.services.email.sendEmail).toBeDefined();
401
469
  expect(instance.services.logger.log).toBeDefined();
402
470
  });
403
471
 
404
472
  test("should support mixing unnamed and named provided services", () => {
405
- const fragment = defineFragment<{}>("test")
406
- .providesService({
473
+ const definition = defineFragment("test")
474
+ .providesBaseService(() => ({
407
475
  helper: () => "help",
408
- })
409
- .providesService("email", {
476
+ }))
477
+ .providesService("email", () => ({
410
478
  sendEmail: async () => {},
411
- });
479
+ }))
480
+ .build();
412
481
 
413
- const instance = createFragment(fragment, {}, [], {});
482
+ const instance = instantiate(definition).withOptions({}).build();
414
483
  expect(instance.services.helper).toBeDefined();
415
484
  expect(instance.services.email.sendEmail).toBeDefined();
416
485
  });
@@ -418,19 +487,20 @@ describe("Fragment Service System", () => {
418
487
 
419
488
  describe("Optional service runtime behavior", () => {
420
489
  test("should handle optional service when not provided", () => {
421
- const fragment = defineFragment<{}>("test")
422
- .usesService<"email", IEmailService>("email", { optional: true })
423
- .providesService(({ deps }) => ({
490
+ const definition = defineFragment("test")
491
+ .usesOptionalService<"email", IEmailService>("email")
492
+ .providesBaseService(({ serviceDeps }) => ({
424
493
  maybeSendEmail: async (to: string) => {
425
- if (deps.email) {
426
- await deps.email.sendEmail(to, "Subject", "Body");
494
+ if (serviceDeps.email) {
495
+ await serviceDeps.email.sendEmail(to, "Subject", "Body");
427
496
  return true;
428
497
  }
429
498
  return false;
430
499
  },
431
- }));
500
+ }))
501
+ .build();
432
502
 
433
- const instance = createFragment(fragment, {}, [], {});
503
+ const instance = instantiate(definition).withOptions({}).build();
434
504
 
435
505
  expect(instance.services.maybeSendEmail).toBeDefined();
436
506
  // Should not throw when optional service is not provided
@@ -441,22 +511,217 @@ describe("Fragment Service System", () => {
441
511
  sendEmail: async () => {},
442
512
  };
443
513
 
444
- const fragment = defineFragment<{}>("test")
445
- .usesService<"email", IEmailService>("email", { optional: true })
446
- .providesService(({ deps }) => ({
514
+ const definition = defineFragment("test")
515
+ .usesOptionalService<"email", IEmailService>("email")
516
+ .providesBaseService(({ serviceDeps }) => ({
447
517
  maybeSendEmail: async (to: string) => {
448
- if (deps.email) {
449
- await deps.email.sendEmail(to, "Subject", "Body");
518
+ if (serviceDeps.email) {
519
+ await serviceDeps.email.sendEmail(to, "Subject", "Body");
450
520
  return true;
451
521
  }
452
522
  return false;
453
523
  },
454
- }));
524
+ }))
525
+ .build();
455
526
 
456
- const instance = createFragment(fragment, {}, [], {}, { email: emailImpl });
527
+ const instance = instantiate(definition)
528
+ .withOptions({})
529
+ .withServices({ email: emailImpl })
530
+ .build();
457
531
 
458
- expect(instance.services.email).toBeDefined();
459
532
  expect(instance.services.maybeSendEmail).toBeDefined();
533
+ // When the optional service is provided, the wrapped method should work
534
+ expect(typeof instance.services.maybeSendEmail).toBe("function");
535
+ });
536
+ });
537
+
538
+ describe("Private Services", () => {
539
+ test("private service should be accessible when defining other services", () => {
540
+ interface IDataStore {
541
+ get(key: string): string | undefined;
542
+ set(key: string, value: string): void;
543
+ }
544
+
545
+ const dataStoreImpl: IDataStore = {
546
+ get: () => "cached-value",
547
+ set: () => {},
548
+ };
549
+
550
+ const definition = defineFragment("test")
551
+ .providesPrivateService("dataStore", () => dataStoreImpl)
552
+ .providesBaseService(({ privateServices }) => ({
553
+ getValue: (key: string) => {
554
+ // Private service is accessible here
555
+ return privateServices.dataStore.get(key);
556
+ },
557
+ }))
558
+ .build();
559
+
560
+ const instance = instantiate(definition).withOptions({}).build();
561
+
562
+ // Private service should NOT be accessible on the instance
563
+ expectTypeOf<typeof instance.services>().not.toMatchTypeOf<{ dataStore: IDataStore }>();
564
+
565
+ // But the public service that uses it should work
566
+ expect(instance.services.getValue).toBeDefined();
567
+ expect(instance.services.getValue("test")).toBe("cached-value");
568
+ });
569
+
570
+ test("private service should NOT be exposed on fragment instance", () => {
571
+ interface IInternalCache {
572
+ cache: Map<string, unknown>;
573
+ }
574
+
575
+ const definition = defineFragment("test")
576
+ .providesPrivateService<"cache", IInternalCache>("cache", () => ({
577
+ cache: new Map(),
578
+ }))
579
+ .providesBaseService(({ privateServices }) => ({
580
+ getCacheSize: () => privateServices.cache.cache.size,
581
+ }))
582
+ .build();
583
+
584
+ const instance = instantiate(definition).withOptions({}).build();
585
+
586
+ // @ts-expect-error - Private service should not be accessible
587
+ expect(instance.services.cache).toBeUndefined();
588
+
589
+ // Only the public service should be accessible
590
+ expect(instance.services.getCacheSize).toBeDefined();
591
+ });
592
+
593
+ test("multiple private services should work together", () => {
594
+ interface ILogger {
595
+ log(msg: string): void;
596
+ }
597
+
598
+ interface ICache {
599
+ get(key: string): unknown;
600
+ }
601
+
602
+ const logger: ILogger = {
603
+ log: () => {},
604
+ };
605
+
606
+ const cache: ICache = {
607
+ get: () => "cached",
608
+ };
609
+
610
+ const definition = defineFragment("test")
611
+ .providesPrivateService("logger", () => logger)
612
+ .providesPrivateService("cache", () => cache)
613
+ .providesBaseService(({ privateServices }) => ({
614
+ getCachedValue: (key: string) => {
615
+ privateServices.logger.log(`Getting ${key}`);
616
+ return privateServices.cache.get(key);
617
+ },
618
+ }))
619
+ .build();
620
+
621
+ const instance = instantiate(definition).withOptions({}).build();
622
+
623
+ expect(instance.services.getCachedValue).toBeDefined();
624
+ expect(instance.services.getCachedValue("test")).toBe("cached");
625
+ });
626
+
627
+ test("private services can access config and deps", () => {
628
+ const definition = defineFragment<{ apiKey: string }>("test")
629
+ .withDependencies(({ config }) => ({
630
+ endpoint: `https://api.example.com/${config.apiKey}`,
631
+ }))
632
+ .providesPrivateService("internalApi", ({ deps }) => ({
633
+ makeRequest: () => `${deps.endpoint}/request`,
634
+ }))
635
+ .providesBaseService(({ privateServices }) => ({
636
+ doRequest: () => privateServices.internalApi.makeRequest(),
637
+ }))
638
+ .build();
639
+
640
+ const instance = instantiate(definition)
641
+ .withConfig({ apiKey: "test-key" })
642
+ .withOptions({})
643
+ .build();
644
+
645
+ expect(instance.services.doRequest()).toBe("https://api.example.com/test-key/request");
646
+ });
647
+
648
+ test("private services can access serviceDeps", () => {
649
+ interface IEmailService {
650
+ sendEmail(to: string): Promise<void>;
651
+ }
652
+
653
+ const emailImpl: IEmailService = {
654
+ sendEmail: async () => {},
655
+ };
656
+
657
+ const definition = defineFragment("test")
658
+ .usesService<"email", IEmailService>("email")
659
+ .providesPrivateService("emailHelper", ({ serviceDeps }) => ({
660
+ sendWelcomeEmail: (to: string) => serviceDeps.email.sendEmail(to),
661
+ }))
662
+ .providesBaseService(({ privateServices }) => ({
663
+ welcomeUser: (email: string) => privateServices.emailHelper.sendWelcomeEmail(email),
664
+ }))
665
+ .build();
666
+
667
+ const instance = instantiate(definition)
668
+ .withServices({ email: emailImpl })
669
+ .withOptions({})
670
+ .build();
671
+
672
+ expect(instance.services.welcomeUser).toBeDefined();
673
+ expectTypeOf<typeof instance.services.welcomeUser>().toExtend<
674
+ (email: string) => Promise<void>
675
+ >();
676
+ });
677
+
678
+ test("named services can also access private services", () => {
679
+ const definition = defineFragment("test")
680
+ .providesPrivateService("helper", () => ({
681
+ multiply: (a: number, b: number) => a * b,
682
+ }))
683
+ .providesService("calculator", ({ privateServices }) => ({
684
+ square: (n: number) => privateServices.helper.multiply(n, n),
685
+ }))
686
+ .build();
687
+
688
+ const instance = instantiate(definition).withOptions({}).build();
689
+
690
+ expect(instance.services.calculator.square(5)).toBe(25);
691
+ // @ts-expect-error - Private service should not be accessible
692
+ expect(instance.services.helper).toBeUndefined();
693
+ });
694
+
695
+ test("private services can access other private services (in order)", () => {
696
+ const definition = defineFragment("test")
697
+ .providesPrivateService("math", () => ({
698
+ add: (a: number, b: number) => a + b,
699
+ multiply: (a: number, b: number) => a * b,
700
+ }))
701
+ .providesPrivateService("calculator", ({ privateServices }) => ({
702
+ // This private service can access the earlier private service
703
+ square: (n: number) => privateServices.math.multiply(n, n),
704
+ addTen: (n: number) => privateServices.math.add(n, 10),
705
+ }))
706
+ .providesBaseService(({ privateServices }) => ({
707
+ compute: (n: number) => {
708
+ // Public service can access both private services
709
+ const squared = privateServices.calculator.square(n);
710
+ return privateServices.calculator.addTen(squared);
711
+ },
712
+ }))
713
+ .build();
714
+
715
+ const instance = instantiate(definition).withOptions({}).build();
716
+
717
+ // 5^2 = 25, 25 + 10 = 35
718
+ expect(instance.services.compute(5)).toBe(35);
719
+
720
+ // Private services should not be accessible on the instance
721
+ // @ts-expect-error - Private service should not be accessible
722
+ expect(instance.services.math).toBeUndefined();
723
+ // @ts-expect-error - Private service should not be accessible
724
+ expect(instance.services.calculator).toBeUndefined();
460
725
  });
461
726
  });
462
727
  });