@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,810 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { defineFragment, FragmentDefinitionBuilder } from "./fragment-definition-builder";
|
|
3
|
+
import type { FragnoPublicConfig } from "./shared-types";
|
|
4
|
+
import type { RequestThisContext } from "./api";
|
|
5
|
+
|
|
6
|
+
describe("FragmentDefinitionBuilder.extend()", () => {
|
|
7
|
+
describe("basic functionality", () => {
|
|
8
|
+
it("should allow extending with a simple transformer", () => {
|
|
9
|
+
const addTimestamp = () => {
|
|
10
|
+
return <
|
|
11
|
+
TConfig,
|
|
12
|
+
TOptions extends FragnoPublicConfig,
|
|
13
|
+
TDeps,
|
|
14
|
+
TBaseServices,
|
|
15
|
+
TServices,
|
|
16
|
+
TServiceDeps,
|
|
17
|
+
TPrivateServices,
|
|
18
|
+
TServiceThisContext extends RequestThisContext,
|
|
19
|
+
THandlerThisContext extends RequestThisContext,
|
|
20
|
+
TRequestStorage,
|
|
21
|
+
>(
|
|
22
|
+
builder: FragmentDefinitionBuilder<
|
|
23
|
+
TConfig,
|
|
24
|
+
TOptions,
|
|
25
|
+
TDeps,
|
|
26
|
+
TBaseServices,
|
|
27
|
+
TServices,
|
|
28
|
+
TServiceDeps,
|
|
29
|
+
TPrivateServices,
|
|
30
|
+
TServiceThisContext,
|
|
31
|
+
THandlerThisContext,
|
|
32
|
+
TRequestStorage
|
|
33
|
+
>,
|
|
34
|
+
): FragmentDefinitionBuilder<
|
|
35
|
+
TConfig,
|
|
36
|
+
TOptions,
|
|
37
|
+
TDeps,
|
|
38
|
+
TBaseServices & { timestamp: () => number },
|
|
39
|
+
TServices,
|
|
40
|
+
TServiceDeps,
|
|
41
|
+
TPrivateServices,
|
|
42
|
+
TServiceThisContext,
|
|
43
|
+
THandlerThisContext,
|
|
44
|
+
TRequestStorage
|
|
45
|
+
> => {
|
|
46
|
+
const currentDef = builder.build();
|
|
47
|
+
const currentBaseServices = currentDef.baseServices;
|
|
48
|
+
|
|
49
|
+
return new FragmentDefinitionBuilder(builder.name, {
|
|
50
|
+
...currentDef,
|
|
51
|
+
baseServices: (context) => {
|
|
52
|
+
const existing = currentBaseServices
|
|
53
|
+
? currentBaseServices(context)
|
|
54
|
+
: ({} as TBaseServices);
|
|
55
|
+
return {
|
|
56
|
+
...existing,
|
|
57
|
+
timestamp: () => Date.now(),
|
|
58
|
+
} as TBaseServices & { timestamp: () => number };
|
|
59
|
+
},
|
|
60
|
+
});
|
|
61
|
+
};
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
const definition = defineFragment<{ enabled: boolean }>("test")
|
|
65
|
+
.withDependencies(({ config }) => ({ enabled: config.enabled }))
|
|
66
|
+
.extend(addTimestamp())
|
|
67
|
+
.build();
|
|
68
|
+
|
|
69
|
+
expect(definition.name).toBe("test");
|
|
70
|
+
expect(definition.dependencies).toBeDefined();
|
|
71
|
+
expect(definition.baseServices).toBeDefined();
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it("should allow chaining multiple extend() calls", () => {
|
|
75
|
+
const addCounter = () => {
|
|
76
|
+
return <
|
|
77
|
+
TConfig,
|
|
78
|
+
TOptions extends FragnoPublicConfig,
|
|
79
|
+
TDeps,
|
|
80
|
+
TBaseServices,
|
|
81
|
+
TServices,
|
|
82
|
+
TServiceDeps,
|
|
83
|
+
TPrivateServices,
|
|
84
|
+
TServiceThisContext extends RequestThisContext,
|
|
85
|
+
THandlerThisContext extends RequestThisContext,
|
|
86
|
+
TRequestStorage,
|
|
87
|
+
>(
|
|
88
|
+
builder: FragmentDefinitionBuilder<
|
|
89
|
+
TConfig,
|
|
90
|
+
TOptions,
|
|
91
|
+
TDeps,
|
|
92
|
+
TBaseServices,
|
|
93
|
+
TServices,
|
|
94
|
+
TServiceDeps,
|
|
95
|
+
TPrivateServices,
|
|
96
|
+
TServiceThisContext,
|
|
97
|
+
THandlerThisContext,
|
|
98
|
+
TRequestStorage
|
|
99
|
+
>,
|
|
100
|
+
): FragmentDefinitionBuilder<
|
|
101
|
+
TConfig,
|
|
102
|
+
TOptions,
|
|
103
|
+
TDeps,
|
|
104
|
+
TBaseServices & { counter: { value: number; increment: () => void } },
|
|
105
|
+
TServices,
|
|
106
|
+
TServiceDeps,
|
|
107
|
+
TPrivateServices,
|
|
108
|
+
TServiceThisContext,
|
|
109
|
+
THandlerThisContext,
|
|
110
|
+
TRequestStorage
|
|
111
|
+
> => {
|
|
112
|
+
const currentDef = builder.build();
|
|
113
|
+
const currentBaseServices = currentDef.baseServices;
|
|
114
|
+
|
|
115
|
+
return new FragmentDefinitionBuilder(builder.name, {
|
|
116
|
+
...currentDef,
|
|
117
|
+
baseServices: (context) => {
|
|
118
|
+
const existing = currentBaseServices
|
|
119
|
+
? currentBaseServices(context)
|
|
120
|
+
: ({} as TBaseServices);
|
|
121
|
+
let count = 0;
|
|
122
|
+
return {
|
|
123
|
+
...existing,
|
|
124
|
+
counter: {
|
|
125
|
+
value: count,
|
|
126
|
+
increment: () => {
|
|
127
|
+
count++;
|
|
128
|
+
},
|
|
129
|
+
},
|
|
130
|
+
} as TBaseServices & { counter: { value: number; increment: () => void } };
|
|
131
|
+
},
|
|
132
|
+
});
|
|
133
|
+
};
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
const addLogger = () => {
|
|
137
|
+
return <
|
|
138
|
+
TConfig,
|
|
139
|
+
TOptions extends FragnoPublicConfig,
|
|
140
|
+
TDeps,
|
|
141
|
+
TBaseServices,
|
|
142
|
+
TServices,
|
|
143
|
+
TServiceDeps,
|
|
144
|
+
TPrivateServices,
|
|
145
|
+
TServiceThisContext extends RequestThisContext,
|
|
146
|
+
THandlerThisContext extends RequestThisContext,
|
|
147
|
+
TRequestStorage,
|
|
148
|
+
>(
|
|
149
|
+
builder: FragmentDefinitionBuilder<
|
|
150
|
+
TConfig,
|
|
151
|
+
TOptions,
|
|
152
|
+
TDeps,
|
|
153
|
+
TBaseServices,
|
|
154
|
+
TServices,
|
|
155
|
+
TServiceDeps,
|
|
156
|
+
TPrivateServices,
|
|
157
|
+
TServiceThisContext,
|
|
158
|
+
THandlerThisContext,
|
|
159
|
+
TRequestStorage
|
|
160
|
+
>,
|
|
161
|
+
): FragmentDefinitionBuilder<
|
|
162
|
+
TConfig,
|
|
163
|
+
TOptions,
|
|
164
|
+
TDeps,
|
|
165
|
+
TBaseServices & { logger: { log: (msg: string) => void } },
|
|
166
|
+
TServices,
|
|
167
|
+
TServiceDeps,
|
|
168
|
+
TPrivateServices,
|
|
169
|
+
TServiceThisContext,
|
|
170
|
+
THandlerThisContext,
|
|
171
|
+
TRequestStorage
|
|
172
|
+
> => {
|
|
173
|
+
const currentDef = builder.build();
|
|
174
|
+
const currentBaseServices = currentDef.baseServices;
|
|
175
|
+
|
|
176
|
+
return new FragmentDefinitionBuilder(builder.name, {
|
|
177
|
+
...currentDef,
|
|
178
|
+
baseServices: (context) => {
|
|
179
|
+
const existing = currentBaseServices
|
|
180
|
+
? currentBaseServices(context)
|
|
181
|
+
: ({} as TBaseServices);
|
|
182
|
+
return {
|
|
183
|
+
...existing,
|
|
184
|
+
logger: {
|
|
185
|
+
log: (_msg: string) => {
|
|
186
|
+
// In tests, we won't actually log
|
|
187
|
+
},
|
|
188
|
+
},
|
|
189
|
+
} as TBaseServices & { logger: { log: (msg: string) => void } };
|
|
190
|
+
},
|
|
191
|
+
});
|
|
192
|
+
};
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
const definition = defineFragment<{ name: string }>("chained")
|
|
196
|
+
.withDependencies(({ config }) => ({ name: config.name }))
|
|
197
|
+
.extend(addCounter())
|
|
198
|
+
.extend(addLogger())
|
|
199
|
+
.build();
|
|
200
|
+
|
|
201
|
+
expect(definition.name).toBe("chained");
|
|
202
|
+
expect(definition.baseServices).toBeDefined();
|
|
203
|
+
|
|
204
|
+
// Verify both extensions are present in the type
|
|
205
|
+
const services = definition.baseServices!({
|
|
206
|
+
config: { name: "test" },
|
|
207
|
+
options: {},
|
|
208
|
+
deps: { name: "test" },
|
|
209
|
+
serviceDeps: {},
|
|
210
|
+
privateServices: {},
|
|
211
|
+
defineService: (svc) => svc,
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
expect(services).toHaveProperty("counter");
|
|
215
|
+
expect(services).toHaveProperty("logger");
|
|
216
|
+
expect(services.counter).toHaveProperty("value");
|
|
217
|
+
expect(services.counter).toHaveProperty("increment");
|
|
218
|
+
expect(services.logger).toHaveProperty("log");
|
|
219
|
+
});
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
describe("ordering", () => {
|
|
223
|
+
it("should apply extends in order - first extend's services available to second", () => {
|
|
224
|
+
// First extension adds a base value
|
|
225
|
+
const addBase = () => {
|
|
226
|
+
return <
|
|
227
|
+
TConfig,
|
|
228
|
+
TOptions extends FragnoPublicConfig,
|
|
229
|
+
TDeps,
|
|
230
|
+
TBaseServices,
|
|
231
|
+
TServices,
|
|
232
|
+
TServiceDeps,
|
|
233
|
+
TPrivateServices,
|
|
234
|
+
TServiceThisContext extends RequestThisContext,
|
|
235
|
+
THandlerThisContext extends RequestThisContext,
|
|
236
|
+
TRequestStorage,
|
|
237
|
+
>(
|
|
238
|
+
builder: FragmentDefinitionBuilder<
|
|
239
|
+
TConfig,
|
|
240
|
+
TOptions,
|
|
241
|
+
TDeps,
|
|
242
|
+
TBaseServices,
|
|
243
|
+
TServices,
|
|
244
|
+
TServiceDeps,
|
|
245
|
+
TPrivateServices,
|
|
246
|
+
TServiceThisContext,
|
|
247
|
+
THandlerThisContext,
|
|
248
|
+
TRequestStorage
|
|
249
|
+
>,
|
|
250
|
+
): FragmentDefinitionBuilder<
|
|
251
|
+
TConfig,
|
|
252
|
+
TOptions,
|
|
253
|
+
TDeps,
|
|
254
|
+
TBaseServices & { baseValue: number },
|
|
255
|
+
TServices,
|
|
256
|
+
TServiceDeps,
|
|
257
|
+
TPrivateServices,
|
|
258
|
+
TServiceThisContext,
|
|
259
|
+
THandlerThisContext,
|
|
260
|
+
TRequestStorage
|
|
261
|
+
> => {
|
|
262
|
+
const currentDef = builder.build();
|
|
263
|
+
const currentBaseServices = currentDef.baseServices;
|
|
264
|
+
|
|
265
|
+
return new FragmentDefinitionBuilder(builder.name, {
|
|
266
|
+
...currentDef,
|
|
267
|
+
baseServices: (context) => {
|
|
268
|
+
const existing = currentBaseServices
|
|
269
|
+
? currentBaseServices(context)
|
|
270
|
+
: ({} as TBaseServices);
|
|
271
|
+
return {
|
|
272
|
+
...existing,
|
|
273
|
+
baseValue: 42,
|
|
274
|
+
} as TBaseServices & { baseValue: number };
|
|
275
|
+
},
|
|
276
|
+
});
|
|
277
|
+
};
|
|
278
|
+
};
|
|
279
|
+
|
|
280
|
+
// Second extension uses the base value to compute a derived value
|
|
281
|
+
const addDerived = () => {
|
|
282
|
+
return <
|
|
283
|
+
TConfig,
|
|
284
|
+
TOptions extends FragnoPublicConfig,
|
|
285
|
+
TDeps,
|
|
286
|
+
TBaseServices extends { baseValue: number },
|
|
287
|
+
TServices,
|
|
288
|
+
TServiceDeps,
|
|
289
|
+
TPrivateServices,
|
|
290
|
+
TServiceThisContext extends RequestThisContext,
|
|
291
|
+
THandlerThisContext extends RequestThisContext,
|
|
292
|
+
TRequestStorage,
|
|
293
|
+
>(
|
|
294
|
+
builder: FragmentDefinitionBuilder<
|
|
295
|
+
TConfig,
|
|
296
|
+
TOptions,
|
|
297
|
+
TDeps,
|
|
298
|
+
TBaseServices,
|
|
299
|
+
TServices,
|
|
300
|
+
TServiceDeps,
|
|
301
|
+
TPrivateServices,
|
|
302
|
+
TServiceThisContext,
|
|
303
|
+
THandlerThisContext,
|
|
304
|
+
TRequestStorage
|
|
305
|
+
>,
|
|
306
|
+
): FragmentDefinitionBuilder<
|
|
307
|
+
TConfig,
|
|
308
|
+
TOptions,
|
|
309
|
+
TDeps,
|
|
310
|
+
TBaseServices & { derivedValue: number },
|
|
311
|
+
TServices,
|
|
312
|
+
TServiceDeps,
|
|
313
|
+
TPrivateServices,
|
|
314
|
+
TServiceThisContext,
|
|
315
|
+
THandlerThisContext,
|
|
316
|
+
TRequestStorage
|
|
317
|
+
> => {
|
|
318
|
+
const currentDef = builder.build();
|
|
319
|
+
const currentBaseServices = currentDef.baseServices;
|
|
320
|
+
|
|
321
|
+
return new FragmentDefinitionBuilder(builder.name, {
|
|
322
|
+
...currentDef,
|
|
323
|
+
baseServices: (context) => {
|
|
324
|
+
const existing = currentBaseServices!(context);
|
|
325
|
+
return {
|
|
326
|
+
...existing,
|
|
327
|
+
derivedValue: existing.baseValue * 2,
|
|
328
|
+
} as TBaseServices & { derivedValue: number };
|
|
329
|
+
},
|
|
330
|
+
});
|
|
331
|
+
};
|
|
332
|
+
};
|
|
333
|
+
|
|
334
|
+
const definition = defineFragment("ordered")
|
|
335
|
+
.extend(addBase())
|
|
336
|
+
.extend(addDerived()) // This depends on addBase being called first
|
|
337
|
+
.build();
|
|
338
|
+
|
|
339
|
+
const services = definition.baseServices!({
|
|
340
|
+
config: {},
|
|
341
|
+
options: {},
|
|
342
|
+
deps: {},
|
|
343
|
+
serviceDeps: {},
|
|
344
|
+
privateServices: {},
|
|
345
|
+
defineService: (svc) => svc,
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
expect(services.baseValue).toBe(42);
|
|
349
|
+
expect(services.derivedValue).toBe(84); // 42 * 2
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
it("withDependencies resets base services - extend before withDependencies is lost", () => {
|
|
353
|
+
const addUtility = () => {
|
|
354
|
+
return <
|
|
355
|
+
TConfig,
|
|
356
|
+
TOptions extends FragnoPublicConfig,
|
|
357
|
+
TDeps,
|
|
358
|
+
TBaseServices,
|
|
359
|
+
TServices,
|
|
360
|
+
TServiceDeps,
|
|
361
|
+
TPrivateServices,
|
|
362
|
+
TServiceThisContext extends RequestThisContext,
|
|
363
|
+
THandlerThisContext extends RequestThisContext,
|
|
364
|
+
TRequestStorage,
|
|
365
|
+
>(
|
|
366
|
+
builder: FragmentDefinitionBuilder<
|
|
367
|
+
TConfig,
|
|
368
|
+
TOptions,
|
|
369
|
+
TDeps,
|
|
370
|
+
TBaseServices,
|
|
371
|
+
TServices,
|
|
372
|
+
TServiceDeps,
|
|
373
|
+
TPrivateServices,
|
|
374
|
+
TServiceThisContext,
|
|
375
|
+
THandlerThisContext,
|
|
376
|
+
TRequestStorage
|
|
377
|
+
>,
|
|
378
|
+
): FragmentDefinitionBuilder<
|
|
379
|
+
TConfig,
|
|
380
|
+
TOptions,
|
|
381
|
+
TDeps,
|
|
382
|
+
TBaseServices & { utility: { helper: () => string } },
|
|
383
|
+
TServices,
|
|
384
|
+
TServiceDeps,
|
|
385
|
+
TPrivateServices,
|
|
386
|
+
TServiceThisContext,
|
|
387
|
+
THandlerThisContext,
|
|
388
|
+
TRequestStorage
|
|
389
|
+
> => {
|
|
390
|
+
const currentDef = builder.build();
|
|
391
|
+
const currentBaseServices = currentDef.baseServices;
|
|
392
|
+
|
|
393
|
+
return new FragmentDefinitionBuilder(builder.name, {
|
|
394
|
+
...currentDef,
|
|
395
|
+
baseServices: (context) => {
|
|
396
|
+
const existing = currentBaseServices
|
|
397
|
+
? currentBaseServices(context)
|
|
398
|
+
: ({} as TBaseServices);
|
|
399
|
+
return {
|
|
400
|
+
...existing,
|
|
401
|
+
utility: {
|
|
402
|
+
helper: () => "helped",
|
|
403
|
+
},
|
|
404
|
+
} as TBaseServices & { utility: { helper: () => string } };
|
|
405
|
+
},
|
|
406
|
+
});
|
|
407
|
+
};
|
|
408
|
+
};
|
|
409
|
+
|
|
410
|
+
// This demonstrates that withDependencies resets baseServices
|
|
411
|
+
// So extending BEFORE withDependencies means the extension is lost
|
|
412
|
+
const definition = defineFragment<{ key: string }>("early-extend")
|
|
413
|
+
.extend(addUtility())
|
|
414
|
+
.withDependencies(({ config }) => ({ key: config.key })) // This resets base services!
|
|
415
|
+
.build();
|
|
416
|
+
|
|
417
|
+
// Base services are undefined because withDependencies reset them
|
|
418
|
+
expect(definition.baseServices).toBeUndefined();
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
it("should preserve types when extending after other builder methods", () => {
|
|
422
|
+
const addFormatter = () => {
|
|
423
|
+
return <
|
|
424
|
+
TConfig,
|
|
425
|
+
TOptions extends FragnoPublicConfig,
|
|
426
|
+
TDeps,
|
|
427
|
+
TBaseServices,
|
|
428
|
+
TServices,
|
|
429
|
+
TServiceDeps,
|
|
430
|
+
TPrivateServices,
|
|
431
|
+
TServiceThisContext extends RequestThisContext,
|
|
432
|
+
THandlerThisContext extends RequestThisContext,
|
|
433
|
+
TRequestStorage,
|
|
434
|
+
>(
|
|
435
|
+
builder: FragmentDefinitionBuilder<
|
|
436
|
+
TConfig,
|
|
437
|
+
TOptions,
|
|
438
|
+
TDeps,
|
|
439
|
+
TBaseServices,
|
|
440
|
+
TServices,
|
|
441
|
+
TServiceDeps,
|
|
442
|
+
TPrivateServices,
|
|
443
|
+
TServiceThisContext,
|
|
444
|
+
THandlerThisContext,
|
|
445
|
+
TRequestStorage
|
|
446
|
+
>,
|
|
447
|
+
): FragmentDefinitionBuilder<
|
|
448
|
+
TConfig,
|
|
449
|
+
TOptions,
|
|
450
|
+
TDeps,
|
|
451
|
+
TBaseServices & { formatter: { format: (s: string) => string } },
|
|
452
|
+
TServices,
|
|
453
|
+
TServiceDeps,
|
|
454
|
+
TPrivateServices,
|
|
455
|
+
TServiceThisContext,
|
|
456
|
+
THandlerThisContext,
|
|
457
|
+
TRequestStorage
|
|
458
|
+
> => {
|
|
459
|
+
const currentDef = builder.build();
|
|
460
|
+
const currentBaseServices = currentDef.baseServices;
|
|
461
|
+
|
|
462
|
+
return new FragmentDefinitionBuilder(builder.name, {
|
|
463
|
+
...currentDef,
|
|
464
|
+
baseServices: (context) => {
|
|
465
|
+
const existing = currentBaseServices
|
|
466
|
+
? currentBaseServices(context)
|
|
467
|
+
: ({} as TBaseServices);
|
|
468
|
+
return {
|
|
469
|
+
...existing,
|
|
470
|
+
formatter: {
|
|
471
|
+
format: (s: string) => s.toUpperCase(),
|
|
472
|
+
},
|
|
473
|
+
} as TBaseServices & { formatter: { format: (s: string) => string } };
|
|
474
|
+
},
|
|
475
|
+
});
|
|
476
|
+
};
|
|
477
|
+
};
|
|
478
|
+
|
|
479
|
+
// Add other things first, then extend
|
|
480
|
+
const definition = defineFragment<{ prefix: string }>("late-extend")
|
|
481
|
+
.withDependencies(({ config }) => ({ prefix: config.prefix }))
|
|
482
|
+
.providesService("prefixer", ({ deps, defineService }) =>
|
|
483
|
+
defineService({
|
|
484
|
+
addPrefix: (s: string) => `${deps.prefix}-${s}`,
|
|
485
|
+
}),
|
|
486
|
+
)
|
|
487
|
+
.extend(addFormatter())
|
|
488
|
+
.build();
|
|
489
|
+
|
|
490
|
+
// Dependencies should still work
|
|
491
|
+
expect(definition.dependencies).toBeDefined();
|
|
492
|
+
const deps = definition.dependencies!({ config: { prefix: "TEST" }, options: {} });
|
|
493
|
+
expect(deps.prefix).toBe("TEST");
|
|
494
|
+
|
|
495
|
+
// Named services should still work
|
|
496
|
+
expect(definition.namedServices).toBeDefined();
|
|
497
|
+
const namedService = definition.namedServices!.prefixer({
|
|
498
|
+
config: { prefix: "TEST" },
|
|
499
|
+
options: {},
|
|
500
|
+
deps: { prefix: "TEST" },
|
|
501
|
+
serviceDeps: {},
|
|
502
|
+
privateServices: {},
|
|
503
|
+
defineService: (svc) => svc,
|
|
504
|
+
});
|
|
505
|
+
expect(namedService.addPrefix("value")).toBe("TEST-value");
|
|
506
|
+
|
|
507
|
+
// Extended base service should be present
|
|
508
|
+
const services = definition.baseServices!({
|
|
509
|
+
config: { prefix: "TEST" },
|
|
510
|
+
options: {},
|
|
511
|
+
deps: { prefix: "TEST" },
|
|
512
|
+
serviceDeps: {},
|
|
513
|
+
privateServices: {},
|
|
514
|
+
defineService: (svc) => svc,
|
|
515
|
+
});
|
|
516
|
+
expect(services.formatter.format("hello")).toBe("HELLO");
|
|
517
|
+
});
|
|
518
|
+
});
|
|
519
|
+
|
|
520
|
+
describe("type preservation", () => {
|
|
521
|
+
it("should preserve all type parameters through extend", () => {
|
|
522
|
+
type MyConfig = { value: number };
|
|
523
|
+
type MyDeps = { computed: number };
|
|
524
|
+
|
|
525
|
+
const addMath = () => {
|
|
526
|
+
return <
|
|
527
|
+
TConfig,
|
|
528
|
+
TOptions extends FragnoPublicConfig,
|
|
529
|
+
TDeps,
|
|
530
|
+
TBaseServices,
|
|
531
|
+
TServices,
|
|
532
|
+
TServiceDeps,
|
|
533
|
+
TPrivateServices,
|
|
534
|
+
TServiceThisContext extends RequestThisContext,
|
|
535
|
+
THandlerThisContext extends RequestThisContext,
|
|
536
|
+
TRequestStorage,
|
|
537
|
+
>(
|
|
538
|
+
builder: FragmentDefinitionBuilder<
|
|
539
|
+
TConfig,
|
|
540
|
+
TOptions,
|
|
541
|
+
TDeps,
|
|
542
|
+
TBaseServices,
|
|
543
|
+
TServices,
|
|
544
|
+
TServiceDeps,
|
|
545
|
+
TPrivateServices,
|
|
546
|
+
TServiceThisContext,
|
|
547
|
+
THandlerThisContext,
|
|
548
|
+
TRequestStorage
|
|
549
|
+
>,
|
|
550
|
+
): FragmentDefinitionBuilder<
|
|
551
|
+
TConfig,
|
|
552
|
+
TOptions,
|
|
553
|
+
TDeps,
|
|
554
|
+
TBaseServices & { math: { add: (a: number, b: number) => number } },
|
|
555
|
+
TServices,
|
|
556
|
+
TServiceDeps,
|
|
557
|
+
TPrivateServices,
|
|
558
|
+
TServiceThisContext,
|
|
559
|
+
THandlerThisContext,
|
|
560
|
+
TRequestStorage
|
|
561
|
+
> => {
|
|
562
|
+
const currentDef = builder.build();
|
|
563
|
+
const currentBaseServices = currentDef.baseServices;
|
|
564
|
+
|
|
565
|
+
return new FragmentDefinitionBuilder(builder.name, {
|
|
566
|
+
...currentDef,
|
|
567
|
+
baseServices: (context) => {
|
|
568
|
+
const existing = currentBaseServices
|
|
569
|
+
? currentBaseServices(context)
|
|
570
|
+
: ({} as TBaseServices);
|
|
571
|
+
return {
|
|
572
|
+
...existing,
|
|
573
|
+
math: {
|
|
574
|
+
add: (a: number, b: number) => a + b,
|
|
575
|
+
},
|
|
576
|
+
} as TBaseServices & { math: { add: (a: number, b: number) => number } };
|
|
577
|
+
},
|
|
578
|
+
});
|
|
579
|
+
};
|
|
580
|
+
};
|
|
581
|
+
|
|
582
|
+
const definition = defineFragment<MyConfig>("type-preserve")
|
|
583
|
+
.withDependencies(({ config }) => ({ computed: config.value * 2 }) as MyDeps)
|
|
584
|
+
.providesService("calculator", ({ deps, defineService }) =>
|
|
585
|
+
defineService({
|
|
586
|
+
getComputed: () => deps.computed,
|
|
587
|
+
}),
|
|
588
|
+
)
|
|
589
|
+
.usesService<"logger", { log: (msg: string) => void }>("logger")
|
|
590
|
+
.extend(addMath())
|
|
591
|
+
.build();
|
|
592
|
+
|
|
593
|
+
// Config type is preserved
|
|
594
|
+
expect(definition.dependencies).toBeDefined();
|
|
595
|
+
const deps = definition.dependencies!({ config: { value: 10 }, options: {} });
|
|
596
|
+
expect(deps.computed).toBe(20);
|
|
597
|
+
|
|
598
|
+
// Named services are preserved
|
|
599
|
+
expect(definition.namedServices).toBeDefined();
|
|
600
|
+
const calculator = definition.namedServices!.calculator({
|
|
601
|
+
config: { value: 10 },
|
|
602
|
+
options: {},
|
|
603
|
+
deps: { computed: 20 },
|
|
604
|
+
serviceDeps: { logger: { log: () => {} } },
|
|
605
|
+
privateServices: {},
|
|
606
|
+
defineService: (svc) => svc,
|
|
607
|
+
});
|
|
608
|
+
expect(calculator.getComputed()).toBe(20);
|
|
609
|
+
|
|
610
|
+
// Service dependencies are preserved
|
|
611
|
+
expect(definition.serviceDependencies).toBeDefined();
|
|
612
|
+
expect(definition.serviceDependencies!.logger).toEqual({ name: "logger", required: true });
|
|
613
|
+
|
|
614
|
+
// Extended base service is present
|
|
615
|
+
const services = definition.baseServices!({
|
|
616
|
+
config: { value: 10 },
|
|
617
|
+
options: {},
|
|
618
|
+
deps: { computed: 20 },
|
|
619
|
+
serviceDeps: { logger: { log: () => {} } },
|
|
620
|
+
privateServices: {},
|
|
621
|
+
defineService: (svc) => svc,
|
|
622
|
+
});
|
|
623
|
+
expect(services.math.add(5, 3)).toBe(8);
|
|
624
|
+
});
|
|
625
|
+
|
|
626
|
+
it("should allow extending to add request storage configuration", () => {
|
|
627
|
+
// This test shows a practical extend() pattern: adding storage after deps are set
|
|
628
|
+
const withRequestId = () => {
|
|
629
|
+
return <
|
|
630
|
+
TConfig,
|
|
631
|
+
TOptions extends FragnoPublicConfig,
|
|
632
|
+
TDeps,
|
|
633
|
+
TBaseServices,
|
|
634
|
+
TServices,
|
|
635
|
+
TServiceDeps,
|
|
636
|
+
TPrivateServices,
|
|
637
|
+
TServiceThisContext extends RequestThisContext,
|
|
638
|
+
THandlerThisContext extends RequestThisContext,
|
|
639
|
+
TRequestStorage,
|
|
640
|
+
>(
|
|
641
|
+
builder: FragmentDefinitionBuilder<
|
|
642
|
+
TConfig,
|
|
643
|
+
TOptions,
|
|
644
|
+
TDeps,
|
|
645
|
+
TBaseServices,
|
|
646
|
+
TServices,
|
|
647
|
+
TServiceDeps,
|
|
648
|
+
TPrivateServices,
|
|
649
|
+
TServiceThisContext,
|
|
650
|
+
THandlerThisContext,
|
|
651
|
+
TRequestStorage
|
|
652
|
+
>,
|
|
653
|
+
) => {
|
|
654
|
+
// Simple approach: use the builder's own methods which handle types correctly
|
|
655
|
+
return builder.withRequestStorage(() => ({
|
|
656
|
+
requestId: Math.random().toString(36),
|
|
657
|
+
timestamp: Date.now(),
|
|
658
|
+
}));
|
|
659
|
+
};
|
|
660
|
+
};
|
|
661
|
+
|
|
662
|
+
const definition = defineFragment("request-extend")
|
|
663
|
+
.withDependencies(() => ({ value: 42 }))
|
|
664
|
+
.extend(withRequestId())
|
|
665
|
+
.build();
|
|
666
|
+
|
|
667
|
+
expect(definition.createRequestStorage).toBeDefined();
|
|
668
|
+
const storage = definition.createRequestStorage!({
|
|
669
|
+
config: {},
|
|
670
|
+
options: {},
|
|
671
|
+
deps: { value: 42 },
|
|
672
|
+
});
|
|
673
|
+
expect(storage).toHaveProperty("requestId");
|
|
674
|
+
expect(storage).toHaveProperty("timestamp");
|
|
675
|
+
expect(typeof storage.requestId).toBe("string");
|
|
676
|
+
expect(typeof storage.timestamp).toBe("number");
|
|
677
|
+
});
|
|
678
|
+
});
|
|
679
|
+
|
|
680
|
+
describe("edge cases", () => {
|
|
681
|
+
it("should handle extend on minimal fragment definition", () => {
|
|
682
|
+
const addSimple = () => {
|
|
683
|
+
return <
|
|
684
|
+
TConfig,
|
|
685
|
+
TOptions extends FragnoPublicConfig,
|
|
686
|
+
TDeps,
|
|
687
|
+
TBaseServices,
|
|
688
|
+
TServices,
|
|
689
|
+
TServiceDeps,
|
|
690
|
+
TPrivateServices,
|
|
691
|
+
TServiceThisContext extends RequestThisContext,
|
|
692
|
+
THandlerThisContext extends RequestThisContext,
|
|
693
|
+
TRequestStorage,
|
|
694
|
+
>(
|
|
695
|
+
builder: FragmentDefinitionBuilder<
|
|
696
|
+
TConfig,
|
|
697
|
+
TOptions,
|
|
698
|
+
TDeps,
|
|
699
|
+
TBaseServices,
|
|
700
|
+
TServices,
|
|
701
|
+
TServiceDeps,
|
|
702
|
+
TPrivateServices,
|
|
703
|
+
TServiceThisContext,
|
|
704
|
+
THandlerThisContext,
|
|
705
|
+
TRequestStorage
|
|
706
|
+
>,
|
|
707
|
+
): FragmentDefinitionBuilder<
|
|
708
|
+
TConfig,
|
|
709
|
+
TOptions,
|
|
710
|
+
TDeps,
|
|
711
|
+
TBaseServices & { simple: string },
|
|
712
|
+
TServices,
|
|
713
|
+
TServiceDeps,
|
|
714
|
+
TPrivateServices,
|
|
715
|
+
TServiceThisContext,
|
|
716
|
+
THandlerThisContext,
|
|
717
|
+
TRequestStorage
|
|
718
|
+
> => {
|
|
719
|
+
const currentDef = builder.build();
|
|
720
|
+
const currentBaseServices = currentDef.baseServices;
|
|
721
|
+
|
|
722
|
+
return new FragmentDefinitionBuilder(builder.name, {
|
|
723
|
+
...currentDef,
|
|
724
|
+
baseServices: (context) => {
|
|
725
|
+
const existing = currentBaseServices
|
|
726
|
+
? currentBaseServices(context)
|
|
727
|
+
: ({} as TBaseServices);
|
|
728
|
+
return {
|
|
729
|
+
...existing,
|
|
730
|
+
simple: "value",
|
|
731
|
+
} as TBaseServices & { simple: string };
|
|
732
|
+
},
|
|
733
|
+
});
|
|
734
|
+
};
|
|
735
|
+
};
|
|
736
|
+
|
|
737
|
+
// Minimal fragment - just a name, then extend
|
|
738
|
+
const definition = defineFragment("minimal").extend(addSimple()).build();
|
|
739
|
+
|
|
740
|
+
expect(definition.name).toBe("minimal");
|
|
741
|
+
expect(definition.baseServices).toBeDefined();
|
|
742
|
+
|
|
743
|
+
const services = definition.baseServices!({
|
|
744
|
+
config: {},
|
|
745
|
+
options: {},
|
|
746
|
+
deps: {},
|
|
747
|
+
serviceDeps: {},
|
|
748
|
+
privateServices: {},
|
|
749
|
+
defineService: (svc) => svc,
|
|
750
|
+
});
|
|
751
|
+
|
|
752
|
+
expect(services.simple).toBe("value");
|
|
753
|
+
});
|
|
754
|
+
|
|
755
|
+
it("should allow extend with identity transformation", () => {
|
|
756
|
+
const identity = () => {
|
|
757
|
+
return <
|
|
758
|
+
TConfig,
|
|
759
|
+
TOptions extends FragnoPublicConfig,
|
|
760
|
+
TDeps,
|
|
761
|
+
TBaseServices,
|
|
762
|
+
TServices,
|
|
763
|
+
TServiceDeps,
|
|
764
|
+
TPrivateServices,
|
|
765
|
+
TServiceThisContext extends RequestThisContext,
|
|
766
|
+
THandlerThisContext extends RequestThisContext,
|
|
767
|
+
TRequestStorage,
|
|
768
|
+
>(
|
|
769
|
+
builder: FragmentDefinitionBuilder<
|
|
770
|
+
TConfig,
|
|
771
|
+
TOptions,
|
|
772
|
+
TDeps,
|
|
773
|
+
TBaseServices,
|
|
774
|
+
TServices,
|
|
775
|
+
TServiceDeps,
|
|
776
|
+
TPrivateServices,
|
|
777
|
+
TServiceThisContext,
|
|
778
|
+
THandlerThisContext,
|
|
779
|
+
TRequestStorage
|
|
780
|
+
>,
|
|
781
|
+
): FragmentDefinitionBuilder<
|
|
782
|
+
TConfig,
|
|
783
|
+
TOptions,
|
|
784
|
+
TDeps,
|
|
785
|
+
TBaseServices,
|
|
786
|
+
TServices,
|
|
787
|
+
TServiceDeps,
|
|
788
|
+
TPrivateServices,
|
|
789
|
+
TServiceThisContext,
|
|
790
|
+
THandlerThisContext,
|
|
791
|
+
TRequestStorage
|
|
792
|
+
> => {
|
|
793
|
+
// Just return the builder unchanged
|
|
794
|
+
return builder;
|
|
795
|
+
};
|
|
796
|
+
};
|
|
797
|
+
|
|
798
|
+
const definition = defineFragment("identity")
|
|
799
|
+
.withDependencies(() => ({ x: 1 }))
|
|
800
|
+
.extend(identity())
|
|
801
|
+
.build();
|
|
802
|
+
|
|
803
|
+
expect(definition.name).toBe("identity");
|
|
804
|
+
expect(definition.dependencies).toBeDefined();
|
|
805
|
+
|
|
806
|
+
const deps = definition.dependencies!({ config: {}, options: {} });
|
|
807
|
+
expect(deps.x).toBe(1);
|
|
808
|
+
});
|
|
809
|
+
});
|
|
810
|
+
});
|