@brandlab-digital/conformance 3.13.0 → 3.15.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 +68 -2
- package/dist/analytics-e2e.d.ts +10 -16
- package/dist/analytics-e2e.d.ts.map +1 -1
- package/dist/analytics-e2e.js +61 -3
- package/dist/analytics-e2e.js.map +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +4 -1
- package/dist/cli.js.map +1 -1
- package/dist/current-release.js +5 -5
- package/dist/current-release.js.map +1 -1
- package/dist/engineering.d.ts.map +1 -1
- package/dist/engineering.js +18 -2
- package/dist/engineering.js.map +1 -1
- package/dist/estate-snapshot.d.ts +54 -0
- package/dist/estate-snapshot.d.ts.map +1 -0
- package/dist/estate-snapshot.js +164 -0
- package/dist/estate-snapshot.js.map +1 -0
- package/dist/estate.d.ts +38 -0
- package/dist/estate.d.ts.map +1 -1
- package/dist/estate.js +71 -28
- package/dist/estate.js.map +1 -1
- package/dist/media-snapshot.d.ts +53 -0
- package/dist/media-snapshot.d.ts.map +1 -0
- package/dist/media-snapshot.js +66 -0
- package/dist/media-snapshot.js.map +1 -0
- package/dist/project.d.ts +20 -0
- package/dist/project.d.ts.map +1 -1
- package/dist/project.js +92 -2
- package/dist/project.js.map +1 -1
- package/dist/ready-cli.d.ts.map +1 -1
- package/dist/ready-cli.js +8 -3
- package/dist/ready-cli.js.map +1 -1
- package/dist/rule-registry.d.ts +5 -0
- package/dist/rule-registry.d.ts.map +1 -1
- package/dist/rule-registry.js +1 -0
- package/dist/rule-registry.js.map +1 -1
- package/dist/site-system/checks.d.ts +28 -3
- package/dist/site-system/checks.d.ts.map +1 -1
- package/dist/site-system/checks.js +5 -2
- package/dist/site-system/checks.js.map +1 -1
- package/dist/site-system/config.d.ts +13 -0
- package/dist/site-system/config.d.ts.map +1 -1
- package/dist/site-system/config.js +30 -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/conversion.d.ts.map +1 -1
- package/dist/site-system/journeys/conversion.js +15 -2
- package/dist/site-system/journeys/conversion.js.map +1 -1
- package/dist/site-system/journeys/menu.d.ts.map +1 -1
- package/dist/site-system/journeys/menu.js +24 -1
- package/dist/site-system/journeys/menu.js.map +1 -1
- package/dist/site-system/journeys/page-helpers.d.ts.map +1 -1
- package/dist/site-system/journeys/page-helpers.js +1 -1
- package/dist/site-system/journeys/page-helpers.js.map +1 -1
- package/dist/site-system/journeys/plan.d.ts +13 -3
- package/dist/site-system/journeys/plan.d.ts.map +1 -1
- package/dist/site-system/journeys/plan.js +12 -2
- package/dist/site-system/journeys/plan.js.map +1 -1
- package/dist/site-system/journeys/shell.d.ts +1 -1
- package/dist/site-system/journeys/shell.js +2 -2
- package/dist/site-system/journeys/shell.js.map +1 -1
- package/dist/site-system/report.d.ts.map +1 -1
- package/dist/site-system/report.js +42 -8
- package/dist/site-system/report.js.map +1 -1
- 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/delivery/h13-inbox.d.ts +23 -0
- package/dist/site-system/rules/delivery/h13-inbox.d.ts.map +1 -0
- package/dist/site-system/rules/delivery/h13-inbox.js +208 -0
- package/dist/site-system/rules/delivery/h13-inbox.js.map +1 -0
- package/dist/site-system/rules/delivery/index.d.ts +12 -0
- package/dist/site-system/rules/delivery/index.d.ts.map +1 -0
- package/dist/site-system/rules/delivery/index.js +7 -0
- package/dist/site-system/rules/delivery/index.js.map +1 -0
- package/dist/site-system/rules/form/c7-thank-you.d.ts +7 -1
- package/dist/site-system/rules/form/c7-thank-you.d.ts.map +1 -1
- package/dist/site-system/rules/form/c7-thank-you.js +26 -3
- package/dist/site-system/rules/form/c7-thank-you.js.map +1 -1
- package/dist/site-system/rules/form/index.d.ts.map +1 -1
- package/dist/site-system/rules/form/index.js +11 -1
- package/dist/site-system/rules/form/index.js.map +1 -1
- package/dist/site-system/rules/full/common.d.ts +2 -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/e1-pattern.d.ts +4 -0
- package/dist/site-system/rules/full/e1-pattern.d.ts.map +1 -0
- package/dist/site-system/rules/full/e1-pattern.js +47 -0
- package/dist/site-system/rules/full/e1-pattern.js.map +1 -0
- package/dist/site-system/rules/full/estate.d.ts +26 -3
- package/dist/site-system/rules/full/estate.d.ts.map +1 -1
- package/dist/site-system/rules/full/estate.js +58 -9
- package/dist/site-system/rules/full/estate.js.map +1 -1
- package/dist/site-system/rules/full/geography.d.ts +15 -2
- package/dist/site-system/rules/full/geography.d.ts.map +1 -1
- package/dist/site-system/rules/full/geography.js +15 -2
- package/dist/site-system/rules/full/geography.js.map +1 -1
- package/dist/site-system/rules/full/index.d.ts +4 -3
- package/dist/site-system/rules/full/index.d.ts.map +1 -1
- package/dist/site-system/rules/full/index.js +20 -5
- package/dist/site-system/rules/full/index.js.map +1 -1
- package/dist/site-system/rules/full/k12-slots.d.ts +4 -0
- package/dist/site-system/rules/full/k12-slots.d.ts.map +1 -0
- package/dist/site-system/rules/full/k12-slots.js +40 -0
- package/dist/site-system/rules/full/k12-slots.js.map +1 -0
- package/dist/site-system/rules/full/l10-location-copy.d.ts +1 -1
- package/dist/site-system/rules/full/l10-location-copy.d.ts.map +1 -1
- package/dist/site-system/rules/full/l10-location-copy.js +5 -2
- package/dist/site-system/rules/full/l10-location-copy.js.map +1 -1
- package/dist/site-system/rules/full/l3-neighbours.d.ts +3 -2
- package/dist/site-system/rules/full/l3-neighbours.d.ts.map +1 -1
- package/dist/site-system/rules/full/l3-neighbours.js +4 -1
- package/dist/site-system/rules/full/l3-neighbours.js.map +1 -1
- package/dist/site-system/rules/full/l4-local-copy.d.ts +2 -1
- package/dist/site-system/rules/full/l4-local-copy.d.ts.map +1 -1
- package/dist/site-system/rules/full/l4-local-copy.js +8 -3
- package/dist/site-system/rules/full/l4-local-copy.js.map +1 -1
- package/dist/site-system/rules/full/l9-data-contract.js +2 -2
- package/dist/site-system/rules/full/l9-data-contract.js.map +1 -1
- package/dist/site-system/rules/full/links.d.ts +3 -1
- package/dist/site-system/rules/full/links.d.ts.map +1 -1
- package/dist/site-system/rules/full/links.js +15 -5
- package/dist/site-system/rules/full/links.js.map +1 -1
- package/dist/site-system/rules/full/loc-index.d.ts +5 -0
- package/dist/site-system/rules/full/loc-index.d.ts.map +1 -1
- package/dist/site-system/rules/full/loc-index.js +43 -2
- package/dist/site-system/rules/full/loc-index.js.map +1 -1
- package/dist/site-system/rules/full/route-kinds.d.ts +15 -0
- package/dist/site-system/rules/full/route-kinds.d.ts.map +1 -0
- package/dist/site-system/rules/full/route-kinds.js +52 -0
- package/dist/site-system/rules/full/route-kinds.js.map +1 -0
- package/dist/site-system/rules/full/strategy.d.ts +6 -0
- package/dist/site-system/rules/full/strategy.d.ts.map +1 -1
- package/dist/site-system/rules/full/strategy.js +98 -10
- package/dist/site-system/rules/full/strategy.js.map +1 -1
- package/dist/site-system/rules/sections/k2-skin-coverage.d.ts +2 -2
- package/dist/site-system/rules/sections/k2-skin-coverage.d.ts.map +1 -1
- package/dist/site-system/rules/sections/k2-skin-coverage.js +13 -2
- package/dist/site-system/rules/sections/k2-skin-coverage.js.map +1 -1
- package/dist/site-system/rules/sections/t6-media-entries.d.ts.map +1 -1
- package/dist/site-system/rules/sections/t6-media-entries.js +69 -5
- package/dist/site-system/rules/sections/t6-media-entries.js.map +1 -1
- package/dist/site-system/runners.js +1 -1
- package/dist/site-system/runners.js.map +1 -1
- package/dist/site-system-e2e.d.ts +1 -1
- package/dist/site-system-e2e.d.ts.map +1 -1
- package/dist/site-system-e2e.js +17 -4
- package/dist/site-system-e2e.js.map +1 -1
- package/package.json +10 -2
package/README.md
CHANGED
|
@@ -22,8 +22,28 @@ A site adopts Site System v2 by declaring `siteSystem: { tier: 'shell' | 'enquir
|
|
|
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
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
|
|
26
|
-
the
|
|
25
|
+
every 7 days, each `{ family, revalidateSeconds, reason, decisionId }`, for W3 from 3.13.0; `replyTime` records a
|
|
26
|
+
decision to promise no reply time on the thank-you page, for C7 from 3.15.0, below). Without it, nothing in the gate
|
|
27
|
+
blocks.
|
|
28
|
+
|
|
29
|
+
**C7, the thank-you page** (enquiry tier): every sampled thank-you page (a `/thank-you`-style path or the library's
|
|
30
|
+
`ThankYou` block) is `noindex, follow`, has a `tel:` link in the block (or `<main>`) and says in its reply slot
|
|
31
|
+
(`data-slot="reply"`) when the business will reply, as a specific time ("within the hour", "within one to two business
|
|
32
|
+
days"; `C7_REPLY_TIME`). Vague words such as "shortly" or "as soon as we can" fail. When the owner decides to promise
|
|
33
|
+
no time, the site records it (3.15.0):
|
|
34
|
+
|
|
35
|
+
```ts
|
|
36
|
+
siteSystem: {
|
|
37
|
+
tier: 'enquiry',
|
|
38
|
+
replyTime: { promise: false, wording: 'We will call you back as soon as possible.', decidedBy: 'owner', decidedOn: '2026-10-24' },
|
|
39
|
+
},
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
C7 then accepts that wording in the reply slot instead of a time (compared ignoring case, spacing and punctuation),
|
|
43
|
+
fails a page without it or one that still promises a time, and names the recorded decision in its pass message. The
|
|
44
|
+
wording may not contain a time itself (digits, minutes, hours, "same day", "next day", a business or working day,
|
|
45
|
+
"today", "tomorrow"); `decidedBy` is `owner` or `romie` (for the owner) and `decidedOn` a real date. `{ promise: true }`
|
|
46
|
+
or no record keeps the specific-time rule. Noindex, the call link and everything else C7 checks are unchanged.
|
|
27
47
|
|
|
28
48
|
- `brandlab-ready` (after `next build` and the site-system journeys) writes `docs/ready/ready-report.md` and
|
|
29
49
|
`.json`: every check with its tier, whether it blocks for this site, its status (pass, fail, not run, excepted,
|
|
@@ -37,6 +57,28 @@ the gate blocks.
|
|
|
37
57
|
`--base-url`, merges `--findings <file>` results for K10 and X1 (the lenient harness) and the brandlab-verify
|
|
38
58
|
search, contact and analytics families, and honours the project's `exceptions` (`ruleId` is the check id;
|
|
39
59
|
expired exceptions do not count).
|
|
60
|
+
- The journeys come from `tests/e2e/site-system.spec.ts`, run with the JSON reporter into
|
|
61
|
+
`test-results/site-system.json`:
|
|
62
|
+
|
|
63
|
+
```ts
|
|
64
|
+
import { defineBrandlabSiteSystemTests } from '@brandlab-digital/conformance/site-system-e2e'
|
|
65
|
+
|
|
66
|
+
defineBrandlabSiteSystemTests({
|
|
67
|
+
tier: 'full', // the tier brandlab.config.ts declares
|
|
68
|
+
pages: {
|
|
69
|
+
home: '/',
|
|
70
|
+
money: ['/services/blocked-drains'], // C1, from the enquiry tier: service pages with the enquiry form
|
|
71
|
+
contact: '/contact',
|
|
72
|
+
families: ['/locations/ryde', '/blog/blocked-drain-cost-sydney'], // W9, full tier: one built page per dynamic route family
|
|
73
|
+
},
|
|
74
|
+
})
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
A journey registers from the release that brings it (W2 and W9 from 3.13.0). The release is the site's installed
|
|
78
|
+
Brandlab release, read from package.json as `brandlab-ready` reads it, a canary included (3.15.0: before it the
|
|
79
|
+
journeys used the conformance package's last published release, so a canary ran none of its newer journeys while
|
|
80
|
+
the gate blocked on them). Leave `release` out; set it only to preview a later release's journeys. When the report
|
|
81
|
+
has journey results but none for a blocking journey check, the gate says which tier, pages and release it needs.
|
|
40
82
|
- `brandlab-ready record <id> --evidence <where the proof is> [--note <text>]` records a launch-day step (T2,
|
|
41
83
|
T3-R, C6, X3 to X10) or a recorded check (K10, X1) in `docs/ready/launch.json`, or a human step (truth-pass,
|
|
42
84
|
trust-carry-over, usefulness-read, codex-second-opinion, launch-proof, look-pick) in `docs/ready/human.json`.
|
|
@@ -68,6 +110,30 @@ the gate blocks.
|
|
|
68
110
|
- **W9** (journey, full): `defineBrandlabSiteSystemTests({ pages: { families: [...] } })` names one built page per
|
|
69
111
|
dynamic route family; an unknown slug answers the shared 404, the uppercase slug answers it too or redirects once
|
|
70
112
|
(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`.
|
|
113
|
+
- **K12-slots** (rendered, full, from 3.14.0): the lines a trade requires inside a section (site-core
|
|
114
|
+
`SECTION_TRADE_PROFILES`, for the trade `defineSectionOrder` names): for trees, the council permit note in
|
|
115
|
+
`LocalFacts` on suburb and service-in-suburb pages and the qualifications and insurance lines on the about page. The
|
|
116
|
+
section carries `data-bl-slot="<slot>"`, or lists `<slot>:no-…` in `data-bl-slot-skip` (a warning, so the record is
|
|
117
|
+
re-checked). The skip is read only from the top-level section element (the outermost `data-bl-section`): a site
|
|
118
|
+
element that wraps `LocalFacts` in its own section hides the skip, and the page fails as if no reason were given.
|
|
119
|
+
Render `LocalFacts` itself as the `SectionSequence` candidate, not inside a wrapper. **E1-pattern** (rendered, full, from 3.14.0): every location page outside `docs/recorded-titles.json`
|
|
120
|
+
has the H1 "{Service} {Place}" (no "in", no "the") and a title that starts with it, at most 60 characters
|
|
121
|
+
(site-core `locationHeadingProblem`). **LOC-INDEX** also reads `estate` in strategy.ts (3.14.0): every location page
|
|
122
|
+
is indexed now or in a wave, a `return` wave's pages are in the build already, and a region page at a suburb's slug
|
|
123
|
+
sends the suburb to "{slug}-suburb"; renamed slugs are read as their registry places.
|
|
124
|
+
- **Estate plan snapshot** (3.15.0): the gate reads `waves` and `estate` from strategy.ts without running it. A site
|
|
125
|
+
that computes them (from its location catalogue, as AA does) records them with `writeEstatePlanSnapshot(siteStrategy,
|
|
126
|
+
{ projectRoot, sources? })` from `@brandlab-digital/conformance/estate-snapshot`, called from a site test so `npm run
|
|
127
|
+
verify` keeps `src/data/estate-plan.generated.json` current (fail that test in CI when it reports `changed`; commit
|
|
128
|
+
the file). The file is deterministic: waves in date order, every list sorted, no timestamp, and a hash of the files
|
|
129
|
+
under `sources` (default `src/data`; tests and `*.generated.json` left out). LOC-INDEX (and L3, L6, L8, which read
|
|
130
|
+
the same plan) use the snapshot when it is present, and LOC-INDEX fails it when it is stale: the hashed files changed
|
|
131
|
+
since it was written, or the record is plain data and says something else. A literal record needs no snapshot.
|
|
132
|
+
- **Media snapshot** (3.15.0): T6 reads `src/data/media.ts` the same way. A computed manifest (gallery entries
|
|
133
|
+
spread in) is recorded with `writeMediaSnapshot(media, { projectRoot, sources? })` from
|
|
134
|
+
`@brandlab-digital/conformance/media-snapshot` into `src/data/media.generated.json`, kept current from a site test in
|
|
135
|
+
the same way; T6 reads it, and fails it when stale. A computed manifest without a snapshot fails T6 with that
|
|
136
|
+
instruction (before 3.15.0 it reported nothing, so the gate showed "No result").
|
|
71
137
|
- `brandlab-edge-probe [--json] <url>...` (run locally as `npm run probe:edge`, at most 25 URLs) logs
|
|
72
138
|
`x-vercel-cache`, `age`, `x-vercel-id` (edge and function region) and time to first byte for each page, its RSC
|
|
73
139
|
payload and its `_tree` prefetch, and a GET to each origin's `/api/contact` (expected 405). It is the measuring
|
package/dist/analytics-e2e.d.ts
CHANGED
|
@@ -1,19 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The shared analytics journeys for a Brandlab production build. Google is
|
|
3
|
-
* never contacted: the tag loader is stubbed and every dataLayer command is
|
|
4
|
-
* recorded in the test process, across navigations.
|
|
5
|
-
*
|
|
6
|
-
* // tests/e2e/analytics.spec.ts
|
|
7
|
-
* import { defineBrandlabAnalyticsTests } from '@brandlab-digital/conformance/analytics-e2e'
|
|
8
|
-
*
|
|
9
|
-
* defineBrandlabAnalyticsTests({
|
|
10
|
-
* paths: ['/', '/services/blocked-drains'],
|
|
11
|
-
* contact: { path: '/contact', fields: { ... }, submit: 'Send enquiry' },
|
|
12
|
-
* })
|
|
13
|
-
*
|
|
14
|
-
* Build the e2e target with a test measurement ID (NEXT_PUBLIC_GA_MEASUREMENT_ID)
|
|
15
|
-
* so BrandlabGoogleTag renders.
|
|
16
|
-
*/
|
|
17
1
|
import { type Page } from '@playwright/test';
|
|
18
2
|
import { type BrandlabContactJourney } from './contact-e2e.js';
|
|
19
3
|
export interface BrandlabAnalyticsTestOptions {
|
|
@@ -27,7 +11,16 @@ export interface BrandlabAnalyticsTestOptions {
|
|
|
27
11
|
};
|
|
28
12
|
/** Event names the project's analytics contract declares in additionalEvents. */
|
|
29
13
|
additionalEvents?: readonly string[];
|
|
14
|
+
/**
|
|
15
|
+
* The contract's Enhanced Conversions mode. Defaults to the one `src/config/analytics.ts` declares
|
|
16
|
+
* (`googleAds.enhancedConversions.mode`). Only `disclosed` lets the entered email and phone reach Google Ads, and only
|
|
17
|
+
* through the documented `user_data` path.
|
|
18
|
+
*/
|
|
19
|
+
enhancedConversions?: BrandlabEnhancedConversionsCheckMode;
|
|
30
20
|
}
|
|
21
|
+
export type BrandlabEnhancedConversionsCheckMode = 'disclosed' | 'consent-required' | 'off';
|
|
22
|
+
/** The Enhanced Conversions mode the project's analytics binding declares, read from its source (`off` when none). */
|
|
23
|
+
export declare function readBrandlabEnhancedConversionsMode(root?: string): BrandlabEnhancedConversionsCheckMode;
|
|
31
24
|
/** Event names that 3.0 no longer sends: legacy lead and phone events, GA4 form aliases and web vitals. */
|
|
32
25
|
export declare const BRANDLAB_RETIRED_ANALYTICS_EVENTS: readonly string[];
|
|
33
26
|
/**
|
|
@@ -47,6 +40,7 @@ export declare function analyticsEvents(commands: readonly AnalyticsCommand[], n
|
|
|
47
40
|
export declare function analyticsEventProblems(commands: readonly AnalyticsCommand[], options?: {
|
|
48
41
|
enteredValues?: readonly string[];
|
|
49
42
|
additionalEvents?: readonly string[];
|
|
43
|
+
enhancedConversions?: BrandlabEnhancedConversionsCheckMode;
|
|
50
44
|
}): string[];
|
|
51
45
|
export declare function defineBrandlabAnalyticsTests(options?: BrandlabAnalyticsTestOptions): void;
|
|
52
46
|
//# sourceMappingURL=analytics-e2e.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"analytics-e2e.d.ts","sourceRoot":"","sources":["../src/analytics-e2e.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"analytics-e2e.d.ts","sourceRoot":"","sources":["../src/analytics-e2e.ts"],"names":[],"mappings":"AAkBA,OAAO,EAAgB,KAAK,IAAI,EAAE,MAAM,kBAAkB,CAAC;AAC3D,OAAO,EAA2C,KAAK,sBAAsB,EAAE,MAAM,kBAAkB,CAAC;AAQxG,MAAM,WAAW,4BAA4B;IAC3C,4EAA4E;IAC5E,KAAK,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAC1B,sFAAsF;IACtF,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,wEAAwE;IACxE,OAAO,CAAC,EAAE,sBAAsB,GAAG;QAAE,KAAK,CAAC,EAAE,MAAM,GAAG,MAAM,CAAA;KAAE,CAAC;IAC/D,iFAAiF;IACjF,gBAAgB,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IACrC;;;;OAIG;IACH,mBAAmB,CAAC,EAAE,oCAAoC,CAAC;CAC5D;AAED,MAAM,MAAM,oCAAoC,GAAG,WAAW,GAAG,kBAAkB,GAAG,KAAK,CAAC;AAE5F,sHAAsH;AACtH,wBAAgB,mCAAmC,CAAC,IAAI,GAAE,MAAsB,GAAG,oCAAoC,CAStH;AAED,2GAA2G;AAC3G,eAAO,MAAM,iCAAiC,mBAE5C,CAAC;AAEH;;;;GAIG;AACH,eAAO,MAAM,yBAAyB,mBAEpC,CAAC;AAKH,MAAM,MAAM,gBAAgB,GAAG,OAAO,EAAE,CAAC;AAEzC,kFAAkF;AAClF,wBAAsB,uBAAuB,CAAC,IAAI,EAAE,IAAI,GAAG,OAAO,CAAC,gBAAgB,EAAE,CAAC,CA+BrF;AAED,wBAAgB,eAAe,CAAC,QAAQ,EAAE,SAAS,gBAAgB,EAAE,EAAE,IAAI,CAAC,EAAE,MAAM;UAGvC,MAAM;YAAgC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;IACzG;AAyCD,gGAAgG;AAChG,wBAAgB,sBAAsB,CACpC,QAAQ,EAAE,SAAS,gBAAgB,EAAE,EACrC,OAAO,GAAE;IACP,aAAa,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAClC,gBAAgB,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IACrC,mBAAmB,CAAC,EAAE,oCAAoC,CAAC;CACvD,GACL,MAAM,EAAE,CAqBV;AAED,wBAAgB,4BAA4B,CAAC,OAAO,GAAE,4BAAiC,QAkGtF"}
|
package/dist/analytics-e2e.js
CHANGED
|
@@ -14,9 +14,23 @@
|
|
|
14
14
|
* Build the e2e target with a test measurement ID (NEXT_PUBLIC_GA_MEASUREMENT_ID)
|
|
15
15
|
* so BrandlabGoogleTag renders.
|
|
16
16
|
*/
|
|
17
|
+
import { readFileSync } from 'node:fs';
|
|
18
|
+
import { join } from 'node:path';
|
|
17
19
|
import { expect, test } from '@playwright/test';
|
|
18
20
|
import { fillBrandlabContactForm, successPattern } from './contact-e2e.js';
|
|
19
21
|
import { ACCEPTED_CONTACT_RESULT, RETRYABLE_CONTACT_FAILURE, contactEndpointMatcher, isolateFromNetwork, } from './e2e-support.js';
|
|
22
|
+
/** The Enhanced Conversions mode the project's analytics binding declares, read from its source (`off` when none). */
|
|
23
|
+
export function readBrandlabEnhancedConversionsMode(root = process.cwd()) {
|
|
24
|
+
let source = '';
|
|
25
|
+
try {
|
|
26
|
+
source = readFileSync(join(root, 'src/config/analytics.ts'), 'utf8');
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
return 'off';
|
|
30
|
+
}
|
|
31
|
+
const mode = /enhancedConversions\s*:\s*\{[^}]*\bmode\s*:\s*['"]([a-z-]+)['"]/.exec(source)?.[1];
|
|
32
|
+
return mode === 'disclosed' || mode === 'consent-required' ? mode : 'off';
|
|
33
|
+
}
|
|
20
34
|
/** Event names that 3.0 no longer sends: legacy lead and phone events, GA4 form aliases and web vitals. */
|
|
21
35
|
export const BRANDLAB_RETIRED_ANALYTICS_EVENTS = Object.freeze([
|
|
22
36
|
'phone_call', 'phone_calls', 'contact_form_submission', 'form_start', 'form_submit', 'generate_lead', 'cta_click', 'web_vital',
|
|
@@ -72,6 +86,45 @@ export function analyticsEvents(commands, name) {
|
|
|
72
86
|
.filter((command) => command[0] === 'event' && typeof command[1] === 'string' && (!name || command[1] === name))
|
|
73
87
|
.map((command) => ({ name: command[1], params: (command[2] ?? {}) }));
|
|
74
88
|
}
|
|
89
|
+
const EMAIL_VALUE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
90
|
+
const digitsOf = (value) => value.replace(/\D/g, '');
|
|
91
|
+
/** Whether a user_data field is the visitor's entered email (`email`) or phone (`phone_number`, normalised to E.164). */
|
|
92
|
+
function isEnteredContactField(key, value, entered) {
|
|
93
|
+
if (typeof value !== 'string')
|
|
94
|
+
return false;
|
|
95
|
+
if (key === 'email') {
|
|
96
|
+
return entered.some((item) => EMAIL_VALUE.test(item) && item.toLowerCase() === value.trim().toLowerCase());
|
|
97
|
+
}
|
|
98
|
+
if (key === 'phone_number') {
|
|
99
|
+
const field = digitsOf(value);
|
|
100
|
+
return field.length >= 8 && entered.some((item) => {
|
|
101
|
+
const digits = digitsOf(item);
|
|
102
|
+
return /^[\d\s()+-]+$/.test(item) && digits.length >= 8
|
|
103
|
+
&& (field === digits || field.endsWith(digits.replace(/^0/, '')));
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
return false;
|
|
107
|
+
}
|
|
108
|
+
function withoutEnteredContact(data, entered) {
|
|
109
|
+
if (!data || typeof data !== 'object' || Array.isArray(data))
|
|
110
|
+
return data;
|
|
111
|
+
return Object.fromEntries(Object.entries(data).filter(([key, value]) => !isEnteredContactField(key, value, entered)));
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Disclosed Enhanced Conversions send the entered email and phone to Google Ads by design, through `gtag('set',
|
|
115
|
+
* 'user_data', …)` (the shared runtime) or a conversion's `user_data` parameter. Only those two fields, in only those
|
|
116
|
+
* two places, are taken out before the entered-value check; everything else is still checked.
|
|
117
|
+
*/
|
|
118
|
+
function withoutDisclosedUserData(command, entered) {
|
|
119
|
+
if (command[0] === 'set' && command[1] === 'user_data')
|
|
120
|
+
return ['set', 'user_data', withoutEnteredContact(command[2], entered)];
|
|
121
|
+
if (command[0] === 'event' && command[1] === 'conversion' && command[2] && typeof command[2] === 'object') {
|
|
122
|
+
const params = command[2];
|
|
123
|
+
if ('user_data' in params)
|
|
124
|
+
return ['event', 'conversion', { ...params, user_data: withoutEnteredContact(params.user_data, entered) }];
|
|
125
|
+
}
|
|
126
|
+
return command;
|
|
127
|
+
}
|
|
75
128
|
/** Problems a set of recorded commands shows: retired or unexpected events, or lead details. */
|
|
76
129
|
export function analyticsEventProblems(commands, options = {}) {
|
|
77
130
|
const problems = [];
|
|
@@ -90,8 +143,12 @@ export function analyticsEventProblems(commands, options = {}) {
|
|
|
90
143
|
problems.push(`${event.name} carries the ${key} parameter`);
|
|
91
144
|
}
|
|
92
145
|
}
|
|
93
|
-
const
|
|
94
|
-
|
|
146
|
+
const entered = (options.enteredValues ?? []).map((entry) => entry.trim()).filter((entry) => entry.length >= 5);
|
|
147
|
+
const checked = options.enhancedConversions === 'disclosed'
|
|
148
|
+
? commands.map((command) => withoutDisclosedUserData(command, entered))
|
|
149
|
+
: commands;
|
|
150
|
+
const serialised = JSON.stringify(checked).toLowerCase();
|
|
151
|
+
for (const value of entered) {
|
|
95
152
|
if (serialised.includes(value.toLowerCase()))
|
|
96
153
|
problems.push(`an analytics command contains the entered value "${value}"`);
|
|
97
154
|
}
|
|
@@ -100,6 +157,7 @@ export function analyticsEventProblems(commands, options = {}) {
|
|
|
100
157
|
export function defineBrandlabAnalyticsTests(options = {}) {
|
|
101
158
|
const paths = options.paths?.length ? options.paths : ['/'];
|
|
102
159
|
const additionalEvents = options.additionalEvents ?? [];
|
|
160
|
+
const enhancedConversions = options.enhancedConversions ?? readBrandlabEnhancedConversionsMode();
|
|
103
161
|
test.describe('Brandlab analytics', () => {
|
|
104
162
|
// Service workers could answer requests outside page and context routing.
|
|
105
163
|
test.use({ serviceWorkers: 'block' });
|
|
@@ -168,7 +226,7 @@ export function defineBrandlabAnalyticsTests(options = {}) {
|
|
|
168
226
|
await expect.poll(() => analyticsEvents(commands, 'contact_form').length).toBe(1);
|
|
169
227
|
await page.waitForTimeout(500);
|
|
170
228
|
expect(analyticsEvents(commands, 'contact_form')).toHaveLength(1);
|
|
171
|
-
expect(analyticsEventProblems(commands, { enteredValues: Object.values(journey.fields), additionalEvents })).toEqual([]);
|
|
229
|
+
expect(analyticsEventProblems(commands, { enteredValues: Object.values(journey.fields), additionalEvents, enhancedConversions })).toEqual([]);
|
|
172
230
|
});
|
|
173
231
|
}
|
|
174
232
|
test('sends no retired or web_vital events', async ({ page }) => {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"analytics-e2e.js","sourceRoot":"","sources":["../src/analytics-e2e.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The shared analytics journeys for a Brandlab production build. Google is\n * never contacted: the tag loader is stubbed and every dataLayer command is\n * recorded in the test process, across navigations.\n *\n * // tests/e2e/analytics.spec.ts\n * import { defineBrandlabAnalyticsTests } from '@brandlab-digital/conformance/analytics-e2e'\n *\n * defineBrandlabAnalyticsTests({\n * paths: ['/', '/services/blocked-drains'],\n * contact: { path: '/contact', fields: { ... }, submit: 'Send enquiry' },\n * })\n *\n * Build the e2e target with a test measurement ID (NEXT_PUBLIC_GA_MEASUREMENT_ID)\n * so BrandlabGoogleTag renders.\n */\nimport { expect, test, type Page } from '@playwright/test';\nimport { fillBrandlabContactForm, successPattern, type BrandlabContactJourney } from './contact-e2e.js';\nimport {\n ACCEPTED_CONTACT_RESULT,\n RETRYABLE_CONTACT_FAILURE,\n contactEndpointMatcher,\n isolateFromNetwork,\n} from './e2e-support.js';\n\nexport interface BrandlabAnalyticsTestOptions {\n /** Pages to load; the first renders a telephone link. Defaults to ['/']. */\n paths?: readonly string[];\n /** Selector of the telephone link to tap; defaults to the first visible tel: link. */\n phoneLink?: string;\n /** The contact journey whose accepted result emits one contact_form. */\n contact?: BrandlabContactJourney & { retry?: string | RegExp };\n /** Event names the project's analytics contract declares in additionalEvents. */\n additionalEvents?: readonly string[];\n}\n\n/** Event names that 3.0 no longer sends: legacy lead and phone events, GA4 form aliases and web vitals. */\nexport const BRANDLAB_RETIRED_ANALYTICS_EVENTS = Object.freeze([\n 'phone_call', 'phone_calls', 'contact_form_submission', 'form_start', 'form_submit', 'generate_lead', 'cta_click', 'web_vital',\n]);\n\n/**\n * The events the shared runtime sends; mirrors site-core's CANONICAL_LEAD_EVENTS,\n * CANONICAL_PHONE_INTERACTION_EVENTS and BRANDLAB_LEAD_DIAGNOSTIC_EVENTS. The\n * Google Ads `conversion` event is also allowed, only with an AW- destination.\n */\nexport const BRANDLAB_ANALYTICS_EVENTS = Object.freeze([\n 'contact_form', 'phone_link_click', 'lead_form_start', 'lead_form_error', 'lead_cta_click',\n]);\n\nconst PII_PARAMETER = /^(?:e_?mail|email_address|user_email|contact_email|phone|phone_number|mobile|mobile_number|user_phone|name|first_name|last_name|full_name|customer_name|contact_name|message|enquiry_message|form_message|address|street_address|residential_address|attachment|attachment_name|file_name|filename)$/i;\nconst PHONE_DEDUPE_WINDOW_MS = 1_200;\n\nexport type AnalyticsCommand = unknown[];\n\n/** Record every gtag/dataLayer command the page pushes, surviving navigations. */\nexport async function recordAnalyticsCommands(page: Page): Promise<AnalyticsCommand[]> {\n const commands: AnalyticsCommand[] = [];\n await page.exposeFunction('__brandlabRecordAnalytics', (command: AnalyticsCommand) => { commands.push(command); });\n await page.addInitScript(() => {\n const record = (entry: unknown) => {\n try {\n const command = entry && typeof entry === 'object' && 'length' in (entry as object)\n ? Array.from(entry as ArrayLike<unknown>)\n : [entry];\n void (window as unknown as { __brandlabRecordAnalytics: (value: unknown) => Promise<void> })\n .__brandlabRecordAnalytics(JSON.parse(JSON.stringify(command)));\n } catch { /* an unserialisable command is not analytics evidence */ }\n };\n const wrapped = new WeakSet<unknown[]>();\n const wrap = (layer: unknown[]) => {\n if (wrapped.has(layer)) return layer;\n wrapped.add(layer);\n const push = layer.push.bind(layer);\n layer.push = (...entries: unknown[]) => { entries.forEach(record); return push(...entries); };\n layer.forEach(record);\n return layer;\n };\n let dataLayer = wrap([]);\n Object.defineProperty(window, 'dataLayer', {\n configurable: true,\n get: () => dataLayer,\n // `window.dataLayer = window.dataLayer || []` reassigns the same array.\n set: (value: unknown[]) => { if (Array.isArray(value)) dataLayer = wrap(value); },\n });\n });\n return commands;\n}\n\nexport function analyticsEvents(commands: readonly AnalyticsCommand[], name?: string) {\n return commands\n .filter((command) => command[0] === 'event' && typeof command[1] === 'string' && (!name || command[1] === name))\n .map((command) => ({ name: command[1] as string, params: (command[2] ?? {}) as Record<string, unknown> }));\n}\n\n/** Problems a set of recorded commands shows: retired or unexpected events, or lead details. */\nexport function analyticsEventProblems(\n commands: readonly AnalyticsCommand[],\n options: { enteredValues?: readonly string[]; additionalEvents?: readonly string[] } = {},\n): string[] {\n const problems: string[] = [];\n const allowed = new Set([...BRANDLAB_ANALYTICS_EVENTS, ...(options.additionalEvents ?? [])]);\n for (const event of analyticsEvents(commands)) {\n if (BRANDLAB_RETIRED_ANALYTICS_EVENTS.includes(event.name)) problems.push(`retired event ${event.name}`);\n else if (event.name === 'conversion') {\n if (!String(event.params.send_to ?? '').startsWith('AW-')) problems.push('conversion event without a Google Ads destination');\n } else if (!allowed.has(event.name)) problems.push(`unexpected event ${event.name}`);\n for (const key of Object.keys(event.params)) {\n if (PII_PARAMETER.test(key)) problems.push(`${event.name} carries the ${key} parameter`);\n }\n }\n const serialised = JSON.stringify(commands).toLowerCase();\n for (const value of (options.enteredValues ?? []).map((entry) => entry.trim()).filter((entry) => entry.length >= 5)) {\n if (serialised.includes(value.toLowerCase())) problems.push(`an analytics command contains the entered value \"${value}\"`);\n }\n return problems;\n}\n\nexport function defineBrandlabAnalyticsTests(options: BrandlabAnalyticsTestOptions = {}) {\n const paths = options.paths?.length ? options.paths : ['/'];\n const additionalEvents = options.additionalEvents ?? [];\n\n test.describe('Brandlab analytics', () => {\n // Service workers could answer requests outside page and context routing.\n test.use({ serviceWorkers: 'block' });\n let commands: AnalyticsCommand[] = [];\n test.beforeEach(async ({ page, baseURL }) => {\n await isolateFromNetwork(page, { baseURL, googleTag: 'stub' });\n commands = await recordAnalyticsCommands(page);\n });\n\n test('renders exactly one Google tag on every page', async ({ page }) => {\n for (const path of paths) {\n commands.length = 0;\n // DOMContentLoaded, not load: next start's image optimiser can stall concurrent first image requests\n // (vercel/next.js#98168), and nothing here needs the images. The expectations below wait for the tag.\n await page.goto(path, { waitUntil: 'domcontentloaded' });\n const loaders = page.locator('script[src*=\"googletagmanager.com/gtag/js\"]');\n await expect(loaders.first(), `${path}: no Google tag rendered (build the e2e target with a test NEXT_PUBLIC_GA_MEASUREMENT_ID)`)\n .toBeAttached({ timeout: 15_000 });\n expect(await loaders.count(), `${path}: renders exactly one Google tag loader`).toBe(1);\n await expect.poll(() => commands.filter((command) => command[0] === 'js').length, `${path}: gtag('js') runs once`).toBe(1);\n const destinations = commands.filter((command) => command[0] === 'config').map((command) => String(command[1]));\n expect(new Set(destinations).size, `${path}: a destination is configured twice`).toBe(destinations.length);\n }\n });\n\n test('sends one phone_link_click per tap', async ({ page }) => {\n await page.goto(paths[0]!, { waitUntil: 'domcontentloaded' });\n const link = options.phoneLink ? page.locator(options.phoneLink).first() : page.locator('a[href^=\"tel:\"]:visible').first();\n await expect(link, 'the page renders a telephone link').toBeVisible();\n await expect.poll(() => commands.some((command) => command[0] === 'js')).toBe(true);\n // Keep the page in place; the shared listener still sees the tap.\n await page.evaluate(() => window.addEventListener('click', (event) => {\n if ((event.target as Element | null)?.closest?.('a[href^=\"tel:\"]')) event.preventDefault();\n }, true));\n await link.click();\n await expect.poll(() => analyticsEvents(commands, 'phone_link_click').length).toBe(1);\n await page.waitForTimeout(PHONE_DEDUPE_WINDOW_MS + 300);\n expect(analyticsEvents(commands, 'phone_link_click'), 'one tap sends one event').toHaveLength(1);\n await link.click();\n await expect.poll(() => analyticsEvents(commands, 'phone_link_click').length).toBe(2);\n for (const event of analyticsEvents(commands, 'phone_link_click')) {\n expect(event.params.placement, 'phone_link_click carries its placement').toBeTruthy();\n }\n expect(analyticsEventProblems(commands, { additionalEvents })).toEqual([]);\n });\n\n if (options.contact) {\n const journey = options.contact;\n test('sends one contact_form only after an accepted enquiry, without lead details', async ({ page }) => {\n let attempts = 0;\n await page.route(contactEndpointMatcher(journey.endpoint), async (route) => {\n attempts += 1;\n await route.fulfill(attempts === 1\n ? { status: 503, json: RETRYABLE_CONTACT_FAILURE }\n : { status: 200, json: ACCEPTED_CONTACT_RESULT });\n });\n await fillBrandlabContactForm(page, journey);\n const submit = page.locator('form').filter({ has: page.locator(`[name=\"${Object.keys(journey.fields)[0]}\"]`) })\n .first().getByRole('button', { name: journey.submit, exact: typeof journey.submit === 'string' });\n await submit.click();\n await expect.poll(() => attempts).toBe(1);\n await page.waitForTimeout(500);\n expect(analyticsEvents(commands, 'contact_form'), 'a failed delivery is not a conversion').toHaveLength(0);\n const retry = journey.retry ?? journey.submit;\n await page.locator('form').filter({ has: page.locator(`[name=\"${Object.keys(journey.fields)[0]}\"]`) })\n .first().getByRole('button', { name: retry, exact: typeof retry === 'string' }).click();\n await expect(page).toHaveURL(successPattern(journey.successPath));\n await expect.poll(() => analyticsEvents(commands, 'contact_form').length).toBe(1);\n await page.waitForTimeout(500);\n expect(analyticsEvents(commands, 'contact_form')).toHaveLength(1);\n expect(analyticsEventProblems(commands, { enteredValues: Object.values(journey.fields), additionalEvents })).toEqual([]);\n });\n }\n\n test('sends no retired or web_vital events', async ({ page }) => {\n for (const path of paths) {\n const tagRuns = commands.filter((command) => command[0] === 'js').length;\n await page.goto(path, { waitUntil: 'domcontentloaded' });\n // The page's instrumentation has run once its Google tag has: any reporter would be listening by then.\n await expect.poll(() => commands.filter((command) => command[0] === 'js').length, `${path}: the Google tag runs`).toBeGreaterThan(tagRuns);\n await page.mouse.wheel(0, 1_200);\n await page.waitForTimeout(300);\n // Web-vitals reporters flush when the page is hidden.\n await page.evaluate(() => {\n Object.defineProperty(document, 'visibilityState', { configurable: true, get: () => 'hidden' });\n document.dispatchEvent(new Event('visibilitychange'));\n window.dispatchEvent(new Event('pagehide'));\n });\n await page.waitForTimeout(300);\n }\n expect(analyticsEventProblems(commands, { additionalEvents })).toEqual([]);\n });\n });\n}\n"]}
|
|
1
|
+
{"version":3,"file":"analytics-e2e.js","sourceRoot":"","sources":["../src/analytics-e2e.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The shared analytics journeys for a Brandlab production build. Google is\n * never contacted: the tag loader is stubbed and every dataLayer command is\n * recorded in the test process, across navigations.\n *\n * // tests/e2e/analytics.spec.ts\n * import { defineBrandlabAnalyticsTests } from '@brandlab-digital/conformance/analytics-e2e'\n *\n * defineBrandlabAnalyticsTests({\n * paths: ['/', '/services/blocked-drains'],\n * contact: { path: '/contact', fields: { ... }, submit: 'Send enquiry' },\n * })\n *\n * Build the e2e target with a test measurement ID (NEXT_PUBLIC_GA_MEASUREMENT_ID)\n * so BrandlabGoogleTag renders.\n */\nimport { readFileSync } from 'node:fs';\nimport { join } from 'node:path';\nimport { expect, test, type Page } from '@playwright/test';\nimport { fillBrandlabContactForm, successPattern, type BrandlabContactJourney } from './contact-e2e.js';\nimport {\n ACCEPTED_CONTACT_RESULT,\n RETRYABLE_CONTACT_FAILURE,\n contactEndpointMatcher,\n isolateFromNetwork,\n} from './e2e-support.js';\n\nexport interface BrandlabAnalyticsTestOptions {\n /** Pages to load; the first renders a telephone link. Defaults to ['/']. */\n paths?: readonly string[];\n /** Selector of the telephone link to tap; defaults to the first visible tel: link. */\n phoneLink?: string;\n /** The contact journey whose accepted result emits one contact_form. */\n contact?: BrandlabContactJourney & { retry?: string | RegExp };\n /** Event names the project's analytics contract declares in additionalEvents. */\n additionalEvents?: readonly string[];\n /**\n * The contract's Enhanced Conversions mode. Defaults to the one `src/config/analytics.ts` declares\n * (`googleAds.enhancedConversions.mode`). Only `disclosed` lets the entered email and phone reach Google Ads, and only\n * through the documented `user_data` path.\n */\n enhancedConversions?: BrandlabEnhancedConversionsCheckMode;\n}\n\nexport type BrandlabEnhancedConversionsCheckMode = 'disclosed' | 'consent-required' | 'off';\n\n/** The Enhanced Conversions mode the project's analytics binding declares, read from its source (`off` when none). */\nexport function readBrandlabEnhancedConversionsMode(root: string = process.cwd()): BrandlabEnhancedConversionsCheckMode {\n let source = '';\n try {\n source = readFileSync(join(root, 'src/config/analytics.ts'), 'utf8');\n } catch {\n return 'off';\n }\n const mode = /enhancedConversions\\s*:\\s*\\{[^}]*\\bmode\\s*:\\s*['\"]([a-z-]+)['\"]/.exec(source)?.[1];\n return mode === 'disclosed' || mode === 'consent-required' ? mode : 'off';\n}\n\n/** Event names that 3.0 no longer sends: legacy lead and phone events, GA4 form aliases and web vitals. */\nexport const BRANDLAB_RETIRED_ANALYTICS_EVENTS = Object.freeze([\n 'phone_call', 'phone_calls', 'contact_form_submission', 'form_start', 'form_submit', 'generate_lead', 'cta_click', 'web_vital',\n]);\n\n/**\n * The events the shared runtime sends; mirrors site-core's CANONICAL_LEAD_EVENTS,\n * CANONICAL_PHONE_INTERACTION_EVENTS and BRANDLAB_LEAD_DIAGNOSTIC_EVENTS. The\n * Google Ads `conversion` event is also allowed, only with an AW- destination.\n */\nexport const BRANDLAB_ANALYTICS_EVENTS = Object.freeze([\n 'contact_form', 'phone_link_click', 'lead_form_start', 'lead_form_error', 'lead_cta_click',\n]);\n\nconst PII_PARAMETER = /^(?:e_?mail|email_address|user_email|contact_email|phone|phone_number|mobile|mobile_number|user_phone|name|first_name|last_name|full_name|customer_name|contact_name|message|enquiry_message|form_message|address|street_address|residential_address|attachment|attachment_name|file_name|filename)$/i;\nconst PHONE_DEDUPE_WINDOW_MS = 1_200;\n\nexport type AnalyticsCommand = unknown[];\n\n/** Record every gtag/dataLayer command the page pushes, surviving navigations. */\nexport async function recordAnalyticsCommands(page: Page): Promise<AnalyticsCommand[]> {\n const commands: AnalyticsCommand[] = [];\n await page.exposeFunction('__brandlabRecordAnalytics', (command: AnalyticsCommand) => { commands.push(command); });\n await page.addInitScript(() => {\n const record = (entry: unknown) => {\n try {\n const command = entry && typeof entry === 'object' && 'length' in (entry as object)\n ? Array.from(entry as ArrayLike<unknown>)\n : [entry];\n void (window as unknown as { __brandlabRecordAnalytics: (value: unknown) => Promise<void> })\n .__brandlabRecordAnalytics(JSON.parse(JSON.stringify(command)));\n } catch { /* an unserialisable command is not analytics evidence */ }\n };\n const wrapped = new WeakSet<unknown[]>();\n const wrap = (layer: unknown[]) => {\n if (wrapped.has(layer)) return layer;\n wrapped.add(layer);\n const push = layer.push.bind(layer);\n layer.push = (...entries: unknown[]) => { entries.forEach(record); return push(...entries); };\n layer.forEach(record);\n return layer;\n };\n let dataLayer = wrap([]);\n Object.defineProperty(window, 'dataLayer', {\n configurable: true,\n get: () => dataLayer,\n // `window.dataLayer = window.dataLayer || []` reassigns the same array.\n set: (value: unknown[]) => { if (Array.isArray(value)) dataLayer = wrap(value); },\n });\n });\n return commands;\n}\n\nexport function analyticsEvents(commands: readonly AnalyticsCommand[], name?: string) {\n return commands\n .filter((command) => command[0] === 'event' && typeof command[1] === 'string' && (!name || command[1] === name))\n .map((command) => ({ name: command[1] as string, params: (command[2] ?? {}) as Record<string, unknown> }));\n}\n\nconst EMAIL_VALUE = /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/;\nconst digitsOf = (value: string) => value.replace(/\\D/g, '');\n\n/** Whether a user_data field is the visitor's entered email (`email`) or phone (`phone_number`, normalised to E.164). */\nfunction isEnteredContactField(key: string, value: unknown, entered: readonly string[]): boolean {\n if (typeof value !== 'string') return false;\n if (key === 'email') {\n return entered.some((item) => EMAIL_VALUE.test(item) && item.toLowerCase() === value.trim().toLowerCase());\n }\n if (key === 'phone_number') {\n const field = digitsOf(value);\n return field.length >= 8 && entered.some((item) => {\n const digits = digitsOf(item);\n return /^[\\d\\s()+-]+$/.test(item) && digits.length >= 8\n && (field === digits || field.endsWith(digits.replace(/^0/, '')));\n });\n }\n return false;\n}\n\nfunction withoutEnteredContact(data: unknown, entered: readonly string[]): unknown {\n if (!data || typeof data !== 'object' || Array.isArray(data)) return data;\n return Object.fromEntries(Object.entries(data).filter(([key, value]) => !isEnteredContactField(key, value, entered)));\n}\n\n/**\n * Disclosed Enhanced Conversions send the entered email and phone to Google Ads by design, through `gtag('set',\n * 'user_data', …)` (the shared runtime) or a conversion's `user_data` parameter. Only those two fields, in only those\n * two places, are taken out before the entered-value check; everything else is still checked.\n */\nfunction withoutDisclosedUserData(command: AnalyticsCommand, entered: readonly string[]): AnalyticsCommand {\n if (command[0] === 'set' && command[1] === 'user_data') return ['set', 'user_data', withoutEnteredContact(command[2], entered)];\n if (command[0] === 'event' && command[1] === 'conversion' && command[2] && typeof command[2] === 'object') {\n const params = command[2] as Record<string, unknown>;\n if ('user_data' in params) return ['event', 'conversion', { ...params, user_data: withoutEnteredContact(params.user_data, entered) }];\n }\n return command;\n}\n\n/** Problems a set of recorded commands shows: retired or unexpected events, or lead details. */\nexport function analyticsEventProblems(\n commands: readonly AnalyticsCommand[],\n options: {\n enteredValues?: readonly string[];\n additionalEvents?: readonly string[];\n enhancedConversions?: BrandlabEnhancedConversionsCheckMode;\n } = {},\n): string[] {\n const problems: string[] = [];\n const allowed = new Set([...BRANDLAB_ANALYTICS_EVENTS, ...(options.additionalEvents ?? [])]);\n for (const event of analyticsEvents(commands)) {\n if (BRANDLAB_RETIRED_ANALYTICS_EVENTS.includes(event.name)) problems.push(`retired event ${event.name}`);\n else if (event.name === 'conversion') {\n if (!String(event.params.send_to ?? '').startsWith('AW-')) problems.push('conversion event without a Google Ads destination');\n } else if (!allowed.has(event.name)) problems.push(`unexpected event ${event.name}`);\n for (const key of Object.keys(event.params)) {\n if (PII_PARAMETER.test(key)) problems.push(`${event.name} carries the ${key} parameter`);\n }\n }\n const entered = (options.enteredValues ?? []).map((entry) => entry.trim()).filter((entry) => entry.length >= 5);\n const checked = options.enhancedConversions === 'disclosed'\n ? commands.map((command) => withoutDisclosedUserData(command, entered))\n : commands;\n const serialised = JSON.stringify(checked).toLowerCase();\n for (const value of entered) {\n if (serialised.includes(value.toLowerCase())) problems.push(`an analytics command contains the entered value \"${value}\"`);\n }\n return problems;\n}\n\nexport function defineBrandlabAnalyticsTests(options: BrandlabAnalyticsTestOptions = {}) {\n const paths = options.paths?.length ? options.paths : ['/'];\n const additionalEvents = options.additionalEvents ?? [];\n const enhancedConversions = options.enhancedConversions ?? readBrandlabEnhancedConversionsMode();\n\n test.describe('Brandlab analytics', () => {\n // Service workers could answer requests outside page and context routing.\n test.use({ serviceWorkers: 'block' });\n let commands: AnalyticsCommand[] = [];\n test.beforeEach(async ({ page, baseURL }) => {\n await isolateFromNetwork(page, { baseURL, googleTag: 'stub' });\n commands = await recordAnalyticsCommands(page);\n });\n\n test('renders exactly one Google tag on every page', async ({ page }) => {\n for (const path of paths) {\n commands.length = 0;\n // DOMContentLoaded, not load: next start's image optimiser can stall concurrent first image requests\n // (vercel/next.js#98168), and nothing here needs the images. The expectations below wait for the tag.\n await page.goto(path, { waitUntil: 'domcontentloaded' });\n const loaders = page.locator('script[src*=\"googletagmanager.com/gtag/js\"]');\n await expect(loaders.first(), `${path}: no Google tag rendered (build the e2e target with a test NEXT_PUBLIC_GA_MEASUREMENT_ID)`)\n .toBeAttached({ timeout: 15_000 });\n expect(await loaders.count(), `${path}: renders exactly one Google tag loader`).toBe(1);\n await expect.poll(() => commands.filter((command) => command[0] === 'js').length, `${path}: gtag('js') runs once`).toBe(1);\n const destinations = commands.filter((command) => command[0] === 'config').map((command) => String(command[1]));\n expect(new Set(destinations).size, `${path}: a destination is configured twice`).toBe(destinations.length);\n }\n });\n\n test('sends one phone_link_click per tap', async ({ page }) => {\n await page.goto(paths[0]!, { waitUntil: 'domcontentloaded' });\n const link = options.phoneLink ? page.locator(options.phoneLink).first() : page.locator('a[href^=\"tel:\"]:visible').first();\n await expect(link, 'the page renders a telephone link').toBeVisible();\n await expect.poll(() => commands.some((command) => command[0] === 'js')).toBe(true);\n // Keep the page in place; the shared listener still sees the tap.\n await page.evaluate(() => window.addEventListener('click', (event) => {\n if ((event.target as Element | null)?.closest?.('a[href^=\"tel:\"]')) event.preventDefault();\n }, true));\n await link.click();\n await expect.poll(() => analyticsEvents(commands, 'phone_link_click').length).toBe(1);\n await page.waitForTimeout(PHONE_DEDUPE_WINDOW_MS + 300);\n expect(analyticsEvents(commands, 'phone_link_click'), 'one tap sends one event').toHaveLength(1);\n await link.click();\n await expect.poll(() => analyticsEvents(commands, 'phone_link_click').length).toBe(2);\n for (const event of analyticsEvents(commands, 'phone_link_click')) {\n expect(event.params.placement, 'phone_link_click carries its placement').toBeTruthy();\n }\n expect(analyticsEventProblems(commands, { additionalEvents })).toEqual([]);\n });\n\n if (options.contact) {\n const journey = options.contact;\n test('sends one contact_form only after an accepted enquiry, without lead details', async ({ page }) => {\n let attempts = 0;\n await page.route(contactEndpointMatcher(journey.endpoint), async (route) => {\n attempts += 1;\n await route.fulfill(attempts === 1\n ? { status: 503, json: RETRYABLE_CONTACT_FAILURE }\n : { status: 200, json: ACCEPTED_CONTACT_RESULT });\n });\n await fillBrandlabContactForm(page, journey);\n const submit = page.locator('form').filter({ has: page.locator(`[name=\"${Object.keys(journey.fields)[0]}\"]`) })\n .first().getByRole('button', { name: journey.submit, exact: typeof journey.submit === 'string' });\n await submit.click();\n await expect.poll(() => attempts).toBe(1);\n await page.waitForTimeout(500);\n expect(analyticsEvents(commands, 'contact_form'), 'a failed delivery is not a conversion').toHaveLength(0);\n const retry = journey.retry ?? journey.submit;\n await page.locator('form').filter({ has: page.locator(`[name=\"${Object.keys(journey.fields)[0]}\"]`) })\n .first().getByRole('button', { name: retry, exact: typeof retry === 'string' }).click();\n await expect(page).toHaveURL(successPattern(journey.successPath));\n await expect.poll(() => analyticsEvents(commands, 'contact_form').length).toBe(1);\n await page.waitForTimeout(500);\n expect(analyticsEvents(commands, 'contact_form')).toHaveLength(1);\n expect(analyticsEventProblems(commands, { enteredValues: Object.values(journey.fields), additionalEvents, enhancedConversions })).toEqual([]);\n });\n }\n\n test('sends no retired or web_vital events', async ({ page }) => {\n for (const path of paths) {\n const tagRuns = commands.filter((command) => command[0] === 'js').length;\n await page.goto(path, { waitUntil: 'domcontentloaded' });\n // The page's instrumentation has run once its Google tag has: any reporter would be listening by then.\n await expect.poll(() => commands.filter((command) => command[0] === 'js').length, `${path}: the Google tag runs`).toBeGreaterThan(tagRuns);\n await page.mouse.wheel(0, 1_200);\n await page.waitForTimeout(300);\n // Web-vitals reporters flush when the page is hidden.\n await page.evaluate(() => {\n Object.defineProperty(document, 'visibilityState', { configurable: true, get: () => 'hidden' });\n document.dispatchEvent(new Event('visibilitychange'));\n window.dispatchEvent(new Event('pagehide'));\n });\n await page.waitForTimeout(300);\n }\n expect(analyticsEventProblems(commands, { additionalEvents })).toEqual([]);\n });\n });\n}\n"]}
|
package/dist/cli.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AASA,OAAO,EAGL,KAAK,uBAAuB,EAE7B,MAAM,cAAc,CAAC;AAEtB,UAAU,UAAU;IAClB,IAAI,EAAE,MAAM,CAAC;IACb,gBAAgB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACjC,IAAI,EAAE,uBAAuB,CAAC;IAC9B,IAAI,EAAE,OAAO,CAAC;IACd,IAAI,EAAE,OAAO,CAAC;IACd,WAAW,EAAE,OAAO,CAAC;IACrB,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B,eAAe,CAAC,EAAE,MAAM,CAAC;CAC1B;AAED,UAAU,KAAK;IACb,MAAM,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IAChC,MAAM,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;CACjC;AA0BD,wBAAgB,4BAA4B,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,GAAG,UAAU,CA+BhF;AAED,wBAAgB,yBAAyB,CACvC,IAAI,EAAE,SAAS,MAAM,EAAE,EACvB,EAAE,GAAE,KAGH,GACA,MAAM,CAwBR;
|
|
1
|
+
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AASA,OAAO,EAGL,KAAK,uBAAuB,EAE7B,MAAM,cAAc,CAAC;AAEtB,UAAU,UAAU;IAClB,IAAI,EAAE,MAAM,CAAC;IACb,gBAAgB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACjC,IAAI,EAAE,uBAAuB,CAAC;IAC9B,IAAI,EAAE,OAAO,CAAC;IACd,IAAI,EAAE,OAAO,CAAC;IACd,WAAW,EAAE,OAAO,CAAC;IACrB,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B,eAAe,CAAC,EAAE,MAAM,CAAC;CAC1B;AAED,UAAU,KAAK;IACb,MAAM,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IAChC,MAAM,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;CACjC;AA0BD,wBAAgB,4BAA4B,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,GAAG,UAAU,CA+BhF;AAED,wBAAgB,yBAAyB,CACvC,IAAI,EAAE,SAAS,MAAM,EAAE,EACvB,EAAE,GAAE,KAGH,GACA,MAAM,CAwBR;AAoFD,wBAAgB,kCAAkC,CAChD,SAAS,EAAE,MAAM,EACjB,QAAQ,EAAE,MAAM,GAAG,SAAS,GAC3B,OAAO,CAOT"}
|
package/dist/cli.js
CHANGED
|
@@ -133,7 +133,10 @@ function formatResult(result) {
|
|
|
133
133
|
: "pass";
|
|
134
134
|
const lines = [
|
|
135
135
|
"Brandlab verification",
|
|
136
|
-
`installed: ${result.installedRelease ??
|
|
136
|
+
`installed: ${result.installedRelease ??
|
|
137
|
+
(result.canaryRelease
|
|
138
|
+
? `canary ${result.canaryRelease.version} of ${result.canaryRelease.line} (not a published release)`
|
|
139
|
+
: "unadopted")}`,
|
|
137
140
|
...(result.packedPreflight ? ['provenance: verified local packed preflight; registry publication and deployment not established'] : []),
|
|
138
141
|
`policy: ${result.verification.policyVersion}`,
|
|
139
142
|
`static checks: ${staticStatus}`,
|
package/dist/cli.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"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","sourcesContent":["#!/usr/bin/env node\nimport { readFileSync, realpathSync } from \"node:fs\";\nimport { resolve } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport { checkEngineeringStructure, type EngineeringCheck } from \"./engineering.js\";\nimport {\n type BrandlabReleaseManifest,\n validateBrandlabReleaseManifest,\n} from \"./release-manifest.js\";\nimport {\n evaluateBrandlabProject,\n shouldFailBrandlabConformance,\n type BrandlabConformanceMode,\n type BrandlabConformanceResult,\n} from \"./project.js\";\n\ninterface CliOptions {\n root: string;\n installedRelease?: string | null;\n mode: BrandlabConformanceMode;\n json: boolean;\n help: boolean;\n engineering: boolean;\n packedReleaseReceipt?: string;\n releaseManifest?: string;\n}\n\ninterface CliIo {\n stdout: (value: string) => void;\n stderr: (value: string) => void;\n}\n\nconst HELP = `Brandlab verification\n\nRuns the current objective verification checks for one project.\n\nUsage:\n brandlab-verify [--root <path>]\n [--installed-release <version|unadopted>]\n [--mode <local|ci|preflight>] [--engineering] [--json]\n [--release-manifest <standards/releases/<version>/manifest.json>]\n [--packed-release <receipt.json> (preflight only)]\n\nExit status:\n local/ci fail only for objective current-release blockers\n preflight uses the same checks against an isolated candidate\n\nStatic verification reports tests, builds, browsers and deployments as not run\nuntil real evidence is supplied. A static check passes only when one of its\nrules ran; otherwise it is reported as not run.\n--release-manifest binds every Brandlab package's lockfile integrity to the\nimmutable release record.\n--engineering adds the complete-migration structural checks. It does not certify\nmanual review, unused-code analysis, runtime journeys or provider cutover.\n`;\n\nexport function parseBrandlabConformanceArgs(argv: readonly string[]): CliOptions {\n const parsed: CliOptions = {\n root: process.cwd(),\n mode: \"local\",\n json: false,\n help: false,\n engineering: false,\n };\n for (let index = 0; index < argv.length; index += 1) {\n const argument = argv[index];\n if (argument === \"--help\" || argument === \"-h\") parsed.help = true;\n else if (argument === \"--json\") parsed.json = true;\n else if (argument === \"--engineering\") parsed.engineering = true;\n else if (argument === \"--packed-release\") parsed.packedReleaseReceipt = resolve(requireValue(argv, ++index, \"--packed-release\"));\n else if (argument === \"--release-manifest\") parsed.releaseManifest = resolve(requireValue(argv, ++index, \"--release-manifest\"));\n else if (argument === \"--root\") {\n parsed.root = resolve(requireValue(argv, ++index, \"--root\"));\n } else if (argument === \"--installed-release\") {\n const value = requireValue(argv, ++index, \"--installed-release\");\n parsed.installedRelease = value === \"unadopted\" ? null : value;\n } else if (argument === \"--mode\") {\n const value = requireValue(argv, ++index, \"--mode\");\n if (![\"local\", \"ci\", \"preflight\"].includes(value)) {\n throw new Error(`Invalid --mode ${value}.`);\n }\n parsed.mode = value as BrandlabConformanceMode;\n } else {\n throw new Error(`Unknown argument ${argument}.`);\n }\n }\n return parsed;\n}\n\nexport function runBrandlabConformanceCli(\n argv: readonly string[],\n io: CliIo = {\n stdout: (value) => process.stdout.write(value),\n stderr: (value) => process.stderr.write(value),\n },\n): number {\n try {\n const options = parseBrandlabConformanceArgs(argv);\n if (options.help) {\n io.stdout(HELP);\n return 0;\n }\n const result = evaluateBrandlabProject({\n root: options.root,\n installedRelease: options.installedRelease,\n mode: options.mode,\n packedReleaseReceipt: options.packedReleaseReceipt,\n ...(options.releaseManifest\n ? { releaseManifest: readReleaseManifest(options.releaseManifest) }\n : {}),\n });\n const engineering = options.engineering ? checkEngineeringStructure(options.root) : undefined;\n io.stdout(options.json ? `${JSON.stringify({ ...result, ...(engineering ? { engineering } : {}) }, null, 2)}\\n`\n : formatResult(result) + (engineering ? formatEngineering(engineering) : ''));\n return shouldFailBrandlabConformance(result) || engineering?.status === 'blocked' ? 1 : 0;\n } catch (error) {\n io.stderr(`${error instanceof Error ? error.message : String(error)}\\n\\n${HELP}`);\n return 2;\n }\n}\n\nfunction formatEngineering(engineering: EngineeringCheck): string {\n const findings = engineering.findings.map((finding) => {\n const location = `${finding.path}${finding.line ? `:${finding.line}` : \"\"}`;\n const label = finding.severity === \"info\" ? `${finding.rule}/info` : finding.rule;\n return `[${label}] ${location}: ${finding.message}\\n`;\n });\n return `Engineering structure: ${engineering.status}\\n${engineering.coverage}\\n${findings.join(\"\")}`;\n}\n\nfunction readReleaseManifest(path: string): BrandlabReleaseManifest {\n let text: string;\n try {\n text = readFileSync(path, \"utf8\");\n } catch {\n throw new Error(`The release manifest ${path} could not be read.`);\n }\n let parsed: unknown;\n try {\n parsed = JSON.parse(text);\n } catch {\n // Parser messages quote file content, so only the path is reported.\n throw new Error(`The release manifest ${path} is invalid JSON.`);\n }\n validateBrandlabReleaseManifest(parsed);\n return parsed;\n}\n\nfunction formatResult(result: BrandlabConformanceResult): string {\n const externalChecks = result.verification.checks.filter(\n (check) => check.status === \"not_run\",\n );\n const staticStatus =\n result.verification.summary.blockers > 0\n ? \"blocked\"\n : result.verification.summary.warnings > 0\n ? \"warning\"\n : \"pass\";\n const lines = [\n \"Brandlab verification\",\n `installed: ${result.installedRelease ?? \"unadopted\"}`,\n ...(result.packedPreflight ? ['provenance: verified local packed preflight; registry publication and deployment not established'] : []),\n `policy: ${result.verification.policyVersion}`,\n `static checks: ${staticStatus}`,\n `external evidence: ${\n externalChecks.length > 0\n ? `not run (${externalChecks.map((check) => check.title).join(\", \")})`\n : \"complete\"\n }`,\n `findings: ${formatSummary(result.verification.summary)}`,\n ];\n for (const family of result.verification.families) {\n if (family.status === \"pass\") continue;\n lines.push(\n `[${family.status}] ${family.title}: ${family.blockers} blocker(s), ${family.warnings} warning(s)`,\n );\n }\n for (const finding of result.operationalFindings) {\n const location = finding.path\n ? ` (${finding.path}${finding.line ? `:${finding.line}` : \"\"})`\n : \"\";\n lines.push(\n `[${finding.familyId}/${finding.severity}] ${finding.ruleId}${location}: ${finding.actual}`,\n );\n }\n return `${lines.join(\"\\n\")}\\n`;\n}\n\nfunction formatSummary(summary: BrandlabConformanceResult[\"summary\"]): string {\n return `${summary.status}; ${summary.blockers} blocker(s), ${summary.warnings} warning(s), ${summary.info} info`;\n}\n\nfunction requireValue(argv: readonly string[], index: number, argument: string): string {\n const value = argv[index];\n if (!value || value.startsWith(\"--\")) throw new Error(`${argument} requires a value.`);\n return value;\n}\n\nexport function isBrandlabConformanceCliEntrypoint(\n moduleUrl: string,\n argvPath: string | undefined,\n): 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 (isBrandlabConformanceCliEntrypoint(import.meta.url, process.argv[1])) {\n process.exitCode = runBrandlabConformanceCli(process.argv.slice(2));\n}\n"]}
|
|
1
|
+
{"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"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","sourcesContent":["#!/usr/bin/env node\nimport { readFileSync, realpathSync } from \"node:fs\";\nimport { resolve } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport { checkEngineeringStructure, type EngineeringCheck } from \"./engineering.js\";\nimport {\n type BrandlabReleaseManifest,\n validateBrandlabReleaseManifest,\n} from \"./release-manifest.js\";\nimport {\n evaluateBrandlabProject,\n shouldFailBrandlabConformance,\n type BrandlabConformanceMode,\n type BrandlabConformanceResult,\n} from \"./project.js\";\n\ninterface CliOptions {\n root: string;\n installedRelease?: string | null;\n mode: BrandlabConformanceMode;\n json: boolean;\n help: boolean;\n engineering: boolean;\n packedReleaseReceipt?: string;\n releaseManifest?: string;\n}\n\ninterface CliIo {\n stdout: (value: string) => void;\n stderr: (value: string) => void;\n}\n\nconst HELP = `Brandlab verification\n\nRuns the current objective verification checks for one project.\n\nUsage:\n brandlab-verify [--root <path>]\n [--installed-release <version|unadopted>]\n [--mode <local|ci|preflight>] [--engineering] [--json]\n [--release-manifest <standards/releases/<version>/manifest.json>]\n [--packed-release <receipt.json> (preflight only)]\n\nExit status:\n local/ci fail only for objective current-release blockers\n preflight uses the same checks against an isolated candidate\n\nStatic verification reports tests, builds, browsers and deployments as not run\nuntil real evidence is supplied. A static check passes only when one of its\nrules ran; otherwise it is reported as not run.\n--release-manifest binds every Brandlab package's lockfile integrity to the\nimmutable release record.\n--engineering adds the complete-migration structural checks. It does not certify\nmanual review, unused-code analysis, runtime journeys or provider cutover.\n`;\n\nexport function parseBrandlabConformanceArgs(argv: readonly string[]): CliOptions {\n const parsed: CliOptions = {\n root: process.cwd(),\n mode: \"local\",\n json: false,\n help: false,\n engineering: false,\n };\n for (let index = 0; index < argv.length; index += 1) {\n const argument = argv[index];\n if (argument === \"--help\" || argument === \"-h\") parsed.help = true;\n else if (argument === \"--json\") parsed.json = true;\n else if (argument === \"--engineering\") parsed.engineering = true;\n else if (argument === \"--packed-release\") parsed.packedReleaseReceipt = resolve(requireValue(argv, ++index, \"--packed-release\"));\n else if (argument === \"--release-manifest\") parsed.releaseManifest = resolve(requireValue(argv, ++index, \"--release-manifest\"));\n else if (argument === \"--root\") {\n parsed.root = resolve(requireValue(argv, ++index, \"--root\"));\n } else if (argument === \"--installed-release\") {\n const value = requireValue(argv, ++index, \"--installed-release\");\n parsed.installedRelease = value === \"unadopted\" ? null : value;\n } else if (argument === \"--mode\") {\n const value = requireValue(argv, ++index, \"--mode\");\n if (![\"local\", \"ci\", \"preflight\"].includes(value)) {\n throw new Error(`Invalid --mode ${value}.`);\n }\n parsed.mode = value as BrandlabConformanceMode;\n } else {\n throw new Error(`Unknown argument ${argument}.`);\n }\n }\n return parsed;\n}\n\nexport function runBrandlabConformanceCli(\n argv: readonly string[],\n io: CliIo = {\n stdout: (value) => process.stdout.write(value),\n stderr: (value) => process.stderr.write(value),\n },\n): number {\n try {\n const options = parseBrandlabConformanceArgs(argv);\n if (options.help) {\n io.stdout(HELP);\n return 0;\n }\n const result = evaluateBrandlabProject({\n root: options.root,\n installedRelease: options.installedRelease,\n mode: options.mode,\n packedReleaseReceipt: options.packedReleaseReceipt,\n ...(options.releaseManifest\n ? { releaseManifest: readReleaseManifest(options.releaseManifest) }\n : {}),\n });\n const engineering = options.engineering ? checkEngineeringStructure(options.root) : undefined;\n io.stdout(options.json ? `${JSON.stringify({ ...result, ...(engineering ? { engineering } : {}) }, null, 2)}\\n`\n : formatResult(result) + (engineering ? formatEngineering(engineering) : ''));\n return shouldFailBrandlabConformance(result) || engineering?.status === 'blocked' ? 1 : 0;\n } catch (error) {\n io.stderr(`${error instanceof Error ? error.message : String(error)}\\n\\n${HELP}`);\n return 2;\n }\n}\n\nfunction formatEngineering(engineering: EngineeringCheck): string {\n const findings = engineering.findings.map((finding) => {\n const location = `${finding.path}${finding.line ? `:${finding.line}` : \"\"}`;\n const label = finding.severity === \"info\" ? `${finding.rule}/info` : finding.rule;\n return `[${label}] ${location}: ${finding.message}\\n`;\n });\n return `Engineering structure: ${engineering.status}\\n${engineering.coverage}\\n${findings.join(\"\")}`;\n}\n\nfunction readReleaseManifest(path: string): BrandlabReleaseManifest {\n let text: string;\n try {\n text = readFileSync(path, \"utf8\");\n } catch {\n throw new Error(`The release manifest ${path} could not be read.`);\n }\n let parsed: unknown;\n try {\n parsed = JSON.parse(text);\n } catch {\n // Parser messages quote file content, so only the path is reported.\n throw new Error(`The release manifest ${path} is invalid JSON.`);\n }\n validateBrandlabReleaseManifest(parsed);\n return parsed;\n}\n\nfunction formatResult(result: BrandlabConformanceResult): string {\n const externalChecks = result.verification.checks.filter(\n (check) => check.status === \"not_run\",\n );\n const staticStatus =\n result.verification.summary.blockers > 0\n ? \"blocked\"\n : result.verification.summary.warnings > 0\n ? \"warning\"\n : \"pass\";\n const lines = [\n \"Brandlab verification\",\n `installed: ${\n result.installedRelease ??\n (result.canaryRelease\n ? `canary ${result.canaryRelease.version} of ${result.canaryRelease.line} (not a published release)`\n : \"unadopted\")\n }`,\n ...(result.packedPreflight ? ['provenance: verified local packed preflight; registry publication and deployment not established'] : []),\n `policy: ${result.verification.policyVersion}`,\n `static checks: ${staticStatus}`,\n `external evidence: ${\n externalChecks.length > 0\n ? `not run (${externalChecks.map((check) => check.title).join(\", \")})`\n : \"complete\"\n }`,\n `findings: ${formatSummary(result.verification.summary)}`,\n ];\n for (const family of result.verification.families) {\n if (family.status === \"pass\") continue;\n lines.push(\n `[${family.status}] ${family.title}: ${family.blockers} blocker(s), ${family.warnings} warning(s)`,\n );\n }\n for (const finding of result.operationalFindings) {\n const location = finding.path\n ? ` (${finding.path}${finding.line ? `:${finding.line}` : \"\"})`\n : \"\";\n lines.push(\n `[${finding.familyId}/${finding.severity}] ${finding.ruleId}${location}: ${finding.actual}`,\n );\n }\n return `${lines.join(\"\\n\")}\\n`;\n}\n\nfunction formatSummary(summary: BrandlabConformanceResult[\"summary\"]): string {\n return `${summary.status}; ${summary.blockers} blocker(s), ${summary.warnings} warning(s), ${summary.info} info`;\n}\n\nfunction requireValue(argv: readonly string[], index: number, argument: string): string {\n const value = argv[index];\n if (!value || value.startsWith(\"--\")) throw new Error(`${argument} requires a value.`);\n return value;\n}\n\nexport function isBrandlabConformanceCliEntrypoint(\n moduleUrl: string,\n argvPath: string | undefined,\n): 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 (isBrandlabConformanceCliEntrypoint(import.meta.url, process.argv[1])) {\n process.exitCode = runBrandlabConformanceCli(process.argv.slice(2));\n}\n"]}
|
package/dist/current-release.js
CHANGED
|
@@ -23,19 +23,19 @@ 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.15.0"
|
|
27
27
|
},
|
|
28
28
|
"@brandlab-digital/eslint-plugin": {
|
|
29
|
-
"version": "3.
|
|
29
|
+
"version": "3.15.0"
|
|
30
30
|
},
|
|
31
31
|
"@brandlab-digital/site-core": {
|
|
32
|
-
"version": "3.
|
|
32
|
+
"version": "3.15.0"
|
|
33
33
|
},
|
|
34
34
|
"@brandlab-digital/site-next": {
|
|
35
|
-
"version": "3.
|
|
35
|
+
"version": "3.15.0"
|
|
36
36
|
}
|
|
37
37
|
},
|
|
38
|
-
"releaseVersion": "3.
|
|
38
|
+
"releaseVersion": "3.15.0",
|
|
39
39
|
"runtime": {
|
|
40
40
|
"bundler": "turbopack",
|
|
41
41
|
"cacheComponents": false,
|
|
@@ -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.15.0\"\n },\n \"@brandlab-digital/eslint-plugin\": {\n \"version\": \"3.15.0\"\n },\n \"@brandlab-digital/site-core\": {\n \"version\": \"3.15.0\"\n },\n \"@brandlab-digital/site-next\": {\n \"version\": \"3.15.0\"\n }\n },\n \"releaseVersion\": \"3.15.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"]}
|
|
@@ -1 +1 @@
|
|
|
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;
|
|
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;AA4BD;;;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
|
@@ -20,7 +20,14 @@ export const BRANDLAB_EXPECTED_SCRIPTS = Object.freeze({
|
|
|
20
20
|
'check:unused:production': 'knip --production',
|
|
21
21
|
'check:standards': 'brandlab-verify --mode ci --engineering',
|
|
22
22
|
});
|
|
23
|
-
const SRC_DIRECTORIES = new Set(['app', 'config', 'data', 'content', 'lib', 'components', 'types']);
|
|
23
|
+
const SRC_DIRECTORIES = new Set(['app', 'config', 'data', 'content', 'lib', 'components', 'types', 'site']);
|
|
24
|
+
/**
|
|
25
|
+
* The Site System v2 skin folder (3.15.0; ui-kit README "How a site uses it"): the site's tokens, skin, thin wrappers
|
|
26
|
+
* and bound quote form keep their documented names, and its own sections live in `signature/` and `decoration/`.
|
|
27
|
+
* Any other file in it follows the usual naming rules.
|
|
28
|
+
*/
|
|
29
|
+
const SITE_SKIN_FILES = new Set(['tokens.css', 'skin.ts', 'skin.css', 'ui.tsx', 'quote-form.tsx', 'copy.ts', 'icons.ts', 'shell.ts']);
|
|
30
|
+
const SITE_SKIN_DIRECTORIES = new Set(['signature', 'decoration']);
|
|
24
31
|
const SRC_ROOT_FILES = /^(?:instrumentation(?:-client)?|proxy|mdx-components)\.(?:ts|tsx)$/;
|
|
25
32
|
const COMPONENT_ROLES = new Set(['layout', 'ui', 'sections', 'features', 'page-families']);
|
|
26
33
|
const TEST_DIRECTORIES = new Set(['config', 'data', 'lib', 'components', 'e2e', 'support']);
|
|
@@ -248,7 +255,14 @@ function inspectLayout(root, capabilities, add) {
|
|
|
248
255
|
}
|
|
249
256
|
if (entry.isFile() && SRC_ROOT_FILES.test(entry.name))
|
|
250
257
|
continue;
|
|
251
|
-
add(path, 'standard-layout', `${path} is outside the standard layout (src/{app,config,data,content,lib,components}).`);
|
|
258
|
+
add(path, 'standard-layout', `${path} is outside the standard layout (src/{app,config,data,content,lib,components,site}).`);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
if (isDirectory(join(root, 'src/site'))) {
|
|
262
|
+
for (const entry of readdirSync(join(root, 'src/site'), { withFileTypes: true })) {
|
|
263
|
+
if (entry.isDirectory() && !entry.name.startsWith('.') && !SITE_SKIN_DIRECTORIES.has(entry.name)) {
|
|
264
|
+
add(`src/site/${entry.name}`, 'standard-layout', 'src/site holds the skin files and the signature/ and decoration/ folders only.');
|
|
265
|
+
}
|
|
252
266
|
}
|
|
253
267
|
}
|
|
254
268
|
if (isDirectory(join(root, 'src/components'))) {
|
|
@@ -268,6 +282,8 @@ function inspectFileName(root, path, add) {
|
|
|
268
282
|
const directory = posix.dirname(path);
|
|
269
283
|
if (directory === 'src' && SRC_ROOT_FILES.test(name))
|
|
270
284
|
return;
|
|
285
|
+
if (directory === 'src/site' && SITE_SKIN_FILES.has(name))
|
|
286
|
+
return;
|
|
271
287
|
if (name.endsWith('.module.css')) {
|
|
272
288
|
const component = name.replace(/\.module\.css$/, '');
|
|
273
289
|
// Exact names from the directory listing: existsSync is case-insensitive on macOS but not on the Linux CI
|