@client-compose/client 0.4.0 → 0.5.1
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 +12 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -1
- package/dist/index.js.map +1 -1
- package/dist/proxy.d.ts +41 -0
- package/dist/proxy.d.ts.map +1 -0
- package/dist/proxy.js +86 -0
- package/dist/proxy.js.map +1 -0
- package/dist/testing.d.ts +9 -0
- package/dist/testing.d.ts.map +1 -1
- package/dist/testing.js +24 -2
- package/dist/testing.js.map +1 -1
- package/package.json +6 -2
- package/src/index.ts +15 -6
- package/src/proxy.ts +147 -0
- package/src/testing.ts +58 -10
package/README.md
CHANGED
|
@@ -83,8 +83,20 @@ const res = await callAction(baseUrl, FollowUserV1, { userId }, { client, fetch:
|
|
|
83
83
|
assert.equal(gateway.actionRequests()[0].input.userId, userId);
|
|
84
84
|
```
|
|
85
85
|
|
|
86
|
+
For code that calls the global `fetch` itself, such as a whole app rendered in a test,
|
|
87
|
+
`installFakeGateway(handlers)` puts the fake composer in its place; call `restore()` afterwards.
|
|
88
|
+
|
|
86
89
|
For React, `@client-compose/react/testing` adds a provider config built around it.
|
|
87
90
|
|
|
91
|
+
## Through your own server
|
|
92
|
+
|
|
93
|
+
`@client-compose/client/proxy` passes a browser's screen and action requests to the composer
|
|
94
|
+
through your server, so it can add what the browser mustn't hold, such as a session kept in an
|
|
95
|
+
httpOnly cookie. `createComposerProxy({ baseUrl, client, headers, onActionResponse })` checks each
|
|
96
|
+
request (anyone can call a server endpoint with anything), sets the client context and headers,
|
|
97
|
+
and returns the composer's failures as data. `@client-compose/react`'s `viaProxy` is the browser
|
|
98
|
+
half.
|
|
99
|
+
|
|
88
100
|
## Links
|
|
89
101
|
|
|
90
102
|
- [Protocol spec](https://github.com/client-compose/client-compose/blob/main/spec/PROTOCOL.md)
|
package/dist/index.d.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { type ActionDescriptor, type ActionRequest, type ActionResponse, type Capabilities, type ClientContext, type DeclaredError, type IslandDescriptor, type IslandEntry, type ScreenRequest, type ScreenResponse } from "@client-compose/protocol";
|
|
2
|
-
type OkEntry<P =
|
|
2
|
+
type OkEntry<P = unknown> = IslandEntry<P> & {
|
|
3
3
|
status: "ok";
|
|
4
4
|
};
|
|
5
5
|
type ErrorEntry = IslandEntry & {
|
|
6
6
|
status: "error";
|
|
7
7
|
};
|
|
8
|
-
export type IslandRenderer<R, P =
|
|
8
|
+
export type IslandRenderer<R, P = unknown> = (props: P, entry: OkEntry<P>) => R;
|
|
9
9
|
export declare class IslandRegistry<R> {
|
|
10
10
|
private renderers;
|
|
11
11
|
/** Register a renderer for a generated island descriptor: `register(ProfileHeaderV2, (props) => …)`. */
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAGA,OAAO,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
package/dist/index.js
CHANGED
|
@@ -9,11 +9,15 @@ export class IslandRegistry {
|
|
|
9
9
|
: [islandOrType.type, islandOrType.version, versionOrRender];
|
|
10
10
|
if (!this.renderers.has(type))
|
|
11
11
|
this.renderers.set(type, new Map());
|
|
12
|
+
// The registry hands each renderer the props of its own island type and version.
|
|
12
13
|
this.renderers.get(type).set(version, render);
|
|
13
14
|
return this;
|
|
14
15
|
}
|
|
15
16
|
capabilities() {
|
|
16
|
-
return Object.fromEntries([...this.renderers].map(([type, versions]) => [
|
|
17
|
+
return Object.fromEntries([...this.renderers].map(([type, versions]) => [
|
|
18
|
+
type,
|
|
19
|
+
[...versions.keys()].sort((a, b) => a - b),
|
|
20
|
+
]));
|
|
17
21
|
}
|
|
18
22
|
get(type, version) {
|
|
19
23
|
return this.renderers.get(type)?.get(version);
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,+EAA+E;AAC/E,kFAAkF;AAElF,OAAO,
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
package/dist/proxy.d.ts
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { type ActionRequest, type ActionResponse, type ClientContext, type ScreenRequest, type ScreenResponse } from "@client-compose/protocol";
|
|
2
|
+
/** A screen request from the browser: the server sets the client context. */
|
|
3
|
+
export type ProxiedScreenRequest = Omit<ScreenRequest, "client">;
|
|
4
|
+
/** An action request from the browser: the server sets the client context. */
|
|
5
|
+
export type ProxiedActionRequest = Omit<ActionRequest, "client">;
|
|
6
|
+
/** The composer's response, or the status and body it failed with. */
|
|
7
|
+
export type ProxyResult<T> = {
|
|
8
|
+
ok: true;
|
|
9
|
+
response: T;
|
|
10
|
+
} | {
|
|
11
|
+
ok: false;
|
|
12
|
+
status: number;
|
|
13
|
+
body: string;
|
|
14
|
+
};
|
|
15
|
+
export interface ComposerProxyOptions {
|
|
16
|
+
/** Composer base URL. */
|
|
17
|
+
baseUrl: string;
|
|
18
|
+
/** The client context for each request, e.g. with the locale from the request's cookies. */
|
|
19
|
+
client: () => ClientContext | Promise<ClientContext>;
|
|
20
|
+
/** Extra headers for each request, e.g. `Authorization` from a session cookie. */
|
|
21
|
+
headers?: () => Record<string, string> | Promise<Record<string, string>>;
|
|
22
|
+
/** Extra options for every request, e.g. `{ cache: "no-store" }`. */
|
|
23
|
+
requestInit?: RequestInit & Record<string, unknown>;
|
|
24
|
+
/** Custom fetch implementation. Defaults to the global `fetch`. */
|
|
25
|
+
fetch?: typeof fetch;
|
|
26
|
+
/**
|
|
27
|
+
* Sees each action's response (ok or a declared error) before the browser does, and returns
|
|
28
|
+
* what the browser gets: e.g. keep a new session in a cookie, and take its token out.
|
|
29
|
+
*/
|
|
30
|
+
onActionResponse?: (request: ActionRequest, response: ActionResponse) => ActionResponse | Promise<ActionResponse>;
|
|
31
|
+
}
|
|
32
|
+
export interface ComposerProxy {
|
|
33
|
+
/** Checks a browser's action request, then performs it with the server's context and headers. */
|
|
34
|
+
performAction(body: unknown): Promise<ProxyResult<ActionResponse>>;
|
|
35
|
+
/** Checks a browser's screen request, then loads it with the server's context and headers. */
|
|
36
|
+
loadScreen(body: unknown): Promise<ProxyResult<ScreenResponse>>;
|
|
37
|
+
}
|
|
38
|
+
export declare function isProxiedScreenRequest(value: unknown): value is ProxiedScreenRequest;
|
|
39
|
+
export declare function isProxiedActionRequest(value: unknown): value is ProxiedActionRequest;
|
|
40
|
+
export declare function createComposerProxy(options: ComposerProxyOptions): ComposerProxy;
|
|
41
|
+
//# sourceMappingURL=proxy.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"proxy.d.ts","sourceRoot":"","sources":["../src/proxy.ts"],"names":[],"mappings":"AAeA,OAAO,EACL,KAAK,aAAa,EAClB,KAAK,cAAc,EAEnB,KAAK,aAAa,EAElB,KAAK,aAAa,EAClB,KAAK,cAAc,EACpB,MAAM,0BAA0B,CAAC;AAIlC,6EAA6E;AAC7E,MAAM,MAAM,oBAAoB,GAAG,IAAI,CAAC,aAAa,EAAE,QAAQ,CAAC,CAAC;AACjE,8EAA8E;AAC9E,MAAM,MAAM,oBAAoB,GAAG,IAAI,CAAC,aAAa,EAAE,QAAQ,CAAC,CAAC;AAEjE,sEAAsE;AACtE,MAAM,MAAM,WAAW,CAAC,CAAC,IACvB;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,QAAQ,EAAE,CAAC,CAAA;CAAE,GAAG;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC;AAE1E,MAAM,WAAW,oBAAoB;IACnC,yBAAyB;IACzB,OAAO,EAAE,MAAM,CAAC;IAChB,4FAA4F;IAC5F,MAAM,EAAE,MAAM,aAAa,GAAG,OAAO,CAAC,aAAa,CAAC,CAAC;IACrD,kFAAkF;IAClF,OAAO,CAAC,EAAE,MAAM,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;IACzE,qEAAqE;IACrE,WAAW,CAAC,EAAE,WAAW,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACpD,mEAAmE;IACnE,KAAK,CAAC,EAAE,OAAO,KAAK,CAAC;IACrB;;;OAGG;IACH,gBAAgB,CAAC,EAAE,CACjB,OAAO,EAAE,aAAa,EACtB,QAAQ,EAAE,cAAc,KACrB,cAAc,GAAG,OAAO,CAAC,cAAc,CAAC,CAAC;CAC/C;AAED,MAAM,WAAW,aAAa;IAC5B,iGAAiG;IACjG,aAAa,CAAC,IAAI,EAAE,OAAO,GAAG,OAAO,CAAC,WAAW,CAAC,cAAc,CAAC,CAAC,CAAC;IACnE,8FAA8F;IAC9F,UAAU,CAAC,IAAI,EAAE,OAAO,GAAG,OAAO,CAAC,WAAW,CAAC,cAAc,CAAC,CAAC,CAAC;CACjE;AAkBD,wBAAgB,sBAAsB,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,oBAAoB,CAQpF;AAED,wBAAgB,sBAAsB,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,oBAAoB,CASpF;AASD,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,oBAAoB,GAAG,aAAa,CAsChF"}
|
package/dist/proxy.js
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
// Passing a browser's screen and action requests to the composer through your own server, e.g. a
|
|
2
|
+
// Next.js Server Action or a Remix action. The server adds what the browser mustn't hold (a session
|
|
3
|
+
// kept in an httpOnly cookie) and vouches for the client context, so the browser's is ignored.
|
|
4
|
+
//
|
|
5
|
+
// const proxy = createComposerProxy({
|
|
6
|
+
// baseUrl: COMPOSER_URL,
|
|
7
|
+
// client: async () => ({ platform: "web", appVersion, locale: await getLocale() }),
|
|
8
|
+
// headers: async () => ({ Authorization: `Bearer ${await getSessionToken()}` }),
|
|
9
|
+
// });
|
|
10
|
+
// export async function performAction(body: unknown) { return proxy.performAction(body); }
|
|
11
|
+
//
|
|
12
|
+
// Anyone can call a server endpoint with anything, so requests are checked before they're sent on.
|
|
13
|
+
// Errors come back as data, since frameworks often strip thrown errors' details on the way to the
|
|
14
|
+
// browser.
|
|
15
|
+
import { PROTOCOL_VERSION, } from "@client-compose/protocol";
|
|
16
|
+
import { ActionFetchError, fetchAction, fetchScreen, ScreenFetchError } from "./index.js";
|
|
17
|
+
const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
18
|
+
function isCapabilities(value) {
|
|
19
|
+
return (isRecord(value) &&
|
|
20
|
+
Object.values(value).every((versions) => Array.isArray(versions) && versions.every((v) => typeof v === "number")));
|
|
21
|
+
}
|
|
22
|
+
function isStringRecord(value) {
|
|
23
|
+
return isRecord(value) && Object.values(value).every((v) => typeof v === "string");
|
|
24
|
+
}
|
|
25
|
+
export function isProxiedScreenRequest(value) {
|
|
26
|
+
return (isRecord(value) &&
|
|
27
|
+
value.protocol === PROTOCOL_VERSION &&
|
|
28
|
+
typeof value.screen === "string" &&
|
|
29
|
+
(value.params === undefined || isStringRecord(value.params)) &&
|
|
30
|
+
isCapabilities(value.islands));
|
|
31
|
+
}
|
|
32
|
+
export function isProxiedActionRequest(value) {
|
|
33
|
+
return (isRecord(value) &&
|
|
34
|
+
value.protocol === PROTOCOL_VERSION &&
|
|
35
|
+
typeof value.action === "string" &&
|
|
36
|
+
typeof value.version === "number" &&
|
|
37
|
+
"input" in value &&
|
|
38
|
+
(value.idempotencyKey === undefined || typeof value.idempotencyKey === "string"));
|
|
39
|
+
}
|
|
40
|
+
/** What the composer itself answers to a malformed request. */
|
|
41
|
+
const BAD_REQUEST = {
|
|
42
|
+
ok: false,
|
|
43
|
+
status: 400,
|
|
44
|
+
body: JSON.stringify({ protocol: PROTOCOL_VERSION, error: { code: "bad_request" } }),
|
|
45
|
+
};
|
|
46
|
+
export function createComposerProxy(options) {
|
|
47
|
+
async function init() {
|
|
48
|
+
const headers = (await options.headers?.()) ?? {};
|
|
49
|
+
return { ...options.requestInit, headers: { ...options.requestInit?.headers, ...headers } };
|
|
50
|
+
}
|
|
51
|
+
async function performAction(body) {
|
|
52
|
+
if (!isProxiedActionRequest(body))
|
|
53
|
+
return BAD_REQUEST;
|
|
54
|
+
const request = { ...body, client: await options.client() };
|
|
55
|
+
try {
|
|
56
|
+
const response = await fetchAction(options.baseUrl, request, await init(), options.fetch);
|
|
57
|
+
return {
|
|
58
|
+
ok: true,
|
|
59
|
+
response: (await options.onActionResponse?.(request, response)) ?? response,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
catch (error) {
|
|
63
|
+
if (error instanceof ActionFetchError)
|
|
64
|
+
return { ok: false, status: error.status, body: error.body };
|
|
65
|
+
throw error;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
async function loadScreen(body) {
|
|
69
|
+
if (!isProxiedScreenRequest(body))
|
|
70
|
+
return BAD_REQUEST;
|
|
71
|
+
const request = { ...body, client: await options.client() };
|
|
72
|
+
try {
|
|
73
|
+
return {
|
|
74
|
+
ok: true,
|
|
75
|
+
response: await fetchScreen(options.baseUrl, request, await init(), options.fetch),
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
catch (error) {
|
|
79
|
+
if (error instanceof ScreenFetchError)
|
|
80
|
+
return { ok: false, status: error.status, body: error.body };
|
|
81
|
+
throw error;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return { performAction, loadScreen };
|
|
85
|
+
}
|
|
86
|
+
//# sourceMappingURL=proxy.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"proxy.js","sourceRoot":"","sources":["../src/proxy.ts"],"names":[],"mappings":"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"}
|
package/dist/testing.d.ts
CHANGED
|
@@ -44,6 +44,15 @@ export interface FakeGateway {
|
|
|
44
44
|
}
|
|
45
45
|
/** A composer whose screens and actions are the handlers given. */
|
|
46
46
|
export declare function createFakeGateway(handlers?: FakeGatewayHandlers): FakeGateway;
|
|
47
|
+
export interface InstalledFakeGateway extends FakeGateway {
|
|
48
|
+
/** Puts back the `fetch` that was there before. */
|
|
49
|
+
restore(): void;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* A fake composer installed as the global `fetch`, for code that fetches by itself (e.g. a whole
|
|
53
|
+
* app rendered in a test). Call `restore()` after the test.
|
|
54
|
+
*/
|
|
55
|
+
export declare function installFakeGateway(handlers?: FakeGatewayHandlers): InstalledFakeGateway;
|
|
47
56
|
/** An island in a screen response, typed by its generated descriptor. */
|
|
48
57
|
export declare function island<P>(key: string, descriptor: IslandDescriptor<P>, props: P): IslandEntry<P>;
|
|
49
58
|
/** An island that failed on the composer (spec §3.4). */
|
package/dist/testing.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"testing.d.ts","sourceRoot":"","sources":["../src/testing.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"testing.d.ts","sourceRoot":"","sources":["../src/testing.ts"],"names":[],"mappings":"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"}
|
package/dist/testing.js
CHANGED
|
@@ -7,6 +7,8 @@
|
|
|
7
7
|
// actions: { follow: () => actionOk(FollowUserV1, { followers: 8 }) },
|
|
8
8
|
// });
|
|
9
9
|
// // pass gateway.fetch as the client's fetch, then assert on gateway.actionRequests()
|
|
10
|
+
//
|
|
11
|
+
// For code that uses the global fetch itself, `installFakeGateway` puts one in its place.
|
|
10
12
|
import { PROTOCOL_VERSION, } from "@client-compose/protocol";
|
|
11
13
|
const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
12
14
|
function strings(value) {
|
|
@@ -23,7 +25,9 @@ function capabilities(value) {
|
|
|
23
25
|
]));
|
|
24
26
|
}
|
|
25
27
|
function clientContext(value) {
|
|
26
|
-
if (!isRecord(value) ||
|
|
28
|
+
if (!isRecord(value) ||
|
|
29
|
+
typeof value.platform !== "string" ||
|
|
30
|
+
typeof value.appVersion !== "string")
|
|
27
31
|
return undefined;
|
|
28
32
|
return { ...value, platform: value.platform, appVersion: value.appVersion };
|
|
29
33
|
}
|
|
@@ -109,6 +113,21 @@ export function createFakeGateway(handlers = {}) {
|
|
|
109
113
|
actionRequests: () => requests.flatMap((r) => asActionRequest(r) ?? []),
|
|
110
114
|
};
|
|
111
115
|
}
|
|
116
|
+
/**
|
|
117
|
+
* A fake composer installed as the global `fetch`, for code that fetches by itself (e.g. a whole
|
|
118
|
+
* app rendered in a test). Call `restore()` after the test.
|
|
119
|
+
*/
|
|
120
|
+
export function installFakeGateway(handlers = {}) {
|
|
121
|
+
const gateway = createFakeGateway(handlers);
|
|
122
|
+
const original = globalThis.fetch;
|
|
123
|
+
globalThis.fetch = gateway.fetch;
|
|
124
|
+
return {
|
|
125
|
+
...gateway,
|
|
126
|
+
restore: () => {
|
|
127
|
+
globalThis.fetch = original;
|
|
128
|
+
},
|
|
129
|
+
};
|
|
130
|
+
}
|
|
112
131
|
// ---- Responses ----
|
|
113
132
|
/** An island in a screen response, typed by its generated descriptor. */
|
|
114
133
|
export function island(key, descriptor, props) {
|
|
@@ -120,7 +139,10 @@ export function islandError(key, descriptor, error) {
|
|
|
120
139
|
}
|
|
121
140
|
/** A screen response. */
|
|
122
141
|
export function screen(id, islands, meta) {
|
|
123
|
-
return {
|
|
142
|
+
return {
|
|
143
|
+
status: 200,
|
|
144
|
+
body: { protocol: PROTOCOL_VERSION, screen: id, islands, ...(meta && { meta }) },
|
|
145
|
+
};
|
|
124
146
|
}
|
|
125
147
|
/** A screen request that fails, e.g. `screenError(404, "not_found")` or `screenError(401, "unauthenticated")`. */
|
|
126
148
|
export function screenError(status, code) {
|
package/dist/testing.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"testing.js","sourceRoot":"","sources":["../src/testing.ts"],"names":[],"mappings":"AAAA,gGAAgG;AAChG,8FAA8F;AAC9F,yFAAyF;AACzF,EAAE;AACF,0CAA0C;AAC1C,6GAA6G;AAC7G,6EAA6E;AAC7E,UAAU;AACV,2FAA2F;
|
|
1
|
+
{"version":3,"file":"testing.js","sourceRoot":"","sources":["../src/testing.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@client-compose/client",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.1",
|
|
4
4
|
"description": "Framework-agnostic Client Compose client: island registry, screen requests and render planning.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"client-compose",
|
|
@@ -26,6 +26,10 @@
|
|
|
26
26
|
"types": "./dist/index.d.ts",
|
|
27
27
|
"default": "./dist/index.js"
|
|
28
28
|
},
|
|
29
|
+
"./proxy": {
|
|
30
|
+
"types": "./dist/proxy.d.ts",
|
|
31
|
+
"default": "./dist/proxy.js"
|
|
32
|
+
},
|
|
29
33
|
"./testing": {
|
|
30
34
|
"types": "./dist/testing.d.ts",
|
|
31
35
|
"default": "./dist/testing.js"
|
|
@@ -43,7 +47,7 @@
|
|
|
43
47
|
"@client-compose/protocol": "0.3.0"
|
|
44
48
|
},
|
|
45
49
|
"devDependencies": {
|
|
46
|
-
"@client-compose/conformance": "0.
|
|
50
|
+
"@client-compose/conformance": "0.5.0"
|
|
47
51
|
},
|
|
48
52
|
"publishConfig": {
|
|
49
53
|
"access": "public"
|
package/src/index.ts
CHANGED
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
// `R` is whatever your UI layer renders: a React element, a DOM node, a string...
|
|
3
3
|
|
|
4
4
|
import {
|
|
5
|
-
PROTOCOL_VERSION,
|
|
6
5
|
type ActionDescriptor,
|
|
7
6
|
type ActionRequest,
|
|
8
7
|
type ActionResponse,
|
|
@@ -11,14 +10,15 @@ import {
|
|
|
11
10
|
type DeclaredError,
|
|
12
11
|
type IslandDescriptor,
|
|
13
12
|
type IslandEntry,
|
|
13
|
+
PROTOCOL_VERSION,
|
|
14
14
|
type ScreenRequest,
|
|
15
15
|
type ScreenResponse,
|
|
16
16
|
} from "@client-compose/protocol";
|
|
17
17
|
|
|
18
|
-
type OkEntry<P =
|
|
18
|
+
type OkEntry<P = unknown> = IslandEntry<P> & { status: "ok" };
|
|
19
19
|
type ErrorEntry = IslandEntry & { status: "error" };
|
|
20
20
|
|
|
21
|
-
export type IslandRenderer<R, P =
|
|
21
|
+
export type IslandRenderer<R, P = unknown> = (props: P, entry: OkEntry<P>) => R;
|
|
22
22
|
|
|
23
23
|
export class IslandRegistry<R> {
|
|
24
24
|
private renderers = new Map<string, Map<number, IslandRenderer<R>>>();
|
|
@@ -36,13 +36,17 @@ export class IslandRegistry<R> {
|
|
|
36
36
|
? [islandOrType, versionOrRender as number, maybeRender!]
|
|
37
37
|
: [islandOrType.type, islandOrType.version, versionOrRender as IslandRenderer<R, P>];
|
|
38
38
|
if (!this.renderers.has(type)) this.renderers.set(type, new Map());
|
|
39
|
-
|
|
39
|
+
// The registry hands each renderer the props of its own island type and version.
|
|
40
|
+
this.renderers.get(type)!.set(version, render as IslandRenderer<R>);
|
|
40
41
|
return this;
|
|
41
42
|
}
|
|
42
43
|
|
|
43
44
|
capabilities(): Capabilities {
|
|
44
45
|
return Object.fromEntries(
|
|
45
|
-
[...this.renderers].map(([type, versions]) => [
|
|
46
|
+
[...this.renderers].map(([type, versions]) => [
|
|
47
|
+
type,
|
|
48
|
+
[...versions.keys()].sort((a, b) => a - b),
|
|
49
|
+
]),
|
|
46
50
|
);
|
|
47
51
|
}
|
|
48
52
|
|
|
@@ -226,6 +230,11 @@ export async function callAction<I, R, E extends DeclaredError>(
|
|
|
226
230
|
input: I,
|
|
227
231
|
options: CallActionOptions,
|
|
228
232
|
): Promise<ActionResponse<R, E>> {
|
|
229
|
-
const request = buildActionRequest(
|
|
233
|
+
const request = buildActionRequest(
|
|
234
|
+
action,
|
|
235
|
+
input,
|
|
236
|
+
options.client,
|
|
237
|
+
options.idempotencyKey ?? newIdempotencyKey(),
|
|
238
|
+
);
|
|
230
239
|
return (await fetchAction(baseUrl, request, options.init, options.fetch)) as ActionResponse<R, E>;
|
|
231
240
|
}
|
package/src/proxy.ts
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
// Passing a browser's screen and action requests to the composer through your own server, e.g. a
|
|
2
|
+
// Next.js Server Action or a Remix action. The server adds what the browser mustn't hold (a session
|
|
3
|
+
// kept in an httpOnly cookie) and vouches for the client context, so the browser's is ignored.
|
|
4
|
+
//
|
|
5
|
+
// const proxy = createComposerProxy({
|
|
6
|
+
// baseUrl: COMPOSER_URL,
|
|
7
|
+
// client: async () => ({ platform: "web", appVersion, locale: await getLocale() }),
|
|
8
|
+
// headers: async () => ({ Authorization: `Bearer ${await getSessionToken()}` }),
|
|
9
|
+
// });
|
|
10
|
+
// export async function performAction(body: unknown) { return proxy.performAction(body); }
|
|
11
|
+
//
|
|
12
|
+
// Anyone can call a server endpoint with anything, so requests are checked before they're sent on.
|
|
13
|
+
// Errors come back as data, since frameworks often strip thrown errors' details on the way to the
|
|
14
|
+
// browser.
|
|
15
|
+
|
|
16
|
+
import {
|
|
17
|
+
type ActionRequest,
|
|
18
|
+
type ActionResponse,
|
|
19
|
+
type Capabilities,
|
|
20
|
+
type ClientContext,
|
|
21
|
+
PROTOCOL_VERSION,
|
|
22
|
+
type ScreenRequest,
|
|
23
|
+
type ScreenResponse,
|
|
24
|
+
} from "@client-compose/protocol";
|
|
25
|
+
|
|
26
|
+
import { ActionFetchError, fetchAction, fetchScreen, ScreenFetchError } from "./index.ts";
|
|
27
|
+
|
|
28
|
+
/** A screen request from the browser: the server sets the client context. */
|
|
29
|
+
export type ProxiedScreenRequest = Omit<ScreenRequest, "client">;
|
|
30
|
+
/** An action request from the browser: the server sets the client context. */
|
|
31
|
+
export type ProxiedActionRequest = Omit<ActionRequest, "client">;
|
|
32
|
+
|
|
33
|
+
/** The composer's response, or the status and body it failed with. */
|
|
34
|
+
export type ProxyResult<T> =
|
|
35
|
+
{ ok: true; response: T } | { ok: false; status: number; body: string };
|
|
36
|
+
|
|
37
|
+
export interface ComposerProxyOptions {
|
|
38
|
+
/** Composer base URL. */
|
|
39
|
+
baseUrl: string;
|
|
40
|
+
/** The client context for each request, e.g. with the locale from the request's cookies. */
|
|
41
|
+
client: () => ClientContext | Promise<ClientContext>;
|
|
42
|
+
/** Extra headers for each request, e.g. `Authorization` from a session cookie. */
|
|
43
|
+
headers?: () => Record<string, string> | Promise<Record<string, string>>;
|
|
44
|
+
/** Extra options for every request, e.g. `{ cache: "no-store" }`. */
|
|
45
|
+
requestInit?: RequestInit & Record<string, unknown>;
|
|
46
|
+
/** Custom fetch implementation. Defaults to the global `fetch`. */
|
|
47
|
+
fetch?: typeof fetch;
|
|
48
|
+
/**
|
|
49
|
+
* Sees each action's response (ok or a declared error) before the browser does, and returns
|
|
50
|
+
* what the browser gets: e.g. keep a new session in a cookie, and take its token out.
|
|
51
|
+
*/
|
|
52
|
+
onActionResponse?: (
|
|
53
|
+
request: ActionRequest,
|
|
54
|
+
response: ActionResponse,
|
|
55
|
+
) => ActionResponse | Promise<ActionResponse>;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export interface ComposerProxy {
|
|
59
|
+
/** Checks a browser's action request, then performs it with the server's context and headers. */
|
|
60
|
+
performAction(body: unknown): Promise<ProxyResult<ActionResponse>>;
|
|
61
|
+
/** Checks a browser's screen request, then loads it with the server's context and headers. */
|
|
62
|
+
loadScreen(body: unknown): Promise<ProxyResult<ScreenResponse>>;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const isRecord = (value: unknown): value is Record<string, unknown> =>
|
|
66
|
+
typeof value === "object" && value !== null && !Array.isArray(value);
|
|
67
|
+
|
|
68
|
+
function isCapabilities(value: unknown): value is Capabilities {
|
|
69
|
+
return (
|
|
70
|
+
isRecord(value) &&
|
|
71
|
+
Object.values(value).every(
|
|
72
|
+
(versions) => Array.isArray(versions) && versions.every((v) => typeof v === "number"),
|
|
73
|
+
)
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function isStringRecord(value: unknown): value is Record<string, string> {
|
|
78
|
+
return isRecord(value) && Object.values(value).every((v) => typeof v === "string");
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function isProxiedScreenRequest(value: unknown): value is ProxiedScreenRequest {
|
|
82
|
+
return (
|
|
83
|
+
isRecord(value) &&
|
|
84
|
+
value.protocol === PROTOCOL_VERSION &&
|
|
85
|
+
typeof value.screen === "string" &&
|
|
86
|
+
(value.params === undefined || isStringRecord(value.params)) &&
|
|
87
|
+
isCapabilities(value.islands)
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function isProxiedActionRequest(value: unknown): value is ProxiedActionRequest {
|
|
92
|
+
return (
|
|
93
|
+
isRecord(value) &&
|
|
94
|
+
value.protocol === PROTOCOL_VERSION &&
|
|
95
|
+
typeof value.action === "string" &&
|
|
96
|
+
typeof value.version === "number" &&
|
|
97
|
+
"input" in value &&
|
|
98
|
+
(value.idempotencyKey === undefined || typeof value.idempotencyKey === "string")
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** What the composer itself answers to a malformed request. */
|
|
103
|
+
const BAD_REQUEST = {
|
|
104
|
+
ok: false,
|
|
105
|
+
status: 400,
|
|
106
|
+
body: JSON.stringify({ protocol: PROTOCOL_VERSION, error: { code: "bad_request" } }),
|
|
107
|
+
} as const;
|
|
108
|
+
|
|
109
|
+
export function createComposerProxy(options: ComposerProxyOptions): ComposerProxy {
|
|
110
|
+
async function init(): Promise<RequestInit> {
|
|
111
|
+
const headers = (await options.headers?.()) ?? {};
|
|
112
|
+
return { ...options.requestInit, headers: { ...options.requestInit?.headers, ...headers } };
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
async function performAction(body: unknown): Promise<ProxyResult<ActionResponse>> {
|
|
116
|
+
if (!isProxiedActionRequest(body)) return BAD_REQUEST;
|
|
117
|
+
const request: ActionRequest = { ...body, client: await options.client() };
|
|
118
|
+
try {
|
|
119
|
+
const response = await fetchAction(options.baseUrl, request, await init(), options.fetch);
|
|
120
|
+
return {
|
|
121
|
+
ok: true,
|
|
122
|
+
response: (await options.onActionResponse?.(request, response)) ?? response,
|
|
123
|
+
};
|
|
124
|
+
} catch (error) {
|
|
125
|
+
if (error instanceof ActionFetchError)
|
|
126
|
+
return { ok: false, status: error.status, body: error.body };
|
|
127
|
+
throw error;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
async function loadScreen(body: unknown): Promise<ProxyResult<ScreenResponse>> {
|
|
132
|
+
if (!isProxiedScreenRequest(body)) return BAD_REQUEST;
|
|
133
|
+
const request: ScreenRequest = { ...body, client: await options.client() };
|
|
134
|
+
try {
|
|
135
|
+
return {
|
|
136
|
+
ok: true,
|
|
137
|
+
response: await fetchScreen(options.baseUrl, request, await init(), options.fetch),
|
|
138
|
+
};
|
|
139
|
+
} catch (error) {
|
|
140
|
+
if (error instanceof ScreenFetchError)
|
|
141
|
+
return { ok: false, status: error.status, body: error.body };
|
|
142
|
+
throw error;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return { performAction, loadScreen };
|
|
147
|
+
}
|
package/src/testing.ts
CHANGED
|
@@ -7,9 +7,10 @@
|
|
|
7
7
|
// actions: { follow: () => actionOk(FollowUserV1, { followers: 8 }) },
|
|
8
8
|
// });
|
|
9
9
|
// // pass gateway.fetch as the client's fetch, then assert on gateway.actionRequests()
|
|
10
|
+
//
|
|
11
|
+
// For code that uses the global fetch itself, `installFakeGateway` puts one in its place.
|
|
10
12
|
|
|
11
13
|
import {
|
|
12
|
-
PROTOCOL_VERSION,
|
|
13
14
|
type ActionDescriptor,
|
|
14
15
|
type ActionEffects,
|
|
15
16
|
type Capabilities,
|
|
@@ -18,6 +19,7 @@ import {
|
|
|
18
19
|
type IslandDescriptor,
|
|
19
20
|
type IslandEntry,
|
|
20
21
|
type IslandError,
|
|
22
|
+
PROTOCOL_VERSION,
|
|
21
23
|
type ScreenMeta,
|
|
22
24
|
} from "@client-compose/protocol";
|
|
23
25
|
|
|
@@ -75,7 +77,9 @@ const isRecord = (value: unknown): value is Record<string, unknown> =>
|
|
|
75
77
|
|
|
76
78
|
function strings(value: unknown): Record<string, string> {
|
|
77
79
|
if (!isRecord(value)) return {};
|
|
78
|
-
return Object.fromEntries(
|
|
80
|
+
return Object.fromEntries(
|
|
81
|
+
Object.entries(value).filter((e): e is [string, string] => typeof e[1] === "string"),
|
|
82
|
+
);
|
|
79
83
|
}
|
|
80
84
|
|
|
81
85
|
function capabilities(value: unknown): Capabilities {
|
|
@@ -89,7 +93,12 @@ function capabilities(value: unknown): Capabilities {
|
|
|
89
93
|
}
|
|
90
94
|
|
|
91
95
|
function clientContext(value: unknown): ClientContext | undefined {
|
|
92
|
-
if (
|
|
96
|
+
if (
|
|
97
|
+
!isRecord(value) ||
|
|
98
|
+
typeof value.platform !== "string" ||
|
|
99
|
+
typeof value.appVersion !== "string"
|
|
100
|
+
)
|
|
101
|
+
return undefined;
|
|
93
102
|
return { ...value, platform: value.platform, appVersion: value.appVersion } as ClientContext;
|
|
94
103
|
}
|
|
95
104
|
|
|
@@ -115,7 +124,8 @@ function headersOf(init: HeadersInit | undefined): Record<string, string> {
|
|
|
115
124
|
|
|
116
125
|
function asScreenRequest(request: RecordedRequest): RecordedScreenRequest | undefined {
|
|
117
126
|
const { body } = request;
|
|
118
|
-
if (!request.url.includes("/screens/") || !isRecord(body) || typeof body.screen !== "string")
|
|
127
|
+
if (!request.url.includes("/screens/") || !isRecord(body) || typeof body.screen !== "string")
|
|
128
|
+
return undefined;
|
|
119
129
|
return {
|
|
120
130
|
...request,
|
|
121
131
|
screen: body.screen,
|
|
@@ -127,7 +137,8 @@ function asScreenRequest(request: RecordedRequest): RecordedScreenRequest | unde
|
|
|
127
137
|
|
|
128
138
|
function asActionRequest(request: RecordedRequest): RecordedActionRequest | undefined {
|
|
129
139
|
const { body } = request;
|
|
130
|
-
if (!request.url.includes("/actions/") || !isRecord(body) || typeof body.action !== "string")
|
|
140
|
+
if (!request.url.includes("/actions/") || !isRecord(body) || typeof body.action !== "string")
|
|
141
|
+
return undefined;
|
|
131
142
|
return {
|
|
132
143
|
...request,
|
|
133
144
|
action: body.action,
|
|
@@ -165,7 +176,9 @@ export function createFakeGateway(handlers: FakeGatewayHandlers = {}): FakeGatew
|
|
|
165
176
|
const actionRequest = asActionRequest(request);
|
|
166
177
|
const actionHandler = actionRequest && handlers.actions?.[actionRequest.action];
|
|
167
178
|
if (actionRequest && actionHandler) return respond(actionHandler(actionRequest));
|
|
168
|
-
throw new Error(
|
|
179
|
+
throw new Error(
|
|
180
|
+
`fake gateway: no handler for ${request.method} ${request.url} ${JSON.stringify(request.body)}`,
|
|
181
|
+
);
|
|
169
182
|
}
|
|
170
183
|
return {
|
|
171
184
|
fetch: fakeFetch,
|
|
@@ -175,6 +188,27 @@ export function createFakeGateway(handlers: FakeGatewayHandlers = {}): FakeGatew
|
|
|
175
188
|
};
|
|
176
189
|
}
|
|
177
190
|
|
|
191
|
+
export interface InstalledFakeGateway extends FakeGateway {
|
|
192
|
+
/** Puts back the `fetch` that was there before. */
|
|
193
|
+
restore(): void;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* A fake composer installed as the global `fetch`, for code that fetches by itself (e.g. a whole
|
|
198
|
+
* app rendered in a test). Call `restore()` after the test.
|
|
199
|
+
*/
|
|
200
|
+
export function installFakeGateway(handlers: FakeGatewayHandlers = {}): InstalledFakeGateway {
|
|
201
|
+
const gateway = createFakeGateway(handlers);
|
|
202
|
+
const original = globalThis.fetch;
|
|
203
|
+
globalThis.fetch = gateway.fetch;
|
|
204
|
+
return {
|
|
205
|
+
...gateway,
|
|
206
|
+
restore: () => {
|
|
207
|
+
globalThis.fetch = original;
|
|
208
|
+
},
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
|
|
178
212
|
// ---- Responses ----
|
|
179
213
|
|
|
180
214
|
/** An island in a screen response, typed by its generated descriptor. */
|
|
@@ -183,13 +217,24 @@ export function island<P>(key: string, descriptor: IslandDescriptor<P>, props: P
|
|
|
183
217
|
}
|
|
184
218
|
|
|
185
219
|
/** An island that failed on the composer (spec §3.4). */
|
|
186
|
-
export function islandError(
|
|
220
|
+
export function islandError(
|
|
221
|
+
key: string,
|
|
222
|
+
descriptor: IslandDescriptor<unknown>,
|
|
223
|
+
error: IslandError,
|
|
224
|
+
): IslandEntry {
|
|
187
225
|
return { key, type: descriptor.type, version: descriptor.version, status: "error", error };
|
|
188
226
|
}
|
|
189
227
|
|
|
190
228
|
/** A screen response. */
|
|
191
|
-
export function screen(
|
|
192
|
-
|
|
229
|
+
export function screen(
|
|
230
|
+
id: string,
|
|
231
|
+
islands: IslandEntry<unknown>[],
|
|
232
|
+
meta?: ScreenMeta,
|
|
233
|
+
): FakeResponse {
|
|
234
|
+
return {
|
|
235
|
+
status: 200,
|
|
236
|
+
body: { protocol: PROTOCOL_VERSION, screen: id, islands, ...(meta && { meta }) },
|
|
237
|
+
};
|
|
193
238
|
}
|
|
194
239
|
|
|
195
240
|
/** A screen request that fails, e.g. `screenError(404, "not_found")` or `screenError(401, "unauthenticated")`. */
|
|
@@ -208,7 +253,10 @@ export function actionOk<I, R, E extends DeclaredError>(
|
|
|
208
253
|
}
|
|
209
254
|
|
|
210
255
|
/** An action that ended with one of its declared errors. */
|
|
211
|
-
export function actionError<I, R, E extends DeclaredError>(
|
|
256
|
+
export function actionError<I, R, E extends DeclaredError>(
|
|
257
|
+
action: ActionDescriptor<I, R, E>,
|
|
258
|
+
error: E,
|
|
259
|
+
): FakeResponse {
|
|
212
260
|
const base = { protocol: PROTOCOL_VERSION, action: action.action, version: action.version };
|
|
213
261
|
return { status: 200, body: { ...base, status: "error", error } };
|
|
214
262
|
}
|