@decocms/start 0.37.3 → 0.39.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/package.json +2 -1
- package/scripts/analyze-traces.mjs +1117 -0
- package/src/admin/index.ts +2 -0
- package/src/admin/invoke.ts +53 -5
- package/src/admin/setup.ts +7 -1
- package/src/apps/autoconfig.ts +50 -72
- package/src/sdk/invoke.ts +123 -12
- package/src/sdk/requestContext.ts +42 -0
- package/src/sdk/setupApps.ts +211 -0
- package/src/sdk/workerEntry.ts +6 -0
package/src/admin/index.ts
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
export { corsHeaders, isAdminOrLocalhost, registerAdminOrigin, registerAdminOrigins } from "./cors";
|
|
2
2
|
export { handleDecofileRead, handleDecofileReload } from "./decofile";
|
|
3
3
|
export {
|
|
4
|
+
clearInvokeHandlers,
|
|
4
5
|
handleInvoke,
|
|
5
6
|
type InvokeAction,
|
|
6
7
|
type InvokeLoader,
|
|
8
|
+
registerInvokeHandlers,
|
|
7
9
|
setInvokeActions,
|
|
8
10
|
setInvokeLoaders,
|
|
9
11
|
} from "./invoke";
|
package/src/admin/invoke.ts
CHANGED
|
@@ -7,11 +7,21 @@
|
|
|
7
7
|
* - FormData parsing for file uploads and form submissions
|
|
8
8
|
* - `?select=field1,field2` to pick fields from the result
|
|
9
9
|
* - Resolves __resolveType in batch payloads
|
|
10
|
+
*
|
|
11
|
+
* Handlers can write to `RequestContext.responseHeaders` to forward
|
|
12
|
+
* headers (e.g., Set-Cookie from VTEX checkout). The invoke endpoint
|
|
13
|
+
* copies those headers into the final HTTP Response.
|
|
10
14
|
*/
|
|
11
15
|
|
|
16
|
+
import { RequestContext } from "../sdk/requestContext";
|
|
17
|
+
|
|
12
18
|
export type InvokeLoader = (props: any, request: Request) => Promise<any>;
|
|
13
19
|
export type InvokeAction = (props: any, request: Request) => Promise<any>;
|
|
14
20
|
|
|
21
|
+
// Additive handler registry — registerInvokeHandlers() adds to this map.
|
|
22
|
+
const handlerRegistry = new Map<string, InvokeLoader | InvokeAction>();
|
|
23
|
+
|
|
24
|
+
// Legacy getter-based registries (backward compat for setInvokeLoaders/Actions).
|
|
15
25
|
let getRegisteredLoaders: () => Record<string, InvokeLoader> = () => ({});
|
|
16
26
|
let getRegisteredActions: () => Record<string, InvokeAction> = () => ({});
|
|
17
27
|
|
|
@@ -23,6 +33,29 @@ export function setInvokeActions(getter: () => Record<string, InvokeAction>) {
|
|
|
23
33
|
getRegisteredActions = getter;
|
|
24
34
|
}
|
|
25
35
|
|
|
36
|
+
/**
|
|
37
|
+
* Additive handler registration — adds handlers without replacing existing ones.
|
|
38
|
+
* First registration wins: if a key already exists, it is NOT overwritten.
|
|
39
|
+
* Use this for automatic manifest-based registration from setupApps().
|
|
40
|
+
*/
|
|
41
|
+
export function registerInvokeHandlers(
|
|
42
|
+
handlers: Record<string, InvokeLoader | InvokeAction>,
|
|
43
|
+
): void {
|
|
44
|
+
for (const [key, handler] of Object.entries(handlers)) {
|
|
45
|
+
if (!handlerRegistry.has(key)) {
|
|
46
|
+
handlerRegistry.set(key, handler);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Clear all registered invoke handlers.
|
|
53
|
+
* Called by setupApps() before re-registering on hot-reload.
|
|
54
|
+
*/
|
|
55
|
+
export function clearInvokeHandlers(): void {
|
|
56
|
+
handlerRegistry.clear();
|
|
57
|
+
}
|
|
58
|
+
|
|
26
59
|
const JSON_HEADERS = { "Content-Type": "application/json" } as const;
|
|
27
60
|
|
|
28
61
|
const isDev =
|
|
@@ -100,6 +133,11 @@ async function parseBody(request: Request): Promise<any> {
|
|
|
100
133
|
function findHandler(
|
|
101
134
|
key: string,
|
|
102
135
|
): { handler: InvokeLoader | InvokeAction; type: "loader" | "action" } | null {
|
|
136
|
+
// 1. Check additive registry first (from registerInvokeHandlers / setupApps)
|
|
137
|
+
const registered = handlerRegistry.get(key);
|
|
138
|
+
if (registered) return { handler: registered, type: "action" };
|
|
139
|
+
|
|
140
|
+
// 2. Fall back to legacy getter-based registries
|
|
103
141
|
const loaders = getRegisteredLoaders();
|
|
104
142
|
if (loaders[key]) return { handler: loaders[key], type: "loader" };
|
|
105
143
|
|
|
@@ -126,15 +164,25 @@ export async function handleInvoke(request: Request): Promise<Response> {
|
|
|
126
164
|
|
|
127
165
|
try {
|
|
128
166
|
const result = await found.handler(body, request);
|
|
129
|
-
// Response passthrough: if the
|
|
130
|
-
// forward it as-is (preserving headers like Set-Cookie).
|
|
131
|
-
// deco-cx/deco's invokeToHttpResponse behavior where auth loaders return
|
|
132
|
-
// Response objects with Set-Cookie headers for HttpOnly cookies.
|
|
167
|
+
// Response passthrough: if the handler returns a Response object,
|
|
168
|
+
// forward it as-is (preserving headers like Set-Cookie).
|
|
133
169
|
if (result instanceof Response) {
|
|
134
170
|
return result;
|
|
135
171
|
}
|
|
136
172
|
const filtered = selectFields(result, select);
|
|
137
|
-
|
|
173
|
+
const response = new Response(JSON.stringify(filtered), { status: 200, headers: JSON_HEADERS });
|
|
174
|
+
|
|
175
|
+
// Copy any headers that handlers wrote to RequestContext.responseHeaders
|
|
176
|
+
// (e.g., Set-Cookie from proxySetCookie). This mirrors deco-cx/deco's
|
|
177
|
+
// ctx.response.headers → HTTP Response forwarding.
|
|
178
|
+
const ctx = RequestContext.current;
|
|
179
|
+
if (ctx) {
|
|
180
|
+
for (const [key, value] of ctx.responseHeaders.entries()) {
|
|
181
|
+
response.headers.append(key, value);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
return response;
|
|
138
186
|
} catch (error) {
|
|
139
187
|
return errorResponse((error as Error).message, 500, error);
|
|
140
188
|
}
|
package/src/admin/setup.ts
CHANGED
|
@@ -8,7 +8,13 @@
|
|
|
8
8
|
* import from "@decocms/start/admin" instead.
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
export {
|
|
11
|
+
export {
|
|
12
|
+
type InvokeAction,
|
|
13
|
+
type InvokeLoader,
|
|
14
|
+
registerInvokeHandlers,
|
|
15
|
+
setInvokeActions,
|
|
16
|
+
setInvokeLoaders,
|
|
17
|
+
} from "./invoke";
|
|
12
18
|
export { setMetaData } from "./meta";
|
|
13
19
|
export {
|
|
14
20
|
type LoaderConfig,
|
package/src/apps/autoconfig.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Auto-configures known apps from CMS blocks.
|
|
2
|
+
* Auto-configures known apps from CMS blocks using AppModContract.
|
|
3
3
|
*
|
|
4
|
-
* Scans the decofile for known app block keys (e.g. "deco-resend")
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* Scans the decofile for known app block keys (e.g. "deco-vtex", "deco-resend")
|
|
5
|
+
* and calls each app's `configure()` function from its mod.ts.
|
|
6
|
+
* Then delegates to `setupApps()` for invoke handler registration, middleware, etc.
|
|
7
7
|
*
|
|
8
8
|
* Usage in setup.ts:
|
|
9
9
|
* import { autoconfigApps } from "@decocms/start/apps/autoconfig";
|
|
@@ -11,62 +11,60 @@
|
|
|
11
11
|
* await autoconfigApps(generatedBlocks);
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
|
-
import { setInvokeActions, type InvokeAction } from "../admin/invoke";
|
|
15
14
|
import { onChange } from "../cms/loader";
|
|
16
15
|
import { resolveSecret } from "../sdk/crypto";
|
|
16
|
+
import {
|
|
17
|
+
setupApps,
|
|
18
|
+
type AppDefinition,
|
|
19
|
+
type AppDefinitionWithHandlers,
|
|
20
|
+
} from "../sdk/setupApps";
|
|
17
21
|
|
|
18
22
|
// ---------------------------------------------------------------------------
|
|
19
|
-
// Known app block keys → dynamic import
|
|
23
|
+
// Known app block keys → dynamic import of their mod.ts
|
|
20
24
|
// ---------------------------------------------------------------------------
|
|
21
25
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
+
const APP_MODS: Record<string, () => Promise<any>> = {
|
|
27
|
+
"deco-vtex": () => import("@decocms/apps/vtex/mod" as string),
|
|
28
|
+
"deco-shopify": () => import("@decocms/apps/shopify/mod" as string),
|
|
29
|
+
"deco-resend": () => import("@decocms/apps/resend/mod" as string),
|
|
30
|
+
};
|
|
26
31
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
const [resendClient, resendActions] = await Promise.all([
|
|
31
|
-
import("@decocms/apps/resend/client" as string),
|
|
32
|
-
import("@decocms/apps/resend/actions/send" as string),
|
|
33
|
-
]);
|
|
34
|
-
const { configureResend } = resendClient as { configureResend: (cfg: any) => void };
|
|
35
|
-
const { sendEmail } = resendActions as { sendEmail: (props: any) => Promise<any> };
|
|
32
|
+
// ---------------------------------------------------------------------------
|
|
33
|
+
// Main
|
|
34
|
+
// ---------------------------------------------------------------------------
|
|
36
35
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
" Set DECO_CRYPTO_KEY to decrypt CMS secrets, or set RESEND_API_KEY as fallback.",
|
|
42
|
-
);
|
|
43
|
-
return {} as Record<string, InvokeAction>;
|
|
44
|
-
}
|
|
36
|
+
async function configureAllApps(
|
|
37
|
+
blocks: Record<string, unknown>,
|
|
38
|
+
): Promise<AppDefinitionWithHandlers[]> {
|
|
39
|
+
const apps: AppDefinitionWithHandlers[] = [];
|
|
45
40
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
});
|
|
41
|
+
for (const [blockKey, importMod] of Object.entries(APP_MODS)) {
|
|
42
|
+
const block = blocks[blockKey];
|
|
43
|
+
if (!block) continue;
|
|
44
|
+
|
|
45
|
+
try {
|
|
46
|
+
const mod = await importMod();
|
|
47
|
+
if (typeof mod.configure !== "function") continue;
|
|
54
48
|
|
|
55
|
-
const
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
49
|
+
const appDef: AppDefinition | null = await mod.configure(
|
|
50
|
+
block,
|
|
51
|
+
resolveSecret,
|
|
52
|
+
);
|
|
53
|
+
if (!appDef) continue;
|
|
54
|
+
|
|
55
|
+
// Attach explicit handlers from mod.ts (e.g. resend's pre-wrapped handlers)
|
|
56
|
+
const withHandlers: AppDefinitionWithHandlers = {
|
|
57
|
+
...appDef,
|
|
58
|
+
handlers: mod.handlers,
|
|
59
59
|
};
|
|
60
|
+
apps.push(withHandlers);
|
|
60
61
|
} catch {
|
|
61
|
-
//
|
|
62
|
-
return {};
|
|
62
|
+
// App not installed or configure failed — skip silently
|
|
63
63
|
}
|
|
64
|
-
}
|
|
65
|
-
};
|
|
64
|
+
}
|
|
66
65
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
// ---------------------------------------------------------------------------
|
|
66
|
+
return apps;
|
|
67
|
+
}
|
|
70
68
|
|
|
71
69
|
/**
|
|
72
70
|
* Auto-configure apps from CMS blocks.
|
|
@@ -75,37 +73,17 @@ const KNOWN_APPS: Record<string, AppAutoconfigurator> = {
|
|
|
75
73
|
export async function autoconfigApps(blocks: Record<string, unknown>) {
|
|
76
74
|
if (typeof document !== "undefined") return; // server-only
|
|
77
75
|
|
|
78
|
-
const
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
const block = blocks[blockKey];
|
|
82
|
-
if (!block) continue;
|
|
83
|
-
|
|
84
|
-
try {
|
|
85
|
-
const appActions = await configurator(block);
|
|
86
|
-
Object.assign(actions, appActions);
|
|
87
|
-
} catch (e) {
|
|
88
|
-
console.warn(`[autoconfig] ${blockKey}:`, e);
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
if (Object.keys(actions).length > 0) {
|
|
93
|
-
setInvokeActions(() => ({ ...actions }));
|
|
76
|
+
const apps = await configureAllApps(blocks);
|
|
77
|
+
if (apps.length > 0) {
|
|
78
|
+
await setupApps(apps);
|
|
94
79
|
}
|
|
95
80
|
|
|
96
81
|
// Re-configure on admin hot-reload
|
|
97
82
|
onChange(async (newBlocks) => {
|
|
98
83
|
if (typeof document !== "undefined") return;
|
|
99
|
-
const
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
if (!block) continue;
|
|
103
|
-
try {
|
|
104
|
-
Object.assign(updatedActions, await configurator(block));
|
|
105
|
-
} catch {}
|
|
106
|
-
}
|
|
107
|
-
if (Object.keys(updatedActions).length > 0) {
|
|
108
|
-
setInvokeActions(() => ({ ...updatedActions }));
|
|
84
|
+
const updatedApps = await configureAllApps(newBlocks);
|
|
85
|
+
if (updatedApps.length > 0) {
|
|
86
|
+
await setupApps(updatedApps);
|
|
109
87
|
}
|
|
110
88
|
});
|
|
111
89
|
}
|
package/src/sdk/invoke.ts
CHANGED
|
@@ -1,21 +1,22 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Typed invoke
|
|
2
|
+
* Typed invoke proxies for client-side RPC to deco loaders/actions.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* Two flavors:
|
|
5
|
+
*
|
|
6
|
+
* 1. `createInvokeProxy<T>()` — flat keys (e.g. `invoke["vtex/loaders/productList.ts"](props)`)
|
|
7
|
+
* 2. `createAppInvoke<T>()` — nested keys (e.g. `invoke.vtex.actions.checkout.addItemsToCart(props)`)
|
|
8
|
+
*
|
|
9
|
+
* Both POST to `/deco/invoke/{key}` under the hood.
|
|
6
10
|
*
|
|
7
11
|
* @example
|
|
8
12
|
* ```ts
|
|
9
|
-
* //
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
* "vtex/loaders/intelligentSearch/productDetailsPage.ts": (props: { slug: string }) => Promise<ProductDetailsPage>;
|
|
13
|
-
* };
|
|
13
|
+
* // Flat proxy (legacy):
|
|
14
|
+
* const invoke = createInvokeProxy<Loaders>();
|
|
15
|
+
* await invoke["vtex/loaders/productList.ts"]({ query: "shoes" });
|
|
14
16
|
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
* const products = await invoke["vtex/loaders/intelligentSearch/productList.ts"]({ query: "shoes" });
|
|
17
|
+
* // Nested proxy (recommended):
|
|
18
|
+
* const invoke = createAppInvoke();
|
|
19
|
+
* await invoke.vtex.actions.checkout.addItemsToCart({ orderFormId, orderItems });
|
|
19
20
|
* ```
|
|
20
21
|
*/
|
|
21
22
|
|
|
@@ -131,3 +132,113 @@ export function invokeQueryOptions<TResult = unknown>(
|
|
|
131
132
|
gcTime: options?.gcTime,
|
|
132
133
|
};
|
|
133
134
|
}
|
|
135
|
+
|
|
136
|
+
// ---------------------------------------------------------------------------
|
|
137
|
+
// Nested invoke proxy — createAppInvoke
|
|
138
|
+
// ---------------------------------------------------------------------------
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Converts flat slash-separated keys into a nested object type.
|
|
142
|
+
*
|
|
143
|
+
* @example
|
|
144
|
+
* ```ts
|
|
145
|
+
* type Map = {
|
|
146
|
+
* "vtex/actions/checkout/addItemsToCart": (props: CartInput) => Promise<OrderForm>;
|
|
147
|
+
* "vtex/loaders/catalog/getProduct": (props: { slug: string }) => Promise<Product>;
|
|
148
|
+
* };
|
|
149
|
+
* type Nested = NestedFromFlat<Map>;
|
|
150
|
+
* // { vtex: { actions: { checkout: { addItemsToCart: (props: CartInput) => Promise<OrderForm> } } } }
|
|
151
|
+
* ```
|
|
152
|
+
*/
|
|
153
|
+
type SplitFirst<S extends string> = S extends `${infer Head}/${infer Tail}`
|
|
154
|
+
? [Head, Tail]
|
|
155
|
+
: [S, never];
|
|
156
|
+
|
|
157
|
+
type BuildNested<Key extends string, Value> = SplitFirst<Key> extends [
|
|
158
|
+
infer H extends string,
|
|
159
|
+
infer T,
|
|
160
|
+
]
|
|
161
|
+
? T extends string
|
|
162
|
+
? { [K in H]: BuildNested<T, Value> }
|
|
163
|
+
: { [K in H]: Value }
|
|
164
|
+
: never;
|
|
165
|
+
|
|
166
|
+
type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends (
|
|
167
|
+
k: infer I,
|
|
168
|
+
) => void
|
|
169
|
+
? I
|
|
170
|
+
: never;
|
|
171
|
+
|
|
172
|
+
type DeepMerge<T> = T extends object
|
|
173
|
+
? { [K in keyof T]: DeepMerge<T[K]> }
|
|
174
|
+
: T;
|
|
175
|
+
|
|
176
|
+
export type NestedFromFlat<T extends Record<string, any>> = DeepMerge<
|
|
177
|
+
UnionToIntersection<
|
|
178
|
+
{ [K in keyof T & string]: BuildNested<K, T[K]> }[keyof T & string]
|
|
179
|
+
>
|
|
180
|
+
>;
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Creates a typed nested invoke proxy.
|
|
184
|
+
*
|
|
185
|
+
* Each property access accumulates path segments. When called as a function,
|
|
186
|
+
* the segments are joined with "/" and POSTed to `/deco/invoke/{key}`.
|
|
187
|
+
* If the primary key returns 404, a `.ts` suffix variant is tried.
|
|
188
|
+
*
|
|
189
|
+
* @example
|
|
190
|
+
* ```ts
|
|
191
|
+
* import { createAppInvoke } from "@decocms/start/sdk/invoke";
|
|
192
|
+
*
|
|
193
|
+
* // Untyped (any):
|
|
194
|
+
* const invoke = createAppInvoke();
|
|
195
|
+
* await invoke.vtex.actions.checkout.addItemsToCart({ orderFormId, orderItems });
|
|
196
|
+
*
|
|
197
|
+
* // Typed (with handler map):
|
|
198
|
+
* type Handlers = {
|
|
199
|
+
* "vtex/actions/checkout/addItemsToCart": (props: CartInput) => Promise<OrderForm>;
|
|
200
|
+
* };
|
|
201
|
+
* const invoke = createAppInvoke<Handlers>();
|
|
202
|
+
* await invoke.vtex.actions.checkout.addItemsToCart({ orderFormId, orderItems });
|
|
203
|
+
* ```
|
|
204
|
+
*/
|
|
205
|
+
export function createAppInvoke(basePath?: string): any;
|
|
206
|
+
export function createAppInvoke<T extends Record<string, any>>(basePath?: string): NestedFromFlat<T>;
|
|
207
|
+
export function createAppInvoke(
|
|
208
|
+
basePath = "/deco/invoke",
|
|
209
|
+
): any {
|
|
210
|
+
function buildProxy(path: string[]): any {
|
|
211
|
+
return new Proxy(
|
|
212
|
+
Object.assign(async (props: any) => {
|
|
213
|
+
const key = path.join("/");
|
|
214
|
+
for (const k of [key, `${key}.ts`]) {
|
|
215
|
+
const response = await fetch(`${basePath}/${k}`, {
|
|
216
|
+
method: "POST",
|
|
217
|
+
headers: { "Content-Type": "application/json" },
|
|
218
|
+
body: JSON.stringify(props ?? {}),
|
|
219
|
+
});
|
|
220
|
+
if (response.status === 404) { await response.body?.cancel(); continue; }
|
|
221
|
+
if (!response.ok) {
|
|
222
|
+
const error = await response.json().catch(() => ({ error: response.statusText }));
|
|
223
|
+
throw new Error(
|
|
224
|
+
`invoke(${k}) failed (${response.status}): ${(error as any).error || response.statusText}`,
|
|
225
|
+
);
|
|
226
|
+
}
|
|
227
|
+
return response.json();
|
|
228
|
+
}
|
|
229
|
+
throw new Error(`invoke(${key}) failed: handler not found`);
|
|
230
|
+
}, {}),
|
|
231
|
+
{
|
|
232
|
+
get(_target: any, prop: string | symbol) {
|
|
233
|
+
if (typeof prop === "symbol") return undefined;
|
|
234
|
+
if (prop === "then" || prop === "catch" || prop === "finally") {
|
|
235
|
+
return undefined;
|
|
236
|
+
}
|
|
237
|
+
return buildProxy([...path, prop]);
|
|
238
|
+
},
|
|
239
|
+
},
|
|
240
|
+
);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
return buildProxy([]);
|
|
244
|
+
}
|
|
@@ -44,6 +44,17 @@ export interface RequestContextData {
|
|
|
44
44
|
_isBot?: boolean;
|
|
45
45
|
/** Arbitrary bag for middleware to attach custom data. */
|
|
46
46
|
bag: Map<string, unknown>;
|
|
47
|
+
/**
|
|
48
|
+
* Outgoing response headers that handlers can write to.
|
|
49
|
+
* Invoke handlers (actions/loaders) use this to forward Set-Cookie
|
|
50
|
+
* and other headers from upstream APIs (e.g., VTEX checkout).
|
|
51
|
+
* The invoke HTTP handler copies these into the final Response.
|
|
52
|
+
*
|
|
53
|
+
* This mirrors deco-cx/deco's `ctx.response.headers` pattern where
|
|
54
|
+
* `proxySetCookie(apiResponse.headers, ctx.response.headers)` forwards
|
|
55
|
+
* cookies transparently.
|
|
56
|
+
*/
|
|
57
|
+
responseHeaders: Headers;
|
|
47
58
|
}
|
|
48
59
|
|
|
49
60
|
// -------------------------------------------------------------------------
|
|
@@ -87,6 +98,7 @@ export const RequestContext = {
|
|
|
87
98
|
signal: controller.signal,
|
|
88
99
|
startedAt: Date.now(),
|
|
89
100
|
bag: new Map(),
|
|
101
|
+
responseHeaders: new Headers(),
|
|
90
102
|
};
|
|
91
103
|
|
|
92
104
|
return storage.run(ctx, fn);
|
|
@@ -169,6 +181,17 @@ export const RequestContext = {
|
|
|
169
181
|
});
|
|
170
182
|
},
|
|
171
183
|
|
|
184
|
+
/**
|
|
185
|
+
* Outgoing response headers. Handlers write here; the invoke endpoint
|
|
186
|
+
* copies them into the HTTP Response (mirroring ctx.response.headers
|
|
187
|
+
* from deco-cx/deco).
|
|
188
|
+
*/
|
|
189
|
+
get responseHeaders(): Headers {
|
|
190
|
+
const ctx = storage.getStore();
|
|
191
|
+
if (!ctx) throw new Error("RequestContext.responseHeaders accessed outside a request scope");
|
|
192
|
+
return ctx.responseHeaders;
|
|
193
|
+
},
|
|
194
|
+
|
|
172
195
|
/**
|
|
173
196
|
* Get/set arbitrary values in the request bag.
|
|
174
197
|
* Useful for middleware to pass data to loaders.
|
|
@@ -182,4 +205,23 @@ export const RequestContext = {
|
|
|
182
205
|
const ctx = storage.getStore();
|
|
183
206
|
ctx?.bag.set(key, value);
|
|
184
207
|
},
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Get an app's state from the request bag.
|
|
211
|
+
* Apps register their state via `setupApps()` which injects it
|
|
212
|
+
* into the bag as `app:{name}:state` before each request.
|
|
213
|
+
*
|
|
214
|
+
* @example
|
|
215
|
+
* ```ts
|
|
216
|
+
* import { RequestContext } from "@decocms/start/sdk/requestContext";
|
|
217
|
+
* import type { VtexState } from "@decocms/apps/vtex/mod";
|
|
218
|
+
*
|
|
219
|
+
* const vtex = RequestContext.getAppState<VtexState>("vtex");
|
|
220
|
+
* if (vtex) console.log(vtex.config.account);
|
|
221
|
+
* ```
|
|
222
|
+
*/
|
|
223
|
+
getAppState<T>(appName: string): T | undefined {
|
|
224
|
+
const ctx = storage.getStore();
|
|
225
|
+
return ctx?.bag.get(`app:${appName}:state`) as T | undefined;
|
|
226
|
+
},
|
|
185
227
|
};
|