@cmssy/core 11.6.0 → 11.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 CHANGED
@@ -1,146 +1,5 @@
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
-
144
3
  // src/preflight.ts
145
4
  var ALLOWED_FRAME_HOSTS = ["cmssy.io", "www.cmssy.io"];
146
5
  function frameAncestorSources(cspHeaderValue) {
@@ -209,36 +68,10 @@ function localeOf(path) {
209
68
  function langOf(body) {
210
69
  return /<html[^>]*\slang=["']([^"']+)["']/i.exec(body)?.[1];
211
70
  }
212
- var LAYOUT_BLOCKS_QUERY = `query CmssySmokeLayouts($workspaceSlug: String!, $pageSlug: String!) {
213
- public {
214
- page {
215
- layouts(workspaceSlug: $workspaceSlug, pageSlug: $pageSlug) {
216
- position
217
- blocks { id isActive }
218
- }
219
- }
220
- }
221
- }`;
222
- async function workspaceHasLayoutBlocks(workspace, pageSlug) {
223
- try {
224
- const data = await graphqlRequest(
225
- workspace,
226
- LAYOUT_BLOCKS_QUERY,
227
- { workspaceSlug: workspace.workspaceSlug, pageSlug },
228
- { public: true, retry: {} },
229
- "edit smoke: layout blocks"
230
- );
231
- const groups = data.public?.page?.layouts ?? [];
232
- return groups.some(
233
- (group) => (group.blocks ?? []).some((block) => block.isActive !== false)
234
- );
235
- } catch {
236
- return null;
237
- }
238
- }
239
71
  async function checkCmssyEditMode(options) {
240
72
  const { baseUrl, secret, path = "/" } = options;
241
73
  const failures = [];
74
+ const skipped = [];
242
75
  const url = (suffix) => `${baseUrl.replace(/\/+$/, "")}${suffix}`;
243
76
  const publicPage = await html(url(path));
244
77
  if (publicPage.status !== 200) {
@@ -248,7 +81,17 @@ async function checkCmssyEditMode(options) {
248
81
  failures.push(`public ${path}: the editor is mounted on a public page`);
249
82
  }
250
83
  const hasServerLayoutBlocks = SERVER_LAYOUT_BLOCKS.test(publicPage.body);
251
- const configured = options.workspace ? await workspaceHasLayoutBlocks(options.workspace, path) : null;
84
+ if (!hasServerLayoutBlocks) {
85
+ skipped.push(
86
+ `layout bridge: the public ${path} server-rendered no <header> or <footer>, so nothing proved they move onto the edit bridge in edit mode`
87
+ );
88
+ }
89
+ const expectLayoutBlocks = options.expectLayoutBlocks === true;
90
+ if (!expectLayoutBlocks) {
91
+ skipped.push(
92
+ "layout slot: expectLayoutBlocks is not set, so neither the mounted slot nor the content it resolved for the editor was asserted"
93
+ );
94
+ }
252
95
  const unverified = await html(url(`${path}?cmssyEdit=1`));
253
96
  if (EDITOR_MARKER.test(unverified.body)) {
254
97
  failures.push(
@@ -271,12 +114,12 @@ async function checkCmssyEditMode(options) {
271
114
  `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?)`
272
115
  );
273
116
  }
274
- if (configured === true && !EDITABLE_LAYOUT_SLOT.test(verified.body)) {
117
+ if (expectLayoutBlocks && !EDITABLE_LAYOUT_SLOT.test(verified.body)) {
275
118
  failures.push(
276
- `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)`
119
+ `edit ${path}: expectLayoutBlocks is set and no editable layout slot is mounted - the header and footer cannot be edited (mount CmssyLayoutSlot from @cmssy/next/server, or resolveCmssyLayoutSlot on the other adapters; see docs/wiring.md \xA75)`
277
120
  );
278
121
  }
279
- if (configured === true && EDITABLE_LAYOUT_SLOT.test(verified.body)) {
122
+ if (expectLayoutBlocks && EDITABLE_LAYOUT_SLOT.test(verified.body)) {
280
123
  const counts = resolvedContentCounts(verified.body);
281
124
  if (counts.length === 0) {
282
125
  failures.push(
@@ -290,6 +133,11 @@ async function checkCmssyEditMode(options) {
290
133
  }
291
134
  const query = `cmssyEdit=1&cmssySecret=${encodeURIComponent(secret)}`;
292
135
  const { localizedPath } = options;
136
+ if (!localizedPath) {
137
+ skipped.push(
138
+ "language: no localizedPath, so nothing checked that a prefixed URL previews in its own language"
139
+ );
140
+ }
293
141
  if (localizedPath) {
294
142
  const locale = options.localizedLocale ?? localeOf(localizedPath);
295
143
  const localized = await html(url(`${localizedPath}?${query}`));
@@ -314,8 +162,12 @@ async function checkCmssyEditMode(options) {
314
162
  }
315
163
  if (options.editRoute !== false) {
316
164
  await checkDirectEditRoute(url, path, query, void 0, failures);
165
+ } else {
166
+ skipped.push(
167
+ "edit route: this adapter serves the editor from the page (editRoute: false), so there is no /cmssy-edit route to reach directly and its frame-ancestors were not checked - nothing to fix unless you expected one"
168
+ );
317
169
  }
318
- return { ok: failures.length === 0, failures };
170
+ return { ok: failures.length === 0, failures, skipped };
319
171
  }
320
172
  async function checkDirectEditRoute(url, routedPath, query, locale, failures) {
321
173
  const withoutPrefix = withoutEditPrefix(routedPath);
@@ -1,65 +1,17 @@
1
1
  interface EditSmokeOptions {
2
- /** A running build of the consumer app, e.g. http://localhost:3000. */
3
2
  baseUrl: string;
4
- /** The site's CMSSY_DRAFT_SECRET. Without it nothing can be verified. */
5
3
  secret: string;
6
- /** A published page to exercise. Defaults to "/". */
7
4
  path?: string;
8
- /**
9
- * The same page under a language prefix, e.g. "/no". The caller states it
10
- * because only the caller knows how its site spells a language; asking a
11
- * workspace would tie this check to content that can change or be deleted.
12
- *
13
- * Given it, the check also proves the preview renders in THAT language rather
14
- * than the default one - by reading `<html lang>`, which is a contract,
15
- * unlike a word from the page's copy that an editor can change at any time.
16
- */
17
5
  localizedPath?: string;
18
- /**
19
- * The language `localizedPath` must render, e.g. "no". Defaults to the first
20
- * path segment, which IS the language on a prefixed site.
21
- */
22
6
  localizedLocale?: string;
23
- /**
24
- * The workspace the app is pointed at. Given it, the check asks the delivery
25
- * API whether the workspace HAS header/footer blocks - and if it does, an app
26
- * that renders none is a failure rather than a site that simply has none.
27
- *
28
- * That distinction is the whole point. `cmssy init` once scaffolded the
29
- * editable-layout wrapper with nothing mounting it, so the app rendered no
30
- * header at all - and this check, unable to tell "none configured" from "none
31
- * rendered", stayed green.
32
- */
33
- workspace?: {
34
- org: string;
35
- workspaceSlug: string;
36
- apiUrl?: string;
37
- };
7
+ expectLayoutBlocks?: boolean;
38
8
  editRoute?: boolean;
39
9
  }
40
10
  interface EditSmokeResult {
41
11
  ok: boolean;
42
12
  failures: string[];
13
+ skipped: string[];
43
14
  }
44
- /**
45
- * Proves a consumer app's EDIT path still works - the path a build cannot check,
46
- * because the site compiles and serves fine while being uneditable.
47
- *
48
- * It asserts four independent things:
49
- * 1. the public page renders WITHOUT the editor, header and footer server-rendered;
50
- * 2. a bare `?cmssyEdit=1` does NOT enter edit mode (an unverified pair must
51
- * not open the door - CMS-948);
52
- * 3. a verified `cmssyEdit=1` + `cmssySecret` renders the editor AND moves the
53
- * header and footer onto the edit bridge;
54
- * 4. given `workspace`, that an app whose workspace HAS layout blocks renders
55
- * them at all - the one thing "no header anywhere" and "no header
56
- * configured" otherwise look alike.
57
- *
58
- * Run it against a started production build:
59
- *
60
- * const result = await checkCmssyEditMode({ baseUrl, secret });
61
- * expect(result.failures).toEqual([]);
62
- */
63
15
  declare function checkCmssyEditMode(options: EditSmokeOptions): Promise<EditSmokeResult>;
64
16
 
65
17
  export { type EditSmokeOptions, type EditSmokeResult, checkCmssyEditMode };
package/dist/testing.d.ts CHANGED
@@ -1,65 +1,17 @@
1
1
  interface EditSmokeOptions {
2
- /** A running build of the consumer app, e.g. http://localhost:3000. */
3
2
  baseUrl: string;
4
- /** The site's CMSSY_DRAFT_SECRET. Without it nothing can be verified. */
5
3
  secret: string;
6
- /** A published page to exercise. Defaults to "/". */
7
4
  path?: string;
8
- /**
9
- * The same page under a language prefix, e.g. "/no". The caller states it
10
- * because only the caller knows how its site spells a language; asking a
11
- * workspace would tie this check to content that can change or be deleted.
12
- *
13
- * Given it, the check also proves the preview renders in THAT language rather
14
- * than the default one - by reading `<html lang>`, which is a contract,
15
- * unlike a word from the page's copy that an editor can change at any time.
16
- */
17
5
  localizedPath?: string;
18
- /**
19
- * The language `localizedPath` must render, e.g. "no". Defaults to the first
20
- * path segment, which IS the language on a prefixed site.
21
- */
22
6
  localizedLocale?: string;
23
- /**
24
- * The workspace the app is pointed at. Given it, the check asks the delivery
25
- * API whether the workspace HAS header/footer blocks - and if it does, an app
26
- * that renders none is a failure rather than a site that simply has none.
27
- *
28
- * That distinction is the whole point. `cmssy init` once scaffolded the
29
- * editable-layout wrapper with nothing mounting it, so the app rendered no
30
- * header at all - and this check, unable to tell "none configured" from "none
31
- * rendered", stayed green.
32
- */
33
- workspace?: {
34
- org: string;
35
- workspaceSlug: string;
36
- apiUrl?: string;
37
- };
7
+ expectLayoutBlocks?: boolean;
38
8
  editRoute?: boolean;
39
9
  }
40
10
  interface EditSmokeResult {
41
11
  ok: boolean;
42
12
  failures: string[];
13
+ skipped: string[];
43
14
  }
44
- /**
45
- * Proves a consumer app's EDIT path still works - the path a build cannot check,
46
- * because the site compiles and serves fine while being uneditable.
47
- *
48
- * It asserts four independent things:
49
- * 1. the public page renders WITHOUT the editor, header and footer server-rendered;
50
- * 2. a bare `?cmssyEdit=1` does NOT enter edit mode (an unverified pair must
51
- * not open the door - CMS-948);
52
- * 3. a verified `cmssyEdit=1` + `cmssySecret` renders the editor AND moves the
53
- * header and footer onto the edit bridge;
54
- * 4. given `workspace`, that an app whose workspace HAS layout blocks renders
55
- * them at all - the one thing "no header anywhere" and "no header
56
- * configured" otherwise look alike.
57
- *
58
- * Run it against a started production build:
59
- *
60
- * const result = await checkCmssyEditMode({ baseUrl, secret });
61
- * expect(result.failures).toEqual([]);
62
- */
63
15
  declare function checkCmssyEditMode(options: EditSmokeOptions): Promise<EditSmokeResult>;
64
16
 
65
17
  export { type EditSmokeOptions, type EditSmokeResult, checkCmssyEditMode };
package/dist/testing.js CHANGED
@@ -1,144 +1,3 @@
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
-
142
1
  // src/preflight.ts
143
2
  var ALLOWED_FRAME_HOSTS = ["cmssy.io", "www.cmssy.io"];
144
3
  function frameAncestorSources(cspHeaderValue) {
@@ -207,36 +66,10 @@ function localeOf(path) {
207
66
  function langOf(body) {
208
67
  return /<html[^>]*\slang=["']([^"']+)["']/i.exec(body)?.[1];
209
68
  }
210
- var LAYOUT_BLOCKS_QUERY = `query CmssySmokeLayouts($workspaceSlug: String!, $pageSlug: String!) {
211
- public {
212
- page {
213
- layouts(workspaceSlug: $workspaceSlug, pageSlug: $pageSlug) {
214
- position
215
- blocks { id isActive }
216
- }
217
- }
218
- }
219
- }`;
220
- async function workspaceHasLayoutBlocks(workspace, pageSlug) {
221
- try {
222
- const data = await graphqlRequest(
223
- workspace,
224
- LAYOUT_BLOCKS_QUERY,
225
- { workspaceSlug: workspace.workspaceSlug, pageSlug },
226
- { public: true, retry: {} },
227
- "edit smoke: layout blocks"
228
- );
229
- const groups = data.public?.page?.layouts ?? [];
230
- return groups.some(
231
- (group) => (group.blocks ?? []).some((block) => block.isActive !== false)
232
- );
233
- } catch {
234
- return null;
235
- }
236
- }
237
69
  async function checkCmssyEditMode(options) {
238
70
  const { baseUrl, secret, path = "/" } = options;
239
71
  const failures = [];
72
+ const skipped = [];
240
73
  const url = (suffix) => `${baseUrl.replace(/\/+$/, "")}${suffix}`;
241
74
  const publicPage = await html(url(path));
242
75
  if (publicPage.status !== 200) {
@@ -246,7 +79,17 @@ async function checkCmssyEditMode(options) {
246
79
  failures.push(`public ${path}: the editor is mounted on a public page`);
247
80
  }
248
81
  const hasServerLayoutBlocks = SERVER_LAYOUT_BLOCKS.test(publicPage.body);
249
- const configured = options.workspace ? await workspaceHasLayoutBlocks(options.workspace, path) : null;
82
+ if (!hasServerLayoutBlocks) {
83
+ skipped.push(
84
+ `layout bridge: the public ${path} server-rendered no <header> or <footer>, so nothing proved they move onto the edit bridge in edit mode`
85
+ );
86
+ }
87
+ const expectLayoutBlocks = options.expectLayoutBlocks === true;
88
+ if (!expectLayoutBlocks) {
89
+ skipped.push(
90
+ "layout slot: expectLayoutBlocks is not set, so neither the mounted slot nor the content it resolved for the editor was asserted"
91
+ );
92
+ }
250
93
  const unverified = await html(url(`${path}?cmssyEdit=1`));
251
94
  if (EDITOR_MARKER.test(unverified.body)) {
252
95
  failures.push(
@@ -269,12 +112,12 @@ async function checkCmssyEditMode(options) {
269
112
  `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?)`
270
113
  );
271
114
  }
272
- if (configured === true && !EDITABLE_LAYOUT_SLOT.test(verified.body)) {
115
+ if (expectLayoutBlocks && !EDITABLE_LAYOUT_SLOT.test(verified.body)) {
273
116
  failures.push(
274
- `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)`
117
+ `edit ${path}: expectLayoutBlocks is set and no editable layout slot is mounted - the header and footer cannot be edited (mount CmssyLayoutSlot from @cmssy/next/server, or resolveCmssyLayoutSlot on the other adapters; see docs/wiring.md \xA75)`
275
118
  );
276
119
  }
277
- if (configured === true && EDITABLE_LAYOUT_SLOT.test(verified.body)) {
120
+ if (expectLayoutBlocks && EDITABLE_LAYOUT_SLOT.test(verified.body)) {
278
121
  const counts = resolvedContentCounts(verified.body);
279
122
  if (counts.length === 0) {
280
123
  failures.push(
@@ -288,6 +131,11 @@ async function checkCmssyEditMode(options) {
288
131
  }
289
132
  const query = `cmssyEdit=1&cmssySecret=${encodeURIComponent(secret)}`;
290
133
  const { localizedPath } = options;
134
+ if (!localizedPath) {
135
+ skipped.push(
136
+ "language: no localizedPath, so nothing checked that a prefixed URL previews in its own language"
137
+ );
138
+ }
291
139
  if (localizedPath) {
292
140
  const locale = options.localizedLocale ?? localeOf(localizedPath);
293
141
  const localized = await html(url(`${localizedPath}?${query}`));
@@ -312,8 +160,12 @@ async function checkCmssyEditMode(options) {
312
160
  }
313
161
  if (options.editRoute !== false) {
314
162
  await checkDirectEditRoute(url, path, query, void 0, failures);
163
+ } else {
164
+ skipped.push(
165
+ "edit route: this adapter serves the editor from the page (editRoute: false), so there is no /cmssy-edit route to reach directly and its frame-ancestors were not checked - nothing to fix unless you expected one"
166
+ );
315
167
  }
316
- return { ok: failures.length === 0, failures };
168
+ return { ok: failures.length === 0, failures, skipped };
317
169
  }
318
170
  async function checkDirectEditRoute(url, routedPath, query, locale, failures) {
319
171
  const withoutPrefix = withoutEditPrefix(routedPath);