@cmssy/core 10.6.1 → 10.8.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/dist/testing.cjs +175 -0
- package/dist/testing.d.cts +20 -2
- package/dist/testing.d.ts +20 -2
- package/dist/testing.js +175 -0
- package/package.json +1 -1
package/dist/testing.cjs
CHANGED
|
@@ -1,12 +1,181 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
// src/data/http.ts
|
|
4
|
+
var CmssyRequestError = class extends Error {
|
|
5
|
+
status;
|
|
6
|
+
/**
|
|
7
|
+
* How long the server asked us to wait, in ms, when it said so. Present on a
|
|
8
|
+
* 429 that carried `Retry-After`; a caller can serve stale content for that
|
|
9
|
+
* long instead of guessing.
|
|
10
|
+
*/
|
|
11
|
+
retryAfterMs;
|
|
12
|
+
constructor(message, status, retryAfterMs2) {
|
|
13
|
+
super(message);
|
|
14
|
+
this.name = "CmssyRequestError";
|
|
15
|
+
this.status = status;
|
|
16
|
+
if (retryAfterMs2 !== void 0) this.retryAfterMs = retryAfterMs2;
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
var DEFAULT_RETRY_STATUSES = [429, 503];
|
|
20
|
+
function retryAfterMs(response) {
|
|
21
|
+
const raw = response.headers?.get("retry-after");
|
|
22
|
+
if (!raw) return null;
|
|
23
|
+
const seconds = Number(raw);
|
|
24
|
+
if (Number.isFinite(seconds)) return Math.max(0, seconds * 1e3);
|
|
25
|
+
const date = Date.parse(raw);
|
|
26
|
+
if (Number.isFinite(date)) return Math.max(0, date - Date.now());
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
function sleep(ms, signal) {
|
|
30
|
+
return new Promise((resolve, reject) => {
|
|
31
|
+
if (signal?.aborted) {
|
|
32
|
+
reject(new Error("cmssy: request aborted"));
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
const timer = setTimeout(() => {
|
|
36
|
+
signal?.removeEventListener("abort", onAbort);
|
|
37
|
+
resolve();
|
|
38
|
+
}, ms);
|
|
39
|
+
function onAbort() {
|
|
40
|
+
clearTimeout(timer);
|
|
41
|
+
reject(new Error("cmssy: request aborted"));
|
|
42
|
+
}
|
|
43
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
async function fetchWithRetry(doFetch, url, init, retry) {
|
|
47
|
+
if (retry === false || retry === void 0) {
|
|
48
|
+
return doFetch(url, init);
|
|
49
|
+
}
|
|
50
|
+
const maxRetries = retry.maxRetries ?? 3;
|
|
51
|
+
const baseDelayMs = retry.baseDelayMs ?? 300;
|
|
52
|
+
const maxDelayMs = retry.maxDelayMs ?? 3e3;
|
|
53
|
+
const maxRetryAfterMs = retry.maxRetryAfterMs ?? 1e4;
|
|
54
|
+
const retryStatuses = retry.retryStatuses ?? DEFAULT_RETRY_STATUSES;
|
|
55
|
+
let response = await doFetch(url, init);
|
|
56
|
+
for (let attempt = 0; attempt < maxRetries; attempt++) {
|
|
57
|
+
if (response.ok || !retryStatuses.includes(response.status)) {
|
|
58
|
+
return response;
|
|
59
|
+
}
|
|
60
|
+
const asked = retryAfterMs(response);
|
|
61
|
+
if (asked !== null && asked > maxRetryAfterMs) return response;
|
|
62
|
+
const backoff = baseDelayMs * 2 ** attempt;
|
|
63
|
+
const wait = asked !== null ? asked : Math.min(backoff, maxDelayMs);
|
|
64
|
+
await sleep(wait, init.signal);
|
|
65
|
+
response = await doFetch(url, init);
|
|
66
|
+
}
|
|
67
|
+
return response;
|
|
68
|
+
}
|
|
69
|
+
async function postGraphql(url, query, variables, options) {
|
|
70
|
+
const doFetch = options.fetch ?? globalThis.fetch;
|
|
71
|
+
if (typeof doFetch !== "function") {
|
|
72
|
+
throw new Error(
|
|
73
|
+
"cmssy: no fetch implementation available - pass options.fetch"
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
const response = await fetchWithRetry(
|
|
77
|
+
doFetch,
|
|
78
|
+
url,
|
|
79
|
+
{
|
|
80
|
+
method: "POST",
|
|
81
|
+
headers: { "content-type": "application/json", ...options.headers },
|
|
82
|
+
body: JSON.stringify({ query, variables }),
|
|
83
|
+
signal: options.signal
|
|
84
|
+
},
|
|
85
|
+
options.retry
|
|
86
|
+
);
|
|
87
|
+
if (!response.ok) {
|
|
88
|
+
let detail = "";
|
|
89
|
+
try {
|
|
90
|
+
const body = await response.json();
|
|
91
|
+
if (body.errors && body.errors.length > 0) {
|
|
92
|
+
detail = ` - ${body.errors.map((error) => error.message ?? "GraphQL error").join("; ")}`;
|
|
93
|
+
}
|
|
94
|
+
} catch {
|
|
95
|
+
detail = "";
|
|
96
|
+
}
|
|
97
|
+
const asked = retryAfterMs(response);
|
|
98
|
+
const wait = asked !== null ? ` - retry after ${Math.ceil(asked / 1e3)}s` : "";
|
|
99
|
+
throw new CmssyRequestError(
|
|
100
|
+
`cmssy: ${options.label} failed (${response.status})${detail}${wait}`,
|
|
101
|
+
response.status,
|
|
102
|
+
asked ?? void 0
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
let json;
|
|
106
|
+
try {
|
|
107
|
+
json = await response.json();
|
|
108
|
+
} catch {
|
|
109
|
+
throw new Error(`cmssy: invalid JSON response from the ${options.label}`);
|
|
110
|
+
}
|
|
111
|
+
if (json.errors && json.errors.length > 0) {
|
|
112
|
+
const message = json.errors.map((error) => error.message ?? "GraphQL error").join("; ");
|
|
113
|
+
throw new Error(`cmssy: ${options.label} error - ${message}`);
|
|
114
|
+
}
|
|
115
|
+
return json.data;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// src/content/content-client.ts
|
|
119
|
+
var DEFAULT_CMSSY_API_URL = "https://api.cmssy.io/graphql";
|
|
120
|
+
function resolveApiUrl(apiUrl) {
|
|
121
|
+
const explicit = apiUrl?.trim();
|
|
122
|
+
if (explicit) return explicit;
|
|
123
|
+
const env = globalThis.process?.env;
|
|
124
|
+
const fromEnv = env?.CMSSY_API_URL?.trim() ?? "";
|
|
125
|
+
return fromEnv.length > 0 ? fromEnv : DEFAULT_CMSSY_API_URL;
|
|
126
|
+
}
|
|
127
|
+
function resolvePublicUrl(config) {
|
|
128
|
+
const base = resolveApiUrl(config.apiUrl).replace(/\/graphql\/?$/, "");
|
|
129
|
+
return `${base}/public/${config.org}/${config.workspaceSlug}/graphql`;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// src/data/graphql-request.ts
|
|
133
|
+
async function graphqlRequest(config, query, variables, options = {}, label = "request") {
|
|
134
|
+
const url = options.public ? resolvePublicUrl(config) : resolveApiUrl(config.apiUrl);
|
|
135
|
+
return postGraphql(url, query, variables, {
|
|
136
|
+
fetch: options.fetch,
|
|
137
|
+
signal: options.signal,
|
|
138
|
+
headers: options.headers,
|
|
139
|
+
retry: options.retry,
|
|
140
|
+
label
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
|
|
3
144
|
// src/testing/edit-smoke.ts
|
|
4
145
|
var EDITOR_MARKER = /data-cmssy-editor/;
|
|
5
146
|
var SERVER_LAYOUT_BLOCKS = /<header|<footer/;
|
|
147
|
+
var EDITABLE_LAYOUT_SLOT = /data-cmssy-layout-slot/;
|
|
6
148
|
async function html(url) {
|
|
7
149
|
const response = await fetch(url, { redirect: "manual" });
|
|
8
150
|
return { status: response.status, body: await response.text() };
|
|
9
151
|
}
|
|
152
|
+
var LAYOUT_BLOCKS_QUERY = `query CmssySmokeLayouts($workspaceSlug: String!, $pageSlug: String!) {
|
|
153
|
+
public {
|
|
154
|
+
page {
|
|
155
|
+
layouts(workspaceSlug: $workspaceSlug, pageSlug: $pageSlug) {
|
|
156
|
+
position
|
|
157
|
+
blocks { id isActive }
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}`;
|
|
162
|
+
async function workspaceHasLayoutBlocks(workspace, pageSlug) {
|
|
163
|
+
try {
|
|
164
|
+
const data = await graphqlRequest(
|
|
165
|
+
workspace,
|
|
166
|
+
LAYOUT_BLOCKS_QUERY,
|
|
167
|
+
{ workspaceSlug: workspace.workspaceSlug, pageSlug },
|
|
168
|
+
{ public: true, retry: {} },
|
|
169
|
+
"edit smoke: layout blocks"
|
|
170
|
+
);
|
|
171
|
+
const groups = data.public?.page?.layouts ?? [];
|
|
172
|
+
return groups.some(
|
|
173
|
+
(group) => (group.blocks ?? []).some((block) => block.isActive !== false)
|
|
174
|
+
);
|
|
175
|
+
} catch {
|
|
176
|
+
return null;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
10
179
|
async function checkCmssyEditMode(options) {
|
|
11
180
|
const { baseUrl, secret, path = "/" } = options;
|
|
12
181
|
const failures = [];
|
|
@@ -19,6 +188,7 @@ async function checkCmssyEditMode(options) {
|
|
|
19
188
|
failures.push(`public ${path}: the editor is mounted on a public page`);
|
|
20
189
|
}
|
|
21
190
|
const hasServerLayoutBlocks = SERVER_LAYOUT_BLOCKS.test(publicPage.body);
|
|
191
|
+
const configured = options.workspace ? await workspaceHasLayoutBlocks(options.workspace, path) : null;
|
|
22
192
|
const unverified = await html(url(`${path}?cmssyEdit=1`));
|
|
23
193
|
if (EDITOR_MARKER.test(unverified.body)) {
|
|
24
194
|
failures.push(
|
|
@@ -41,6 +211,11 @@ async function checkCmssyEditMode(options) {
|
|
|
41
211
|
`edit ${path}: the header and footer are still server-rendered - the editor will let you select them but show no fields (is CMSSY_EDIT_HEADER set on the rewrite?)`
|
|
42
212
|
);
|
|
43
213
|
}
|
|
214
|
+
if (configured === true && !EDITABLE_LAYOUT_SLOT.test(verified.body)) {
|
|
215
|
+
failures.push(
|
|
216
|
+
`edit ${path}: the workspace defines layout blocks and no editable layout slot is mounted - the header and footer cannot be edited (10.0 removed CmssyLayoutSlot; see docs/wiring.md \xA75)`
|
|
217
|
+
);
|
|
218
|
+
}
|
|
44
219
|
const { localizedPath } = options;
|
|
45
220
|
if (localizedPath) {
|
|
46
221
|
const locale = options.localizedLocale ?? localizedPath.split("/").filter(Boolean)[0];
|
package/dist/testing.d.cts
CHANGED
|
@@ -18,6 +18,21 @@ interface EditSmokeOptions {
|
|
|
18
18
|
* path segment, which IS the language on a prefixed site.
|
|
19
19
|
*/
|
|
20
20
|
localizedLocale?: string;
|
|
21
|
+
/**
|
|
22
|
+
* The workspace the app is pointed at. Given it, the check asks the delivery
|
|
23
|
+
* API whether the workspace HAS header/footer blocks - and if it does, an app
|
|
24
|
+
* that renders none is a failure rather than a site that simply has none.
|
|
25
|
+
*
|
|
26
|
+
* That distinction is the whole point. `cmssy init` once scaffolded the
|
|
27
|
+
* editable-layout wrapper with nothing mounting it, so the app rendered no
|
|
28
|
+
* header at all - and this check, unable to tell "none configured" from "none
|
|
29
|
+
* rendered", stayed green.
|
|
30
|
+
*/
|
|
31
|
+
workspace?: {
|
|
32
|
+
org: string;
|
|
33
|
+
workspaceSlug: string;
|
|
34
|
+
apiUrl?: string;
|
|
35
|
+
};
|
|
21
36
|
}
|
|
22
37
|
interface EditSmokeResult {
|
|
23
38
|
ok: boolean;
|
|
@@ -27,12 +42,15 @@ interface EditSmokeResult {
|
|
|
27
42
|
* Proves a consumer app's EDIT path still works - the path a build cannot check,
|
|
28
43
|
* because the site compiles and serves fine while being uneditable.
|
|
29
44
|
*
|
|
30
|
-
* It asserts
|
|
45
|
+
* It asserts four independent things:
|
|
31
46
|
* 1. the public page renders WITHOUT the editor, header and footer server-rendered;
|
|
32
47
|
* 2. a bare `?cmssyEdit=1` does NOT enter edit mode (an unverified pair must
|
|
33
48
|
* not open the door - CMS-948);
|
|
34
49
|
* 3. a verified `cmssyEdit=1` + `cmssySecret` renders the editor AND moves the
|
|
35
|
-
* header and footer onto the edit bridge
|
|
50
|
+
* header and footer onto the edit bridge;
|
|
51
|
+
* 4. given `workspace`, that an app whose workspace HAS layout blocks renders
|
|
52
|
+
* them at all - the one thing "no header anywhere" and "no header
|
|
53
|
+
* configured" otherwise look alike.
|
|
36
54
|
*
|
|
37
55
|
* Run it against a started production build:
|
|
38
56
|
*
|
package/dist/testing.d.ts
CHANGED
|
@@ -18,6 +18,21 @@ interface EditSmokeOptions {
|
|
|
18
18
|
* path segment, which IS the language on a prefixed site.
|
|
19
19
|
*/
|
|
20
20
|
localizedLocale?: string;
|
|
21
|
+
/**
|
|
22
|
+
* The workspace the app is pointed at. Given it, the check asks the delivery
|
|
23
|
+
* API whether the workspace HAS header/footer blocks - and if it does, an app
|
|
24
|
+
* that renders none is a failure rather than a site that simply has none.
|
|
25
|
+
*
|
|
26
|
+
* That distinction is the whole point. `cmssy init` once scaffolded the
|
|
27
|
+
* editable-layout wrapper with nothing mounting it, so the app rendered no
|
|
28
|
+
* header at all - and this check, unable to tell "none configured" from "none
|
|
29
|
+
* rendered", stayed green.
|
|
30
|
+
*/
|
|
31
|
+
workspace?: {
|
|
32
|
+
org: string;
|
|
33
|
+
workspaceSlug: string;
|
|
34
|
+
apiUrl?: string;
|
|
35
|
+
};
|
|
21
36
|
}
|
|
22
37
|
interface EditSmokeResult {
|
|
23
38
|
ok: boolean;
|
|
@@ -27,12 +42,15 @@ interface EditSmokeResult {
|
|
|
27
42
|
* Proves a consumer app's EDIT path still works - the path a build cannot check,
|
|
28
43
|
* because the site compiles and serves fine while being uneditable.
|
|
29
44
|
*
|
|
30
|
-
* It asserts
|
|
45
|
+
* It asserts four independent things:
|
|
31
46
|
* 1. the public page renders WITHOUT the editor, header and footer server-rendered;
|
|
32
47
|
* 2. a bare `?cmssyEdit=1` does NOT enter edit mode (an unverified pair must
|
|
33
48
|
* not open the door - CMS-948);
|
|
34
49
|
* 3. a verified `cmssyEdit=1` + `cmssySecret` renders the editor AND moves the
|
|
35
|
-
* header and footer onto the edit bridge
|
|
50
|
+
* header and footer onto the edit bridge;
|
|
51
|
+
* 4. given `workspace`, that an app whose workspace HAS layout blocks renders
|
|
52
|
+
* them at all - the one thing "no header anywhere" and "no header
|
|
53
|
+
* configured" otherwise look alike.
|
|
36
54
|
*
|
|
37
55
|
* Run it against a started production build:
|
|
38
56
|
*
|
package/dist/testing.js
CHANGED
|
@@ -1,10 +1,179 @@
|
|
|
1
|
+
// src/data/http.ts
|
|
2
|
+
var CmssyRequestError = class extends Error {
|
|
3
|
+
status;
|
|
4
|
+
/**
|
|
5
|
+
* How long the server asked us to wait, in ms, when it said so. Present on a
|
|
6
|
+
* 429 that carried `Retry-After`; a caller can serve stale content for that
|
|
7
|
+
* long instead of guessing.
|
|
8
|
+
*/
|
|
9
|
+
retryAfterMs;
|
|
10
|
+
constructor(message, status, retryAfterMs2) {
|
|
11
|
+
super(message);
|
|
12
|
+
this.name = "CmssyRequestError";
|
|
13
|
+
this.status = status;
|
|
14
|
+
if (retryAfterMs2 !== void 0) this.retryAfterMs = retryAfterMs2;
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
var DEFAULT_RETRY_STATUSES = [429, 503];
|
|
18
|
+
function retryAfterMs(response) {
|
|
19
|
+
const raw = response.headers?.get("retry-after");
|
|
20
|
+
if (!raw) return null;
|
|
21
|
+
const seconds = Number(raw);
|
|
22
|
+
if (Number.isFinite(seconds)) return Math.max(0, seconds * 1e3);
|
|
23
|
+
const date = Date.parse(raw);
|
|
24
|
+
if (Number.isFinite(date)) return Math.max(0, date - Date.now());
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
function sleep(ms, signal) {
|
|
28
|
+
return new Promise((resolve, reject) => {
|
|
29
|
+
if (signal?.aborted) {
|
|
30
|
+
reject(new Error("cmssy: request aborted"));
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
const timer = setTimeout(() => {
|
|
34
|
+
signal?.removeEventListener("abort", onAbort);
|
|
35
|
+
resolve();
|
|
36
|
+
}, ms);
|
|
37
|
+
function onAbort() {
|
|
38
|
+
clearTimeout(timer);
|
|
39
|
+
reject(new Error("cmssy: request aborted"));
|
|
40
|
+
}
|
|
41
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
async function fetchWithRetry(doFetch, url, init, retry) {
|
|
45
|
+
if (retry === false || retry === void 0) {
|
|
46
|
+
return doFetch(url, init);
|
|
47
|
+
}
|
|
48
|
+
const maxRetries = retry.maxRetries ?? 3;
|
|
49
|
+
const baseDelayMs = retry.baseDelayMs ?? 300;
|
|
50
|
+
const maxDelayMs = retry.maxDelayMs ?? 3e3;
|
|
51
|
+
const maxRetryAfterMs = retry.maxRetryAfterMs ?? 1e4;
|
|
52
|
+
const retryStatuses = retry.retryStatuses ?? DEFAULT_RETRY_STATUSES;
|
|
53
|
+
let response = await doFetch(url, init);
|
|
54
|
+
for (let attempt = 0; attempt < maxRetries; attempt++) {
|
|
55
|
+
if (response.ok || !retryStatuses.includes(response.status)) {
|
|
56
|
+
return response;
|
|
57
|
+
}
|
|
58
|
+
const asked = retryAfterMs(response);
|
|
59
|
+
if (asked !== null && asked > maxRetryAfterMs) return response;
|
|
60
|
+
const backoff = baseDelayMs * 2 ** attempt;
|
|
61
|
+
const wait = asked !== null ? asked : Math.min(backoff, maxDelayMs);
|
|
62
|
+
await sleep(wait, init.signal);
|
|
63
|
+
response = await doFetch(url, init);
|
|
64
|
+
}
|
|
65
|
+
return response;
|
|
66
|
+
}
|
|
67
|
+
async function postGraphql(url, query, variables, options) {
|
|
68
|
+
const doFetch = options.fetch ?? globalThis.fetch;
|
|
69
|
+
if (typeof doFetch !== "function") {
|
|
70
|
+
throw new Error(
|
|
71
|
+
"cmssy: no fetch implementation available - pass options.fetch"
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
const response = await fetchWithRetry(
|
|
75
|
+
doFetch,
|
|
76
|
+
url,
|
|
77
|
+
{
|
|
78
|
+
method: "POST",
|
|
79
|
+
headers: { "content-type": "application/json", ...options.headers },
|
|
80
|
+
body: JSON.stringify({ query, variables }),
|
|
81
|
+
signal: options.signal
|
|
82
|
+
},
|
|
83
|
+
options.retry
|
|
84
|
+
);
|
|
85
|
+
if (!response.ok) {
|
|
86
|
+
let detail = "";
|
|
87
|
+
try {
|
|
88
|
+
const body = await response.json();
|
|
89
|
+
if (body.errors && body.errors.length > 0) {
|
|
90
|
+
detail = ` - ${body.errors.map((error) => error.message ?? "GraphQL error").join("; ")}`;
|
|
91
|
+
}
|
|
92
|
+
} catch {
|
|
93
|
+
detail = "";
|
|
94
|
+
}
|
|
95
|
+
const asked = retryAfterMs(response);
|
|
96
|
+
const wait = asked !== null ? ` - retry after ${Math.ceil(asked / 1e3)}s` : "";
|
|
97
|
+
throw new CmssyRequestError(
|
|
98
|
+
`cmssy: ${options.label} failed (${response.status})${detail}${wait}`,
|
|
99
|
+
response.status,
|
|
100
|
+
asked ?? void 0
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
let json;
|
|
104
|
+
try {
|
|
105
|
+
json = await response.json();
|
|
106
|
+
} catch {
|
|
107
|
+
throw new Error(`cmssy: invalid JSON response from the ${options.label}`);
|
|
108
|
+
}
|
|
109
|
+
if (json.errors && json.errors.length > 0) {
|
|
110
|
+
const message = json.errors.map((error) => error.message ?? "GraphQL error").join("; ");
|
|
111
|
+
throw new Error(`cmssy: ${options.label} error - ${message}`);
|
|
112
|
+
}
|
|
113
|
+
return json.data;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// src/content/content-client.ts
|
|
117
|
+
var DEFAULT_CMSSY_API_URL = "https://api.cmssy.io/graphql";
|
|
118
|
+
function resolveApiUrl(apiUrl) {
|
|
119
|
+
const explicit = apiUrl?.trim();
|
|
120
|
+
if (explicit) return explicit;
|
|
121
|
+
const env = globalThis.process?.env;
|
|
122
|
+
const fromEnv = env?.CMSSY_API_URL?.trim() ?? "";
|
|
123
|
+
return fromEnv.length > 0 ? fromEnv : DEFAULT_CMSSY_API_URL;
|
|
124
|
+
}
|
|
125
|
+
function resolvePublicUrl(config) {
|
|
126
|
+
const base = resolveApiUrl(config.apiUrl).replace(/\/graphql\/?$/, "");
|
|
127
|
+
return `${base}/public/${config.org}/${config.workspaceSlug}/graphql`;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// src/data/graphql-request.ts
|
|
131
|
+
async function graphqlRequest(config, query, variables, options = {}, label = "request") {
|
|
132
|
+
const url = options.public ? resolvePublicUrl(config) : resolveApiUrl(config.apiUrl);
|
|
133
|
+
return postGraphql(url, query, variables, {
|
|
134
|
+
fetch: options.fetch,
|
|
135
|
+
signal: options.signal,
|
|
136
|
+
headers: options.headers,
|
|
137
|
+
retry: options.retry,
|
|
138
|
+
label
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
|
|
1
142
|
// src/testing/edit-smoke.ts
|
|
2
143
|
var EDITOR_MARKER = /data-cmssy-editor/;
|
|
3
144
|
var SERVER_LAYOUT_BLOCKS = /<header|<footer/;
|
|
145
|
+
var EDITABLE_LAYOUT_SLOT = /data-cmssy-layout-slot/;
|
|
4
146
|
async function html(url) {
|
|
5
147
|
const response = await fetch(url, { redirect: "manual" });
|
|
6
148
|
return { status: response.status, body: await response.text() };
|
|
7
149
|
}
|
|
150
|
+
var LAYOUT_BLOCKS_QUERY = `query CmssySmokeLayouts($workspaceSlug: String!, $pageSlug: String!) {
|
|
151
|
+
public {
|
|
152
|
+
page {
|
|
153
|
+
layouts(workspaceSlug: $workspaceSlug, pageSlug: $pageSlug) {
|
|
154
|
+
position
|
|
155
|
+
blocks { id isActive }
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}`;
|
|
160
|
+
async function workspaceHasLayoutBlocks(workspace, pageSlug) {
|
|
161
|
+
try {
|
|
162
|
+
const data = await graphqlRequest(
|
|
163
|
+
workspace,
|
|
164
|
+
LAYOUT_BLOCKS_QUERY,
|
|
165
|
+
{ workspaceSlug: workspace.workspaceSlug, pageSlug },
|
|
166
|
+
{ public: true, retry: {} },
|
|
167
|
+
"edit smoke: layout blocks"
|
|
168
|
+
);
|
|
169
|
+
const groups = data.public?.page?.layouts ?? [];
|
|
170
|
+
return groups.some(
|
|
171
|
+
(group) => (group.blocks ?? []).some((block) => block.isActive !== false)
|
|
172
|
+
);
|
|
173
|
+
} catch {
|
|
174
|
+
return null;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
8
177
|
async function checkCmssyEditMode(options) {
|
|
9
178
|
const { baseUrl, secret, path = "/" } = options;
|
|
10
179
|
const failures = [];
|
|
@@ -17,6 +186,7 @@ async function checkCmssyEditMode(options) {
|
|
|
17
186
|
failures.push(`public ${path}: the editor is mounted on a public page`);
|
|
18
187
|
}
|
|
19
188
|
const hasServerLayoutBlocks = SERVER_LAYOUT_BLOCKS.test(publicPage.body);
|
|
189
|
+
const configured = options.workspace ? await workspaceHasLayoutBlocks(options.workspace, path) : null;
|
|
20
190
|
const unverified = await html(url(`${path}?cmssyEdit=1`));
|
|
21
191
|
if (EDITOR_MARKER.test(unverified.body)) {
|
|
22
192
|
failures.push(
|
|
@@ -39,6 +209,11 @@ async function checkCmssyEditMode(options) {
|
|
|
39
209
|
`edit ${path}: the header and footer are still server-rendered - the editor will let you select them but show no fields (is CMSSY_EDIT_HEADER set on the rewrite?)`
|
|
40
210
|
);
|
|
41
211
|
}
|
|
212
|
+
if (configured === true && !EDITABLE_LAYOUT_SLOT.test(verified.body)) {
|
|
213
|
+
failures.push(
|
|
214
|
+
`edit ${path}: the workspace defines layout blocks and no editable layout slot is mounted - the header and footer cannot be edited (10.0 removed CmssyLayoutSlot; see docs/wiring.md \xA75)`
|
|
215
|
+
);
|
|
216
|
+
}
|
|
42
217
|
const { localizedPath } = options;
|
|
43
218
|
if (localizedPath) {
|
|
44
219
|
const locale = options.localizedLocale ?? localizedPath.split("/").filter(Boolean)[0];
|