@brandlab-digital/conformance 2.1.0 → 3.0.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/dist/analytics-e2e.d.ts +52 -0
- package/dist/analytics-e2e.d.ts.map +1 -0
- package/dist/analytics-e2e.js +189 -0
- package/dist/analytics-e2e.js.map +1 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +40 -3
- package/dist/cli.js.map +1 -1
- package/dist/contact-e2e.d.ts +43 -0
- package/dist/contact-e2e.d.ts.map +1 -0
- package/dist/contact-e2e.js +193 -0
- package/dist/contact-e2e.js.map +1 -0
- package/dist/current-release.js +7 -7
- package/dist/current-release.js.map +1 -1
- package/dist/e2e-support.d.ts +42 -0
- package/dist/e2e-support.d.ts.map +1 -0
- package/dist/e2e-support.js +126 -0
- package/dist/e2e-support.js.map +1 -0
- package/dist/engineering.d.ts +14 -0
- package/dist/engineering.d.ts.map +1 -1
- package/dist/engineering.js +446 -50
- package/dist/engineering.js.map +1 -1
- package/dist/estate.d.ts +105 -0
- package/dist/estate.d.ts.map +1 -0
- package/dist/estate.js +391 -0
- package/dist/estate.js.map +1 -0
- package/dist/families-e2e.d.ts +45 -0
- package/dist/families-e2e.d.ts.map +1 -0
- package/dist/families-e2e.js +248 -0
- package/dist/families-e2e.js.map +1 -0
- package/dist/glob.d.ts +21 -0
- package/dist/glob.d.ts.map +1 -0
- package/dist/glob.js +138 -0
- package/dist/glob.js.map +1 -0
- package/dist/knip.d.ts +47 -0
- package/dist/knip.d.ts.map +1 -0
- package/dist/knip.js +84 -0
- package/dist/knip.js.map +1 -0
- package/dist/project.d.ts +4 -1
- package/dist/project.d.ts.map +1 -1
- package/dist/project.js +163 -147
- package/dist/project.js.map +1 -1
- package/dist/public-copy.d.ts +36 -0
- package/dist/public-copy.d.ts.map +1 -0
- package/dist/public-copy.js +517 -0
- package/dist/public-copy.js.map +1 -0
- package/dist/rule-registry.d.ts +292 -0
- package/dist/rule-registry.d.ts.map +1 -0
- package/dist/rule-registry.js +68 -0
- package/dist/rule-registry.js.map +1 -0
- package/dist/search-certification.d.ts +4 -0
- package/dist/search-certification.d.ts.map +1 -1
- package/dist/search-certification.js +17 -3
- package/dist/search-certification.js.map +1 -1
- package/dist/source-files.d.ts +54 -0
- package/dist/source-files.d.ts.map +1 -0
- package/dist/source-files.js +323 -0
- package/dist/source-files.js.map +1 -0
- package/dist/static-rules.d.ts +45 -0
- package/dist/static-rules.d.ts.map +1 -0
- package/dist/static-rules.js +762 -0
- package/dist/static-rules.js.map +1 -0
- package/dist/verification-policy.d.ts +13 -4
- package/dist/verification-policy.d.ts.map +1 -1
- package/dist/verification-policy.js +31 -12
- package/dist/verification-policy.js.map +1 -1
- package/package.json +37 -1
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The shared page-family journey for a Brandlab production build: one page per
|
|
3
|
+
* app route, at 390 and 1440 pixels wide, must render without sideways
|
|
4
|
+
* scrolling, uncaught page errors or Content Security Policy violations.
|
|
5
|
+
*
|
|
6
|
+
* // tests/e2e/families.spec.ts
|
|
7
|
+
* import { defineBrandlabFamiliesTests } from '@brandlab-digital/conformance/families-e2e'
|
|
8
|
+
* import { routes } from '../../src/data/routes'
|
|
9
|
+
*
|
|
10
|
+
* defineBrandlabFamiliesTests({ routes })
|
|
11
|
+
*
|
|
12
|
+
* The app routes come from the build's `.next/app-path-routes-manifest.json`,
|
|
13
|
+
* so run `next build` first. Each app route's page is the first catalogue path
|
|
14
|
+
* Next routes to it; an app route with no page fails the run until `samples`
|
|
15
|
+
* names one or `skip` gives the reason it has none (fail closed). Every other
|
|
16
|
+
* request leaves the browser only as a GET or HEAD to the build under test.
|
|
17
|
+
*
|
|
18
|
+
* A page is checked once its document has loaded (DOMContentLoaded) and the
|
|
19
|
+
* network has settled, or after a capped settle wait: images the build generates
|
|
20
|
+
* for the first time under load are not a failure. A navigation that times out
|
|
21
|
+
* is retried once; any other navigation failure, a wrong status, a redirect, a
|
|
22
|
+
* missing policy, a page error or a CSP violation fails the check at once.
|
|
23
|
+
*/
|
|
24
|
+
import { readFileSync } from 'node:fs';
|
|
25
|
+
import { isAbsolute, join } from 'node:path';
|
|
26
|
+
import { errors, expect, test } from '@playwright/test';
|
|
27
|
+
import { isolateFromNetwork } from './e2e-support.js';
|
|
28
|
+
export const BRANDLAB_FAMILIES_WIDTHS = Object.freeze([390, 1440]);
|
|
29
|
+
const NOT_FOUND_APP_ROUTE = '/_not-found';
|
|
30
|
+
const NOT_FOUND_PROBE = '/brandlab-families-unknown-page';
|
|
31
|
+
const INTERNAL_APP_ROUTES = new Set(['/_global-error']);
|
|
32
|
+
const VIEWPORT_HEIGHTS = { 390: 844, 1440: 900 };
|
|
33
|
+
const NAVIGATION_TIMEOUT_MS = 45_000;
|
|
34
|
+
const SETTLE_TIMEOUT_MS = 10_000;
|
|
35
|
+
const FONTS_TIMEOUT_MS = 5_000;
|
|
36
|
+
const SEGMENT_RANK = { static: 0, dynamic: 1, 'catch-all': 2, 'optional-catch-all': 3 };
|
|
37
|
+
function parseAppRoute(appRoute) {
|
|
38
|
+
return appRoute.split('/').filter(Boolean).map((part) => {
|
|
39
|
+
if (/^\[\[\.\.\.[^\]]+\]\]$/.test(part))
|
|
40
|
+
return { kind: 'optional-catch-all' };
|
|
41
|
+
if (/^\[\.\.\.[^\]]+\]$/.test(part))
|
|
42
|
+
return { kind: 'catch-all' };
|
|
43
|
+
if (/^\[[^\]]+\]$/.test(part))
|
|
44
|
+
return { kind: 'dynamic' };
|
|
45
|
+
return { kind: 'static', value: part };
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
function matches(segments, parts) {
|
|
49
|
+
const [segment, ...rest] = segments;
|
|
50
|
+
if (!segment)
|
|
51
|
+
return parts.length === 0;
|
|
52
|
+
if (segment.kind === 'static')
|
|
53
|
+
return parts[0] === segment.value && matches(rest, parts.slice(1));
|
|
54
|
+
if (segment.kind === 'dynamic')
|
|
55
|
+
return parts.length > 0 && matches(rest, parts.slice(1));
|
|
56
|
+
// Next allows a catch-all only as the final segment.
|
|
57
|
+
return segment.kind === 'catch-all' ? parts.length > 0 : true;
|
|
58
|
+
}
|
|
59
|
+
function specificity(segments) {
|
|
60
|
+
return segments.map((segment) => SEGMENT_RANK[segment.kind]);
|
|
61
|
+
}
|
|
62
|
+
function compareSpecificity(left, right) {
|
|
63
|
+
for (let index = 0; index < Math.max(left.length, right.length); index += 1) {
|
|
64
|
+
const difference = (left[index] ?? -1) - (right[index] ?? -1);
|
|
65
|
+
if (difference !== 0)
|
|
66
|
+
return difference;
|
|
67
|
+
}
|
|
68
|
+
return 0;
|
|
69
|
+
}
|
|
70
|
+
/** Page app routes from `app-path-routes-manifest.json`, without Next's internal error pages. */
|
|
71
|
+
export function appPageRoutes(manifest) {
|
|
72
|
+
const routes = Object.entries(manifest)
|
|
73
|
+
.filter(([entry]) => entry === '/page' || entry.endsWith('/page'))
|
|
74
|
+
.map(([, appRoute]) => appRoute)
|
|
75
|
+
.filter((appRoute) => !INTERNAL_APP_ROUTES.has(appRoute))
|
|
76
|
+
// Intercepting and parallel-slot routes have no URL of their own.
|
|
77
|
+
.filter((appRoute) => !/\/(?:@|\(\.{1,3}\))/.test(appRoute));
|
|
78
|
+
return [...new Set(routes)].sort();
|
|
79
|
+
}
|
|
80
|
+
/** The app route Next serves a path from, by Next's precedence: static, then dynamic, then catch-all. */
|
|
81
|
+
export function resolveAppRoute(appRoutes, path) {
|
|
82
|
+
const parts = path.split(/[?#]/, 1)[0].split('/').filter(Boolean);
|
|
83
|
+
const candidates = appRoutes
|
|
84
|
+
.filter((appRoute) => appRoute !== NOT_FOUND_APP_ROUTE)
|
|
85
|
+
.map((appRoute) => ({ appRoute, segments: parseAppRoute(appRoute) }))
|
|
86
|
+
.filter(({ segments }) => matches(segments, parts))
|
|
87
|
+
.sort((left, right) => compareSpecificity(specificity(left.segments), specificity(right.segments)));
|
|
88
|
+
return candidates[0]?.appRoute;
|
|
89
|
+
}
|
|
90
|
+
/** One page per app route, and every reason the plan cannot cover the build. */
|
|
91
|
+
export function planBrandlabFamilyPages(input) {
|
|
92
|
+
const appRoutes = appPageRoutes(input.manifest);
|
|
93
|
+
const known = new Set(appRoutes);
|
|
94
|
+
const problems = [];
|
|
95
|
+
const chosen = new Map();
|
|
96
|
+
for (const [appRoute, reason] of Object.entries(input.skip ?? {})) {
|
|
97
|
+
if (!known.has(appRoute))
|
|
98
|
+
problems.push(`skip names ${appRoute}, which is not an app route of this build`);
|
|
99
|
+
if (typeof reason !== 'string' || !reason.trim())
|
|
100
|
+
problems.push(`skip for ${appRoute} gives no reason`);
|
|
101
|
+
}
|
|
102
|
+
for (const [appRoute, path] of Object.entries(input.samples ?? {})) {
|
|
103
|
+
if (!known.has(appRoute)) {
|
|
104
|
+
problems.push(`samples names ${appRoute}, which is not an app route of this build`);
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
const served = resolveAppRoute(appRoutes, path);
|
|
108
|
+
if (appRoute !== NOT_FOUND_APP_ROUTE && served !== appRoute) {
|
|
109
|
+
problems.push(`the sample ${path} for ${appRoute} is served by ${served ?? 'no app route'}`);
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
chosen.set(appRoute, path);
|
|
113
|
+
}
|
|
114
|
+
for (const route of input.routes) {
|
|
115
|
+
const appRoute = resolveAppRoute(appRoutes, route.path);
|
|
116
|
+
if (appRoute && !chosen.has(appRoute))
|
|
117
|
+
chosen.set(appRoute, route.path);
|
|
118
|
+
}
|
|
119
|
+
if (known.has(NOT_FOUND_APP_ROUTE) && !chosen.has(NOT_FOUND_APP_ROUTE))
|
|
120
|
+
chosen.set(NOT_FOUND_APP_ROUTE, NOT_FOUND_PROBE);
|
|
121
|
+
const pages = [];
|
|
122
|
+
for (const appRoute of appRoutes) {
|
|
123
|
+
if (input.skip && Object.hasOwn(input.skip, appRoute))
|
|
124
|
+
continue;
|
|
125
|
+
const path = chosen.get(appRoute);
|
|
126
|
+
if (!path) {
|
|
127
|
+
problems.push(`${appRoute} has no page to check: list one in the catalogue or samples, or skip it with a reason`);
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
pages.push({ appRoute, path, status: appRoute === NOT_FOUND_APP_ROUTE ? 404 : 200 });
|
|
131
|
+
}
|
|
132
|
+
if (appRoutes.length === 0)
|
|
133
|
+
problems.push('the build has no app pages');
|
|
134
|
+
return { pages, problems };
|
|
135
|
+
}
|
|
136
|
+
function readManifest(buildDir) {
|
|
137
|
+
const directory = isAbsolute(buildDir) ? buildDir : join(process.cwd(), buildDir);
|
|
138
|
+
const file = join(directory, 'app-path-routes-manifest.json');
|
|
139
|
+
try {
|
|
140
|
+
const parsed = JSON.parse(readFileSync(file, 'utf8'));
|
|
141
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed))
|
|
142
|
+
return { problem: `${file} is not an app route manifest` };
|
|
143
|
+
return { manifest: parsed };
|
|
144
|
+
}
|
|
145
|
+
catch {
|
|
146
|
+
return { problem: `${file} is missing or unreadable: run next build before the families spec` };
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Open a page for the family checks: wait for its document, then for the network to
|
|
151
|
+
* settle within the cap. A navigation timeout is retried once, because a first
|
|
152
|
+
* request under load can outlast the limit while the build renders the page or
|
|
153
|
+
* generates its images; every other navigation error is thrown at once.
|
|
154
|
+
*/
|
|
155
|
+
async function openBrandlabFamilyPage(page, path, options = {}) {
|
|
156
|
+
const navigationTimeout = options.navigationTimeoutMs ?? NAVIGATION_TIMEOUT_MS;
|
|
157
|
+
const settleTimeout = options.settleTimeoutMs ?? SETTLE_TIMEOUT_MS;
|
|
158
|
+
let response;
|
|
159
|
+
try {
|
|
160
|
+
response = await page.goto(path, { waitUntil: 'domcontentloaded', timeout: navigationTimeout });
|
|
161
|
+
}
|
|
162
|
+
catch (error) {
|
|
163
|
+
if (!(error instanceof errors.TimeoutError))
|
|
164
|
+
throw error;
|
|
165
|
+
options.note?.(`${path}: the first navigation timed out after ${navigationTimeout} ms; retried once`);
|
|
166
|
+
response = await page.goto(path, { waitUntil: 'domcontentloaded', timeout: navigationTimeout });
|
|
167
|
+
}
|
|
168
|
+
try {
|
|
169
|
+
await page.waitForLoadState('networkidle', { timeout: settleTimeout });
|
|
170
|
+
}
|
|
171
|
+
catch (error) {
|
|
172
|
+
if (!(error instanceof errors.TimeoutError))
|
|
173
|
+
throw error;
|
|
174
|
+
options.note?.(`${path}: the network was still busy ${settleTimeout} ms after the document loaded (images may still be generating); checked the rendered page`);
|
|
175
|
+
}
|
|
176
|
+
return response;
|
|
177
|
+
}
|
|
178
|
+
export function defineBrandlabFamiliesTests(options) {
|
|
179
|
+
const widths = options.widths?.length ? options.widths : BRANDLAB_FAMILIES_WIDTHS;
|
|
180
|
+
const navigationTimeoutMs = options.navigationTimeoutMs ?? NAVIGATION_TIMEOUT_MS;
|
|
181
|
+
const settleTimeoutMs = options.settleTimeoutMs ?? SETTLE_TIMEOUT_MS;
|
|
182
|
+
// Room for a retried navigation, the settle wait and the checks, unless the project allows more.
|
|
183
|
+
const testBudget = 2 * navigationTimeoutMs + settleTimeoutMs + FONTS_TIMEOUT_MS + 30_000;
|
|
184
|
+
const { manifest, problem } = readManifest(options.buildDir ?? '.next');
|
|
185
|
+
const plan = manifest
|
|
186
|
+
? planBrandlabFamilyPages({ manifest, routes: options.routes, samples: options.samples, skip: options.skip })
|
|
187
|
+
: { pages: [], problems: [problem] };
|
|
188
|
+
test.describe('Brandlab page families', () => {
|
|
189
|
+
// Service workers could answer requests outside page and context routing.
|
|
190
|
+
test.use({ serviceWorkers: 'block' });
|
|
191
|
+
test('every app route has a page to check', () => {
|
|
192
|
+
expect(plan.problems).toEqual([]);
|
|
193
|
+
});
|
|
194
|
+
for (const page of plan.pages) {
|
|
195
|
+
for (const width of widths) {
|
|
196
|
+
test(`${page.appRoute} renders cleanly at ${width}px (${page.path})`, async ({ page: browserPage, baseURL }, testInfo) => {
|
|
197
|
+
if (testInfo.timeout !== 0 && testInfo.timeout < testBudget)
|
|
198
|
+
testInfo.setTimeout(testBudget);
|
|
199
|
+
await isolateFromNetwork(browserPage, { baseURL, googleTag: 'stub' });
|
|
200
|
+
const problems = [];
|
|
201
|
+
browserPage.on('pageerror', (error) => problems.push(`page error: ${error.message}`));
|
|
202
|
+
browserPage.on('console', (message) => {
|
|
203
|
+
if (message.type() === 'error' && /Content Security Policy/i.test(message.text())) {
|
|
204
|
+
problems.push(`CSP: ${message.text()}`);
|
|
205
|
+
}
|
|
206
|
+
});
|
|
207
|
+
await browserPage.exposeFunction('__brandlabRecordCspViolation', (violation) => {
|
|
208
|
+
problems.push(`CSP violation: ${violation}`);
|
|
209
|
+
});
|
|
210
|
+
await browserPage.addInitScript(() => {
|
|
211
|
+
document.addEventListener('securitypolicyviolation', (event) => {
|
|
212
|
+
void window
|
|
213
|
+
.__brandlabRecordCspViolation(`${event.effectiveDirective} blocked ${event.blockedURI || 'inline code'}`);
|
|
214
|
+
}, true);
|
|
215
|
+
});
|
|
216
|
+
await browserPage.setViewportSize({ width, height: VIEWPORT_HEIGHTS[width] ?? 900 });
|
|
217
|
+
const response = await openBrandlabFamilyPage(browserPage, page.path, {
|
|
218
|
+
navigationTimeoutMs,
|
|
219
|
+
settleTimeoutMs,
|
|
220
|
+
note: (description) => testInfo.annotations.push({ type: 'brandlab-families', description }),
|
|
221
|
+
});
|
|
222
|
+
expect(response, `${page.path} did not answer`).not.toBeNull();
|
|
223
|
+
expect(response.request().redirectedFrom()?.url() ?? null, `${page.path} redirects`).toBeNull();
|
|
224
|
+
expect(response.status(), `${page.path} status`).toBe(page.status);
|
|
225
|
+
const headers = response.headers();
|
|
226
|
+
expect(Boolean(headers['content-security-policy'] || headers['content-security-policy-report-only']), `${page.path} is served without a Content-Security-Policy`).toBe(true);
|
|
227
|
+
await browserPage.evaluate(async (timeout) => {
|
|
228
|
+
await Promise.race([document.fonts?.ready, new Promise((resolve) => setTimeout(resolve, timeout))]);
|
|
229
|
+
}, FONTS_TIMEOUT_MS);
|
|
230
|
+
await browserPage.waitForTimeout(300);
|
|
231
|
+
const sideways = await browserPage.evaluate(() => {
|
|
232
|
+
const root = document.scrollingElement ?? document.documentElement;
|
|
233
|
+
const top = window.scrollY;
|
|
234
|
+
window.scrollTo({ left: root.scrollWidth, top, behavior: 'instant' });
|
|
235
|
+
const scrolled = window.scrollX;
|
|
236
|
+
window.scrollTo({ left: 0, top, behavior: 'instant' });
|
|
237
|
+
return { scrolled, scrollWidth: root.scrollWidth, viewport: window.innerWidth };
|
|
238
|
+
});
|
|
239
|
+
if (sideways.scrolled > 0) {
|
|
240
|
+
problems.push(`scrolls sideways by ${sideways.scrolled}px: the page is ${sideways.scrollWidth}px wide in a ${sideways.viewport}px viewport`);
|
|
241
|
+
}
|
|
242
|
+
expect(problems, `${page.path} at ${width}px`).toEqual([]);
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
//# sourceMappingURL=families-e2e.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"families-e2e.js","sourceRoot":"","sources":["../src/families-e2e.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The shared page-family journey for a Brandlab production build: one page per\n * app route, at 390 and 1440 pixels wide, must render without sideways\n * scrolling, uncaught page errors or Content Security Policy violations.\n *\n * // tests/e2e/families.spec.ts\n * import { defineBrandlabFamiliesTests } from '@brandlab-digital/conformance/families-e2e'\n * import { routes } from '../../src/data/routes'\n *\n * defineBrandlabFamiliesTests({ routes })\n *\n * The app routes come from the build's `.next/app-path-routes-manifest.json`,\n * so run `next build` first. Each app route's page is the first catalogue path\n * Next routes to it; an app route with no page fails the run until `samples`\n * names one or `skip` gives the reason it has none (fail closed). Every other\n * request leaves the browser only as a GET or HEAD to the build under test.\n *\n * A page is checked once its document has loaded (DOMContentLoaded) and the\n * network has settled, or after a capped settle wait: images the build generates\n * for the first time under load are not a failure. A navigation that times out\n * is retried once; any other navigation failure, a wrong status, a redirect, a\n * missing policy, a page error or a CSP violation fails the check at once.\n */\nimport { readFileSync } from 'node:fs';\nimport { isAbsolute, join } from 'node:path';\nimport { errors, expect, test, type Page, type Response } from '@playwright/test';\nimport { isolateFromNetwork } from './e2e-support.js';\n\nexport interface BrandlabFamiliesTestOptions {\n /** The route catalogue; each app route's page is the first path Next routes to it. */\n routes: readonly { path: string }[];\n /** Pages for app routes the catalogue does not list, keyed by app route (for example `'/quote'`). */\n samples?: Readonly<Record<string, string>>;\n /** App routes with no page to check, each with the reason. */\n skip?: Readonly<Record<string, string>>;\n /** The production build directory, relative to the working directory (default `.next`). */\n buildDir?: string;\n /** Viewport widths (default 390 and 1440). */\n widths?: readonly number[];\n /** How long a document may take to load (DOMContentLoaded), default 45 seconds; a timeout is retried once. */\n navigationTimeoutMs?: number;\n /** How long to wait for the network to settle before checking, default 10 seconds; a busy network is not a failure. */\n settleTimeoutMs?: number;\n}\n\nexport interface BrandlabFamilyPage {\n /** The app route, for example `/services/[slug]`. */\n appRoute: string;\n /** The page checked for it. */\n path: string;\n /** 200, or 404 for the not-found page. */\n status: 200 | 404;\n}\n\nexport const BRANDLAB_FAMILIES_WIDTHS = Object.freeze([390, 1440]);\nconst NOT_FOUND_APP_ROUTE = '/_not-found';\nconst NOT_FOUND_PROBE = '/brandlab-families-unknown-page';\nconst INTERNAL_APP_ROUTES = new Set(['/_global-error']);\nconst VIEWPORT_HEIGHTS: Readonly<Record<number, number>> = { 390: 844, 1440: 900 };\nconst NAVIGATION_TIMEOUT_MS = 45_000;\nconst SETTLE_TIMEOUT_MS = 10_000;\nconst FONTS_TIMEOUT_MS = 5_000;\n\ntype Segment =\n | { kind: 'static'; value: string }\n | { kind: 'dynamic' }\n | { kind: 'catch-all' }\n | { kind: 'optional-catch-all' };\n\nconst SEGMENT_RANK = { static: 0, dynamic: 1, 'catch-all': 2, 'optional-catch-all': 3 } as const;\n\nfunction parseAppRoute(appRoute: string): Segment[] {\n return appRoute.split('/').filter(Boolean).map((part): Segment => {\n if (/^\\[\\[\\.\\.\\.[^\\]]+\\]\\]$/.test(part)) return { kind: 'optional-catch-all' };\n if (/^\\[\\.\\.\\.[^\\]]+\\]$/.test(part)) return { kind: 'catch-all' };\n if (/^\\[[^\\]]+\\]$/.test(part)) return { kind: 'dynamic' };\n return { kind: 'static', value: part };\n });\n}\n\nfunction matches(segments: readonly Segment[], parts: readonly string[]): boolean {\n const [segment, ...rest] = segments;\n if (!segment) return parts.length === 0;\n if (segment.kind === 'static') return parts[0] === segment.value && matches(rest, parts.slice(1));\n if (segment.kind === 'dynamic') return parts.length > 0 && matches(rest, parts.slice(1));\n // Next allows a catch-all only as the final segment.\n return segment.kind === 'catch-all' ? parts.length > 0 : true;\n}\n\nfunction specificity(segments: readonly Segment[]): number[] {\n return segments.map((segment) => SEGMENT_RANK[segment.kind]);\n}\n\nfunction compareSpecificity(left: readonly number[], right: readonly number[]): number {\n for (let index = 0; index < Math.max(left.length, right.length); index += 1) {\n const difference = (left[index] ?? -1) - (right[index] ?? -1);\n if (difference !== 0) return difference;\n }\n return 0;\n}\n\n/** Page app routes from `app-path-routes-manifest.json`, without Next's internal error pages. */\nexport function appPageRoutes(manifest: Readonly<Record<string, string>>): string[] {\n const routes = Object.entries(manifest)\n .filter(([entry]) => entry === '/page' || entry.endsWith('/page'))\n .map(([, appRoute]) => appRoute)\n .filter((appRoute) => !INTERNAL_APP_ROUTES.has(appRoute))\n // Intercepting and parallel-slot routes have no URL of their own.\n .filter((appRoute) => !/\\/(?:@|\\(\\.{1,3}\\))/.test(appRoute));\n return [...new Set(routes)].sort();\n}\n\n/** The app route Next serves a path from, by Next's precedence: static, then dynamic, then catch-all. */\nexport function resolveAppRoute(appRoutes: readonly string[], path: string): string | undefined {\n const parts = path.split(/[?#]/, 1)[0]!.split('/').filter(Boolean);\n const candidates = appRoutes\n .filter((appRoute) => appRoute !== NOT_FOUND_APP_ROUTE)\n .map((appRoute) => ({ appRoute, segments: parseAppRoute(appRoute) }))\n .filter(({ segments }) => matches(segments, parts))\n .sort((left, right) => compareSpecificity(specificity(left.segments), specificity(right.segments)));\n return candidates[0]?.appRoute;\n}\n\n/** One page per app route, and every reason the plan cannot cover the build. */\nexport function planBrandlabFamilyPages(input: {\n manifest: Readonly<Record<string, string>>;\n routes: readonly { path: string }[];\n samples?: Readonly<Record<string, string>>;\n skip?: Readonly<Record<string, string>>;\n}): { pages: BrandlabFamilyPage[]; problems: string[] } {\n const appRoutes = appPageRoutes(input.manifest);\n const known = new Set(appRoutes);\n const problems: string[] = [];\n const chosen = new Map<string, string>();\n for (const [appRoute, reason] of Object.entries(input.skip ?? {})) {\n if (!known.has(appRoute)) problems.push(`skip names ${appRoute}, which is not an app route of this build`);\n if (typeof reason !== 'string' || !reason.trim()) problems.push(`skip for ${appRoute} gives no reason`);\n }\n for (const [appRoute, path] of Object.entries(input.samples ?? {})) {\n if (!known.has(appRoute)) {\n problems.push(`samples names ${appRoute}, which is not an app route of this build`);\n continue;\n }\n const served = resolveAppRoute(appRoutes, path);\n if (appRoute !== NOT_FOUND_APP_ROUTE && served !== appRoute) {\n problems.push(`the sample ${path} for ${appRoute} is served by ${served ?? 'no app route'}`);\n continue;\n }\n chosen.set(appRoute, path);\n }\n for (const route of input.routes) {\n const appRoute = resolveAppRoute(appRoutes, route.path);\n if (appRoute && !chosen.has(appRoute)) chosen.set(appRoute, route.path);\n }\n if (known.has(NOT_FOUND_APP_ROUTE) && !chosen.has(NOT_FOUND_APP_ROUTE)) chosen.set(NOT_FOUND_APP_ROUTE, NOT_FOUND_PROBE);\n const pages: BrandlabFamilyPage[] = [];\n for (const appRoute of appRoutes) {\n if (input.skip && Object.hasOwn(input.skip, appRoute)) continue;\n const path = chosen.get(appRoute);\n if (!path) {\n problems.push(`${appRoute} has no page to check: list one in the catalogue or samples, or skip it with a reason`);\n continue;\n }\n pages.push({ appRoute, path, status: appRoute === NOT_FOUND_APP_ROUTE ? 404 : 200 });\n }\n if (appRoutes.length === 0) problems.push('the build has no app pages');\n return { pages, problems };\n}\n\nfunction readManifest(buildDir: string): { manifest?: Record<string, string>; problem?: string } {\n const directory = isAbsolute(buildDir) ? buildDir : join(process.cwd(), buildDir);\n const file = join(directory, 'app-path-routes-manifest.json');\n try {\n const parsed = JSON.parse(readFileSync(file, 'utf8')) as unknown;\n if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) return { problem: `${file} is not an app route manifest` };\n return { manifest: parsed as Record<string, string> };\n } catch {\n return { problem: `${file} is missing or unreadable: run next build before the families spec` };\n }\n}\n\n/**\n * Open a page for the family checks: wait for its document, then for the network to\n * settle within the cap. A navigation timeout is retried once, because a first\n * request under load can outlast the limit while the build renders the page or\n * generates its images; every other navigation error is thrown at once.\n */\nasync function openBrandlabFamilyPage(\n page: Page,\n path: string,\n options: { navigationTimeoutMs?: number; settleTimeoutMs?: number; note?: (note: string) => void } = {},\n): Promise<Response | null> {\n const navigationTimeout = options.navigationTimeoutMs ?? NAVIGATION_TIMEOUT_MS;\n const settleTimeout = options.settleTimeoutMs ?? SETTLE_TIMEOUT_MS;\n let response: Response | null;\n try {\n response = await page.goto(path, { waitUntil: 'domcontentloaded', timeout: navigationTimeout });\n } catch (error) {\n if (!(error instanceof errors.TimeoutError)) throw error;\n options.note?.(`${path}: the first navigation timed out after ${navigationTimeout} ms; retried once`);\n response = await page.goto(path, { waitUntil: 'domcontentloaded', timeout: navigationTimeout });\n }\n try {\n await page.waitForLoadState('networkidle', { timeout: settleTimeout });\n } catch (error) {\n if (!(error instanceof errors.TimeoutError)) throw error;\n options.note?.(`${path}: the network was still busy ${settleTimeout} ms after the document loaded (images may still be generating); checked the rendered page`);\n }\n return response;\n}\n\nexport function defineBrandlabFamiliesTests(options: BrandlabFamiliesTestOptions) {\n const widths = options.widths?.length ? options.widths : BRANDLAB_FAMILIES_WIDTHS;\n const navigationTimeoutMs = options.navigationTimeoutMs ?? NAVIGATION_TIMEOUT_MS;\n const settleTimeoutMs = options.settleTimeoutMs ?? SETTLE_TIMEOUT_MS;\n // Room for a retried navigation, the settle wait and the checks, unless the project allows more.\n const testBudget = 2 * navigationTimeoutMs + settleTimeoutMs + FONTS_TIMEOUT_MS + 30_000;\n const { manifest, problem } = readManifest(options.buildDir ?? '.next');\n const plan = manifest\n ? planBrandlabFamilyPages({ manifest, routes: options.routes, samples: options.samples, skip: options.skip })\n : { pages: [], problems: [problem!] };\n\n test.describe('Brandlab page families', () => {\n // Service workers could answer requests outside page and context routing.\n test.use({ serviceWorkers: 'block' });\n\n test('every app route has a page to check', () => {\n expect(plan.problems).toEqual([]);\n });\n\n for (const page of plan.pages) {\n for (const width of widths) {\n test(`${page.appRoute} renders cleanly at ${width}px (${page.path})`, async ({ page: browserPage, baseURL }, testInfo) => {\n if (testInfo.timeout !== 0 && testInfo.timeout < testBudget) testInfo.setTimeout(testBudget);\n await isolateFromNetwork(browserPage, { baseURL, googleTag: 'stub' });\n const problems: string[] = [];\n browserPage.on('pageerror', (error) => problems.push(`page error: ${error.message}`));\n browserPage.on('console', (message) => {\n if (message.type() === 'error' && /Content Security Policy/i.test(message.text())) {\n problems.push(`CSP: ${message.text()}`);\n }\n });\n await browserPage.exposeFunction('__brandlabRecordCspViolation', (violation: string) => {\n problems.push(`CSP violation: ${violation}`);\n });\n await browserPage.addInitScript(() => {\n document.addEventListener('securitypolicyviolation', (event) => {\n void (window as unknown as { __brandlabRecordCspViolation: (value: string) => Promise<void> })\n .__brandlabRecordCspViolation(`${event.effectiveDirective} blocked ${event.blockedURI || 'inline code'}`);\n }, true);\n });\n await browserPage.setViewportSize({ width, height: VIEWPORT_HEIGHTS[width] ?? 900 });\n\n const response = await openBrandlabFamilyPage(browserPage, page.path, {\n navigationTimeoutMs,\n settleTimeoutMs,\n note: (description) => testInfo.annotations.push({ type: 'brandlab-families', description }),\n });\n expect(response, `${page.path} did not answer`).not.toBeNull();\n expect(response!.request().redirectedFrom()?.url() ?? null, `${page.path} redirects`).toBeNull();\n expect(response!.status(), `${page.path} status`).toBe(page.status);\n const headers = response!.headers();\n expect(\n Boolean(headers['content-security-policy'] || headers['content-security-policy-report-only']),\n `${page.path} is served without a Content-Security-Policy`,\n ).toBe(true);\n\n await browserPage.evaluate(async (timeout) => {\n await Promise.race([document.fonts?.ready, new Promise((resolve) => setTimeout(resolve, timeout))]);\n }, FONTS_TIMEOUT_MS);\n await browserPage.waitForTimeout(300);\n const sideways = await browserPage.evaluate(() => {\n const root = document.scrollingElement ?? document.documentElement;\n const top = window.scrollY;\n window.scrollTo({ left: root.scrollWidth, top, behavior: 'instant' });\n const scrolled = window.scrollX;\n window.scrollTo({ left: 0, top, behavior: 'instant' });\n return { scrolled, scrollWidth: root.scrollWidth, viewport: window.innerWidth };\n });\n if (sideways.scrolled > 0) {\n problems.push(`scrolls sideways by ${sideways.scrolled}px: the page is ${sideways.scrollWidth}px wide in a ${sideways.viewport}px viewport`);\n }\n expect(problems, `${page.path} at ${width}px`).toEqual([]);\n });\n }\n }\n });\n}\n"]}
|
package/dist/glob.d.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A small glob matcher for untrusted patterns (.vercelignore lines, npm-run-all
|
|
3
|
+
* script globs). It never builds a regular expression and never throws:
|
|
4
|
+
*
|
|
5
|
+
* - `*` matches any run of characters within one segment, `?` one character;
|
|
6
|
+
* - `[abc]`, `[a-z]`, `[!a-z]` / `[^a-z]` match one character; an unclosed `[` is literal;
|
|
7
|
+
* - `\x` matches `x` literally;
|
|
8
|
+
* - with `globstar`, a segment that is exactly `**` matches any number of segments.
|
|
9
|
+
*
|
|
10
|
+
* Segments are separated by `separator`. Matching uses the classic single
|
|
11
|
+
* backtrack-point wildcard algorithm, so the work is bounded by
|
|
12
|
+
* pattern length × text length rather than growing exponentially.
|
|
13
|
+
*/
|
|
14
|
+
export interface GlobOptions {
|
|
15
|
+
/** Character that `*` and `?` never match; defaults to "/". */
|
|
16
|
+
separator?: string;
|
|
17
|
+
/** Treat a whole `**` segment as matching zero or more segments. */
|
|
18
|
+
globstar?: boolean;
|
|
19
|
+
}
|
|
20
|
+
export declare function globMatch(pattern: string, text: string, options?: GlobOptions): boolean;
|
|
21
|
+
//# sourceMappingURL=glob.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"glob.d.ts","sourceRoot":"","sources":["../src/glob.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AACH,MAAM,WAAW,WAAW;IAC1B,+DAA+D;IAC/D,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,oEAAoE;IACpE,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAQD,wBAAgB,SAAS,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,GAAE,WAAgB,GAAG,OAAO,CAiC3F"}
|
package/dist/glob.js
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
export function globMatch(pattern, text, options = {}) {
|
|
2
|
+
const separator = options.separator ?? '/';
|
|
3
|
+
const patternSegments = pattern.split(separator);
|
|
4
|
+
const textSegments = text.split(separator);
|
|
5
|
+
if (!options.globstar) {
|
|
6
|
+
return patternSegments.length === textSegments.length &&
|
|
7
|
+
patternSegments.every((segment, index) => segmentMatch(tokenize(segment), textSegments[index]));
|
|
8
|
+
}
|
|
9
|
+
// The same bounded algorithm one level up: `**` segments are the wildcards.
|
|
10
|
+
const compiled = patternSegments.map((segment) => (segment === '**' ? null : tokenize(segment)));
|
|
11
|
+
let p = 0;
|
|
12
|
+
let t = 0;
|
|
13
|
+
let starP = -1;
|
|
14
|
+
let starT = 0;
|
|
15
|
+
while (t < textSegments.length) {
|
|
16
|
+
const current = compiled[p];
|
|
17
|
+
if (p < compiled.length && current === null) {
|
|
18
|
+
starP = p;
|
|
19
|
+
starT = t;
|
|
20
|
+
p += 1;
|
|
21
|
+
}
|
|
22
|
+
else if (p < compiled.length && current && segmentMatch(current, textSegments[t])) {
|
|
23
|
+
p += 1;
|
|
24
|
+
t += 1;
|
|
25
|
+
}
|
|
26
|
+
else if (starP !== -1) {
|
|
27
|
+
p = starP + 1;
|
|
28
|
+
starT += 1;
|
|
29
|
+
t = starT;
|
|
30
|
+
}
|
|
31
|
+
else {
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
while (p < compiled.length && compiled[p] === null)
|
|
36
|
+
p += 1;
|
|
37
|
+
return p === compiled.length;
|
|
38
|
+
}
|
|
39
|
+
function segmentMatch(tokens, text) {
|
|
40
|
+
const characters = [...text];
|
|
41
|
+
let p = 0;
|
|
42
|
+
let t = 0;
|
|
43
|
+
let starP = -1;
|
|
44
|
+
let starT = 0;
|
|
45
|
+
while (t < characters.length) {
|
|
46
|
+
const token = tokens[p];
|
|
47
|
+
if (token?.kind === 'star') {
|
|
48
|
+
starP = p;
|
|
49
|
+
starT = t;
|
|
50
|
+
p += 1;
|
|
51
|
+
}
|
|
52
|
+
else if (token && tokenMatches(token, characters[t])) {
|
|
53
|
+
p += 1;
|
|
54
|
+
t += 1;
|
|
55
|
+
}
|
|
56
|
+
else if (starP !== -1) {
|
|
57
|
+
p = starP + 1;
|
|
58
|
+
starT += 1;
|
|
59
|
+
t = starT;
|
|
60
|
+
}
|
|
61
|
+
else {
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
while (tokens[p]?.kind === 'star')
|
|
66
|
+
p += 1;
|
|
67
|
+
return p === tokens.length;
|
|
68
|
+
}
|
|
69
|
+
function tokenMatches(token, character) {
|
|
70
|
+
if (token.kind === 'any')
|
|
71
|
+
return true;
|
|
72
|
+
if (token.kind === 'literal')
|
|
73
|
+
return token.value === character;
|
|
74
|
+
const inClass = token.items.some((item) => typeof item === 'string' ? item === character : item[0] <= character && character <= item[1]);
|
|
75
|
+
return inClass !== token.negated;
|
|
76
|
+
}
|
|
77
|
+
function tokenize(segment) {
|
|
78
|
+
const characters = [...segment];
|
|
79
|
+
const tokens = [];
|
|
80
|
+
for (let index = 0; index < characters.length; index += 1) {
|
|
81
|
+
const character = characters[index];
|
|
82
|
+
if (character === '\\' && index + 1 < characters.length) {
|
|
83
|
+
index += 1;
|
|
84
|
+
tokens.push({ kind: 'literal', value: characters[index] });
|
|
85
|
+
}
|
|
86
|
+
else if (character === '*') {
|
|
87
|
+
if (tokens.at(-1)?.kind !== 'star')
|
|
88
|
+
tokens.push({ kind: 'star' });
|
|
89
|
+
}
|
|
90
|
+
else if (character === '?') {
|
|
91
|
+
tokens.push({ kind: 'any' });
|
|
92
|
+
}
|
|
93
|
+
else if (character === '[') {
|
|
94
|
+
const parsed = parseClass(characters, index);
|
|
95
|
+
if (parsed) {
|
|
96
|
+
tokens.push(parsed.token);
|
|
97
|
+
index = parsed.end;
|
|
98
|
+
}
|
|
99
|
+
else {
|
|
100
|
+
tokens.push({ kind: 'literal', value: character });
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
else {
|
|
104
|
+
tokens.push({ kind: 'literal', value: character });
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return tokens;
|
|
108
|
+
}
|
|
109
|
+
function parseClass(characters, start) {
|
|
110
|
+
let index = start + 1;
|
|
111
|
+
const negated = characters[index] === '!' || characters[index] === '^';
|
|
112
|
+
if (negated)
|
|
113
|
+
index += 1;
|
|
114
|
+
const items = [];
|
|
115
|
+
let first = true;
|
|
116
|
+
while (index < characters.length) {
|
|
117
|
+
let character = characters[index];
|
|
118
|
+
if (character === ']' && !first)
|
|
119
|
+
return { token: { kind: 'class', negated, items }, end: index };
|
|
120
|
+
if (character === '\\' && index + 1 < characters.length) {
|
|
121
|
+
index += 1;
|
|
122
|
+
character = characters[index];
|
|
123
|
+
}
|
|
124
|
+
const next = characters[index + 1];
|
|
125
|
+
const rangeEnd = characters[index + 2];
|
|
126
|
+
if (next === '-' && rangeEnd !== undefined && rangeEnd !== ']') {
|
|
127
|
+
items.push([character, rangeEnd]);
|
|
128
|
+
index += 3;
|
|
129
|
+
}
|
|
130
|
+
else {
|
|
131
|
+
items.push(character);
|
|
132
|
+
index += 1;
|
|
133
|
+
}
|
|
134
|
+
first = false;
|
|
135
|
+
}
|
|
136
|
+
return null;
|
|
137
|
+
}
|
|
138
|
+
//# sourceMappingURL=glob.js.map
|
package/dist/glob.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"glob.js","sourceRoot":"","sources":["../src/glob.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * A small glob matcher for untrusted patterns (.vercelignore lines, npm-run-all\n * script globs). It never builds a regular expression and never throws:\n *\n * - `*` matches any run of characters within one segment, `?` one character;\n * - `[abc]`, `[a-z]`, `[!a-z]` / `[^a-z]` match one character; an unclosed `[` is literal;\n * - `\\x` matches `x` literally;\n * - with `globstar`, a segment that is exactly `**` matches any number of segments.\n *\n * Segments are separated by `separator`. Matching uses the classic single\n * backtrack-point wildcard algorithm, so the work is bounded by\n * pattern length × text length rather than growing exponentially.\n */\nexport interface GlobOptions {\n /** Character that `*` and `?` never match; defaults to \"/\". */\n separator?: string;\n /** Treat a whole `**` segment as matching zero or more segments. */\n globstar?: boolean;\n}\n\ntype Token =\n | { kind: 'literal'; value: string }\n | { kind: 'any' }\n | { kind: 'star' }\n | { kind: 'class'; negated: boolean; items: readonly (string | readonly [string, string])[] };\n\nexport function globMatch(pattern: string, text: string, options: GlobOptions = {}): boolean {\n const separator = options.separator ?? '/';\n const patternSegments = pattern.split(separator);\n const textSegments = text.split(separator);\n if (!options.globstar) {\n return patternSegments.length === textSegments.length &&\n patternSegments.every((segment, index) => segmentMatch(tokenize(segment), textSegments[index]!));\n }\n // The same bounded algorithm one level up: `**` segments are the wildcards.\n const compiled = patternSegments.map((segment) => (segment === '**' ? null : tokenize(segment)));\n let p = 0;\n let t = 0;\n let starP = -1;\n let starT = 0;\n while (t < textSegments.length) {\n const current = compiled[p];\n if (p < compiled.length && current === null) {\n starP = p;\n starT = t;\n p += 1;\n } else if (p < compiled.length && current && segmentMatch(current, textSegments[t]!)) {\n p += 1;\n t += 1;\n } else if (starP !== -1) {\n p = starP + 1;\n starT += 1;\n t = starT;\n } else {\n return false;\n }\n }\n while (p < compiled.length && compiled[p] === null) p += 1;\n return p === compiled.length;\n}\n\nfunction segmentMatch(tokens: readonly Token[], text: string): boolean {\n const characters = [...text];\n let p = 0;\n let t = 0;\n let starP = -1;\n let starT = 0;\n while (t < characters.length) {\n const token = tokens[p];\n if (token?.kind === 'star') {\n starP = p;\n starT = t;\n p += 1;\n } else if (token && tokenMatches(token, characters[t]!)) {\n p += 1;\n t += 1;\n } else if (starP !== -1) {\n p = starP + 1;\n starT += 1;\n t = starT;\n } else {\n return false;\n }\n }\n while (tokens[p]?.kind === 'star') p += 1;\n return p === tokens.length;\n}\n\nfunction tokenMatches(token: Exclude<Token, { kind: 'star' }>, character: string): boolean {\n if (token.kind === 'any') return true;\n if (token.kind === 'literal') return token.value === character;\n const inClass = token.items.some((item) =>\n typeof item === 'string' ? item === character : item[0] <= character && character <= item[1]);\n return inClass !== token.negated;\n}\n\nfunction tokenize(segment: string): Token[] {\n const characters = [...segment];\n const tokens: Token[] = [];\n for (let index = 0; index < characters.length; index += 1) {\n const character = characters[index]!;\n if (character === '\\\\' && index + 1 < characters.length) {\n index += 1;\n tokens.push({ kind: 'literal', value: characters[index]! });\n } else if (character === '*') {\n if (tokens.at(-1)?.kind !== 'star') tokens.push({ kind: 'star' });\n } else if (character === '?') {\n tokens.push({ kind: 'any' });\n } else if (character === '[') {\n const parsed = parseClass(characters, index);\n if (parsed) {\n tokens.push(parsed.token);\n index = parsed.end;\n } else {\n tokens.push({ kind: 'literal', value: character });\n }\n } else {\n tokens.push({ kind: 'literal', value: character });\n }\n }\n return tokens;\n}\n\nfunction parseClass(characters: readonly string[], start: number): { token: Token; end: number } | null {\n let index = start + 1;\n const negated = characters[index] === '!' || characters[index] === '^';\n if (negated) index += 1;\n const items: (string | readonly [string, string])[] = [];\n let first = true;\n while (index < characters.length) {\n let character = characters[index]!;\n if (character === ']' && !first) return { token: { kind: 'class', negated, items }, end: index };\n if (character === '\\\\' && index + 1 < characters.length) {\n index += 1;\n character = characters[index]!;\n }\n const next = characters[index + 1];\n const rangeEnd = characters[index + 2];\n if (next === '-' && rangeEnd !== undefined && rangeEnd !== ']') {\n items.push([character, rangeEnd]);\n index += 3;\n } else {\n items.push(character);\n index += 1;\n }\n first = false;\n }\n return null;\n}\n"]}
|
package/dist/knip.d.ts
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The shared Knip preset for Brandlab Next.js projects:
|
|
3
|
+
*
|
|
4
|
+
* // knip.ts
|
|
5
|
+
* import { brandlabKnip } from '@brandlab-digital/conformance/knip'
|
|
6
|
+
* export default brandlabKnip({ mdx: true })
|
|
7
|
+
*
|
|
8
|
+
* It keeps Knip's own Next.js, Playwright, Vitest and ESLint plugin entries
|
|
9
|
+
* (overriding them would replace the maintained defaults) and adds only what
|
|
10
|
+
* those plugins cannot see in a Brandlab project: production entries Next loads
|
|
11
|
+
* outside the app graph, CSS-only dependencies and, when enabled, MDX articles.
|
|
12
|
+
* Knip calls the returned function with its command-line mode.
|
|
13
|
+
*/
|
|
14
|
+
export type BrandlabKnipCompiler = (text: string, filename: string) => string;
|
|
15
|
+
/** The subset of Knip's configuration the preset produces; it satisfies `KnipConfig`. */
|
|
16
|
+
export interface BrandlabKnipConfig {
|
|
17
|
+
entry: string[];
|
|
18
|
+
ignoreExportsUsedInFile: boolean;
|
|
19
|
+
compilers: Record<string, true | BrandlabKnipCompiler>;
|
|
20
|
+
project?: string[];
|
|
21
|
+
ignoreDependencies?: string[];
|
|
22
|
+
}
|
|
23
|
+
/** The parsed Knip arguments Knip passes to a function configuration. */
|
|
24
|
+
export interface BrandlabKnipMode {
|
|
25
|
+
production?: boolean;
|
|
26
|
+
strict?: boolean;
|
|
27
|
+
}
|
|
28
|
+
/** A Knip function configuration: Knip calls it with its parsed arguments. */
|
|
29
|
+
export type BrandlabKnipConfigFunction = (mode?: BrandlabKnipMode) => BrandlabKnipConfig;
|
|
30
|
+
export interface BrandlabKnipOptions {
|
|
31
|
+
/** The project renders long-form MDX articles from src/content/. */
|
|
32
|
+
mdx?: boolean;
|
|
33
|
+
/**
|
|
34
|
+
* Additional real entry files that no plugin can discover. Suffix a pattern
|
|
35
|
+
* with `!` when it is part of the production graph. Never list whole source
|
|
36
|
+
* directories to silence findings.
|
|
37
|
+
*/
|
|
38
|
+
entry?: readonly string[];
|
|
39
|
+
}
|
|
40
|
+
export declare function brandlabKnip(options?: BrandlabKnipOptions): BrandlabKnipConfigFunction;
|
|
41
|
+
/**
|
|
42
|
+
* Expose CSS-only dependencies (`@import "tailwindcss"`, `@plugin`, `@config`,
|
|
43
|
+
* `@reference`, local `@import`) to Knip as module imports. Remote URLs and
|
|
44
|
+
* data URIs are not dependencies and are skipped.
|
|
45
|
+
*/
|
|
46
|
+
export declare function compileCssImports(text: string): string;
|
|
47
|
+
//# sourceMappingURL=knip.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"knip.d.ts","sourceRoot":"","sources":["../src/knip.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,MAAM,MAAM,oBAAoB,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,KAAK,MAAM,CAAC;AAE9E,yFAAyF;AACzF,MAAM,WAAW,kBAAkB;IACjC,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,uBAAuB,EAAE,OAAO,CAAC;IACjC,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,IAAI,GAAG,oBAAoB,CAAC,CAAC;IACvD,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;IACnB,kBAAkB,CAAC,EAAE,MAAM,EAAE,CAAC;CAC/B;AAED,yEAAyE;AACzE,MAAM,WAAW,gBAAgB;IAC/B,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB;AAED,8EAA8E;AAC9E,MAAM,MAAM,0BAA0B,GAAG,CAAC,IAAI,CAAC,EAAE,gBAAgB,KAAK,kBAAkB,CAAC;AAEzF,MAAM,WAAW,mBAAmB;IAClC,oEAAoE;IACpE,GAAG,CAAC,EAAE,OAAO,CAAC;IACd;;;;OAIG;IACH,KAAK,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CAC3B;AAYD,wBAAgB,YAAY,CAAC,OAAO,GAAE,mBAAwB,GAAG,0BAA0B,CA2C1F;AAMD;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAQtD"}
|
package/dist/knip.js
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The shared Knip preset for Brandlab Next.js projects:
|
|
3
|
+
*
|
|
4
|
+
* // knip.ts
|
|
5
|
+
* import { brandlabKnip } from '@brandlab-digital/conformance/knip'
|
|
6
|
+
* export default brandlabKnip({ mdx: true })
|
|
7
|
+
*
|
|
8
|
+
* It keeps Knip's own Next.js, Playwright, Vitest and ESLint plugin entries
|
|
9
|
+
* (overriding them would replace the maintained defaults) and adds only what
|
|
10
|
+
* those plugins cannot see in a Brandlab project: production entries Next loads
|
|
11
|
+
* outside the app graph, CSS-only dependencies and, when enabled, MDX articles.
|
|
12
|
+
* Knip calls the returned function with its command-line mode.
|
|
13
|
+
*/
|
|
14
|
+
const PRODUCTION_ENTRIES = [
|
|
15
|
+
// Knip's production mode excludes next.config by design, so the redirect table it
|
|
16
|
+
// imports (the fixed 3.0 owner) would look unused although Next applies it.
|
|
17
|
+
'src/data/redirects.ts!',
|
|
18
|
+
// Read statically by brandlab-verify; it is part of the project, not dead code.
|
|
19
|
+
'brandlab.config.ts!',
|
|
20
|
+
];
|
|
21
|
+
const SOURCE_EXTENSIONS = ['js', 'mjs', 'cjs', 'jsx', 'ts', 'tsx', 'mts', 'cts'];
|
|
22
|
+
export function brandlabKnip(options = {}) {
|
|
23
|
+
const entry = [...PRODUCTION_ENTRIES];
|
|
24
|
+
const compilers = { css: compileCssImports };
|
|
25
|
+
if (options.mdx) {
|
|
26
|
+
// Articles are loaded by slug through dynamic imports that Knip cannot follow.
|
|
27
|
+
entry.push('src/content/**/*.{md,mdx}!');
|
|
28
|
+
// Knip's MDX compiler exposes each article's imports to the module graph.
|
|
29
|
+
compilers.mdx = true;
|
|
30
|
+
}
|
|
31
|
+
for (const pattern of options.entry ?? []) {
|
|
32
|
+
if (typeof pattern !== 'string' || !pattern.trim()) {
|
|
33
|
+
throw new TypeError('brandlabKnip entry patterns must be non-empty strings.');
|
|
34
|
+
}
|
|
35
|
+
if (/^(?:src\/?)?\*\*(?:\/\*)?!?$/.test(pattern.trim())) {
|
|
36
|
+
throw new Error(`brandlabKnip entry "${pattern}" marks all source as used; list the real entry files instead.`);
|
|
37
|
+
}
|
|
38
|
+
entry.push(pattern.trim());
|
|
39
|
+
}
|
|
40
|
+
const extensions = `{${[...SOURCE_EXTENSIONS, ...Object.keys(compilers), ...(options.mdx ? ['md'] : [])].join(',')}}`;
|
|
41
|
+
const config = {
|
|
42
|
+
entry: [...new Set(entry)],
|
|
43
|
+
// Exports that are also consumed inside their own module are not dead code.
|
|
44
|
+
ignoreExportsUsedInFile: true,
|
|
45
|
+
compilers,
|
|
46
|
+
};
|
|
47
|
+
// Otherwise no ignoreDependencies: optional peers of the shared packages
|
|
48
|
+
// (eslint-config-next for configs.next) already count as used, and anything else
|
|
49
|
+
// must stay visible.
|
|
50
|
+
return (mode = {}) => {
|
|
51
|
+
const copy = { ...config, entry: [...config.entry], compilers: { ...config.compilers } };
|
|
52
|
+
if (mode.production !== true && mode.strict !== true)
|
|
53
|
+
return copy;
|
|
54
|
+
return {
|
|
55
|
+
...copy,
|
|
56
|
+
// Knip's default project marks every file as production (**/*!), so production mode
|
|
57
|
+
// reported test tooling (vitest.setup.ts, tests/ helpers) as unused files. The
|
|
58
|
+
// application lives in src/; root production files are entries (brandlab.config.ts)
|
|
59
|
+
// or belong to a plugin (next.config.ts, mdx-components.tsx).
|
|
60
|
+
project: [`src/**/*.${extensions}!`],
|
|
61
|
+
// Knip's production mode never reads next.config (not even as an entry), where the
|
|
62
|
+
// MDX wrapper imports @next/mdx; the default graph still checks that import.
|
|
63
|
+
...(options.mdx ? { ignoreDependencies: ['@next/mdx'] } : {}),
|
|
64
|
+
};
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
const CSS_COMMENT = /\/\*[\s\S]*?\*\//g;
|
|
68
|
+
const CSS_DIRECTIVE = /@(?:import|config|plugin|reference)\s+(?:url\(\s*)?(?:"([^"]+)"|'([^']+)'|([^'"\s;)]+))\s*\)?[^;]*;/g;
|
|
69
|
+
/**
|
|
70
|
+
* Expose CSS-only dependencies (`@import "tailwindcss"`, `@plugin`, `@config`,
|
|
71
|
+
* `@reference`, local `@import`) to Knip as module imports. Remote URLs and
|
|
72
|
+
* data URIs are not dependencies and are skipped.
|
|
73
|
+
*/
|
|
74
|
+
export function compileCssImports(text) {
|
|
75
|
+
const imports = [];
|
|
76
|
+
for (const match of text.replace(CSS_COMMENT, '').matchAll(CSS_DIRECTIVE)) {
|
|
77
|
+
const specifier = match[1] ?? match[2] ?? match[3];
|
|
78
|
+
if (!specifier || /^(?:[a-z][a-z0-9+.-]*:|\/\/|#)/i.test(specifier))
|
|
79
|
+
continue;
|
|
80
|
+
imports.push(`import ${JSON.stringify(specifier)};`);
|
|
81
|
+
}
|
|
82
|
+
return imports.join('\n');
|
|
83
|
+
}
|
|
84
|
+
//# sourceMappingURL=knip.js.map
|