@brandlab-digital/conformance 3.12.0 → 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.
- package/README.md +32 -1
- package/dist/current-release.js +6 -6
- package/dist/current-release.js.map +1 -1
- package/dist/edge-probe-cli.d.ts +57 -0
- package/dist/edge-probe-cli.d.ts.map +1 -0
- package/dist/edge-probe-cli.js +134 -0
- package/dist/edge-probe-cli.js.map +1 -0
- package/dist/engineering.d.ts +4 -1
- package/dist/engineering.d.ts.map +1 -1
- package/dist/engineering.js +19 -14
- package/dist/engineering.js.map +1 -1
- package/dist/estate-sample.d.ts +60 -0
- package/dist/estate-sample.d.ts.map +1 -0
- package/dist/estate-sample.js +144 -0
- package/dist/estate-sample.js.map +1 -0
- package/dist/estate.d.ts +17 -0
- package/dist/estate.d.ts.map +1 -1
- package/dist/estate.js +47 -10
- package/dist/estate.js.map +1 -1
- package/dist/origin-region.d.ts +14 -0
- package/dist/origin-region.d.ts.map +1 -0
- package/dist/origin-region.js +46 -0
- package/dist/origin-region.js.map +1 -0
- package/dist/prefetch-probe-cli.d.ts +36 -0
- package/dist/prefetch-probe-cli.d.ts.map +1 -0
- package/dist/prefetch-probe-cli.js +163 -0
- package/dist/prefetch-probe-cli.js.map +1 -0
- package/dist/site-system/checks.d.ts +36 -0
- package/dist/site-system/checks.d.ts.map +1 -1
- package/dist/site-system/checks.js +6 -0
- package/dist/site-system/checks.js.map +1 -1
- package/dist/site-system/config.d.ts +27 -0
- package/dist/site-system/config.d.ts.map +1 -1
- package/dist/site-system/config.js +50 -1
- package/dist/site-system/config.js.map +1 -1
- package/dist/site-system/index.d.ts +1 -1
- package/dist/site-system/index.d.ts.map +1 -1
- package/dist/site-system/index.js +1 -1
- package/dist/site-system/index.js.map +1 -1
- package/dist/site-system/journeys/plan.d.ts +9 -2
- package/dist/site-system/journeys/plan.d.ts.map +1 -1
- package/dist/site-system/journeys/plan.js +11 -1
- package/dist/site-system/journeys/plan.js.map +1 -1
- package/dist/site-system/journeys/prefetch-measure.d.ts +28 -0
- package/dist/site-system/journeys/prefetch-measure.d.ts.map +1 -0
- package/dist/site-system/journeys/prefetch-measure.js +94 -0
- package/dist/site-system/journeys/prefetch-measure.js.map +1 -0
- package/dist/site-system/journeys/prefetch-policy.d.ts +85 -0
- package/dist/site-system/journeys/prefetch-policy.d.ts.map +1 -0
- package/dist/site-system/journeys/prefetch-policy.js +152 -0
- package/dist/site-system/journeys/prefetch-policy.js.map +1 -0
- package/dist/site-system/journeys/prefetch.d.ts +3 -0
- package/dist/site-system/journeys/prefetch.d.ts.map +1 -0
- package/dist/site-system/journeys/prefetch.js +37 -0
- package/dist/site-system/journeys/prefetch.js.map +1 -0
- package/dist/site-system/journeys/variants.d.ts +34 -0
- package/dist/site-system/journeys/variants.d.ts.map +1 -0
- package/dist/site-system/journeys/variants.js +139 -0
- package/dist/site-system/journeys/variants.js.map +1 -0
- package/dist/site-system/rule-modules.d.ts.map +1 -1
- package/dist/site-system/rule-modules.js +5 -0
- package/dist/site-system/rule-modules.js.map +1 -1
- package/dist/site-system/rules/full/common.d.ts +3 -0
- package/dist/site-system/rules/full/common.d.ts.map +1 -1
- package/dist/site-system/rules/full/common.js.map +1 -1
- package/dist/site-system/rules/full/estate.d.ts +4 -0
- package/dist/site-system/rules/full/estate.d.ts.map +1 -1
- package/dist/site-system/rules/full/estate.js +11 -2
- package/dist/site-system/rules/full/estate.js.map +1 -1
- package/dist/site-system/rules/full/index.d.ts +2 -1
- package/dist/site-system/rules/full/index.d.ts.map +1 -1
- package/dist/site-system/rules/full/index.js +14 -7
- package/dist/site-system/rules/full/index.js.map +1 -1
- package/dist/site-system/rules/full/w3-refresh.d.ts +22 -0
- package/dist/site-system/rules/full/w3-refresh.d.ts.map +1 -0
- package/dist/site-system/rules/full/w3-refresh.js +70 -0
- package/dist/site-system/rules/full/w3-refresh.js.map +1 -0
- package/dist/site-system/rules/guards/index.d.ts +22 -0
- package/dist/site-system/rules/guards/index.d.ts.map +1 -0
- package/dist/site-system/rules/guards/index.js +109 -0
- package/dist/site-system/rules/guards/index.js.map +1 -0
- package/dist/site-system/rules/guards/w10-crawlers.d.ts +55 -0
- package/dist/site-system/rules/guards/w10-crawlers.d.ts.map +1 -0
- package/dist/site-system/rules/guards/w10-crawlers.js +182 -0
- package/dist/site-system/rules/guards/w10-crawlers.js.map +1 -0
- package/dist/site-system/rules/guards/w9-params.d.ts +19 -0
- package/dist/site-system/rules/guards/w9-params.d.ts.map +1 -0
- package/dist/site-system/rules/guards/w9-params.js +84 -0
- package/dist/site-system/rules/guards/w9-params.js.map +1 -0
- package/dist/site-system-e2e.d.ts +1 -0
- package/dist/site-system-e2e.d.ts.map +1 -1
- package/dist/site-system-e2e.js +8 -2
- package/dist/site-system-e2e.js.map +1 -1
- package/package.json +6 -4
package/README.md
CHANGED
|
@@ -21,7 +21,9 @@ Proprietary software © Brandlab Digital Marketing. All rights reserved. Publish
|
|
|
21
21
|
A site adopts Site System v2 by declaring `siteSystem: { tier: 'shell' | 'enquiry' | 'full' }` in
|
|
22
22
|
`brandlab.config.ts` (`newBuild: true` for new builds and redesigns; `samples` adds pages to check; `labels`,
|
|
23
23
|
`freeQuotes`, `thirdParties` and `phones` record the quote labels, free-quote offer, third-party hosts and extra
|
|
24
|
-
`tel:` links the shell rules accept
|
|
24
|
+
`tel:` links the shell rules accept; `refreshExceptions` lists estate families allowed to refresh more often than
|
|
25
|
+
every 7 days, each `{ family, revalidateSeconds, reason, decisionId }`, for W3 from 3.13.0). Without it, nothing in
|
|
26
|
+
the gate blocks.
|
|
25
27
|
|
|
26
28
|
- `brandlab-ready` (after `next build` and the site-system journeys) writes `docs/ready/ready-report.md` and
|
|
27
29
|
`.json`: every check with its tier, whether it blocks for this site, its status (pass, fail, not run, excepted,
|
|
@@ -41,3 +43,32 @@ A site adopts Site System v2 by declaring `siteSystem: { tier: 'shell' | 'enquir
|
|
|
41
43
|
`brandlab-ready --launch` also fails until every step is recorded.
|
|
42
44
|
- Rule modules export a `SiteSystemRenderedRunner` from `@brandlab-digital/conformance/site-system` and are
|
|
43
45
|
listed in `SITE_SYSTEM_RENDERED_RUNNERS`.
|
|
46
|
+
- W2 (shell tier, from 3.13.0) runs inside the site-system journeys on each sampled page at 390 x 844 and
|
|
47
|
+
1440 x 900: at most 8 prefetched pages on landing and 12 after a full scroll; none from footer, header panel,
|
|
48
|
+
drawer, area (`coverage`), nearby (`nearby-areas`), map (`area-map`, `data-area-map`), finder or other-services
|
|
49
|
+
links (a site marks its own lists with `data-bl-links="area|nearby|map|finder|other-services"`), none through a
|
|
50
|
+
hidden link and never the current page. Compressed prefetch bytes above twice the page's own HTML add a warning.
|
|
51
|
+
- `brandlab-prefetch-probe <url> [--viewport 390x844 ...] [--json] [--strict]` measures one page locally (a local
|
|
52
|
+
build, a preview or a live page) in headless Chromium: the pages prefetched on landing and after a full scroll at
|
|
53
|
+
each viewport, their compressed and decoded bytes, the edge cache result and the W2 result. No request leaves for
|
|
54
|
+
another site, and it never crawls.
|
|
55
|
+
|
|
56
|
+
## Cost guards (from 3.13.0)
|
|
57
|
+
|
|
58
|
+
- **W5** (static, shell): `vercel.json` `regions` equal `brandlab.config.ts` `hosting.originRegion` (Sydney,
|
|
59
|
+
`syd1`, when there is no hosting block). Any other region needs an `origin-region-v1` decision whose `reviewBy`
|
|
60
|
+
is at most six months after `decidedOn`; the check fails once `reviewBy` passes (Sydney time). The engineering
|
|
61
|
+
check `vercel-region` (`brandlab-verify --engineering`, inside every site's `npm run verify`) applies the same
|
|
62
|
+
rule except the date: a lapsed `reviewBy` never stops a build, a fix or a rollback.
|
|
63
|
+
- **W10** (rendered, shell): the built `robots.txt` (or the running site's, with `--base-url`; a Preview that
|
|
64
|
+
answers noindex, or a build made for a Preview whose `robots.txt` disallows everything, is not judged) lets every search and answer crawler (site-core `BRANDLAB_SEARCH_CRAWLERS`) fetch
|
|
65
|
+
home and the representative routes, and gives none of them a Crawl-delay. Firewall rules that match on the user
|
|
66
|
+
agent (`vercel.json` `routes` with `mitigate`, and `hosting.firewall`) must not deny or challenge them; a rate
|
|
67
|
+
limit on one is a warning.
|
|
68
|
+
- **W9** (journey, full): `defineBrandlabSiteSystemTests({ pages: { families: [...] } })` names one built page per
|
|
69
|
+
dynamic route family; an unknown slug answers the shared 404, the uppercase slug answers it too or redirects once
|
|
70
|
+
(301 or 308) to its exact lowercase form, and `?utm_source=x` serves the same cacheable page as the clean URL. **W9-params** (static, full): no page under a dynamic route reads `searchParams`.
|
|
71
|
+
- `brandlab-edge-probe [--json] <url>...` (run locally as `npm run probe:edge`, at most 25 URLs) logs
|
|
72
|
+
`x-vercel-cache`, `age`, `x-vercel-id` (edge and function region) and time to first byte for each page, its RSC
|
|
73
|
+
payload and its `_tree` prefetch, and a GET to each origin's `/api/contact` (expected 405). It is the measuring
|
|
74
|
+
stick of the origin-region benchmark (`operations/shared-platform/ORIGIN_REGION.md`).
|
package/dist/current-release.js
CHANGED
|
@@ -23,23 +23,23 @@ export const BRANDLAB_CURRENT_RUNTIME_RELEASE = Object.freeze({
|
|
|
23
23
|
"kind": "brandlab-runtime-release-contract",
|
|
24
24
|
"packages": {
|
|
25
25
|
"@brandlab-digital/conformance": {
|
|
26
|
-
"version": "3.
|
|
26
|
+
"version": "3.13.0"
|
|
27
27
|
},
|
|
28
28
|
"@brandlab-digital/eslint-plugin": {
|
|
29
|
-
"version": "3.
|
|
29
|
+
"version": "3.13.0"
|
|
30
30
|
},
|
|
31
31
|
"@brandlab-digital/site-core": {
|
|
32
|
-
"version": "3.
|
|
32
|
+
"version": "3.13.0"
|
|
33
33
|
},
|
|
34
34
|
"@brandlab-digital/site-next": {
|
|
35
|
-
"version": "3.
|
|
35
|
+
"version": "3.13.0"
|
|
36
36
|
}
|
|
37
37
|
},
|
|
38
|
-
"releaseVersion": "3.
|
|
38
|
+
"releaseVersion": "3.13.0",
|
|
39
39
|
"runtime": {
|
|
40
40
|
"bundler": "turbopack",
|
|
41
41
|
"cacheComponents": false,
|
|
42
|
-
"next": "16.
|
|
42
|
+
"next": "16.4.2",
|
|
43
43
|
"nextVersionPolicy": "exact",
|
|
44
44
|
"node": "24.x",
|
|
45
45
|
"packageManager": "npm@11.14.1",
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"current-release.js","sourceRoot":"","sources":["../src/current-release.ts"],"names":[],"mappings":"AAAA,2EAA2E;AAC3E,wEAAwE;AACxE,OAAO,EAEL,sCAAsC,GACvC,MAAM,uBAAuB,CAAC;AAE/B,MAAM,CAAC,MAAM,gCAAgC,GAAG,MAAM,CAAC,MAAM,CAC7D;IACE,cAAc,EAAE;QACd,uCAAuC;QACvC,8BAA8B;QAC9B,yBAAyB;QACzB,6BAA6B;QAC7B,0BAA0B;QAC1B,4BAA4B;QAC5B,kCAAkC;QAClC,wCAAwC;QACxC,yBAAyB;KAC1B;IACD,WAAW,EAAE;QACX,WAAW,EAAE,cAAc;QAC3B,cAAc,EAAE,iBAAiB;QACjC,eAAe,EAAE,mBAAmB;QACpC,SAAS,EAAE,YAAY;QACvB,KAAK,EAAE,QAAQ;KAChB;IACD,MAAM,EAAE,mCAAmC;IAC3C,UAAU,EAAE;QACV,+BAA+B,EAAE;YAC/B,SAAS,EAAE,QAAQ;SACpB;QACD,iCAAiC,EAAE;YACjC,SAAS,EAAE,QAAQ;SACpB;QACD,6BAA6B,EAAE;YAC7B,SAAS,EAAE,QAAQ;SACpB;QACD,6BAA6B,EAAE;YAC7B,SAAS,EAAE,QAAQ;SACpB;KACF;IACD,gBAAgB,EAAE,QAAQ;IAC1B,SAAS,EAAE;QACT,SAAS,EAAE,WAAW;QACtB,iBAAiB,EAAE,KAAK;QACxB,MAAM,EAAE,QAAQ;QAChB,mBAAmB,EAAE,OAAO;QAC5B,MAAM,EAAE,MAAM;QACd,gBAAgB,EAAE,aAAa;QAC/B,OAAO,EAAE,QAAQ;QACjB,UAAU,EAAE,QAAQ;QACpB,QAAQ,EAAE,KAAK;QACf,OAAO,EAAE,QAAQ;QACjB,YAAY,EAAE,OAAO;KACtB;IACD,eAAe,EAAE,sCAAsC;CACxD,CAC6C,CAAC;AAE/C,sCAAsC,CAAC,gCAAgC,CAAC,CAAC","sourcesContent":["// Generated by standards/scripts/generate-conformance-current-release.mjs.\n// Each exact package release verifies its own current runtime contract.\nimport {\n type BrandlabRuntimeReleaseContract,\n validateBrandlabRuntimeReleaseContract,\n} from \"./release-manifest.js\";\n\nexport const BRANDLAB_CURRENT_RUNTIME_RELEASE = Object.freeze(\n{\n \"capabilities\": [\n \"typed-project-business-and-route-data\",\n \"essential-search-foundations\",\n \"shared-contact-delivery\",\n \"privacy-safe-lead-analytics\",\n \"security-patched-runtime\",\n \"live-google-review-summary\",\n \"high-signal-project-verification\",\n \"packed-positive-and-negative-consumers\",\n \"changesets-oidc-release\"\n ],\n \"contracts\": {\n \"analytics\": \"analytics-v4\",\n \"architecture\": \"architecture-v7\",\n \"configuration\": \"project-config-v7\",\n \"contact\": \"contact-v4\",\n \"seo\": \"seo-v4\"\n },\n \"kind\": \"brandlab-runtime-release-contract\",\n \"packages\": {\n \"@brandlab-digital/conformance\": {\n \"version\": \"3.
|
|
1
|
+
{"version":3,"file":"current-release.js","sourceRoot":"","sources":["../src/current-release.ts"],"names":[],"mappings":"AAAA,2EAA2E;AAC3E,wEAAwE;AACxE,OAAO,EAEL,sCAAsC,GACvC,MAAM,uBAAuB,CAAC;AAE/B,MAAM,CAAC,MAAM,gCAAgC,GAAG,MAAM,CAAC,MAAM,CAC7D;IACE,cAAc,EAAE;QACd,uCAAuC;QACvC,8BAA8B;QAC9B,yBAAyB;QACzB,6BAA6B;QAC7B,0BAA0B;QAC1B,4BAA4B;QAC5B,kCAAkC;QAClC,wCAAwC;QACxC,yBAAyB;KAC1B;IACD,WAAW,EAAE;QACX,WAAW,EAAE,cAAc;QAC3B,cAAc,EAAE,iBAAiB;QACjC,eAAe,EAAE,mBAAmB;QACpC,SAAS,EAAE,YAAY;QACvB,KAAK,EAAE,QAAQ;KAChB;IACD,MAAM,EAAE,mCAAmC;IAC3C,UAAU,EAAE;QACV,+BAA+B,EAAE;YAC/B,SAAS,EAAE,QAAQ;SACpB;QACD,iCAAiC,EAAE;YACjC,SAAS,EAAE,QAAQ;SACpB;QACD,6BAA6B,EAAE;YAC7B,SAAS,EAAE,QAAQ;SACpB;QACD,6BAA6B,EAAE;YAC7B,SAAS,EAAE,QAAQ;SACpB;KACF;IACD,gBAAgB,EAAE,QAAQ;IAC1B,SAAS,EAAE;QACT,SAAS,EAAE,WAAW;QACtB,iBAAiB,EAAE,KAAK;QACxB,MAAM,EAAE,QAAQ;QAChB,mBAAmB,EAAE,OAAO;QAC5B,MAAM,EAAE,MAAM;QACd,gBAAgB,EAAE,aAAa;QAC/B,OAAO,EAAE,QAAQ;QACjB,UAAU,EAAE,QAAQ;QACpB,QAAQ,EAAE,KAAK;QACf,OAAO,EAAE,QAAQ;QACjB,YAAY,EAAE,OAAO;KACtB;IACD,eAAe,EAAE,sCAAsC;CACxD,CAC6C,CAAC;AAE/C,sCAAsC,CAAC,gCAAgC,CAAC,CAAC","sourcesContent":["// Generated by standards/scripts/generate-conformance-current-release.mjs.\n// Each exact package release verifies its own current runtime contract.\nimport {\n type BrandlabRuntimeReleaseContract,\n validateBrandlabRuntimeReleaseContract,\n} from \"./release-manifest.js\";\n\nexport const BRANDLAB_CURRENT_RUNTIME_RELEASE = Object.freeze(\n{\n \"capabilities\": [\n \"typed-project-business-and-route-data\",\n \"essential-search-foundations\",\n \"shared-contact-delivery\",\n \"privacy-safe-lead-analytics\",\n \"security-patched-runtime\",\n \"live-google-review-summary\",\n \"high-signal-project-verification\",\n \"packed-positive-and-negative-consumers\",\n \"changesets-oidc-release\"\n ],\n \"contracts\": {\n \"analytics\": \"analytics-v4\",\n \"architecture\": \"architecture-v7\",\n \"configuration\": \"project-config-v7\",\n \"contact\": \"contact-v4\",\n \"seo\": \"seo-v4\"\n },\n \"kind\": \"brandlab-runtime-release-contract\",\n \"packages\": {\n \"@brandlab-digital/conformance\": {\n \"version\": \"3.13.0\"\n },\n \"@brandlab-digital/eslint-plugin\": {\n \"version\": \"3.13.0\"\n },\n \"@brandlab-digital/site-core\": {\n \"version\": \"3.13.0\"\n },\n \"@brandlab-digital/site-next\": {\n \"version\": \"3.13.0\"\n }\n },\n \"releaseVersion\": \"3.13.0\",\n \"runtime\": {\n \"bundler\": \"turbopack\",\n \"cacheComponents\": false,\n \"next\": \"16.4.2\",\n \"nextVersionPolicy\": \"exact\",\n \"node\": \"24.x\",\n \"packageManager\": \"npm@11.14.1\",\n \"react\": \"19.2.8\",\n \"reactDom\": \"19.2.8\",\n \"router\": \"app\",\n \"sharp\": \"0.35.5\",\n \"typescript\": \"6.0.3\"\n },\n \"schemaVersion\": \"brandlab.runtime-release-contract.v1\"\n},\n) as unknown as BrandlabRuntimeReleaseContract;\n\nvalidateBrandlabRuntimeReleaseContract(BRANDLAB_CURRENT_RUNTIME_RELEASE);\n"]}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
export declare const EDGE_PROBE_MAX_URLS = 25;
|
|
3
|
+
export type EdgeProbeKind = "page" | "rsc" | "tree" | "contact";
|
|
4
|
+
export interface EdgeProbeRequest {
|
|
5
|
+
readonly kind: EdgeProbeKind;
|
|
6
|
+
readonly url: string;
|
|
7
|
+
readonly headers: Readonly<Record<string, string>>;
|
|
8
|
+
/** The status the request should answer; only the contact GET has one (405). */
|
|
9
|
+
readonly expectStatus?: number;
|
|
10
|
+
}
|
|
11
|
+
export interface EdgeProbeResult {
|
|
12
|
+
readonly kind: EdgeProbeKind;
|
|
13
|
+
readonly url: string;
|
|
14
|
+
readonly status: number | null;
|
|
15
|
+
readonly cache: string | null;
|
|
16
|
+
readonly age: string | null;
|
|
17
|
+
readonly vercelId: string | null;
|
|
18
|
+
/** The function region from x-vercel-id when the request reached a function. */
|
|
19
|
+
readonly functionRegion: string | null;
|
|
20
|
+
readonly edgeRegion: string | null;
|
|
21
|
+
readonly ttfbMs: number | null;
|
|
22
|
+
readonly problem?: string;
|
|
23
|
+
}
|
|
24
|
+
/** The requests for the given URLs: page, RSC and `_tree` for each, then one contact GET per origin. */
|
|
25
|
+
export declare function planEdgeProbe(urls: readonly string[]): EdgeProbeRequest[];
|
|
26
|
+
/** `syd1::iad1::abc-123` → edge syd1, function iad1; `syd1::abc-123` (served at the edge) → edge syd1, no function. */
|
|
27
|
+
export declare function vercelIdRegions(vercelId: string | null): {
|
|
28
|
+
edge: string | null;
|
|
29
|
+
function: string | null;
|
|
30
|
+
};
|
|
31
|
+
/** One result as a row of the plain-text table. */
|
|
32
|
+
export declare function formatEdgeProbeRow(result: EdgeProbeResult): string;
|
|
33
|
+
type Fetcher = (url: string, init: {
|
|
34
|
+
headers: Record<string, string>;
|
|
35
|
+
redirect: "manual";
|
|
36
|
+
signal?: AbortSignal;
|
|
37
|
+
}) => Promise<{
|
|
38
|
+
status: number;
|
|
39
|
+
headers: {
|
|
40
|
+
get(name: string): string | null;
|
|
41
|
+
};
|
|
42
|
+
body?: {
|
|
43
|
+
cancel(): Promise<void>;
|
|
44
|
+
} | null;
|
|
45
|
+
}>;
|
|
46
|
+
export declare function runEdgeProbe(requests: readonly EdgeProbeRequest[], options?: {
|
|
47
|
+
fetch?: Fetcher;
|
|
48
|
+
now?: () => number;
|
|
49
|
+
}): Promise<EdgeProbeResult[]>;
|
|
50
|
+
export declare function runEdgeProbeCli(argv: readonly string[], io: {
|
|
51
|
+
stdout: (text: string) => void;
|
|
52
|
+
stderr: (text: string) => void;
|
|
53
|
+
}, options?: {
|
|
54
|
+
fetch?: Fetcher;
|
|
55
|
+
}): Promise<number>;
|
|
56
|
+
export {};
|
|
57
|
+
//# sourceMappingURL=edge-probe-cli.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"edge-probe-cli.d.ts","sourceRoot":"","sources":["../src/edge-probe-cli.ts"],"names":[],"mappings":";AAgBA,eAAO,MAAM,mBAAmB,KAAK,CAAC;AAEtC,MAAM,MAAM,aAAa,GAAG,MAAM,GAAG,KAAK,GAAG,MAAM,GAAG,SAAS,CAAC;AAEhE,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,IAAI,EAAE,aAAa,CAAC;IAC7B,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,OAAO,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;IACnD,gFAAgF;IAChF,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAC;CAChC;AAED,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,IAAI,EAAE,aAAa,CAAC;IAC7B,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/B,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,QAAQ,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,QAAQ,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACjC,gFAAgF;IAChF,QAAQ,CAAC,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;IACvC,QAAQ,CAAC,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IACnC,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/B,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;CAC3B;AAED,wGAAwG;AACxG,wBAAgB,aAAa,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,GAAG,gBAAgB,EAAE,CAsBzE;AAED,uHAAuH;AACvH,wBAAgB,eAAe,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,GAAG;IAAE,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,CAKzG;AAED,mDAAmD;AACnD,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,eAAe,GAAG,MAAM,CAWlE;AAED,KAAK,OAAO,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE;IAAE,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAAC,QAAQ,EAAE,QAAQ,CAAC;IAAC,MAAM,CAAC,EAAE,WAAW,CAAA;CAAE,KAAK,OAAO,CAAC;IAC3H,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE;QAAE,GAAG,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAAA;KAAE,CAAC;IAC9C,IAAI,CAAC,EAAE;QAAE,MAAM,IAAI,OAAO,CAAC,IAAI,CAAC,CAAA;KAAE,GAAG,IAAI,CAAC;CAC3C,CAAC,CAAC;AAEH,wBAAsB,YAAY,CAAC,QAAQ,EAAE,SAAS,gBAAgB,EAAE,EAAE,OAAO,GAAE;IAAE,KAAK,CAAC,EAAE,OAAO,CAAC;IAAC,GAAG,CAAC,EAAE,MAAM,MAAM,CAAA;CAAO,GAAG,OAAO,CAAC,eAAe,EAAE,CAAC,CA8B3J;AAED,wBAAsB,eAAe,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,EAAE,EAAE,EAAE;IAAE,MAAM,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IAAC,MAAM,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAA;CAAE,EAAE,OAAO,GAAE;IAAE,KAAK,CAAC,EAAE,OAAO,CAAA;CAAO,GAAG,OAAO,CAAC,MAAM,CAAC,CAczL"}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* brandlab-edge-probe (3.13.0): `npm run probe:edge`, run on a laptop, never in CI. For each fixed URL it requests the
|
|
4
|
+
* page, its RSC payload and its `_tree` segment prefetch the way the Next 16 router does, then makes one GET to
|
|
5
|
+
* `/api/contact` on each origin (which answers 405), and logs `x-vercel-cache`, `age`, `x-vercel-id` and the time to
|
|
6
|
+
* first byte. It is the measuring stick for the origin-region benchmark (operations/shared-platform/ORIGIN_REGION.md):
|
|
7
|
+
* `x-vercel-id` reads `<edge>::<function region>::<id>`, so `syd1::iad1::…` on `/api/contact` confirms a Washington
|
|
8
|
+
* function. A handful of requests, never a crawl: at most 25 URLs.
|
|
9
|
+
*
|
|
10
|
+
* npx brandlab-edge-probe https://www.example.com.au/ https://www.example.com.au/services/blocked-drains
|
|
11
|
+
* npx brandlab-edge-probe --json https://www.example.com.au/
|
|
12
|
+
*/
|
|
13
|
+
import { realpathSync } from "node:fs";
|
|
14
|
+
import { resolve } from "node:path";
|
|
15
|
+
import { fileURLToPath } from "node:url";
|
|
16
|
+
export const EDGE_PROBE_MAX_URLS = 25;
|
|
17
|
+
/** The requests for the given URLs: page, RSC and `_tree` for each, then one contact GET per origin. */
|
|
18
|
+
export function planEdgeProbe(urls) {
|
|
19
|
+
if (urls.length === 0)
|
|
20
|
+
throw new Error("Name at least one URL to probe.");
|
|
21
|
+
if (urls.length > EDGE_PROBE_MAX_URLS)
|
|
22
|
+
throw new Error(`Probe at most ${EDGE_PROBE_MAX_URLS} URLs; this is a measurement, not a crawl.`);
|
|
23
|
+
const requests = [];
|
|
24
|
+
const origins = [];
|
|
25
|
+
for (const raw of urls) {
|
|
26
|
+
let url;
|
|
27
|
+
try {
|
|
28
|
+
url = new URL(raw);
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
throw new Error(`"${raw}" is not an absolute URL such as https://www.example.com.au/services.`);
|
|
32
|
+
}
|
|
33
|
+
if (url.protocol !== "https:" && url.protocol !== "http:")
|
|
34
|
+
throw new Error(`"${raw}" is not an http(s) URL.`);
|
|
35
|
+
url.hash = "";
|
|
36
|
+
const href = url.toString();
|
|
37
|
+
requests.push({ kind: "page", url: href, headers: { accept: "text/html" } });
|
|
38
|
+
requests.push({ kind: "rsc", url: href, headers: { rsc: "1" } });
|
|
39
|
+
requests.push({ kind: "tree", url: href, headers: { rsc: "1", "next-router-prefetch": "1", "next-router-segment-prefetch": "/_tree" } });
|
|
40
|
+
if (!origins.includes(url.origin))
|
|
41
|
+
origins.push(url.origin);
|
|
42
|
+
}
|
|
43
|
+
for (const origin of origins)
|
|
44
|
+
requests.push({ kind: "contact", url: `${origin}/api/contact`, headers: { accept: "application/json" }, expectStatus: 405 });
|
|
45
|
+
return requests;
|
|
46
|
+
}
|
|
47
|
+
/** `syd1::iad1::abc-123` → edge syd1, function iad1; `syd1::abc-123` (served at the edge) → edge syd1, no function. */
|
|
48
|
+
export function vercelIdRegions(vercelId) {
|
|
49
|
+
if (!vercelId)
|
|
50
|
+
return { edge: null, function: null };
|
|
51
|
+
const parts = vercelId.split("::");
|
|
52
|
+
const region = (part) => (part && /^[a-z]{3}\d$/.test(part) ? part : null);
|
|
53
|
+
return { edge: region(parts[0]), function: parts.length >= 3 ? region(parts[1]) : null };
|
|
54
|
+
}
|
|
55
|
+
/** One result as a row of the plain-text table. */
|
|
56
|
+
export function formatEdgeProbeRow(result) {
|
|
57
|
+
const cells = [
|
|
58
|
+
result.kind.padEnd(7),
|
|
59
|
+
String(result.status ?? "-").padEnd(4),
|
|
60
|
+
(result.cache ?? "-").padEnd(8),
|
|
61
|
+
`age ${result.age ?? "-"}`.padEnd(10),
|
|
62
|
+
`${result.ttfbMs === null ? "-" : `${result.ttfbMs} ms`}`.padStart(8),
|
|
63
|
+
`${result.edgeRegion ?? "-"}${result.functionRegion ? ` -> ${result.functionRegion}` : ""}`.padEnd(14),
|
|
64
|
+
result.url,
|
|
65
|
+
];
|
|
66
|
+
return `${cells.join(" ")}${result.problem ? ` (${result.problem})` : ""}`;
|
|
67
|
+
}
|
|
68
|
+
export async function runEdgeProbe(requests, options = {}) {
|
|
69
|
+
const fetcher = options.fetch ?? globalThis.fetch;
|
|
70
|
+
const now = options.now ?? (() => performance.now());
|
|
71
|
+
const results = [];
|
|
72
|
+
for (const request of requests) {
|
|
73
|
+
const started = now();
|
|
74
|
+
try {
|
|
75
|
+
const response = await fetcher(request.url, { headers: { ...request.headers, "user-agent": "brandlab-edge-probe" }, redirect: "manual", signal: AbortSignal.timeout(30_000) });
|
|
76
|
+
const ttfbMs = Math.round(now() - started);
|
|
77
|
+
await response.body?.cancel().catch(() => undefined);
|
|
78
|
+
const vercelId = response.headers.get("x-vercel-id");
|
|
79
|
+
const regions = vercelIdRegions(vercelId);
|
|
80
|
+
const problem = request.expectStatus !== undefined && response.status !== request.expectStatus ? `expected ${request.expectStatus}` : undefined;
|
|
81
|
+
results.push({
|
|
82
|
+
kind: request.kind,
|
|
83
|
+
url: request.url,
|
|
84
|
+
status: response.status,
|
|
85
|
+
cache: response.headers.get("x-vercel-cache"),
|
|
86
|
+
age: response.headers.get("age"),
|
|
87
|
+
vercelId,
|
|
88
|
+
functionRegion: regions.function,
|
|
89
|
+
edgeRegion: regions.edge,
|
|
90
|
+
ttfbMs,
|
|
91
|
+
...(problem ? { problem } : {}),
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
catch (error) {
|
|
95
|
+
results.push({ kind: request.kind, url: request.url, status: null, cache: null, age: null, vercelId: null, functionRegion: null, edgeRegion: null, ttfbMs: null, problem: error instanceof Error ? error.message : String(error) });
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return results;
|
|
99
|
+
}
|
|
100
|
+
export async function runEdgeProbeCli(argv, io, options = {}) {
|
|
101
|
+
try {
|
|
102
|
+
const json = argv.includes("--json");
|
|
103
|
+
const urls = argv.filter((argument) => argument !== "--json");
|
|
104
|
+
const unknown = urls.find((argument) => argument.startsWith("--"));
|
|
105
|
+
if (unknown)
|
|
106
|
+
throw new Error(`Unknown option ${unknown}. Usage: brandlab-edge-probe [--json] <url>...`);
|
|
107
|
+
const results = await runEdgeProbe(planEdgeProbe(urls), options);
|
|
108
|
+
if (json)
|
|
109
|
+
io.stdout(`${JSON.stringify(results, null, 2)}\n`);
|
|
110
|
+
else
|
|
111
|
+
io.stdout(`${results.map(formatEdgeProbeRow).join("\n")}\n`);
|
|
112
|
+
return results.some((result) => result.problem) ? 1 : 0;
|
|
113
|
+
}
|
|
114
|
+
catch (error) {
|
|
115
|
+
io.stderr(`${error instanceof Error ? error.message : String(error)}\n`);
|
|
116
|
+
return 2;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
function isEntrypoint(moduleUrl, argvPath) {
|
|
120
|
+
if (!argvPath)
|
|
121
|
+
return false;
|
|
122
|
+
try {
|
|
123
|
+
return realpathSync(fileURLToPath(moduleUrl)) === realpathSync(resolve(argvPath));
|
|
124
|
+
}
|
|
125
|
+
catch {
|
|
126
|
+
return false;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
if (isEntrypoint(import.meta.url, process.argv[1])) {
|
|
130
|
+
runEdgeProbeCli(process.argv.slice(2), { stdout: (value) => process.stdout.write(value), stderr: (value) => process.stderr.write(value) }).then((code) => {
|
|
131
|
+
process.exitCode = code;
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
//# sourceMappingURL=edge-probe-cli.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"edge-probe-cli.js","sourceRoot":"","sources":["../src/edge-probe-cli.ts"],"names":[],"mappings":";AACA;;;;;;;;;;GAUG;AACH,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AACvC,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACpC,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAEzC,MAAM,CAAC,MAAM,mBAAmB,GAAG,EAAE,CAAC;AA0BtC,wGAAwG;AACxG,MAAM,UAAU,aAAa,CAAC,IAAuB;IACnD,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC;QAAE,MAAM,IAAI,KAAK,CAAC,iCAAiC,CAAC,CAAC;IAC1E,IAAI,IAAI,CAAC,MAAM,GAAG,mBAAmB;QAAE,MAAM,IAAI,KAAK,CAAC,iBAAiB,mBAAmB,4CAA4C,CAAC,CAAC;IACzI,MAAM,QAAQ,GAAuB,EAAE,CAAC;IACxC,MAAM,OAAO,GAAa,EAAE,CAAC;IAC7B,KAAK,MAAM,GAAG,IAAI,IAAI,EAAE,CAAC;QACvB,IAAI,GAAQ,CAAC;QACb,IAAI,CAAC;YACH,GAAG,GAAG,IAAI,GAAG,CAAC,GAAG,CAAC,CAAC;QACrB,CAAC;QAAC,MAAM,CAAC;YACP,MAAM,IAAI,KAAK,CAAC,IAAI,GAAG,uEAAuE,CAAC,CAAC;QAClG,CAAC;QACD,IAAI,GAAG,CAAC,QAAQ,KAAK,QAAQ,IAAI,GAAG,CAAC,QAAQ,KAAK,OAAO;YAAE,MAAM,IAAI,KAAK,CAAC,IAAI,GAAG,0BAA0B,CAAC,CAAC;QAC9G,GAAG,CAAC,IAAI,GAAG,EAAE,CAAC;QACd,MAAM,IAAI,GAAG,GAAG,CAAC,QAAQ,EAAE,CAAC;QAC5B,QAAQ,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,IAAI,EAAE,OAAO,EAAE,EAAE,MAAM,EAAE,WAAW,EAAE,EAAE,CAAC,CAAC;QAC7E,QAAQ,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,KAAK,EAAE,GAAG,EAAE,IAAI,EAAE,OAAO,EAAE,EAAE,GAAG,EAAE,GAAG,EAAE,EAAE,CAAC,CAAC;QACjE,QAAQ,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,IAAI,EAAE,OAAO,EAAE,EAAE,GAAG,EAAE,GAAG,EAAE,sBAAsB,EAAE,GAAG,EAAE,8BAA8B,EAAE,QAAQ,EAAE,EAAE,CAAC,CAAC;QACzI,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAC,MAAM,CAAC;YAAE,OAAO,CAAC,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;IAC9D,CAAC;IACD,KAAK,MAAM,MAAM,IAAI,OAAO;QAAE,QAAQ,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,SAAS,EAAE,GAAG,EAAE,GAAG,MAAM,cAAc,EAAE,OAAO,EAAE,EAAE,MAAM,EAAE,kBAAkB,EAAE,EAAE,YAAY,EAAE,GAAG,EAAE,CAAC,CAAC;IAC3J,OAAO,QAAQ,CAAC;AAClB,CAAC;AAED,uHAAuH;AACvH,MAAM,UAAU,eAAe,CAAC,QAAuB;IACrD,IAAI,CAAC,QAAQ;QAAE,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;IACrD,MAAM,KAAK,GAAG,QAAQ,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;IACnC,MAAM,MAAM,GAAG,CAAC,IAAwB,EAAE,EAAE,CAAC,CAAC,IAAI,IAAI,cAAc,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC;IAC/F,OAAO,EAAE,IAAI,EAAE,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,KAAK,CAAC,MAAM,IAAI,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;AAC3F,CAAC;AAED,mDAAmD;AACnD,MAAM,UAAU,kBAAkB,CAAC,MAAuB;IACxD,MAAM,KAAK,GAAG;QACZ,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC;QACrB,MAAM,CAAC,MAAM,CAAC,MAAM,IAAI,GAAG,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC;QACtC,CAAC,MAAM,CAAC,KAAK,IAAI,GAAG,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC;QAC/B,OAAO,MAAM,CAAC,GAAG,IAAI,GAAG,EAAE,CAAC,MAAM,CAAC,EAAE,CAAC;QACrC,GAAG,MAAM,CAAC,MAAM,KAAK,IAAI,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,MAAM,CAAC,MAAM,KAAK,EAAE,CAAC,QAAQ,CAAC,CAAC,CAAC;QACrE,GAAG,MAAM,CAAC,UAAU,IAAI,GAAG,GAAG,MAAM,CAAC,cAAc,CAAC,CAAC,CAAC,OAAO,MAAM,CAAC,cAAc,EAAE,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,MAAM,CAAC,EAAE,CAAC;QACtG,MAAM,CAAC,GAAG;KACX,CAAC;IACF,OAAO,GAAG,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,MAAM,CAAC,OAAO,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC;AAC/E,CAAC;AAQD,MAAM,CAAC,KAAK,UAAU,YAAY,CAAC,QAAqC,EAAE,UAAmD,EAAE;IAC7H,MAAM,OAAO,GAAG,OAAO,CAAC,KAAK,IAAK,UAAU,CAAC,KAA4B,CAAC;IAC1E,MAAM,GAAG,GAAG,OAAO,CAAC,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC,WAAW,CAAC,GAAG,EAAE,CAAC,CAAC;IACrD,MAAM,OAAO,GAAsB,EAAE,CAAC;IACtC,KAAK,MAAM,OAAO,IAAI,QAAQ,EAAE,CAAC;QAC/B,MAAM,OAAO,GAAG,GAAG,EAAE,CAAC;QACtB,IAAI,CAAC;YACH,MAAM,QAAQ,GAAG,MAAM,OAAO,CAAC,OAAO,CAAC,GAAG,EAAE,EAAE,OAAO,EAAE,EAAE,GAAG,OAAO,CAAC,OAAO,EAAE,YAAY,EAAE,qBAAqB,EAAE,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,EAAE,WAAW,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;YAC/K,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,EAAE,GAAG,OAAO,CAAC,CAAC;YAC3C,MAAM,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,SAAS,CAAC,CAAC;YACrD,MAAM,QAAQ,GAAG,QAAQ,CAAC,OAAO,CAAC,GAAG,CAAC,aAAa,CAAC,CAAC;YACrD,MAAM,OAAO,GAAG,eAAe,CAAC,QAAQ,CAAC,CAAC;YAC1C,MAAM,OAAO,GAAG,OAAO,CAAC,YAAY,KAAK,SAAS,IAAI,QAAQ,CAAC,MAAM,KAAK,OAAO,CAAC,YAAY,CAAC,CAAC,CAAC,YAAY,OAAO,CAAC,YAAY,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC;YAChJ,OAAO,CAAC,IAAI,CAAC;gBACX,IAAI,EAAE,OAAO,CAAC,IAAI;gBAClB,GAAG,EAAE,OAAO,CAAC,GAAG;gBAChB,MAAM,EAAE,QAAQ,CAAC,MAAM;gBACvB,KAAK,EAAE,QAAQ,CAAC,OAAO,CAAC,GAAG,CAAC,gBAAgB,CAAC;gBAC7C,GAAG,EAAE,QAAQ,CAAC,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC;gBAChC,QAAQ;gBACR,cAAc,EAAE,OAAO,CAAC,QAAQ;gBAChC,UAAU,EAAE,OAAO,CAAC,IAAI;gBACxB,MAAM;gBACN,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;aAChC,CAAC,CAAC;QACL,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,OAAO,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,OAAO,CAAC,IAAI,EAAE,GAAG,EAAE,OAAO,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,GAAG,EAAE,IAAI,EAAE,QAAQ,EAAE,IAAI,EAAE,cAAc,EAAE,IAAI,EAAE,UAAU,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,EAAE,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;QACtO,CAAC;IACH,CAAC;IACD,OAAO,OAAO,CAAC;AACjB,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,eAAe,CAAC,IAAuB,EAAE,EAAsE,EAAE,UAA+B,EAAE;IACtK,IAAI,CAAC;QACH,MAAM,IAAI,GAAG,IAAI,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC;QACrC,MAAM,IAAI,GAAG,IAAI,CAAC,MAAM,CAAC,CAAC,QAAQ,EAAE,EAAE,CAAC,QAAQ,KAAK,QAAQ,CAAC,CAAC;QAC9D,MAAM,OAAO,GAAG,IAAI,CAAC,IAAI,CAAC,CAAC,QAAQ,EAAE,EAAE,CAAC,QAAQ,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC,CAAC;QACnE,IAAI,OAAO;YAAE,MAAM,IAAI,KAAK,CAAC,kBAAkB,OAAO,gDAAgD,CAAC,CAAC;QACxG,MAAM,OAAO,GAAG,MAAM,YAAY,CAAC,aAAa,CAAC,IAAI,CAAC,EAAE,OAAO,CAAC,CAAC;QACjE,IAAI,IAAI;YAAE,EAAE,CAAC,MAAM,CAAC,GAAG,IAAI,CAAC,SAAS,CAAC,OAAO,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC;;YACxD,EAAE,CAAC,MAAM,CAAC,GAAG,OAAO,CAAC,GAAG,CAAC,kBAAkB,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QAClE,OAAO,OAAO,CAAC,IAAI,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IAC1D,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,EAAE,CAAC,MAAM,CAAC,GAAG,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;QACzE,OAAO,CAAC,CAAC;IACX,CAAC;AACH,CAAC;AAED,SAAS,YAAY,CAAC,SAAiB,EAAE,QAA4B;IACnE,IAAI,CAAC,QAAQ;QAAE,OAAO,KAAK,CAAC;IAC5B,IAAI,CAAC;QACH,OAAO,YAAY,CAAC,aAAa,CAAC,SAAS,CAAC,CAAC,KAAK,YAAY,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC,CAAC;IACpF,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,KAAK,CAAC;IACf,CAAC;AACH,CAAC;AAED,IAAI,YAAY,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,EAAE,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;IACnD,eAAe,CAAC,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,EAAE,MAAM,EAAE,CAAC,KAAK,EAAE,EAAE,CAAC,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC,KAAK,EAAE,EAAE,CAAC,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,EAAE;QACvJ,OAAO,CAAC,QAAQ,GAAG,IAAI,CAAC;IAC1B,CAAC,CAAC,CAAC;AACL,CAAC","sourcesContent":["#!/usr/bin/env node\n/**\n * brandlab-edge-probe (3.13.0): `npm run probe:edge`, run on a laptop, never in CI. For each fixed URL it requests the\n * page, its RSC payload and its `_tree` segment prefetch the way the Next 16 router does, then makes one GET to\n * `/api/contact` on each origin (which answers 405), and logs `x-vercel-cache`, `age`, `x-vercel-id` and the time to\n * first byte. It is the measuring stick for the origin-region benchmark (operations/shared-platform/ORIGIN_REGION.md):\n * `x-vercel-id` reads `<edge>::<function region>::<id>`, so `syd1::iad1::…` on `/api/contact` confirms a Washington\n * function. A handful of requests, never a crawl: at most 25 URLs.\n *\n * npx brandlab-edge-probe https://www.example.com.au/ https://www.example.com.au/services/blocked-drains\n * npx brandlab-edge-probe --json https://www.example.com.au/\n */\nimport { realpathSync } from \"node:fs\";\nimport { resolve } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\n\nexport const EDGE_PROBE_MAX_URLS = 25;\n\nexport type EdgeProbeKind = \"page\" | \"rsc\" | \"tree\" | \"contact\";\n\nexport interface EdgeProbeRequest {\n readonly kind: EdgeProbeKind;\n readonly url: string;\n readonly headers: Readonly<Record<string, string>>;\n /** The status the request should answer; only the contact GET has one (405). */\n readonly expectStatus?: number;\n}\n\nexport interface EdgeProbeResult {\n readonly kind: EdgeProbeKind;\n readonly url: string;\n readonly status: number | null;\n readonly cache: string | null;\n readonly age: string | null;\n readonly vercelId: string | null;\n /** The function region from x-vercel-id when the request reached a function. */\n readonly functionRegion: string | null;\n readonly edgeRegion: string | null;\n readonly ttfbMs: number | null;\n readonly problem?: string;\n}\n\n/** The requests for the given URLs: page, RSC and `_tree` for each, then one contact GET per origin. */\nexport function planEdgeProbe(urls: readonly string[]): EdgeProbeRequest[] {\n if (urls.length === 0) throw new Error(\"Name at least one URL to probe.\");\n if (urls.length > EDGE_PROBE_MAX_URLS) throw new Error(`Probe at most ${EDGE_PROBE_MAX_URLS} URLs; this is a measurement, not a crawl.`);\n const requests: EdgeProbeRequest[] = [];\n const origins: string[] = [];\n for (const raw of urls) {\n let url: URL;\n try {\n url = new URL(raw);\n } catch {\n throw new Error(`\"${raw}\" is not an absolute URL such as https://www.example.com.au/services.`);\n }\n if (url.protocol !== \"https:\" && url.protocol !== \"http:\") throw new Error(`\"${raw}\" is not an http(s) URL.`);\n url.hash = \"\";\n const href = url.toString();\n requests.push({ kind: \"page\", url: href, headers: { accept: \"text/html\" } });\n requests.push({ kind: \"rsc\", url: href, headers: { rsc: \"1\" } });\n requests.push({ kind: \"tree\", url: href, headers: { rsc: \"1\", \"next-router-prefetch\": \"1\", \"next-router-segment-prefetch\": \"/_tree\" } });\n if (!origins.includes(url.origin)) origins.push(url.origin);\n }\n for (const origin of origins) requests.push({ kind: \"contact\", url: `${origin}/api/contact`, headers: { accept: \"application/json\" }, expectStatus: 405 });\n return requests;\n}\n\n/** `syd1::iad1::abc-123` → edge syd1, function iad1; `syd1::abc-123` (served at the edge) → edge syd1, no function. */\nexport function vercelIdRegions(vercelId: string | null): { edge: string | null; function: string | null } {\n if (!vercelId) return { edge: null, function: null };\n const parts = vercelId.split(\"::\");\n const region = (part: string | undefined) => (part && /^[a-z]{3}\\d$/.test(part) ? part : null);\n return { edge: region(parts[0]), function: parts.length >= 3 ? region(parts[1]) : null };\n}\n\n/** One result as a row of the plain-text table. */\nexport function formatEdgeProbeRow(result: EdgeProbeResult): string {\n const cells = [\n result.kind.padEnd(7),\n String(result.status ?? \"-\").padEnd(4),\n (result.cache ?? \"-\").padEnd(8),\n `age ${result.age ?? \"-\"}`.padEnd(10),\n `${result.ttfbMs === null ? \"-\" : `${result.ttfbMs} ms`}`.padStart(8),\n `${result.edgeRegion ?? \"-\"}${result.functionRegion ? ` -> ${result.functionRegion}` : \"\"}`.padEnd(14),\n result.url,\n ];\n return `${cells.join(\" \")}${result.problem ? ` (${result.problem})` : \"\"}`;\n}\n\ntype Fetcher = (url: string, init: { headers: Record<string, string>; redirect: \"manual\"; signal?: AbortSignal }) => Promise<{\n status: number;\n headers: { get(name: string): string | null };\n body?: { cancel(): Promise<void> } | null;\n}>;\n\nexport async function runEdgeProbe(requests: readonly EdgeProbeRequest[], options: { fetch?: Fetcher; now?: () => number } = {}): Promise<EdgeProbeResult[]> {\n const fetcher = options.fetch ?? (globalThis.fetch as unknown as Fetcher);\n const now = options.now ?? (() => performance.now());\n const results: EdgeProbeResult[] = [];\n for (const request of requests) {\n const started = now();\n try {\n const response = await fetcher(request.url, { headers: { ...request.headers, \"user-agent\": \"brandlab-edge-probe\" }, redirect: \"manual\", signal: AbortSignal.timeout(30_000) });\n const ttfbMs = Math.round(now() - started);\n await response.body?.cancel().catch(() => undefined);\n const vercelId = response.headers.get(\"x-vercel-id\");\n const regions = vercelIdRegions(vercelId);\n const problem = request.expectStatus !== undefined && response.status !== request.expectStatus ? `expected ${request.expectStatus}` : undefined;\n results.push({\n kind: request.kind,\n url: request.url,\n status: response.status,\n cache: response.headers.get(\"x-vercel-cache\"),\n age: response.headers.get(\"age\"),\n vercelId,\n functionRegion: regions.function,\n edgeRegion: regions.edge,\n ttfbMs,\n ...(problem ? { problem } : {}),\n });\n } catch (error) {\n results.push({ kind: request.kind, url: request.url, status: null, cache: null, age: null, vercelId: null, functionRegion: null, edgeRegion: null, ttfbMs: null, problem: error instanceof Error ? error.message : String(error) });\n }\n }\n return results;\n}\n\nexport async function runEdgeProbeCli(argv: readonly string[], io: { stdout: (text: string) => void; stderr: (text: string) => void }, options: { fetch?: Fetcher } = {}): Promise<number> {\n try {\n const json = argv.includes(\"--json\");\n const urls = argv.filter((argument) => argument !== \"--json\");\n const unknown = urls.find((argument) => argument.startsWith(\"--\"));\n if (unknown) throw new Error(`Unknown option ${unknown}. Usage: brandlab-edge-probe [--json] <url>...`);\n const results = await runEdgeProbe(planEdgeProbe(urls), options);\n if (json) io.stdout(`${JSON.stringify(results, null, 2)}\\n`);\n else io.stdout(`${results.map(formatEdgeProbeRow).join(\"\\n\")}\\n`);\n return results.some((result) => result.problem) ? 1 : 0;\n } catch (error) {\n io.stderr(`${error instanceof Error ? error.message : String(error)}\\n`);\n return 2;\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 runEdgeProbeCli(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/engineering.d.ts
CHANGED
|
@@ -17,7 +17,10 @@ export interface EngineeringCheck {
|
|
|
17
17
|
coverage: string;
|
|
18
18
|
outstandingEvidence: readonly string[];
|
|
19
19
|
}
|
|
20
|
-
/**
|
|
20
|
+
/**
|
|
21
|
+
* The default Vercel function region (3.2.1): Sydney. From 3.13.0 a site may record another in brandlab.config.ts
|
|
22
|
+
* (`hosting.originRegion`) with an origin-region-v1 decision; `vercel.json` regions must equal the recorded value.
|
|
23
|
+
*/
|
|
21
24
|
export declare const BRANDLAB_FUNCTION_REGION = "syd1";
|
|
22
25
|
/**
|
|
23
26
|
* The Vercel ignored build step of every Brandlab site (3.3.3). Exit 0 skips a deployment, anything else builds it:
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"engineering.d.ts","sourceRoot":"","sources":["../src/engineering.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"engineering.d.ts","sourceRoot":"","sources":["../src/engineering.ts"],"names":[],"mappings":"AAqBA,eAAO,MAAM,yBAAyB,sGAAuG,CAAC;AAE9I,oFAAoF;AACpF,eAAO,MAAM,+BAA+B,4IAC+F,CAAC;AAE5I,iFAAiF;AACjF,eAAO,MAAM,yBAAyB,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CASrE,CAAC;AAEH,MAAM,MAAM,0BAA0B,GAAG,SAAS,GAAG,MAAM,CAAC;AAC5D,MAAM,WAAW,kBAAkB;IAAG,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,0BAA0B,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAA;CAAE;AACxI,MAAM,WAAW,gBAAgB;IAC/B,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC;IAC3B,QAAQ,EAAE,kBAAkB,EAAE,CAAC;IAC/B,QAAQ,EAAE,MAAM,CAAC;IACjB,mBAAmB,EAAE,SAAS,MAAM,EAAE,CAAC;CACxC;AAqBD;;;GAGG;AACH,eAAO,MAAM,wBAAwB,SAAS,CAAC;AAC/C;;;;;GAKG;AACH,eAAO,MAAM,8BAA8B,iOAC6K,CAAC;AAqBzN,iGAAiG;AACjG,wBAAgB,yBAAyB,CAAC,IAAI,EAAE,MAAM,GAAG,gBAAgB,CAyBxE;AAwCD,uDAAuD;AACvD,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,EAAE,CAE3D;AAED;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,GAAG,MAAM,EAAE,GAAG,IAAI,CA2B5G"}
|
package/dist/engineering.js
CHANGED
|
@@ -4,6 +4,8 @@ import ts from 'typescript';
|
|
|
4
4
|
import { globMatch } from './glob.js';
|
|
5
5
|
import { collectBrandlabSource, createBrandlabModuleResolver, findImportedCalls, isDirectory, lineOfNode, listModuleReferences, objectPropertyValue, parseBrandlabSourceFile, walkProjectFiles, } from './source-files.js';
|
|
6
6
|
import { describeCapabilities } from './static-rules.js';
|
|
7
|
+
import { originRegionProblems } from './origin-region.js';
|
|
8
|
+
import { parseProjectHosting } from './site-system/config.js';
|
|
7
9
|
export const ENGINEERING_FAST_COMMANDS = ['typecheck', 'lint', 'test', 'check:unused', 'check:unused:production', 'check:standards'];
|
|
8
10
|
/** The exact fast verification command from the Brandlab 3.0 target (section 6). */
|
|
9
11
|
export const BRANDLAB_EXPECTED_VERIFY_SCRIPT = 'npm run check:standards && npm run typecheck && npm run lint && npm run test && npm run check:unused && npm run check:unused:production';
|
|
@@ -29,7 +31,10 @@ const CODE_OR_STYLE = /\.(?:[cm]?[jt]sx?|mdx|css)$/;
|
|
|
29
31
|
const PASCAL_COMPONENT = /^[A-Z][A-Za-z0-9]*\.tsx$/;
|
|
30
32
|
const CAMEL_MODULE = /^[a-z][A-Za-z0-9]*(?:\.d)?\.ts$/;
|
|
31
33
|
const SEMVER = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/;
|
|
32
|
-
/**
|
|
34
|
+
/**
|
|
35
|
+
* The default Vercel function region (3.2.1): Sydney. From 3.13.0 a site may record another in brandlab.config.ts
|
|
36
|
+
* (`hosting.originRegion`) with an origin-region-v1 decision; `vercel.json` regions must equal the recorded value.
|
|
37
|
+
*/
|
|
33
38
|
export const BRANDLAB_FUNCTION_REGION = 'syd1';
|
|
34
39
|
/**
|
|
35
40
|
* The Vercel ignored build step of every Brandlab site (3.3.3). Exit 0 skips a deployment, anything else builds it:
|
|
@@ -79,7 +84,7 @@ export function checkEngineeringStructure(root) {
|
|
|
79
84
|
inspectServerBoundaries(root, capabilities, source, add);
|
|
80
85
|
inspectTests(root, capabilities, add);
|
|
81
86
|
inspectToolPresets(root, add);
|
|
82
|
-
inspectDelivery(root, add);
|
|
87
|
+
inspectDelivery(root, add, config);
|
|
83
88
|
return { status: findings.some(finding => finding.severity === 'blocker') ? 'blocked' : 'pass', findings,
|
|
84
89
|
coverage: 'Static layout, naming, command, dependency-pin, tool-preset, CI, deployment and documentation structure only. This is not engineering-standard completion.',
|
|
85
90
|
outstandingEvidence: ['Usage and deletion review including assets, configuration and dynamic references', 'Business data and component/style ownership review', 'Clean install, fast checks and production build', 'Full discovered technical URL estate', 'All distinct page families and important browser journeys', 'Exact-source protected Preview', 'Reporting/Ads provider cutover and authorised production verification'] };
|
|
@@ -637,12 +642,15 @@ function inspectToolPresets(root, add) {
|
|
|
637
642
|
}
|
|
638
643
|
}
|
|
639
644
|
// --- CI, dependencies, Vercel and docs (section 6) ---------------------------
|
|
640
|
-
|
|
645
|
+
/** A day before any decision, so a recorded decision's reviewBy never counts as passed. */
|
|
646
|
+
const ORIGIN_DECISION_NEVER_LAPSED = '0000-01-01';
|
|
647
|
+
function inspectDelivery(root, add, config) {
|
|
641
648
|
const workflowDirectory = join(root, '.github/workflows');
|
|
642
649
|
const workflows = isDirectory(workflowDirectory) ? readdirSync(workflowDirectory).filter(name => /\.ya?ml$/.test(name)).sort() : [];
|
|
643
|
-
|
|
650
|
+
// CI v2 (verify-next-v2.yml, 3.13.0) is the same verifier with draft, ready-for-review and merge depths.
|
|
651
|
+
const callers = workflows.filter(name => /uses\s*:\s*['"]?RomieDandan\/brandlab\/\.github\/workflows\/verify-next(?:-v2)?\.yml@/.test(readFileSync(join(workflowDirectory, name), 'utf8')));
|
|
644
652
|
if (callers.length !== 1 || callers[0] !== 'verify.yml') {
|
|
645
|
-
add('.github/workflows/verify.yml', 'ci-workflow', `Exactly one .github/workflows/verify.yml calls RomieDandan/brandlab/.github/workflows/verify-next.yml at the fleet SHA (found ${callers.length ? callers.join(', ') : 'none'}).`);
|
|
653
|
+
add('.github/workflows/verify.yml', 'ci-workflow', `Exactly one .github/workflows/verify.yml calls RomieDandan/brandlab/.github/workflows/verify-next.yml (or verify-next-v2.yml) at the fleet SHA (found ${callers.length ? callers.join(', ') : 'none'}).`);
|
|
646
654
|
}
|
|
647
655
|
for (const path of ['.github/dependabot.yml', '.github/dependabot.yaml', 'renovate.json', 'renovate.json5', '.github/renovate.json', '.renovaterc', '.renovaterc.json']) {
|
|
648
656
|
if (existsSync(join(root, path)))
|
|
@@ -658,15 +666,12 @@ function inspectDelivery(root, add) {
|
|
|
658
666
|
}
|
|
659
667
|
if (installCommand !== 'npm ci')
|
|
660
668
|
add('vercel.json', 'vercel-install', 'vercel.json sets installCommand to "npm ci".');
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
}
|
|
668
|
-
if (!Array.isArray(regions) || regions.length !== 1 || regions[0] !== BRANDLAB_FUNCTION_REGION) {
|
|
669
|
-
add('vercel.json', 'vercel-region', `vercel.json runs the server functions in Sydney: "regions": ["${BRANDLAB_FUNCTION_REGION}"] (found ${regions === undefined ? 'no regions' : JSON.stringify(regions)}). The visitors are in Australia; a Washington (iad1) function adds a Pacific round trip to every request it serves.`);
|
|
669
|
+
// W5 (3.13.0): the regions equal the recorded origin region, Sydney unless an origin-region-v1 decision says otherwise.
|
|
670
|
+
// This check runs inside every site's `npm run verify`, so it never judges the date: a lapsed reviewBy must not stop
|
|
671
|
+
// a build, a fix or a rollback. The lapse fails only the ready gate's W5 (as site-core's config validation does).
|
|
672
|
+
const hosting = config === null ? undefined : parseProjectHosting(config);
|
|
673
|
+
for (const problem of originRegionProblems({ hosting, vercelJson: vercel, today: ORIGIN_DECISION_NEVER_LAPSED })) {
|
|
674
|
+
add(problem.startsWith('vercel.json') ? 'vercel.json' : 'brandlab.config.ts', 'vercel-region', problem);
|
|
670
675
|
}
|
|
671
676
|
let ignoreCommand;
|
|
672
677
|
try {
|