@salesforce/storefront-next-dev 1.4.0 → 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 (137) 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.js +3 -3
  122. package/dist/index.js.map +1 -1
  123. package/dist/live.js +277 -0
  124. package/dist/logger.js +1 -4
  125. package/dist/measured-run.js +91 -0
  126. package/dist/metric-budget.js +125 -0
  127. package/dist/mrt/ssr.mjs +19 -19
  128. package/dist/mrt/ssr.mjs.map +1 -1
  129. package/dist/mrt/streamingHandler.mjs +26 -26
  130. package/dist/mrt/streamingHandler.mjs.map +1 -1
  131. package/dist/package.js +5 -0
  132. package/dist/sanitize.js +185 -0
  133. package/dist/schema.js +121 -0
  134. package/dist/server.js +8 -60
  135. package/dist/server2.js +185 -0
  136. package/dist/ts-import.js +57 -0
  137. package/package.json +22 -5
package/README.md CHANGED
@@ -137,8 +137,17 @@ Run these commands from your project directory:
137
137
  # Generate cartridge metadata for your site
138
138
  pnpm sfnext generate-cartridge
139
139
 
140
- # Deploy generated metadata to B2C Commerce (uses dw.json for all settings)
141
- pnpm sfnext deploy-cartridge
140
+ # Select a site other than app.defaultSiteId for PDP/PLP route metadata
141
+ pnpm sfnext generate-cartridge --site-id RefArch
142
+
143
+ # Generate a site-specific cartridge (scaffolds identity files once; no base required)
144
+ pnpm sfnext generate-cartridge --site-id RefArch --cartridge-name app_storefrontnext_refarch
145
+
146
+ # Validate a custom cartridge
147
+ pnpm sfnext validate-cartridge --cartridge-name app_storefrontnext_refarch
148
+
149
+ # Deploy a named cartridge with the existing filter
150
+ pnpm sfnext deploy-cartridge -c app_storefrontnext_refarch
142
151
  ```
143
152
 
144
153
  ## CLI Options
@@ -273,6 +282,8 @@ The cartridge commands are independent tools for working with B2C Commerce metad
273
282
  - Scans `src/` directory for `@Component`, `@PageType`, and `@Aspect` decorators
274
283
  - Generates JSON metadata files in `cartridge/cartridge/experience/` directory
275
284
  - Creates separate files for components, page types, and aspects
285
+ - Canonical PDP/PLP route metadata uses the selected site's complete `app.url.seoRoutes` entry (default: `app.defaultSiteId`); `--site-id` overrides it. A `'*'` entry is used only when no site-specific entry exists. Without `seoRoutes`, the metadata route prefixes are `p` and `c`.
286
+ - `--cartridge-name` writes to `cartridges/<name>` instead of `app_storefrontnext_base`. A new name scaffolds only `.project` and `<name>.properties` (no base cartridge, hooks, or sample assets). An existing target is left unchanged, so non-metadata files are preserved; generated components/pages/aspects are replaced. Put the named cartridge **before** the base entry in that site's cartridge path so the base still supplies hooks and shared behavior. Deploy with existing `-c`. Separate outputs must be regenerated explicitly.
276
287
 
277
288
  **Deploy Metadata**: `deploy-cartridge` uploads metadata directories to B2C Commerce
278
289
 
package/dist/assets.js ADDED
@@ -0,0 +1,25 @@
1
+ import { t as version } from "./package.js";
2
+ import { existsSync } from "node:fs";
3
+ import path from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+
6
+ //#region src/audit/assets.ts
7
+ /** The storefront-next-dev version, not the host CLI's: under `b2c sfnext`, oclif's config version is b2c's. */
8
+ const VERSION = version;
9
+ function packageRoot(from) {
10
+ for (let dir = from;; dir = path.dirname(dir)) {
11
+ if (existsSync(path.join(dir, "package.json"))) return dir;
12
+ if (path.dirname(dir) === dir) throw new Error(`No package.json above ${from}`);
13
+ }
14
+ }
15
+ const here = path.dirname(fileURLToPath(import.meta.url));
16
+ const root = packageRoot(here);
17
+ /**
18
+ * The directory holding the audit's runtime assets (catalog, instructions, UI). tsdown moves shared code into
19
+ * chunks at the top of `dist`, so paths relative to a module break after bundling. This resolves to `src/audit`
20
+ * when running from source (tests) and `dist/audit` when running the build.
21
+ */
22
+ const AUDIT_ROOT = path.join(root, path.relative(root, here).split(path.sep)[0] === "src" ? "src" : "dist", "audit");
23
+
24
+ //#endregion
25
+ export { VERSION as n, AUDIT_ROOT as t };
@@ -0,0 +1,21 @@
1
+ # Best Practices
2
+
3
+ ## Purpose and boundaries
4
+
5
+ Inspect storefront-next framework use, server loaders/actions, and production integrations. Follow data flow before judging a text match.
6
+
7
+ ## Investigation sequence
8
+
9
+ Map routes and data boundaries, inspect every rule, and corroborate source conclusions with captured behavior.
10
+
11
+ ## Evidence and redaction
12
+
13
+ Cite source lines and Playhouse diagnostics without secrets or personal data.
14
+
15
+ ## Severity and blind spots
16
+
17
+ Opaque integrations without independent end-to-end evidence are `not-verified`.
18
+
19
+ ## Draft rules and migration gaps
20
+
21
+ Application/data caching strategy, authenticated cache safety, observability, localization, resilient image handling, CSP integration, and framework-version-specific recommendations remain uncodified. Deployed public-resource caching is owned by `performance/cache-control`. Do not silently treat the remaining gaps as active rules; record relevant review as an ad hoc observation and coverage gap.
@@ -0,0 +1,20 @@
1
+ ---
2
+ id: best-practices/commerce-api-client
3
+ title: Commerce API Calls Use the SDK Clients
4
+ severity: high
5
+ appliesTo: [home, plp, pdp]
6
+ inputs: [source.code]
7
+ ---
8
+
9
+ ## Intent
10
+ Checks that calls to the Commerce APIs (SCAPI and OCAPI) go through the Storefront Next SDK clients instead of hand-built `fetch` requests. The SDK clients handle authentication, token refresh, routing and request correlation, which hand-built calls tend to get wrong.
11
+ ## Inspect
12
+ Trace raw fetches and SCAPI/OCAPI URLs through authentication, routing, token refresh, and correlation handling; exclude generated clients.
13
+ ## Evaluate
14
+ Finding requires a hand-built commerce call that bypasses the supported client, not a URL text match alone.
15
+ ## Evidence
16
+ Cite call site and the data/auth path it bypasses.
17
+ ## Remediation
18
+ Route the operation through the supported request-scoped SDK client.
19
+ ## Examples
20
+ Compliant: createApiClients call in a server loader. Problematic: direct fetch to a commerce endpoint. Ambiguous: URL in documentation.
@@ -0,0 +1,19 @@
1
+ ---
2
+ id: best-practices/error-boundaries
3
+ title: Route Error Pages
4
+ severity: medium
5
+ appliesTo: [route]
6
+ inputs: [source.code, manual.failure-state]
7
+ ---
8
+ ## Intent
9
+ Checks that when a route's loader, action or rendering fails, shoppers see a clear error page with the right HTTP status and a way to continue, and that the error is logged. Without this, a failure shows a blank or misleading page.
10
+ ## Inspect
11
+ Trace representative route failures to their route-level error handling and inspect the user-visible status, recovery action, and logging boundary.
12
+ ## Evaluate
13
+ Report missing or misleading handling for a demonstrated production failure path; do not require a particular framework abstraction by name.
14
+ ## Evidence
15
+ Cite the failure path, route source, response/status behavior, and visible fallback.
16
+ ## Remediation
17
+ Add scoped error handling that preserves status semantics, safe messaging, recovery, and server-side diagnostics.
18
+ ## Examples
19
+ Compliant: failed product load renders a scoped retry state with correct status. Problematic: uncaught failure renders blank content. Ambiguous: global platform handling not observable.
@@ -0,0 +1,20 @@
1
+ ---
2
+ id: best-practices/production-integrations
3
+ title: No Mock or Demo Integrations
4
+ severity: critical
5
+ appliesTo: [cart, checkout, account]
6
+ inputs: [source.code, manual.end-to-end]
7
+ ---
8
+
9
+ ## Intent
10
+ Checks that cart, checkout, payment, order management, tax and loyalty flows call real systems, not mock, demo or TODO code left over from development. A stubbed integration in these flows can lose orders or revenue at launch.
11
+ ## Inspect
12
+ Trace cart, checkout, payment, OMS, tax, and loyalty flows for demo/mock/TODO paths and validate opaque systems end to end.
13
+ ## Evaluate
14
+ Use finding for demonstrated stub behavior; use `not-verified` for opaque integrations lacking test evidence.
15
+ ## Evidence
16
+ Cite executable source path or end-to-end result, never a comment marker alone.
17
+ ## Remediation
18
+ Wire the production integration or disable the feature until verified.
19
+ ## Examples
20
+ Compliant: real integration plus successful E2E evidence. Problematic: mock success on place-order. Ambiguous: TODO comment beside complete code.
@@ -0,0 +1,19 @@
1
+ ---
2
+ id: best-practices/route-revalidation
3
+ title: "Layout Loaders Don't Rerun Needlessly"
4
+ severity: medium
5
+ appliesTo: [home, plp, pdp]
6
+ inputs: [source.code]
7
+ ---
8
+ ## Intent
9
+ Checks that loaders for layouts that stay on screen across pages, such as the app shell, use `shouldRevalidate` so they don't fetch the same data again on every navigation. Rerunning an expensive layout loader slows down every page change.
10
+ ## Inspect
11
+ Identify persistent layout loaders and inspect shouldRevalidate against their actual inputs.
12
+ ## Evaluate
13
+ Finding requires repeated expensive work without a valid revalidation need.
14
+ ## Evidence
15
+ Cite loader, mounted scope, calls, and revalidation behavior.
16
+ ## Remediation
17
+ Gate revalidation to meaningful input changes.
18
+ ## Examples
19
+ Compliant: scoped shouldRevalidate. Problematic: shell fan-out each navigation. Ambiguous: cheap loader.
@@ -0,0 +1,19 @@
1
+ ---
2
+ id: best-practices/server-data-loading
3
+ title: First-Render Data Loads on the Server
4
+ severity: high
5
+ appliesTo: [home, plp, pdp]
6
+ inputs: [source.code]
7
+ ---
8
+ ## Intent
9
+ Checks that the data a page needs for its first render is loaded in server loaders, not fetched in the browser after hydration (with `clientLoader` or a `useEffect` fetch). Loading it on the server sends shoppers and search engines complete HTML without waiting for JavaScript.
10
+ ## Inspect
11
+ Trace clientLoader, clientAction, useEffect fetches, and route loader ownership.
12
+ ## Evaluate
13
+ Finding requires first-render data being unnecessarily delayed until hydration.
14
+ ## Evidence
15
+ Cite route, data need, client call, and available server path.
16
+ ## Remediation
17
+ Move required initial data to a server loader/action.
18
+ ## Examples
19
+ Compliant: loader-fed render. Problematic: PDP fetch starts after hydration. Ambiguous: post-interaction effect fetch.
@@ -0,0 +1,18 @@
1
+ # Performance
2
+
3
+ ## Purpose and boundaries
4
+
5
+ Use Playhouse summary, Opportunities, LCP, TBT, third-party, document, and network artifacts as measured evidence. Inspect source only to explain measured symptoms or delivered response policy; never recreate Playhouse algorithms. The exception is `performance/server-request-waterfall`, which maps server-render network calls from source because Playhouse can't see inside the server.
6
+
7
+ ## Investigation sequence
8
+
9
+ Inspect every rule, compare representative and outlier cells, then trace measured symptoms to source.
10
+
11
+ ## Evidence and redaction
12
+
13
+ Cite artifact name, JSON pointer/value, page, and line-addressable source where used.
14
+
15
+ ## Severity and blind spots
16
+
17
+ Uncollected and authenticated pages remain explicit coverage gaps. Header policy requires a representative sanitized HAR with trustworthy response type and size evidence.
18
+ Public-resource caching does not establish that personalized or API responses are safe for shared caches.
@@ -0,0 +1,15 @@
1
+ import "../../../../../har.js";
2
+ import { i as kib, n as documentSizeDataSchema, r as documentSizeRule } from "../../../../../document-size.js";
3
+
4
+ //#region src/audit/catalog/categories/performance/rules/app-config-size.ts
5
+ var app_config_size_default = documentSizeRule({
6
+ label: "App config",
7
+ budget: 12 * 1024,
8
+ value: (size) => size.appConfigBytes,
9
+ format: kib,
10
+ evidence: (size) => size.scripts.appConfig[0] ? { artifact: size.scripts.appConfig[0] } : null,
11
+ decideFailures: false
12
+ });
13
+
14
+ //#endregion
15
+ export { documentSizeDataSchema as dataSchema, app_config_size_default as default };
@@ -0,0 +1,22 @@
1
+ ---
2
+ id: performance/app-config-size
3
+ title: App Config Size
4
+ severity: low
5
+ appliesTo: [home, plp, pdp]
6
+ inputs: [playhouse.scripts, source.code]
7
+ ---
8
+
9
+ ## Intent
10
+ Checks that the app config Storefront Next writes into every page (`window.__APP_CONFIG__`) is 12 KiB or less. The config is repeated in each page's HTML, so settings the browser never reads add to every page load. When a page is over budget, the report points to the config sections the browser doesn't need.
11
+ ## Inspect
12
+ The rule script measures the archived `window.__APP_CONFIG__` script in each page's HTML. If every page is within 12 KiB, it passes and this rule needs no analysis.
13
+
14
+ Otherwise, use the `data` file (sizes are in bytes): `appConfigSections` gives each top-level config section's size, largest first, and `scripts.appConfig` names the archived config script (relative to the page's `archiveDir`). Read the largest sections in that script, then check in the project where each large section is read (`getConfig`, `useConfig`, `window.__APP_CONFIG__`) and whether the browser needs it, or only server code does. Settings browser code reads at any point, including after hydration or on interaction, are needed. Also check whether the root loader returns the same config, which embeds it in the page data a second time.
15
+ ## Evaluate
16
+ Report one `finding` for the rule when any page's config is over 12 KiB; when the config is the same on every page, cite it once and say so. Name the largest sections and, for each, whether the browser reads it, with an approximate size. Suggest keeping server-only settings out of the config sent to the browser, as suggestions the customer can choose from.
17
+ ## Evidence
18
+ The archived config script (`playhouse`, with its size as `value`) and `source` evidence for where the large sections are defined and read.
19
+ ## Remediation
20
+ Leave settings the browser never reads out of the config sent to the browser. Re-run Playhouse to confirm the config is at or under 12 KiB.
21
+ ## Examples
22
+ Compliant: a 10 KiB config. Problematic: a 15 KiB config whose largest section is only read on the server.
@@ -0,0 +1,59 @@
1
+ import { a as harRuleDataSchema, d as sameHostResponses, i as harRule, o as header, r as cacheControl, u as resourceKind } from "../../../../../har.js";
2
+
3
+ //#region src/audit/catalog/categories/performance/rules/cache-control.ts
4
+ const dataSchema = harRuleDataSchema;
5
+ const YEAR = 31536e3;
6
+ const HOUR = 3600;
7
+ const DAY = 86400;
8
+ /** A content hash before the extension, e.g. `entry.client-Be9z2OkY.js` or `main.3f2a9c0d.css`; it needs a digit or an uppercase letter so words don't count. */
9
+ function fingerprinted(url) {
10
+ const hash = /[.-]([A-Za-z0-9_-]{8}|[a-f0-9]{8,})\.(?:m?js|css)$/.exec(new URL(url).pathname)?.[1];
11
+ return hash !== void 0 && /[0-9A-Z]/.test(hash);
12
+ }
13
+ function minimumMaxAge(entry) {
14
+ const kind = resourceKind(entry);
15
+ if (kind === "js" || kind === "css") return fingerprinted(entry.request.url) ? {
16
+ seconds: YEAR,
17
+ label: "a fingerprinted asset"
18
+ } : {
19
+ seconds: HOUR,
20
+ label: "a non-fingerprinted script or stylesheet"
21
+ };
22
+ if (kind === "image" || kind === "font") return {
23
+ seconds: DAY,
24
+ label: `a public ${kind}`
25
+ };
26
+ }
27
+ var cache_control_default = harRule("same-host script, stylesheet, image and font responses", (match) => {
28
+ const eligible = sameHostResponses(match).flatMap((item) => {
29
+ const minimum = minimumMaxAge(item.entry);
30
+ return minimum ? [{
31
+ item,
32
+ minimum
33
+ }] : [];
34
+ });
35
+ const failures = eligible.flatMap(({ item, minimum }) => {
36
+ const value = header(item.entry, "cache-control");
37
+ const directives = cacheControl(item.entry);
38
+ const reasons = [];
39
+ if (!value) reasons.push("Cache-Control is missing.");
40
+ else {
41
+ for (const directive of ["private", "no-store"]) if (directives.has(directive)) reasons.push(`Cache-Control is ${directive}.`);
42
+ const maxAge = directives.get("max-age");
43
+ if (maxAge === void 0 || !/^\d+$/.test(maxAge)) reasons.push("Cache-Control has no valid max-age.");
44
+ else if (Number(maxAge) < minimum.seconds) reasons.push(`max-age=${maxAge} is under ${minimum.seconds} seconds for ${minimum.label}.`);
45
+ }
46
+ return reasons.length ? [{
47
+ entry: item,
48
+ reasons,
49
+ headers: { "cache-control": value }
50
+ }] : [];
51
+ });
52
+ return {
53
+ responses: eligible.length,
54
+ failures
55
+ };
56
+ }, "have long enough Cache-Control lifetimes");
57
+
58
+ //#endregion
59
+ export { dataSchema, cache_control_default as default };
@@ -0,0 +1,20 @@
1
+ ---
2
+ id: performance/cache-control
3
+ title: Browser Cache Lifetimes for Static Files
4
+ severity: medium
5
+ appliesTo: [home, plp, pdp]
6
+ inputs: [playhouse.network-har]
7
+ ---
8
+
9
+ ## Intent
10
+ Checks the `Cache-Control` header on the storefront's own scripts, stylesheets, images and fonts: at least 1 year for fingerprinted scripts and stylesheets (a content hash in the file name, e.g. `entry.client-Be9z2OkY.js`), 1 hour for other scripts and stylesheets, and 1 day for images and fonts. Long enough lifetimes let browsers and CDNs reuse files instead of downloading them again. HTML, API responses and third-party files are not checked.
11
+ ## Inspect
12
+ The rule script matches the final document response in `network-mobile-cold.har` (via `network-mobile-cold.json`) for each page. Only same-host responses with status 200 are checked; third-party responses are skipped. It classifies scripts, stylesheets, images and fonts by content type, treats a script or stylesheet as fingerprinted when its file name carries a content hash (e.g. `entry.client-Be9z2OkY.js`), and reads `Cache-Control`. If no page's document can be matched, the rule is not decided.
13
+ ## Evaluate
14
+ Fail when `Cache-Control` is missing, `private` or `no-store`, or its `max-age` is missing or under: 1 year for fingerprinted scripts and stylesheets, 1 hour for other scripts and stylesheets, 1 day for images and fonts. HTML and API/JSON responses are not graded. Pass otherwise.
15
+ ## Evidence
16
+ The HAR entry of each failing response (up to 10), with its `Cache-Control`.
17
+ ## Remediation
18
+ Set the policy at the layer that owns the response: Managed Runtime or the public edge for bundle assets, a customer CDN when it rewrites headers, application code for application-owned responses. Use `public, max-age=31536000, immutable` only for content-hashed files and shorter lifetimes for URLs that change in place. Re-run Playhouse to confirm.
19
+ ## Examples
20
+ Compliant: `app-a1b2c3d4.js` with `public, max-age=31536000`. Problematic: a fingerprinted bundle with no `Cache-Control`, or a product image with `private, no-store`.
@@ -0,0 +1,13 @@
1
+ import "../../../../../measured-run.js";
2
+ import { n as metricBudget, t as dataSchema } from "../../../../../metric-budget.js";
3
+
4
+ //#region src/audit/catalog/categories/performance/rules/cls-budget.ts
5
+ var cls_budget_default = metricBudget({
6
+ metric: "cls",
7
+ label: "CLS",
8
+ threshold: .1,
9
+ format: (value) => value.toFixed(2)
10
+ });
11
+
12
+ //#endregion
13
+ export { dataSchema, cls_budget_default as default };
@@ -0,0 +1,20 @@
1
+ ---
2
+ id: performance/cls-budget
3
+ title: Cumulative Layout Shift (CLS)
4
+ severity: high
5
+ appliesTo: [home, plp, pdp]
6
+ inputs: [playhouse.summary]
7
+ ---
8
+
9
+ ## Intent
10
+ Checks that each page's median Cumulative Layout Shift, measured by Lighthouse on mobile with a cold cache, is 0.1 or less, the Core Web Vitals "good" threshold. A high CLS means content jumps around while the page loads.
11
+ ## Inspect
12
+ The rule script reads each page's `summary.json` and takes the mobile cold-cache cell's `medianRun.cls`. Playhouse writes 0 for metrics it could not measure, and a rendered page always has a positive LCP, so a run with an LCP of 0 or less measured nothing. Such runs are left out: when any run failed, the script picks the median again from the measured runs the way Playhouse does (the run closest to the median LCP, ties to the earlier run) and reads that run's `runs[].row.cls`.
13
+ ## Evaluate
14
+ Pass when every measured page's median CLS is at or under 0.1; fail otherwise. A page without a mobile cold-cache cell, or without any measured run, is skipped. If no page can be measured, the rule is not decided.
15
+ ## Evidence
16
+ One `summary.json` pointer per page, `/cells/<i>/medianRun/cls` (or `/runs/<j>/row/cls` when runs were left out), with the measured value and how many runs were not measured.
17
+ ## Remediation
18
+ Use `opportunities.json` and `report-mobile-cold.html` for the failing page to find the shifting elements. Reserve space for images, embeds and late-loading content (explicit width/height or aspect-ratio), and avoid inserting content above existing content. Re-run the same Playhouse capture to confirm the mobile cold-cache median is at or under 0.1.
19
+ ## Examples
20
+ Compliant: home median CLS 0.04. Problematic: pdp median CLS 0.22.
@@ -0,0 +1,35 @@
1
+ import { a as harRuleDataSchema, d as sameHostResponses, f as tokens, i as harRule, o as header, t as COMPRESSED_ENCODINGS, u as resourceKind } from "../../../../../har.js";
2
+
3
+ //#region src/audit/catalog/categories/performance/rules/content-encoding.ts
4
+ const dataSchema = harRuleDataSchema;
5
+ const MIN_BYTES = {
6
+ html: 1024,
7
+ js: 10 * 1024,
8
+ css: 10 * 1024
9
+ };
10
+ var content_encoding_default = harRule("same-host HTML (1 KiB+), JavaScript and CSS (10 KiB+) responses", (match) => {
11
+ const eligible = sameHostResponses(match).filter(({ entry }) => {
12
+ const min = MIN_BYTES[resourceKind(entry)];
13
+ const size = entry.response.content?.size;
14
+ return min !== void 0 && typeof size === "number" && size >= min;
15
+ });
16
+ const failures = eligible.flatMap((item) => {
17
+ const encoding = header(item.entry, "content-encoding");
18
+ if (tokens(encoding).some((token) => COMPRESSED_ENCODINGS.includes(token))) return [];
19
+ return [{
20
+ entry: item,
21
+ reasons: [`${resourceKind(item.entry).toUpperCase()} response of ${Math.round((item.entry.response.content?.size ?? 0) / 1024)} KiB has no br, gzip or zstd Content-Encoding.`],
22
+ headers: {
23
+ "content-type": header(item.entry, "content-type"),
24
+ "content-encoding": encoding
25
+ }
26
+ }];
27
+ });
28
+ return {
29
+ responses: eligible.length,
30
+ failures
31
+ };
32
+ }, "are compressed with Brotli, gzip or zstd");
33
+
34
+ //#endregion
35
+ export { dataSchema, content_encoding_default as default };
@@ -0,0 +1,20 @@
1
+ ---
2
+ id: performance/content-encoding
3
+ title: HTML, JavaScript and CSS Compression
4
+ severity: high
5
+ appliesTo: [home, plp, pdp]
6
+ inputs: [playhouse.network-har]
7
+ ---
8
+
9
+ ## Intent
10
+ Checks that the storefront's own HTML (1 KiB or more), JavaScript and CSS (10 KiB or more) are sent compressed with Brotli, gzip or zstd, so pages download fewer bytes. Third-party files and smaller responses are not checked. Managed Runtime compresses by default, so a failure usually points to a CDN or proxy in front of it.
11
+ ## Inspect
12
+ The rule script matches the final document response in `network-mobile-cold.har` (via `network-mobile-cold.json`) for each page. Only same-host responses with status 200 are checked; third-party responses are skipped. It reads the content type and decoded size (`response.content`) and `Content-Encoding`. If no page's document can be matched, the rule is not decided.
13
+ ## Evaluate
14
+ Fail when HTML of at least 1 KiB, or JavaScript or CSS of at least 10 KiB (decoded size), has no `br`, `gzip` or `zstd` `Content-Encoding`. Responses without a known type or size are skipped. Pass otherwise.
15
+ ## Evidence
16
+ The HAR entry of each failing response (up to 10), with its size, `Content-Type` and `Content-Encoding`.
17
+ ## Remediation
18
+ Check that the public edge and every customer CDN in front of Managed Runtime preserve its compression; do not add application-level compression on Managed Runtime. Escalate an MRT-owned miss to Salesforce. For a non-MRT origin, enable Brotli or gzip. Re-run Playhouse to confirm.
19
+ ## Examples
20
+ Compliant: a 180 KiB bundle with `Content-Encoding: br`. Problematic: a 40 KiB stylesheet with no `Content-Encoding`.
@@ -0,0 +1,18 @@
1
+ import "../../../../../har.js";
2
+ import { n as documentSizeDataSchema, r as documentSizeRule, t as count } from "../../../../../document-size.js";
3
+
4
+ //#region src/audit/catalog/categories/performance/rules/dom-size.ts
5
+ var dom_size_default = documentSizeRule({
6
+ label: "Element count",
7
+ budget: 1400,
8
+ value: (size) => size.elements,
9
+ format: count,
10
+ evidence: (size) => size.lighthouseArtifact ? {
11
+ artifact: size.lighthouseArtifact,
12
+ pointer: "/audits/dom-size-insight/details/items"
13
+ } : null,
14
+ decideFailures: false
15
+ });
16
+
17
+ //#endregion
18
+ export { documentSizeDataSchema as dataSchema, dom_size_default as default };
@@ -0,0 +1,22 @@
1
+ ---
2
+ id: performance/dom-size
3
+ title: DOM Size (Number of Elements)
4
+ severity: medium
5
+ appliesTo: [home, plp, pdp]
6
+ inputs: [playhouse.lighthouse, playhouse.scripts, source.code]
7
+ ---
8
+
9
+ ## Intent
10
+ Checks that each page has 1,400 HTML elements or fewer after it loads, the limit Lighthouse used to flag. Every element adds to the HTML download and to the browser's style, layout and hydration work, so pages with large DOMs render and respond more slowly. When a page is over the limit, the report points to the components that render markup the shopper doesn't need yet.
11
+ ## Inspect
12
+ The rule script reads the element count and DOM depth from Lighthouse's DOM size insight (`dom-size-insight`) in each page's mobile cold-cache Lighthouse result. If every page has 1,400 elements or fewer, it passes and this rule needs no analysis.
13
+
14
+ Otherwise, use the `data` file and the Lighthouse result it names (`lighthouseArtifact`). The insight also names the deepest element and the element with the most children. Fetch the page (`curl -sL <url>`) to see which parts of the markup repeat most, then find the components in the project that render them.
15
+ ## Evaluate
16
+ Report a `finding` for each page over 1,400 elements. Name the parts of the page that add the most elements and the components behind them. Typical causes: a mega menu that renders every category up front, every carousel slide or variant panel rendered while hidden, long lists without pagination, and deeply nested wrapper elements. Suggest rendering hidden or below-the-fold markup only when it's needed, as suggestions the customer can choose from.
17
+ ## Evidence
18
+ The Lighthouse DOM size insight for each page over the limit (`playhouse`, with the element count as `value`), and `source` evidence for each component that renders the most markup.
19
+ ## Remediation
20
+ Render menus, hidden panels and off-screen slides when they open, paginate long lists, and remove wrapper elements that add no styling. Re-run Playhouse to confirm each page has 1,400 elements or fewer.
21
+ ## Examples
22
+ Compliant: a PLP with 720 elements. Problematic: a PLP with 1,620 elements because every product tile renders all of its color swatches.
@@ -0,0 +1,15 @@
1
+ import "../../../../../har.js";
2
+ import { i as kib, n as documentSizeDataSchema, r as documentSizeRule } from "../../../../../document-size.js";
3
+
4
+ //#region src/audit/catalog/categories/performance/rules/html-document-size.ts
5
+ var html_document_size_default = documentSizeRule({
6
+ label: "HTML size",
7
+ budget: 350 * 1024,
8
+ value: (size) => size.htmlBytes,
9
+ format: kib,
10
+ evidence: () => null,
11
+ decideFailures: true
12
+ });
13
+
14
+ //#endregion
15
+ export { documentSizeDataSchema as dataSchema, html_document_size_default as default };
@@ -0,0 +1,20 @@
1
+ ---
2
+ id: performance/html-document-size
3
+ title: HTML Document Size
4
+ severity: medium
5
+ appliesTo: [home, plp, pdp]
6
+ inputs: [playhouse.network-har, playhouse.scripts]
7
+ ---
8
+
9
+ ## Intent
10
+ Checks that each page's HTML is 350 KiB or less before compression. The browser has to download and parse all of the HTML, and React then hydrates it, before the page responds, so large HTML slows the first render on mobile. The result splits each page into its main parts (page data, app config, other inline scripts and markup) so the biggest one is easy to spot.
11
+ ## Inspect
12
+ The rule script takes the decoded size of each page's document response from `network-mobile-cold.har`. It splits that size using the inline scripts Playhouse archived from the same HTML (`scripts/index.json`): React Router page data (`streamController.enqueue(...)` scripts), the app config (`window.__APP_CONFIG__`), other inline scripts, and everything else, counted as markup.
13
+ ## Evaluate
14
+ Pass when every measured page's decoded HTML is at or under 350 KiB; fail otherwise. A page whose document can't be matched in the HAR is skipped. If no page can be measured, the rule is not decided.
15
+ ## Evidence
16
+ The document's HAR entry for each failing page, with its size and breakdown.
17
+ ## Remediation
18
+ Start with the largest part of the breakdown: page data is covered by `performance/page-data-size`, the app config by `performance/app-config-size`, and markup by `performance/dom-size`. Re-run Playhouse to confirm each page's HTML is at or under 350 KiB.
19
+ ## Examples
20
+ Compliant: a PLP of 290 KiB. Problematic: a PDP of 501 KiB, of which 297 KiB is page data.
@@ -0,0 +1,141 @@
1
+ import { o as readImageRun } from "../../../../../images.js";
2
+ import { z } from "zod";
3
+
4
+ //#region src/audit/catalog/categories/performance/rules/image-dimensions.ts
5
+ /** Shifts smaller than this aren't noticeable. */
6
+ const MIN_SHIFT = .01;
7
+ const UNSIZED_CAUSE = "Unsized image element";
8
+ const elementSchema = z.object({
9
+ selector: z.string().nullable(),
10
+ top: z.number().nullable(),
11
+ bottom: z.number().nullable()
12
+ }).strict();
13
+ const dataSchema = z.object({
14
+ cell: z.string(),
15
+ minShift: z.number(),
16
+ pages: z.array(z.object({
17
+ page: z.string(),
18
+ lighthouseArtifact: z.string(),
19
+ cls: z.number().nullable(),
20
+ unsizedShift: z.number(),
21
+ shifts: z.array(z.object({
22
+ cluster: z.number(),
23
+ score: z.number(),
24
+ element: elementSchema,
25
+ culprits: z.array(elementSchema.extend({
26
+ url: z.string().nullable(),
27
+ plausible: z.boolean()
28
+ }).strict())
29
+ }).strict()),
30
+ unsizedImages: z.array(elementSchema.extend({ url: z.string() }).strict())
31
+ }).strict()),
32
+ skipped: z.array(z.object({
33
+ page: z.string(),
34
+ reason: z.string()
35
+ }).strict())
36
+ }).strict();
37
+ const SRC = /\ssrc="([^"]+)"/;
38
+ const element = (node) => ({
39
+ selector: node?.selector ?? null,
40
+ top: node?.boundingRect?.top ?? null,
41
+ bottom: node?.boundingRect?.bottom ?? null
42
+ });
43
+ /**
44
+ * Lighthouse's layout shift culprits insight links a shift to unsized images by timing, so it can name an image that
45
+ * is nowhere near the shifted element. An image moves an element only from above it or beside it, so culprits that
46
+ * start below the element's top are marked implausible and don't count.
47
+ */
48
+ function unsizedShifts(run$1) {
49
+ const urls = new Map((run$1.lhr.audits["unsized-images"]?.details?.items ?? []).flatMap((item) => item.url && item.node?.path ? [[item.node.path, item.url]] : []));
50
+ const shifts = [];
51
+ let unsizedShift = 0;
52
+ (run$1.lhr.audits["cls-culprits-insight"]?.details?.items ?? []).forEach((cluster, index) => {
53
+ let clusterShift = 0;
54
+ for (const row of cluster.items ?? []) {
55
+ const causes = (row.subItems?.items ?? []).filter((item) => item.cause === UNSIZED_CAUSE);
56
+ if (!causes.length || !row.node?.selector) continue;
57
+ const shifted = element(row.node);
58
+ const culprits = causes.flatMap(({ extra }) => {
59
+ if (!extra?.selector) return [];
60
+ const src = extra.snippet?.match(SRC)?.[1];
61
+ const url = extra.path && urls.get(extra.path) || (src && !src.includes("…") ? src : null);
62
+ const image = element(extra);
63
+ return [{
64
+ ...image,
65
+ url,
66
+ plausible: image.top === null || shifted.top === null || image.top <= shifted.top
67
+ }];
68
+ });
69
+ const score = row.score ?? 0;
70
+ shifts.push({
71
+ cluster: index,
72
+ score,
73
+ element: shifted,
74
+ culprits
75
+ });
76
+ if (causes.length > culprits.length || culprits.some((culprit) => culprit.plausible)) clusterShift += score;
77
+ }
78
+ unsizedShift = Math.max(unsizedShift, clusterShift);
79
+ });
80
+ return {
81
+ unsizedShift,
82
+ shifts
83
+ };
84
+ }
85
+ const round = (value) => value.toFixed(3).replace(/0+$/, "").replace(/\.$/, "");
86
+ const run = async ({ pages, playhouse }) => {
87
+ const data = {
88
+ cell: "mobile-cold",
89
+ minShift: MIN_SHIFT,
90
+ pages: [],
91
+ skipped: []
92
+ };
93
+ for (const page of pages) {
94
+ const imageRun = await readImageRun(playhouse, page.label);
95
+ if ("reason" in imageRun) {
96
+ data.skipped.push({
97
+ page: page.label,
98
+ reason: imageRun.reason
99
+ });
100
+ continue;
101
+ }
102
+ if (!imageRun.lhr.audits["cls-culprits-insight"]) {
103
+ data.skipped.push({
104
+ page: page.label,
105
+ reason: "The Lighthouse result has no layout shift culprits insight."
106
+ });
107
+ continue;
108
+ }
109
+ data.pages.push({
110
+ page: page.label,
111
+ lighthouseArtifact: imageRun.lighthouseArtifact,
112
+ cls: imageRun.lhr.audits["cumulative-layout-shift"]?.numericValue ?? null,
113
+ ...unsizedShifts(imageRun),
114
+ unsizedImages: (imageRun.lhr.audits["unsized-images"]?.details?.items ?? []).flatMap((item) => item.url ? [{
115
+ url: item.url,
116
+ ...element(item.node)
117
+ }] : [])
118
+ });
119
+ }
120
+ if (!data.pages.length) return { data };
121
+ if (data.pages.some((item) => item.unsizedShift >= MIN_SHIFT)) return { data };
122
+ const unsized = data.pages.reduce((total, item) => total + item.unsizedImages.length, 0);
123
+ return {
124
+ data,
125
+ decision: {
126
+ status: "pass",
127
+ summary: `Images without dimensions cause no noticeable layout shift on ${data.pages.map((item) => item.page).join(", ")}${unsized ? `; Lighthouse lists ${unsized} image${unsized === 1 ? "" : "s"} without width and height, but their layout reserves the space` : ""}.`,
128
+ evidence: data.pages.map((item) => ({
129
+ type: "playhouse",
130
+ page: item.page,
131
+ artifact: item.lighthouseArtifact,
132
+ pointer: "/audits/cls-culprits-insight",
133
+ value: `CLS ${round(item.cls ?? 0)}, ${round(item.unsizedShift)} from unsized images`
134
+ }))
135
+ }
136
+ };
137
+ };
138
+ var image_dimensions_default = run;
139
+
140
+ //#endregion
141
+ export { dataSchema, image_dimensions_default as default };