@taprootio/site-authoring 0.1.1 → 0.3.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.
@@ -2,9 +2,17 @@ import { lstat, realpath } from "node:fs/promises";
2
2
  import path from "node:path";
3
3
 
4
4
  import { NAVIGATION_MAXIMUM_DEPTH, TEMPLATE_TYPE_FREE_FORM } from "../api.js";
5
- import { sharedThemeContextNames } from "../content/free-form-sections.js";
5
+ import { freeFormRootPresentation, sharedThemeContextNames } from "../content/free-form-sections.js";
6
6
  import { VERB_VALIDATE } from "../constants.js";
7
7
  import { isCanonicalUuid, SiteAuthoringError } from "../errors.js";
8
+ import {
9
+ FIXTURE_CONTRACT_VERSION,
10
+ FIXTURE_DELIVERY_ORIGIN_DOMAIN,
11
+ FIXTURE_MANIFEST_FILE_NAME,
12
+ FIXTURE_MAXIMUM_DELIVERY_ORIGINS,
13
+ FIXTURE_METADATA_FIELDS,
14
+ FIXTURE_ROOT_FIELDS,
15
+ } from "../fixture-contract.js";
8
16
  import { FOOTER_SETTINGS_FILE } from "../footer-workspace.js";
9
17
  import { boundedList, successResult } from "../session.js";
10
18
  import { SETTINGS_GROUPS, SETTINGS_TYPE_SITE_PUBLISHING_PREFERENCES } from "../settings-catalog.js";
@@ -26,22 +34,9 @@ import {
26
34
  WORKSPACE_LIMITS,
27
35
  } from "../workspace.js";
28
36
 
29
- export const FIXTURE_MANIFEST_FILE_NAME = "manifest.fixture.json";
30
- export const FIXTURE_CONTRACT_VERSION = 1;
31
37
  const MAXIMUM_REPORTED = 200;
32
- const FIXTURE_ROOT_KEYS = new Set([
33
- "manifestVersion",
34
- "siteId",
35
- "pulledAt",
36
- "pages",
37
- "pagesTruncated",
38
- "navigation",
39
- "settings",
40
- "settingsSkipped",
41
- "deployments",
42
- "fixture",
43
- ]);
44
- const FIXTURE_METADATA_KEYS = new Set(["contractVersion", "imageIds", "deliveryOrigins"]);
38
+ const FIXTURE_ROOT_KEYS = new Set(FIXTURE_ROOT_FIELDS);
39
+ const FIXTURE_METADATA_KEYS = new Set(FIXTURE_METADATA_FIELDS);
45
40
 
46
41
  function fail(code, message, field, exitCode) {
47
42
  throw new SiteAuthoringError(code, message, { field, exitCode });
@@ -163,7 +158,10 @@ function validateFixtureManifest(manifest) {
163
158
  if (imageIds.size !== manifest.fixture.imageIds.length) {
164
159
  fail("fixture.image_ids_invalid", "fixture.imageIds must not contain duplicates.", "fixture.imageIds");
165
160
  }
166
- if (!Array.isArray(manifest.fixture.deliveryOrigins) || manifest.fixture.deliveryOrigins.length > 100) {
161
+ if (
162
+ !Array.isArray(manifest.fixture.deliveryOrigins)
163
+ || manifest.fixture.deliveryOrigins.length > FIXTURE_MAXIMUM_DELIVERY_ORIGINS
164
+ ) {
167
165
  fail(
168
166
  "fixture.delivery_origins_invalid",
169
167
  "fixture.deliveryOrigins must be a bounded list of reserved HTTPS example origins.",
@@ -186,11 +184,14 @@ function validateFixtureManifest(manifest) {
186
184
  || url.pathname !== "/"
187
185
  || url.search !== ""
188
186
  || url.hash !== ""
189
- || !(url.hostname === "example.test" || url.hostname.endsWith(".example.test"))
187
+ || !(
188
+ url.hostname === FIXTURE_DELIVERY_ORIGIN_DOMAIN
189
+ || url.hostname.endsWith(`.${FIXTURE_DELIVERY_ORIGIN_DOMAIN}`)
190
+ )
190
191
  ) {
191
192
  fail(
192
193
  "fixture.delivery_origins_invalid",
193
- "Every fixture delivery origin must be an origin-only HTTPS URL under the reserved example.test domain.",
194
+ `Every fixture delivery origin must be an origin-only HTTPS URL under the reserved ${FIXTURE_DELIVERY_ORIGIN_DOMAIN} domain.`,
194
195
  `fixture.deliveryOrigins[${index}]`,
195
196
  );
196
197
  }
@@ -340,6 +341,34 @@ function validatePageImageReferences(value, field, knownImageIds, deliveryOrigin
340
341
  }
341
342
  }
342
343
 
344
+ /** Whether a validated page document places a root-band component at its root. */
345
+ function pageUsesRootBand(document) {
346
+ return Array.isArray(document?.content)
347
+ && document.content.some((node) => freeFormRootPresentation(node)?.rootPlacement === "root-band");
348
+ }
349
+
350
+ /**
351
+ * Advisory only: a root-band component spans the viewport, so a header whose
352
+ * brand and buttons stop at the content edges usually reads as misaligned
353
+ * beside it. The wide header with the centered menu is the recommended pair,
354
+ * never a requirement, so this is a hint in the result and on stderr, not a
355
+ * failure (TR00697).
356
+ */
357
+ function headerWidthHints(headerWidth, rootBandPages) {
358
+ if (rootBandPages.length === 0 || headerWidth === "wide") return [];
359
+ const pages = boundedList(rootBandPages, MAXIMUM_REPORTED);
360
+ return [{
361
+ code: "header.width_contained_with_root_band",
362
+ message: `${rootBandPages.length} page(s) use a full-bleed root-band component while site-header.headerWidth is `
363
+ + `'${headerWidth}'. Full-bleed designs usually read better with headerWidth 'wide' (brand and buttons at the `
364
+ + "viewport edges) paired with headerLayout 'centered-menu'.",
365
+ setting: "site-header.headerWidth",
366
+ suggested: { headerWidth: "wide", headerLayout: "centered-menu" },
367
+ pages: pages.items,
368
+ ...(pages.truncated ? { pagesTruncated: true } : {}),
369
+ }];
370
+ }
371
+
343
372
  export async function validateFixture(invocation = {}) {
344
373
  const onProgress = typeof invocation.onProgress === "function" ? invocation.onProgress : () => {};
345
374
  const fixtureRoot = await resolveFixtureRoot(invocation.cwd ?? process.cwd(), invocation.fixturePath);
@@ -377,6 +406,7 @@ export async function validateFixture(invocation = {}) {
377
406
  fail("fixture.page_untracked", `Page source '${untracked}' is not bound by ${FIXTURE_MANIFEST_FILE_NAME}.`, untracked);
378
407
  }
379
408
  const validatedPages = [];
409
+ const rootBandPages = [];
380
410
  for (const file of pageFiles) {
381
411
  onProgress(`Validating '${file}'.`);
382
412
  const entry = manifestByFile.get(file);
@@ -404,7 +434,10 @@ export async function validateFixture(invocation = {}) {
404
434
  }
405
435
  validatePageImageReferences(page.document, file, imageIds, deliveryOrigins);
406
436
  validatedPages.push({ file, path: page.pagePath });
437
+ if (pageUsesRootBand(page.document)) rootBandPages.push(file);
407
438
  }
439
+ const hints = headerWidthHints(presentation.header.headerWidth, rootBandPages);
440
+ for (const hint of hints) onProgress(`Hint: ${hint.message}`);
408
441
 
409
442
  onProgress("Validating navigation shape and local PAGE targets.");
410
443
  const navigation = validateNavigationWorkspaceDocument(
@@ -459,6 +492,7 @@ export async function validateFixture(invocation = {}) {
459
492
  count: presentation.themes.warningCount,
460
493
  ...(presentation.themes.warningsTruncated ? { truncated: true } : {}),
461
494
  },
495
+ hints,
462
496
  proves: [
463
497
  "fixture structure and bounded files",
464
498
  "page content and named theme contexts",