@fragno-dev/core 0.1.7 → 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.
- package/.turbo/turbo-build.log +131 -64
- package/CHANGELOG.md +19 -0
- package/dist/api/api.d.ts +38 -2
- package/dist/api/api.d.ts.map +1 -0
- package/dist/api/api.js +9 -2
- package/dist/api/api.js.map +1 -0
- package/dist/api/bind-services.d.ts +6 -0
- package/dist/api/bind-services.d.ts.map +1 -0
- package/dist/api/bind-services.js +20 -0
- package/dist/api/bind-services.js.map +1 -0
- package/dist/api/error.d.ts +26 -0
- package/dist/api/error.d.ts.map +1 -0
- package/dist/{api-DngJDcmO.js → api/error.js} +2 -8
- package/dist/api/error.js.map +1 -0
- package/dist/api/fragment-definition-builder.d.ts +313 -0
- package/dist/api/fragment-definition-builder.d.ts.map +1 -0
- package/dist/api/fragment-definition-builder.js +326 -0
- package/dist/api/fragment-definition-builder.js.map +1 -0
- package/dist/api/fragment-instantiator.d.ts +216 -0
- package/dist/api/fragment-instantiator.d.ts.map +1 -0
- package/dist/api/fragment-instantiator.js +487 -0
- package/dist/api/fragment-instantiator.js.map +1 -0
- package/dist/api/fragno-response.d.ts +30 -0
- package/dist/api/fragno-response.d.ts.map +1 -0
- package/dist/api/fragno-response.js +73 -0
- package/dist/api/fragno-response.js.map +1 -0
- package/dist/api/internal/path.d.ts +50 -0
- package/dist/api/internal/path.d.ts.map +1 -0
- package/dist/api/internal/path.js +76 -0
- package/dist/api/internal/path.js.map +1 -0
- package/dist/api/internal/response-stream.d.ts +43 -0
- package/dist/api/internal/response-stream.d.ts.map +1 -0
- package/dist/api/internal/response-stream.js +81 -0
- package/dist/api/internal/response-stream.js.map +1 -0
- package/dist/api/internal/route.js +10 -0
- package/dist/api/internal/route.js.map +1 -0
- package/dist/api/mutable-request-state.d.ts +82 -0
- package/dist/api/mutable-request-state.d.ts.map +1 -0
- package/dist/api/mutable-request-state.js +97 -0
- package/dist/api/mutable-request-state.js.map +1 -0
- package/dist/api/request-context-storage.d.ts +42 -0
- package/dist/api/request-context-storage.d.ts.map +1 -0
- package/dist/api/request-context-storage.js +43 -0
- package/dist/api/request-context-storage.js.map +1 -0
- package/dist/api/request-input-context.d.ts +89 -0
- package/dist/api/request-input-context.d.ts.map +1 -0
- package/dist/api/request-input-context.js +118 -0
- package/dist/api/request-input-context.js.map +1 -0
- package/dist/api/request-middleware.d.ts +50 -0
- package/dist/api/request-middleware.d.ts.map +1 -0
- package/dist/api/request-middleware.js +83 -0
- package/dist/api/request-middleware.js.map +1 -0
- package/dist/api/request-output-context.d.ts +41 -0
- package/dist/api/request-output-context.d.ts.map +1 -0
- package/dist/api/request-output-context.js +119 -0
- package/dist/api/request-output-context.js.map +1 -0
- package/dist/api/route-handler-input-options.d.ts +21 -0
- package/dist/api/route-handler-input-options.d.ts.map +1 -0
- package/dist/api/route.d.ts +54 -3
- package/dist/api/route.d.ts.map +1 -0
- package/dist/api/route.js +29 -2
- package/dist/api/route.js.map +1 -0
- package/dist/api/shared-types.d.ts +47 -0
- package/dist/api/shared-types.d.ts.map +1 -0
- package/dist/api/shared-types.js +1 -0
- package/dist/client/client-error.d.ts +60 -0
- package/dist/client/client-error.d.ts.map +1 -0
- package/dist/client/client-error.js +92 -0
- package/dist/client/client-error.js.map +1 -0
- package/dist/client/client.d.ts +210 -4
- package/dist/client/client.d.ts.map +1 -0
- package/dist/client/client.js +397 -6
- package/dist/client/client.js.map +1 -0
- package/dist/client/client.svelte.d.ts +5 -3
- package/dist/client/client.svelte.d.ts.map +1 -1
- package/dist/client/client.svelte.js +1 -5
- package/dist/client/client.svelte.js.map +1 -1
- package/dist/client/internal/fetcher-merge.js +36 -0
- package/dist/client/internal/fetcher-merge.js.map +1 -0
- package/dist/client/internal/ndjson-streaming.js +139 -0
- package/dist/client/internal/ndjson-streaming.js.map +1 -0
- package/dist/client/react.d.ts +5 -3
- package/dist/client/react.d.ts.map +1 -1
- package/dist/client/react.js +3 -5
- package/dist/client/react.js.map +1 -1
- package/dist/client/solid.d.ts +5 -3
- package/dist/client/solid.d.ts.map +1 -1
- package/dist/client/solid.js +2 -5
- package/dist/client/solid.js.map +1 -1
- package/dist/client/vanilla.d.ts +5 -3
- package/dist/client/vanilla.d.ts.map +1 -1
- package/dist/client/vanilla.js +2 -43
- package/dist/client/vanilla.js.map +1 -1
- package/dist/client/vue.d.ts +5 -3
- package/dist/client/vue.d.ts.map +1 -1
- package/dist/client/vue.js +1 -5
- package/dist/client/vue.js.map +1 -1
- package/dist/http/http-status.d.ts +26 -0
- package/dist/http/http-status.d.ts.map +1 -0
- package/dist/integrations/react-ssr.js +1 -1
- package/dist/internal/symbols.d.ts +9 -0
- package/dist/internal/symbols.d.ts.map +1 -0
- package/dist/internal/symbols.js +10 -0
- package/dist/internal/symbols.js.map +1 -0
- package/dist/mod-client.d.ts +36 -0
- package/dist/mod-client.d.ts.map +1 -0
- package/dist/mod-client.js +21 -0
- package/dist/mod-client.js.map +1 -0
- package/dist/mod.d.ts +7 -4
- package/dist/mod.js +4 -6
- package/dist/request/request.d.ts +4 -0
- package/dist/request/request.js +5 -0
- package/dist/test/test.d.ts +62 -35
- package/dist/test/test.d.ts.map +1 -1
- package/dist/test/test.js +75 -40
- package/dist/test/test.js.map +1 -1
- package/dist/util/async.js +40 -0
- package/dist/util/async.js.map +1 -0
- package/dist/util/content-type.js +49 -0
- package/dist/util/content-type.js.map +1 -0
- package/dist/util/nanostores.js +31 -0
- package/dist/util/nanostores.js.map +1 -0
- package/dist/{ssr-BByDVfFD.js → util/ssr.js} +2 -2
- package/dist/util/ssr.js.map +1 -0
- package/dist/util/types-util.d.ts +8 -0
- package/dist/util/types-util.d.ts.map +1 -0
- package/package.json +19 -12
- package/src/api/api.ts +41 -6
- package/src/api/bind-services.ts +42 -0
- package/src/api/fragment-definition-builder.extend.test.ts +810 -0
- package/src/api/fragment-definition-builder.test.ts +499 -0
- package/src/api/fragment-definition-builder.ts +1088 -0
- package/src/api/fragment-instantiator.test.ts +1488 -0
- package/src/api/fragment-instantiator.ts +1053 -0
- package/src/api/fragment-services.test.ts +727 -0
- package/src/api/request-context-storage.ts +64 -0
- package/src/api/request-middleware.test.ts +301 -225
- package/src/api/route.test.ts +87 -1
- package/src/api/route.ts +345 -24
- package/src/api/shared-types.ts +43 -0
- package/src/client/client-builder.test.ts +23 -23
- package/src/client/client.ssr.test.ts +3 -3
- package/src/client/client.svelte.test.ts +15 -15
- package/src/client/client.test.ts +22 -22
- package/src/client/client.ts +72 -12
- package/src/client/internal/fetcher-merge.ts +1 -1
- package/src/client/react.test.ts +2 -2
- package/src/client/solid.test.ts +2 -2
- package/src/client/vanilla.test.ts +2 -2
- package/src/client/vue.test.ts +2 -2
- package/src/internal/symbols.ts +5 -0
- package/src/mod-client.ts +59 -0
- package/src/mod.ts +26 -9
- package/src/request/request.ts +8 -0
- package/src/test/test.test.ts +200 -381
- package/src/test/test.ts +190 -117
- package/tsdown.config.ts +8 -5
- package/dist/api/fragment-builder.d.ts +0 -4
- package/dist/api/fragment-builder.js +0 -3
- package/dist/api/fragment-instantiation.d.ts +0 -4
- package/dist/api/fragment-instantiation.js +0 -6
- package/dist/api-BWN97TOr.d.ts +0 -377
- package/dist/api-BWN97TOr.d.ts.map +0 -1
- package/dist/api-DngJDcmO.js.map +0 -1
- package/dist/client-C5LsYHEI.js +0 -782
- package/dist/client-C5LsYHEI.js.map +0 -1
- package/dist/fragment-builder-DOnCVBqc.js +0 -47
- package/dist/fragment-builder-DOnCVBqc.js.map +0 -1
- package/dist/fragment-builder-MGr68GNb.d.ts +0 -409
- package/dist/fragment-builder-MGr68GNb.d.ts.map +0 -1
- package/dist/fragment-instantiation-C4wvwl6V.js +0 -446
- package/dist/fragment-instantiation-C4wvwl6V.js.map +0 -1
- package/dist/request-output-context-CdIjwmEN.js +0 -320
- package/dist/request-output-context-CdIjwmEN.js.map +0 -1
- package/dist/route-Bl9Zr1Yv.d.ts +0 -26
- package/dist/route-Bl9Zr1Yv.d.ts.map +0 -1
- package/dist/route-C5Uryylh.js +0 -21
- package/dist/route-C5Uryylh.js.map +0 -1
- package/dist/ssr-BByDVfFD.js.map +0 -1
- package/src/api/fragment-builder.ts +0 -80
- package/src/api/fragment-instantiation.test.ts +0 -460
- package/src/api/fragment-instantiation.ts +0 -499
- package/src/api/fragment.test.ts +0 -537
|
@@ -0,0 +1,499 @@
|
|
|
1
|
+
import { describe, it, expect, expectTypeOf, vi } from "vitest";
|
|
2
|
+
import { defineFragment, type FragmentDefinition } from "./fragment-definition-builder";
|
|
3
|
+
import type { FragnoPublicConfig } from "./shared-types";
|
|
4
|
+
import type { RequestThisContext } from "./api";
|
|
5
|
+
|
|
6
|
+
describe("FragmentDefinitionBuilder", () => {
|
|
7
|
+
describe("defineFragment", () => {
|
|
8
|
+
it("should create a basic fragment builder", () => {
|
|
9
|
+
const builder = defineFragment("test-fragment");
|
|
10
|
+
expect(builder.name).toBe("test-fragment");
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it("should build a minimal definition", () => {
|
|
14
|
+
const definition = defineFragment("test-fragment").build();
|
|
15
|
+
|
|
16
|
+
expect(definition.name).toBe("test-fragment");
|
|
17
|
+
expect(definition.dependencies).toBeUndefined();
|
|
18
|
+
expect(definition.baseServices).toBeUndefined();
|
|
19
|
+
expect(definition.namedServices).toBeUndefined();
|
|
20
|
+
expect(definition.serviceDependencies).toBeUndefined();
|
|
21
|
+
});
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
describe("withDependencies", () => {
|
|
25
|
+
it("should define dependencies", () => {
|
|
26
|
+
interface Config {
|
|
27
|
+
apiKey: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const definition = defineFragment<Config>("test-fragment")
|
|
31
|
+
.withDependencies(({ config, options }) => ({
|
|
32
|
+
apiKey: config.apiKey,
|
|
33
|
+
mountRoute: options.mountRoute,
|
|
34
|
+
}))
|
|
35
|
+
.build();
|
|
36
|
+
|
|
37
|
+
expect(definition.dependencies).toBeDefined();
|
|
38
|
+
|
|
39
|
+
// Test the dependencies function
|
|
40
|
+
const deps = definition.dependencies!({
|
|
41
|
+
config: { apiKey: "test-key" },
|
|
42
|
+
options: { mountRoute: "/api" },
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
expect(deps.apiKey).toBe("test-key");
|
|
46
|
+
expect(deps.mountRoute).toBe("/api");
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("should reset services when setting dependencies", () => {
|
|
50
|
+
const definition = defineFragment("test-fragment")
|
|
51
|
+
.providesBaseService(() => ({
|
|
52
|
+
method1: () => "test",
|
|
53
|
+
}))
|
|
54
|
+
.withDependencies(() => ({
|
|
55
|
+
dep1: "value",
|
|
56
|
+
}))
|
|
57
|
+
.build();
|
|
58
|
+
|
|
59
|
+
// Base services should be reset
|
|
60
|
+
expect(definition.baseServices).toBeUndefined();
|
|
61
|
+
expect(definition.dependencies).toBeDefined();
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("should reset request storage and context when called late in chain", () => {
|
|
65
|
+
// This demonstrates that calling withDependencies late erases earlier storage/context setup
|
|
66
|
+
const definition = defineFragment("test-fragment")
|
|
67
|
+
.withRequestStorage(() => ({
|
|
68
|
+
counter: 0,
|
|
69
|
+
userId: "user-123",
|
|
70
|
+
}))
|
|
71
|
+
.withThisContext(({ storage }) => {
|
|
72
|
+
const ctx = {
|
|
73
|
+
get counter() {
|
|
74
|
+
return storage.getStore()?.counter ?? 0;
|
|
75
|
+
},
|
|
76
|
+
get userId() {
|
|
77
|
+
return storage.getStore()?.userId;
|
|
78
|
+
},
|
|
79
|
+
};
|
|
80
|
+
return { serviceContext: ctx, handlerContext: ctx };
|
|
81
|
+
})
|
|
82
|
+
// Calling withDependencies here will erase the storage and context configuration!
|
|
83
|
+
.withDependencies(() => ({
|
|
84
|
+
apiKey: "secret",
|
|
85
|
+
}))
|
|
86
|
+
.build();
|
|
87
|
+
|
|
88
|
+
// Storage and context should be reset (undefined)
|
|
89
|
+
expect(definition.createRequestStorage).toBeUndefined();
|
|
90
|
+
expect(definition.createThisContext).toBeUndefined();
|
|
91
|
+
expect(definition.getExternalStorage).toBeUndefined();
|
|
92
|
+
expect(definition.dependencies).toBeDefined();
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it("should preserve storage and context when dependencies set early", () => {
|
|
96
|
+
// This is the recommended pattern: set dependencies first
|
|
97
|
+
const definition = defineFragment("test-fragment")
|
|
98
|
+
.withDependencies(() => ({
|
|
99
|
+
apiKey: "secret",
|
|
100
|
+
}))
|
|
101
|
+
.withRequestStorage(({ deps }) => ({
|
|
102
|
+
counter: 0,
|
|
103
|
+
apiKey: deps.apiKey,
|
|
104
|
+
}))
|
|
105
|
+
.withThisContext(({ storage }) => {
|
|
106
|
+
const ctx = {
|
|
107
|
+
get counter() {
|
|
108
|
+
return storage.getStore()?.counter ?? 0;
|
|
109
|
+
},
|
|
110
|
+
};
|
|
111
|
+
return { serviceContext: ctx, handlerContext: ctx };
|
|
112
|
+
})
|
|
113
|
+
.build();
|
|
114
|
+
|
|
115
|
+
// Everything should be preserved
|
|
116
|
+
expect(definition.createRequestStorage).toBeDefined();
|
|
117
|
+
expect(definition.createThisContext).toBeDefined();
|
|
118
|
+
expect(definition.dependencies).toBeDefined();
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it("should warn when withDependencies is called after storage/services are configured", () => {
|
|
122
|
+
const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
123
|
+
|
|
124
|
+
defineFragment("test-fragment")
|
|
125
|
+
.withRequestStorage(() => ({ counter: 0 }))
|
|
126
|
+
.providesService("myService", () => ({ test: () => "hi" }))
|
|
127
|
+
.withDependencies(() => ({ apiKey: "secret" }));
|
|
128
|
+
|
|
129
|
+
expect(warnSpy).toHaveBeenCalledWith(
|
|
130
|
+
expect.stringContaining(
|
|
131
|
+
'[Fragno] Warning: withDependencies() on fragment "test-fragment" is resetting',
|
|
132
|
+
),
|
|
133
|
+
);
|
|
134
|
+
|
|
135
|
+
warnSpy.mockRestore();
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it("should not warn when withDependencies is called early", () => {
|
|
139
|
+
const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
140
|
+
|
|
141
|
+
defineFragment("test-fragment")
|
|
142
|
+
.withDependencies(() => ({ apiKey: "secret" }))
|
|
143
|
+
.withRequestStorage(() => ({ counter: 0 }))
|
|
144
|
+
.providesService("myService", () => ({ test: () => "hi" }));
|
|
145
|
+
|
|
146
|
+
expect(warnSpy).not.toHaveBeenCalled();
|
|
147
|
+
|
|
148
|
+
warnSpy.mockRestore();
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
describe("providesBaseService", () => {
|
|
153
|
+
it("should define unnamed services", () => {
|
|
154
|
+
const definition = defineFragment("test-fragment")
|
|
155
|
+
.withDependencies(() => ({ apiKey: "key" }))
|
|
156
|
+
.providesBaseService(({ deps }) => ({
|
|
157
|
+
method1: () => `${deps.apiKey}-method1`,
|
|
158
|
+
method2: () => "method2",
|
|
159
|
+
}))
|
|
160
|
+
.build();
|
|
161
|
+
|
|
162
|
+
expect(definition.baseServices).toBeDefined();
|
|
163
|
+
|
|
164
|
+
// Test the services function
|
|
165
|
+
const services = definition.baseServices!({
|
|
166
|
+
config: {},
|
|
167
|
+
options: {},
|
|
168
|
+
deps: { apiKey: "test-key" },
|
|
169
|
+
serviceDeps: {},
|
|
170
|
+
privateServices: {},
|
|
171
|
+
defineService: (svc) => svc,
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
expect(services.method1()).toBe("test-key-method1");
|
|
175
|
+
expect(services.method2()).toBe("method2");
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
it("should define unnamed services with defineService", () => {
|
|
179
|
+
interface Bla extends RequestThisContext {
|
|
180
|
+
myThisNumber: number;
|
|
181
|
+
myThisString: string;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const definition = defineFragment<{}, FragnoPublicConfig, Bla>("test-fragment")
|
|
185
|
+
.withDependencies(() => ({ apiKey: "key" }))
|
|
186
|
+
.providesBaseService(({ deps, defineService }) =>
|
|
187
|
+
defineService({
|
|
188
|
+
method1: function () {
|
|
189
|
+
this.myThisNumber++;
|
|
190
|
+
expectTypeOf(this).toMatchObjectType<{
|
|
191
|
+
myThisNumber: number;
|
|
192
|
+
myThisString: string;
|
|
193
|
+
}>();
|
|
194
|
+
return `${deps.apiKey}-method1`;
|
|
195
|
+
},
|
|
196
|
+
}),
|
|
197
|
+
)
|
|
198
|
+
.build();
|
|
199
|
+
|
|
200
|
+
expect(definition.baseServices).toBeDefined();
|
|
201
|
+
});
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
describe("providesService", () => {
|
|
205
|
+
it("should define named services", () => {
|
|
206
|
+
const definition = defineFragment("test-fragment")
|
|
207
|
+
.withDependencies(() => ({ apiKey: "key" }))
|
|
208
|
+
.providesService("email", ({ deps }) => ({
|
|
209
|
+
send: (to: string) => `Sending to ${to} with ${deps.apiKey}`,
|
|
210
|
+
}))
|
|
211
|
+
.build();
|
|
212
|
+
|
|
213
|
+
expect(definition.namedServices).toBeDefined();
|
|
214
|
+
expect(definition.namedServices!.email).toBeDefined();
|
|
215
|
+
|
|
216
|
+
// Test the service function
|
|
217
|
+
const emailService = definition.namedServices!.email({
|
|
218
|
+
config: {},
|
|
219
|
+
options: {},
|
|
220
|
+
deps: { apiKey: "test-key" },
|
|
221
|
+
serviceDeps: {},
|
|
222
|
+
privateServices: {},
|
|
223
|
+
defineService: (svc) => svc,
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
expect(emailService.send("user@example.com")).toBe(
|
|
227
|
+
"Sending to user@example.com with test-key",
|
|
228
|
+
);
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
it("should support multiple named services", () => {
|
|
232
|
+
const definition = defineFragment("test-fragment")
|
|
233
|
+
.providesService("email", () => ({
|
|
234
|
+
send: () => "email sent",
|
|
235
|
+
}))
|
|
236
|
+
.providesService("sms", () => ({
|
|
237
|
+
send: () => "sms sent",
|
|
238
|
+
}))
|
|
239
|
+
.build();
|
|
240
|
+
|
|
241
|
+
expect(definition.namedServices!.email).toBeDefined();
|
|
242
|
+
expect(definition.namedServices!.sms).toBeDefined();
|
|
243
|
+
|
|
244
|
+
const emailService = definition.namedServices!.email({
|
|
245
|
+
config: {},
|
|
246
|
+
options: {},
|
|
247
|
+
deps: {},
|
|
248
|
+
serviceDeps: {},
|
|
249
|
+
privateServices: {},
|
|
250
|
+
defineService: (svc) => svc,
|
|
251
|
+
});
|
|
252
|
+
const smsService = definition.namedServices!.sms({
|
|
253
|
+
config: {},
|
|
254
|
+
options: {},
|
|
255
|
+
deps: {},
|
|
256
|
+
serviceDeps: {},
|
|
257
|
+
privateServices: {},
|
|
258
|
+
defineService: (svc) => svc,
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
expect(emailService.send()).toBe("email sent");
|
|
262
|
+
expect(smsService.send()).toBe("sms sent");
|
|
263
|
+
});
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
describe("usesService", () => {
|
|
267
|
+
it("should declare required service dependency", () => {
|
|
268
|
+
interface EmailService {
|
|
269
|
+
send: (to: string) => void;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
const definition = defineFragment("test-fragment")
|
|
273
|
+
.usesService<"email", EmailService>("email")
|
|
274
|
+
.build();
|
|
275
|
+
|
|
276
|
+
expect(definition.serviceDependencies).toBeDefined();
|
|
277
|
+
expect(definition.serviceDependencies!.email).toEqual({
|
|
278
|
+
name: "email",
|
|
279
|
+
required: true,
|
|
280
|
+
});
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
it("should declare optional service dependency", () => {
|
|
284
|
+
interface LogService {
|
|
285
|
+
log: (msg: string) => void;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
const definition = defineFragment("test-fragment")
|
|
289
|
+
.usesOptionalService<"logger", LogService>("logger")
|
|
290
|
+
.build();
|
|
291
|
+
|
|
292
|
+
expect(definition.serviceDependencies!.logger).toEqual({
|
|
293
|
+
name: "logger",
|
|
294
|
+
required: false,
|
|
295
|
+
});
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
it("should support multiple service dependencies", () => {
|
|
299
|
+
interface EmailService {
|
|
300
|
+
send: (to: string) => void;
|
|
301
|
+
}
|
|
302
|
+
interface LogService {
|
|
303
|
+
log: (msg: string) => void;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
const definition = defineFragment("test-fragment")
|
|
307
|
+
.usesService<"email", EmailService>("email")
|
|
308
|
+
.usesOptionalService<"logger", LogService>("logger")
|
|
309
|
+
.build();
|
|
310
|
+
|
|
311
|
+
expect(definition.serviceDependencies!.email.required).toBe(true);
|
|
312
|
+
expect(definition.serviceDependencies!.logger.required).toBe(false);
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
it("should allow services to use service dependencies", () => {
|
|
316
|
+
interface EmailService {
|
|
317
|
+
send: (to: string) => string;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
const definition = defineFragment("test-fragment")
|
|
321
|
+
.withDependencies(() => ({ apiKey: "key" }))
|
|
322
|
+
.usesService<"email", EmailService>("email")
|
|
323
|
+
.providesBaseService(({ deps, serviceDeps }) => ({
|
|
324
|
+
sendWelcome: () => {
|
|
325
|
+
// serviceDeps should have email
|
|
326
|
+
return serviceDeps.email.send("welcome@example.com");
|
|
327
|
+
},
|
|
328
|
+
getKey: () => deps.apiKey,
|
|
329
|
+
}))
|
|
330
|
+
.build();
|
|
331
|
+
|
|
332
|
+
// Test with mock email service
|
|
333
|
+
const services = definition.baseServices!({
|
|
334
|
+
config: {},
|
|
335
|
+
options: {},
|
|
336
|
+
deps: { apiKey: "test-key" },
|
|
337
|
+
serviceDeps: {
|
|
338
|
+
email: {
|
|
339
|
+
send: (to: string) => `Email sent to ${to}`,
|
|
340
|
+
},
|
|
341
|
+
},
|
|
342
|
+
privateServices: {},
|
|
343
|
+
defineService: (svc) => svc,
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
expect(services.sendWelcome()).toBe("Email sent to welcome@example.com");
|
|
347
|
+
expect(services.getKey()).toBe("test-key");
|
|
348
|
+
});
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
describe("complex scenarios", () => {
|
|
352
|
+
it("should support full fragment definition", () => {
|
|
353
|
+
interface Config {
|
|
354
|
+
apiKey: string;
|
|
355
|
+
debug: boolean;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
interface LogService {
|
|
359
|
+
log: (msg: string) => void;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
const definition = defineFragment<Config>("complex-fragment")
|
|
363
|
+
.withDependencies(({ config }) => ({
|
|
364
|
+
apiKey: config.apiKey,
|
|
365
|
+
debug: config.debug,
|
|
366
|
+
}))
|
|
367
|
+
.usesOptionalService<"logger", LogService>("logger")
|
|
368
|
+
.providesBaseService(({ deps, serviceDeps }) => ({
|
|
369
|
+
getData: () => {
|
|
370
|
+
if (serviceDeps.logger) {
|
|
371
|
+
serviceDeps.logger.log("Getting data");
|
|
372
|
+
}
|
|
373
|
+
return `data-${deps.apiKey}`;
|
|
374
|
+
},
|
|
375
|
+
}))
|
|
376
|
+
.providesService("analytics", ({ deps }) => ({
|
|
377
|
+
track: (event: string) => `Tracking ${event} with ${deps.apiKey}`,
|
|
378
|
+
}))
|
|
379
|
+
.build();
|
|
380
|
+
|
|
381
|
+
expect(definition.name).toBe("complex-fragment");
|
|
382
|
+
expect(definition.dependencies).toBeDefined();
|
|
383
|
+
expect(definition.baseServices).toBeDefined();
|
|
384
|
+
expect(definition.namedServices).toBeDefined();
|
|
385
|
+
expect(definition.serviceDependencies).toBeDefined();
|
|
386
|
+
|
|
387
|
+
// Test execution
|
|
388
|
+
const logs: string[] = [];
|
|
389
|
+
const deps = definition.dependencies!({
|
|
390
|
+
config: { apiKey: "my-key", debug: true },
|
|
391
|
+
options: {},
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
const services = definition.baseServices!({
|
|
395
|
+
config: { apiKey: "my-key", debug: true },
|
|
396
|
+
options: {},
|
|
397
|
+
deps,
|
|
398
|
+
serviceDeps: {
|
|
399
|
+
logger: {
|
|
400
|
+
log: (msg) => logs.push(msg),
|
|
401
|
+
},
|
|
402
|
+
},
|
|
403
|
+
privateServices: {},
|
|
404
|
+
defineService: (svc) => svc,
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
const analyticsService = definition.namedServices!.analytics({
|
|
408
|
+
config: { apiKey: "my-key", debug: true },
|
|
409
|
+
options: {},
|
|
410
|
+
deps,
|
|
411
|
+
serviceDeps: {
|
|
412
|
+
logger: {
|
|
413
|
+
log: (msg) => logs.push(msg),
|
|
414
|
+
},
|
|
415
|
+
},
|
|
416
|
+
privateServices: {},
|
|
417
|
+
defineService: (svc) => svc,
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
expect(services.getData()).toBe("data-my-key");
|
|
421
|
+
expect(logs).toContain("Getting data");
|
|
422
|
+
expect(analyticsService.track("click")).toBe("Tracking click with my-key");
|
|
423
|
+
});
|
|
424
|
+
});
|
|
425
|
+
|
|
426
|
+
describe("type safety", () => {
|
|
427
|
+
it("should infer correct types", () => {
|
|
428
|
+
interface Config {
|
|
429
|
+
port: number;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
const builder = defineFragment<Config>("typed-fragment")
|
|
433
|
+
.withDependencies(({ config }) => ({
|
|
434
|
+
port: config.port,
|
|
435
|
+
}))
|
|
436
|
+
.providesService("server", ({ deps }) => ({
|
|
437
|
+
start: () => `Server starting on port ${deps.port}`,
|
|
438
|
+
}));
|
|
439
|
+
|
|
440
|
+
const definition = builder.build();
|
|
441
|
+
|
|
442
|
+
// Type check: definition should have correct structure
|
|
443
|
+
type DefType = typeof definition;
|
|
444
|
+
const _typeCheck: DefType extends FragmentDefinition<
|
|
445
|
+
Config,
|
|
446
|
+
FragnoPublicConfig,
|
|
447
|
+
{ port: number },
|
|
448
|
+
{},
|
|
449
|
+
{ server: { start: () => string } },
|
|
450
|
+
{},
|
|
451
|
+
{},
|
|
452
|
+
RequestThisContext,
|
|
453
|
+
RequestThisContext,
|
|
454
|
+
{}
|
|
455
|
+
>
|
|
456
|
+
? true
|
|
457
|
+
: false = true;
|
|
458
|
+
|
|
459
|
+
expect(_typeCheck).toBe(true);
|
|
460
|
+
});
|
|
461
|
+
});
|
|
462
|
+
|
|
463
|
+
describe("extend", () => {
|
|
464
|
+
it("should allow extending builder with transformation function", () => {
|
|
465
|
+
const builder = defineFragment("test");
|
|
466
|
+
|
|
467
|
+
// Simple transformation that wraps the builder
|
|
468
|
+
const extended = builder.extend((b) => ({
|
|
469
|
+
builder: b,
|
|
470
|
+
additionalMethod: () => "extended",
|
|
471
|
+
}));
|
|
472
|
+
|
|
473
|
+
expect(extended.builder).toBe(builder);
|
|
474
|
+
expect(extended.additionalMethod()).toBe("extended");
|
|
475
|
+
});
|
|
476
|
+
|
|
477
|
+
it("should pass correct type through transformer", () => {
|
|
478
|
+
interface Config {
|
|
479
|
+
apiKey: string;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
const builder = defineFragment<Config>("test").withDependencies(({ config }) => ({
|
|
483
|
+
key: config.apiKey,
|
|
484
|
+
}));
|
|
485
|
+
|
|
486
|
+
// Transformer that returns a new builder type
|
|
487
|
+
const extended = builder.extend((b) => {
|
|
488
|
+
const def = b.build();
|
|
489
|
+
return {
|
|
490
|
+
definition: def,
|
|
491
|
+
name: def.name,
|
|
492
|
+
};
|
|
493
|
+
});
|
|
494
|
+
|
|
495
|
+
expect(extended.name).toBe("test");
|
|
496
|
+
expect(extended.definition.name).toBe("test");
|
|
497
|
+
});
|
|
498
|
+
});
|
|
499
|
+
});
|