extension-develop 3.5.0 → 3.6.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 CHANGED
@@ -101,20 +101,31 @@ Options accepted by each command. Values shown are typical types or enumerations
101
101
 
102
102
  ### dev
103
103
 
104
- | Option | Type / Values | Description |
105
- | ------------- | ----------------------------------- | --------------------------------------------------------------------------------- |
106
- | mode | development, production, none | Build mode |
107
- | polyfill | boolean | Include `webextension-polyfill` when possible |
108
- | port | number or string | Dev server port |
109
- | source | string | Inspect a source directory |
110
- | watchSource | boolean | Watch the source directory |
111
- | logs | off,error,warn,info,debug,trace,all | Unified logger verbosity (all shows everything) |
112
- | logContext | list or `all` | Comma-separated contexts (background,content,page,sidebar,popup,options,devtools) |
113
- | logFormat | pretty,json | Pretty text or JSON output |
114
- | logTimestamps | boolean | Include timestamps in pretty output |
115
- | logColor | boolean | Colorize pretty output |
116
- | logUrl | string or /regex/flags | Filter by URL substring or JS-style regex literal |
117
- | logTab | number | Filter by tabId |
104
+ | Option | Type / Values | Description |
105
+ | ------------------- | ----------------------------------- | --------------------------------------------------------------------------------- |
106
+ | mode | development, production, none | Build mode |
107
+ | polyfill | boolean | Include `webextension-polyfill` when possible |
108
+ | port | number or string | Dev server port |
109
+ | source | string | Inspect a source directory |
110
+ | watchSource | boolean | Watch the source directory |
111
+ | sourceFormat | pretty,json,ndjson | Output format for source HTML |
112
+ | sourceSummary | boolean | Output a compact summary instead of full HTML |
113
+ | sourceMeta | boolean | Output page metadata (readyState, viewport, frames) |
114
+ | sourceProbe | string[] | CSS selectors to probe for quick validation |
115
+ | sourceTree | off,root-only | Output a compact extension root tree |
116
+ | sourceConsole | boolean | Output console summary (best-effort) |
117
+ | sourceDom | boolean | Output DOM snapshots and diffs |
118
+ | sourceMaxBytes | number | Limit HTML output size in bytes (0 disables truncation) |
119
+ | sourceRedact | off,safe,strict | Redact sensitive content in HTML output |
120
+ | sourceIncludeShadow | off,open-only,all | Control Shadow DOM inclusion |
121
+ | sourceDiff | boolean | Include diff metadata on watch updates |
122
+ | logs | off,error,warn,info,debug,trace,all | Unified logger verbosity (all shows everything) |
123
+ | logContext | list or `all` | Comma-separated contexts (background,content,page,sidebar,popup,options,devtools) |
124
+ | logFormat | pretty,json,ndjson | Pretty text or JSON/NDJSON output |
125
+ | logTimestamps | boolean | Include timestamps in pretty output |
126
+ | logColor | boolean | Colorize pretty output |
127
+ | logUrl | string or /regex/flags | Filter by URL substring or JS-style regex literal |
128
+ | logTab | number | Filter by tabId |
118
129
 
119
130
  ### build
120
131
 
@@ -161,7 +172,7 @@ Options accepted by each command. Values shown are typical types or enumerations
161
172
  - Supported sections:
162
173
  - config(config: Configuration): mutate the assembled Rspack config. Supports a function or a plain object. When an object is provided, it is deep‑merged on top of the assembled config.
163
174
  - commands.dev | .build | .start | .preview: per‑command options (browser, profile, binaries, flags, preferences, unified logger defaults, packaging). These defaults are applied for all respective commands.
164
- - browser.chrome | .firefox | .edge | .chromium-based | .gecko-based: start flags, excluded flags, preferences, binaries, and profile reuse (persistProfile).
175
+ - browser.chrome | .firefox | .edge | .chromium-based | .gecko-based: start flags, excluded flags, preferences, binaries, profile reuse (persistProfile), and per-browser `extensions`.
165
176
  - extensions: load-only companion extensions (unpacked dirs) loaded alongside your extension in dev/preview/start.
166
177
  - Example: { dir: "./extensions" } loads every "./extensions/\*" folder that contains a manifest.json.
167
178
  - Precedence when composing options: browser._ → commands._ → CLI flags. CLI flags always win over config defaults.
@@ -180,9 +191,13 @@ Use this when you have other unpacked extensions you want loaded alongside your
180
191
  - **How they’re loaded**: they’re appended into the browser runner’s `--load-extension` list (Chromium) / addon install list (Firefox) **before** your extension. Your extension is always loaded last for precedence.
181
192
  - **Discovery**:
182
193
  - `extensions.dir`: scans one level deep (e.g. `./extensions/*/manifest.json`)
194
+ - When `dir` points to `./extensions`, browser folders like `./extensions/chrome/*` and `./extensions/firefox/*` are also scanned.
183
195
  - `extensions.paths`: explicit directories (absolute or relative to the project root)
184
196
  - **Overrides**: top-level `extensions` applies to all commands, but `commands.<cmd>.extensions` overrides it for that command.
185
197
  - **Invalid entries**: ignored. In author mode (`EXTENSION_AUTHOR_MODE=true`) we print a warning if `extensions` is configured but nothing resolves.
198
+ - **Store URLs**: entries pointing to Chrome Web Store, Edge Addons, or AMO are downloaded on-demand into `./extensions/<browser>/<id-or-slug>`.
199
+ - **Local paths**: only paths under `./extensions/` are accepted for companion extensions.
200
+ - **CLI**: use `--extensions <csv>` to provide a comma-separated list of paths or store URLs.
186
201
 
187
202
  Example:
188
203
 
package/dist/215.cjs CHANGED
@@ -74,7 +74,7 @@ exports.modules = {
74
74
  return mainHTML;
75
75
  }
76
76
  }
77
- async function getPageHTML(cdp, sessionId) {
77
+ async function getPageHTML(cdp, sessionId, includeShadow = 'open-only') {
78
78
  try {
79
79
  await cdp.evaluate(sessionId, 'document.title');
80
80
  } catch {}
@@ -97,6 +97,7 @@ exports.modules = {
97
97
  }
98
98
  })()`);
99
99
  const mainHTML = 'string' == typeof mainHTMLRaw ? mainHTMLRaw : String(mainHTMLRaw || '');
100
+ if ('off' === includeShadow) return mainHTML;
100
101
  try {
101
102
  const mergedHtmlRaw = await cdp.evaluate(sessionId, `(() => { try {
102
103
  var cloned = document.documentElement.cloneNode(true);
@@ -199,7 +200,7 @@ exports.modules = {
199
200
  while(Date.now() < deadline){
200
201
  try {
201
202
  const injected = await cdp.evaluate(sessionId, `(() => { try {
202
- const hosts = Array.from(document.querySelectorAll('[data-extension-root="true"]'));
203
+ const hosts = Array.from(document.querySelectorAll('#extension-root,[data-extension-root="true"]'));
203
204
  if (!hosts.length) return false;
204
205
  const markers = ['iskilar_box','content_script','content_title','js-probe'];
205
206
  for (const h of hosts) {
@@ -244,7 +245,7 @@ exports.modules = {
244
245
  try {
245
246
  this.targetWebSocketUrl = await (0, discovery.N)(this.host, this.port, this.isDev());
246
247
  this.ws = await establishBrowserConnection(this.targetWebSocketUrl, this.isDev(), (data)=>this.handleMessage(data), (reason)=>{
247
- this.pendingRequests.forEach(({ reject }, id)=>{
248
+ this.pendingRequests.forEach(({ reject, timeout }, id)=>{
248
249
  try {
249
250
  reject(new Error(reason));
250
251
  } catch (error) {
@@ -253,6 +254,7 @@ exports.modules = {
253
254
  console.warn(messages.r0s(String(err.message || err)));
254
255
  }
255
256
  }
257
+ if (timeout) clearTimeout(timeout);
256
258
  this.pendingRequests.delete(id);
257
259
  });
258
260
  });
@@ -281,6 +283,7 @@ exports.modules = {
281
283
  if (message.id) {
282
284
  const pending = this.pendingRequests.get(message.id);
283
285
  if (pending) {
286
+ if (pending.timeout) clearTimeout(pending.timeout);
284
287
  this.pendingRequests.delete(message.id);
285
288
  if (message.error) pending.reject(new Error(JSON.stringify(message.error)));
286
289
  else pending.resolve(message.result);
@@ -299,7 +302,7 @@ exports.modules = {
299
302
  }
300
303
  }
301
304
  }
302
- async sendCommand(method, params = {}, sessionId) {
305
+ async sendCommand(method, params = {}, sessionId, timeoutMs = 12000) {
303
306
  return new Promise((resolve, reject)=>{
304
307
  if (!this.ws || this.ws.readyState !== external_ws_default().OPEN) return reject(new Error('WebSocket is not open'));
305
308
  const id = ++this.messageId;
@@ -310,12 +313,22 @@ exports.modules = {
310
313
  };
311
314
  if (sessionId) message.sessionId = sessionId;
312
315
  try {
316
+ const timeout = setTimeout(()=>{
317
+ const pending = this.pendingRequests.get(id);
318
+ if (!pending) return;
319
+ this.pendingRequests.delete(id);
320
+ pending.reject(new Error(`CDP command timed out (${timeoutMs}ms): ${String(pending.method || method)}`));
321
+ }, timeoutMs);
313
322
  this.pendingRequests.set(id, {
314
323
  resolve,
315
- reject
324
+ reject,
325
+ timeout,
326
+ method
316
327
  });
317
328
  this.ws.send(JSON.stringify(message));
318
329
  } catch (error) {
330
+ const pending = this.pendingRequests.get(id);
331
+ if (pending?.timeout) clearTimeout(pending.timeout);
319
332
  this.pendingRequests.delete(id);
320
333
  reject(error);
321
334
  }
@@ -376,8 +389,8 @@ exports.modules = {
376
389
  }, sessionId);
377
390
  return response.result?.value;
378
391
  }
379
- async getPageHTML(sessionId) {
380
- return getPageHTML(this, sessionId);
392
+ async getPageHTML(sessionId, includeShadow = 'open-only') {
393
+ return getPageHTML(this, sessionId, includeShadow);
381
394
  }
382
395
  async closeTarget(targetId) {
383
396
  await this.sendCommand('Target.closeTarget', {
@@ -392,13 +405,14 @@ exports.modules = {
392
405
  });
393
406
  return true;
394
407
  } catch (error) {
395
- const attempts = 3;
408
+ const attempts = 8;
396
409
  for(let i = 0; i < attempts; i++){
397
410
  try {
398
411
  const ok = await this.reloadExtensionViaTargetEval(extensionId);
399
412
  if (ok) return true;
400
413
  } catch {}
401
- await new Promise((r)=>setTimeout(r, 150 * (i + 1)));
414
+ const backoffMs = Math.min(1200, 150 * (i + 1));
415
+ await new Promise((r)=>setTimeout(r, backoffMs));
402
416
  }
403
417
  console.warn(messages.ccn(extensionId, error.message || String(error)));
404
418
  return false;
@@ -417,24 +431,28 @@ exports.modules = {
417
431
  const preferredOrder = [
418
432
  'service_worker',
419
433
  'background_page',
420
- 'worker'
434
+ 'worker',
435
+ 'page'
421
436
  ];
422
437
  for (const type of preferredOrder){
423
- const t = (targets || []).find((t)=>{
438
+ const matchingTargets = (targets || []).filter((t)=>{
424
439
  const url = String(t?.url || '');
425
440
  const tt = String(t?.type || '');
426
- return tt === type && url.startsWith(`chrome-extension://${extensionId}/`);
441
+ const inExtensionScope = url.startsWith(`chrome-extension://${extensionId}/`);
442
+ return tt === type && inExtensionScope;
427
443
  });
428
- if (!t) continue;
429
- const targetId = t?.targetId;
430
- if (targetId) try {
431
- const sessionId = await this.attachToTarget(targetId);
432
- await this.sendCommand('Runtime.enable', {}, sessionId);
433
- await this.sendCommand('Runtime.evaluate', {
434
- expression: 'chrome && chrome.runtime && chrome.runtime.reload && chrome.runtime.reload()'
435
- }, sessionId);
436
- return true;
437
- } catch {}
444
+ for (const target of matchingTargets){
445
+ const targetId = target?.targetId;
446
+ if (targetId) try {
447
+ const sessionId = await this.attachToTarget(targetId);
448
+ await this.sendCommand('Runtime.enable', {}, sessionId);
449
+ await this.sendCommand('Runtime.evaluate', {
450
+ expression: '(function(){ try { if (!chrome || !chrome.runtime || !chrome.runtime.reload) return false; chrome.runtime.reload(); return true; } catch (error) { return false; } })()',
451
+ returnByValue: true
452
+ }, sessionId);
453
+ return true;
454
+ } catch {}
455
+ }
438
456
  }
439
457
  return false;
440
458
  } catch {
package/dist/323.cjs CHANGED
@@ -12,11 +12,98 @@ exports.modules = {
12
12
  var banner = __webpack_require__("./webpack/plugin-browsers/browsers-lib/banner.ts");
13
13
  var external_path_ = __webpack_require__("path");
14
14
  var external_fs_ = __webpack_require__("fs");
15
- async function deriveExtensionIdFromTargetsHelper(cdp, outPath, maxRetries = 6, backoffMs = 150) {
15
+ async function deriveExtensionIdFromTargetsHelper(cdp, outPath, maxRetries = 6, backoffMs = 150, profilePath, extensionPaths) {
16
+ let expectedName;
17
+ let expectedVersion;
18
+ let expectedManifestVersion;
19
+ let expectedNameIsMsg = false;
20
+ try {
21
+ const manifest = JSON.parse(external_fs_.readFileSync(external_path_.join(outPath, 'manifest.json'), 'utf-8'));
22
+ expectedName = manifest?.name;
23
+ expectedVersion = manifest?.version;
24
+ expectedManifestVersion = manifest?.manifest_version;
25
+ expectedNameIsMsg = 'string' == typeof expectedName && /__MSG_/i.test(expectedName);
26
+ if (expectedNameIsMsg) {
27
+ const defaultLocale = String(manifest?.default_locale || '').trim();
28
+ const msgKeyMatch = String(expectedName || '').match(/__MSG_(.+)__/i);
29
+ const msgKey = msgKeyMatch ? msgKeyMatch[1] : '';
30
+ if (defaultLocale && msgKey) {
31
+ const messagesPath = external_path_.join(outPath, '_locales', defaultLocale, 'messages.json');
32
+ if (external_fs_.existsSync(messagesPath)) {
33
+ const messagesJson = JSON.parse(external_fs_.readFileSync(messagesPath, 'utf-8'));
34
+ const resolved = String(messagesJson?.[msgKey]?.message || '').trim();
35
+ if (resolved) {
36
+ expectedName = resolved;
37
+ expectedNameIsMsg = false;
38
+ }
39
+ }
40
+ }
41
+ }
42
+ } catch {}
43
+ const trimTrailingSep = (p)=>p.replace(/[\\\/]+$/g, '');
44
+ const normalizePath = (p)=>{
45
+ try {
46
+ const resolved = external_path_.resolve(p);
47
+ if (external_fs_.existsSync(resolved)) return trimTrailingSep(external_fs_.realpathSync(resolved));
48
+ return trimTrailingSep(resolved);
49
+ } catch {
50
+ return trimTrailingSep(external_path_.resolve(p));
51
+ }
52
+ };
53
+ const resolvedOutPath = normalizePath(outPath);
54
+ const normalizedCandidates = Array.isArray(extensionPaths) ? extensionPaths.map((p)=>p ? normalizePath(p) : '').filter(Boolean) : [];
55
+ const resolvedCandidates = normalizedCandidates.length ? normalizedCandidates : [
56
+ resolvedOutPath
57
+ ];
58
+ const platformIsCaseInsensitive = 'win32' === process.platform || 'darwin' === process.platform;
59
+ const normalizeForCompare = (p)=>platformIsCaseInsensitive ? p.toLowerCase() : p;
60
+ const matchesAnyCandidate = (p)=>{
61
+ const n = normalizeForCompare(p);
62
+ return resolvedCandidates.some((candidate)=>n === normalizeForCompare(candidate));
63
+ };
64
+ const deriveFromProfile = ()=>{
65
+ if (!profilePath) return null;
66
+ const candidates = [];
67
+ const pushPrefIfExists = (dir)=>{
68
+ const prefPath = external_path_.join(dir, 'Preferences');
69
+ if (external_fs_.existsSync(prefPath)) candidates.push(prefPath);
70
+ };
71
+ try {
72
+ pushPrefIfExists(profilePath);
73
+ pushPrefIfExists(external_path_.join(profilePath, 'Default'));
74
+ const entries = external_fs_.readdirSync(profilePath);
75
+ for (const entry of entries)if (/^Profile\s+\d+$/i.test(entry)) pushPrefIfExists(external_path_.join(profilePath, entry));
76
+ } catch {}
77
+ for (const prefPath of candidates)try {
78
+ if (!external_fs_.existsSync(prefPath)) continue;
79
+ const prefs = JSON.parse(external_fs_.readFileSync(prefPath, 'utf-8'));
80
+ const settings = prefs?.extensions?.settings;
81
+ if (!settings || 'object' != typeof settings) continue;
82
+ const entries = Object.entries(settings);
83
+ let fallbackId = null;
84
+ for (const [id, info] of entries){
85
+ const storedPath = String(info?.path || '');
86
+ if (!storedPath) continue;
87
+ const normalized = normalizePath(storedPath);
88
+ if (!matchesAnyCandidate(normalized)) continue;
89
+ const manifestName = String(info?.manifest?.name || '');
90
+ const manifestVersion = String(info?.manifest?.version || '');
91
+ if (expectedName && manifestName === expectedName) return id;
92
+ if (expectedVersion && manifestVersion === expectedVersion) return id;
93
+ fallbackId = id;
94
+ }
95
+ if (fallbackId) return fallbackId;
96
+ } catch {}
97
+ return null;
98
+ };
16
99
  let retries = 0;
17
100
  while(retries <= maxRetries){
18
101
  try {
19
102
  const targets = await cdp.getTargets();
103
+ const profileCandidateId = deriveFromProfile();
104
+ let firstEvalId = null;
105
+ let evalIdCount = 0;
106
+ let urlDerivedId = null;
20
107
  for (const t of targets || []){
21
108
  const url = String(t?.url || '');
22
109
  const type = String(t?.type || '');
@@ -26,6 +113,8 @@ exports.modules = {
26
113
  'worker'
27
114
  ].includes(type);
28
115
  if (!typeOk) continue;
116
+ const urlMatch = url.match(/^chrome-extension:\/\/([^\/]+)/);
117
+ if (!urlDerivedId && urlMatch?.[1]) urlDerivedId = String(urlMatch[1]);
29
118
  if (url && !url.startsWith('chrome-extension://')) continue;
30
119
  const targetId = t?.targetId;
31
120
  if (targetId) try {
@@ -35,9 +124,22 @@ exports.modules = {
35
124
  const info = await cdp.evaluate(sessionId, '(()=>{try{const m=chrome.runtime.getManifest?.();return {id:chrome.runtime?.id||"",name:m?.name||"",version:m?.version||"",manifestVersion:m?.manifest_version||0}}catch(_){return null}})()');
36
125
  const id = String(info?.id || '').trim();
37
126
  if (!id) continue;
38
- return id;
127
+ evalIdCount += 1;
128
+ if (!firstEvalId) firstEvalId = id;
129
+ if (profileCandidateId && id === profileCandidateId) return id;
130
+ const gotName = String(info?.name || '');
131
+ const gotVersion = String(info?.version || '');
132
+ const gotManifestVersion = Number(info?.manifestVersion || 0);
133
+ const nameMatches = expectedName && !expectedNameIsMsg ? gotName === expectedName : false;
134
+ const versionMatches = expectedVersion ? gotVersion === expectedVersion : false;
135
+ const manifestVersionMatches = expectedManifestVersion ? gotManifestVersion === expectedManifestVersion : false;
136
+ if (nameMatches && (!profileCandidateId || id === profileCandidateId)) return id;
137
+ if (expectedVersion && versionMatches && (expectedManifestVersion ? manifestVersionMatches : true) && (!profileCandidateId || id === profileCandidateId)) return id;
39
138
  } catch {}
40
139
  }
140
+ if (1 === evalIdCount && firstEvalId) return firstEvalId;
141
+ if (profileCandidateId) return profileCandidateId;
142
+ if (urlDerivedId) return urlDerivedId;
41
143
  } catch {}
42
144
  await new Promise((r)=>setTimeout(r, backoffMs));
43
145
  retries++;
@@ -214,7 +316,7 @@ exports.modules = {
214
316
  }
215
317
  async deriveExtensionIdFromTargets(maxRetries = 20, backoffMs = 200) {
216
318
  if (!this.cdp) return null;
217
- return await deriveExtensionIdFromTargetsHelper(this.cdp, this.outPath, maxRetries, backoffMs);
319
+ return await deriveExtensionIdFromTargetsHelper(this.cdp, this.outPath, maxRetries, backoffMs, this.profilePath, this.extensionPaths);
218
320
  }
219
321
  async hardReload() {
220
322
  if (!this.cdp || !this.extensionId) return false;
@@ -322,17 +424,25 @@ exports.modules = {
322
424
  _define_property(this, "outPath", void 0);
323
425
  _define_property(this, "browser", void 0);
324
426
  _define_property(this, "cdpPort", void 0);
427
+ _define_property(this, "profilePath", void 0);
428
+ _define_property(this, "extensionPaths", void 0);
325
429
  _define_property(this, "cdp", null);
326
430
  _define_property(this, "extensionId", null);
327
431
  this.outPath = args.outPath;
328
432
  this.browser = args.browser;
329
433
  this.cdpPort = args.cdpPort;
434
+ this.profilePath = args.profilePath;
435
+ this.extensionPaths = args.extensionPaths;
330
436
  }
331
437
  }
332
438
  var extension_output_path = __webpack_require__("./webpack/plugin-browsers/run-chromium/chromium-launch/extension-output-path.ts");
333
439
  async function setupCdpAfterLaunch(compilation, plugin, chromiumArgs) {
334
440
  const loadExtensionFlag = chromiumArgs.find((flag)=>flag.startsWith('--load-extension='));
335
441
  const extensionOutputPath = (0, extension_output_path.W)(compilation, loadExtensionFlag);
442
+ const extensionPaths = loadExtensionFlag ? loadExtensionFlag.replace('--load-extension=', '').split(',').map((s)=>s.trim()).filter(Boolean) : [];
443
+ const selectedExtensionPaths = extensionOutputPath && extensionOutputPath.length > 0 ? [
444
+ extensionOutputPath
445
+ ] : extensionPaths;
336
446
  const remoteDebugPortFlag = chromiumArgs.find((flag)=>flag.startsWith('--remote-debugging-port='));
337
447
  const chromeRemoteDebugPort = remoteDebugPortFlag ? parseInt(remoteDebugPortFlag.split('=')[1], 10) : (0, shared_utils.jl)(plugin.port, plugin.instanceId);
338
448
  const userDataDirFlag = chromiumArgs.find((flag)=>flag.startsWith('--user-data-dir='));
@@ -344,7 +454,9 @@ exports.modules = {
344
454
  const cdpExtensionController = new CDPExtensionController({
345
455
  outPath: extensionOutputPath,
346
456
  browser: 'chromium-based' === plugin.browser ? 'chrome' : plugin.browser,
347
- cdpPort: chromeRemoteDebugPort
457
+ cdpPort: chromeRemoteDebugPort,
458
+ profilePath: userDataDir || void 0,
459
+ extensionPaths: selectedExtensionPaths
348
460
  });
349
461
  const retryAsync = async (operation, attempts = 5, initialDelayMs = 150)=>{
350
462
  let lastError;
@@ -361,7 +473,13 @@ exports.modules = {
361
473
  if ('true' === process.env.EXTENSION_AUTHOR_MODE) console.log(messages.M3V('127.0.0.1', chromeRemoteDebugPort));
362
474
  let extensionControllerInfo = null;
363
475
  try {
364
- extensionControllerInfo = await cdpExtensionController.ensureLoaded();
476
+ const ensureLoadedTimeoutMs = 10000;
477
+ extensionControllerInfo = await Promise.race([
478
+ cdpExtensionController.ensureLoaded(),
479
+ new Promise((_, reject)=>{
480
+ setTimeout(()=>reject(new Error(`ensureLoaded timeout (${ensureLoadedTimeoutMs}ms)`)), ensureLoadedTimeoutMs);
481
+ })
482
+ ]);
365
483
  } catch (error) {
366
484
  if ('true' === process.env.EXTENSION_AUTHOR_MODE) console.warn(`[CDP] ensureLoaded failed: ${String(error?.message || error)}`);
367
485
  }