@salesforce/storefront-next-dev 1.4.0 → 1.5.0-alpha.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 +13 -2
- package/dist/assets.js +25 -0
- package/dist/audit/catalog/categories/best-practices/CATEGORY.md +21 -0
- package/dist/audit/catalog/categories/best-practices/rules/commerce-api-client.md +20 -0
- package/dist/audit/catalog/categories/best-practices/rules/deferred-ui.js +575 -0
- package/dist/audit/catalog/categories/best-practices/rules/deferred-ui.md +20 -0
- package/dist/audit/catalog/categories/best-practices/rules/error-boundaries.md +19 -0
- package/dist/audit/catalog/categories/best-practices/rules/production-integrations.md +20 -0
- package/dist/audit/catalog/categories/best-practices/rules/route-revalidation.md +19 -0
- package/dist/audit/catalog/categories/best-practices/rules/server-data-loading.md +19 -0
- package/dist/audit/catalog/categories/performance/CATEGORY.md +18 -0
- package/dist/audit/catalog/categories/performance/rules/app-config-size.js +15 -0
- package/dist/audit/catalog/categories/performance/rules/app-config-size.md +22 -0
- package/dist/audit/catalog/categories/performance/rules/cache-control.js +59 -0
- package/dist/audit/catalog/categories/performance/rules/cache-control.md +20 -0
- package/dist/audit/catalog/categories/performance/rules/cls-budget.js +13 -0
- package/dist/audit/catalog/categories/performance/rules/cls-budget.md +20 -0
- package/dist/audit/catalog/categories/performance/rules/content-encoding.js +35 -0
- package/dist/audit/catalog/categories/performance/rules/content-encoding.md +20 -0
- package/dist/audit/catalog/categories/performance/rules/dom-size.js +18 -0
- package/dist/audit/catalog/categories/performance/rules/dom-size.md +22 -0
- package/dist/audit/catalog/categories/performance/rules/font-display.js +248 -0
- package/dist/audit/catalog/categories/performance/rules/font-display.md +21 -0
- package/dist/audit/catalog/categories/performance/rules/html-document-size.js +15 -0
- package/dist/audit/catalog/categories/performance/rules/html-document-size.md +20 -0
- package/dist/audit/catalog/categories/performance/rules/image-dimensions.js +141 -0
- package/dist/audit/catalog/categories/performance/rules/image-dimensions.md +22 -0
- package/dist/audit/catalog/categories/performance/rules/image-transformation.js +117 -0
- package/dist/audit/catalog/categories/performance/rules/image-transformation.md +22 -0
- package/dist/audit/catalog/categories/performance/rules/javascript-size.js +126 -0
- package/dist/audit/catalog/categories/performance/rules/javascript-size.md +20 -0
- package/dist/audit/catalog/categories/performance/rules/large-images.js +187 -0
- package/dist/audit/catalog/categories/performance/rules/large-images.md +22 -0
- package/dist/audit/catalog/categories/performance/rules/lcp-budget.js +13 -0
- package/dist/audit/catalog/categories/performance/rules/lcp-budget.md +20 -0
- package/dist/audit/catalog/categories/performance/rules/lcp-image-priority.js +227 -0
- package/dist/audit/catalog/categories/performance/rules/lcp-image-priority.md +27 -0
- package/dist/audit/catalog/categories/performance/rules/page-data-size.js +15 -0
- package/dist/audit/catalog/categories/performance/rules/page-data-size.md +22 -0
- package/dist/audit/catalog/categories/performance/rules/reduce-javascript-on-load.js +97 -0
- package/dist/audit/catalog/categories/performance/rules/reduce-javascript-on-load.md +73 -0
- package/dist/audit/catalog/categories/performance/rules/render-blocking-font-size.js +201 -0
- package/dist/audit/catalog/categories/performance/rules/render-blocking-font-size.md +20 -0
- package/dist/audit/catalog/categories/performance/rules/server-request-waterfall.md +85 -0
- package/dist/audit/catalog/categories/performance/rules/tbt-budget.js +13 -0
- package/dist/audit/catalog/categories/performance/rules/tbt-budget.md +20 -0
- package/dist/audit/catalog/categories/performance/rules/third-party-fonts.js +265 -0
- package/dist/audit/catalog/categories/performance/rules/third-party-fonts.md +20 -0
- package/dist/audit/catalog/categories/performance/rules/ttfb-budget.js +17 -0
- package/dist/audit/catalog/categories/performance/rules/ttfb-budget.md +20 -0
- package/dist/audit/catalog/categories/security/CATEGORY.md +17 -0
- package/dist/audit/catalog/categories/security/rules/browser-hardening-headers.js +37 -0
- package/dist/audit/catalog/categories/security/rules/browser-hardening-headers.md +20 -0
- package/dist/audit/catalog/categories/security/rules/content-security-policy.js +40 -0
- package/dist/audit/catalog/categories/security/rules/content-security-policy.md +20 -0
- package/dist/audit/catalog/categories/security/rules/csp-runtime-compatibility.md +24 -0
- package/dist/audit/catalog/categories/security/rules/dependency-vulnerabilities.md +19 -0
- package/dist/audit/catalog/categories/security/rules/dynamic-code-execution.md +19 -0
- package/dist/audit/catalog/categories/security/rules/hardcoded-credentials.md +19 -0
- package/dist/audit/catalog/categories/security/rules/html-config-secrets.js +46 -0
- package/dist/audit/catalog/categories/security/rules/html-config-secrets.md +22 -0
- package/dist/audit/catalog/categories/security/rules/protected-route-authorization.md +19 -0
- package/dist/audit/catalog/categories/security/rules/public-client-secrets.md +20 -0
- package/dist/audit/catalog/categories/security/rules/raw-html-sinks.md +20 -0
- package/dist/audit/catalog/categories/security/rules/strict-transport-security.js +183 -0
- package/dist/audit/catalog/categories/security/rules/strict-transport-security.md +24 -0
- package/dist/audit/catalog/categories/seo/CATEGORY.md +17 -0
- package/dist/audit/catalog/categories/seo/rules/canonical-url.md +20 -0
- package/dist/audit/catalog/categories/seo/rules/locale-alternates.md +19 -0
- package/dist/audit/catalog/categories/seo/rules/meta-description.md +19 -0
- package/dist/audit/catalog/categories/seo/rules/page-title.md +19 -0
- package/dist/audit/catalog/categories/seo/rules/robots-indexing.md +20 -0
- package/dist/audit/catalog/categories/seo/rules/structured-data.md +19 -0
- package/dist/audit/instructions/evaluate-rule.md +42 -0
- package/dist/audit/schemas/audit-result.schema.json +1237 -0
- package/dist/audit/ui/assets/banner-brand-default-CDRB3_S0.png +0 -0
- package/dist/audit/ui/assets/banner-group-public-default-BGRJzVY8.png +0 -0
- package/dist/audit/ui/assets/banner-user-default-BW81Ba3z.png +0 -0
- package/dist/audit/ui/assets/bg-info@2x-CzpP0vaE.png +0 -0
- package/dist/audit/ui/assets/einstein-figure-B1xFOo8O.svg +1 -0
- package/dist/audit/ui/assets/group_avatar_160-CrxVHlYA.png +0 -0
- package/dist/audit/ui/assets/group_avatar_200-CSLS8I0r.png +0 -0
- package/dist/audit/ui/assets/index-CtE_JUcs.css +1 -0
- package/dist/audit/ui/assets/index-aSdbHIeq.js +44 -0
- package/dist/audit/ui/assets/profile_avatar_160-CtQTUBo0.png +0 -0
- package/dist/audit/ui/assets/profile_avatar_200-DAztxshF.png +0 -0
- package/dist/audit/ui/assets/symbols-BRmpfDbO.svg +1 -0
- package/dist/audit/ui/assets/symbols-D0Ne6690.svg +1 -0
- package/dist/audit/ui/einstein.png +0 -0
- package/dist/audit/ui/index.html +13 -0
- package/dist/audit.js +1898 -0
- package/dist/cartridge-services/index.d.ts +5 -0
- package/dist/cartridge-services/index.d.ts.map +1 -1
- package/dist/cartridge-services/index.js +94 -7
- package/dist/cartridge-services/index.js.map +1 -1
- package/dist/catalog.js +89 -0
- package/dist/commands/audit/catalog.js +27 -0
- package/dist/commands/audit/finalize.js +42 -0
- package/dist/commands/audit/prepare.js +84 -0
- package/dist/commands/audit/progress.js +51 -0
- package/dist/commands/audit/serve.js +63 -0
- package/dist/commands/config/aggregate-extensions.js +1 -0
- package/dist/commands/create-bundle.js +2 -1
- package/dist/commands/create-instructions.js +1 -0
- package/dist/commands/create-storefront.js +1 -0
- package/dist/commands/deploy-cartridge.js +35 -11
- package/dist/commands/dev.js +2 -0
- package/dist/commands/extensions/create.js +1 -0
- package/dist/commands/extensions/install.js +1 -0
- package/dist/commands/extensions/list.js +1 -0
- package/dist/commands/extensions/remove.js +1 -0
- package/dist/commands/generate-cartridge.js +49 -5
- package/dist/commands/locales/aggregate-extensions.js +1 -0
- package/dist/commands/prepare-local.js +1 -0
- package/dist/commands/preview.js +2 -0
- package/dist/commands/push.js +37 -7
- package/dist/commands/validate-cartridge.js +13 -4
- package/dist/config.js +13 -2
- package/dist/document-size.js +231 -0
- package/dist/events.js +688 -0
- package/dist/flags2.js +34 -0
- package/dist/generate-cartridge.js +41 -6
- package/dist/har.js +205 -0
- package/dist/images.js +146 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/live.js +277 -0
- package/dist/logger.js +1 -4
- package/dist/measured-run.js +91 -0
- package/dist/metric-budget.js +125 -0
- package/dist/mrt/ssr.mjs +50 -50
- package/dist/mrt/ssr.mjs.map +1 -1
- package/dist/mrt/streamingHandler.mjs +56 -56
- package/dist/mrt/streamingHandler.mjs.map +1 -1
- package/dist/package.js +5 -0
- package/dist/sanitize.js +185 -0
- package/dist/schema.js +121 -0
- package/dist/server.js +8 -60
- package/dist/server2.js +185 -0
- package/dist/ts-import.js +57 -0
- package/package.json +24 -6
package/dist/flags2.js
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { n as PROJECT_DIRECTORY_FLAG } from "./flags.js";
|
|
2
|
+
import { n as sanitizeText } from "./sanitize.js";
|
|
3
|
+
import { Command, Errors, Flags } from "@oclif/core";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
|
|
6
|
+
//#region src/audit/command.ts
|
|
7
|
+
/** Base for the audit commands: an error's message is sanitized before oclif prints it, so a token in a `--url` stays out of the terminal. */
|
|
8
|
+
var AuditCommand = class extends Command {
|
|
9
|
+
async catch(error) {
|
|
10
|
+
error.message = sanitizeText(error.message);
|
|
11
|
+
return super.catch(error);
|
|
12
|
+
}
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
//#endregion
|
|
16
|
+
//#region src/audit/flags.ts
|
|
17
|
+
/** A string flag that rejects an empty value (`--workspace ""` or `--workspace=`) instead of treating it as the
|
|
18
|
+
* current directory. Like oclif's own parse errors, it exits 2. */
|
|
19
|
+
const text = Flags.custom({ parse(value) {
|
|
20
|
+
return value ? Promise.resolve(value) : Promise.reject(new Errors.CLIError("needs a value", { exit: 2 }));
|
|
21
|
+
} });
|
|
22
|
+
/** `--workspace`, for the verbs that act on an existing workspace. */
|
|
23
|
+
const workspaceFlag = (description, options = {}) => text({
|
|
24
|
+
...options,
|
|
25
|
+
helpValue: "<dir>",
|
|
26
|
+
description: `${description} (default: <project-directory>/.site-audit)`
|
|
27
|
+
});
|
|
28
|
+
/** The audit workspace: `--workspace`, else `.site-audit` in the project directory. */
|
|
29
|
+
function workspaceFor(flags) {
|
|
30
|
+
return path.resolve(flags.workspace ?? path.join(flags[PROJECT_DIRECTORY_FLAG], ".site-audit"));
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
//#endregion
|
|
34
|
+
export { AuditCommand as i, workspaceFlag as n, workspaceFor as r, text as t };
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { t as logger } from "./logger.js";
|
|
2
2
|
import { t as formatDirectoryWithProjectBiome } from "./format-with-project-biome.js";
|
|
3
|
+
import { t as importTypescript } from "./ts-import.js";
|
|
3
4
|
import { existsSync, readFileSync, unlinkSync } from "node:fs";
|
|
4
5
|
import { basename, extname, join, resolve } from "node:path";
|
|
5
6
|
import { access, mkdir, readFile, readdir, rm, writeFile } from "node:fs/promises";
|
|
@@ -10,6 +11,14 @@ import { randomUUID } from "node:crypto";
|
|
|
10
11
|
import { npmRunPathEnv } from "npm-run-path";
|
|
11
12
|
|
|
12
13
|
//#region src/cartridge-services/react-router-config.ts
|
|
14
|
+
const DEFAULT_PAGE_DESIGNER_PREFIXES = {
|
|
15
|
+
product: "p",
|
|
16
|
+
category: "c"
|
|
17
|
+
};
|
|
18
|
+
const PAGE_DESIGNER_PARAMETER_NAMES = {
|
|
19
|
+
product: "productId",
|
|
20
|
+
category: "categoryId"
|
|
21
|
+
};
|
|
13
22
|
let isCliAvailable = null;
|
|
14
23
|
function filePathEndsWithRoute(filePath, routeFile) {
|
|
15
24
|
const routeFileNormalized = routeFile.replace(/^\.\//, "");
|
|
@@ -78,18 +87,26 @@ function getReactRouterRoutes(projectDirectory) {
|
|
|
78
87
|
}
|
|
79
88
|
/**
|
|
80
89
|
* Convert a file path to its corresponding route path using React Router's CLI.
|
|
81
|
-
*
|
|
90
|
+
* PDP/PLP paths use configured prefixes and named ID parameters for Page Designer.
|
|
82
91
|
* @param filePath - Absolute path to the route file
|
|
83
92
|
* @param projectRoot - The project root directory
|
|
84
|
-
* @
|
|
93
|
+
* @param seoPrefixes - Optional selected site's PDP/PLP prefixes. Defaults to p/c.
|
|
94
|
+
* @returns The computed Page Designer route path (e.g., '/cart', '/p/:productId')
|
|
85
95
|
* @example
|
|
86
96
|
* const route = filePathToRoute('/path/to/project/src/routes/_app.cart.tsx', '/path/to/project');
|
|
87
97
|
* // Returns: '/cart'
|
|
88
98
|
*/
|
|
89
|
-
function filePathToRoute(filePath, projectRoot) {
|
|
99
|
+
function filePathToRoute(filePath, projectRoot, seoPrefixes) {
|
|
90
100
|
const filePathPosix = filePath.replace(/\\/g, "/");
|
|
91
101
|
const canonicalRoutes = flattenRoutes(getReactRouterRoutes(projectRoot)).filter((route) => !route.id.endsWith("--root-duplicate"));
|
|
92
102
|
for (const route of canonicalRoutes) if (routeFileMatches(filePathPosix, route.file.replace(/\\/g, "/"))) {
|
|
103
|
+
const routeFileName = route.id.split("/").at(-1);
|
|
104
|
+
const seoResource = routeFileName === "_app.p.$" ? "product" : routeFileName === "_app.c.$" ? "category" : void 0;
|
|
105
|
+
if (seoResource) {
|
|
106
|
+
const configuredPrefix = seoPrefixes?.[seoResource] ?? DEFAULT_PAGE_DESIGNER_PREFIXES[seoResource];
|
|
107
|
+
const pathSegments = (route.path ?? "").split("/").filter(Boolean);
|
|
108
|
+
return `${`/${(pathSegments.at(-1) === "*" ? pathSegments.slice(0, -2) : pathSegments).join("/")}`.replace(/\/$/, "")}/${configuredPrefix}/:${PAGE_DESIGNER_PARAMETER_NAMES[seoResource]}`.replace(/\/+/g, "/");
|
|
109
|
+
}
|
|
93
110
|
const seoAliases = canonicalRoutes.filter((candidate) => candidate.id.startsWith(`${route.id}--seo-alias--`)).sort((left, right) => left.path.localeCompare(right.path));
|
|
94
111
|
if (seoAliases.length > 0) {
|
|
95
112
|
if (seoAliases.length > 1) logger.warn(`Multiple SEO route aliases match ${filePath}; using ${seoAliases[0].path} for Page Designer metadata.`);
|
|
@@ -485,7 +502,7 @@ async function processComponentFile(filePath, _projectRoot) {
|
|
|
485
502
|
return [];
|
|
486
503
|
}
|
|
487
504
|
}
|
|
488
|
-
async function processPageTypeFile(filePath, projectRoot) {
|
|
505
|
+
async function processPageTypeFile(filePath, projectRoot, seoPrefixes) {
|
|
489
506
|
try {
|
|
490
507
|
const content = await readFile(filePath, "utf-8");
|
|
491
508
|
const pageTypes = [];
|
|
@@ -504,7 +521,7 @@ async function processPageTypeFile(filePath, projectRoot) {
|
|
|
504
521
|
const pageTypeConfig = parseDecoratorArgs(pageTypeDecorator);
|
|
505
522
|
const attributes = extractAttributesFromSource(sourceFile, className);
|
|
506
523
|
const regionDefinitions = extractRegionDefinitionsFromSource(sourceFile, className);
|
|
507
|
-
const route = filePathToRoute(filePath, projectRoot);
|
|
524
|
+
const route = filePathToRoute(filePath, projectRoot, seoPrefixes);
|
|
508
525
|
const pageTypeMetadata = {
|
|
509
526
|
typeId: pageTypeConfig.id || className.toLowerCase(),
|
|
510
527
|
name: pageTypeConfig.name || toHumanReadableName(className),
|
|
@@ -632,6 +649,23 @@ function lintGeneratedFiles(metadataDir) {
|
|
|
632
649
|
logger.debug("🔧 Formatting generated JSON files with Biome...");
|
|
633
650
|
formatDirectoryWithProjectBiome(metadataDir);
|
|
634
651
|
}
|
|
652
|
+
function isCanonicalProductOrCategoryRouteFile(filePath, projectRoot) {
|
|
653
|
+
const relativePath = filePath.slice(projectRoot.length).replace(/\\/g, "/").replace(/^\//, "");
|
|
654
|
+
return /^src\/(?:.*\/)?routes\/_app\.(?:p|c)\.\$\.tsx$/.test(relativePath);
|
|
655
|
+
}
|
|
656
|
+
async function loadPageDesignerSeoPrefixes(projectRoot, requestedSiteId) {
|
|
657
|
+
const configPath = join(projectRoot, "config.server.ts");
|
|
658
|
+
if (existsSync(configPath) !== true) return void 0;
|
|
659
|
+
const app = (await importTypescript(configPath, { projectDirectory: projectRoot })).default?.app;
|
|
660
|
+
const seoRoutes = (app?.url)?.seoRoutes;
|
|
661
|
+
if (seoRoutes === void 0) return void 0;
|
|
662
|
+
const siteId = requestedSiteId ?? (typeof app?.defaultSiteId === "string" ? app.defaultSiteId : void 0);
|
|
663
|
+
const selected = siteId && Object.prototype.hasOwnProperty.call(seoRoutes, siteId) ? seoRoutes[siteId] : seoRoutes["*"];
|
|
664
|
+
return {
|
|
665
|
+
product: selected?.product?.prefix,
|
|
666
|
+
category: selected?.category?.prefix
|
|
667
|
+
};
|
|
668
|
+
}
|
|
635
669
|
async function generateMetadata(projectDirectory, metadataDirectory, options) {
|
|
636
670
|
try {
|
|
637
671
|
const filePaths = options?.filePaths;
|
|
@@ -706,13 +740,14 @@ async function generateMetadata(projectDirectory, metadataDirectory, options) {
|
|
|
706
740
|
else logger.warn(` - Unable to access ${configMetadataDir}:`, error.message);
|
|
707
741
|
}
|
|
708
742
|
}
|
|
743
|
+
const seoPrefixes = files.some((file) => isCanonicalProductOrCategoryRouteFile(file, projectRoot)) ? await loadPageDesignerSeoPrefixes(projectRoot, options?.siteId) : void 0;
|
|
709
744
|
const allComponents = [];
|
|
710
745
|
const allPageTypes = [];
|
|
711
746
|
const allAspects = [];
|
|
712
747
|
for (const file of files) {
|
|
713
748
|
const components = await processComponentFile(file, projectRoot);
|
|
714
749
|
allComponents.push(...components);
|
|
715
|
-
const pageTypes = await processPageTypeFile(file, projectRoot);
|
|
750
|
+
const pageTypes = await processPageTypeFile(file, projectRoot, seoPrefixes);
|
|
716
751
|
allPageTypes.push(...pageTypes);
|
|
717
752
|
const aspects = await processAspectFile(file, projectRoot);
|
|
718
753
|
allAspects.push(...aspects);
|
package/dist/har.js
ADDED
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
|
|
3
|
+
//#region src/audit/rule-helpers/har.ts
|
|
4
|
+
const headerSchema = z.object({
|
|
5
|
+
name: z.string(),
|
|
6
|
+
value: z.string()
|
|
7
|
+
}).passthrough();
|
|
8
|
+
const entrySchema = z.object({
|
|
9
|
+
request: z.object({ url: z.string() }).passthrough(),
|
|
10
|
+
response: z.object({
|
|
11
|
+
status: z.number(),
|
|
12
|
+
headers: z.array(headerSchema),
|
|
13
|
+
content: z.object({
|
|
14
|
+
size: z.number().optional(),
|
|
15
|
+
mimeType: z.string().optional()
|
|
16
|
+
}).passthrough().optional()
|
|
17
|
+
}).passthrough()
|
|
18
|
+
}).passthrough();
|
|
19
|
+
const harSchema = z.object({ log: z.object({ entries: z.array(entrySchema) }).passthrough() }).passthrough();
|
|
20
|
+
const networkSchema = z.object({ document: z.object({ url: z.string() }).passthrough() }).passthrough();
|
|
21
|
+
/** Header rules read the same cell as the metric budgets. */
|
|
22
|
+
const REPRESENTATIVE_CELL = "mobile-cold";
|
|
23
|
+
function headerValues(entry, name) {
|
|
24
|
+
const wanted = name.toLowerCase();
|
|
25
|
+
return entry.response.headers.flatMap((item, index) => item.name.toLowerCase() === wanted ? [{
|
|
26
|
+
value: item.value,
|
|
27
|
+
index
|
|
28
|
+
}] : []);
|
|
29
|
+
}
|
|
30
|
+
/** A header's combined value (repeated headers joined with ", ", as HTTP defines), or null when absent. */
|
|
31
|
+
function header(entry, name) {
|
|
32
|
+
const values = headerValues(entry, name);
|
|
33
|
+
return values.length ? values.map((item) => item.value).join(", ") : null;
|
|
34
|
+
}
|
|
35
|
+
/** Comma-separated header tokens, lowercased. */
|
|
36
|
+
function tokens(value) {
|
|
37
|
+
return (value ?? "").split(",").map((item) => item.trim().toLowerCase()).filter(Boolean);
|
|
38
|
+
}
|
|
39
|
+
function normalizedUrl(value) {
|
|
40
|
+
try {
|
|
41
|
+
const url = new URL(value);
|
|
42
|
+
url.hash = "";
|
|
43
|
+
return url.toString();
|
|
44
|
+
} catch {
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
/** Finds the final document response of one Playhouse cell in its HAR, using `network-<cell>.json` for the document URL. */
|
|
49
|
+
async function matchDocument(playhouse, page, cell) {
|
|
50
|
+
const harArtifact = `network-${cell}.har`;
|
|
51
|
+
const [har, network] = await Promise.all([playhouse.readJson(page, harArtifact, harSchema), playhouse.readJson(page, `network-${cell}.json`, networkSchema)]);
|
|
52
|
+
const finalUrl = network?.document.url ?? null;
|
|
53
|
+
if (!har || !finalUrl) return {
|
|
54
|
+
harArtifact,
|
|
55
|
+
finalUrl,
|
|
56
|
+
reason: "The HAR or final document identity was unavailable."
|
|
57
|
+
};
|
|
58
|
+
const expected = normalizedUrl(finalUrl);
|
|
59
|
+
const index = har.log.entries.map((entry) => normalizedUrl(entry.request.url)).lastIndexOf(expected);
|
|
60
|
+
if (index < 0) return {
|
|
61
|
+
harArtifact,
|
|
62
|
+
finalUrl,
|
|
63
|
+
reason: "The final document response could not be matched in the HAR."
|
|
64
|
+
};
|
|
65
|
+
return {
|
|
66
|
+
harArtifact,
|
|
67
|
+
finalUrl,
|
|
68
|
+
entries: har.log.entries,
|
|
69
|
+
document: {
|
|
70
|
+
entry: har.log.entries[index],
|
|
71
|
+
index
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
/** Successful (200) responses served from the document's own host; third-party responses are not graded. */
|
|
76
|
+
function sameHostResponses(match) {
|
|
77
|
+
const host = new URL(match.finalUrl).host;
|
|
78
|
+
return match.entries.flatMap((entry, index) => {
|
|
79
|
+
try {
|
|
80
|
+
return entry.response.status === 200 && new URL(entry.request.url).host === host ? [{
|
|
81
|
+
entry,
|
|
82
|
+
index
|
|
83
|
+
}] : [];
|
|
84
|
+
} catch {
|
|
85
|
+
return [];
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
function resourceKind(entry) {
|
|
90
|
+
const type = (entry.response.content?.mimeType || header(entry, "content-type") || "").split(";")[0].trim().toLowerCase();
|
|
91
|
+
if (type === "text/html" || type === "application/xhtml+xml") return "html";
|
|
92
|
+
if (type.includes("javascript") || type.includes("ecmascript")) return "js";
|
|
93
|
+
if (type === "text/css") return "css";
|
|
94
|
+
if (type.startsWith("image/")) return "image";
|
|
95
|
+
if (type.startsWith("font/") || type.includes("font-")) return "font";
|
|
96
|
+
return "other";
|
|
97
|
+
}
|
|
98
|
+
/** `Cache-Control` directives, lowercased, with their values (`max-age=60` → `max-age` → `60`). */
|
|
99
|
+
function cacheControl(entry) {
|
|
100
|
+
return new Map(tokens(header(entry, "cache-control")).map((item) => {
|
|
101
|
+
const [name, ...value] = item.split("=");
|
|
102
|
+
return [name.trim(), value.join("=").trim().replace(/^"|"$/g, "")];
|
|
103
|
+
}));
|
|
104
|
+
}
|
|
105
|
+
const COMPRESSED_ENCODINGS = [
|
|
106
|
+
"br",
|
|
107
|
+
"gzip",
|
|
108
|
+
"zstd"
|
|
109
|
+
];
|
|
110
|
+
const harRuleDataSchema = z.object({
|
|
111
|
+
cell: z.string(),
|
|
112
|
+
checked: z.array(z.object({
|
|
113
|
+
page: z.string(),
|
|
114
|
+
url: z.string(),
|
|
115
|
+
responses: z.number()
|
|
116
|
+
}).strict()),
|
|
117
|
+
failures: z.array(z.object({
|
|
118
|
+
page: z.string(),
|
|
119
|
+
url: z.string(),
|
|
120
|
+
entry: z.number(),
|
|
121
|
+
reasons: z.array(z.string()),
|
|
122
|
+
headers: z.record(z.string(), z.string().nullable())
|
|
123
|
+
}).strict()),
|
|
124
|
+
skipped: z.array(z.object({
|
|
125
|
+
page: z.string(),
|
|
126
|
+
reason: z.string()
|
|
127
|
+
}).strict())
|
|
128
|
+
}).strict();
|
|
129
|
+
const MAX_FAILURE_EVIDENCE = 10;
|
|
130
|
+
/**
|
|
131
|
+
* A rule script that runs `check` on each page's representative HAR. It fails when any response fails, passes when every
|
|
132
|
+
* checked response passes, and leaves the rule undecided when no page could be checked or no response was eligible.
|
|
133
|
+
* `passed` completes the pass summary ("All 12 <subject> checked on home <passed>.").
|
|
134
|
+
*/
|
|
135
|
+
function harRule(subject, check, passed = "passed") {
|
|
136
|
+
return async ({ pages, playhouse }) => {
|
|
137
|
+
const data = {
|
|
138
|
+
cell: REPRESENTATIVE_CELL,
|
|
139
|
+
checked: [],
|
|
140
|
+
failures: [],
|
|
141
|
+
skipped: []
|
|
142
|
+
};
|
|
143
|
+
const documents = [];
|
|
144
|
+
for (const page of pages) {
|
|
145
|
+
const match = await matchDocument(playhouse, page.label, REPRESENTATIVE_CELL);
|
|
146
|
+
if ("reason" in match) {
|
|
147
|
+
data.skipped.push({
|
|
148
|
+
page: page.label,
|
|
149
|
+
reason: match.reason
|
|
150
|
+
});
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
const result = check(match);
|
|
154
|
+
data.checked.push({
|
|
155
|
+
page: page.label,
|
|
156
|
+
url: match.finalUrl,
|
|
157
|
+
responses: result.responses
|
|
158
|
+
});
|
|
159
|
+
data.failures.push(...result.failures.map((failure) => ({
|
|
160
|
+
page: page.label,
|
|
161
|
+
url: failure.entry.entry.request.url,
|
|
162
|
+
entry: failure.entry.index,
|
|
163
|
+
reasons: failure.reasons,
|
|
164
|
+
headers: failure.headers
|
|
165
|
+
})));
|
|
166
|
+
documents.push({
|
|
167
|
+
type: "playhouse",
|
|
168
|
+
page: page.label,
|
|
169
|
+
artifact: match.harArtifact,
|
|
170
|
+
pointer: `/log/entries/${match.document.index}/request/url`,
|
|
171
|
+
value: match.finalUrl
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
const responses = data.checked.reduce((total, item) => total + item.responses, 0);
|
|
175
|
+
if (!responses) return { data };
|
|
176
|
+
const pageList = data.checked.map((item) => item.page).join(", ");
|
|
177
|
+
if (!data.failures.length) return {
|
|
178
|
+
data,
|
|
179
|
+
decision: {
|
|
180
|
+
status: "pass",
|
|
181
|
+
summary: `All ${responses} ${subject} checked on ${pageList} ${passed}.`,
|
|
182
|
+
evidence: documents
|
|
183
|
+
}
|
|
184
|
+
};
|
|
185
|
+
const harArtifact = `network-${REPRESENTATIVE_CELL}.har`;
|
|
186
|
+
const reasons = [...new Set(data.failures.flatMap((failure) => failure.reasons))];
|
|
187
|
+
return {
|
|
188
|
+
data,
|
|
189
|
+
decision: {
|
|
190
|
+
status: "fail",
|
|
191
|
+
summary: `${data.failures.length} of ${responses} ${subject} failed: ${reasons.slice(0, 3).join(" ")}${reasons.length > 3 ? ` (+${reasons.length - 3} more)` : ""}`,
|
|
192
|
+
evidence: data.failures.slice(0, MAX_FAILURE_EVIDENCE).map((failure) => ({
|
|
193
|
+
type: "playhouse",
|
|
194
|
+
page: failure.page,
|
|
195
|
+
artifact: harArtifact,
|
|
196
|
+
pointer: `/log/entries/${failure.entry}/response/headers`,
|
|
197
|
+
value: `${failure.url}: ${failure.reasons.join(" ")}`
|
|
198
|
+
}))
|
|
199
|
+
}
|
|
200
|
+
};
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
//#endregion
|
|
205
|
+
export { harRuleDataSchema as a, matchDocument as c, sameHostResponses as d, tokens as f, harRule as i, normalizedUrl as l, REPRESENTATIVE_CELL as n, header as o, cacheControl as r, headerValues as s, COMPRESSED_ENCODINGS as t, resourceKind as u };
|
package/dist/images.js
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
|
|
3
|
+
//#region src/audit/rule-helpers/images.ts
|
|
4
|
+
/**
|
|
5
|
+
* Image rules read the mobile cold-cache median run: its own HAR and Lighthouse result, so request timings line up with
|
|
6
|
+
* the LCP that `summary-lcp.json` reports for that run.
|
|
7
|
+
*/
|
|
8
|
+
const CELL = {
|
|
9
|
+
formFactor: "mobile",
|
|
10
|
+
cacheState: "cold"
|
|
11
|
+
};
|
|
12
|
+
/** Savings smaller than this aren't worth a suggestion. */
|
|
13
|
+
const MIN_SAVINGS_BYTES = 10 * 1024;
|
|
14
|
+
const lcpSummarySchema = z.object({ cells: z.array(z.object({
|
|
15
|
+
formFactor: z.string(),
|
|
16
|
+
cacheState: z.string(),
|
|
17
|
+
median: z.object({
|
|
18
|
+
runId: z.string(),
|
|
19
|
+
lcp: z.number().nullable().optional(),
|
|
20
|
+
breakdown: z.record(z.string(), z.number().nullable()).nullable().optional(),
|
|
21
|
+
resource: z.object({ url: z.string() }).passthrough().nullable().optional()
|
|
22
|
+
}).passthrough().nullable().optional()
|
|
23
|
+
}).passthrough()) }).passthrough();
|
|
24
|
+
const entrySchema = z.object({
|
|
25
|
+
_requestTime: z.number().optional(),
|
|
26
|
+
_priority: z.string().optional(),
|
|
27
|
+
_initialPriority: z.string().optional(),
|
|
28
|
+
_resourceType: z.string().optional(),
|
|
29
|
+
_initiator_type: z.string().optional(),
|
|
30
|
+
_renderBlocking: z.string().optional(),
|
|
31
|
+
time: z.number(),
|
|
32
|
+
request: z.object({
|
|
33
|
+
url: z.string(),
|
|
34
|
+
_isLinkPreload: z.boolean().optional()
|
|
35
|
+
}).passthrough(),
|
|
36
|
+
response: z.object({
|
|
37
|
+
status: z.number(),
|
|
38
|
+
bodySize: z.number().optional(),
|
|
39
|
+
content: z.object({
|
|
40
|
+
size: z.number().optional(),
|
|
41
|
+
mimeType: z.string().optional()
|
|
42
|
+
}).passthrough().optional()
|
|
43
|
+
}).passthrough()
|
|
44
|
+
}).passthrough();
|
|
45
|
+
const harSchema = z.object({ log: z.object({ entries: z.array(entrySchema) }).passthrough() }).passthrough();
|
|
46
|
+
const rectSchema = z.object({
|
|
47
|
+
top: z.number(),
|
|
48
|
+
width: z.number(),
|
|
49
|
+
height: z.number()
|
|
50
|
+
}).passthrough();
|
|
51
|
+
const nodeSchema = z.object({
|
|
52
|
+
path: z.string().optional(),
|
|
53
|
+
selector: z.string().optional(),
|
|
54
|
+
nodeLabel: z.string().optional(),
|
|
55
|
+
snippet: z.string().optional(),
|
|
56
|
+
boundingRect: rectSchema.optional()
|
|
57
|
+
}).passthrough();
|
|
58
|
+
const tableItem = z.record(z.string(), z.unknown());
|
|
59
|
+
const audit = (details) => z.object({
|
|
60
|
+
score: z.number().nullable().optional(),
|
|
61
|
+
numericValue: z.number().optional(),
|
|
62
|
+
metricSavings: z.record(z.string(), z.number()).optional(),
|
|
63
|
+
details: details.optional()
|
|
64
|
+
}).passthrough().optional();
|
|
65
|
+
const lhrSchema = z.object({
|
|
66
|
+
configSettings: z.object({ screenEmulation: z.object({
|
|
67
|
+
deviceScaleFactor: z.number(),
|
|
68
|
+
height: z.number()
|
|
69
|
+
}).passthrough() }).passthrough(),
|
|
70
|
+
audits: z.object({
|
|
71
|
+
"image-delivery-insight": audit(z.object({ items: z.array(z.object({
|
|
72
|
+
url: z.string(),
|
|
73
|
+
totalBytes: z.number(),
|
|
74
|
+
wastedBytes: z.number(),
|
|
75
|
+
node: nodeSchema.optional(),
|
|
76
|
+
subItems: z.object({ items: z.array(z.object({
|
|
77
|
+
reason: z.string(),
|
|
78
|
+
wastedBytes: z.number()
|
|
79
|
+
}).passthrough()) }).passthrough().optional()
|
|
80
|
+
}).passthrough()).optional() }).passthrough()),
|
|
81
|
+
"lcp-discovery-insight": audit(z.object({ items: z.array(tableItem).optional() }).passthrough()),
|
|
82
|
+
"unsized-images": audit(z.object({ items: z.array(z.object({
|
|
83
|
+
url: z.string().optional(),
|
|
84
|
+
node: nodeSchema.optional()
|
|
85
|
+
}).passthrough()).optional() }).passthrough()),
|
|
86
|
+
"cls-culprits-insight": audit(z.object({ items: z.array(tableItem).optional() }).passthrough()),
|
|
87
|
+
"render-blocking-insight": audit(z.object({ items: z.array(z.object({ url: z.string() }).passthrough()).optional() }).passthrough()),
|
|
88
|
+
"cumulative-layout-shift": audit(z.unknown())
|
|
89
|
+
}).passthrough()
|
|
90
|
+
}).passthrough();
|
|
91
|
+
/** The mobile cold-cache median run of one page, or why it can't be read. */
|
|
92
|
+
async function readImageRun(playhouse, page) {
|
|
93
|
+
const median = (await playhouse.readJson(page, "summary-lcp.json", lcpSummarySchema))?.cells.find((cell) => cell.formFactor === CELL.formFactor && cell.cacheState === CELL.cacheState)?.median;
|
|
94
|
+
if (!median) return { reason: "summary-lcp.json has no mobile cold-cache median run." };
|
|
95
|
+
const harArtifact = `runs/${median.runId}/network.har`;
|
|
96
|
+
const lighthouseArtifact = `runs/${median.runId}/lhr.json`;
|
|
97
|
+
const [har, lhr] = await Promise.all([playhouse.readJson(page, harArtifact, harSchema), playhouse.readJson(page, lighthouseArtifact, lhrSchema)]);
|
|
98
|
+
if (!har || !lhr) return { reason: `The median run's ${har ? "Lighthouse result" : "HAR"} was not archived.` };
|
|
99
|
+
const entries = har.log.entries;
|
|
100
|
+
const origin = Math.min(...entries.map((entry) => entry._requestTime ?? Infinity));
|
|
101
|
+
return {
|
|
102
|
+
page,
|
|
103
|
+
runId: median.runId,
|
|
104
|
+
harArtifact,
|
|
105
|
+
lighthouseArtifact,
|
|
106
|
+
entries,
|
|
107
|
+
lhr,
|
|
108
|
+
dpr: lhr.configSettings.screenEmulation.deviceScaleFactor,
|
|
109
|
+
viewportHeight: lhr.configSettings.screenEmulation.height,
|
|
110
|
+
lcpMs: median.lcp ?? null,
|
|
111
|
+
lcpBreakdown: median.breakdown ?? {},
|
|
112
|
+
lcpUrl: median.resource?.url ?? null,
|
|
113
|
+
startMs: (entry) => entry._requestTime === void 0 || !Number.isFinite(origin) ? NaN : Math.round((entry._requestTime - origin) * 1e3)
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
const kib = (bytes) => `${Math.round(bytes / 1024)} KiB`;
|
|
117
|
+
/** Bytes an entry downloaded during [start, end] seconds, assuming it downloaded at an even rate. */
|
|
118
|
+
function bytesDuring(entry, start, end) {
|
|
119
|
+
const from = entry._requestTime;
|
|
120
|
+
const bytes = entry.response.bodySize ?? 0;
|
|
121
|
+
if (from === void 0 || bytes <= 0) return 0;
|
|
122
|
+
const to = from + entry.time / 1e3;
|
|
123
|
+
const overlap = Math.min(to, end) - Math.max(from, start);
|
|
124
|
+
return overlap > 0 ? Math.round(bytes * overlap / Math.max(to - from, .001)) : 0;
|
|
125
|
+
}
|
|
126
|
+
const isImage = (entry) => entry._resourceType === "image" || (entry.response.content?.mimeType ?? "").startsWith("image/");
|
|
127
|
+
/** A URL's file name without its query, for readable summaries. */
|
|
128
|
+
function fileName(url) {
|
|
129
|
+
try {
|
|
130
|
+
return decodeURIComponent(new URL(url).pathname.split("/").pop() || url);
|
|
131
|
+
} catch {
|
|
132
|
+
return url;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
/** URL without its query string, to match the same image requested at different sizes. */
|
|
136
|
+
function withoutQuery(url) {
|
|
137
|
+
try {
|
|
138
|
+
const parsed = new URL(url);
|
|
139
|
+
return `${parsed.origin}${parsed.pathname}`;
|
|
140
|
+
} catch {
|
|
141
|
+
return url;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
//#endregion
|
|
146
|
+
export { kib as a, isImage as i, bytesDuring as n, readImageRun as o, fileName as r, withoutQuery as s, MIN_SAVINGS_BYTES as t };
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","names":[],"sources":["../src/plugins/pageDesignerPreloadManifest.ts","../src/plugins/staticRegistry.ts","../src/plugins/eventInstrumentationValidator.ts","../src/storefront-next-targets.ts","../src/plugins/transformTargets.ts","../src/plugins/uiTargetDevMode.ts","../src/plugins/hybridProxy.ts","../src/plugins/ecdnMatcher.ts"],"sourcesContent":[],"mappings":";;;;;AI6BgB,UJGC,iCAAA,CIOc;;;;;;;AGuG/B;;;UN3EiB,0BAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;8BA+Be;;;;;AE3DhC;AAkEA;;UD/EiB,mCAAA;;AEGjB;;;;ECHiB;AA4BjB;;;;ECOiB;AAyZjB;;;;ACxUA;;;;;;;AHjHgB,UDUC,2BAAA,CCV+B;;;;ACHhD;AA4BA;;;;ACOA;AAyZA;;;;ACxUA;;;;;;;;;;;;;;;;;;;mBJrEqB;;;;;;;;;kCAUe;;;;;;;;;;;;;;;;;;;;;iBAsBpB,qBAAA,UAA8B,8BAAmC;;;iBC5EjE,gCAAA,CAAA;;;EJGC,cAAA,CAAA,MAAA,EIOc,cJPmB,CAAA,EAAA,IAAA;;;;ICmCjC,GAAA,EAAA,IAAA;;;;;AE5BjB;AAkEA;;;;AC5EA;;;;ACHiB,UAAA,qBAAA,
|
|
1
|
+
{"version":3,"file":"index.d.ts","names":[],"sources":["../src/plugins/pageDesignerPreloadManifest.ts","../src/plugins/staticRegistry.ts","../src/plugins/eventInstrumentationValidator.ts","../src/storefront-next-targets.ts","../src/plugins/transformTargets.ts","../src/plugins/uiTargetDevMode.ts","../src/plugins/hybridProxy.ts","../src/plugins/ecdnMatcher.ts"],"sourcesContent":[],"mappings":";;;;;AI6BgB,UJGC,iCAAA,CIOc;;;;;;;AGuG/B;;;UN3EiB,0BAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;8BA+Be;;;;;AE3DhC;AAkEA;;UD/EiB,mCAAA;;AEGjB;;;;ECHiB;AA4BjB;;;;ECOiB;AAyZjB;;;;ACxUA;;;;;;;AHjHgB,UDUC,2BAAA,CCV+B;;;;ACHhD;AA4BA;;;;ACOA;AAyZA;;;;ACxUA;;;;;;;;;;;;;;;;;;;mBJrEqB;;;;;;;;;kCAUe;;;;;;;;;;;;;;;;;;;;;iBAsBpB,qBAAA,UAA8B,8BAAmC;;;iBC5EjE,gCAAA,CAAA;;;EJGC,cAAA,CAAA,MAAA,EIOc,cJPmB,CAAA,EAAA,IAAA;;;;ICmCjC,GAAA,EAAA,IAAA;;;;;AE5BjB;AAkEA;;;;AC5EA;;;;ACHiB,UAAA,qBAAA,CAcH;EAcE;;;;ECOC;AAyZjB;;;;ACxUA;;YFtGc;;;;;;;;;;;;;iBAcE,qBAAA,WAAgC,wBAAwB;;;;UCOvD,wBAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAyZD,iBAAA,UAA2B,2BAA2B;;;;;;;;;;;;;;;;;;;;;;iBCxUtD,iBAAA"}
|