@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.
- 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/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/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-BKCWlO9b.css +1 -0
- package/dist/audit/ui/assets/index-CUPt8e4A.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 +1896 -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 +11 -3
- 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 +3 -1
- 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.js +3 -3
- package/dist/index.js.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 +19 -19
- package/dist/mrt/ssr.mjs.map +1 -1
- package/dist/mrt/streamingHandler.mjs +26 -26
- 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 +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
|
-
#
|
|
141
|
-
pnpm sfnext
|
|
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 };
|