chrome-devtools-mcp 0.21.0 → 0.23.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/README.md +87 -21
  2. package/build/src/HeapSnapshotManager.js +94 -0
  3. package/build/src/McpContext.js +26 -181
  4. package/build/src/McpPage.js +214 -0
  5. package/build/src/McpResponse.js +151 -13
  6. package/build/src/PageCollector.js +10 -24
  7. package/build/src/TextSnapshot.js +230 -0
  8. package/build/src/WaitForHelper.js +31 -0
  9. package/build/src/bin/check-latest-version.js +25 -0
  10. package/build/src/bin/chrome-devtools-mcp-cli-options.js +34 -10
  11. package/build/src/bin/chrome-devtools-mcp-main.js +2 -0
  12. package/build/src/bin/chrome-devtools.js +25 -14
  13. package/build/src/bin/cliDefinitions.js +14 -8
  14. package/build/src/daemon/client.js +11 -11
  15. package/build/src/daemon/daemon.js +6 -9
  16. package/build/src/daemon/utils.js +19 -14
  17. package/build/src/formatters/HeapSnapshotFormatter.js +38 -0
  18. package/build/src/formatters/NetworkFormatter.js +24 -7
  19. package/build/src/index.js +12 -1
  20. package/build/src/telemetry/ClearcutLogger.js +34 -12
  21. package/build/src/telemetry/flagUtils.js +46 -4
  22. package/build/src/telemetry/toolMetricsUtils.js +88 -0
  23. package/build/src/telemetry/watchdog/ClearcutSender.js +4 -3
  24. package/build/src/third_party/THIRD_PARTY_NOTICES +59 -32
  25. package/build/src/third_party/bundled-packages.json +6 -4
  26. package/build/src/third_party/devtools-formatter-worker.js +61 -64
  27. package/build/src/third_party/devtools-heap-snapshot-worker.js +9690 -0
  28. package/build/src/third_party/index.js +62661 -60590
  29. package/build/src/third_party/lighthouse-devtools-mcp-bundle.js +3501 -2658
  30. package/build/src/tools/categories.js +3 -0
  31. package/build/src/tools/console.js +42 -39
  32. package/build/src/tools/emulation.js +1 -1
  33. package/build/src/tools/extensions.js +5 -11
  34. package/build/src/tools/inPage.js +3 -13
  35. package/build/src/tools/input.js +15 -16
  36. package/build/src/tools/lighthouse.js +2 -2
  37. package/build/src/tools/memory.js +48 -3
  38. package/build/src/tools/network.js +4 -4
  39. package/build/src/tools/pages.js +212 -146
  40. package/build/src/tools/performance.js +1 -1
  41. package/build/src/tools/screencast.js +20 -8
  42. package/build/src/tools/screenshot.js +3 -3
  43. package/build/src/tools/script.js +22 -16
  44. package/build/src/tools/tools.js +2 -0
  45. package/build/src/tools/webmcp.js +63 -0
  46. package/build/src/utils/check-for-updates.js +73 -0
  47. package/build/src/utils/files.js +4 -0
  48. package/build/src/utils/id.js +15 -0
  49. package/build/src/version.js +1 -1
  50. package/package.json +13 -8
  51. package/build/src/utils/ExtensionRegistry.js +0 -35
@@ -1,3 +1,30 @@
1
+ Name: urlpattern-polyfill
2
+ URL: https://github.com/kenchris/urlpattern-polyfill
3
+ Version: 10.1.0
4
+ License: MIT
5
+
6
+ Copyright 2020 Intel Corporation
7
+
8
+ Permission is hereby granted, free of charge, to any person obtaining a copy
9
+ of this software and associated documentation files (the "Software"), to deal
10
+ in the Software without restriction, including without limitation the rights
11
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
12
+ copies of the Software, and to permit persons to whom the Software is
13
+ furnished to do so, subject to the following conditions:
14
+
15
+ The above copyright notice and this permission notice shall be included in
16
+ all copies or substantial portions of the Software.
17
+
18
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
21
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
23
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
24
+ THE SOFTWARE.
25
+
26
+ -------------------- DEPENDENCY DIVIDER --------------------
27
+
1
28
  Name: core-js
2
29
  URL: https://core-js.io
3
30
  Version: 3.49.0
@@ -293,6 +320,30 @@ Permission to use, copy, modify, and/or distribute this software for any purpose
293
320
  THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
294
321
 
295
322
 
323
+ -------------------- DEPENDENCY DIVIDER --------------------
324
+
325
+ Name: semver
326
+ URL: git+https://github.com/npm/node-semver.git
327
+ Version: 7.7.4
328
+ License: ISC
329
+
330
+ The ISC License
331
+
332
+ Copyright (c) Isaac Z. Schlueter and Contributors
333
+
334
+ Permission to use, copy, modify, and/or distribute this software for any
335
+ purpose with or without fee is hereby granted, provided that the above
336
+ copyright notice and this permission notice appear in all copies.
337
+
338
+ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
339
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
340
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
341
+ ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
342
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
343
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR
344
+ IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
345
+
346
+
296
347
  -------------------- DEPENDENCY DIVIDER --------------------
297
348
 
298
349
  Name: debug
@@ -422,7 +473,7 @@ SOFTWARE.
422
473
 
423
474
  Name: @modelcontextprotocol/sdk
424
475
  URL: https://modelcontextprotocol.io
425
- Version: 1.28.0
476
+ Version: 1.29.0
426
477
  License: MIT
427
478
 
428
479
  MIT License
@@ -818,7 +869,7 @@ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
818
869
 
819
870
  Name: puppeteer-core
820
871
  URL: https://github.com/puppeteer/puppeteer/tree/main/packages/puppeteer-core
821
- Version: 24.40.0
872
+ Version: 24.42.0
822
873
  License: Apache-2.0
823
874
 
824
875
  -------------------- DEPENDENCY DIVIDER --------------------
@@ -828,30 +879,6 @@ URL: https://github.com/puppeteer/puppeteer/tree/main/packages/browsers
828
879
  Version: 2.13.0
829
880
  License: Apache-2.0
830
881
 
831
- -------------------- DEPENDENCY DIVIDER --------------------
832
-
833
- Name: semver
834
- URL: git+https://github.com/npm/node-semver.git
835
- Version: 7.7.4
836
- License: ISC
837
-
838
- The ISC License
839
-
840
- Copyright (c) Isaac Z. Schlueter and Contributors
841
-
842
- Permission to use, copy, modify, and/or distribute this software for any
843
- purpose with or without fee is hereby granted, provided that the above
844
- copyright notice and this permission notice appear in all copies.
845
-
846
- THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
847
- WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
848
- MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
849
- ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
850
- WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
851
- ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR
852
- IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
853
-
854
-
855
882
  -------------------- DEPENDENCY DIVIDER --------------------
856
883
 
857
884
  Name: proxy-agent
@@ -1237,7 +1264,7 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
1237
1264
 
1238
1265
  Name: basic-ftp
1239
1266
  URL: https://github.com/patrickjuchli/basic-ftp.git
1240
- Version: 5.2.0
1267
+ Version: 5.3.0
1241
1268
  License: MIT
1242
1269
 
1243
1270
  Copyright (c) 2019 Patrick Juchli
@@ -2459,7 +2486,7 @@ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLI
2459
2486
 
2460
2487
  Name: @paulirish/trace_engine
2461
2488
  URL: N/A
2462
- Version: 0.0.61
2489
+ Version: 0.0.64
2463
2490
  License: BSD-3-Clause
2464
2491
 
2465
2492
  // Copyright 2014 The Chromium Authors
@@ -2525,7 +2552,7 @@ SOFTWARE.
2525
2552
 
2526
2553
  Name: axe-core
2527
2554
  URL: https://www.deque.com/axe/
2528
- Version: 4.11.0
2555
+ Version: 4.11.2
2529
2556
  License: MPL-2.0
2530
2557
 
2531
2558
  Mozilla Public License, version 2.0
@@ -3961,7 +3988,7 @@ SOFTWARE.
3961
3988
 
3962
3989
  Name: puppeteer-core
3963
3990
  URL: https://github.com/puppeteer/puppeteer/tree/main/packages/puppeteer-core
3964
- Version: 24.23.0
3991
+ Version: 24.40.0
3965
3992
  License: Apache-2.0
3966
3993
 
3967
3994
  -------------------- DEPENDENCY DIVIDER --------------------
@@ -4027,7 +4054,7 @@ SOFTWARE.
4027
4054
 
4028
4055
  Name: tldts-core
4029
4056
  URL: https://github.com/remusao/tldts#readme
4030
- Version: 7.0.17
4057
+ Version: 7.0.27
4031
4058
  License: MIT
4032
4059
 
4033
4060
  Copyright (c) 2017 Thomas Parisot, 2018 Rémi Berson
@@ -4049,7 +4076,7 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTH
4049
4076
 
4050
4077
  Name: tldts-icann
4051
4078
  URL: https://github.com/remusao/tldts#readme
4052
- Version: 7.0.17
4079
+ Version: 7.0.27
4053
4080
  License: MIT
4054
4081
 
4055
4082
  Copyright (c) 2017 Thomas Parisot, 2018 Rémi Berson
@@ -1,9 +1,11 @@
1
1
  {
2
- "@modelcontextprotocol/sdk": "1.28.0",
3
- "chrome-devtools-frontend": "1.0.1602348",
2
+ "@modelcontextprotocol/sdk": "1.29.0",
3
+ "chrome-devtools-frontend": "1.0.1613625",
4
4
  "core-js": "3.49.0",
5
5
  "debug": "4.4.3",
6
- "lighthouse": "13.0.3",
6
+ "lighthouse": "13.1.0",
7
+ "semver": "^7.7.4",
8
+ "urlpattern-polyfill": "^10.1.0",
7
9
  "yargs": "18.0.0",
8
- "puppeteer-core": "24.40.0"
10
+ "puppeteer-core": "24.42.0"
9
11
  }
@@ -26,36 +26,36 @@ function arrayDoesNotContainNullOrUndefined(arr) {
26
26
  const EmptyUrlString = '';
27
27
 
28
28
  // Copyright 2025 The Chromium Authors
29
- class NodeWorkerScope {
29
+ class WebWorkerScope {
30
30
  postMessage(message) {
31
- WorkerThreads.parentPort?.postMessage(message);
31
+ self.postMessage(message);
32
32
  }
33
33
  set onmessage(listener) {
34
- WorkerThreads.parentPort?.on('message', data => {
35
- listener({ data });
36
- });
34
+ self.addEventListener('message', listener);
37
35
  }
38
36
  }
39
- class NodeWorker {
37
+ class WebWorker {
40
38
  #workerPromise;
41
- #disposed = false;
39
+ #disposed;
42
40
  #rejectWorkerPromise;
43
- constructor(url) {
44
- this.#workerPromise = new Promise((resolve, reject) => {
41
+ constructor(workerLocation) {
42
+ this.#workerPromise = new Promise((fulfill, reject) => {
45
43
  this.#rejectWorkerPromise = reject;
46
- const worker = new WorkerThreads.Worker(new URL(url));
47
- worker.once('message', (message) => {
48
- if (message === 'workerReady') {
49
- resolve(worker);
50
- }
51
- });
52
- worker.on('error', reject);
44
+ const worker = new Worker(new URL(workerLocation), { type: 'module' });
45
+ worker.onerror = event => {
46
+ console.error(`Failed to load worker for ${workerLocation}:`, event);
47
+ };
48
+ worker.onmessage = (event) => {
49
+ console.assert(event.data === 'workerReady');
50
+ worker.onmessage = null;
51
+ fulfill(worker);
52
+ };
53
53
  });
54
54
  }
55
- postMessage(message) {
55
+ postMessage(message, transfer) {
56
56
  void this.#workerPromise.then(worker => {
57
57
  if (!this.#disposed) {
58
- worker.postMessage(message);
58
+ worker.postMessage(message, transfer ?? []);
59
59
  }
60
60
  });
61
61
  }
@@ -63,7 +63,7 @@ class NodeWorker {
63
63
  this.#disposed = true;
64
64
  void this.#workerPromise.then(worker => worker.terminate());
65
65
  }
66
- terminate(immediately) {
66
+ terminate(immediately = false) {
67
67
  if (immediately) {
68
68
  this.#rejectWorkerPromise?.(new Error('Worker terminated'));
69
69
  }
@@ -71,28 +71,20 @@ class NodeWorker {
71
71
  }
72
72
  set onmessage(listener) {
73
73
  void this.#workerPromise.then(worker => {
74
- worker.on('message', data => {
75
- if (!this.#disposed) {
76
- listener({ data });
77
- }
78
- });
74
+ worker.onmessage = listener;
79
75
  });
80
76
  }
81
77
  set onerror(listener) {
82
78
  void this.#workerPromise.then(worker => {
83
- worker.on('error', (error) => {
84
- if (!this.#disposed) {
85
- listener({ type: 'error', ...error });
86
- }
87
- });
79
+ worker.onerror = listener;
88
80
  });
89
81
  }
90
82
  }
91
83
  const HOST_RUNTIME$2 = {
92
84
  createWorker(url) {
93
- return new NodeWorker(url);
85
+ return new WebWorker(url);
94
86
  },
95
- workerScope: new NodeWorkerScope(),
87
+ workerScope: new WebWorkerScope(),
96
88
  };
97
89
 
98
90
  var HostRuntime$1 = /*#__PURE__*/Object.freeze({
@@ -102,42 +94,42 @@ var HostRuntime$1 = /*#__PURE__*/Object.freeze({
102
94
 
103
95
  // Copyright 2025 The Chromium Authors
104
96
 
105
- var node = /*#__PURE__*/Object.freeze({
97
+ var browser = /*#__PURE__*/Object.freeze({
106
98
  __proto__: null,
107
99
  HostRuntime: HostRuntime$1
108
100
  });
109
101
 
110
102
  // Copyright 2025 The Chromium Authors
111
- class WebWorkerScope {
103
+ class NodeWorkerScope {
112
104
  postMessage(message) {
113
- self.postMessage(message);
105
+ WorkerThreads.parentPort?.postMessage(message);
114
106
  }
115
107
  set onmessage(listener) {
116
- self.onmessage = listener;
108
+ WorkerThreads.parentPort?.addEventListener('message', msg => {
109
+ listener(msg);
110
+ });
117
111
  }
118
112
  }
119
- class WebWorker {
113
+ class NodeWorker {
120
114
  #workerPromise;
121
- #disposed;
115
+ #disposed = false;
122
116
  #rejectWorkerPromise;
123
- constructor(workerLocation) {
124
- this.#workerPromise = new Promise((fulfill, reject) => {
117
+ constructor(url) {
118
+ this.#workerPromise = new Promise((resolve, reject) => {
125
119
  this.#rejectWorkerPromise = reject;
126
- const worker = new Worker(new URL(workerLocation), { type: 'module' });
127
- worker.onerror = event => {
128
- console.error(`Failed to load worker for ${workerLocation}:`, event);
129
- };
130
- worker.onmessage = (event) => {
131
- console.assert(event.data === 'workerReady');
132
- worker.onmessage = null;
133
- fulfill(worker);
134
- };
120
+ const worker = new WorkerThreads.Worker(new URL(url));
121
+ worker.once('message', (message) => {
122
+ if (message === 'workerReady') {
123
+ resolve(worker);
124
+ }
125
+ });
126
+ worker.on('error', reject);
135
127
  });
136
128
  }
137
129
  postMessage(message, transfer) {
138
130
  void this.#workerPromise.then(worker => {
139
131
  if (!this.#disposed) {
140
- worker.postMessage(message, transfer ?? []);
132
+ worker.postMessage(message, transfer);
141
133
  }
142
134
  });
143
135
  }
@@ -145,7 +137,7 @@ class WebWorker {
145
137
  this.#disposed = true;
146
138
  void this.#workerPromise.then(worker => worker.terminate());
147
139
  }
148
- terminate(immediately = false) {
140
+ terminate(immediately) {
149
141
  if (immediately) {
150
142
  this.#rejectWorkerPromise?.(new Error('Worker terminated'));
151
143
  }
@@ -153,20 +145,28 @@ class WebWorker {
153
145
  }
154
146
  set onmessage(listener) {
155
147
  void this.#workerPromise.then(worker => {
156
- worker.onmessage = listener;
148
+ worker.on('message', (data) => {
149
+ if (!this.#disposed) {
150
+ listener({ data, ports: [] });
151
+ }
152
+ });
157
153
  });
158
154
  }
159
155
  set onerror(listener) {
160
156
  void this.#workerPromise.then(worker => {
161
- worker.onerror = listener;
157
+ worker.on('error', (error) => {
158
+ if (!this.#disposed) {
159
+ listener({ type: 'error', ...error });
160
+ }
161
+ });
162
162
  });
163
163
  }
164
164
  }
165
165
  const HOST_RUNTIME$1 = {
166
166
  createWorker(url) {
167
- return new WebWorker(url);
167
+ return new NodeWorker(url);
168
168
  },
169
- workerScope: new WebWorkerScope(),
169
+ workerScope: new NodeWorkerScope(),
170
170
  };
171
171
 
172
172
  var HostRuntime = /*#__PURE__*/Object.freeze({
@@ -176,7 +176,7 @@ var HostRuntime = /*#__PURE__*/Object.freeze({
176
176
 
177
177
  // Copyright 2025 The Chromium Authors
178
178
 
179
- var browser = /*#__PURE__*/Object.freeze({
179
+ var node = /*#__PURE__*/Object.freeze({
180
180
  __proto__: null,
181
181
  HostRuntime: HostRuntime
182
182
  });
@@ -186,12 +186,12 @@ const IS_NODE = typeof process !== 'undefined' && process.versions?.node !== nul
186
186
  const IS_BROWSER =
187
187
  typeof window !== 'undefined' || (typeof self !== 'undefined' && typeof self.postMessage === 'function');
188
188
  const HOST_RUNTIME = await (async () => {
189
- if (IS_NODE) {
190
- return (await Promise.resolve().then(function () { return node; })).HostRuntime.HOST_RUNTIME;
191
- }
192
189
  if (IS_BROWSER) {
193
190
  return (await Promise.resolve().then(function () { return browser; })).HostRuntime.HOST_RUNTIME;
194
191
  }
192
+ if (IS_NODE) {
193
+ return (await Promise.resolve().then(function () { return node; })).HostRuntime.HOST_RUNTIME;
194
+ }
195
195
  throw new Error('Unknown runtime!');
196
196
  })();
197
197
 
@@ -3114,13 +3114,9 @@ var ExperimentName;
3114
3114
  ExperimentName["LIVE_HEAP_PROFILE"] = "live-heap-profile";
3115
3115
  ExperimentName["PROTOCOL_MONITOR"] = "protocol-monitor";
3116
3116
  ExperimentName["SAMPLING_HEAP_PROFILER_TIMELINE"] = "sampling-heap-profiler-timeline";
3117
- ExperimentName["SHOW_OPTION_TO_EXPOSE_INTERNALS_IN_HEAP_SNAPSHOT"] = "show-option-to-expose-internals-in-heap-snapshot";
3118
3117
  ExperimentName["TIMELINE_INVALIDATION_TRACKING"] = "timeline-invalidation-tracking";
3119
- ExperimentName["TIMELINE_SHOW_ALL_EVENTS"] = "timeline-show-all-events";
3120
3118
  ExperimentName["APCA"] = "apca";
3121
3119
  ExperimentName["FONT_EDITOR"] = "font-editor";
3122
- ExperimentName["FULL_ACCESSIBILITY_TREE"] = "full-accessibility-tree";
3123
- ExperimentName["EXPERIMENTAL_COOKIE_FEATURES"] = "experimental-cookie-features";
3124
3120
  ExperimentName["INSTRUMENTATION_BREAKPOINTS"] = "instrumentation-breakpoints";
3125
3121
  ExperimentName["USE_SOURCE_MAP_SCOPES"] = "use-source-map-scopes";
3126
3122
  ExperimentName["TIMELINE_DEBUG_MODE"] = "timeline-debug-mode";
@@ -9111,6 +9107,7 @@ function hsva2rgba(hsva) {
9111
9107
  }
9112
9108
  const EPSILON = 0.01;
9113
9109
  const WIDE_RANGE_EPSILON = 1;
9110
+ const STRICT_EPSILON = 1e-4;
9114
9111
  function equals(a, b, accuracy = EPSILON) {
9115
9112
  if (Array.isArray(a) && Array.isArray(b)) {
9116
9113
  if (a.length !== b.length) {
@@ -9349,7 +9346,7 @@ class LCH {
9349
9346
  return false;
9350
9347
  }
9351
9348
  isHuePowerless() {
9352
- return equals(this.c, 0);
9349
+ return equals(this.c, 0, STRICT_EPSILON);
9353
9350
  }
9354
9351
  static fromSpec(spec, text) {
9355
9352
  const L = parsePercentage(spec[0], [0, 100]) ?? parseNumber(spec[0]);
@@ -9821,7 +9818,7 @@ class HSL {
9821
9818
  this.l = clamp(l, { min: 0, max: 1 });
9822
9819
  s = equals(this.l, 0) || equals(this.l, 1) ? 0 : s;
9823
9820
  this.s = clamp(s, { min: 0, max: 1 });
9824
- h = equals(this.s, 0) ? 0 : h;
9821
+ h = equals(this.s, 0, STRICT_EPSILON) ? 0 : h;
9825
9822
  this.h = normalizeHue(h * 360) / 360;
9826
9823
  this.alpha = clamp(alpha ?? null, { min: 0, max: 1 });
9827
9824
  this.#authoredText = authoredText;