@salesforce/storefront-next-dev 1.4.0-alpha.1 → 1.4.1-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.
Files changed (136) hide show
  1. package/README.md +13 -2
  2. package/dist/assets.js +25 -0
  3. package/dist/audit/catalog/categories/best-practices/CATEGORY.md +21 -0
  4. package/dist/audit/catalog/categories/best-practices/rules/commerce-api-client.md +20 -0
  5. package/dist/audit/catalog/categories/best-practices/rules/error-boundaries.md +19 -0
  6. package/dist/audit/catalog/categories/best-practices/rules/production-integrations.md +20 -0
  7. package/dist/audit/catalog/categories/best-practices/rules/route-revalidation.md +19 -0
  8. package/dist/audit/catalog/categories/best-practices/rules/server-data-loading.md +19 -0
  9. package/dist/audit/catalog/categories/performance/CATEGORY.md +18 -0
  10. package/dist/audit/catalog/categories/performance/rules/app-config-size.js +15 -0
  11. package/dist/audit/catalog/categories/performance/rules/app-config-size.md +22 -0
  12. package/dist/audit/catalog/categories/performance/rules/cache-control.js +59 -0
  13. package/dist/audit/catalog/categories/performance/rules/cache-control.md +20 -0
  14. package/dist/audit/catalog/categories/performance/rules/cls-budget.js +13 -0
  15. package/dist/audit/catalog/categories/performance/rules/cls-budget.md +20 -0
  16. package/dist/audit/catalog/categories/performance/rules/content-encoding.js +35 -0
  17. package/dist/audit/catalog/categories/performance/rules/content-encoding.md +20 -0
  18. package/dist/audit/catalog/categories/performance/rules/dom-size.js +18 -0
  19. package/dist/audit/catalog/categories/performance/rules/dom-size.md +22 -0
  20. package/dist/audit/catalog/categories/performance/rules/html-document-size.js +15 -0
  21. package/dist/audit/catalog/categories/performance/rules/html-document-size.md +20 -0
  22. package/dist/audit/catalog/categories/performance/rules/image-dimensions.js +141 -0
  23. package/dist/audit/catalog/categories/performance/rules/image-dimensions.md +22 -0
  24. package/dist/audit/catalog/categories/performance/rules/image-transformation.js +117 -0
  25. package/dist/audit/catalog/categories/performance/rules/image-transformation.md +22 -0
  26. package/dist/audit/catalog/categories/performance/rules/javascript-size.js +126 -0
  27. package/dist/audit/catalog/categories/performance/rules/javascript-size.md +20 -0
  28. package/dist/audit/catalog/categories/performance/rules/large-images.js +187 -0
  29. package/dist/audit/catalog/categories/performance/rules/large-images.md +22 -0
  30. package/dist/audit/catalog/categories/performance/rules/lcp-budget.js +13 -0
  31. package/dist/audit/catalog/categories/performance/rules/lcp-budget.md +20 -0
  32. package/dist/audit/catalog/categories/performance/rules/lcp-image-priority.js +227 -0
  33. package/dist/audit/catalog/categories/performance/rules/lcp-image-priority.md +27 -0
  34. package/dist/audit/catalog/categories/performance/rules/page-data-size.js +15 -0
  35. package/dist/audit/catalog/categories/performance/rules/page-data-size.md +22 -0
  36. package/dist/audit/catalog/categories/performance/rules/reduce-javascript-on-load.js +97 -0
  37. package/dist/audit/catalog/categories/performance/rules/reduce-javascript-on-load.md +73 -0
  38. package/dist/audit/catalog/categories/performance/rules/render-blocking-font-size.js +201 -0
  39. package/dist/audit/catalog/categories/performance/rules/render-blocking-font-size.md +20 -0
  40. package/dist/audit/catalog/categories/performance/rules/server-request-waterfall.md +85 -0
  41. package/dist/audit/catalog/categories/performance/rules/tbt-budget.js +13 -0
  42. package/dist/audit/catalog/categories/performance/rules/tbt-budget.md +20 -0
  43. package/dist/audit/catalog/categories/performance/rules/third-party-fonts.js +265 -0
  44. package/dist/audit/catalog/categories/performance/rules/third-party-fonts.md +20 -0
  45. package/dist/audit/catalog/categories/performance/rules/ttfb-budget.js +17 -0
  46. package/dist/audit/catalog/categories/performance/rules/ttfb-budget.md +20 -0
  47. package/dist/audit/catalog/categories/security/CATEGORY.md +17 -0
  48. package/dist/audit/catalog/categories/security/rules/browser-hardening-headers.js +37 -0
  49. package/dist/audit/catalog/categories/security/rules/browser-hardening-headers.md +20 -0
  50. package/dist/audit/catalog/categories/security/rules/content-security-policy.js +40 -0
  51. package/dist/audit/catalog/categories/security/rules/content-security-policy.md +20 -0
  52. package/dist/audit/catalog/categories/security/rules/csp-runtime-compatibility.md +24 -0
  53. package/dist/audit/catalog/categories/security/rules/dependency-vulnerabilities.md +19 -0
  54. package/dist/audit/catalog/categories/security/rules/dynamic-code-execution.md +19 -0
  55. package/dist/audit/catalog/categories/security/rules/hardcoded-credentials.md +19 -0
  56. package/dist/audit/catalog/categories/security/rules/html-config-secrets.js +46 -0
  57. package/dist/audit/catalog/categories/security/rules/html-config-secrets.md +22 -0
  58. package/dist/audit/catalog/categories/security/rules/protected-route-authorization.md +19 -0
  59. package/dist/audit/catalog/categories/security/rules/public-client-secrets.md +20 -0
  60. package/dist/audit/catalog/categories/security/rules/raw-html-sinks.md +20 -0
  61. package/dist/audit/catalog/categories/security/rules/strict-transport-security.js +183 -0
  62. package/dist/audit/catalog/categories/security/rules/strict-transport-security.md +24 -0
  63. package/dist/audit/catalog/categories/seo/CATEGORY.md +17 -0
  64. package/dist/audit/catalog/categories/seo/rules/canonical-url.md +20 -0
  65. package/dist/audit/catalog/categories/seo/rules/locale-alternates.md +19 -0
  66. package/dist/audit/catalog/categories/seo/rules/meta-description.md +19 -0
  67. package/dist/audit/catalog/categories/seo/rules/page-title.md +19 -0
  68. package/dist/audit/catalog/categories/seo/rules/robots-indexing.md +20 -0
  69. package/dist/audit/catalog/categories/seo/rules/structured-data.md +19 -0
  70. package/dist/audit/instructions/evaluate-rule.md +42 -0
  71. package/dist/audit/schemas/audit-result.schema.json +1237 -0
  72. package/dist/audit/ui/assets/banner-brand-default-CDRB3_S0.png +0 -0
  73. package/dist/audit/ui/assets/banner-group-public-default-BGRJzVY8.png +0 -0
  74. package/dist/audit/ui/assets/banner-user-default-BW81Ba3z.png +0 -0
  75. package/dist/audit/ui/assets/bg-info@2x-CzpP0vaE.png +0 -0
  76. package/dist/audit/ui/assets/einstein-figure-B1xFOo8O.svg +1 -0
  77. package/dist/audit/ui/assets/group_avatar_160-CrxVHlYA.png +0 -0
  78. package/dist/audit/ui/assets/group_avatar_200-CSLS8I0r.png +0 -0
  79. package/dist/audit/ui/assets/index-BKCWlO9b.css +1 -0
  80. package/dist/audit/ui/assets/index-CUPt8e4A.js +44 -0
  81. package/dist/audit/ui/assets/profile_avatar_160-CtQTUBo0.png +0 -0
  82. package/dist/audit/ui/assets/profile_avatar_200-DAztxshF.png +0 -0
  83. package/dist/audit/ui/assets/symbols-BRmpfDbO.svg +1 -0
  84. package/dist/audit/ui/assets/symbols-D0Ne6690.svg +1 -0
  85. package/dist/audit/ui/einstein.png +0 -0
  86. package/dist/audit/ui/index.html +13 -0
  87. package/dist/audit.js +1896 -0
  88. package/dist/cartridge-services/index.d.ts +5 -0
  89. package/dist/cartridge-services/index.d.ts.map +1 -1
  90. package/dist/cartridge-services/index.js +94 -7
  91. package/dist/cartridge-services/index.js.map +1 -1
  92. package/dist/catalog.js +89 -0
  93. package/dist/commands/audit/catalog.js +27 -0
  94. package/dist/commands/audit/finalize.js +42 -0
  95. package/dist/commands/audit/prepare.js +84 -0
  96. package/dist/commands/audit/progress.js +51 -0
  97. package/dist/commands/audit/serve.js +63 -0
  98. package/dist/commands/config/aggregate-extensions.js +1 -0
  99. package/dist/commands/create-bundle.js +2 -1
  100. package/dist/commands/create-instructions.js +1 -0
  101. package/dist/commands/create-storefront.js +1 -0
  102. package/dist/commands/deploy-cartridge.js +11 -3
  103. package/dist/commands/dev.js +2 -0
  104. package/dist/commands/extensions/create.js +1 -0
  105. package/dist/commands/extensions/install.js +1 -0
  106. package/dist/commands/extensions/list.js +1 -0
  107. package/dist/commands/extensions/remove.js +1 -0
  108. package/dist/commands/generate-cartridge.js +49 -5
  109. package/dist/commands/locales/aggregate-extensions.js +1 -0
  110. package/dist/commands/prepare-local.js +1 -0
  111. package/dist/commands/preview.js +2 -0
  112. package/dist/commands/push.js +3 -1
  113. package/dist/commands/validate-cartridge.js +13 -4
  114. package/dist/config.js +13 -2
  115. package/dist/document-size.js +231 -0
  116. package/dist/events.js +688 -0
  117. package/dist/flags2.js +34 -0
  118. package/dist/generate-cartridge.js +41 -6
  119. package/dist/har.js +205 -0
  120. package/dist/images.js +146 -0
  121. package/dist/index.d.ts.map +1 -1
  122. package/dist/live.js +277 -0
  123. package/dist/logger.js +1 -4
  124. package/dist/measured-run.js +91 -0
  125. package/dist/metric-budget.js +125 -0
  126. package/dist/mrt/ssr.mjs +19 -19
  127. package/dist/mrt/ssr.mjs.map +1 -1
  128. package/dist/mrt/streamingHandler.mjs +26 -26
  129. package/dist/mrt/streamingHandler.mjs.map +1 -1
  130. package/dist/package.js +5 -0
  131. package/dist/sanitize.js +185 -0
  132. package/dist/schema.js +121 -0
  133. package/dist/server.js +8 -60
  134. package/dist/server2.js +185 -0
  135. package/dist/ts-import.js +57 -0
  136. package/package.json +21 -4
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
- * This ensures we get the exact same route resolution as React Router uses internally.
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
- * @returns The route path (e.g., '/cart', '/p/*')
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 };
@@ -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,CAcH;EAcE;;;;ECOC;AAyZjB;;;;ACxUA;;YFtGc;;;;;;;;;;;;;iBAcE,qBAAA,WAAgC,wBAAwB;;;;UCOvD,wBAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAyZD,iBAAA,UAA2B,2BAA2B;;;;;;;;;;;;;;;;;;;;;;iBCxUtD,iBAAA"}
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,CAAqB;EA4BtB;;;;ECOC;AAyZjB;;;;ACxUA;;YFtGc;;;;;;;;;;;;;iBAcE,qBAAA,WAAgC,wBAAwB;;;;UCOvD,wBAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAyZD,iBAAA,UAA2B,2BAA2B;;;;;;;;;;;;;;;;;;;;;;iBCxUtD,iBAAA"}