@brandlab-digital/conformance 3.12.1 → 3.13.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.
Files changed (94) hide show
  1. package/README.md +32 -1
  2. package/dist/current-release.js +5 -5
  3. package/dist/current-release.js.map +1 -1
  4. package/dist/edge-probe-cli.d.ts +57 -0
  5. package/dist/edge-probe-cli.d.ts.map +1 -0
  6. package/dist/edge-probe-cli.js +134 -0
  7. package/dist/edge-probe-cli.js.map +1 -0
  8. package/dist/engineering.d.ts +4 -1
  9. package/dist/engineering.d.ts.map +1 -1
  10. package/dist/engineering.js +19 -14
  11. package/dist/engineering.js.map +1 -1
  12. package/dist/estate-sample.d.ts +60 -0
  13. package/dist/estate-sample.d.ts.map +1 -0
  14. package/dist/estate-sample.js +144 -0
  15. package/dist/estate-sample.js.map +1 -0
  16. package/dist/estate.d.ts +17 -0
  17. package/dist/estate.d.ts.map +1 -1
  18. package/dist/estate.js +47 -10
  19. package/dist/estate.js.map +1 -1
  20. package/dist/origin-region.d.ts +14 -0
  21. package/dist/origin-region.d.ts.map +1 -0
  22. package/dist/origin-region.js +46 -0
  23. package/dist/origin-region.js.map +1 -0
  24. package/dist/prefetch-probe-cli.d.ts +36 -0
  25. package/dist/prefetch-probe-cli.d.ts.map +1 -0
  26. package/dist/prefetch-probe-cli.js +163 -0
  27. package/dist/prefetch-probe-cli.js.map +1 -0
  28. package/dist/site-system/checks.d.ts +36 -0
  29. package/dist/site-system/checks.d.ts.map +1 -1
  30. package/dist/site-system/checks.js +6 -0
  31. package/dist/site-system/checks.js.map +1 -1
  32. package/dist/site-system/config.d.ts +27 -0
  33. package/dist/site-system/config.d.ts.map +1 -1
  34. package/dist/site-system/config.js +50 -1
  35. package/dist/site-system/config.js.map +1 -1
  36. package/dist/site-system/index.d.ts +1 -1
  37. package/dist/site-system/index.d.ts.map +1 -1
  38. package/dist/site-system/index.js +1 -1
  39. package/dist/site-system/index.js.map +1 -1
  40. package/dist/site-system/journeys/plan.d.ts +9 -2
  41. package/dist/site-system/journeys/plan.d.ts.map +1 -1
  42. package/dist/site-system/journeys/plan.js +11 -1
  43. package/dist/site-system/journeys/plan.js.map +1 -1
  44. package/dist/site-system/journeys/prefetch-measure.d.ts +28 -0
  45. package/dist/site-system/journeys/prefetch-measure.d.ts.map +1 -0
  46. package/dist/site-system/journeys/prefetch-measure.js +94 -0
  47. package/dist/site-system/journeys/prefetch-measure.js.map +1 -0
  48. package/dist/site-system/journeys/prefetch-policy.d.ts +85 -0
  49. package/dist/site-system/journeys/prefetch-policy.d.ts.map +1 -0
  50. package/dist/site-system/journeys/prefetch-policy.js +152 -0
  51. package/dist/site-system/journeys/prefetch-policy.js.map +1 -0
  52. package/dist/site-system/journeys/prefetch.d.ts +3 -0
  53. package/dist/site-system/journeys/prefetch.d.ts.map +1 -0
  54. package/dist/site-system/journeys/prefetch.js +37 -0
  55. package/dist/site-system/journeys/prefetch.js.map +1 -0
  56. package/dist/site-system/journeys/variants.d.ts +34 -0
  57. package/dist/site-system/journeys/variants.d.ts.map +1 -0
  58. package/dist/site-system/journeys/variants.js +139 -0
  59. package/dist/site-system/journeys/variants.js.map +1 -0
  60. package/dist/site-system/rule-modules.d.ts.map +1 -1
  61. package/dist/site-system/rule-modules.js +5 -0
  62. package/dist/site-system/rule-modules.js.map +1 -1
  63. package/dist/site-system/rules/full/common.d.ts +3 -0
  64. package/dist/site-system/rules/full/common.d.ts.map +1 -1
  65. package/dist/site-system/rules/full/common.js.map +1 -1
  66. package/dist/site-system/rules/full/estate.d.ts +4 -0
  67. package/dist/site-system/rules/full/estate.d.ts.map +1 -1
  68. package/dist/site-system/rules/full/estate.js +11 -2
  69. package/dist/site-system/rules/full/estate.js.map +1 -1
  70. package/dist/site-system/rules/full/index.d.ts +2 -1
  71. package/dist/site-system/rules/full/index.d.ts.map +1 -1
  72. package/dist/site-system/rules/full/index.js +14 -7
  73. package/dist/site-system/rules/full/index.js.map +1 -1
  74. package/dist/site-system/rules/full/w3-refresh.d.ts +22 -0
  75. package/dist/site-system/rules/full/w3-refresh.d.ts.map +1 -0
  76. package/dist/site-system/rules/full/w3-refresh.js +70 -0
  77. package/dist/site-system/rules/full/w3-refresh.js.map +1 -0
  78. package/dist/site-system/rules/guards/index.d.ts +22 -0
  79. package/dist/site-system/rules/guards/index.d.ts.map +1 -0
  80. package/dist/site-system/rules/guards/index.js +109 -0
  81. package/dist/site-system/rules/guards/index.js.map +1 -0
  82. package/dist/site-system/rules/guards/w10-crawlers.d.ts +55 -0
  83. package/dist/site-system/rules/guards/w10-crawlers.d.ts.map +1 -0
  84. package/dist/site-system/rules/guards/w10-crawlers.js +182 -0
  85. package/dist/site-system/rules/guards/w10-crawlers.js.map +1 -0
  86. package/dist/site-system/rules/guards/w9-params.d.ts +19 -0
  87. package/dist/site-system/rules/guards/w9-params.d.ts.map +1 -0
  88. package/dist/site-system/rules/guards/w9-params.js +84 -0
  89. package/dist/site-system/rules/guards/w9-params.js.map +1 -0
  90. package/dist/site-system-e2e.d.ts +1 -0
  91. package/dist/site-system-e2e.d.ts.map +1 -1
  92. package/dist/site-system-e2e.js +8 -2
  93. package/dist/site-system-e2e.js.map +1 -1
  94. package/package.json +6 -4
@@ -0,0 +1,152 @@
1
+ /**
2
+ * W2's rules, pure so they are unit-tested without a browser: which requests are Next prefetches, which page each
3
+ * one fetches, which link roles must never prefetch, and the verdict for one page at one viewport. The journey and
4
+ * `brandlab-prefetch-probe` share them. The roles mirror site-next's `LINK_PREFETCH_POLICY` (3.13.0).
5
+ */
6
+ /** At most this many distinct pages prefetched on landing, and after a full scroll. */
7
+ export const PREFETCH_LANDING_LIMIT = 8;
8
+ export const PREFETCH_SCROLLED_LIMIT = 12;
9
+ /** Compressed prefetch bytes above this multiple of the page's own compressed HTML earn a warning (advisory). */
10
+ export const PREFETCH_BYTES_WARNING_RATIO = 2;
11
+ /**
12
+ * Containers whose links must never prefetch, by role. Shared components carry `data-bl` hooks and sections their
13
+ * matrix id; a site marks its own lists with `data-bl-links`.
14
+ */
15
+ export const PREFETCH_OFF_CONTAINERS = Object.freeze({
16
+ footer: '[data-bl="footer"], [role="contentinfo"], footer:not(article footer, aside footer, main footer, nav footer, section footer, dialog footer)',
17
+ 'header panel': '[data-bl="header"] [data-slot="panel"]',
18
+ drawer: '[data-bl="drawer"], dialog',
19
+ area: '[data-bl-section="coverage"], [data-bl-links="area"]',
20
+ nearby: '[data-bl-section="nearby-areas"], [data-bl-links="nearby"]',
21
+ map: '[data-bl-section="area-map"], [data-area-map], [data-bl-links="map"]',
22
+ finder: '[data-bl-links="finder"]',
23
+ 'other services': '[data-bl-links="other-services"]',
24
+ });
25
+ /** A page path without query, fragment, Next's `.rsc` and `.segments/` suffixes or a trailing slash. */
26
+ export function prefetchPagePath(path) {
27
+ let bare = (path.split(/[?#]/, 1)[0] ?? '').replace(/\.segments\/.*$/, '').replace(/(?:\.prefetch)?\.rsc$/, '');
28
+ if (bare === '/index' || bare.endsWith('/index'))
29
+ bare = bare.slice(0, -'/index'.length);
30
+ try {
31
+ bare = decodeURI(bare);
32
+ }
33
+ catch {
34
+ // keep the encoded form
35
+ }
36
+ return bare.replace(/\/+$/, '') || '/';
37
+ }
38
+ /**
39
+ * The page a request prefetches, or null when it is not a Next prefetch of this site. A prefetch is a GET for the
40
+ * React Server Components payload (`rsc: 1`, `next-router-prefetch`, a segment prefetch header, an `_rsc` query or an
41
+ * `.rsc` path) on the site's own origin.
42
+ */
43
+ export function prefetchedPage(request, origin) {
44
+ if (request.method !== 'GET')
45
+ return null;
46
+ let url;
47
+ try {
48
+ url = new URL(request.url);
49
+ }
50
+ catch {
51
+ return null;
52
+ }
53
+ if (url.origin !== origin)
54
+ return null;
55
+ const headers = Object.fromEntries(Object.entries(request.headers).map(([name, value]) => [name.toLowerCase(), value]));
56
+ const rsc = headers.rsc === '1'
57
+ || 'next-router-prefetch' in headers
58
+ || 'next-router-segment-prefetch' in headers
59
+ || url.searchParams.has('_rsc')
60
+ || /\.rsc$/.test(url.pathname);
61
+ return rsc ? prefetchPagePath(url.pathname) : null;
62
+ }
63
+ function kilobytes(bytes) {
64
+ return `${Math.round(bytes / 102.4) / 10} KB`;
65
+ }
66
+ function list(paths, max = 6) {
67
+ const shown = paths.slice(0, max).join(', ');
68
+ return paths.length > max ? `${shown} and ${paths.length - max} more` : shown;
69
+ }
70
+ /** Why a prefetched page breaks the policy, or null when a showing link outside the off roles explains it. */
71
+ function offenceFor(path, page, links) {
72
+ if (path === page)
73
+ return 'the current page';
74
+ const linked = links.filter((link) => link.path === path);
75
+ if (linked.length === 0)
76
+ return null;
77
+ if (linked.some((link) => link.rendered && link.roles.length === 0))
78
+ return null;
79
+ const roles = [...new Set(linked.filter((link) => link.rendered).flatMap((link) => link.roles))];
80
+ if (roles.length > 0)
81
+ return `${roles.join(' and ')} links`;
82
+ const hiddenRoles = [...new Set(linked.flatMap((link) => link.roles))];
83
+ return hiddenRoles.length > 0 ? `a hidden link (${hiddenRoles.join(' and ')})` : 'a hidden link';
84
+ }
85
+ /** W2's verdict for one page at one viewport. */
86
+ export function evaluatePrefetch(measurement, viewport) {
87
+ const page = prefetchPagePath(measurement.page);
88
+ const landing = [...new Set(measurement.landing.map(prefetchPagePath))];
89
+ const scrolled = [...new Set([...landing, ...measurement.scrolled.map(prefetchPagePath)])];
90
+ const failures = [];
91
+ if (landing.length > PREFETCH_LANDING_LIMIT) {
92
+ failures.push(`${viewport}: ${landing.length} pages prefetched on landing, more than ${PREFETCH_LANDING_LIMIT} (${list(landing)})`);
93
+ }
94
+ if (scrolled.length > PREFETCH_SCROLLED_LIMIT) {
95
+ failures.push(`${viewport}: ${scrolled.length} pages prefetched after a full scroll, more than ${PREFETCH_SCROLLED_LIMIT} (${list(scrolled)})`);
96
+ }
97
+ const offences = new Map();
98
+ for (const path of scrolled) {
99
+ const offence = offenceFor(path, page, measurement.links);
100
+ if (offence)
101
+ offences.set(offence, [...(offences.get(offence) ?? []), path]);
102
+ }
103
+ for (const [offence, paths] of offences) {
104
+ failures.push(offence === 'the current page' ? `${viewport}: prefetched the current page (${page})` : `${viewport}: prefetched from ${offence}: ${list(paths)}`);
105
+ }
106
+ const { prefetchCompressed, prefetchDecoded, htmlCompressed } = measurement.bytes;
107
+ const warnings = [];
108
+ if (htmlCompressed > 0 && prefetchCompressed > PREFETCH_BYTES_WARNING_RATIO * htmlCompressed) {
109
+ warnings.push(`Warning: ${viewport}: prefetches carried ${kilobytes(prefetchCompressed)} compressed, more than twice the page's own HTML (${kilobytes(htmlCompressed)}).`);
110
+ }
111
+ const summary = `${viewport}: ${landing.length} on landing, ${scrolled.length} after a full scroll, ${kilobytes(prefetchCompressed)} compressed (${kilobytes(prefetchDecoded)} decoded)`;
112
+ return { failures, warnings, summary };
113
+ }
114
+ /**
115
+ * Every same-site link on the page with its off roles and whether it shows. Runs in the page: it uses only its
116
+ * argument and the DOM, plus the site-system page helpers' `isRendered` when they are installed.
117
+ */
118
+ export function collectPrefetchLinks(containers) {
119
+ const helpers = window.__brandlabSiteSystem;
120
+ const shows = (element) => {
121
+ if (helpers)
122
+ return helpers.isRendered(element);
123
+ const rect = element.getBoundingClientRect();
124
+ const style = getComputedStyle(element);
125
+ return rect.width > 0 && rect.height > 0 && style.visibility === 'visible' && style.opacity !== '0';
126
+ };
127
+ const links = [];
128
+ for (const anchor of Array.from(document.querySelectorAll('a[href]'))) {
129
+ let url;
130
+ try {
131
+ url = new URL(anchor.getAttribute('href') ?? '', window.location.href);
132
+ }
133
+ catch {
134
+ continue;
135
+ }
136
+ if (url.origin !== window.location.origin)
137
+ continue;
138
+ const roles = Object.entries(containers).filter(([, selector]) => anchor.closest(selector) !== null).map(([role]) => role);
139
+ links.push({ path: url.pathname, roles, rendered: shows(anchor) });
140
+ }
141
+ return links;
142
+ }
143
+ /** The links as `evaluatePrefetch` reads them: paths normalised, unknown roles dropped. */
144
+ export function toPrefetchLinks(raw) {
145
+ const known = new Set(Object.keys(PREFETCH_OFF_CONTAINERS));
146
+ return raw.map((link) => ({
147
+ path: prefetchPagePath(link.path),
148
+ roles: link.roles.filter((role) => known.has(role)),
149
+ rendered: link.rendered,
150
+ }));
151
+ }
152
+ //# sourceMappingURL=prefetch-policy.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"prefetch-policy.js","sourceRoot":"","sources":["../../../src/site-system/journeys/prefetch-policy.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * W2's rules, pure so they are unit-tested without a browser: which requests are Next prefetches, which page each\n * one fetches, which link roles must never prefetch, and the verdict for one page at one viewport. The journey and\n * `brandlab-prefetch-probe` share them. The roles mirror site-next's `LINK_PREFETCH_POLICY` (3.13.0).\n */\n\n/** At most this many distinct pages prefetched on landing, and after a full scroll. */\nexport const PREFETCH_LANDING_LIMIT = 8;\nexport const PREFETCH_SCROLLED_LIMIT = 12;\n/** Compressed prefetch bytes above this multiple of the page's own compressed HTML earn a warning (advisory). */\nexport const PREFETCH_BYTES_WARNING_RATIO = 2;\n\n/**\n * Containers whose links must never prefetch, by role. Shared components carry `data-bl` hooks and sections their\n * matrix id; a site marks its own lists with `data-bl-links`.\n */\nexport const PREFETCH_OFF_CONTAINERS = Object.freeze({\n footer: '[data-bl=\"footer\"], [role=\"contentinfo\"], footer:not(article footer, aside footer, main footer, nav footer, section footer, dialog footer)',\n 'header panel': '[data-bl=\"header\"] [data-slot=\"panel\"]',\n drawer: '[data-bl=\"drawer\"], dialog',\n area: '[data-bl-section=\"coverage\"], [data-bl-links=\"area\"]',\n nearby: '[data-bl-section=\"nearby-areas\"], [data-bl-links=\"nearby\"]',\n map: '[data-bl-section=\"area-map\"], [data-area-map], [data-bl-links=\"map\"]',\n finder: '[data-bl-links=\"finder\"]',\n 'other services': '[data-bl-links=\"other-services\"]',\n} as const);\n\nexport type PrefetchOffRole = keyof typeof PREFETCH_OFF_CONTAINERS;\n\n/** One same-site link on the page after the full scroll. */\nexport interface PrefetchLink {\n /** The page it points to, normalised as `prefetchPagePath` does. */\n readonly path: string;\n /** The off roles of the containers it sits in (empty for a link that may prefetch). */\n readonly roles: readonly PrefetchOffRole[];\n /** Whether it shows (not hidden, transparent, collapsed or zero-size). */\n readonly rendered: boolean;\n}\n\nexport interface PrefetchMeasurement {\n /** The sampled page, normalised. */\n readonly page: string;\n /** Distinct pages prefetched before any scroll. */\n readonly landing: readonly string[];\n /** Distinct pages prefetched by the end of a full scroll (landing included). */\n readonly scrolled: readonly string[];\n readonly links: readonly PrefetchLink[];\n /** Compressed and decoded bytes of every prefetch response, and the page's own compressed HTML (0 when unknown). */\n readonly bytes: { readonly prefetchCompressed: number; readonly prefetchDecoded: number; readonly htmlCompressed: number };\n}\n\nexport interface PrefetchVerdict {\n readonly failures: readonly string[];\n readonly warnings: readonly string[];\n /** One line for the report, whatever the result. */\n readonly summary: string;\n}\n\n/** A page path without query, fragment, Next's `.rsc` and `.segments/` suffixes or a trailing slash. */\nexport function prefetchPagePath(path: string): string {\n let bare = (path.split(/[?#]/, 1)[0] ?? '').replace(/\\.segments\\/.*$/, '').replace(/(?:\\.prefetch)?\\.rsc$/, '');\n if (bare === '/index' || bare.endsWith('/index')) bare = bare.slice(0, -'/index'.length);\n try {\n bare = decodeURI(bare);\n } catch {\n // keep the encoded form\n }\n return bare.replace(/\\/+$/, '') || '/';\n}\n\n/**\n * The page a request prefetches, or null when it is not a Next prefetch of this site. A prefetch is a GET for the\n * React Server Components payload (`rsc: 1`, `next-router-prefetch`, a segment prefetch header, an `_rsc` query or an\n * `.rsc` path) on the site's own origin.\n */\nexport function prefetchedPage(request: { url: string; method: string; headers: Readonly<Record<string, string>> }, origin: string): string | null {\n if (request.method !== 'GET') return null;\n let url: URL;\n try {\n url = new URL(request.url);\n } catch {\n return null;\n }\n if (url.origin !== origin) return null;\n const headers = Object.fromEntries(Object.entries(request.headers).map(([name, value]) => [name.toLowerCase(), value]));\n const rsc = headers.rsc === '1'\n || 'next-router-prefetch' in headers\n || 'next-router-segment-prefetch' in headers\n || url.searchParams.has('_rsc')\n || /\\.rsc$/.test(url.pathname);\n return rsc ? prefetchPagePath(url.pathname) : null;\n}\n\nfunction kilobytes(bytes: number): string {\n return `${Math.round(bytes / 102.4) / 10} KB`;\n}\n\nfunction list(paths: readonly string[], max = 6): string {\n const shown = paths.slice(0, max).join(', ');\n return paths.length > max ? `${shown} and ${paths.length - max} more` : shown;\n}\n\n/** Why a prefetched page breaks the policy, or null when a showing link outside the off roles explains it. */\nfunction offenceFor(path: string, page: string, links: readonly PrefetchLink[]): string | null {\n if (path === page) return 'the current page';\n const linked = links.filter((link) => link.path === path);\n if (linked.length === 0) return null;\n if (linked.some((link) => link.rendered && link.roles.length === 0)) return null;\n const roles = [...new Set(linked.filter((link) => link.rendered).flatMap((link) => link.roles))];\n if (roles.length > 0) return `${roles.join(' and ')} links`;\n const hiddenRoles = [...new Set(linked.flatMap((link) => link.roles))];\n return hiddenRoles.length > 0 ? `a hidden link (${hiddenRoles.join(' and ')})` : 'a hidden link';\n}\n\n/** W2's verdict for one page at one viewport. */\nexport function evaluatePrefetch(measurement: PrefetchMeasurement, viewport: string): PrefetchVerdict {\n const page = prefetchPagePath(measurement.page);\n const landing = [...new Set(measurement.landing.map(prefetchPagePath))];\n const scrolled = [...new Set([...landing, ...measurement.scrolled.map(prefetchPagePath)])];\n const failures: string[] = [];\n if (landing.length > PREFETCH_LANDING_LIMIT) {\n failures.push(`${viewport}: ${landing.length} pages prefetched on landing, more than ${PREFETCH_LANDING_LIMIT} (${list(landing)})`);\n }\n if (scrolled.length > PREFETCH_SCROLLED_LIMIT) {\n failures.push(`${viewport}: ${scrolled.length} pages prefetched after a full scroll, more than ${PREFETCH_SCROLLED_LIMIT} (${list(scrolled)})`);\n }\n const offences = new Map<string, string[]>();\n for (const path of scrolled) {\n const offence = offenceFor(path, page, measurement.links);\n if (offence) offences.set(offence, [...(offences.get(offence) ?? []), path]);\n }\n for (const [offence, paths] of offences) {\n failures.push(offence === 'the current page' ? `${viewport}: prefetched the current page (${page})` : `${viewport}: prefetched from ${offence}: ${list(paths)}`);\n }\n const { prefetchCompressed, prefetchDecoded, htmlCompressed } = measurement.bytes;\n const warnings: string[] = [];\n if (htmlCompressed > 0 && prefetchCompressed > PREFETCH_BYTES_WARNING_RATIO * htmlCompressed) {\n warnings.push(\n `Warning: ${viewport}: prefetches carried ${kilobytes(prefetchCompressed)} compressed, more than twice the page's own HTML (${kilobytes(htmlCompressed)}).`,\n );\n }\n const summary = `${viewport}: ${landing.length} on landing, ${scrolled.length} after a full scroll, ${kilobytes(prefetchCompressed)} compressed (${kilobytes(prefetchDecoded)} decoded)`;\n return { failures, warnings, summary };\n}\n\n/**\n * Every same-site link on the page with its off roles and whether it shows. Runs in the page: it uses only its\n * argument and the DOM, plus the site-system page helpers' `isRendered` when they are installed.\n */\nexport function collectPrefetchLinks(containers: Readonly<Record<string, string>>): { path: string; roles: string[]; rendered: boolean }[] {\n const helpers = (window as unknown as { __brandlabSiteSystem?: { isRendered(element: Element): boolean } }).__brandlabSiteSystem;\n const shows = (element: Element) => {\n if (helpers) return helpers.isRendered(element);\n const rect = element.getBoundingClientRect();\n const style = getComputedStyle(element);\n return rect.width > 0 && rect.height > 0 && style.visibility === 'visible' && style.opacity !== '0';\n };\n const links: { path: string; roles: string[]; rendered: boolean }[] = [];\n for (const anchor of Array.from(document.querySelectorAll('a[href]'))) {\n let url: URL;\n try {\n url = new URL(anchor.getAttribute('href') ?? '', window.location.href);\n } catch {\n continue;\n }\n if (url.origin !== window.location.origin) continue;\n const roles = Object.entries(containers).filter(([, selector]) => anchor.closest(selector) !== null).map(([role]) => role);\n links.push({ path: url.pathname, roles, rendered: shows(anchor) });\n }\n return links;\n}\n\n/** The links as `evaluatePrefetch` reads them: paths normalised, unknown roles dropped. */\nexport function toPrefetchLinks(raw: readonly { path: string; roles: readonly string[]; rendered: boolean }[]): PrefetchLink[] {\n const known = new Set<string>(Object.keys(PREFETCH_OFF_CONTAINERS));\n return raw.map((link) => ({\n path: prefetchPagePath(link.path),\n roles: link.roles.filter((role): role is PrefetchOffRole => known.has(role)),\n rendered: link.rendered,\n }));\n}\n"]}
@@ -0,0 +1,3 @@
1
+ import { type SiteSystemJourneyPlan } from './plan.js';
2
+ export declare function definePrefetchJourneys(plan: SiteSystemJourneyPlan, runs: ReadonlySet<string>): void;
3
+ //# sourceMappingURL=prefetch.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"prefetch.d.ts","sourceRoot":"","sources":["../../../src/site-system/journeys/prefetch.ts"],"names":[],"mappings":"AASA,OAAO,EAA6F,KAAK,qBAAqB,EAAE,MAAM,WAAW,CAAC;AAMlJ,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,qBAAqB,EAAE,IAAI,EAAE,WAAW,CAAC,MAAM,CAAC,GAAG,IAAI,CAoBnG"}
@@ -0,0 +1,37 @@
1
+ /**
2
+ * W2 (shell tier, from 3.13.0): prefetch by link role. On each sampled page at 390 x 844 and 1440 x 900 the
3
+ * production build may prefetch at most 8 distinct pages on landing and 12 after a full scroll, never from footer,
4
+ * header panel, drawer, area, nearby, map, finder or other-services links, never through a hidden link and never the
5
+ * current page. Compressed prefetch bytes are advisory: above twice the page's own compressed HTML the run notes a
6
+ * warning. Runs inside the existing site-system journeys, so it needs no CI job of its own.
7
+ */
8
+ import { expect, test } from '@playwright/test';
9
+ import { note, openSitePage, raiseTimeout } from './browser.js';
10
+ import { SITE_SYSTEM_DESKTOP_VIEWPORT, SITE_SYSTEM_PHONE_VIEWPORT, siteSystemJourneyTitle as title } from './plan.js';
11
+ import { measurePrefetch } from './prefetch-measure.js';
12
+ import { evaluatePrefetch } from './prefetch-policy.js';
13
+ const VIEWPORTS = [SITE_SYSTEM_PHONE_VIEWPORT, SITE_SYSTEM_DESKTOP_VIEWPORT];
14
+ export function definePrefetchJourneys(plan, runs) {
15
+ if (!runs.has('W2'))
16
+ return;
17
+ test.describe('prefetch by link role', () => {
18
+ for (const path of plan.sampled) {
19
+ test(title('W2', `${path} prefetches by link role at 390 x 844 and 1440 x 900`), async ({ page }, testInfo) => {
20
+ raiseTimeout(testInfo, 4 * plan.navigationTimeoutMs + 60_000);
21
+ const failures = [];
22
+ for (const viewport of VIEWPORTS) {
23
+ const label = `${viewport.width} x ${viewport.height}`;
24
+ await page.setViewportSize({ ...viewport });
25
+ const measurement = await measurePrefetch(page, path, { open: (target, at) => openSitePage(target, at, plan) });
26
+ const verdict = evaluatePrefetch(measurement, label);
27
+ note(testInfo, `${path} ${verdict.summary}`);
28
+ for (const warning of verdict.warnings)
29
+ note(testInfo, warning);
30
+ failures.push(...verdict.failures);
31
+ }
32
+ expect(failures, `${path} breaks the prefetch policy`).toEqual([]);
33
+ });
34
+ }
35
+ });
36
+ }
37
+ //# sourceMappingURL=prefetch.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"prefetch.js","sourceRoot":"","sources":["../../../src/site-system/journeys/prefetch.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,kBAAkB,CAAC;AAChD,OAAO,EAAE,IAAI,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAChE,OAAO,EAAE,4BAA4B,EAAE,0BAA0B,EAAE,sBAAsB,IAAI,KAAK,EAA8B,MAAM,WAAW,CAAC;AAClJ,OAAO,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AACxD,OAAO,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAC;AAExD,MAAM,SAAS,GAAG,CAAC,0BAA0B,EAAE,4BAA4B,CAAU,CAAC;AAEtF,MAAM,UAAU,sBAAsB,CAAC,IAA2B,EAAE,IAAyB;IAC3F,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC;QAAE,OAAO;IAC5B,IAAI,CAAC,QAAQ,CAAC,uBAAuB,EAAE,GAAG,EAAE;QAC1C,KAAK,MAAM,IAAI,IAAI,IAAI,CAAC,OAAO,EAAE,CAAC;YAChC,IAAI,CAAC,KAAK,CAAC,IAAI,EAAE,GAAG,IAAI,sDAAsD,CAAC,EAAE,KAAK,EAAE,EAAE,IAAI,EAAE,EAAE,QAAQ,EAAE,EAAE;gBAC5G,YAAY,CAAC,QAAQ,EAAE,CAAC,GAAG,IAAI,CAAC,mBAAmB,GAAG,MAAM,CAAC,CAAC;gBAC9D,MAAM,QAAQ,GAAa,EAAE,CAAC;gBAC9B,KAAK,MAAM,QAAQ,IAAI,SAAS,EAAE,CAAC;oBACjC,MAAM,KAAK,GAAG,GAAG,QAAQ,CAAC,KAAK,MAAM,QAAQ,CAAC,MAAM,EAAE,CAAC;oBACvD,MAAM,IAAI,CAAC,eAAe,CAAC,EAAE,GAAG,QAAQ,EAAE,CAAC,CAAC;oBAC5C,MAAM,WAAW,GAAG,MAAM,eAAe,CAAC,IAAI,EAAE,IAAI,EAAE,EAAE,IAAI,EAAE,CAAC,MAAM,EAAE,EAAE,EAAE,EAAE,CAAC,YAAY,CAAC,MAAM,EAAE,EAAE,EAAE,IAAI,CAAC,EAAE,CAAC,CAAC;oBAChH,MAAM,OAAO,GAAG,gBAAgB,CAAC,WAAW,EAAE,KAAK,CAAC,CAAC;oBACrD,IAAI,CAAC,QAAQ,EAAE,GAAG,IAAI,IAAI,OAAO,CAAC,OAAO,EAAE,CAAC,CAAC;oBAC7C,KAAK,MAAM,OAAO,IAAI,OAAO,CAAC,QAAQ;wBAAE,IAAI,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC;oBAChE,QAAQ,CAAC,IAAI,CAAC,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAC;gBACrC,CAAC;gBACD,MAAM,CAAC,QAAQ,EAAE,GAAG,IAAI,6BAA6B,CAAC,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC;YACrE,CAAC,CAAC,CAAC;QACL,CAAC;IACH,CAAC,CAAC,CAAC;AACL,CAAC","sourcesContent":["/**\n * W2 (shell tier, from 3.13.0): prefetch by link role. On each sampled page at 390 x 844 and 1440 x 900 the\n * production build may prefetch at most 8 distinct pages on landing and 12 after a full scroll, never from footer,\n * header panel, drawer, area, nearby, map, finder or other-services links, never through a hidden link and never the\n * current page. Compressed prefetch bytes are advisory: above twice the page's own compressed HTML the run notes a\n * warning. Runs inside the existing site-system journeys, so it needs no CI job of its own.\n */\nimport { expect, test } from '@playwright/test';\nimport { note, openSitePage, raiseTimeout } from './browser.js';\nimport { SITE_SYSTEM_DESKTOP_VIEWPORT, SITE_SYSTEM_PHONE_VIEWPORT, siteSystemJourneyTitle as title, type SiteSystemJourneyPlan } from './plan.js';\nimport { measurePrefetch } from './prefetch-measure.js';\nimport { evaluatePrefetch } from './prefetch-policy.js';\n\nconst VIEWPORTS = [SITE_SYSTEM_PHONE_VIEWPORT, SITE_SYSTEM_DESKTOP_VIEWPORT] as const;\n\nexport function definePrefetchJourneys(plan: SiteSystemJourneyPlan, runs: ReadonlySet<string>): void {\n if (!runs.has('W2')) return;\n test.describe('prefetch by link role', () => {\n for (const path of plan.sampled) {\n test(title('W2', `${path} prefetches by link role at 390 x 844 and 1440 x 900`), async ({ page }, testInfo) => {\n raiseTimeout(testInfo, 4 * plan.navigationTimeoutMs + 60_000);\n const failures: string[] = [];\n for (const viewport of VIEWPORTS) {\n const label = `${viewport.width} x ${viewport.height}`;\n await page.setViewportSize({ ...viewport });\n const measurement = await measurePrefetch(page, path, { open: (target, at) => openSitePage(target, at, plan) });\n const verdict = evaluatePrefetch(measurement, label);\n note(testInfo, `${path} ${verdict.summary}`);\n for (const warning of verdict.warnings) note(testInfo, warning);\n failures.push(...verdict.failures);\n }\n expect(failures, `${path} breaks the prefetch policy`).toEqual([]);\n });\n }\n });\n}\n"]}
@@ -0,0 +1,34 @@
1
+ import { type SiteSystemJourneyPlan } from './plan.js';
2
+ /** A slug no Brandlab site builds. */
3
+ export declare const W9_UNKNOWN_SLUG = "brandlab-w9-no-such-page";
4
+ /** The query a tracking link adds. */
5
+ export declare const W9_TRACKING_QUERY = "utm_source=x";
6
+ /** The headers that say whether and how a response is cached. */
7
+ export declare const W9_CACHE_HEADERS: readonly ["cache-control", "x-nextjs-cache", "x-nextjs-prerender", "x-vercel-cache"];
8
+ export interface W9Variants {
9
+ readonly unknown: string;
10
+ /** Null when the last segment has no letters to uppercase. */
11
+ readonly uppercase: string | null;
12
+ readonly tracking: string;
13
+ }
14
+ export declare function urlVariants(path: string): W9Variants;
15
+ export interface W9Response {
16
+ readonly status: number;
17
+ /** Lower-cased header names. */
18
+ readonly headers: Readonly<Record<string, string>>;
19
+ readonly html: string;
20
+ /** The requested URL, to resolve a relative `location` against. */
21
+ readonly url?: string;
22
+ }
23
+ /** What is wrong with a variant that should be the shared 404, judged against a missing path at the site root. */
24
+ export declare function notFoundVariantProblems(label: string, path: string, variant: W9Response, reference: W9Response): string[];
25
+ /**
26
+ * What is wrong with the uppercase variant: the shared 404 passes, and so does one permanent redirect (301 or 308)
27
+ * to the exact lowercase form of the same path on the same site (no query, no fragment). Any other redirect or a 200
28
+ * fails as for the unknown slug.
29
+ */
30
+ export declare function uppercaseVariantProblems(path: string, variant: W9Response, reference: W9Response): string[];
31
+ /** What is wrong with the tracking-query variant against the clean URL. */
32
+ export declare function trackingVariantProblems(path: string, clean: W9Response, tracked: W9Response): string[];
33
+ export declare function defineVariantJourneys(plan: SiteSystemJourneyPlan, runs: ReadonlySet<string>): void;
34
+ //# sourceMappingURL=variants.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"variants.d.ts","sourceRoot":"","sources":["../../../src/site-system/journeys/variants.ts"],"names":[],"mappings":"AAUA,OAAO,EAAmC,KAAK,qBAAqB,EAAE,MAAM,WAAW,CAAC;AAExF,sCAAsC;AACtC,eAAO,MAAM,eAAe,6BAA6B,CAAC;AAC1D,sCAAsC;AACtC,eAAO,MAAM,iBAAiB,iBAAiB,CAAC;AAChD,iEAAiE;AACjE,eAAO,MAAM,gBAAgB,sFAAuF,CAAC;AAErH,MAAM,WAAW,UAAU;IACzB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,8DAA8D;IAC9D,QAAQ,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IAClC,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;CAC3B;AAED,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,UAAU,CAWpD;AAED,MAAM,WAAW,UAAU;IACzB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,gCAAgC;IAChC,QAAQ,CAAC,OAAO,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;IACnD,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,mEAAmE;IACnE,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC;CACvB;AAaD,kHAAkH;AAClH,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,UAAU,EAAE,SAAS,EAAE,UAAU,GAAG,MAAM,EAAE,CASzH;AAED;;;;GAIG;AACH,wBAAgB,wBAAwB,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,UAAU,EAAE,SAAS,EAAE,UAAU,GAAG,MAAM,EAAE,CAe3G;AAED,2EAA2E;AAC3E,wBAAgB,uBAAuB,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,UAAU,EAAE,OAAO,EAAE,UAAU,GAAG,MAAM,EAAE,CAoBtG;AASD,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,qBAAqB,EAAE,IAAI,EAAE,WAAW,CAAC,MAAM,CAAC,GAAG,IAAI,CAyBlG"}
@@ -0,0 +1,139 @@
1
+ /**
2
+ * W9 (full tier, from 3.13.0): URL variants never cost an origin render. For one built page per dynamic route family:
3
+ * - an unknown slug answers the shared not-found page (404, the same page as any missing path), not a 200, a redirect
4
+ * or a family-specific page; the uppercase slug answers the same 404, or one permanent redirect (301 or 308) straight
5
+ * to its exact lowercase form on the same site, which the CDN caches;
6
+ * - a tracking query (`?utm_source=x`) serves the same cacheable page as the clean URL: same status, canonical and
7
+ * cache headers, never `private` or `no-store`.
8
+ * The requests go straight to the build under test; no browser is needed. The pure parts are unit-tested.
9
+ */
10
+ import { expect, test } from '@playwright/test';
11
+ import { siteSystemJourneyTitle as title } from './plan.js';
12
+ /** A slug no Brandlab site builds. */
13
+ export const W9_UNKNOWN_SLUG = 'brandlab-w9-no-such-page';
14
+ /** The query a tracking link adds. */
15
+ export const W9_TRACKING_QUERY = 'utm_source=x';
16
+ /** The headers that say whether and how a response is cached. */
17
+ export const W9_CACHE_HEADERS = ['cache-control', 'x-nextjs-cache', 'x-nextjs-prerender', 'x-vercel-cache'];
18
+ export function urlVariants(path) {
19
+ const clean = path.replace(/\/+$/, '');
20
+ const cut = clean.lastIndexOf('/');
21
+ const parent = clean.slice(0, cut);
22
+ const slug = clean.slice(cut + 1);
23
+ const upper = slug.toUpperCase();
24
+ return {
25
+ unknown: `${parent}/${W9_UNKNOWN_SLUG}`,
26
+ uppercase: upper === slug ? null : `${parent}/${upper}`,
27
+ tracking: `${clean}?${W9_TRACKING_QUERY}`,
28
+ };
29
+ }
30
+ function documentTitle(html) {
31
+ return /<title[^>]*>([\s\S]*?)<\/title>/i.exec(html)?.[1]?.replace(/\s+/g, ' ').trim() ?? '';
32
+ }
33
+ function canonical(html) {
34
+ for (const tag of html.match(/<link\b[^>]*>/gi) ?? []) {
35
+ if (/\brel=["']?canonical\b/i.test(tag))
36
+ return /\bhref=["']([^"']*)["']/i.exec(tag)?.[1] ?? null;
37
+ }
38
+ return null;
39
+ }
40
+ /** What is wrong with a variant that should be the shared 404, judged against a missing path at the site root. */
41
+ export function notFoundVariantProblems(label, path, variant, reference) {
42
+ if (variant.status >= 300 && variant.status < 400) {
43
+ return [`${label} ${path} redirects (${variant.status} to ${variant.headers.location ?? 'nowhere'}) instead of answering the shared 404`];
44
+ }
45
+ if (variant.status !== 404)
46
+ return [`${label} ${path} answers ${variant.status}, not 404`];
47
+ if (reference.status === 404 && documentTitle(variant.html) !== documentTitle(reference.html)) {
48
+ return [`${label} ${path} answers a 404 titled "${documentTitle(variant.html)}", not the shared not-found page ("${documentTitle(reference.html)}")`];
49
+ }
50
+ return [];
51
+ }
52
+ /**
53
+ * What is wrong with the uppercase variant: the shared 404 passes, and so does one permanent redirect (301 or 308)
54
+ * to the exact lowercase form of the same path on the same site (no query, no fragment). Any other redirect or a 200
55
+ * fails as for the unknown slug.
56
+ */
57
+ export function uppercaseVariantProblems(path, variant, reference) {
58
+ if (variant.status === 301 || variant.status === 308) {
59
+ const cut = path.lastIndexOf('/');
60
+ const lowercase = `${path.slice(0, cut)}/${path.slice(cut + 1).toLowerCase()}`;
61
+ const base = new URL(variant.url ?? path, 'http://w9.invalid');
62
+ let target = null;
63
+ try {
64
+ target = variant.headers.location ? new URL(variant.headers.location, base) : null;
65
+ }
66
+ catch {
67
+ target = null;
68
+ }
69
+ if (target && target.origin === base.origin && target.pathname === lowercase && !target.search && !target.hash)
70
+ return [];
71
+ return [`the uppercase slug ${path} redirects (${variant.status} to ${variant.headers.location ?? 'nowhere'}) somewhere other than its lowercase form ${lowercase}`];
72
+ }
73
+ return notFoundVariantProblems('the uppercase slug', path, variant, reference);
74
+ }
75
+ /** What is wrong with the tracking-query variant against the clean URL. */
76
+ export function trackingVariantProblems(path, clean, tracked) {
77
+ const problems = [];
78
+ const query = `${path}?${W9_TRACKING_QUERY}`;
79
+ if (tracked.status !== clean.status)
80
+ problems.push(`${query} answers ${tracked.status} where ${path} answers ${clean.status}`);
81
+ const cacheControl = tracked.headers['cache-control'] ?? '';
82
+ if (/\b(?:private|no-store)\b/i.test(cacheControl))
83
+ problems.push(`${query} is not cacheable (cache-control "${cacheControl}"): the page reads the query, so every tracking link renders it at the origin`);
84
+ for (const name of W9_CACHE_HEADERS) {
85
+ const before = clean.headers[name];
86
+ const after = tracked.headers[name];
87
+ if (name === 'x-vercel-cache' || name === 'x-nextjs-cache') {
88
+ // HIT, MISS and STALE differ between two requests; only the header's presence says the response is cached.
89
+ if (before !== undefined && after === undefined)
90
+ problems.push(`${query} has no ${name} header where ${path} has "${before}"`);
91
+ }
92
+ else if (before !== after) {
93
+ problems.push(`${query} sends ${name} ${after === undefined ? 'none' : `"${after}"`} where ${path} sends ${before === undefined ? 'none' : `"${before}"`}`);
94
+ }
95
+ }
96
+ if (canonical(clean.html) !== canonical(tracked.html)) {
97
+ problems.push(`${query} names the canonical ${canonical(tracked.html) ?? 'none'} where ${path} names ${canonical(clean.html) ?? 'none'}`);
98
+ }
99
+ return problems;
100
+ }
101
+ async function read(response) {
102
+ const headers = {};
103
+ for (const [name, value] of Object.entries(response.headers()))
104
+ headers[name.toLowerCase()] = value;
105
+ const type = headers['content-type'] ?? '';
106
+ return { status: response.status(), headers, html: type.includes('html') ? await response.text() : '', url: response.url() };
107
+ }
108
+ export function defineVariantJourneys(plan, runs) {
109
+ if (!runs.has('W9'))
110
+ return;
111
+ test.describe('URL variants', () => {
112
+ if (plan.problems.W9) {
113
+ const problem = plan.problems.W9;
114
+ test(title('W9', 'the family pages are named'), () => {
115
+ expect([problem]).toEqual([]);
116
+ });
117
+ }
118
+ for (const path of plan.families) {
119
+ test(title('W9', `unknown, uppercase and tracking variants of ${path} cost no origin render`), async ({ request }) => {
120
+ const get = async (target) => read(await request.get(target, { maxRedirects: 0, failOnStatusCode: false, timeout: plan.navigationTimeoutMs }));
121
+ const variants = urlVariants(path);
122
+ const reference = await get(`/${W9_UNKNOWN_SLUG}`);
123
+ const problems = [];
124
+ if (reference.status !== 404)
125
+ problems.push(`/${W9_UNKNOWN_SLUG} answers ${reference.status}, not the shared 404`);
126
+ problems.push(...notFoundVariantProblems('the unknown slug', variants.unknown, await get(variants.unknown), reference));
127
+ if (variants.uppercase)
128
+ problems.push(...uppercaseVariantProblems(variants.uppercase, await get(variants.uppercase), reference));
129
+ const clean = await get(path);
130
+ if (clean.status !== 200)
131
+ problems.push(`${path} answers ${clean.status}, not 200: name a built page of the family`);
132
+ else
133
+ problems.push(...trackingVariantProblems(path, clean, await get(variants.tracking)));
134
+ expect(problems).toEqual([]);
135
+ });
136
+ }
137
+ });
138
+ }
139
+ //# sourceMappingURL=variants.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"variants.js","sourceRoot":"","sources":["../../../src/site-system/journeys/variants.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * W9 (full tier, from 3.13.0): URL variants never cost an origin render. For one built page per dynamic route family:\n * - an unknown slug answers the shared not-found page (404, the same page as any missing path), not a 200, a redirect\n * or a family-specific page; the uppercase slug answers the same 404, or one permanent redirect (301 or 308) straight\n * to its exact lowercase form on the same site, which the CDN caches;\n * - a tracking query (`?utm_source=x`) serves the same cacheable page as the clean URL: same status, canonical and\n * cache headers, never `private` or `no-store`.\n * The requests go straight to the build under test; no browser is needed. The pure parts are unit-tested.\n */\nimport { expect, test, type APIResponse } from '@playwright/test';\nimport { siteSystemJourneyTitle as title, type SiteSystemJourneyPlan } from './plan.js';\n\n/** A slug no Brandlab site builds. */\nexport const W9_UNKNOWN_SLUG = 'brandlab-w9-no-such-page';\n/** The query a tracking link adds. */\nexport const W9_TRACKING_QUERY = 'utm_source=x';\n/** The headers that say whether and how a response is cached. */\nexport const W9_CACHE_HEADERS = ['cache-control', 'x-nextjs-cache', 'x-nextjs-prerender', 'x-vercel-cache'] as const;\n\nexport interface W9Variants {\n readonly unknown: string;\n /** Null when the last segment has no letters to uppercase. */\n readonly uppercase: string | null;\n readonly tracking: string;\n}\n\nexport function urlVariants(path: string): W9Variants {\n const clean = path.replace(/\\/+$/, '');\n const cut = clean.lastIndexOf('/');\n const parent = clean.slice(0, cut);\n const slug = clean.slice(cut + 1);\n const upper = slug.toUpperCase();\n return {\n unknown: `${parent}/${W9_UNKNOWN_SLUG}`,\n uppercase: upper === slug ? null : `${parent}/${upper}`,\n tracking: `${clean}?${W9_TRACKING_QUERY}`,\n };\n}\n\nexport interface W9Response {\n readonly status: number;\n /** Lower-cased header names. */\n readonly headers: Readonly<Record<string, string>>;\n readonly html: string;\n /** The requested URL, to resolve a relative `location` against. */\n readonly url?: string;\n}\n\nfunction documentTitle(html: string): string {\n return /<title[^>]*>([\\s\\S]*?)<\\/title>/i.exec(html)?.[1]?.replace(/\\s+/g, ' ').trim() ?? '';\n}\n\nfunction canonical(html: string): string | null {\n for (const tag of html.match(/<link\\b[^>]*>/gi) ?? []) {\n if (/\\brel=[\"']?canonical\\b/i.test(tag)) return /\\bhref=[\"']([^\"']*)[\"']/i.exec(tag)?.[1] ?? null;\n }\n return null;\n}\n\n/** What is wrong with a variant that should be the shared 404, judged against a missing path at the site root. */\nexport function notFoundVariantProblems(label: string, path: string, variant: W9Response, reference: W9Response): string[] {\n if (variant.status >= 300 && variant.status < 400) {\n return [`${label} ${path} redirects (${variant.status} to ${variant.headers.location ?? 'nowhere'}) instead of answering the shared 404`];\n }\n if (variant.status !== 404) return [`${label} ${path} answers ${variant.status}, not 404`];\n if (reference.status === 404 && documentTitle(variant.html) !== documentTitle(reference.html)) {\n return [`${label} ${path} answers a 404 titled \"${documentTitle(variant.html)}\", not the shared not-found page (\"${documentTitle(reference.html)}\")`];\n }\n return [];\n}\n\n/**\n * What is wrong with the uppercase variant: the shared 404 passes, and so does one permanent redirect (301 or 308)\n * to the exact lowercase form of the same path on the same site (no query, no fragment). Any other redirect or a 200\n * fails as for the unknown slug.\n */\nexport function uppercaseVariantProblems(path: string, variant: W9Response, reference: W9Response): string[] {\n if (variant.status === 301 || variant.status === 308) {\n const cut = path.lastIndexOf('/');\n const lowercase = `${path.slice(0, cut)}/${path.slice(cut + 1).toLowerCase()}`;\n const base = new URL(variant.url ?? path, 'http://w9.invalid');\n let target: URL | null = null;\n try {\n target = variant.headers.location ? new URL(variant.headers.location, base) : null;\n } catch {\n target = null;\n }\n if (target && target.origin === base.origin && target.pathname === lowercase && !target.search && !target.hash) return [];\n return [`the uppercase slug ${path} redirects (${variant.status} to ${variant.headers.location ?? 'nowhere'}) somewhere other than its lowercase form ${lowercase}`];\n }\n return notFoundVariantProblems('the uppercase slug', path, variant, reference);\n}\n\n/** What is wrong with the tracking-query variant against the clean URL. */\nexport function trackingVariantProblems(path: string, clean: W9Response, tracked: W9Response): string[] {\n const problems: string[] = [];\n const query = `${path}?${W9_TRACKING_QUERY}`;\n if (tracked.status !== clean.status) problems.push(`${query} answers ${tracked.status} where ${path} answers ${clean.status}`);\n const cacheControl = tracked.headers['cache-control'] ?? '';\n if (/\\b(?:private|no-store)\\b/i.test(cacheControl)) problems.push(`${query} is not cacheable (cache-control \"${cacheControl}\"): the page reads the query, so every tracking link renders it at the origin`);\n for (const name of W9_CACHE_HEADERS) {\n const before = clean.headers[name];\n const after = tracked.headers[name];\n if (name === 'x-vercel-cache' || name === 'x-nextjs-cache') {\n // HIT, MISS and STALE differ between two requests; only the header's presence says the response is cached.\n if (before !== undefined && after === undefined) problems.push(`${query} has no ${name} header where ${path} has \"${before}\"`);\n } else if (before !== after) {\n problems.push(`${query} sends ${name} ${after === undefined ? 'none' : `\"${after}\"`} where ${path} sends ${before === undefined ? 'none' : `\"${before}\"`}`);\n }\n }\n if (canonical(clean.html) !== canonical(tracked.html)) {\n problems.push(`${query} names the canonical ${canonical(tracked.html) ?? 'none'} where ${path} names ${canonical(clean.html) ?? 'none'}`);\n }\n return problems;\n}\n\nasync function read(response: APIResponse): Promise<W9Response> {\n const headers: Record<string, string> = {};\n for (const [name, value] of Object.entries(response.headers())) headers[name.toLowerCase()] = value;\n const type = headers['content-type'] ?? '';\n return { status: response.status(), headers, html: type.includes('html') ? await response.text() : '', url: response.url() };\n}\n\nexport function defineVariantJourneys(plan: SiteSystemJourneyPlan, runs: ReadonlySet<string>): void {\n if (!runs.has('W9')) return;\n test.describe('URL variants', () => {\n if (plan.problems.W9) {\n const problem = plan.problems.W9;\n test(title('W9', 'the family pages are named'), () => {\n expect([problem]).toEqual([]);\n });\n }\n for (const path of plan.families) {\n test(title('W9', `unknown, uppercase and tracking variants of ${path} cost no origin render`), async ({ request }) => {\n const get = async (target: string) => read(await request.get(target, { maxRedirects: 0, failOnStatusCode: false, timeout: plan.navigationTimeoutMs }));\n const variants = urlVariants(path);\n const reference = await get(`/${W9_UNKNOWN_SLUG}`);\n const problems: string[] = [];\n if (reference.status !== 404) problems.push(`/${W9_UNKNOWN_SLUG} answers ${reference.status}, not the shared 404`);\n problems.push(...notFoundVariantProblems('the unknown slug', variants.unknown, await get(variants.unknown), reference));\n if (variants.uppercase) problems.push(...uppercaseVariantProblems(variants.uppercase, await get(variants.uppercase), reference));\n const clean = await get(path);\n if (clean.status !== 200) problems.push(`${path} answers ${clean.status}, not 200: name a built page of the family`);\n else problems.push(...trackingVariantProblems(path, clean, await get(variants.tracking)));\n expect(problems).toEqual([]);\n });\n }\n });\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"rule-modules.d.ts","sourceRoot":"","sources":["../../src/site-system/rule-modules.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,cAAc,CAAC;AA8B7D,eAAO,MAAM,4BAA4B,EAAE,SAAS,wBAAwB,EAS3E,CAAC"}
1
+ {"version":3,"file":"rule-modules.d.ts","sourceRoot":"","sources":["../../src/site-system/rule-modules.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,cAAc,CAAC;AAkC7D,eAAO,MAAM,4BAA4B,EAAE,SAAS,wBAAwB,EAU3E,CAAC"}
@@ -2,6 +2,7 @@ import { runDesignRules } from "./rules/design/index.js";
2
2
  import { runEnquiryRenderedRules } from "./rules/enquiry/index.js";
3
3
  import { runFormRules } from "./rules/form/index.js";
4
4
  import { runFullTierRules } from "./rules/full/index.js";
5
+ import { runCostGuardRules } from "./rules/guards/index.js";
5
6
  import { runLibraryRules } from "./rules/library/index.js";
6
7
  import { runNavigationRules } from "./rules/navigation/index.js";
7
8
  import { runSectionRules } from "./rules/sections/index.js";
@@ -25,6 +26,9 @@ const sectionRules = function sectionRules(input) {
25
26
  const fullTierRules = function fullTierRules(input) {
26
27
  return runFullTierRules(input);
27
28
  };
29
+ const costGuardRules = function costGuardRules(input) {
30
+ return runCostGuardRules(input);
31
+ };
28
32
  export const SITE_SYSTEM_RENDERED_RUNNERS = [
29
33
  shellRules,
30
34
  enquiryRules,
@@ -34,5 +38,6 @@ export const SITE_SYSTEM_RENDERED_RUNNERS = [
34
38
  formRules,
35
39
  sectionRules,
36
40
  fullTierRules,
41
+ costGuardRules,
37
42
  ];
38
43
  //# sourceMappingURL=rule-modules.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"rule-modules.js","sourceRoot":"","sources":["../../src/site-system/rule-modules.ts"],"names":[],"mappings":"AAMA,OAAO,EAAE,cAAc,EAAE,MAAM,yBAAyB,CAAC;AACzD,OAAO,EAAE,uBAAuB,EAAE,MAAM,0BAA0B,CAAC;AACnE,OAAO,EAAE,YAAY,EAAE,MAAM,uBAAuB,CAAC;AACrD,OAAO,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAC;AACzD,OAAO,EAAE,eAAe,EAAE,MAAM,0BAA0B,CAAC;AAC3D,OAAO,EAAE,kBAAkB,EAAE,MAAM,6BAA6B,CAAC;AACjE,OAAO,EAAE,eAAe,EAAE,MAAM,2BAA2B,CAAC;AAC5D,OAAO,EAAE,qBAAqB,EAAE,MAAM,wBAAwB,CAAC;AAE/D,+DAA+D;AAC/D,MAAM,UAAU,GAA6B,SAAS,UAAU,CAAC,KAAK;IACpE,OAAO,qBAAqB,CAAC,KAAK,CAAC,CAAC;AACtC,CAAC,CAAC;AACF,MAAM,YAAY,GAA6B,SAAS,YAAY,CAAC,KAAK;IACxE,OAAO,uBAAuB,CAAC,KAAK,CAAC,CAAC;AACxC,CAAC,CAAC;AACF,MAAM,YAAY,GAA6B,SAAS,YAAY,CAAC,KAAK;IACxE,OAAO,eAAe,CAAC,KAAK,CAAC,CAAC;AAChC,CAAC,CAAC;AACF,MAAM,SAAS,GAA6B,SAAS,SAAS,CAAC,KAAK;IAClE,OAAO,YAAY,CAAC,KAAK,CAAC,CAAC;AAC7B,CAAC,CAAC;AACF,MAAM,YAAY,GAA6B,SAAS,YAAY,CAAC,KAAK;IACxE,OAAO,eAAe,CAAC,KAAK,CAAC,CAAC;AAChC,CAAC,CAAC;AACF,MAAM,aAAa,GAA6B,SAAS,aAAa,CAAC,KAAK;IAC1E,OAAO,gBAAgB,CAAC,KAAK,CAAC,CAAC;AACjC,CAAC,CAAC;AAEF,MAAM,CAAC,MAAM,4BAA4B,GAAwC;IAC/E,UAAU;IACV,YAAY;IACZ,cAAc;IACd,kBAAkB;IAClB,YAAY;IACZ,SAAS;IACT,YAAY;IACZ,aAAa;CACd,CAAC","sourcesContent":["/**\n * The rule modules `brandlab-ready` runs. Each module under ./rules/ exports a SiteSystemRenderedRunner: plug it in\n * here with one import line and one list entry. Rule modules import from ./runners.js and ./checks.js, never from\n * this file or ./index.js, so the list adds no import cycle.\n */\nimport type { SiteSystemRenderedRunner } from \"./runners.js\";\nimport { runDesignRules } from \"./rules/design/index.js\";\nimport { runEnquiryRenderedRules } from \"./rules/enquiry/index.js\";\nimport { runFormRules } from \"./rules/form/index.js\";\nimport { runFullTierRules } from \"./rules/full/index.js\";\nimport { runLibraryRules } from \"./rules/library/index.js\";\nimport { runNavigationRules } from \"./rules/navigation/index.js\";\nimport { runSectionRules } from \"./rules/sections/index.js\";\nimport { runShellRenderedRules } from \"./rules/shell/index.js\";\n\n// Named wrappers, so a runner that throws is reported by name.\nconst shellRules: SiteSystemRenderedRunner = function shellRules(input) {\n return runShellRenderedRules(input);\n};\nconst enquiryRules: SiteSystemRenderedRunner = function enquiryRules(input) {\n return runEnquiryRenderedRules(input);\n};\nconst libraryRules: SiteSystemRenderedRunner = function libraryRules(input) {\n return runLibraryRules(input);\n};\nconst formRules: SiteSystemRenderedRunner = function formRules(input) {\n return runFormRules(input);\n};\nconst sectionRules: SiteSystemRenderedRunner = function sectionRules(input) {\n return runSectionRules(input);\n};\nconst fullTierRules: SiteSystemRenderedRunner = function fullTierRules(input) {\n return runFullTierRules(input);\n};\n\nexport const SITE_SYSTEM_RENDERED_RUNNERS: readonly SiteSystemRenderedRunner[] = [\n shellRules,\n enquiryRules,\n runDesignRules,\n runNavigationRules,\n libraryRules,\n formRules,\n sectionRules,\n fullTierRules,\n];\n"]}
1
+ {"version":3,"file":"rule-modules.js","sourceRoot":"","sources":["../../src/site-system/rule-modules.ts"],"names":[],"mappings":"AAMA,OAAO,EAAE,cAAc,EAAE,MAAM,yBAAyB,CAAC;AACzD,OAAO,EAAE,uBAAuB,EAAE,MAAM,0BAA0B,CAAC;AACnE,OAAO,EAAE,YAAY,EAAE,MAAM,uBAAuB,CAAC;AACrD,OAAO,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAC;AACzD,OAAO,EAAE,iBAAiB,EAAE,MAAM,yBAAyB,CAAC;AAC5D,OAAO,EAAE,eAAe,EAAE,MAAM,0BAA0B,CAAC;AAC3D,OAAO,EAAE,kBAAkB,EAAE,MAAM,6BAA6B,CAAC;AACjE,OAAO,EAAE,eAAe,EAAE,MAAM,2BAA2B,CAAC;AAC5D,OAAO,EAAE,qBAAqB,EAAE,MAAM,wBAAwB,CAAC;AAE/D,+DAA+D;AAC/D,MAAM,UAAU,GAA6B,SAAS,UAAU,CAAC,KAAK;IACpE,OAAO,qBAAqB,CAAC,KAAK,CAAC,CAAC;AACtC,CAAC,CAAC;AACF,MAAM,YAAY,GAA6B,SAAS,YAAY,CAAC,KAAK;IACxE,OAAO,uBAAuB,CAAC,KAAK,CAAC,CAAC;AACxC,CAAC,CAAC;AACF,MAAM,YAAY,GAA6B,SAAS,YAAY,CAAC,KAAK;IACxE,OAAO,eAAe,CAAC,KAAK,CAAC,CAAC;AAChC,CAAC,CAAC;AACF,MAAM,SAAS,GAA6B,SAAS,SAAS,CAAC,KAAK;IAClE,OAAO,YAAY,CAAC,KAAK,CAAC,CAAC;AAC7B,CAAC,CAAC;AACF,MAAM,YAAY,GAA6B,SAAS,YAAY,CAAC,KAAK;IACxE,OAAO,eAAe,CAAC,KAAK,CAAC,CAAC;AAChC,CAAC,CAAC;AACF,MAAM,aAAa,GAA6B,SAAS,aAAa,CAAC,KAAK;IAC1E,OAAO,gBAAgB,CAAC,KAAK,CAAC,CAAC;AACjC,CAAC,CAAC;AACF,MAAM,cAAc,GAA6B,SAAS,cAAc,CAAC,KAAK;IAC5E,OAAO,iBAAiB,CAAC,KAAK,CAAC,CAAC;AAClC,CAAC,CAAC;AAEF,MAAM,CAAC,MAAM,4BAA4B,GAAwC;IAC/E,UAAU;IACV,YAAY;IACZ,cAAc;IACd,kBAAkB;IAClB,YAAY;IACZ,SAAS;IACT,YAAY;IACZ,aAAa;IACb,cAAc;CACf,CAAC","sourcesContent":["/**\n * The rule modules `brandlab-ready` runs. Each module under ./rules/ exports a SiteSystemRenderedRunner: plug it in\n * here with one import line and one list entry. Rule modules import from ./runners.js and ./checks.js, never from\n * this file or ./index.js, so the list adds no import cycle.\n */\nimport type { SiteSystemRenderedRunner } from \"./runners.js\";\nimport { runDesignRules } from \"./rules/design/index.js\";\nimport { runEnquiryRenderedRules } from \"./rules/enquiry/index.js\";\nimport { runFormRules } from \"./rules/form/index.js\";\nimport { runFullTierRules } from \"./rules/full/index.js\";\nimport { runCostGuardRules } from \"./rules/guards/index.js\";\nimport { runLibraryRules } from \"./rules/library/index.js\";\nimport { runNavigationRules } from \"./rules/navigation/index.js\";\nimport { runSectionRules } from \"./rules/sections/index.js\";\nimport { runShellRenderedRules } from \"./rules/shell/index.js\";\n\n// Named wrappers, so a runner that throws is reported by name.\nconst shellRules: SiteSystemRenderedRunner = function shellRules(input) {\n return runShellRenderedRules(input);\n};\nconst enquiryRules: SiteSystemRenderedRunner = function enquiryRules(input) {\n return runEnquiryRenderedRules(input);\n};\nconst libraryRules: SiteSystemRenderedRunner = function libraryRules(input) {\n return runLibraryRules(input);\n};\nconst formRules: SiteSystemRenderedRunner = function formRules(input) {\n return runFormRules(input);\n};\nconst sectionRules: SiteSystemRenderedRunner = function sectionRules(input) {\n return runSectionRules(input);\n};\nconst fullTierRules: SiteSystemRenderedRunner = function fullTierRules(input) {\n return runFullTierRules(input);\n};\nconst costGuardRules: SiteSystemRenderedRunner = function costGuardRules(input) {\n return runCostGuardRules(input);\n};\n\nexport const SITE_SYSTEM_RENDERED_RUNNERS: readonly SiteSystemRenderedRunner[] = [\n shellRules,\n enquiryRules,\n runDesignRules,\n runNavigationRules,\n libraryRules,\n formRules,\n sectionRules,\n fullTierRules,\n costGuardRules,\n];\n"]}
@@ -1,4 +1,5 @@
1
1
  import type { SiteSystemCheckId, SiteSystemFinding } from "../../checks.js";
2
+ import type { SiteSystemRefreshException } from "../../config.js";
2
3
  import type { Estate } from "./estate.js";
3
4
  import type { FullGeography } from "./geography.js";
4
5
  import type { FullStrategy } from "./strategy.js";
@@ -11,6 +12,8 @@ export interface FullContext {
11
12
  readonly geographyProblem: string;
12
13
  readonly strategy: FullStrategy;
13
14
  readonly projectRoot: string;
15
+ /** Estate families brandlab.config.ts allows to refresh more often than weekly (W3); absent when none. */
16
+ readonly refreshExceptions?: readonly SiteSystemRefreshException[];
14
17
  /** Today in Sydney, YYYY-MM-DD: the day waves are judged on. */
15
18
  readonly today: string;
16
19
  /** A family's canonical sections and cap (site-core `defaultSectionOrder`), or a string saying why they are unknown. */
@@ -1 +1 @@
1
- {"version":3,"file":"common.d.ts","sourceRoot":"","sources":["../../../../src/site-system/rules/full/common.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AAE5E,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAC1C,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AACpD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AAElD,OAAO,EAAE,IAAI,EAAE,aAAa,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,sBAAsB,CAAC;AAEzE,yCAAyC;AACzC,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,0FAA0F;IAC1F,QAAQ,CAAC,SAAS,EAAE,aAAa,GAAG,IAAI,CAAC;IACzC,QAAQ,CAAC,gBAAgB,EAAE,MAAM,CAAC;IAClC,QAAQ,CAAC,QAAQ,EAAE,YAAY,CAAC;IAChC,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,gEAAgE;IAChE,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,wHAAwH;IACxH,QAAQ,CAAC,YAAY,EACjB,CAAC,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,KAAK;QAAE,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,OAAO,EAAE,SAAS;YAAE,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;YAAC,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAA;SAAE,EAAE,CAAA;KAAE,GAAG,SAAS,CAAC,GAC9J,MAAM,CAAC;CACZ;AAED,4FAA4F;AAC5F,eAAO,MAAM,oBAAoB,KAAK,CAAC;AAEvC,4FAA4F;AAC5F,wBAAgB,OAAO,CAAC,OAAO,EAAE,iBAAiB,EAAE,QAAQ,EAAE,SAAS,iBAAiB,EAAE,EAAE,WAAW,EAAE,MAAM,GAAG,iBAAiB,EAAE,CAkBpI;AAED,sFAAsF;AACtF,wBAAgB,OAAO,CAAC,OAAO,EAAE,iBAAiB,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,iBAAiB,CAEtG;AAED,2HAA2H;AAC3H,wBAAgB,UAAU,CAAC,OAAO,EAAE,iBAAiB,EAAE,MAAM,EAAE,MAAM,GAAG,iBAAiB,GAAG,IAAI,CAG/F;AAED,oBAAoB;AACpB,wBAAgB,MAAM,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,EAAE,KAAK,SAAI,GAAG,MAAM,CAKlE;AAED,mEAAmE;AACnE,eAAO,MAAM,aAAa,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAI1D,CAAC"}
1
+ {"version":3,"file":"common.d.ts","sourceRoot":"","sources":["../../../../src/site-system/rules/full/common.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AAE5E,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,iBAAiB,CAAC;AAClE,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAC1C,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AACpD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AAElD,OAAO,EAAE,IAAI,EAAE,aAAa,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,sBAAsB,CAAC;AAEzE,yCAAyC;AACzC,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,0FAA0F;IAC1F,QAAQ,CAAC,SAAS,EAAE,aAAa,GAAG,IAAI,CAAC;IACzC,QAAQ,CAAC,gBAAgB,EAAE,MAAM,CAAC;IAClC,QAAQ,CAAC,QAAQ,EAAE,YAAY,CAAC;IAChC,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,0GAA0G;IAC1G,QAAQ,CAAC,iBAAiB,CAAC,EAAE,SAAS,0BAA0B,EAAE,CAAC;IACnE,gEAAgE;IAChE,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,wHAAwH;IACxH,QAAQ,CAAC,YAAY,EACjB,CAAC,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,KAAK;QAAE,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,OAAO,EAAE,SAAS;YAAE,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;YAAC,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAA;SAAE,EAAE,CAAA;KAAE,GAAG,SAAS,CAAC,GAC9J,MAAM,CAAC;CACZ;AAED,4FAA4F;AAC5F,eAAO,MAAM,oBAAoB,KAAK,CAAC;AAEvC,4FAA4F;AAC5F,wBAAgB,OAAO,CAAC,OAAO,EAAE,iBAAiB,EAAE,QAAQ,EAAE,SAAS,iBAAiB,EAAE,EAAE,WAAW,EAAE,MAAM,GAAG,iBAAiB,EAAE,CAkBpI;AAED,sFAAsF;AACtF,wBAAgB,OAAO,CAAC,OAAO,EAAE,iBAAiB,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,iBAAiB,CAEtG;AAED,2HAA2H;AAC3H,wBAAgB,UAAU,CAAC,OAAO,EAAE,iBAAiB,EAAE,MAAM,EAAE,MAAM,GAAG,iBAAiB,GAAG,IAAI,CAG/F;AAED,oBAAoB;AACpB,wBAAgB,MAAM,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,EAAE,KAAK,SAAI,GAAG,MAAM,CAKlE;AAED,mEAAmE;AACnE,eAAO,MAAM,aAAa,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAI1D,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"common.js","sourceRoot":"","sources":["../../../../src/site-system/rules/full/common.ts"],"names":[],"mappings":"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","sourcesContent":["import type { SiteSystemCheckId, SiteSystemFinding } from \"../../checks.js\";\nimport { fail, pass } from \"../enquiry/common.js\";\nimport type { Estate } from \"./estate.js\";\nimport type { FullGeography } from \"./geography.js\";\nimport type { FullStrategy } from \"./strategy.js\";\n\nexport { fail, notApplicable, pass, plural } from \"../enquiry/common.js\";\n\n/** Everything a full-tier rule reads. */\nexport interface FullContext {\n readonly estate: Estate;\n /** Null when the site-core geography could not be loaded; `geographyProblem` says why. */\n readonly geography: FullGeography | null;\n readonly geographyProblem: string;\n readonly strategy: FullStrategy;\n readonly projectRoot: string;\n /** Today in Sydney, YYYY-MM-DD: the day waves are judged on. */\n readonly today: string;\n /** A family's canonical sections and cap (site-core `defaultSectionOrder`), or a string saying why they are unknown. */\n readonly defaultOrder:\n | ((family: string, trade?: string) => { readonly cap: number; readonly entries: readonly { readonly id: string; readonly requirement: string }[] } | undefined)\n | string;\n}\n\n/** The most failures one check lists one by one; the rest are counted in a last finding. */\nexport const FULL_LISTED_FAILURES = 25;\n\n/** The failures (at most `FULL_LISTED_FAILURES`, then a count of the rest), or one pass. */\nexport function outcome(checkId: SiteSystemCheckId, failures: readonly SiteSystemFinding[], passMessage: string): SiteSystemFinding[] {\n const failed = failures.filter((finding) => finding.status === \"fail\");\n const allWarnings = failures.filter((finding) => finding.status !== \"fail\");\n const warnings = allWarnings.length <= FULL_LISTED_FAILURES\n ? allWarnings\n : [\n ...allWarnings.slice(0, FULL_LISTED_FAILURES),\n { checkId, status: \"pass\" as const, severity: \"warning\" as const, message: `${allWarnings.length - FULL_LISTED_FAILURES} more warnings of the same kind.` },\n ];\n if (failed.length === 0) return [...warnings, pass(checkId, passMessage)];\n if (failed.length <= FULL_LISTED_FAILURES) return [...failed, ...warnings];\n const rest = failed.slice(FULL_LISTED_FAILURES);\n const examples = [...new Set(rest.flatMap((finding) => finding.target ?? []))].slice(0, 3);\n return [\n ...failed.slice(0, FULL_LISTED_FAILURES),\n fail(checkId, undefined, `${rest.length} more ${rest.length === 1 ? \"problem\" : \"problems\"} of the same kind${examples.length > 0 ? `, for example on ${examples.join(\", \")}` : \"\"}.`),\n ...warnings,\n ];\n}\n\n/** A finding that never blocks: the check passes and the report shows the warning. */\nexport function warning(checkId: SiteSystemCheckId, target: string, message: string): SiteSystemFinding {\n return { checkId, status: \"pass\", target, message, severity: \"warning\" };\n}\n\n/** The estate-wide checks need the whole build, not a sample: without it they fail, so the gate never passes unchecked. */\nexport function needsBuild(checkId: SiteSystemCheckId, estate: Estate): SiteSystemFinding | null {\n if (estate.complete) return null;\n return fail(checkId, undefined, \"The production build could not be read (no .next/prerender-manifest.json), so the whole estate could not be checked; run next build first.\");\n}\n\n/** \"a, b and c\". */\nexport function listOf(items: readonly string[], limit = 4): string {\n const shown = items.slice(0, limit);\n const more = items.length - shown.length;\n if (more > 0) return `${shown.join(\", \")} and ${more} more`;\n return shown.length <= 1 ? shown.join(\"\") : `${shown.slice(0, -1).join(\", \")} and ${shown.at(-1)}`;\n}\n\n/** \"the suburb page /locations/ryde\" style labels for a family. */\nexport const FAMILY_LABELS: Readonly<Record<string, string>> = {\n suburb: \"suburb page\",\n \"service-in-suburb\": \"service-in-suburb page\",\n region: \"region page\",\n};\n"]}
1
+ {"version":3,"file":"common.js","sourceRoot":"","sources":["../../../../src/site-system/rules/full/common.ts"],"names":[],"mappings":"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","sourcesContent":["import type { SiteSystemCheckId, SiteSystemFinding } from \"../../checks.js\";\nimport { fail, pass } from \"../enquiry/common.js\";\nimport type { SiteSystemRefreshException } from \"../../config.js\";\nimport type { Estate } from \"./estate.js\";\nimport type { FullGeography } from \"./geography.js\";\nimport type { FullStrategy } from \"./strategy.js\";\n\nexport { fail, notApplicable, pass, plural } from \"../enquiry/common.js\";\n\n/** Everything a full-tier rule reads. */\nexport interface FullContext {\n readonly estate: Estate;\n /** Null when the site-core geography could not be loaded; `geographyProblem` says why. */\n readonly geography: FullGeography | null;\n readonly geographyProblem: string;\n readonly strategy: FullStrategy;\n readonly projectRoot: string;\n /** Estate families brandlab.config.ts allows to refresh more often than weekly (W3); absent when none. */\n readonly refreshExceptions?: readonly SiteSystemRefreshException[];\n /** Today in Sydney, YYYY-MM-DD: the day waves are judged on. */\n readonly today: string;\n /** A family's canonical sections and cap (site-core `defaultSectionOrder`), or a string saying why they are unknown. */\n readonly defaultOrder:\n | ((family: string, trade?: string) => { readonly cap: number; readonly entries: readonly { readonly id: string; readonly requirement: string }[] } | undefined)\n | string;\n}\n\n/** The most failures one check lists one by one; the rest are counted in a last finding. */\nexport const FULL_LISTED_FAILURES = 25;\n\n/** The failures (at most `FULL_LISTED_FAILURES`, then a count of the rest), or one pass. */\nexport function outcome(checkId: SiteSystemCheckId, failures: readonly SiteSystemFinding[], passMessage: string): SiteSystemFinding[] {\n const failed = failures.filter((finding) => finding.status === \"fail\");\n const allWarnings = failures.filter((finding) => finding.status !== \"fail\");\n const warnings = allWarnings.length <= FULL_LISTED_FAILURES\n ? allWarnings\n : [\n ...allWarnings.slice(0, FULL_LISTED_FAILURES),\n { checkId, status: \"pass\" as const, severity: \"warning\" as const, message: `${allWarnings.length - FULL_LISTED_FAILURES} more warnings of the same kind.` },\n ];\n if (failed.length === 0) return [...warnings, pass(checkId, passMessage)];\n if (failed.length <= FULL_LISTED_FAILURES) return [...failed, ...warnings];\n const rest = failed.slice(FULL_LISTED_FAILURES);\n const examples = [...new Set(rest.flatMap((finding) => finding.target ?? []))].slice(0, 3);\n return [\n ...failed.slice(0, FULL_LISTED_FAILURES),\n fail(checkId, undefined, `${rest.length} more ${rest.length === 1 ? \"problem\" : \"problems\"} of the same kind${examples.length > 0 ? `, for example on ${examples.join(\", \")}` : \"\"}.`),\n ...warnings,\n ];\n}\n\n/** A finding that never blocks: the check passes and the report shows the warning. */\nexport function warning(checkId: SiteSystemCheckId, target: string, message: string): SiteSystemFinding {\n return { checkId, status: \"pass\", target, message, severity: \"warning\" };\n}\n\n/** The estate-wide checks need the whole build, not a sample: without it they fail, so the gate never passes unchecked. */\nexport function needsBuild(checkId: SiteSystemCheckId, estate: Estate): SiteSystemFinding | null {\n if (estate.complete) return null;\n return fail(checkId, undefined, \"The production build could not be read (no .next/prerender-manifest.json), so the whole estate could not be checked; run next build first.\");\n}\n\n/** \"a, b and c\". */\nexport function listOf(items: readonly string[], limit = 4): string {\n const shown = items.slice(0, limit);\n const more = items.length - shown.length;\n if (more > 0) return `${shown.join(\", \")} and ${more} more`;\n return shown.length <= 1 ? shown.join(\"\") : `${shown.slice(0, -1).join(\", \")} and ${shown.at(-1)}`;\n}\n\n/** \"the suburb page /locations/ryde\" style labels for a family. */\nexport const FAMILY_LABELS: Readonly<Record<string, string>> = {\n suburb: \"suburb page\",\n \"service-in-suburb\": \"service-in-suburb page\",\n region: \"region page\",\n};\n"]}
@@ -72,6 +72,8 @@ export interface EstatePage {
72
72
  readonly path: string;
73
73
  /** The app route Next built the page from (`/locations/[suburb]`), when known. */
74
74
  readonly srcRoute: string | undefined;
75
+ /** The page's timed refresh in seconds as Next built it (`initialRevalidateSeconds`); false for never; absent when unknown. */
76
+ readonly revalidate?: number | false;
75
77
  readonly indexable: boolean;
76
78
  readonly inSitemap: boolean;
77
79
  /** The sitemap file that lists the page, without its number and extension (`service-locations`). */
@@ -98,6 +100,8 @@ export interface Estate {
98
100
  readonly sitemap: ReadonlySet<string> | null;
99
101
  /** Dynamic app routes in the build and how many pages each produced. */
100
102
  readonly dynamicRoutes: ReadonlyMap<string, number>;
103
+ /** The timed refresh of pages a dynamic route renders on demand (`fallbackRevalidate`), for routes with a fallback. */
104
+ readonly fallbackRevalidate: ReadonlyMap<string, number | false>;
101
105
  readonly buildDir: string;
102
106
  /** Routes the prerender manifest lists with no HTML in the build; `location` when the route is a location page's. */
103
107
  readonly missingHtml: readonly {
@@ -1 +1 @@
1
- {"version":3,"file":"estate.d.ts","sourceRoot":"","sources":["../../../../src/site-system/rules/full/estate.ts"],"names":[],"mappings":"AAeA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAIvD,OAAO,EAAe,KAAK,aAAa,EAAE,KAAK,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAEjF,+FAA+F;AAC/F,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,KAAK,EAAE,SAAS,GAAG,UAAU,CAAC;IACvC,QAAQ,CAAC,OAAO,EAAE,SAAS;QAAE,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;IACnF,QAAQ,CAAC,QAAQ,EAAE,SAAS;QAAE,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;IACtI,QAAQ,CAAC,OAAO,EAAE,SAAS;QAAE,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;IACtI,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,CAAC;CACnC;AAED,MAAM,MAAM,aAAa,GACrB;IAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GACzB;IAAE,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GACrD;IAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;CAAE,GACjD;IAAE,QAAQ,CAAC,IAAI,EAAE,KAAK,CAAC;IAAC,QAAQ,CAAC,QAAQ,EAAE,YAAY,CAAA;CAAE,CAAC;AAE9D,eAAO,MAAM,iBAAiB,oDAAqD,CAAC;AACpF,MAAM,MAAM,cAAc,GAAG,CAAC,OAAO,iBAAiB,CAAC,CAAC,MAAM,CAAC,CAAC;AAEhE,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,IAAI,cAAc,CAErF;AAED,2EAA2E;AAC3E,MAAM,WAAW,YAAY;IAC3B,qGAAqG;IACrG,QAAQ,CAAC,KAAK,EAAE,SAAS;QAAE,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;IAC/E,QAAQ,CAAC,IAAI,EAAE,SAAS;QAAE,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;IACjF,6EAA6E;IAC7E,QAAQ,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,GAAG,IAAI,CAAC;IACxC,mEAAmE;IACnE,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/B,QAAQ,CAAC,OAAO,EAAE;QAAE,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,UAAU,EAAE,OAAO,CAAC;QAAC,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAA;KAAE,GAAG,IAAI,CAAC;IAC9G,QAAQ,CAAC,SAAS,EAAE;QAAE,QAAQ,CAAC,WAAW,EAAE,OAAO,CAAC;QAAC,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC;IAC3F,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAC1B,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;CAC3B;AAED,MAAM,WAAW,UAAU;IACzB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,kFAAkF;IAClF,QAAQ,CAAC,QAAQ,EAAE,MAAM,GAAG,SAAS,CAAC;IACtC,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAC;IAC5B,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAC;IAC5B,oGAAoG;IACpG,QAAQ,CAAC,WAAW,EAAE,MAAM,GAAG,SAAS,CAAC;IACzC,mDAAmD;IACnD,QAAQ,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,CAAC;IAClC,0FAA0F;IAC1F,QAAQ,CAAC,UAAU,EAAE,SAAS,CAAC,SAAS,MAAM,EAAE,CAAC,EAAE,CAAC;IACpD,QAAQ,CAAC,QAAQ,EAAE,aAAa,CAAC;IACjC,0DAA0D;IAC1D,QAAQ,CAAC,QAAQ,EAAE,SAAS,MAAM,EAAE,CAAC;IACrC,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,KAAK,EAAE,SAAS,GAAG,SAAS,CAAC;IACtC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC;IAC3B,YAAY,EAAE,UAAU,GAAG,SAAS,GAAG,OAAO,GAAG,MAAM,GAAG,MAAM,GAAG,SAAS,CAAC;IAC7E,IAAI,EAAE,YAAY,GAAG,SAAS,CAAC;CAChC;AAED,MAAM,WAAW,MAAM;IACrB,QAAQ,CAAC,KAAK,EAAE,SAAS,UAAU,EAAE,CAAC;IACtC,QAAQ,CAAC,MAAM,EAAE,WAAW,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC;IACjD,6EAA6E;IAC7E,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC;IAC3B,uFAAuF;IACvF,QAAQ,CAAC,OAAO,EAAE,WAAW,CAAC,MAAM,CAAC,GAAG,IAAI,CAAC;IAC7C,wEAAwE;IACxE,QAAQ,CAAC,aAAa,EAAE,WAAW,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACpD,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,qHAAqH;IACrH,QAAQ,CAAC,WAAW,EAAE,SAAS;QAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAA;KAAE,EAAE,CAAC;CACxF;AAID,gGAAgG;AAChG,wBAAgB,UAAU,CAAC,KAAK,EAAE;IAChC,QAAQ,CAAC,KAAK,EAAE,SAAS,cAAc,EAAE,CAAC;IAC1C,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,SAAS,EAAE,aAAa,GAAG,IAAI,CAAC;IACzC,iFAAiF;IACjF,QAAQ,CAAC,KAAK,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;CACnD,GAAG,MAAM,CAkGT;AA8PD,mHAAmH;AACnH,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,WAAW,CAAC,MAAM,CAAC,GAAG,MAAM,GAAG,IAAI,CAiBlG"}
1
+ {"version":3,"file":"estate.d.ts","sourceRoot":"","sources":["../../../../src/site-system/rules/full/estate.ts"],"names":[],"mappings":"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"}