@brandlab-digital/conformance 3.12.1 → 3.14.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 (149) hide show
  1. package/README.md +41 -1
  2. package/dist/analytics-e2e.d.ts +10 -16
  3. package/dist/analytics-e2e.d.ts.map +1 -1
  4. package/dist/analytics-e2e.js +61 -3
  5. package/dist/analytics-e2e.js.map +1 -1
  6. package/dist/cli.d.ts.map +1 -1
  7. package/dist/cli.js +4 -1
  8. package/dist/cli.js.map +1 -1
  9. package/dist/current-release.js +5 -5
  10. package/dist/current-release.js.map +1 -1
  11. package/dist/edge-probe-cli.d.ts +57 -0
  12. package/dist/edge-probe-cli.d.ts.map +1 -0
  13. package/dist/edge-probe-cli.js +134 -0
  14. package/dist/edge-probe-cli.js.map +1 -0
  15. package/dist/engineering.d.ts +4 -1
  16. package/dist/engineering.d.ts.map +1 -1
  17. package/dist/engineering.js +19 -14
  18. package/dist/engineering.js.map +1 -1
  19. package/dist/estate-sample.d.ts +60 -0
  20. package/dist/estate-sample.d.ts.map +1 -0
  21. package/dist/estate-sample.js +144 -0
  22. package/dist/estate-sample.js.map +1 -0
  23. package/dist/estate.d.ts +45 -0
  24. package/dist/estate.d.ts.map +1 -1
  25. package/dist/estate.js +82 -22
  26. package/dist/estate.js.map +1 -1
  27. package/dist/origin-region.d.ts +14 -0
  28. package/dist/origin-region.d.ts.map +1 -0
  29. package/dist/origin-region.js +46 -0
  30. package/dist/origin-region.js.map +1 -0
  31. package/dist/prefetch-probe-cli.d.ts +36 -0
  32. package/dist/prefetch-probe-cli.d.ts.map +1 -0
  33. package/dist/prefetch-probe-cli.js +163 -0
  34. package/dist/prefetch-probe-cli.js.map +1 -0
  35. package/dist/project.d.ts +20 -0
  36. package/dist/project.d.ts.map +1 -1
  37. package/dist/project.js +92 -2
  38. package/dist/project.js.map +1 -1
  39. package/dist/ready-cli.d.ts.map +1 -1
  40. package/dist/ready-cli.js +8 -3
  41. package/dist/ready-cli.js.map +1 -1
  42. package/dist/rule-registry.d.ts +5 -0
  43. package/dist/rule-registry.d.ts.map +1 -1
  44. package/dist/rule-registry.js +1 -0
  45. package/dist/rule-registry.js.map +1 -1
  46. package/dist/site-system/checks.d.ts +62 -1
  47. package/dist/site-system/checks.d.ts.map +1 -1
  48. package/dist/site-system/checks.js +9 -0
  49. package/dist/site-system/checks.js.map +1 -1
  50. package/dist/site-system/config.d.ts +27 -0
  51. package/dist/site-system/config.d.ts.map +1 -1
  52. package/dist/site-system/config.js +50 -1
  53. package/dist/site-system/config.js.map +1 -1
  54. package/dist/site-system/index.d.ts +1 -1
  55. package/dist/site-system/index.d.ts.map +1 -1
  56. package/dist/site-system/index.js +1 -1
  57. package/dist/site-system/index.js.map +1 -1
  58. package/dist/site-system/journeys/plan.d.ts +9 -2
  59. package/dist/site-system/journeys/plan.d.ts.map +1 -1
  60. package/dist/site-system/journeys/plan.js +11 -1
  61. package/dist/site-system/journeys/plan.js.map +1 -1
  62. package/dist/site-system/journeys/prefetch-measure.d.ts +28 -0
  63. package/dist/site-system/journeys/prefetch-measure.d.ts.map +1 -0
  64. package/dist/site-system/journeys/prefetch-measure.js +94 -0
  65. package/dist/site-system/journeys/prefetch-measure.js.map +1 -0
  66. package/dist/site-system/journeys/prefetch-policy.d.ts +85 -0
  67. package/dist/site-system/journeys/prefetch-policy.d.ts.map +1 -0
  68. package/dist/site-system/journeys/prefetch-policy.js +152 -0
  69. package/dist/site-system/journeys/prefetch-policy.js.map +1 -0
  70. package/dist/site-system/journeys/prefetch.d.ts +3 -0
  71. package/dist/site-system/journeys/prefetch.d.ts.map +1 -0
  72. package/dist/site-system/journeys/prefetch.js +37 -0
  73. package/dist/site-system/journeys/prefetch.js.map +1 -0
  74. package/dist/site-system/journeys/variants.d.ts +34 -0
  75. package/dist/site-system/journeys/variants.d.ts.map +1 -0
  76. package/dist/site-system/journeys/variants.js +139 -0
  77. package/dist/site-system/journeys/variants.js.map +1 -0
  78. package/dist/site-system/report.d.ts.map +1 -1
  79. package/dist/site-system/report.js +19 -5
  80. package/dist/site-system/report.js.map +1 -1
  81. package/dist/site-system/rule-modules.d.ts.map +1 -1
  82. package/dist/site-system/rule-modules.js +10 -0
  83. package/dist/site-system/rule-modules.js.map +1 -1
  84. package/dist/site-system/rules/delivery/h13-inbox.d.ts +23 -0
  85. package/dist/site-system/rules/delivery/h13-inbox.d.ts.map +1 -0
  86. package/dist/site-system/rules/delivery/h13-inbox.js +208 -0
  87. package/dist/site-system/rules/delivery/h13-inbox.js.map +1 -0
  88. package/dist/site-system/rules/delivery/index.d.ts +12 -0
  89. package/dist/site-system/rules/delivery/index.d.ts.map +1 -0
  90. package/dist/site-system/rules/delivery/index.js +7 -0
  91. package/dist/site-system/rules/delivery/index.js.map +1 -0
  92. package/dist/site-system/rules/full/common.d.ts +3 -0
  93. package/dist/site-system/rules/full/common.d.ts.map +1 -1
  94. package/dist/site-system/rules/full/common.js.map +1 -1
  95. package/dist/site-system/rules/full/e1-pattern.d.ts +4 -0
  96. package/dist/site-system/rules/full/e1-pattern.d.ts.map +1 -0
  97. package/dist/site-system/rules/full/e1-pattern.js +47 -0
  98. package/dist/site-system/rules/full/e1-pattern.js.map +1 -0
  99. package/dist/site-system/rules/full/estate.d.ts +20 -1
  100. package/dist/site-system/rules/full/estate.d.ts.map +1 -1
  101. package/dist/site-system/rules/full/estate.js +33 -5
  102. package/dist/site-system/rules/full/estate.js.map +1 -1
  103. package/dist/site-system/rules/full/geography.d.ts +8 -2
  104. package/dist/site-system/rules/full/geography.d.ts.map +1 -1
  105. package/dist/site-system/rules/full/geography.js +6 -2
  106. package/dist/site-system/rules/full/geography.js.map +1 -1
  107. package/dist/site-system/rules/full/index.d.ts +3 -2
  108. package/dist/site-system/rules/full/index.d.ts.map +1 -1
  109. package/dist/site-system/rules/full/index.js +27 -9
  110. package/dist/site-system/rules/full/index.js.map +1 -1
  111. package/dist/site-system/rules/full/k12-slots.d.ts +4 -0
  112. package/dist/site-system/rules/full/k12-slots.d.ts.map +1 -0
  113. package/dist/site-system/rules/full/k12-slots.js +40 -0
  114. package/dist/site-system/rules/full/k12-slots.js.map +1 -0
  115. package/dist/site-system/rules/full/loc-index.d.ts +5 -0
  116. package/dist/site-system/rules/full/loc-index.d.ts.map +1 -1
  117. package/dist/site-system/rules/full/loc-index.js +43 -2
  118. package/dist/site-system/rules/full/loc-index.js.map +1 -1
  119. package/dist/site-system/rules/full/strategy.d.ts +4 -0
  120. package/dist/site-system/rules/full/strategy.d.ts.map +1 -1
  121. package/dist/site-system/rules/full/strategy.js +19 -3
  122. package/dist/site-system/rules/full/strategy.js.map +1 -1
  123. package/dist/site-system/rules/full/w3-refresh.d.ts +22 -0
  124. package/dist/site-system/rules/full/w3-refresh.d.ts.map +1 -0
  125. package/dist/site-system/rules/full/w3-refresh.js +70 -0
  126. package/dist/site-system/rules/full/w3-refresh.js.map +1 -0
  127. package/dist/site-system/rules/guards/index.d.ts +22 -0
  128. package/dist/site-system/rules/guards/index.d.ts.map +1 -0
  129. package/dist/site-system/rules/guards/index.js +109 -0
  130. package/dist/site-system/rules/guards/index.js.map +1 -0
  131. package/dist/site-system/rules/guards/w10-crawlers.d.ts +55 -0
  132. package/dist/site-system/rules/guards/w10-crawlers.d.ts.map +1 -0
  133. package/dist/site-system/rules/guards/w10-crawlers.js +182 -0
  134. package/dist/site-system/rules/guards/w10-crawlers.js.map +1 -0
  135. package/dist/site-system/rules/guards/w9-params.d.ts +19 -0
  136. package/dist/site-system/rules/guards/w9-params.d.ts.map +1 -0
  137. package/dist/site-system/rules/guards/w9-params.js +84 -0
  138. package/dist/site-system/rules/guards/w9-params.js.map +1 -0
  139. package/dist/site-system/rules/sections/k2-skin-coverage.d.ts +2 -2
  140. package/dist/site-system/rules/sections/k2-skin-coverage.d.ts.map +1 -1
  141. package/dist/site-system/rules/sections/k2-skin-coverage.js +6 -2
  142. package/dist/site-system/rules/sections/k2-skin-coverage.js.map +1 -1
  143. package/dist/site-system/runners.js +1 -1
  144. package/dist/site-system/runners.js.map +1 -1
  145. package/dist/site-system-e2e.d.ts +1 -0
  146. package/dist/site-system-e2e.d.ts.map +1 -1
  147. package/dist/site-system-e2e.js +8 -2
  148. package/dist/site-system-e2e.js.map +1 -1
  149. package/package.json +6 -4
@@ -0,0 +1,163 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * brandlab-prefetch-probe (3.13.0): measures one page's Next prefetches, run locally against a URL (a local
4
+ * production build, a protected preview or a live page). For each viewport it lists the pages prefetched on landing
5
+ * and after a full scroll, with their compressed and decoded bytes and the edge cache result, and says whether the
6
+ * page would pass W2 (prefetch by link role).
7
+ *
8
+ * npx brandlab-prefetch-probe https://www.example.com.au/areas/hills-district
9
+ * npx brandlab-prefetch-probe http://localhost:3000/ --viewport 390x844 --json
10
+ *
11
+ * It loads the page once per viewport in headless Chromium (`@playwright/test`, an optional peer) and lets no request
12
+ * leave for another site, so analytics never counts it. It reads one page; it never crawls.
13
+ */
14
+ import { realpathSync } from "node:fs";
15
+ import { resolve } from "node:path";
16
+ import { fileURLToPath } from "node:url";
17
+ import { measurePrefetch } from "./site-system/journeys/prefetch-measure.js";
18
+ import { evaluatePrefetch } from "./site-system/journeys/prefetch-policy.js";
19
+ export const PROBE_DEFAULT_VIEWPORTS = Object.freeze([
20
+ { width: 390, height: 844 },
21
+ { width: 1440, height: 900 },
22
+ ]);
23
+ const HELP = `brandlab-prefetch-probe <url> [--viewport WIDTHxHEIGHT ...] [--json] [--strict]
24
+
25
+ Lists the pages one page prefetches on landing and after a full scroll, with bytes, at 390 x 844 and 1440 x 900
26
+ (or the viewports named), and whether it would pass W2. --strict exits 1 when it would not.`;
27
+ const MOBILE_USER_AGENT = "Mozilla/5.0 (Linux; Android 14; Pixel 8) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0 Mobile Safari/537.36";
28
+ export function parseProbeArgs(argv) {
29
+ let url;
30
+ const viewports = [];
31
+ let json = false;
32
+ let strict = false;
33
+ for (let index = 0; index < argv.length; index += 1) {
34
+ const argument = argv[index];
35
+ if (argument === "--json")
36
+ json = true;
37
+ else if (argument === "--strict")
38
+ strict = true;
39
+ else if (argument === "--viewport") {
40
+ const value = argv[++index] ?? "";
41
+ const match = /^(\d{3,4})x(\d{3,4})$/.exec(value);
42
+ if (!match)
43
+ throw new Error(`--viewport takes WIDTHxHEIGHT, for example 390x844 (got ${JSON.stringify(value)}).`);
44
+ viewports.push({ width: Number(match[1]), height: Number(match[2]) });
45
+ }
46
+ else if (argument.startsWith("--"))
47
+ throw new Error(`Unknown option ${argument}.`);
48
+ else if (url)
49
+ throw new Error("Name one page URL.");
50
+ else
51
+ url = argument;
52
+ }
53
+ if (!url)
54
+ throw new Error("Name the page URL to probe.");
55
+ let parsed;
56
+ try {
57
+ parsed = new URL(url);
58
+ }
59
+ catch {
60
+ throw new Error(`${url} is not a full URL; include http:// or https://.`);
61
+ }
62
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:")
63
+ throw new Error(`${url} is not an http or https URL.`);
64
+ return { url: parsed.href, viewports: viewports.length > 0 ? viewports : [...PROBE_DEFAULT_VIEWPORTS], json, strict };
65
+ }
66
+ function kilobytes(bytes) {
67
+ return `${Math.round(bytes / 102.4) / 10} KB`;
68
+ }
69
+ /** Edge cache results by count, for example "HIT 3, MISS 9". */
70
+ export function cacheSummary(requests) {
71
+ const counts = new Map();
72
+ for (const request of requests)
73
+ counts.set(request.cache ?? "none", (counts.get(request.cache ?? "none") ?? 0) + 1);
74
+ return [...counts].map(([cache, count]) => `${cache} ${count}`).join(", ") || "none";
75
+ }
76
+ /** The plain-text report: per viewport, the counts, bytes, pages and the W2 result. */
77
+ export function formatProbeReport(url, results) {
78
+ const lines = [`Prefetch probe: ${url}`];
79
+ for (const { viewport, measurement, verdict } of results) {
80
+ const scrollOnly = measurement.scrolled.filter((page) => !measurement.landing.includes(page));
81
+ lines.push("", `${viewport}`, ` On landing: ${measurement.landing.length} page(s)${measurement.landing.length ? `: ${measurement.landing.join(", ")}` : ""}`, ` After a full scroll: ${measurement.scrolled.length} page(s)${scrollOnly.length ? `; added ${scrollOnly.join(", ")}` : ""}`, ` Bytes: ${kilobytes(measurement.bytes.prefetchCompressed)} compressed, ${kilobytes(measurement.bytes.prefetchDecoded)} decoded, in ${measurement.requests.length} request(s); the page's HTML ${kilobytes(measurement.bytes.htmlCompressed)} compressed`, ` Edge cache: ${cacheSummary(measurement.requests)}`, verdict.failures.length === 0 ? " W2: would pass" : ` W2: would fail\n${verdict.failures.map((failure) => ` - ${failure}`).join("\n")}`, ...verdict.warnings.map((warning) => ` ${warning}`));
82
+ }
83
+ return `${lines.join("\n")}\n`;
84
+ }
85
+ /** The JSON report: the same results, with each request. */
86
+ export function probeJson(url, results) {
87
+ return `${JSON.stringify({ url, viewports: results.map(({ viewport, measurement, verdict }) => ({ viewport, ...measurement, verdict })) }, null, 2)}\n`;
88
+ }
89
+ async function loadChromium() {
90
+ const specifier = "@playwright/test";
91
+ try {
92
+ return (await import(specifier)).chromium;
93
+ }
94
+ catch {
95
+ throw new Error("brandlab-prefetch-probe needs @playwright/test with Chromium: npm install --save-dev @playwright/test && npx playwright install chromium");
96
+ }
97
+ }
98
+ export async function probePrefetch(options, chromium) {
99
+ const browser = await (chromium ?? (await loadChromium())).launch({ headless: true });
100
+ const target = new URL(options.url);
101
+ const results = [];
102
+ try {
103
+ for (const viewport of options.viewports) {
104
+ const mobile = viewport.width < 768;
105
+ const context = await browser.newContext({
106
+ viewport: { ...viewport },
107
+ isMobile: mobile,
108
+ hasTouch: mobile,
109
+ serviceWorkers: "block",
110
+ ...(mobile ? { userAgent: MOBILE_USER_AGENT } : {}),
111
+ });
112
+ try {
113
+ // Only the site under test: analytics, tags and other sites never see the probe.
114
+ await context.route("**/*", (route) => (new URL(route.request().url()).origin === target.origin ? route.continue() : route.abort()));
115
+ const page = await context.newPage();
116
+ const measurement = await measurePrefetch(page, `${target.pathname}${target.search}`, {
117
+ open: (opened) => opened.goto(target.href, { waitUntil: "load", timeout: 60_000 }),
118
+ });
119
+ const label = `${viewport.width} x ${viewport.height}`;
120
+ results.push({ viewport: label, measurement, verdict: evaluatePrefetch(measurement, label) });
121
+ }
122
+ finally {
123
+ await context.close();
124
+ }
125
+ }
126
+ }
127
+ finally {
128
+ await browser.close();
129
+ }
130
+ return results;
131
+ }
132
+ export async function runPrefetchProbeCli(argv, io, probe = probePrefetch) {
133
+ if (argv.length === 0 || argv.includes("--help") || argv.includes("-h")) {
134
+ io.stdout(`${HELP}\n`);
135
+ return argv.length === 0 ? 1 : 0;
136
+ }
137
+ try {
138
+ const options = parseProbeArgs(argv);
139
+ const results = await probe(options);
140
+ io.stdout(options.json ? probeJson(options.url, results) : formatProbeReport(options.url, results));
141
+ return options.strict && results.some((result) => result.verdict.failures.length > 0) ? 1 : 0;
142
+ }
143
+ catch (error) {
144
+ io.stderr(`${error instanceof Error ? error.message : String(error)}\n`);
145
+ return 1;
146
+ }
147
+ }
148
+ function isEntrypoint(moduleUrl, argvPath) {
149
+ if (!argvPath)
150
+ return false;
151
+ try {
152
+ return realpathSync(fileURLToPath(moduleUrl)) === realpathSync(resolve(argvPath));
153
+ }
154
+ catch {
155
+ return false;
156
+ }
157
+ }
158
+ if (isEntrypoint(import.meta.url, process.argv[1])) {
159
+ runPrefetchProbeCli(process.argv.slice(2), { stdout: (value) => process.stdout.write(value), stderr: (value) => process.stderr.write(value) }).then((code) => {
160
+ process.exitCode = code;
161
+ });
162
+ }
163
+ //# sourceMappingURL=prefetch-probe-cli.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"prefetch-probe-cli.js","sourceRoot":"","sources":["../src/prefetch-probe-cli.ts"],"names":[],"mappings":"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","sourcesContent":["#!/usr/bin/env node\n/**\n * brandlab-prefetch-probe (3.13.0): measures one page's Next prefetches, run locally against a URL (a local\n * production build, a protected preview or a live page). For each viewport it lists the pages prefetched on landing\n * and after a full scroll, with their compressed and decoded bytes and the edge cache result, and says whether the\n * page would pass W2 (prefetch by link role).\n *\n * npx brandlab-prefetch-probe https://www.example.com.au/areas/hills-district\n * npx brandlab-prefetch-probe http://localhost:3000/ --viewport 390x844 --json\n *\n * It loads the page once per viewport in headless Chromium (`@playwright/test`, an optional peer) and lets no request\n * leave for another site, so analytics never counts it. It reads one page; it never crawls.\n */\nimport { realpathSync } from \"node:fs\";\nimport { resolve } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport { measurePrefetch, type PrefetchMeasureResult } from \"./site-system/journeys/prefetch-measure.js\";\nimport { evaluatePrefetch, type PrefetchVerdict } from \"./site-system/journeys/prefetch-policy.js\";\n\nexport interface ProbeViewport {\n readonly width: number;\n readonly height: number;\n}\n\nexport interface ProbeOptions {\n readonly url: string;\n readonly viewports: readonly ProbeViewport[];\n readonly json: boolean;\n /** Exit 1 when the page would fail W2. */\n readonly strict: boolean;\n}\n\nexport interface ProbeViewportResult {\n readonly viewport: string;\n readonly measurement: PrefetchMeasureResult;\n readonly verdict: PrefetchVerdict;\n}\n\nexport const PROBE_DEFAULT_VIEWPORTS: readonly ProbeViewport[] = Object.freeze([\n { width: 390, height: 844 },\n { width: 1440, height: 900 },\n]);\n\nconst HELP = `brandlab-prefetch-probe <url> [--viewport WIDTHxHEIGHT ...] [--json] [--strict]\n\nLists the pages one page prefetches on landing and after a full scroll, with bytes, at 390 x 844 and 1440 x 900\n(or the viewports named), and whether it would pass W2. --strict exits 1 when it would not.`;\n\nconst MOBILE_USER_AGENT = \"Mozilla/5.0 (Linux; Android 14; Pixel 8) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0 Mobile Safari/537.36\";\n\nexport function parseProbeArgs(argv: readonly string[]): ProbeOptions {\n let url: string | undefined;\n const viewports: ProbeViewport[] = [];\n let json = false;\n let strict = false;\n for (let index = 0; index < argv.length; index += 1) {\n const argument = argv[index]!;\n if (argument === \"--json\") json = true;\n else if (argument === \"--strict\") strict = true;\n else if (argument === \"--viewport\") {\n const value = argv[++index] ?? \"\";\n const match = /^(\\d{3,4})x(\\d{3,4})$/.exec(value);\n if (!match) throw new Error(`--viewport takes WIDTHxHEIGHT, for example 390x844 (got ${JSON.stringify(value)}).`);\n viewports.push({ width: Number(match[1]), height: Number(match[2]) });\n } else if (argument.startsWith(\"--\")) throw new Error(`Unknown option ${argument}.`);\n else if (url) throw new Error(\"Name one page URL.\");\n else url = argument;\n }\n if (!url) throw new Error(\"Name the page URL to probe.\");\n let parsed: URL;\n try {\n parsed = new URL(url);\n } catch {\n throw new Error(`${url} is not a full URL; include http:// or https://.`);\n }\n if (parsed.protocol !== \"http:\" && parsed.protocol !== \"https:\") throw new Error(`${url} is not an http or https URL.`);\n return { url: parsed.href, viewports: viewports.length > 0 ? viewports : [...PROBE_DEFAULT_VIEWPORTS], json, strict };\n}\n\nfunction kilobytes(bytes: number): string {\n return `${Math.round(bytes / 102.4) / 10} KB`;\n}\n\n/** Edge cache results by count, for example \"HIT 3, MISS 9\". */\nexport function cacheSummary(requests: PrefetchMeasureResult[\"requests\"]): string {\n const counts = new Map<string, number>();\n for (const request of requests) counts.set(request.cache ?? \"none\", (counts.get(request.cache ?? \"none\") ?? 0) + 1);\n return [...counts].map(([cache, count]) => `${cache} ${count}`).join(\", \") || \"none\";\n}\n\n/** The plain-text report: per viewport, the counts, bytes, pages and the W2 result. */\nexport function formatProbeReport(url: string, results: readonly ProbeViewportResult[]): string {\n const lines = [`Prefetch probe: ${url}`];\n for (const { viewport, measurement, verdict } of results) {\n const scrollOnly = measurement.scrolled.filter((page) => !measurement.landing.includes(page));\n lines.push(\n \"\",\n `${viewport}`,\n ` On landing: ${measurement.landing.length} page(s)${measurement.landing.length ? `: ${measurement.landing.join(\", \")}` : \"\"}`,\n ` After a full scroll: ${measurement.scrolled.length} page(s)${scrollOnly.length ? `; added ${scrollOnly.join(\", \")}` : \"\"}`,\n ` Bytes: ${kilobytes(measurement.bytes.prefetchCompressed)} compressed, ${kilobytes(measurement.bytes.prefetchDecoded)} decoded, in ${measurement.requests.length} request(s); the page's HTML ${kilobytes(measurement.bytes.htmlCompressed)} compressed`,\n ` Edge cache: ${cacheSummary(measurement.requests)}`,\n verdict.failures.length === 0 ? \" W2: would pass\" : ` W2: would fail\\n${verdict.failures.map((failure) => ` - ${failure}`).join(\"\\n\")}`,\n ...verdict.warnings.map((warning) => ` ${warning}`),\n );\n }\n return `${lines.join(\"\\n\")}\\n`;\n}\n\n/** The JSON report: the same results, with each request. */\nexport function probeJson(url: string, results: readonly ProbeViewportResult[]): string {\n return `${JSON.stringify({ url, viewports: results.map(({ viewport, measurement, verdict }) => ({ viewport, ...measurement, verdict })) }, null, 2)}\\n`;\n}\n\ntype Chromium = typeof import(\"@playwright/test\").chromium;\n\nasync function loadChromium(): Promise<Chromium> {\n const specifier: string = \"@playwright/test\";\n try {\n return ((await import(specifier)) as { chromium: Chromium }).chromium;\n } catch {\n throw new Error(\"brandlab-prefetch-probe needs @playwright/test with Chromium: npm install --save-dev @playwright/test && npx playwright install chromium\");\n }\n}\n\nexport async function probePrefetch(options: ProbeOptions, chromium?: Chromium): Promise<ProbeViewportResult[]> {\n const browser = await (chromium ?? (await loadChromium())).launch({ headless: true });\n const target = new URL(options.url);\n const results: ProbeViewportResult[] = [];\n try {\n for (const viewport of options.viewports) {\n const mobile = viewport.width < 768;\n const context = await browser.newContext({\n viewport: { ...viewport },\n isMobile: mobile,\n hasTouch: mobile,\n serviceWorkers: \"block\",\n ...(mobile ? { userAgent: MOBILE_USER_AGENT } : {}),\n });\n try {\n // Only the site under test: analytics, tags and other sites never see the probe.\n await context.route(\"**/*\", (route) => (new URL(route.request().url()).origin === target.origin ? route.continue() : route.abort()));\n const page = await context.newPage();\n const measurement = await measurePrefetch(page, `${target.pathname}${target.search}`, {\n open: (opened) => opened.goto(target.href, { waitUntil: \"load\", timeout: 60_000 }),\n });\n const label = `${viewport.width} x ${viewport.height}`;\n results.push({ viewport: label, measurement, verdict: evaluatePrefetch(measurement, label) });\n } finally {\n await context.close();\n }\n }\n } finally {\n await browser.close();\n }\n return results;\n}\n\ninterface CliIo {\n stdout(value: string): void;\n stderr(value: string): void;\n}\n\nexport async function runPrefetchProbeCli(argv: readonly string[], io: CliIo, probe: typeof probePrefetch = probePrefetch): Promise<number> {\n if (argv.length === 0 || argv.includes(\"--help\") || argv.includes(\"-h\")) {\n io.stdout(`${HELP}\\n`);\n return argv.length === 0 ? 1 : 0;\n }\n try {\n const options = parseProbeArgs(argv);\n const results = await probe(options);\n io.stdout(options.json ? probeJson(options.url, results) : formatProbeReport(options.url, results));\n return options.strict && results.some((result) => result.verdict.failures.length > 0) ? 1 : 0;\n } catch (error) {\n io.stderr(`${error instanceof Error ? error.message : String(error)}\\n`);\n return 1;\n }\n}\n\nfunction isEntrypoint(moduleUrl: string, argvPath: string | undefined): boolean {\n if (!argvPath) return false;\n try {\n return realpathSync(fileURLToPath(moduleUrl)) === realpathSync(resolve(argvPath));\n } catch {\n return false;\n }\n}\n\nif (isEntrypoint(import.meta.url, process.argv[1])) {\n runPrefetchProbeCli(process.argv.slice(2), { stdout: (value) => process.stdout.write(value), stderr: (value) => process.stderr.write(value) }).then((code) => {\n process.exitCode = code;\n });\n}\n"]}
package/dist/project.d.ts CHANGED
@@ -38,11 +38,24 @@ export interface BrandlabConformanceResult {
38
38
  implementationFingerprint: string;
39
39
  mode: BrandlabConformanceMode;
40
40
  installedRelease: string | null;
41
+ /**
42
+ * Set only when all four packages are one canary prerelease (`X.Y.Z-canary.N`). A canary is never an
43
+ * installed release: `installedRelease` stays null, so it cannot stand as release evidence.
44
+ */
45
+ canaryRelease?: BrandlabCanaryRelease;
41
46
  findings: BrandlabFinding[];
42
47
  operationalFindings: BrandlabOperationalFinding[];
43
48
  verification: BrandlabVerificationResult;
44
49
  summary: ConformanceSummary;
45
50
  }
51
+ export interface BrandlabCanaryRelease {
52
+ /** The canary version, e.g. `3.14.0-canary.2`. */
53
+ version: string;
54
+ /** The release line it previews, e.g. `3.14.0`. */
55
+ line: string;
56
+ /** `embedded`: checked against this checker's contract for the line; `not-cut`: the line has no contract yet. */
57
+ contract: "embedded" | "not-cut";
58
+ }
46
59
  export interface EvaluateBrandlabProjectOptions {
47
60
  packedReleaseReceipt?: string;
48
61
  root: string;
@@ -62,6 +75,13 @@ interface ProjectPackageJsonLike {
62
75
  export declare function evaluateBrandlabProject(options: EvaluateBrandlabProjectOptions): BrandlabConformanceResult;
63
76
  export declare function shouldFailBrandlabConformance(result: BrandlabConformanceResult): boolean;
64
77
  export declare function detectInstalledBrandlabRelease(packageJson: ProjectPackageJsonLike | null): string | null;
78
+ /**
79
+ * Reads one canary prerelease across all four packages, pinned exactly (`3.14.0-canary.2`) or installed from
80
+ * the packed tarball (`file:.canary/brandlab-digital-site-core-3.14.0-canary.2.tgz`). A canary previews the line
81
+ * `X.Y.Z`: it is checked against that line's contract when this checker carries it, and is otherwise reported
82
+ * as a canary notice. Lines older than the checker's release are not supported. Never a published release.
83
+ */
84
+ export declare function detectBrandlabCanaryRelease(packageJson: ProjectPackageJsonLike | null, release?: Pick<BrandlabRuntimeReleaseContract, "releaseVersion">): BrandlabCanaryRelease | null;
65
85
  /** Variable names documented in an env example file (`KEY=`, `export KEY=`); values are ignored. */
66
86
  export declare function parseEnvironmentKeys(text: string): Set<string>;
67
87
  export type { BrandlabContractVersions };
@@ -1 +1 @@
1
- {"version":3,"file":"project.d.ts","sourceRoot":"","sources":["../src/project.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"project.d.ts","sourceRoot":"","sources":["../src/project.ts"],"names":[],"mappings":"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"}
package/dist/project.js CHANGED
@@ -160,7 +160,10 @@ export function evaluateBrandlabProject(options) {
160
160
  ? verifyPackedPreflight(root, resolve(options.packedReleaseReceipt)) : undefined;
161
161
  const detectedRelease = packedPreflight?.version ?? detectInstalledBrandlabRelease(packageJson);
162
162
  const installedRelease = options.installedRelease === undefined ? detectedRelease : options.installedRelease;
163
- const manifest = release.releaseVersion === installedRelease ? release : null;
163
+ const canary = !detectedRelease && options.installedRelease === undefined
164
+ ? detectBrandlabCanaryRelease(packageJson, release)
165
+ : null;
166
+ const manifest = release.releaseVersion === installedRelease || canary?.contract === "embedded" ? release : null;
164
167
  const attestation = options.releaseManifest?.releaseVersion === installedRelease
165
168
  ? options.releaseManifest
166
169
  : null;
@@ -174,6 +177,8 @@ export function evaluateBrandlabProject(options) {
174
177
  packageLock,
175
178
  installedRelease,
176
179
  detectedRelease,
180
+ canary,
181
+ releaseVersion: release.releaseVersion,
177
182
  manifest,
178
183
  attestation,
179
184
  packedPreflight,
@@ -225,6 +230,7 @@ export function evaluateBrandlabProject(options) {
225
230
  implementationFingerprint,
226
231
  mode,
227
232
  installedRelease,
233
+ ...(canary ? { canaryRelease: canary } : {}),
228
234
  findings: operationalFindings,
229
235
  operationalFindings,
230
236
  verification: policyResult.verification,
@@ -234,16 +240,88 @@ export function evaluateBrandlabProject(options) {
234
240
  export function shouldFailBrandlabConformance(result) {
235
241
  return result.verification.summary.blockers > 0;
236
242
  }
243
+ const CANARY_VERSION_PATTERN = /^((?:0|[1-9]\d*)\.(?:0|[1-9]\d*)\.(?:0|[1-9]\d*))-canary\.(?:0|[1-9]\d*)$/;
237
244
  export function detectInstalledBrandlabRelease(packageJson) {
238
245
  if (!packageJson)
239
246
  return null;
240
247
  const versions = BRANDLAB_SHARED_PACKAGE_NAMES.map((packageName) => packageJson.dependencies?.[packageName] ??
241
248
  packageJson.devDependencies?.[packageName] ??
242
249
  null);
243
- if (versions.some((version) => !version || !isBrandlabSemver(version)))
250
+ // A canary (X.Y.Z-canary.N) is never an installed release; detectBrandlabCanaryRelease reads it.
251
+ if (versions.some((version) => !version || !isBrandlabSemver(version) || CANARY_VERSION_PATTERN.test(version))) {
244
252
  return null;
253
+ }
245
254
  return new Set(versions).size === 1 ? versions[0] : null;
246
255
  }
256
+ /**
257
+ * Reads one canary prerelease across all four packages, pinned exactly (`3.14.0-canary.2`) or installed from
258
+ * the packed tarball (`file:.canary/brandlab-digital-site-core-3.14.0-canary.2.tgz`). A canary previews the line
259
+ * `X.Y.Z`: it is checked against that line's contract when this checker carries it, and is otherwise reported
260
+ * as a canary notice. Lines older than the checker's release are not supported. Never a published release.
261
+ */
262
+ export function detectBrandlabCanaryRelease(packageJson, release = BRANDLAB_CURRENT_RUNTIME_RELEASE) {
263
+ if (!packageJson)
264
+ return null;
265
+ const versions = BRANDLAB_SHARED_PACKAGE_NAMES.map((packageName) => {
266
+ const spec = packageJson.dependencies?.[packageName] ?? packageJson.devDependencies?.[packageName];
267
+ if (!spec)
268
+ return null;
269
+ if (CANARY_VERSION_PATTERN.test(spec))
270
+ return spec;
271
+ if (!spec.startsWith("file:"))
272
+ return null;
273
+ const tarballPrefix = `brandlab-digital-${packageName.split("/")[1]}-`;
274
+ const fileName = spec.slice("file:".length).split("/").at(-1) ?? "";
275
+ if (!fileName.startsWith(tarballPrefix) || !fileName.endsWith(".tgz"))
276
+ return null;
277
+ const version = fileName.slice(tarballPrefix.length, -".tgz".length);
278
+ return CANARY_VERSION_PATTERN.test(version) ? version : null;
279
+ });
280
+ const [version] = versions;
281
+ if (!version || versions.some((candidate) => candidate !== version))
282
+ return null;
283
+ const line = CANARY_VERSION_PATTERN.exec(version)[1];
284
+ const contract = line === release.releaseVersion ? "embedded" : "not-cut";
285
+ return { version, line, contract };
286
+ }
287
+ function inspectCanary(options) {
288
+ const { canary } = options;
289
+ recordEvaluation(options.evaluations, "release", "brandlab-release-unsupported");
290
+ if (compareBrandlabSemver(canary.line, options.release) < 0) {
291
+ options.findings.push(releaseFinding("brandlab-release-unsupported", `A published release, or a canary of ${options.release} or later`, `${canary.version} is a canary of an older line than ${options.release}.`, "Install the published release or a current canary."));
292
+ return;
293
+ }
294
+ recordEvaluation(options.evaluations, "release", "brandlab-release-canary");
295
+ options.findings.push({
296
+ ...releaseFinding("brandlab-release-canary", "A published Brandlab release", canary.contract === "embedded"
297
+ ? `${canary.version} is a canary of ${canary.line}, not a published release; it was checked against the ${canary.line} contract.`
298
+ : `${canary.version} is a canary of ${canary.line}, not a published release; ${canary.line} has no release contract yet.`, `Install the published ${canary.line} before release or launch.`, "package.json"),
299
+ severity: "warning",
300
+ });
301
+ recordEvaluation(options.evaluations, "release", "brandlab-release-package-lock");
302
+ if (options.packageLock.state !== "valid") {
303
+ options.findings.push(releaseFinding(`brandlab-release-package-lock-${options.packageLock.state}`, "A valid npm lockfile v2 or v3", `package-lock.json state is ${options.packageLock.state}.`, "Regenerate the lockfile from the exact package declarations.", "package-lock.json"));
304
+ return;
305
+ }
306
+ recordEvaluation(options.evaluations, "release", "brandlab-release-package-pin-mismatch");
307
+ for (const packageName of BRANDLAB_SHARED_PACKAGE_NAMES) {
308
+ const declared = declaredPackageSpec(options.packageJson, packageName);
309
+ const locked = lockedRootSpec(options.packageLock, packageName);
310
+ const resolved = options.packageLock.resolvedPackages.get(packageName);
311
+ if (locked !== declared || resolved?.version !== canary.version || resolved.linked) {
312
+ options.findings.push(releaseFinding("brandlab-release-package-pin-mismatch", `${packageName}@${canary.version} declared once and locked exactly`, `package.json=${declared ?? "missing"}; lockfile=${locked ?? "missing"}; resolved=${resolved?.version ?? "missing"}.`, "Reinstall the four canary packages together and regenerate the lockfile.", "package.json"));
313
+ }
314
+ }
315
+ if (options.manifest) {
316
+ inspectRuntime({
317
+ findings: options.findings,
318
+ evaluations: options.evaluations,
319
+ packageJson: options.packageJson,
320
+ packageLock: options.packageLock,
321
+ manifest: options.manifest,
322
+ });
323
+ }
324
+ }
247
325
  function inspectRelease(options) {
248
326
  recordEvaluation(options.evaluations, "release", "brandlab-release-package-json-missing");
249
327
  if (!options.packageJson) {
@@ -251,6 +329,18 @@ function inspectRelease(options) {
251
329
  return;
252
330
  }
253
331
  recordEvaluation(options.evaluations, "release", "brandlab-release-installed-version-unresolved");
332
+ if (!options.installedRelease && options.canary) {
333
+ inspectCanary({
334
+ findings: options.findings,
335
+ evaluations: options.evaluations,
336
+ packageJson: options.packageJson,
337
+ packageLock: options.packageLock,
338
+ canary: options.canary,
339
+ release: options.releaseVersion,
340
+ manifest: options.manifest,
341
+ });
342
+ return;
343
+ }
254
344
  if (!options.installedRelease) {
255
345
  options.findings.push(releaseFinding("brandlab-release-installed-version-unresolved", "All four Brandlab packages pinned to one exact release", "The installed Brandlab release could not be resolved.", "Pin all four packages to one exact supported version."));
256
346
  return;