@decocms/blocks-admin 7.28.1 → 8.0.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 -2
- package/src/admin/invoke.test.ts +79 -0
- package/src/admin/invoke.ts +21 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@decocms/blocks-admin",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "8.0.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Deco admin protocol: /live/_meta, /.decofile, /deco/invoke, /deco/render, plus admin-facing site setup and commerce-app auto-configuration",
|
|
6
6
|
"repository": {
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
"lint:unused": "knip"
|
|
26
26
|
},
|
|
27
27
|
"dependencies": {
|
|
28
|
-
"@decocms/blocks": "
|
|
28
|
+
"@decocms/blocks": "8.0.0"
|
|
29
29
|
},
|
|
30
30
|
"peerDependencies": {
|
|
31
31
|
"react": "^19.0.0",
|
package/src/admin/invoke.test.ts
CHANGED
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
17
|
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
|
18
|
+
import { clearDraftCache, loadBlocks, setBlocks } from "@decocms/blocks/cms";
|
|
18
19
|
import { RequestContext } from "@decocms/blocks/sdk/requestContext";
|
|
19
20
|
import {
|
|
20
21
|
clearInvokeHandlers,
|
|
@@ -139,3 +140,81 @@ describe("handleInvoke — Set-Cookie propagation (batch)", () => {
|
|
|
139
140
|
expect(cookies).toContain(COOKIE_C);
|
|
140
141
|
});
|
|
141
142
|
});
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Draft binding: /deco/invoke is a separate request from the page render, so
|
|
146
|
+
* the page's request-scoped draft never reaches it. handleInvoke must
|
|
147
|
+
* re-resolve the draft from the request it received and bind it, or a
|
|
148
|
+
* client-fetched (lazy) section resolves its loaders against PUBLISHED blocks.
|
|
149
|
+
*/
|
|
150
|
+
describe("handleInvoke — draft binding", () => {
|
|
151
|
+
const origFetch = globalThis.fetch;
|
|
152
|
+
|
|
153
|
+
beforeEach(() => {
|
|
154
|
+
clearInvokeHandlers();
|
|
155
|
+
clearDraftCache();
|
|
156
|
+
setBlocks({ "site/x": { value: "published" } });
|
|
157
|
+
registerInvokeHandlers({
|
|
158
|
+
"site/loaders/x.ts": async () => (loadBlocks() as Record<string, any>)["site/x"],
|
|
159
|
+
});
|
|
160
|
+
});
|
|
161
|
+
afterEach(() => {
|
|
162
|
+
clearInvokeHandlers();
|
|
163
|
+
clearDraftCache();
|
|
164
|
+
globalThis.fetch = origFetch;
|
|
165
|
+
delete process.env.DECO_ALLOWED_PREVIEW_HOSTS;
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
function invokeReq(headers: Record<string, string>): Request {
|
|
169
|
+
return new Request("https://preview.example/deco/invoke/site/loaders/x.ts", {
|
|
170
|
+
method: "POST",
|
|
171
|
+
headers: { "content-type": "application/json", ...headers },
|
|
172
|
+
body: "{}",
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
it("resolves loaders against the DRAFT when the request carries an allowed draft cookie", async () => {
|
|
177
|
+
process.env.DECO_ALLOWED_PREVIEW_HOSTS = "preview.example";
|
|
178
|
+
globalThis.fetch = (async () =>
|
|
179
|
+
new Response(JSON.stringify({ "site/x": { value: "draft" } }), {
|
|
180
|
+
headers: { "content-type": "application/json" },
|
|
181
|
+
})) as unknown as typeof fetch;
|
|
182
|
+
|
|
183
|
+
const res = await handleInvoke(
|
|
184
|
+
invokeReq({
|
|
185
|
+
"x-forwarded-host": "preview.example",
|
|
186
|
+
cookie: "__deco_draft=abc.preview-studio.decocms.com@vX",
|
|
187
|
+
}),
|
|
188
|
+
);
|
|
189
|
+
expect(await res.json()).toEqual({ value: "draft" });
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it("resolves against PUBLISHED blocks when no draft cookie is present — no network", async () => {
|
|
193
|
+
process.env.DECO_ALLOWED_PREVIEW_HOSTS = "preview.example";
|
|
194
|
+
let called = false;
|
|
195
|
+
globalThis.fetch = (async () => {
|
|
196
|
+
called = true;
|
|
197
|
+
return new Response("{}");
|
|
198
|
+
}) as unknown as typeof fetch;
|
|
199
|
+
|
|
200
|
+
const res = await handleInvoke(invokeReq({ "x-forwarded-host": "preview.example" }));
|
|
201
|
+
expect(await res.json()).toEqual({ value: "published" });
|
|
202
|
+
expect(called).toBe(false);
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
it("ignores a draft cookie on a non-allowed host (production stays published)", async () => {
|
|
206
|
+
process.env.DECO_ALLOWED_PREVIEW_HOSTS = "preview.example";
|
|
207
|
+
globalThis.fetch = (async () =>
|
|
208
|
+
new Response(JSON.stringify({ "site/x": { value: "draft" } }), {
|
|
209
|
+
headers: { "content-type": "application/json" },
|
|
210
|
+
})) as unknown as typeof fetch;
|
|
211
|
+
|
|
212
|
+
const res = await handleInvoke(
|
|
213
|
+
invokeReq({
|
|
214
|
+
"x-forwarded-host": "prod.example",
|
|
215
|
+
cookie: "__deco_draft=abc.preview-studio.decocms.com@vX",
|
|
216
|
+
}),
|
|
217
|
+
);
|
|
218
|
+
expect(await res.json()).toEqual({ value: "published" });
|
|
219
|
+
});
|
|
220
|
+
});
|
package/src/admin/invoke.ts
CHANGED
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
* copies those headers into the final HTTP Response.
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
|
+
import { resolveDraftForRequest, withBlocksOverride } from "@decocms/blocks/cms";
|
|
16
17
|
import { RequestContext } from "@decocms/blocks/sdk/requestContext";
|
|
17
18
|
|
|
18
19
|
export type InvokeLoader = (props: any, request: Request) => Promise<any>;
|
|
@@ -213,7 +214,27 @@ function warnMissingHandlerOnce(key: string): void {
|
|
|
213
214
|
console.warn(`[invoke] ${unknownHandlerMessage(key)}`);
|
|
214
215
|
}
|
|
215
216
|
|
|
217
|
+
/**
|
|
218
|
+
* Bind a draft, if the request carries one, for the whole invoke resolution.
|
|
219
|
+
*
|
|
220
|
+
* A loader/action reaches `loadBlocks()` while resolving its `__resolveType`
|
|
221
|
+
* refs; without this, `/deco/invoke` — a separate request from the page render,
|
|
222
|
+
* so the page's request-scoped draft binding never reaches it — would resolve
|
|
223
|
+
* against PUBLISHED blocks and hand a reviewer stale content in any
|
|
224
|
+
* client-fetched (lazy/on-scroll) or self-fetched section. `withBlocksOverride`
|
|
225
|
+
* uses AsyncLocalStorage, so each concurrent invoke gets its own override with
|
|
226
|
+
* no cross-request bleed (the same isolation the layout-cache race taught us to
|
|
227
|
+
* keep). Inert unless the draft gate passes — no draft, no wrap, published path
|
|
228
|
+
* untouched.
|
|
229
|
+
*/
|
|
216
230
|
export async function handleInvoke(request: Request): Promise<Response> {
|
|
231
|
+
const draftBlocks = await resolveDraftForRequest(request);
|
|
232
|
+
return draftBlocks
|
|
233
|
+
? withBlocksOverride(draftBlocks, () => dispatchInvoke(request))
|
|
234
|
+
: dispatchInvoke(request);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
async function dispatchInvoke(request: Request): Promise<Response> {
|
|
217
238
|
const url = new URL(request.url);
|
|
218
239
|
const pathParts = url.pathname.split("/deco/invoke/");
|
|
219
240
|
const invokeKey = pathParts[1] || "";
|