@client-compose/client 0.8.0 → 0.9.0
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/README.md +23 -0
- package/dist/index.d.ts +26 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +35 -1
- package/dist/index.js.map +1 -1
- package/dist/proxy.d.ts +3 -1
- package/dist/proxy.d.ts.map +1 -1
- package/dist/proxy.js +20 -0
- package/dist/proxy.js.map +1 -1
- package/dist/strings.d.ts +26 -0
- package/dist/strings.d.ts.map +1 -0
- package/dist/strings.js +130 -0
- package/dist/strings.js.map +1 -0
- package/dist/testing.d.ts +24 -5
- package/dist/testing.d.ts.map +1 -1
- package/dist/testing.js +33 -5
- package/dist/testing.js.map +1 -1
- package/package.json +3 -3
- package/src/index.ts +69 -7
- package/src/proxy.ts +28 -0
- package/src/strings.ts +178 -0
- package/src/testing.ts +64 -4
package/src/testing.ts
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
// screens: { profile: () => screen("profile", [island("header", ProfileHeaderV1, { name: "Ada" })]) },
|
|
7
7
|
// actions: { follow: () => actionOk(FollowUserV1, { followers: 8 }) },
|
|
8
8
|
// islands: { "profile/posts": (r) => islandResponse("profile", island("posts", PostsV1, …, "page:3")) },
|
|
9
|
+
// strings: () => stringsResponse("en", { retry: "Try again" }),
|
|
9
10
|
// });
|
|
10
11
|
// // pass gateway.fetch as the client's fetch, then assert on gateway.actionRequests()
|
|
11
12
|
//
|
|
@@ -21,8 +22,10 @@ import {
|
|
|
21
22
|
type IslandEntry,
|
|
22
23
|
type IslandError,
|
|
23
24
|
type PendingIsland,
|
|
25
|
+
type PluralRules,
|
|
24
26
|
PROTOCOL_VERSION,
|
|
25
27
|
type ScreenMeta,
|
|
28
|
+
type Strings,
|
|
26
29
|
} from "@client-compose/protocol";
|
|
27
30
|
|
|
28
31
|
/**
|
|
@@ -71,6 +74,11 @@ export interface RecordedActionRequest extends RecordedRequest {
|
|
|
71
74
|
idempotencyKey: string | undefined;
|
|
72
75
|
}
|
|
73
76
|
|
|
77
|
+
/** A strings request (spec §4.7): the app strings, in the client's language. */
|
|
78
|
+
export interface RecordedStringsRequest extends RecordedRequest {
|
|
79
|
+
client: ClientContext | undefined;
|
|
80
|
+
}
|
|
81
|
+
|
|
74
82
|
export interface FakeGatewayHandlers {
|
|
75
83
|
/** By screen id. */
|
|
76
84
|
screens?: Record<string, (request: RecordedScreenRequest) => FakeResponse>;
|
|
@@ -78,6 +86,8 @@ export interface FakeGatewayHandlers {
|
|
|
78
86
|
actions?: Record<string, (request: RecordedActionRequest) => FakeResponse>;
|
|
79
87
|
/** Island requests, by `"{screen}/{key}"`. */
|
|
80
88
|
islands?: Record<string, (request: RecordedIslandRequest) => FakeResponse>;
|
|
89
|
+
/** The app strings (POST /strings). */
|
|
90
|
+
strings?: (request: RecordedStringsRequest) => FakeResponse;
|
|
81
91
|
/** Anything else, e.g. uploads to storage: return undefined to leave a request to the others. */
|
|
82
92
|
other?: (request: RecordedRequest) => FakeResponse | undefined;
|
|
83
93
|
}
|
|
@@ -90,6 +100,7 @@ export interface FakeGateway {
|
|
|
90
100
|
screenRequests(): RecordedScreenRequest[];
|
|
91
101
|
islandRequests(): RecordedIslandRequest[];
|
|
92
102
|
actionRequests(): RecordedActionRequest[];
|
|
103
|
+
stringsRequests(): RecordedStringsRequest[];
|
|
93
104
|
}
|
|
94
105
|
|
|
95
106
|
const isRecord = (value: unknown): value is Record<string, unknown> =>
|
|
@@ -208,6 +219,14 @@ function asActionRequest(request: RecordedRequest): RecordedActionRequest | unde
|
|
|
208
219
|
};
|
|
209
220
|
}
|
|
210
221
|
|
|
222
|
+
function asStringsRequest(request: RecordedRequest): RecordedStringsRequest | undefined {
|
|
223
|
+
const path = request.url.split("?")[0] ?? "";
|
|
224
|
+
if (!/\/strings\/?$/.test(path) || !isRecord(request.body)) {
|
|
225
|
+
return undefined;
|
|
226
|
+
}
|
|
227
|
+
return { ...request, client: clientContext(request.body.client) };
|
|
228
|
+
}
|
|
229
|
+
|
|
211
230
|
function respond(response: FakeResponse): Response {
|
|
212
231
|
if (response.stream) {
|
|
213
232
|
return new Response(response.stream(), {
|
|
@@ -253,6 +272,10 @@ export function createFakeGateway(handlers: FakeGatewayHandlers = {}): FakeGatew
|
|
|
253
272
|
if (actionRequest && actionHandler) {
|
|
254
273
|
return respond(actionHandler(actionRequest));
|
|
255
274
|
}
|
|
275
|
+
const stringsRequest = asStringsRequest(request);
|
|
276
|
+
if (stringsRequest && handlers.strings) {
|
|
277
|
+
return respond(handlers.strings(stringsRequest));
|
|
278
|
+
}
|
|
256
279
|
throw new Error(
|
|
257
280
|
`fake gateway: no handler for ${request.method} ${request.url} ${JSON.stringify(request.body)}`,
|
|
258
281
|
);
|
|
@@ -263,6 +286,7 @@ export function createFakeGateway(handlers: FakeGatewayHandlers = {}): FakeGatew
|
|
|
263
286
|
screenRequests: () => requests.flatMap((r) => asScreenRequest(r) ?? []),
|
|
264
287
|
islandRequests: () => requests.flatMap((r) => asIslandRequest(r) ?? []),
|
|
265
288
|
actionRequests: () => requests.flatMap((r) => asActionRequest(r) ?? []),
|
|
289
|
+
stringsRequests: () => requests.flatMap((r) => asStringsRequest(r) ?? []),
|
|
266
290
|
};
|
|
267
291
|
}
|
|
268
292
|
|
|
@@ -289,16 +313,24 @@ export function installFakeGateway(handlers: FakeGatewayHandlers = {}): Installe
|
|
|
289
313
|
|
|
290
314
|
// ---- Responses ----
|
|
291
315
|
|
|
316
|
+
/** More of an island's entry: its next page's cursor (spec §4.5), and its strings (spec §4.7). */
|
|
317
|
+
export interface IslandOptions {
|
|
318
|
+
next?: string;
|
|
319
|
+
strings?: Strings;
|
|
320
|
+
}
|
|
321
|
+
|
|
292
322
|
/**
|
|
293
323
|
* An island in a screen or island response, typed by its generated descriptor. Give `next` for an
|
|
294
|
-
* island with more pages (spec §4.5)
|
|
324
|
+
* island with more pages (spec §4.5), or `{ next, strings }`.
|
|
295
325
|
*/
|
|
296
|
-
export function island<P>(
|
|
326
|
+
export function island<P, S>(
|
|
297
327
|
key: string,
|
|
298
|
-
descriptor: IslandDescriptor<P>,
|
|
328
|
+
descriptor: IslandDescriptor<P, S>,
|
|
299
329
|
props: P,
|
|
300
|
-
|
|
330
|
+
more?: string | IslandOptions,
|
|
301
331
|
): IslandEntry<P> {
|
|
332
|
+
const { next, strings } =
|
|
333
|
+
typeof more === "string" ? { next: more, strings: undefined } : (more ?? {});
|
|
302
334
|
return {
|
|
303
335
|
key,
|
|
304
336
|
type: descriptor.type,
|
|
@@ -306,9 +338,16 @@ export function island<P>(
|
|
|
306
338
|
status: "ok",
|
|
307
339
|
props,
|
|
308
340
|
...(next !== undefined && { next }),
|
|
341
|
+
...(strings !== undefined && { strings }),
|
|
309
342
|
};
|
|
310
343
|
}
|
|
311
344
|
|
|
345
|
+
/** The language a screen is in, and its plural rules (spec §4.7). */
|
|
346
|
+
export interface ScreenLanguage {
|
|
347
|
+
locale: string;
|
|
348
|
+
plurals: PluralRules;
|
|
349
|
+
}
|
|
350
|
+
|
|
312
351
|
/** An island that failed on the composer (spec §3.4). */
|
|
313
352
|
export function islandError(
|
|
314
353
|
key: string,
|
|
@@ -329,6 +368,7 @@ export function screen(
|
|
|
329
368
|
id: string,
|
|
330
369
|
islands: IslandEntry<unknown>[],
|
|
331
370
|
meta?: ScreenMeta,
|
|
371
|
+
language?: ScreenLanguage,
|
|
332
372
|
): FakeResponse {
|
|
333
373
|
const head = {
|
|
334
374
|
kind: "head",
|
|
@@ -336,6 +376,7 @@ export function screen(
|
|
|
336
376
|
screen: id,
|
|
337
377
|
islands,
|
|
338
378
|
...(meta && { meta }),
|
|
379
|
+
...language,
|
|
339
380
|
};
|
|
340
381
|
return {
|
|
341
382
|
status: 200,
|
|
@@ -376,6 +417,7 @@ export function streamedScreen(
|
|
|
376
417
|
id: string,
|
|
377
418
|
islands: (IslandEntry<unknown> | PendingIsland)[],
|
|
378
419
|
meta?: ScreenMeta,
|
|
420
|
+
language?: ScreenLanguage,
|
|
379
421
|
): StreamedScreen {
|
|
380
422
|
const encoder = new TextEncoder();
|
|
381
423
|
const queued: string[] = [];
|
|
@@ -399,6 +441,7 @@ export function streamedScreen(
|
|
|
399
441
|
screen: id,
|
|
400
442
|
islands,
|
|
401
443
|
...(meta && { meta }),
|
|
444
|
+
...language,
|
|
402
445
|
});
|
|
403
446
|
return {
|
|
404
447
|
status: 200,
|
|
@@ -447,6 +490,23 @@ export function islandResponse(screen: string, entry: IslandEntry<unknown>): Fak
|
|
|
447
490
|
};
|
|
448
491
|
}
|
|
449
492
|
|
|
493
|
+
/** The app strings in a language (spec §4.7), with its plural rules if its counted strings need them. */
|
|
494
|
+
export function stringsResponse(
|
|
495
|
+
locale: string,
|
|
496
|
+
strings: Strings,
|
|
497
|
+
plurals: PluralRules = {},
|
|
498
|
+
): FakeResponse {
|
|
499
|
+
return {
|
|
500
|
+
status: 200,
|
|
501
|
+
body: {
|
|
502
|
+
protocol: PROTOCOL_VERSION,
|
|
503
|
+
locale,
|
|
504
|
+
plurals,
|
|
505
|
+
strings,
|
|
506
|
+
},
|
|
507
|
+
};
|
|
508
|
+
}
|
|
509
|
+
|
|
450
510
|
/** A screen request that fails, e.g. `screenError(404, "not_found")` or `screenError(401, "unauthenticated")`. */
|
|
451
511
|
export function screenError(status: number, code: string): FakeResponse {
|
|
452
512
|
return { status, body: { protocol: PROTOCOL_VERSION, error: { code } } };
|