├── .gitignore ├── .mocharc.json ├── .npmignore ├── README.md ├── audits ├── soft-nav-fcp.js └── soft-nav-lcp.js ├── lib └── metric-timings.js ├── package-lock.json ├── package.json ├── plugin.js ├── test ├── e2e │ └── run.js └── unit │ └── lib │ └── metric-timings.test.js ├── tsconfig.json └── types └── global.d.ts /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode 2 | 3 | node_modules 4 | 5 | lighthouse-plugin-soft-navigation*.tgz 6 | *.report.html 7 | *.trace.json -------------------------------------------------------------------------------- /.mocharc.json: -------------------------------------------------------------------------------- 1 | { 2 | "spec": ["./test/**/*.test.js"] 3 | } -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .vscode 2 | 3 | test 4 | .mocharc.json 5 | 6 | types 7 | tsconfig.json 8 | 9 | lighthouse-plugin-soft-navigation*.tgz 10 | *.report.html 11 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Lighthouse Soft Navigation Plugin 2 | 3 | This is a plugin for [Lighthouse](https://github.com/GoogleChrome/lighthouse) that measures metrics such as FCP and LCP in a soft navigation. 4 | 5 | ![image](https://user-images.githubusercontent.com/6752989/220523511-9ec52d43-d0da-4765-96f7-0ed8a8edfa07.png) 6 | 7 | ## Definition 8 | 9 | A soft navigation is a same-document navigation triggered by user interaction that mutates the DOM and updates the page URL (e.g. using the [history API](https://developer.mozilla.org/en-US/docs/Web/API/History_API)). You can read more about the definition of a soft navigation over in [WICG/softnavigations](https://github.com/WICG/soft-navigations). 10 | 11 | ## Requirements 12 | 13 | > **Warning** 14 | > These features are necessary for this plugin to work but they are experimental and therefore subject to change without warning. I will try to keep the plugin and requirements up to date as things change. 15 | 16 | - Use the latest [Chrome Canary](https://www.google.com/chrome/canary/). 17 | - Launch Chrome with `--enable-experimental-web-platform-features` to enable soft navigation measurement. 18 | 19 | ## Installation 20 | 21 | Install with npm or yarn. Make sure Lighthouse and Puppeteer are installed as well. 22 | 23 | ```sh 24 | npm install lighthouse puppeteer lighthouse-plugin-soft-navigation 25 | ``` 26 | 27 | ```sh 28 | yarn add lighthouse puppeteer lighthouse-plugin-soft-navigation 29 | ``` 30 | 31 | ## Usage 32 | 33 | Include this plugin in your Lighthouse config so Lighthouse [timespan](https://github.com/GoogleChrome/lighthouse/blob/main/docs/user-flows.md#timespan) mode can audit interactions that trigger a soft navigation. 34 | 35 | ```js 36 | import fs from 'fs'; 37 | import puppeteer from 'puppeteer'; 38 | import {startFlow} from 'lighthouse'; 39 | 40 | const browser = await puppeteer.launch({ 41 | headless: false, 42 | args: ['--enable-experimental-web-platform-features'], 43 | // Make sure you are using the latest Chrome Canary. 44 | executablePath: '/path/to/chrome' 45 | }); 46 | const page = await browser.newPage(); 47 | 48 | const config = { 49 | extends: 'lighthouse:default', 50 | plugins: ['lighthouse-plugin-soft-navigation'], 51 | } 52 | 53 | const flow = await startFlow(page, {config}); 54 | 55 | // This will trigger a hard navigation. 56 | // This step will be like any normal navigation in the report. 57 | await flow.navigate('https://example.com'); 58 | 59 | // Clicking `a.link` will trigger a soft navigation. 60 | // This step will include soft navigation metrics from the plugin in the report. 61 | await flow.startTimespan(); 62 | await page.click('a.link'); 63 | await flow.endTimespan(); 64 | 65 | const report = await flow.generateReport(); 66 | fs.saveFileSync('report.html', report); 67 | ``` 68 | -------------------------------------------------------------------------------- /audits/soft-nav-fcp.js: -------------------------------------------------------------------------------- 1 | import {Audit} from 'lighthouse'; 2 | import {ProcessedTrace} from 'lighthouse/core/computed/processed-trace.js'; 3 | import {computeMetricTimings} from '../lib/metric-timings.js'; 4 | 5 | /** @typedef {import('lighthouse/types/audit.js').default.Meta} AuditMeta */ 6 | /** @typedef {import('lighthouse/types/audit.js').default.Product} AuditProduct */ 7 | /** @typedef {import('lighthouse/types/audit.js').default.Context} AuditContext */ 8 | 9 | class SoftNavFCP extends Audit { 10 | /** 11 | * @return {AuditMeta} 12 | */ 13 | static get meta() { 14 | return { 15 | id: 'soft-nav-fcp', 16 | title: 'Soft Navigation First Contentful Paint', 17 | description: 'First Contentful Paint of a soft navigation.', 18 | scoreDisplayMode: Audit.SCORING_MODES.NUMERIC, 19 | supportedModes: ['timespan'], 20 | requiredArtifacts: ['Trace'], 21 | }; 22 | } 23 | 24 | /** 25 | * @param {import('lighthouse').Artifacts} artifacts 26 | * @param {AuditContext} context 27 | * @return {Promise} 28 | */ 29 | static async audit(artifacts, context) { 30 | const trace = artifacts.Trace; 31 | const processedTrace = await ProcessedTrace.request(trace, context); 32 | 33 | const {fcpTiming} = computeMetricTimings(processedTrace.mainThreadEvents); 34 | if (!fcpTiming) { 35 | return { 36 | notApplicable: true, 37 | score: 1, 38 | } 39 | } 40 | 41 | return { 42 | numericValue: fcpTiming, 43 | numericUnit: 'millisecond', 44 | displayValue: `${fcpTiming} ms`, 45 | score: Audit.computeLogNormalScore({ 46 | p10: 1800, 47 | median: 3000 48 | }, fcpTiming) 49 | } 50 | } 51 | } 52 | 53 | export default SoftNavFCP; -------------------------------------------------------------------------------- /audits/soft-nav-lcp.js: -------------------------------------------------------------------------------- 1 | import {Audit} from 'lighthouse'; 2 | import {ProcessedTrace} from 'lighthouse/core/computed/processed-trace.js'; 3 | import {computeMetricTimings} from '../lib/metric-timings.js'; 4 | 5 | /** @typedef {import('lighthouse/types/audit.js').default.Meta} AuditMeta */ 6 | /** @typedef {import('lighthouse/types/audit.js').default.Product} AuditProduct */ 7 | /** @typedef {import('lighthouse/types/audit.js').default.Context} AuditContext */ 8 | 9 | class SoftNavLCP extends Audit { 10 | /** 11 | * @return {AuditMeta} 12 | */ 13 | static get meta() { 14 | return { 15 | id: 'soft-nav-lcp', 16 | title: 'Soft Navigation Largest Contentful Paint', 17 | description: 'Largest Contentful Paint of a soft navigation.', 18 | scoreDisplayMode: Audit.SCORING_MODES.NUMERIC, 19 | supportedModes: ['timespan'], 20 | requiredArtifacts: ['Trace'], 21 | }; 22 | } 23 | 24 | /** 25 | * @param {import('lighthouse').Artifacts} artifacts 26 | * @param {AuditContext} context 27 | * @return {Promise} 28 | */ 29 | static async audit(artifacts, context) { 30 | const trace = artifacts.Trace; 31 | const processedTrace = await ProcessedTrace.request(trace, context); 32 | 33 | const {lcpTiming} = computeMetricTimings(processedTrace.mainThreadEvents); 34 | if (!lcpTiming) { 35 | return { 36 | notApplicable: true, 37 | score: 1, 38 | } 39 | } 40 | 41 | return { 42 | numericValue: lcpTiming, 43 | numericUnit: 'millisecond', 44 | displayValue: `${lcpTiming} ms`, 45 | score: Audit.computeLogNormalScore({ 46 | p10: 2500, 47 | median: 4000 48 | }, lcpTiming) 49 | } 50 | } 51 | } 52 | 53 | export default SoftNavLCP; -------------------------------------------------------------------------------- /lib/metric-timings.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This is a re-implementation of how Lighthouse's `ProcessedNavigation` gets these metrics. 3 | * In theory we could use `ProcessedNavigation` once it can find a soft nav's `timeOrigin`. 4 | * 5 | * @param {import('lighthouse').TraceEvent[]} traceEvents trace events sorted by `ts` 6 | * @return {{lcpTiming?: number, fcpTiming?: number}} 7 | */ 8 | export function computeMetricTimings(traceEvents) { 9 | /** @type {import('lighthouse').TraceEvent|undefined} */ 10 | let softNavEvent; 11 | /** @type {import('lighthouse').TraceEvent|undefined} */ 12 | let lastLcpCandidate; 13 | /** @type {import('lighthouse').TraceEvent|undefined} */ 14 | let fcpEvent; 15 | 16 | for (const event of traceEvents) { 17 | switch (event.name) { 18 | case 'SoftNavigationHeuristics_SoftNavigationDetected': 19 | if (softNavEvent) throw new Error('Multiple soft navigations detected'); 20 | softNavEvent = event; 21 | break; 22 | case 'largestContentfulPaint::Candidate': 23 | if (softNavEvent) { 24 | lastLcpCandidate = event; 25 | } 26 | break; 27 | case 'firstContentfulPaint': 28 | if (softNavEvent) { 29 | fcpEvent = event; 30 | } 31 | break; 32 | } 33 | } 34 | 35 | if (!softNavEvent) return {}; 36 | const timeOriginTs = softNavEvent.ts; 37 | 38 | /** 39 | * @param {import('lighthouse').TraceEvent|undefined} event 40 | * @return {number|undefined} 41 | */ 42 | const getTiming = (event) => { 43 | if (!event) return undefined; 44 | return (event.ts - timeOriginTs) / 1000; 45 | } 46 | 47 | return { 48 | lcpTiming: getTiming(lastLcpCandidate), 49 | fcpTiming: getTiming(fcpEvent), 50 | } 51 | } -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lighthouse-plugin-soft-navigation", 3 | "version": "1.1.0", 4 | "lockfileVersion": 3, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "lighthouse-plugin-soft-navigation", 9 | "version": "1.1.0", 10 | "license": "ISC", 11 | "devDependencies": { 12 | "@types/mocha": "^10.0.1", 13 | "lighthouse": "^10.0.1", 14 | "mocha": "^10.2.0", 15 | "puppeteer": "^19.7.1", 16 | "typescript": "^4.9.5" 17 | }, 18 | "peerDependencies": { 19 | "lighthouse": ">=10.0.0" 20 | } 21 | }, 22 | "node_modules/@babel/code-frame": { 23 | "version": "7.18.6", 24 | "dev": true, 25 | "license": "MIT", 26 | "dependencies": { 27 | "@babel/highlight": "^7.18.6" 28 | }, 29 | "engines": { 30 | "node": ">=6.9.0" 31 | } 32 | }, 33 | "node_modules/@babel/helper-validator-identifier": { 34 | "version": "7.19.1", 35 | "dev": true, 36 | "license": "MIT", 37 | "engines": { 38 | "node": ">=6.9.0" 39 | } 40 | }, 41 | "node_modules/@babel/highlight": { 42 | "version": "7.18.6", 43 | "dev": true, 44 | "license": "MIT", 45 | "dependencies": { 46 | "@babel/helper-validator-identifier": "^7.18.6", 47 | "chalk": "^2.0.0", 48 | "js-tokens": "^4.0.0" 49 | }, 50 | "engines": { 51 | "node": ">=6.9.0" 52 | } 53 | }, 54 | "node_modules/@sentry/core": { 55 | "version": "6.19.7", 56 | "dev": true, 57 | "license": "BSD-3-Clause", 58 | "dependencies": { 59 | "@sentry/hub": "6.19.7", 60 | "@sentry/minimal": "6.19.7", 61 | "@sentry/types": "6.19.7", 62 | "@sentry/utils": "6.19.7", 63 | "tslib": "^1.9.3" 64 | }, 65 | "engines": { 66 | "node": ">=6" 67 | } 68 | }, 69 | "node_modules/@sentry/hub": { 70 | "version": "6.19.7", 71 | "dev": true, 72 | "license": "BSD-3-Clause", 73 | "dependencies": { 74 | "@sentry/types": "6.19.7", 75 | "@sentry/utils": "6.19.7", 76 | "tslib": "^1.9.3" 77 | }, 78 | "engines": { 79 | "node": ">=6" 80 | } 81 | }, 82 | "node_modules/@sentry/minimal": { 83 | "version": "6.19.7", 84 | "dev": true, 85 | "license": "BSD-3-Clause", 86 | "dependencies": { 87 | "@sentry/hub": "6.19.7", 88 | "@sentry/types": "6.19.7", 89 | "tslib": "^1.9.3" 90 | }, 91 | "engines": { 92 | "node": ">=6" 93 | } 94 | }, 95 | "node_modules/@sentry/node": { 96 | "version": "6.19.7", 97 | "dev": true, 98 | "license": "BSD-3-Clause", 99 | "dependencies": { 100 | "@sentry/core": "6.19.7", 101 | "@sentry/hub": "6.19.7", 102 | "@sentry/types": "6.19.7", 103 | "@sentry/utils": "6.19.7", 104 | "cookie": "^0.4.1", 105 | "https-proxy-agent": "^5.0.0", 106 | "lru_map": "^0.3.3", 107 | "tslib": "^1.9.3" 108 | }, 109 | "engines": { 110 | "node": ">=6" 111 | } 112 | }, 113 | "node_modules/@sentry/types": { 114 | "version": "6.19.7", 115 | "dev": true, 116 | "license": "BSD-3-Clause", 117 | "engines": { 118 | "node": ">=6" 119 | } 120 | }, 121 | "node_modules/@sentry/utils": { 122 | "version": "6.19.7", 123 | "dev": true, 124 | "license": "BSD-3-Clause", 125 | "dependencies": { 126 | "@sentry/types": "6.19.7", 127 | "tslib": "^1.9.3" 128 | }, 129 | "engines": { 130 | "node": ">=6" 131 | } 132 | }, 133 | "node_modules/@types/mocha": { 134 | "version": "10.0.1", 135 | "dev": true, 136 | "license": "MIT" 137 | }, 138 | "node_modules/@types/node": { 139 | "version": "18.13.0", 140 | "dev": true, 141 | "license": "MIT" 142 | }, 143 | "node_modules/@types/yauzl": { 144 | "version": "2.10.0", 145 | "dev": true, 146 | "license": "MIT", 147 | "optional": true, 148 | "dependencies": { 149 | "@types/node": "*" 150 | } 151 | }, 152 | "node_modules/agent-base": { 153 | "version": "6.0.2", 154 | "dev": true, 155 | "license": "MIT", 156 | "dependencies": { 157 | "debug": "4" 158 | }, 159 | "engines": { 160 | "node": ">= 6.0.0" 161 | } 162 | }, 163 | "node_modules/ansi-colors": { 164 | "version": "4.1.3", 165 | "dev": true, 166 | "license": "MIT", 167 | "engines": { 168 | "node": ">=6" 169 | } 170 | }, 171 | "node_modules/ansi-regex": { 172 | "version": "5.0.1", 173 | "dev": true, 174 | "license": "MIT", 175 | "engines": { 176 | "node": ">=8" 177 | } 178 | }, 179 | "node_modules/ansi-styles": { 180 | "version": "4.3.0", 181 | "dev": true, 182 | "license": "MIT", 183 | "dependencies": { 184 | "color-convert": "^2.0.1" 185 | }, 186 | "engines": { 187 | "node": ">=8" 188 | }, 189 | "funding": { 190 | "url": "https://github.com/chalk/ansi-styles?sponsor=1" 191 | } 192 | }, 193 | "node_modules/anymatch": { 194 | "version": "3.1.3", 195 | "dev": true, 196 | "license": "ISC", 197 | "dependencies": { 198 | "normalize-path": "^3.0.0", 199 | "picomatch": "^2.0.4" 200 | }, 201 | "engines": { 202 | "node": ">= 8" 203 | } 204 | }, 205 | "node_modules/argparse": { 206 | "version": "2.0.1", 207 | "dev": true, 208 | "license": "Python-2.0" 209 | }, 210 | "node_modules/axe-core": { 211 | "version": "4.6.3", 212 | "dev": true, 213 | "license": "MPL-2.0", 214 | "engines": { 215 | "node": ">=4" 216 | } 217 | }, 218 | "node_modules/balanced-match": { 219 | "version": "1.0.2", 220 | "dev": true, 221 | "license": "MIT" 222 | }, 223 | "node_modules/base64-js": { 224 | "version": "1.5.1", 225 | "dev": true, 226 | "funding": [ 227 | { 228 | "type": "github", 229 | "url": "https://github.com/sponsors/feross" 230 | }, 231 | { 232 | "type": "patreon", 233 | "url": "https://www.patreon.com/feross" 234 | }, 235 | { 236 | "type": "consulting", 237 | "url": "https://feross.org/support" 238 | } 239 | ], 240 | "license": "MIT" 241 | }, 242 | "node_modules/binary-extensions": { 243 | "version": "2.2.0", 244 | "dev": true, 245 | "license": "MIT", 246 | "engines": { 247 | "node": ">=8" 248 | } 249 | }, 250 | "node_modules/bl": { 251 | "version": "4.1.0", 252 | "dev": true, 253 | "license": "MIT", 254 | "dependencies": { 255 | "buffer": "^5.5.0", 256 | "inherits": "^2.0.4", 257 | "readable-stream": "^3.4.0" 258 | } 259 | }, 260 | "node_modules/brace-expansion": { 261 | "version": "1.1.11", 262 | "dev": true, 263 | "license": "MIT", 264 | "dependencies": { 265 | "balanced-match": "^1.0.0", 266 | "concat-map": "0.0.1" 267 | } 268 | }, 269 | "node_modules/braces": { 270 | "version": "3.0.2", 271 | "dev": true, 272 | "license": "MIT", 273 | "dependencies": { 274 | "fill-range": "^7.0.1" 275 | }, 276 | "engines": { 277 | "node": ">=8" 278 | } 279 | }, 280 | "node_modules/browser-stdout": { 281 | "version": "1.3.1", 282 | "dev": true, 283 | "license": "ISC" 284 | }, 285 | "node_modules/buffer": { 286 | "version": "5.7.1", 287 | "dev": true, 288 | "funding": [ 289 | { 290 | "type": "github", 291 | "url": "https://github.com/sponsors/feross" 292 | }, 293 | { 294 | "type": "patreon", 295 | "url": "https://www.patreon.com/feross" 296 | }, 297 | { 298 | "type": "consulting", 299 | "url": "https://feross.org/support" 300 | } 301 | ], 302 | "license": "MIT", 303 | "dependencies": { 304 | "base64-js": "^1.3.1", 305 | "ieee754": "^1.1.13" 306 | } 307 | }, 308 | "node_modules/buffer-crc32": { 309 | "version": "0.2.13", 310 | "dev": true, 311 | "license": "MIT", 312 | "engines": { 313 | "node": "*" 314 | } 315 | }, 316 | "node_modules/callsites": { 317 | "version": "3.1.0", 318 | "dev": true, 319 | "license": "MIT", 320 | "engines": { 321 | "node": ">=6" 322 | } 323 | }, 324 | "node_modules/camelcase": { 325 | "version": "6.3.0", 326 | "dev": true, 327 | "license": "MIT", 328 | "engines": { 329 | "node": ">=10" 330 | }, 331 | "funding": { 332 | "url": "https://github.com/sponsors/sindresorhus" 333 | } 334 | }, 335 | "node_modules/chalk": { 336 | "version": "2.4.2", 337 | "dev": true, 338 | "license": "MIT", 339 | "dependencies": { 340 | "ansi-styles": "^3.2.1", 341 | "escape-string-regexp": "^1.0.5", 342 | "supports-color": "^5.3.0" 343 | }, 344 | "engines": { 345 | "node": ">=4" 346 | } 347 | }, 348 | "node_modules/chalk/node_modules/ansi-styles": { 349 | "version": "3.2.1", 350 | "dev": true, 351 | "license": "MIT", 352 | "dependencies": { 353 | "color-convert": "^1.9.0" 354 | }, 355 | "engines": { 356 | "node": ">=4" 357 | } 358 | }, 359 | "node_modules/chalk/node_modules/color-convert": { 360 | "version": "1.9.3", 361 | "dev": true, 362 | "license": "MIT", 363 | "dependencies": { 364 | "color-name": "1.1.3" 365 | } 366 | }, 367 | "node_modules/chalk/node_modules/color-name": { 368 | "version": "1.1.3", 369 | "dev": true, 370 | "license": "MIT" 371 | }, 372 | "node_modules/chalk/node_modules/escape-string-regexp": { 373 | "version": "1.0.5", 374 | "dev": true, 375 | "license": "MIT", 376 | "engines": { 377 | "node": ">=0.8.0" 378 | } 379 | }, 380 | "node_modules/chokidar": { 381 | "version": "3.5.3", 382 | "dev": true, 383 | "funding": [ 384 | { 385 | "type": "individual", 386 | "url": "https://paulmillr.com/funding/" 387 | } 388 | ], 389 | "license": "MIT", 390 | "dependencies": { 391 | "anymatch": "~3.1.2", 392 | "braces": "~3.0.2", 393 | "glob-parent": "~5.1.2", 394 | "is-binary-path": "~2.1.0", 395 | "is-glob": "~4.0.1", 396 | "normalize-path": "~3.0.0", 397 | "readdirp": "~3.6.0" 398 | }, 399 | "engines": { 400 | "node": ">= 8.10.0" 401 | }, 402 | "optionalDependencies": { 403 | "fsevents": "~2.3.2" 404 | } 405 | }, 406 | "node_modules/chownr": { 407 | "version": "1.1.4", 408 | "dev": true, 409 | "license": "ISC" 410 | }, 411 | "node_modules/chrome-launcher": { 412 | "version": "0.15.1", 413 | "dev": true, 414 | "license": "Apache-2.0", 415 | "dependencies": { 416 | "@types/node": "*", 417 | "escape-string-regexp": "^4.0.0", 418 | "is-wsl": "^2.2.0", 419 | "lighthouse-logger": "^1.0.0" 420 | }, 421 | "bin": { 422 | "print-chrome-path": "bin/print-chrome-path.js" 423 | }, 424 | "engines": { 425 | "node": ">=12.13.0" 426 | } 427 | }, 428 | "node_modules/cliui": { 429 | "version": "8.0.1", 430 | "dev": true, 431 | "license": "ISC", 432 | "dependencies": { 433 | "string-width": "^4.2.0", 434 | "strip-ansi": "^6.0.1", 435 | "wrap-ansi": "^7.0.0" 436 | }, 437 | "engines": { 438 | "node": ">=12" 439 | } 440 | }, 441 | "node_modules/color-convert": { 442 | "version": "2.0.1", 443 | "dev": true, 444 | "license": "MIT", 445 | "dependencies": { 446 | "color-name": "~1.1.4" 447 | }, 448 | "engines": { 449 | "node": ">=7.0.0" 450 | } 451 | }, 452 | "node_modules/color-name": { 453 | "version": "1.1.4", 454 | "dev": true, 455 | "license": "MIT" 456 | }, 457 | "node_modules/concat-map": { 458 | "version": "0.0.1", 459 | "dev": true, 460 | "license": "MIT" 461 | }, 462 | "node_modules/configstore": { 463 | "version": "5.0.1", 464 | "dev": true, 465 | "license": "BSD-2-Clause", 466 | "dependencies": { 467 | "dot-prop": "^5.2.0", 468 | "graceful-fs": "^4.1.2", 469 | "make-dir": "^3.0.0", 470 | "unique-string": "^2.0.0", 471 | "write-file-atomic": "^3.0.0", 472 | "xdg-basedir": "^4.0.0" 473 | }, 474 | "engines": { 475 | "node": ">=8" 476 | } 477 | }, 478 | "node_modules/cookie": { 479 | "version": "0.4.2", 480 | "dev": true, 481 | "license": "MIT", 482 | "engines": { 483 | "node": ">= 0.6" 484 | } 485 | }, 486 | "node_modules/cosmiconfig": { 487 | "version": "8.0.0", 488 | "dev": true, 489 | "license": "MIT", 490 | "dependencies": { 491 | "import-fresh": "^3.2.1", 492 | "js-yaml": "^4.1.0", 493 | "parse-json": "^5.0.0", 494 | "path-type": "^4.0.0" 495 | }, 496 | "engines": { 497 | "node": ">=14" 498 | } 499 | }, 500 | "node_modules/cross-fetch": { 501 | "version": "3.1.5", 502 | "dev": true, 503 | "license": "MIT", 504 | "dependencies": { 505 | "node-fetch": "2.6.7" 506 | } 507 | }, 508 | "node_modules/crypto-random-string": { 509 | "version": "2.0.0", 510 | "dev": true, 511 | "license": "MIT", 512 | "engines": { 513 | "node": ">=8" 514 | } 515 | }, 516 | "node_modules/csp_evaluator": { 517 | "version": "1.1.1", 518 | "dev": true, 519 | "license": "Apache-2.0" 520 | }, 521 | "node_modules/debug": { 522 | "version": "4.3.4", 523 | "dev": true, 524 | "license": "MIT", 525 | "dependencies": { 526 | "ms": "2.1.2" 527 | }, 528 | "engines": { 529 | "node": ">=6.0" 530 | }, 531 | "peerDependenciesMeta": { 532 | "supports-color": { 533 | "optional": true 534 | } 535 | } 536 | }, 537 | "node_modules/decamelize": { 538 | "version": "4.0.0", 539 | "dev": true, 540 | "license": "MIT", 541 | "engines": { 542 | "node": ">=10" 543 | }, 544 | "funding": { 545 | "url": "https://github.com/sponsors/sindresorhus" 546 | } 547 | }, 548 | "node_modules/define-lazy-prop": { 549 | "version": "2.0.0", 550 | "dev": true, 551 | "license": "MIT", 552 | "engines": { 553 | "node": ">=8" 554 | } 555 | }, 556 | "node_modules/devtools-protocol": { 557 | "version": "0.0.1094867", 558 | "dev": true, 559 | "license": "BSD-3-Clause" 560 | }, 561 | "node_modules/diff": { 562 | "version": "5.0.0", 563 | "dev": true, 564 | "license": "BSD-3-Clause", 565 | "engines": { 566 | "node": ">=0.3.1" 567 | } 568 | }, 569 | "node_modules/dot-prop": { 570 | "version": "5.3.0", 571 | "dev": true, 572 | "license": "MIT", 573 | "dependencies": { 574 | "is-obj": "^2.0.0" 575 | }, 576 | "engines": { 577 | "node": ">=8" 578 | } 579 | }, 580 | "node_modules/emoji-regex": { 581 | "version": "8.0.0", 582 | "dev": true, 583 | "license": "MIT" 584 | }, 585 | "node_modules/end-of-stream": { 586 | "version": "1.4.4", 587 | "dev": true, 588 | "license": "MIT", 589 | "dependencies": { 590 | "once": "^1.4.0" 591 | } 592 | }, 593 | "node_modules/enquirer": { 594 | "version": "2.3.6", 595 | "dev": true, 596 | "license": "MIT", 597 | "dependencies": { 598 | "ansi-colors": "^4.1.1" 599 | }, 600 | "engines": { 601 | "node": ">=8.6" 602 | } 603 | }, 604 | "node_modules/error-ex": { 605 | "version": "1.3.2", 606 | "dev": true, 607 | "license": "MIT", 608 | "dependencies": { 609 | "is-arrayish": "^0.2.1" 610 | } 611 | }, 612 | "node_modules/escalade": { 613 | "version": "3.1.1", 614 | "dev": true, 615 | "license": "MIT", 616 | "engines": { 617 | "node": ">=6" 618 | } 619 | }, 620 | "node_modules/escape-string-regexp": { 621 | "version": "4.0.0", 622 | "dev": true, 623 | "license": "MIT", 624 | "engines": { 625 | "node": ">=10" 626 | }, 627 | "funding": { 628 | "url": "https://github.com/sponsors/sindresorhus" 629 | } 630 | }, 631 | "node_modules/extract-zip": { 632 | "version": "2.0.1", 633 | "dev": true, 634 | "license": "BSD-2-Clause", 635 | "dependencies": { 636 | "debug": "^4.1.1", 637 | "get-stream": "^5.1.0", 638 | "yauzl": "^2.10.0" 639 | }, 640 | "bin": { 641 | "extract-zip": "cli.js" 642 | }, 643 | "engines": { 644 | "node": ">= 10.17.0" 645 | }, 646 | "optionalDependencies": { 647 | "@types/yauzl": "^2.9.1" 648 | } 649 | }, 650 | "node_modules/fd-slicer": { 651 | "version": "1.1.0", 652 | "dev": true, 653 | "license": "MIT", 654 | "dependencies": { 655 | "pend": "~1.2.0" 656 | } 657 | }, 658 | "node_modules/fill-range": { 659 | "version": "7.0.1", 660 | "dev": true, 661 | "license": "MIT", 662 | "dependencies": { 663 | "to-regex-range": "^5.0.1" 664 | }, 665 | "engines": { 666 | "node": ">=8" 667 | } 668 | }, 669 | "node_modules/find-up": { 670 | "version": "5.0.0", 671 | "dev": true, 672 | "license": "MIT", 673 | "dependencies": { 674 | "locate-path": "^6.0.0", 675 | "path-exists": "^4.0.0" 676 | }, 677 | "engines": { 678 | "node": ">=10" 679 | }, 680 | "funding": { 681 | "url": "https://github.com/sponsors/sindresorhus" 682 | } 683 | }, 684 | "node_modules/flat": { 685 | "version": "5.0.2", 686 | "dev": true, 687 | "license": "BSD-3-Clause", 688 | "bin": { 689 | "flat": "cli.js" 690 | } 691 | }, 692 | "node_modules/fs-constants": { 693 | "version": "1.0.0", 694 | "dev": true, 695 | "license": "MIT" 696 | }, 697 | "node_modules/fs.realpath": { 698 | "version": "1.0.0", 699 | "dev": true, 700 | "license": "ISC" 701 | }, 702 | "node_modules/get-caller-file": { 703 | "version": "2.0.5", 704 | "dev": true, 705 | "license": "ISC", 706 | "engines": { 707 | "node": "6.* || 8.* || >= 10.*" 708 | } 709 | }, 710 | "node_modules/get-stream": { 711 | "version": "5.2.0", 712 | "dev": true, 713 | "license": "MIT", 714 | "dependencies": { 715 | "pump": "^3.0.0" 716 | }, 717 | "engines": { 718 | "node": ">=8" 719 | }, 720 | "funding": { 721 | "url": "https://github.com/sponsors/sindresorhus" 722 | } 723 | }, 724 | "node_modules/glob": { 725 | "version": "7.2.3", 726 | "dev": true, 727 | "license": "ISC", 728 | "dependencies": { 729 | "fs.realpath": "^1.0.0", 730 | "inflight": "^1.0.4", 731 | "inherits": "2", 732 | "minimatch": "^3.1.1", 733 | "once": "^1.3.0", 734 | "path-is-absolute": "^1.0.0" 735 | }, 736 | "engines": { 737 | "node": "*" 738 | }, 739 | "funding": { 740 | "url": "https://github.com/sponsors/isaacs" 741 | } 742 | }, 743 | "node_modules/glob-parent": { 744 | "version": "5.1.2", 745 | "dev": true, 746 | "license": "ISC", 747 | "dependencies": { 748 | "is-glob": "^4.0.1" 749 | }, 750 | "engines": { 751 | "node": ">= 6" 752 | } 753 | }, 754 | "node_modules/graceful-fs": { 755 | "version": "4.2.10", 756 | "dev": true, 757 | "license": "ISC" 758 | }, 759 | "node_modules/has-flag": { 760 | "version": "3.0.0", 761 | "dev": true, 762 | "license": "MIT", 763 | "engines": { 764 | "node": ">=4" 765 | } 766 | }, 767 | "node_modules/he": { 768 | "version": "1.2.0", 769 | "dev": true, 770 | "license": "MIT", 771 | "bin": { 772 | "he": "bin/he" 773 | } 774 | }, 775 | "node_modules/http-link-header": { 776 | "version": "0.8.0", 777 | "dev": true, 778 | "license": "MIT" 779 | }, 780 | "node_modules/https-proxy-agent": { 781 | "version": "5.0.1", 782 | "dev": true, 783 | "license": "MIT", 784 | "dependencies": { 785 | "agent-base": "6", 786 | "debug": "4" 787 | }, 788 | "engines": { 789 | "node": ">= 6" 790 | } 791 | }, 792 | "node_modules/ieee754": { 793 | "version": "1.2.1", 794 | "dev": true, 795 | "funding": [ 796 | { 797 | "type": "github", 798 | "url": "https://github.com/sponsors/feross" 799 | }, 800 | { 801 | "type": "patreon", 802 | "url": "https://www.patreon.com/feross" 803 | }, 804 | { 805 | "type": "consulting", 806 | "url": "https://feross.org/support" 807 | } 808 | ], 809 | "license": "BSD-3-Clause" 810 | }, 811 | "node_modules/image-ssim": { 812 | "version": "0.2.0", 813 | "dev": true, 814 | "license": "MIT" 815 | }, 816 | "node_modules/import-fresh": { 817 | "version": "3.3.0", 818 | "dev": true, 819 | "license": "MIT", 820 | "dependencies": { 821 | "parent-module": "^1.0.0", 822 | "resolve-from": "^4.0.0" 823 | }, 824 | "engines": { 825 | "node": ">=6" 826 | }, 827 | "funding": { 828 | "url": "https://github.com/sponsors/sindresorhus" 829 | } 830 | }, 831 | "node_modules/imurmurhash": { 832 | "version": "0.1.4", 833 | "dev": true, 834 | "license": "MIT", 835 | "engines": { 836 | "node": ">=0.8.19" 837 | } 838 | }, 839 | "node_modules/inflight": { 840 | "version": "1.0.6", 841 | "dev": true, 842 | "license": "ISC", 843 | "dependencies": { 844 | "once": "^1.3.0", 845 | "wrappy": "1" 846 | } 847 | }, 848 | "node_modules/inherits": { 849 | "version": "2.0.4", 850 | "dev": true, 851 | "license": "ISC" 852 | }, 853 | "node_modules/intl-messageformat": { 854 | "version": "4.4.0", 855 | "dev": true, 856 | "license": "BSD-3-Clause", 857 | "dependencies": { 858 | "intl-messageformat-parser": "^1.8.1" 859 | } 860 | }, 861 | "node_modules/intl-messageformat-parser": { 862 | "version": "1.8.1", 863 | "dev": true, 864 | "license": "BSD-3-Clause" 865 | }, 866 | "node_modules/is-arrayish": { 867 | "version": "0.2.1", 868 | "dev": true, 869 | "license": "MIT" 870 | }, 871 | "node_modules/is-binary-path": { 872 | "version": "2.1.0", 873 | "dev": true, 874 | "license": "MIT", 875 | "dependencies": { 876 | "binary-extensions": "^2.0.0" 877 | }, 878 | "engines": { 879 | "node": ">=8" 880 | } 881 | }, 882 | "node_modules/is-docker": { 883 | "version": "2.2.1", 884 | "dev": true, 885 | "license": "MIT", 886 | "bin": { 887 | "is-docker": "cli.js" 888 | }, 889 | "engines": { 890 | "node": ">=8" 891 | }, 892 | "funding": { 893 | "url": "https://github.com/sponsors/sindresorhus" 894 | } 895 | }, 896 | "node_modules/is-extglob": { 897 | "version": "2.1.1", 898 | "dev": true, 899 | "license": "MIT", 900 | "engines": { 901 | "node": ">=0.10.0" 902 | } 903 | }, 904 | "node_modules/is-fullwidth-code-point": { 905 | "version": "3.0.0", 906 | "dev": true, 907 | "license": "MIT", 908 | "engines": { 909 | "node": ">=8" 910 | } 911 | }, 912 | "node_modules/is-glob": { 913 | "version": "4.0.3", 914 | "dev": true, 915 | "license": "MIT", 916 | "dependencies": { 917 | "is-extglob": "^2.1.1" 918 | }, 919 | "engines": { 920 | "node": ">=0.10.0" 921 | } 922 | }, 923 | "node_modules/is-number": { 924 | "version": "7.0.0", 925 | "dev": true, 926 | "license": "MIT", 927 | "engines": { 928 | "node": ">=0.12.0" 929 | } 930 | }, 931 | "node_modules/is-obj": { 932 | "version": "2.0.0", 933 | "dev": true, 934 | "license": "MIT", 935 | "engines": { 936 | "node": ">=8" 937 | } 938 | }, 939 | "node_modules/is-plain-obj": { 940 | "version": "2.1.0", 941 | "dev": true, 942 | "license": "MIT", 943 | "engines": { 944 | "node": ">=8" 945 | } 946 | }, 947 | "node_modules/is-typedarray": { 948 | "version": "1.0.0", 949 | "dev": true, 950 | "license": "MIT" 951 | }, 952 | "node_modules/is-unicode-supported": { 953 | "version": "0.1.0", 954 | "dev": true, 955 | "license": "MIT", 956 | "engines": { 957 | "node": ">=10" 958 | }, 959 | "funding": { 960 | "url": "https://github.com/sponsors/sindresorhus" 961 | } 962 | }, 963 | "node_modules/is-wsl": { 964 | "version": "2.2.0", 965 | "dev": true, 966 | "license": "MIT", 967 | "dependencies": { 968 | "is-docker": "^2.0.0" 969 | }, 970 | "engines": { 971 | "node": ">=8" 972 | } 973 | }, 974 | "node_modules/jpeg-js": { 975 | "version": "0.4.4", 976 | "dev": true, 977 | "license": "BSD-3-Clause" 978 | }, 979 | "node_modules/js-library-detector": { 980 | "version": "6.6.0", 981 | "dev": true, 982 | "license": "MIT", 983 | "engines": { 984 | "node": ">=12" 985 | } 986 | }, 987 | "node_modules/js-tokens": { 988 | "version": "4.0.0", 989 | "dev": true, 990 | "license": "MIT" 991 | }, 992 | "node_modules/js-yaml": { 993 | "version": "4.1.0", 994 | "dev": true, 995 | "license": "MIT", 996 | "dependencies": { 997 | "argparse": "^2.0.1" 998 | }, 999 | "bin": { 1000 | "js-yaml": "bin/js-yaml.js" 1001 | } 1002 | }, 1003 | "node_modules/json-parse-even-better-errors": { 1004 | "version": "2.3.1", 1005 | "dev": true, 1006 | "license": "MIT" 1007 | }, 1008 | "node_modules/lighthouse": { 1009 | "version": "10.0.1", 1010 | "dev": true, 1011 | "license": "Apache-2.0", 1012 | "dependencies": { 1013 | "@sentry/node": "^6.17.4", 1014 | "axe-core": "4.6.3", 1015 | "chrome-launcher": "^0.15.1", 1016 | "configstore": "^5.0.1", 1017 | "csp_evaluator": "1.1.1", 1018 | "enquirer": "^2.3.6", 1019 | "http-link-header": "^0.8.0", 1020 | "intl-messageformat": "^4.4.0", 1021 | "jpeg-js": "^0.4.4", 1022 | "js-library-detector": "^6.6.0", 1023 | "lighthouse-logger": "^1.3.0", 1024 | "lighthouse-stack-packs": "1.9.1", 1025 | "lodash": "^4.17.21", 1026 | "lookup-closest-locale": "6.2.0", 1027 | "metaviewport-parser": "0.3.0", 1028 | "open": "^8.4.0", 1029 | "parse-cache-control": "1.0.1", 1030 | "ps-list": "^8.0.0", 1031 | "puppeteer-core": "^19.6.0", 1032 | "robots-parser": "^3.0.0", 1033 | "semver": "^5.3.0", 1034 | "speedline-core": "^1.4.3", 1035 | "third-party-web": "^0.20.2", 1036 | "ws": "^7.0.0", 1037 | "yargs": "^17.3.1", 1038 | "yargs-parser": "^21.0.0" 1039 | }, 1040 | "bin": { 1041 | "chrome-debug": "core/scripts/manual-chrome-launcher.js", 1042 | "lighthouse": "cli/index.js", 1043 | "smokehouse": "cli/test/smokehouse/frontends/smokehouse-bin.js" 1044 | }, 1045 | "engines": { 1046 | "node": ">=16.16" 1047 | } 1048 | }, 1049 | "node_modules/lighthouse-logger": { 1050 | "version": "1.3.0", 1051 | "dev": true, 1052 | "license": "Apache-2.0", 1053 | "dependencies": { 1054 | "debug": "^2.6.9", 1055 | "marky": "^1.2.2" 1056 | } 1057 | }, 1058 | "node_modules/lighthouse-logger/node_modules/debug": { 1059 | "version": "2.6.9", 1060 | "dev": true, 1061 | "license": "MIT", 1062 | "dependencies": { 1063 | "ms": "2.0.0" 1064 | } 1065 | }, 1066 | "node_modules/lighthouse-logger/node_modules/ms": { 1067 | "version": "2.0.0", 1068 | "dev": true, 1069 | "license": "MIT" 1070 | }, 1071 | "node_modules/lighthouse-stack-packs": { 1072 | "version": "1.9.1", 1073 | "dev": true, 1074 | "license": "Apache-2.0" 1075 | }, 1076 | "node_modules/lines-and-columns": { 1077 | "version": "1.2.4", 1078 | "dev": true, 1079 | "license": "MIT" 1080 | }, 1081 | "node_modules/locate-path": { 1082 | "version": "6.0.0", 1083 | "dev": true, 1084 | "license": "MIT", 1085 | "dependencies": { 1086 | "p-locate": "^5.0.0" 1087 | }, 1088 | "engines": { 1089 | "node": ">=10" 1090 | }, 1091 | "funding": { 1092 | "url": "https://github.com/sponsors/sindresorhus" 1093 | } 1094 | }, 1095 | "node_modules/lodash": { 1096 | "version": "4.17.21", 1097 | "dev": true, 1098 | "license": "MIT" 1099 | }, 1100 | "node_modules/log-symbols": { 1101 | "version": "4.1.0", 1102 | "dev": true, 1103 | "license": "MIT", 1104 | "dependencies": { 1105 | "chalk": "^4.1.0", 1106 | "is-unicode-supported": "^0.1.0" 1107 | }, 1108 | "engines": { 1109 | "node": ">=10" 1110 | }, 1111 | "funding": { 1112 | "url": "https://github.com/sponsors/sindresorhus" 1113 | } 1114 | }, 1115 | "node_modules/log-symbols/node_modules/chalk": { 1116 | "version": "4.1.2", 1117 | "dev": true, 1118 | "license": "MIT", 1119 | "dependencies": { 1120 | "ansi-styles": "^4.1.0", 1121 | "supports-color": "^7.1.0" 1122 | }, 1123 | "engines": { 1124 | "node": ">=10" 1125 | }, 1126 | "funding": { 1127 | "url": "https://github.com/chalk/chalk?sponsor=1" 1128 | } 1129 | }, 1130 | "node_modules/log-symbols/node_modules/has-flag": { 1131 | "version": "4.0.0", 1132 | "dev": true, 1133 | "license": "MIT", 1134 | "engines": { 1135 | "node": ">=8" 1136 | } 1137 | }, 1138 | "node_modules/log-symbols/node_modules/supports-color": { 1139 | "version": "7.2.0", 1140 | "dev": true, 1141 | "license": "MIT", 1142 | "dependencies": { 1143 | "has-flag": "^4.0.0" 1144 | }, 1145 | "engines": { 1146 | "node": ">=8" 1147 | } 1148 | }, 1149 | "node_modules/lookup-closest-locale": { 1150 | "version": "6.2.0", 1151 | "dev": true, 1152 | "license": "MIT" 1153 | }, 1154 | "node_modules/lru_map": { 1155 | "version": "0.3.3", 1156 | "dev": true, 1157 | "license": "MIT" 1158 | }, 1159 | "node_modules/make-dir": { 1160 | "version": "3.1.0", 1161 | "dev": true, 1162 | "license": "MIT", 1163 | "dependencies": { 1164 | "semver": "^6.0.0" 1165 | }, 1166 | "engines": { 1167 | "node": ">=8" 1168 | }, 1169 | "funding": { 1170 | "url": "https://github.com/sponsors/sindresorhus" 1171 | } 1172 | }, 1173 | "node_modules/make-dir/node_modules/semver": { 1174 | "version": "6.3.0", 1175 | "dev": true, 1176 | "license": "ISC", 1177 | "bin": { 1178 | "semver": "bin/semver.js" 1179 | } 1180 | }, 1181 | "node_modules/marky": { 1182 | "version": "1.2.5", 1183 | "dev": true, 1184 | "license": "Apache-2.0" 1185 | }, 1186 | "node_modules/metaviewport-parser": { 1187 | "version": "0.3.0", 1188 | "dev": true, 1189 | "license": "MIT" 1190 | }, 1191 | "node_modules/minimatch": { 1192 | "version": "3.1.2", 1193 | "dev": true, 1194 | "license": "ISC", 1195 | "dependencies": { 1196 | "brace-expansion": "^1.1.7" 1197 | }, 1198 | "engines": { 1199 | "node": "*" 1200 | } 1201 | }, 1202 | "node_modules/mkdirp-classic": { 1203 | "version": "0.5.3", 1204 | "dev": true, 1205 | "license": "MIT" 1206 | }, 1207 | "node_modules/mocha": { 1208 | "version": "10.2.0", 1209 | "dev": true, 1210 | "license": "MIT", 1211 | "dependencies": { 1212 | "ansi-colors": "4.1.1", 1213 | "browser-stdout": "1.3.1", 1214 | "chokidar": "3.5.3", 1215 | "debug": "4.3.4", 1216 | "diff": "5.0.0", 1217 | "escape-string-regexp": "4.0.0", 1218 | "find-up": "5.0.0", 1219 | "glob": "7.2.0", 1220 | "he": "1.2.0", 1221 | "js-yaml": "4.1.0", 1222 | "log-symbols": "4.1.0", 1223 | "minimatch": "5.0.1", 1224 | "ms": "2.1.3", 1225 | "nanoid": "3.3.3", 1226 | "serialize-javascript": "6.0.0", 1227 | "strip-json-comments": "3.1.1", 1228 | "supports-color": "8.1.1", 1229 | "workerpool": "6.2.1", 1230 | "yargs": "16.2.0", 1231 | "yargs-parser": "20.2.4", 1232 | "yargs-unparser": "2.0.0" 1233 | }, 1234 | "bin": { 1235 | "_mocha": "bin/_mocha", 1236 | "mocha": "bin/mocha.js" 1237 | }, 1238 | "engines": { 1239 | "node": ">= 14.0.0" 1240 | }, 1241 | "funding": { 1242 | "type": "opencollective", 1243 | "url": "https://opencollective.com/mochajs" 1244 | } 1245 | }, 1246 | "node_modules/mocha/node_modules/ansi-colors": { 1247 | "version": "4.1.1", 1248 | "dev": true, 1249 | "license": "MIT", 1250 | "engines": { 1251 | "node": ">=6" 1252 | } 1253 | }, 1254 | "node_modules/mocha/node_modules/cliui": { 1255 | "version": "7.0.4", 1256 | "dev": true, 1257 | "license": "ISC", 1258 | "dependencies": { 1259 | "string-width": "^4.2.0", 1260 | "strip-ansi": "^6.0.0", 1261 | "wrap-ansi": "^7.0.0" 1262 | } 1263 | }, 1264 | "node_modules/mocha/node_modules/glob": { 1265 | "version": "7.2.0", 1266 | "dev": true, 1267 | "license": "ISC", 1268 | "dependencies": { 1269 | "fs.realpath": "^1.0.0", 1270 | "inflight": "^1.0.4", 1271 | "inherits": "2", 1272 | "minimatch": "^3.0.4", 1273 | "once": "^1.3.0", 1274 | "path-is-absolute": "^1.0.0" 1275 | }, 1276 | "engines": { 1277 | "node": "*" 1278 | }, 1279 | "funding": { 1280 | "url": "https://github.com/sponsors/isaacs" 1281 | } 1282 | }, 1283 | "node_modules/mocha/node_modules/glob/node_modules/minimatch": { 1284 | "version": "3.1.2", 1285 | "dev": true, 1286 | "license": "ISC", 1287 | "dependencies": { 1288 | "brace-expansion": "^1.1.7" 1289 | }, 1290 | "engines": { 1291 | "node": "*" 1292 | } 1293 | }, 1294 | "node_modules/mocha/node_modules/has-flag": { 1295 | "version": "4.0.0", 1296 | "dev": true, 1297 | "license": "MIT", 1298 | "engines": { 1299 | "node": ">=8" 1300 | } 1301 | }, 1302 | "node_modules/mocha/node_modules/minimatch": { 1303 | "version": "5.0.1", 1304 | "dev": true, 1305 | "license": "ISC", 1306 | "dependencies": { 1307 | "brace-expansion": "^2.0.1" 1308 | }, 1309 | "engines": { 1310 | "node": ">=10" 1311 | } 1312 | }, 1313 | "node_modules/mocha/node_modules/minimatch/node_modules/brace-expansion": { 1314 | "version": "2.0.1", 1315 | "dev": true, 1316 | "license": "MIT", 1317 | "dependencies": { 1318 | "balanced-match": "^1.0.0" 1319 | } 1320 | }, 1321 | "node_modules/mocha/node_modules/ms": { 1322 | "version": "2.1.3", 1323 | "dev": true, 1324 | "license": "MIT" 1325 | }, 1326 | "node_modules/mocha/node_modules/supports-color": { 1327 | "version": "8.1.1", 1328 | "dev": true, 1329 | "license": "MIT", 1330 | "dependencies": { 1331 | "has-flag": "^4.0.0" 1332 | }, 1333 | "engines": { 1334 | "node": ">=10" 1335 | }, 1336 | "funding": { 1337 | "url": "https://github.com/chalk/supports-color?sponsor=1" 1338 | } 1339 | }, 1340 | "node_modules/mocha/node_modules/yargs": { 1341 | "version": "16.2.0", 1342 | "dev": true, 1343 | "license": "MIT", 1344 | "dependencies": { 1345 | "cliui": "^7.0.2", 1346 | "escalade": "^3.1.1", 1347 | "get-caller-file": "^2.0.5", 1348 | "require-directory": "^2.1.1", 1349 | "string-width": "^4.2.0", 1350 | "y18n": "^5.0.5", 1351 | "yargs-parser": "^20.2.2" 1352 | }, 1353 | "engines": { 1354 | "node": ">=10" 1355 | } 1356 | }, 1357 | "node_modules/mocha/node_modules/yargs-parser": { 1358 | "version": "20.2.4", 1359 | "dev": true, 1360 | "license": "ISC", 1361 | "engines": { 1362 | "node": ">=10" 1363 | } 1364 | }, 1365 | "node_modules/ms": { 1366 | "version": "2.1.2", 1367 | "dev": true, 1368 | "license": "MIT" 1369 | }, 1370 | "node_modules/nanoid": { 1371 | "version": "3.3.3", 1372 | "dev": true, 1373 | "license": "MIT", 1374 | "bin": { 1375 | "nanoid": "bin/nanoid.cjs" 1376 | }, 1377 | "engines": { 1378 | "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" 1379 | } 1380 | }, 1381 | "node_modules/node-fetch": { 1382 | "version": "2.6.7", 1383 | "dev": true, 1384 | "license": "MIT", 1385 | "dependencies": { 1386 | "whatwg-url": "^5.0.0" 1387 | }, 1388 | "engines": { 1389 | "node": "4.x || >=6.0.0" 1390 | }, 1391 | "peerDependencies": { 1392 | "encoding": "^0.1.0" 1393 | }, 1394 | "peerDependenciesMeta": { 1395 | "encoding": { 1396 | "optional": true 1397 | } 1398 | } 1399 | }, 1400 | "node_modules/normalize-path": { 1401 | "version": "3.0.0", 1402 | "dev": true, 1403 | "license": "MIT", 1404 | "engines": { 1405 | "node": ">=0.10.0" 1406 | } 1407 | }, 1408 | "node_modules/once": { 1409 | "version": "1.4.0", 1410 | "dev": true, 1411 | "license": "ISC", 1412 | "dependencies": { 1413 | "wrappy": "1" 1414 | } 1415 | }, 1416 | "node_modules/open": { 1417 | "version": "8.4.1", 1418 | "dev": true, 1419 | "license": "MIT", 1420 | "dependencies": { 1421 | "define-lazy-prop": "^2.0.0", 1422 | "is-docker": "^2.1.1", 1423 | "is-wsl": "^2.2.0" 1424 | }, 1425 | "engines": { 1426 | "node": ">=12" 1427 | }, 1428 | "funding": { 1429 | "url": "https://github.com/sponsors/sindresorhus" 1430 | } 1431 | }, 1432 | "node_modules/p-limit": { 1433 | "version": "3.1.0", 1434 | "dev": true, 1435 | "license": "MIT", 1436 | "dependencies": { 1437 | "yocto-queue": "^0.1.0" 1438 | }, 1439 | "engines": { 1440 | "node": ">=10" 1441 | }, 1442 | "funding": { 1443 | "url": "https://github.com/sponsors/sindresorhus" 1444 | } 1445 | }, 1446 | "node_modules/p-locate": { 1447 | "version": "5.0.0", 1448 | "dev": true, 1449 | "license": "MIT", 1450 | "dependencies": { 1451 | "p-limit": "^3.0.2" 1452 | }, 1453 | "engines": { 1454 | "node": ">=10" 1455 | }, 1456 | "funding": { 1457 | "url": "https://github.com/sponsors/sindresorhus" 1458 | } 1459 | }, 1460 | "node_modules/parent-module": { 1461 | "version": "1.0.1", 1462 | "dev": true, 1463 | "license": "MIT", 1464 | "dependencies": { 1465 | "callsites": "^3.0.0" 1466 | }, 1467 | "engines": { 1468 | "node": ">=6" 1469 | } 1470 | }, 1471 | "node_modules/parse-cache-control": { 1472 | "version": "1.0.1", 1473 | "dev": true 1474 | }, 1475 | "node_modules/parse-json": { 1476 | "version": "5.2.0", 1477 | "dev": true, 1478 | "license": "MIT", 1479 | "dependencies": { 1480 | "@babel/code-frame": "^7.0.0", 1481 | "error-ex": "^1.3.1", 1482 | "json-parse-even-better-errors": "^2.3.0", 1483 | "lines-and-columns": "^1.1.6" 1484 | }, 1485 | "engines": { 1486 | "node": ">=8" 1487 | }, 1488 | "funding": { 1489 | "url": "https://github.com/sponsors/sindresorhus" 1490 | } 1491 | }, 1492 | "node_modules/path-exists": { 1493 | "version": "4.0.0", 1494 | "dev": true, 1495 | "license": "MIT", 1496 | "engines": { 1497 | "node": ">=8" 1498 | } 1499 | }, 1500 | "node_modules/path-is-absolute": { 1501 | "version": "1.0.1", 1502 | "dev": true, 1503 | "license": "MIT", 1504 | "engines": { 1505 | "node": ">=0.10.0" 1506 | } 1507 | }, 1508 | "node_modules/path-type": { 1509 | "version": "4.0.0", 1510 | "dev": true, 1511 | "license": "MIT", 1512 | "engines": { 1513 | "node": ">=8" 1514 | } 1515 | }, 1516 | "node_modules/pend": { 1517 | "version": "1.2.0", 1518 | "dev": true, 1519 | "license": "MIT" 1520 | }, 1521 | "node_modules/picomatch": { 1522 | "version": "2.3.1", 1523 | "dev": true, 1524 | "license": "MIT", 1525 | "engines": { 1526 | "node": ">=8.6" 1527 | }, 1528 | "funding": { 1529 | "url": "https://github.com/sponsors/jonschlinkert" 1530 | } 1531 | }, 1532 | "node_modules/progress": { 1533 | "version": "2.0.3", 1534 | "dev": true, 1535 | "license": "MIT", 1536 | "engines": { 1537 | "node": ">=0.4.0" 1538 | } 1539 | }, 1540 | "node_modules/proxy-from-env": { 1541 | "version": "1.1.0", 1542 | "dev": true, 1543 | "license": "MIT" 1544 | }, 1545 | "node_modules/ps-list": { 1546 | "version": "8.1.1", 1547 | "dev": true, 1548 | "license": "MIT", 1549 | "engines": { 1550 | "node": "^12.20.0 || ^14.13.1 || >=16.0.0" 1551 | }, 1552 | "funding": { 1553 | "url": "https://github.com/sponsors/sindresorhus" 1554 | } 1555 | }, 1556 | "node_modules/pump": { 1557 | "version": "3.0.0", 1558 | "dev": true, 1559 | "license": "MIT", 1560 | "dependencies": { 1561 | "end-of-stream": "^1.1.0", 1562 | "once": "^1.3.1" 1563 | } 1564 | }, 1565 | "node_modules/puppeteer": { 1566 | "version": "19.7.1", 1567 | "dev": true, 1568 | "hasInstallScript": true, 1569 | "license": "Apache-2.0", 1570 | "dependencies": { 1571 | "cosmiconfig": "8.0.0", 1572 | "https-proxy-agent": "5.0.1", 1573 | "progress": "2.0.3", 1574 | "proxy-from-env": "1.1.0", 1575 | "puppeteer-core": "19.7.1" 1576 | }, 1577 | "engines": { 1578 | "node": ">=14.1.0" 1579 | } 1580 | }, 1581 | "node_modules/puppeteer-core": { 1582 | "version": "19.7.1", 1583 | "dev": true, 1584 | "license": "Apache-2.0", 1585 | "dependencies": { 1586 | "cross-fetch": "3.1.5", 1587 | "debug": "4.3.4", 1588 | "devtools-protocol": "0.0.1094867", 1589 | "extract-zip": "2.0.1", 1590 | "https-proxy-agent": "5.0.1", 1591 | "proxy-from-env": "1.1.0", 1592 | "rimraf": "3.0.2", 1593 | "tar-fs": "2.1.1", 1594 | "unbzip2-stream": "1.4.3", 1595 | "ws": "8.11.0" 1596 | }, 1597 | "engines": { 1598 | "node": ">=14.1.0" 1599 | }, 1600 | "peerDependencies": { 1601 | "chromium-bidi": "0.4.3", 1602 | "typescript": ">= 4.7.4" 1603 | }, 1604 | "peerDependenciesMeta": { 1605 | "chromium-bidi": { 1606 | "optional": true 1607 | }, 1608 | "typescript": { 1609 | "optional": true 1610 | } 1611 | } 1612 | }, 1613 | "node_modules/puppeteer-core/node_modules/ws": { 1614 | "version": "8.11.0", 1615 | "dev": true, 1616 | "license": "MIT", 1617 | "engines": { 1618 | "node": ">=10.0.0" 1619 | }, 1620 | "peerDependencies": { 1621 | "bufferutil": "^4.0.1", 1622 | "utf-8-validate": "^5.0.2" 1623 | }, 1624 | "peerDependenciesMeta": { 1625 | "bufferutil": { 1626 | "optional": true 1627 | }, 1628 | "utf-8-validate": { 1629 | "optional": true 1630 | } 1631 | } 1632 | }, 1633 | "node_modules/randombytes": { 1634 | "version": "2.1.0", 1635 | "dev": true, 1636 | "license": "MIT", 1637 | "dependencies": { 1638 | "safe-buffer": "^5.1.0" 1639 | } 1640 | }, 1641 | "node_modules/readable-stream": { 1642 | "version": "3.6.0", 1643 | "dev": true, 1644 | "license": "MIT", 1645 | "dependencies": { 1646 | "inherits": "^2.0.3", 1647 | "string_decoder": "^1.1.1", 1648 | "util-deprecate": "^1.0.1" 1649 | }, 1650 | "engines": { 1651 | "node": ">= 6" 1652 | } 1653 | }, 1654 | "node_modules/readdirp": { 1655 | "version": "3.6.0", 1656 | "dev": true, 1657 | "license": "MIT", 1658 | "dependencies": { 1659 | "picomatch": "^2.2.1" 1660 | }, 1661 | "engines": { 1662 | "node": ">=8.10.0" 1663 | } 1664 | }, 1665 | "node_modules/require-directory": { 1666 | "version": "2.1.1", 1667 | "dev": true, 1668 | "license": "MIT", 1669 | "engines": { 1670 | "node": ">=0.10.0" 1671 | } 1672 | }, 1673 | "node_modules/resolve-from": { 1674 | "version": "4.0.0", 1675 | "dev": true, 1676 | "license": "MIT", 1677 | "engines": { 1678 | "node": ">=4" 1679 | } 1680 | }, 1681 | "node_modules/rimraf": { 1682 | "version": "3.0.2", 1683 | "dev": true, 1684 | "license": "ISC", 1685 | "dependencies": { 1686 | "glob": "^7.1.3" 1687 | }, 1688 | "bin": { 1689 | "rimraf": "bin.js" 1690 | }, 1691 | "funding": { 1692 | "url": "https://github.com/sponsors/isaacs" 1693 | } 1694 | }, 1695 | "node_modules/robots-parser": { 1696 | "version": "3.0.0", 1697 | "dev": true, 1698 | "license": "MIT", 1699 | "engines": { 1700 | "node": ">=0.10" 1701 | } 1702 | }, 1703 | "node_modules/safe-buffer": { 1704 | "version": "5.2.1", 1705 | "dev": true, 1706 | "funding": [ 1707 | { 1708 | "type": "github", 1709 | "url": "https://github.com/sponsors/feross" 1710 | }, 1711 | { 1712 | "type": "patreon", 1713 | "url": "https://www.patreon.com/feross" 1714 | }, 1715 | { 1716 | "type": "consulting", 1717 | "url": "https://feross.org/support" 1718 | } 1719 | ], 1720 | "license": "MIT" 1721 | }, 1722 | "node_modules/semver": { 1723 | "version": "5.7.1", 1724 | "dev": true, 1725 | "license": "ISC", 1726 | "bin": { 1727 | "semver": "bin/semver" 1728 | } 1729 | }, 1730 | "node_modules/serialize-javascript": { 1731 | "version": "6.0.0", 1732 | "dev": true, 1733 | "license": "BSD-3-Clause", 1734 | "dependencies": { 1735 | "randombytes": "^2.1.0" 1736 | } 1737 | }, 1738 | "node_modules/signal-exit": { 1739 | "version": "3.0.7", 1740 | "dev": true, 1741 | "license": "ISC" 1742 | }, 1743 | "node_modules/speedline-core": { 1744 | "version": "1.4.3", 1745 | "dev": true, 1746 | "license": "MIT", 1747 | "dependencies": { 1748 | "@types/node": "*", 1749 | "image-ssim": "^0.2.0", 1750 | "jpeg-js": "^0.4.1" 1751 | }, 1752 | "engines": { 1753 | "node": ">=8.0" 1754 | } 1755 | }, 1756 | "node_modules/string_decoder": { 1757 | "version": "1.3.0", 1758 | "dev": true, 1759 | "license": "MIT", 1760 | "dependencies": { 1761 | "safe-buffer": "~5.2.0" 1762 | } 1763 | }, 1764 | "node_modules/string-width": { 1765 | "version": "4.2.3", 1766 | "dev": true, 1767 | "license": "MIT", 1768 | "dependencies": { 1769 | "emoji-regex": "^8.0.0", 1770 | "is-fullwidth-code-point": "^3.0.0", 1771 | "strip-ansi": "^6.0.1" 1772 | }, 1773 | "engines": { 1774 | "node": ">=8" 1775 | } 1776 | }, 1777 | "node_modules/strip-ansi": { 1778 | "version": "6.0.1", 1779 | "dev": true, 1780 | "license": "MIT", 1781 | "dependencies": { 1782 | "ansi-regex": "^5.0.1" 1783 | }, 1784 | "engines": { 1785 | "node": ">=8" 1786 | } 1787 | }, 1788 | "node_modules/strip-json-comments": { 1789 | "version": "3.1.1", 1790 | "dev": true, 1791 | "license": "MIT", 1792 | "engines": { 1793 | "node": ">=8" 1794 | }, 1795 | "funding": { 1796 | "url": "https://github.com/sponsors/sindresorhus" 1797 | } 1798 | }, 1799 | "node_modules/supports-color": { 1800 | "version": "5.5.0", 1801 | "dev": true, 1802 | "license": "MIT", 1803 | "dependencies": { 1804 | "has-flag": "^3.0.0" 1805 | }, 1806 | "engines": { 1807 | "node": ">=4" 1808 | } 1809 | }, 1810 | "node_modules/tar-fs": { 1811 | "version": "2.1.1", 1812 | "dev": true, 1813 | "license": "MIT", 1814 | "dependencies": { 1815 | "chownr": "^1.1.1", 1816 | "mkdirp-classic": "^0.5.2", 1817 | "pump": "^3.0.0", 1818 | "tar-stream": "^2.1.4" 1819 | } 1820 | }, 1821 | "node_modules/tar-stream": { 1822 | "version": "2.2.0", 1823 | "dev": true, 1824 | "license": "MIT", 1825 | "dependencies": { 1826 | "bl": "^4.0.3", 1827 | "end-of-stream": "^1.4.1", 1828 | "fs-constants": "^1.0.0", 1829 | "inherits": "^2.0.3", 1830 | "readable-stream": "^3.1.1" 1831 | }, 1832 | "engines": { 1833 | "node": ">=6" 1834 | } 1835 | }, 1836 | "node_modules/third-party-web": { 1837 | "version": "0.20.2", 1838 | "dev": true, 1839 | "license": "MIT" 1840 | }, 1841 | "node_modules/through": { 1842 | "version": "2.3.8", 1843 | "dev": true, 1844 | "license": "MIT" 1845 | }, 1846 | "node_modules/to-regex-range": { 1847 | "version": "5.0.1", 1848 | "dev": true, 1849 | "license": "MIT", 1850 | "dependencies": { 1851 | "is-number": "^7.0.0" 1852 | }, 1853 | "engines": { 1854 | "node": ">=8.0" 1855 | } 1856 | }, 1857 | "node_modules/tr46": { 1858 | "version": "0.0.3", 1859 | "dev": true, 1860 | "license": "MIT" 1861 | }, 1862 | "node_modules/tslib": { 1863 | "version": "1.14.1", 1864 | "dev": true, 1865 | "license": "0BSD" 1866 | }, 1867 | "node_modules/typedarray-to-buffer": { 1868 | "version": "3.1.5", 1869 | "dev": true, 1870 | "license": "MIT", 1871 | "dependencies": { 1872 | "is-typedarray": "^1.0.0" 1873 | } 1874 | }, 1875 | "node_modules/typescript": { 1876 | "version": "4.9.5", 1877 | "dev": true, 1878 | "license": "Apache-2.0", 1879 | "bin": { 1880 | "tsc": "bin/tsc", 1881 | "tsserver": "bin/tsserver" 1882 | }, 1883 | "engines": { 1884 | "node": ">=4.2.0" 1885 | } 1886 | }, 1887 | "node_modules/unbzip2-stream": { 1888 | "version": "1.4.3", 1889 | "dev": true, 1890 | "license": "MIT", 1891 | "dependencies": { 1892 | "buffer": "^5.2.1", 1893 | "through": "^2.3.8" 1894 | } 1895 | }, 1896 | "node_modules/unique-string": { 1897 | "version": "2.0.0", 1898 | "dev": true, 1899 | "license": "MIT", 1900 | "dependencies": { 1901 | "crypto-random-string": "^2.0.0" 1902 | }, 1903 | "engines": { 1904 | "node": ">=8" 1905 | } 1906 | }, 1907 | "node_modules/util-deprecate": { 1908 | "version": "1.0.2", 1909 | "dev": true, 1910 | "license": "MIT" 1911 | }, 1912 | "node_modules/webidl-conversions": { 1913 | "version": "3.0.1", 1914 | "dev": true, 1915 | "license": "BSD-2-Clause" 1916 | }, 1917 | "node_modules/whatwg-url": { 1918 | "version": "5.0.0", 1919 | "dev": true, 1920 | "license": "MIT", 1921 | "dependencies": { 1922 | "tr46": "~0.0.3", 1923 | "webidl-conversions": "^3.0.0" 1924 | } 1925 | }, 1926 | "node_modules/workerpool": { 1927 | "version": "6.2.1", 1928 | "dev": true, 1929 | "license": "Apache-2.0" 1930 | }, 1931 | "node_modules/wrap-ansi": { 1932 | "version": "7.0.0", 1933 | "dev": true, 1934 | "license": "MIT", 1935 | "dependencies": { 1936 | "ansi-styles": "^4.0.0", 1937 | "string-width": "^4.1.0", 1938 | "strip-ansi": "^6.0.0" 1939 | }, 1940 | "engines": { 1941 | "node": ">=10" 1942 | }, 1943 | "funding": { 1944 | "url": "https://github.com/chalk/wrap-ansi?sponsor=1" 1945 | } 1946 | }, 1947 | "node_modules/wrappy": { 1948 | "version": "1.0.2", 1949 | "dev": true, 1950 | "license": "ISC" 1951 | }, 1952 | "node_modules/write-file-atomic": { 1953 | "version": "3.0.3", 1954 | "dev": true, 1955 | "license": "ISC", 1956 | "dependencies": { 1957 | "imurmurhash": "^0.1.4", 1958 | "is-typedarray": "^1.0.0", 1959 | "signal-exit": "^3.0.2", 1960 | "typedarray-to-buffer": "^3.1.5" 1961 | } 1962 | }, 1963 | "node_modules/ws": { 1964 | "version": "7.5.9", 1965 | "dev": true, 1966 | "license": "MIT", 1967 | "engines": { 1968 | "node": ">=8.3.0" 1969 | }, 1970 | "peerDependencies": { 1971 | "bufferutil": "^4.0.1", 1972 | "utf-8-validate": "^5.0.2" 1973 | }, 1974 | "peerDependenciesMeta": { 1975 | "bufferutil": { 1976 | "optional": true 1977 | }, 1978 | "utf-8-validate": { 1979 | "optional": true 1980 | } 1981 | } 1982 | }, 1983 | "node_modules/xdg-basedir": { 1984 | "version": "4.0.0", 1985 | "dev": true, 1986 | "license": "MIT", 1987 | "engines": { 1988 | "node": ">=8" 1989 | } 1990 | }, 1991 | "node_modules/y18n": { 1992 | "version": "5.0.8", 1993 | "dev": true, 1994 | "license": "ISC", 1995 | "engines": { 1996 | "node": ">=10" 1997 | } 1998 | }, 1999 | "node_modules/yargs": { 2000 | "version": "17.7.0", 2001 | "dev": true, 2002 | "license": "MIT", 2003 | "dependencies": { 2004 | "cliui": "^8.0.1", 2005 | "escalade": "^3.1.1", 2006 | "get-caller-file": "^2.0.5", 2007 | "require-directory": "^2.1.1", 2008 | "string-width": "^4.2.3", 2009 | "y18n": "^5.0.5", 2010 | "yargs-parser": "^21.1.1" 2011 | }, 2012 | "engines": { 2013 | "node": ">=12" 2014 | } 2015 | }, 2016 | "node_modules/yargs-parser": { 2017 | "version": "21.1.1", 2018 | "dev": true, 2019 | "license": "ISC", 2020 | "engines": { 2021 | "node": ">=12" 2022 | } 2023 | }, 2024 | "node_modules/yargs-unparser": { 2025 | "version": "2.0.0", 2026 | "dev": true, 2027 | "license": "MIT", 2028 | "dependencies": { 2029 | "camelcase": "^6.0.0", 2030 | "decamelize": "^4.0.0", 2031 | "flat": "^5.0.2", 2032 | "is-plain-obj": "^2.1.0" 2033 | }, 2034 | "engines": { 2035 | "node": ">=10" 2036 | } 2037 | }, 2038 | "node_modules/yauzl": { 2039 | "version": "2.10.0", 2040 | "dev": true, 2041 | "license": "MIT", 2042 | "dependencies": { 2043 | "buffer-crc32": "~0.2.3", 2044 | "fd-slicer": "~1.1.0" 2045 | } 2046 | }, 2047 | "node_modules/yocto-queue": { 2048 | "version": "0.1.0", 2049 | "dev": true, 2050 | "license": "MIT", 2051 | "engines": { 2052 | "node": ">=10" 2053 | }, 2054 | "funding": { 2055 | "url": "https://github.com/sponsors/sindresorhus" 2056 | } 2057 | } 2058 | } 2059 | } 2060 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lighthouse-plugin-soft-navigation", 3 | "version": "1.1.0", 4 | "description": "Lighthouse plugin that reports metrics such as LCP and FCP from a soft navigation.", 5 | "type": "module", 6 | "main": "plugin.js", 7 | "license": "ISC", 8 | "scripts": { 9 | "reset-link": "npm link && npm link lighthouse-plugin-soft-navigation", 10 | "type-check": "tsc", 11 | "pretest:e2e": "npm run reset-link", 12 | "test:e2e": "node test/e2e/run.js", 13 | "test:unit": "mocha", 14 | "test": "npm run type-check && npm run test:unit && npm run test:e2e" 15 | }, 16 | "devDependencies": { 17 | "@types/mocha": "^10.0.1", 18 | "lighthouse": "^10.0.1", 19 | "mocha": "^10.2.0", 20 | "puppeteer": "^19.7.1", 21 | "typescript": "^4.9.5" 22 | }, 23 | "peerDependencies": { 24 | "lighthouse": ">=10.0.0" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /plugin.js: -------------------------------------------------------------------------------- 1 | /** @type {import('lighthouse').Config.Plugin} */ 2 | export default { 3 | audits: [ 4 | {path: 'lighthouse-plugin-soft-navigation/audits/soft-nav-fcp'}, 5 | {path: 'lighthouse-plugin-soft-navigation/audits/soft-nav-lcp'}, 6 | ], 7 | groups: { 8 | 'metrics': { 9 | title: 'Metrics associated with a soft navigation', 10 | } 11 | }, 12 | category: { 13 | title: 'Soft Navigation', 14 | supportedModes: ['timespan'], 15 | auditRefs: [ 16 | {id: 'soft-nav-fcp', weight: 1, group: 'metrics'}, 17 | {id: 'soft-nav-lcp', weight: 1, group: 'metrics'}, 18 | ] 19 | }, 20 | } -------------------------------------------------------------------------------- /test/e2e/run.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import open from 'open'; 3 | import puppeteer from 'puppeteer'; 4 | import {startTimespan} from 'lighthouse'; 5 | import assert from 'assert'; 6 | 7 | const browser = await puppeteer.launch({ 8 | headless: false, 9 | executablePath: process.env.CHROME_PATH, 10 | args: ['--enable-experimental-web-platform-features'], 11 | }); 12 | const page = await browser.newPage(); 13 | 14 | /** @type {import('lighthouse').Config} */ 15 | const config = { 16 | extends: 'lighthouse:default', 17 | plugins: ['lighthouse-plugin-soft-navigation'], 18 | 19 | settings: { 20 | output: 'html', 21 | }, 22 | } 23 | 24 | await page.goto('https://next-movies-zeta.vercel.app/?category=Popular&page=1'); 25 | await page.waitForTimeout(2000); 26 | 27 | const timespan = await startTimespan(page, {config}); 28 | 29 | await page.click('.hamburger-button'); 30 | await page.waitForSelector('a[href="/?category=Popular&page=1"]'); 31 | await page.click('a[href="/?category=Top+Rated&page=1"]'); 32 | await page.waitForTimeout(2000); 33 | 34 | const result = await timespan.endTimespan(); 35 | if (!result) throw new Error('No result') 36 | 37 | if (process.argv.includes('--trace')) { 38 | fs.writeFileSync('timespan.trace.json', JSON.stringify(result.artifacts.Trace, null, 2)); 39 | } 40 | 41 | if (process.argv.includes('--view')) { 42 | // @ts-expect-error 43 | fs.writeFileSync('timespan.report.html', result.report); 44 | open('timespan.report.html'); 45 | } 46 | 47 | const softNavCategory = result.lhr.categories['lighthouse-plugin-soft-navigation']; 48 | softNavCategory.score = null; 49 | 50 | assert.deepStrictEqual(softNavCategory, { 51 | id: 'lighthouse-plugin-soft-navigation', 52 | title: 'Soft Navigation', 53 | description: undefined, 54 | manualDescription: undefined, 55 | supportedModes: ['timespan'], 56 | score: null, 57 | auditRefs: [ 58 | { 59 | id: 'soft-nav-fcp', 60 | weight: 1, 61 | group: 'lighthouse-plugin-soft-navigation-metrics' 62 | }, 63 | { 64 | id: 'soft-nav-lcp', 65 | weight: 1, 66 | group: 'lighthouse-plugin-soft-navigation-metrics' 67 | } 68 | ], 69 | }); 70 | 71 | const softNavFcpAudit = result.lhr.audits['soft-nav-fcp']; 72 | assert.ok(softNavFcpAudit); 73 | assert.strictEqual(softNavFcpAudit.score, 1); 74 | assert.ok(softNavFcpAudit.numericValue); 75 | assert.ok(softNavFcpAudit.numericValue < 1500); 76 | 77 | const softNavLcpAudit = result.lhr.audits['soft-nav-lcp']; 78 | assert.ok(softNavLcpAudit); 79 | assert.ok(softNavLcpAudit.score && softNavLcpAudit.score > 0.95); 80 | assert.ok(softNavLcpAudit.numericValue); 81 | assert.ok(softNavLcpAudit.numericValue < 2000); 82 | 83 | await browser.close(); 84 | -------------------------------------------------------------------------------- /test/unit/lib/metric-timings.test.js: -------------------------------------------------------------------------------- 1 | import assert from 'assert'; 2 | import {computeMetricTimings} from '../../../lib/metric-timings.js'; 3 | 4 | /** @typedef {import('lighthouse').TraceEvent} TraceEvent */ 5 | 6 | /** 7 | * @param {Partial & {name: string, ts: number}} event 8 | * @return {TraceEvent} 9 | */ 10 | function traceEvent(event) { 11 | return { 12 | args: {}, 13 | cat: 'devtools.timeline', 14 | pid: 111, 15 | tid: 2222, 16 | dur: 50, 17 | ph: 'X', 18 | ...event, 19 | } 20 | } 21 | 22 | describe('computeMetricTimings', () => { 23 | it('computes the soft navigation fcp and lcp', () => { 24 | /** @type {TraceEvent[]} */ 25 | const traceEvents = [ 26 | traceEvent({name: 'SoftNavigationHeuristics_SoftNavigationDetected', ts: 1_000_000}), 27 | traceEvent({name: 'firstContentfulPaint', ts: 1_080_000}), 28 | traceEvent({name: 'largestContentfulPaint::Candidate', ts: 1_090_000}), 29 | ]; 30 | 31 | const timings = computeMetricTimings(traceEvents); 32 | 33 | assert.strictEqual(timings.fcpTiming, 80); 34 | assert.strictEqual(timings.lcpTiming, 90); 35 | }); 36 | 37 | it('returns undefined for missing lcp', () => { 38 | /** @type {TraceEvent[]} */ 39 | const traceEvents = [ 40 | traceEvent({name: 'SoftNavigationHeuristics_SoftNavigationDetected', ts: 1_000_000}), 41 | traceEvent({name: 'firstContentfulPaint', ts: 1_090_000}), 42 | ]; 43 | 44 | const timings = computeMetricTimings(traceEvents); 45 | 46 | assert.strictEqual(timings.fcpTiming, 90); 47 | assert.strictEqual(timings.lcpTiming, undefined); 48 | }); 49 | 50 | it('returns undefined for missing fcp', () => { 51 | /** @type {TraceEvent[]} */ 52 | const traceEvents = [ 53 | traceEvent({name: 'SoftNavigationHeuristics_SoftNavigationDetected', ts: 1_000_000}), 54 | ]; 55 | 56 | const timings = computeMetricTimings(traceEvents); 57 | 58 | assert.strictEqual(timings.fcpTiming, undefined); 59 | assert.strictEqual(timings.lcpTiming, undefined); 60 | }); 61 | 62 | it('throws if multiple soft navigations are found', () => { 63 | /** @type {TraceEvent[]} */ 64 | const traceEvents = [ 65 | traceEvent({name: 'SoftNavigationHeuristics_SoftNavigationDetected', ts: 1_000_000}), 66 | traceEvent({name: 'firstContentfulPaint', ts: 1_090_000}), 67 | traceEvent({name: 'largestContentfulPaint::Candidate', ts: 1_090_000}), 68 | traceEvent({name: 'SoftNavigationHeuristics_SoftNavigationDetected', ts: 2_000_000}), 69 | traceEvent({name: 'firstContentfulPaint', ts: 2_090_000}), 70 | traceEvent({name: 'largestContentfulPaint::Candidate', ts: 2_090_000}), 71 | ]; 72 | 73 | assert.throws(() => computeMetricTimings(traceEvents), new Error('Multiple soft navigations detected')); 74 | }); 75 | 76 | it('ignores metric timings before soft navigation detected', () => { 77 | /** @type {TraceEvent[]} */ 78 | const traceEvents = [ 79 | traceEvent({name: 'firstContentfulPaint', ts: 1_020_000}), 80 | traceEvent({name: 'largestContentfulPaint::Candidate', ts: 1_030_000}), 81 | traceEvent({name: 'SoftNavigationHeuristics_SoftNavigationDetected', ts: 1_050_000}), 82 | traceEvent({name: 'firstContentfulPaint', ts: 1_090_000}), 83 | traceEvent({name: 'largestContentfulPaint::Candidate', ts: 1_090_000}), 84 | ]; 85 | 86 | const timings = computeMetricTimings(traceEvents); 87 | 88 | assert.strictEqual(timings.fcpTiming, 40); 89 | assert.strictEqual(timings.lcpTiming, 40); 90 | }); 91 | }); -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "noEmit": true, 4 | "target": "esnext", 5 | "module": "NodeNext", 6 | "moduleResolution": "NodeNext", 7 | "strict": true, 8 | "allowJs": true, 9 | "checkJs": true, 10 | } 11 | } -------------------------------------------------------------------------------- /types/global.d.ts: -------------------------------------------------------------------------------- 1 | import 'lighthouse/types/internal/global.js'; --------------------------------------------------------------------------------