sourcemonkey 0.0.0-stage → 0.5.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 (367) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +87 -2
  3. package/dist/bytes.js +37 -0
  4. package/dist/channel-placeholder.js +5 -0
  5. package/dist/collection-scan.js +17 -0
  6. package/dist/collections.html +353 -0
  7. package/dist/compare.html +358 -0
  8. package/dist/composed-source.js +205 -0
  9. package/dist/connect-prompt.html +47 -0
  10. package/dist/debug/icons.html +211 -0
  11. package/dist/debug/storage.html +163 -0
  12. package/dist/debug-log.js +75 -0
  13. package/dist/diagnostic-location.js +64 -0
  14. package/dist/eval-hook.js +308 -0
  15. package/dist/extension/background.js +5263 -0
  16. package/dist/extension/channel.js +133 -0
  17. package/dist/extension/code-links.js +187 -0
  18. package/dist/extension/code-tabs.js +460 -0
  19. package/dist/extension/collection-dialog.js +619 -0
  20. package/dist/extension/collection-display.js +322 -0
  21. package/dist/extension/collections.js +2905 -0
  22. package/dist/extension/compare.js +867 -0
  23. package/dist/extension/concurrency.js +186 -0
  24. package/dist/extension/connect-grants.js +109 -0
  25. package/dist/extension/connect-prompt-page.js +70 -0
  26. package/dist/extension/connect-prompt.js +266 -0
  27. package/dist/extension/copy-button.js +63 -0
  28. package/dist/extension/copy-dialog.js +502 -0
  29. package/dist/extension/copy-preference.js +66 -0
  30. package/dist/extension/debug/icons.js +203 -0
  31. package/dist/extension/debug/storage.js +365 -0
  32. package/dist/extension/deep-link.js +77 -0
  33. package/dist/extension/delete-dialog.js +432 -0
  34. package/dist/extension/delete-icon.js +40 -0
  35. package/dist/extension/details-tooltips.js +66 -0
  36. package/dist/extension/directories.js +679 -0
  37. package/dist/extension/export-backup.js +407 -0
  38. package/dist/extension/export-dialog.js +142 -0
  39. package/dist/extension/file-access.js +74 -0
  40. package/dist/extension/file-changes.js +346 -0
  41. package/dist/extension/frame-info.js +85 -0
  42. package/dist/extension/github-provider.js +336 -0
  43. package/dist/extension/glyph.js +84 -0
  44. package/dist/extension/gm-bindings.js +601 -0
  45. package/dist/extension/gm-download.js +419 -0
  46. package/dist/extension/gm-menu.js +228 -0
  47. package/dist/extension/gm-notification.js +141 -0
  48. package/dist/extension/gm-originator-map.js +23 -0
  49. package/dist/extension/gm-tab-data.js +121 -0
  50. package/dist/extension/gm-tab.js +160 -0
  51. package/dist/extension/gm-url-change.js +111 -0
  52. package/dist/extension/gm-url-scheme.js +27 -0
  53. package/dist/extension/gm-values.js +176 -0
  54. package/dist/extension/gm-window-control.js +104 -0
  55. package/dist/extension/gm-xhr-headers.js +333 -0
  56. package/dist/extension/gm-xhr.js +584 -0
  57. package/dist/extension/injector.js +3145 -0
  58. package/dist/extension/install-capture.js +277 -0
  59. package/dist/extension/line-diff.js +233 -0
  60. package/dist/extension/local-copy.js +560 -0
  61. package/dist/extension/local-delete.js +276 -0
  62. package/dist/extension/location-kinds.js +108 -0
  63. package/dist/extension/log-format.js +158 -0
  64. package/dist/extension/managed-changes.js +361 -0
  65. package/dist/extension/managed-items.js +57 -0
  66. package/dist/extension/match-index.js +375 -0
  67. package/dist/extension/missing-scripts.js +66 -0
  68. package/dist/extension/more-menu.js +109 -0
  69. package/dist/extension/move-dialog.js +197 -0
  70. package/dist/extension/offscreen.js +37 -0
  71. package/dist/extension/options.js +504 -0
  72. package/dist/extension/page-heading.js +170 -0
  73. package/dist/extension/page-links.js +199 -0
  74. package/dist/extension/parse-check-client.js +70 -0
  75. package/dist/extension/parse-check.js +161 -0
  76. package/dist/extension/parse-sandbox-host.js +172 -0
  77. package/dist/extension/parse-sandbox.js +175 -0
  78. package/dist/extension/popup-commands-open.js +41 -0
  79. package/dist/extension/popup-view.js +387 -0
  80. package/dist/extension/popup.js +703 -0
  81. package/dist/extension/prism-userscript.js +53 -0
  82. package/dist/extension/refresh-error.js +136 -0
  83. package/dist/extension/refresh-targets.js +160 -0
  84. package/dist/extension/related-versions.js +1144 -0
  85. package/dist/extension/resize-observer-loop.js +19 -0
  86. package/dist/extension/row-cache.js +387 -0
  87. package/dist/extension/script-blobs.js +413 -0
  88. package/dist/extension/script-details.js +658 -0
  89. package/dist/extension/script-enablement.js +285 -0
  90. package/dist/extension/script-identity.js +310 -0
  91. package/dist/extension/script-keys.js +54 -0
  92. package/dist/extension/script-log-id.js +155 -0
  93. package/dist/extension/script-log.js +290 -0
  94. package/dist/extension/script-rejection.js +104 -0
  95. package/dist/extension/script-sites.js +158 -0
  96. package/dist/extension/script-store.js +3507 -0
  97. package/dist/extension/script-updater.js +991 -0
  98. package/dist/extension/script-url-hash.js +61 -0
  99. package/dist/extension/select-all.js +37 -0
  100. package/dist/extension/select-open-guard.js +90 -0
  101. package/dist/extension/site-icon.js +1261 -0
  102. package/dist/extension/sm-dev-link.js +302 -0
  103. package/dist/extension/solo-click.js +58 -0
  104. package/dist/extension/sticky-numbers.js +188 -0
  105. package/dist/extension/storage-breakdown.js +253 -0
  106. package/dist/extension/storage-scan.js +86 -0
  107. package/dist/extension/storage-usage.js +194 -0
  108. package/dist/extension/structural-keys.js +29 -0
  109. package/dist/extension/sw-teardown.js +36 -0
  110. package/dist/extension/tab-status.js +403 -0
  111. package/dist/extension/toolbar-summary.js +105 -0
  112. package/dist/extension/user-scripts-access.js +43 -0
  113. package/dist/extension/userscript.js +3672 -0
  114. package/dist/extension/value-store.js +204 -0
  115. package/dist/extension/version-probe-fetch.js +68 -0
  116. package/dist/extension/worker-client.js +109 -0
  117. package/dist/extension/worker-ops.js +171 -0
  118. package/dist/extension/zip-writer.js +24 -0
  119. package/dist/external-files.js +878 -0
  120. package/dist/file-loader.js +436 -0
  121. package/dist/file-locations.js +439 -0
  122. package/dist/generated-wrappers.js +5 -0
  123. package/dist/github-auth.js +148 -0
  124. package/dist/github-urls.js +46 -0
  125. package/dist/gm-api-catalog.js +217 -0
  126. package/dist/gm-connect.js +273 -0
  127. package/dist/gm-xhr-types.js +5 -0
  128. package/dist/http-read.js +142 -0
  129. package/dist/icons/icon-128.png +0 -0
  130. package/dist/icons/icon-16.png +0 -0
  131. package/dist/icons/icon-48.png +0 -0
  132. package/dist/local-file-overrides.js +558 -0
  133. package/dist/manifest.json +38 -0
  134. package/dist/markup-response.js +126 -0
  135. package/dist/match-targets.js +305 -0
  136. package/dist/menu-captions.js +22 -0
  137. package/dist/offscreen.html +39 -0
  138. package/dist/options.html +197 -0
  139. package/dist/parse-check-protocol.js +51 -0
  140. package/dist/parse-location.js +171 -0
  141. package/dist/parse-sandbox.html +56 -0
  142. package/dist/phase-timing.js +155 -0
  143. package/dist/popup.html +197 -0
  144. package/dist/public-suffixes.js +6957 -0
  145. package/dist/run-context.js +31 -0
  146. package/dist/runtime-messages.js +34 -0
  147. package/dist/sandboxed-page.js +35 -0
  148. package/dist/script-build.js +787 -0
  149. package/dist/script-display.js +383 -0
  150. package/dist/script-icon.js +43 -0
  151. package/dist/script-links.js +204 -0
  152. package/dist/script-source-url.js +214 -0
  153. package/dist/script-world.js +65 -0
  154. package/dist/sm-dev-protocol.js +84 -0
  155. package/dist/source-map.js +177 -0
  156. package/dist/stack-link.js +138 -0
  157. package/dist/styles.css +825 -0
  158. package/dist/unsafe-window-use.js +229 -0
  159. package/dist/url-match.js +734 -0
  160. package/dist/userscript-header.js +248 -0
  161. package/dist/userscript.html +1165 -0
  162. package/dist/vendor/fflate.LICENSE +21 -0
  163. package/dist/vendor/fflate.js +2692 -0
  164. package/dist/vendor/prism/prism-line-numbers.css +1 -0
  165. package/dist/vendor/prism/prism-line-numbers.js +1 -0
  166. package/dist/vendor/prism/prism.css +1 -0
  167. package/dist/vendor/prism/prism.js +1946 -0
  168. package/dist/version-compare.js +141 -0
  169. package/dist/version-probe.js +120 -0
  170. package/lib/build-info.json +1 -0
  171. package/lib/bytes.d.ts +3 -0
  172. package/lib/bytes.js +37 -0
  173. package/lib/channel-placeholder.d.ts +1 -0
  174. package/lib/channel-placeholder.js +5 -0
  175. package/lib/collection-scan.d.ts +8 -0
  176. package/lib/collection-scan.js +17 -0
  177. package/lib/composed-source.d.ts +28 -0
  178. package/lib/composed-source.js +205 -0
  179. package/lib/debug-log.d.ts +9 -0
  180. package/lib/debug-log.js +75 -0
  181. package/lib/diagnostic-location.d.ts +8 -0
  182. package/lib/diagnostic-location.js +64 -0
  183. package/lib/eval-hook.d.ts +34 -0
  184. package/lib/eval-hook.js +308 -0
  185. package/lib/external-files.d.ts +111 -0
  186. package/lib/external-files.js +878 -0
  187. package/lib/file-loader.d.ts +48 -0
  188. package/lib/file-loader.js +436 -0
  189. package/lib/file-locations.d.ts +44 -0
  190. package/lib/file-locations.js +439 -0
  191. package/lib/generated-wrappers.d.ts +11 -0
  192. package/lib/generated-wrappers.js +5 -0
  193. package/lib/github-auth.d.ts +12 -0
  194. package/lib/github-auth.js +148 -0
  195. package/lib/github-urls.d.ts +1 -0
  196. package/lib/github-urls.js +46 -0
  197. package/lib/gm-api-catalog.d.ts +24 -0
  198. package/lib/gm-api-catalog.js +217 -0
  199. package/lib/gm-connect.d.ts +34 -0
  200. package/lib/gm-connect.js +273 -0
  201. package/lib/gm-xhr-types.d.ts +68 -0
  202. package/lib/gm-xhr-types.js +5 -0
  203. package/lib/harness/browser-extensions.d.ts +73 -0
  204. package/lib/harness/browser-extensions.js +285 -0
  205. package/lib/harness/browser-lang.d.ts +20 -0
  206. package/lib/harness/browser-lang.js +86 -0
  207. package/lib/harness/browser-launch.d.ts +14 -0
  208. package/lib/harness/browser-launch.js +184 -0
  209. package/lib/harness/checks.d.ts +110 -0
  210. package/lib/harness/checks.js +321 -0
  211. package/lib/harness/cli.d.ts +2 -0
  212. package/lib/harness/cli.js +2259 -0
  213. package/lib/harness/command-args.d.ts +91 -0
  214. package/lib/harness/command-args.js +369 -0
  215. package/lib/harness/command-lines.d.ts +80 -0
  216. package/lib/harness/command-lines.js +180 -0
  217. package/lib/harness/command-list.d.ts +206 -0
  218. package/lib/harness/command-list.js +1475 -0
  219. package/lib/harness/console-commands.d.ts +463 -0
  220. package/lib/harness/console-commands.js +3216 -0
  221. package/lib/harness/download-watch.d.ts +21 -0
  222. package/lib/harness/download-watch.js +156 -0
  223. package/lib/harness/extension-backend.d.ts +41 -0
  224. package/lib/harness/extension-backend.js +100 -0
  225. package/lib/harness/extension-host.d.ts +112 -0
  226. package/lib/harness/extension-host.js +552 -0
  227. package/lib/harness/extension-launch.d.ts +67 -0
  228. package/lib/harness/extension-launch.js +285 -0
  229. package/lib/harness/extension-link.d.ts +315 -0
  230. package/lib/harness/extension-link.js +607 -0
  231. package/lib/harness/external-manager.d.ts +146 -0
  232. package/lib/harness/external-manager.js +414 -0
  233. package/lib/harness/fixture.d.ts +165 -0
  234. package/lib/harness/fixture.js +461 -0
  235. package/lib/harness/frame-sessions.d.ts +71 -0
  236. package/lib/harness/frame-sessions.js +248 -0
  237. package/lib/harness/frames.d.ts +68 -0
  238. package/lib/harness/frames.js +216 -0
  239. package/lib/harness/help-format.d.ts +17 -0
  240. package/lib/harness/help-format.js +127 -0
  241. package/lib/harness/host.d.ts +352 -0
  242. package/lib/harness/host.js +423 -0
  243. package/lib/harness/index.d.ts +8 -0
  244. package/lib/harness/index.js +10 -0
  245. package/lib/harness/input-file.d.ts +18 -0
  246. package/lib/harness/input-file.js +61 -0
  247. package/lib/harness/json-output.d.ts +565 -0
  248. package/lib/harness/json-output.js +71 -0
  249. package/lib/harness/manager-actions.d.ts +21 -0
  250. package/lib/harness/manager-actions.js +129 -0
  251. package/lib/harness/manager-commands.d.ts +142 -0
  252. package/lib/harness/manager-commands.js +1196 -0
  253. package/lib/harness/manager-fetch.d.ts +33 -0
  254. package/lib/harness/manager-fetch.js +239 -0
  255. package/lib/harness/managers.d.ts +24 -0
  256. package/lib/harness/managers.js +27 -0
  257. package/lib/harness/output-style.d.ts +125 -0
  258. package/lib/harness/output-style.js +322 -0
  259. package/lib/harness/page-commands.d.ts +134 -0
  260. package/lib/harness/page-commands.js +1141 -0
  261. package/lib/harness/page-output.d.ts +67 -0
  262. package/lib/harness/page-output.js +139 -0
  263. package/lib/harness/page-watch.d.ts +171 -0
  264. package/lib/harness/page-watch.js +607 -0
  265. package/lib/harness/parse-check-node.d.ts +3 -0
  266. package/lib/harness/parse-check-node.js +132 -0
  267. package/lib/harness/path-args.d.ts +22 -0
  268. package/lib/harness/path-args.js +148 -0
  269. package/lib/harness/playwright-chromium.d.ts +16 -0
  270. package/lib/harness/playwright-chromium.js +86 -0
  271. package/lib/harness/prepare.d.ts +168 -0
  272. package/lib/harness/prepare.js +364 -0
  273. package/lib/harness/prompt-editor.d.ts +17 -0
  274. package/lib/harness/prompt-editor.js +354 -0
  275. package/lib/harness/prompt-history.d.ts +13 -0
  276. package/lib/harness/prompt-history.js +89 -0
  277. package/lib/harness/rebuild-advice.d.ts +6 -0
  278. package/lib/harness/rebuild-advice.js +39 -0
  279. package/lib/harness/run-log.d.ts +47 -0
  280. package/lib/harness/run-log.js +140 -0
  281. package/lib/harness/run-logs.d.ts +132 -0
  282. package/lib/harness/run-logs.js +241 -0
  283. package/lib/harness/run.d.ts +194 -0
  284. package/lib/harness/run.js +1973 -0
  285. package/lib/harness/runs.d.ts +229 -0
  286. package/lib/harness/runs.js +726 -0
  287. package/lib/harness/save-html.d.ts +39 -0
  288. package/lib/harness/save-html.js +89 -0
  289. package/lib/harness/saved-pages.d.ts +120 -0
  290. package/lib/harness/saved-pages.js +275 -0
  291. package/lib/harness/script-mark.d.ts +17 -0
  292. package/lib/harness/script-mark.js +56 -0
  293. package/lib/harness/snapshot.d.ts +143 -0
  294. package/lib/harness/snapshot.js +474 -0
  295. package/lib/harness/stuck-tabs.d.ts +19 -0
  296. package/lib/harness/stuck-tabs.js +131 -0
  297. package/lib/harness/tab-refs.d.ts +20 -0
  298. package/lib/harness/tab-refs.js +80 -0
  299. package/lib/harness/tampermonkey.d.ts +71 -0
  300. package/lib/harness/tampermonkey.js +356 -0
  301. package/lib/harness/tm-ids.d.ts +18 -0
  302. package/lib/harness/tm-ids.js +88 -0
  303. package/lib/harness/violentmonkey.d.ts +34 -0
  304. package/lib/harness/violentmonkey.js +162 -0
  305. package/lib/harness/wait.d.ts +77 -0
  306. package/lib/harness/wait.js +458 -0
  307. package/lib/http-read.d.ts +30 -0
  308. package/lib/http-read.js +142 -0
  309. package/lib/local-file-overrides.d.ts +35 -0
  310. package/lib/local-file-overrides.js +558 -0
  311. package/lib/manifest.json +38 -0
  312. package/lib/markup-response.d.ts +11 -0
  313. package/lib/markup-response.js +126 -0
  314. package/lib/match-targets.d.ts +18 -0
  315. package/lib/match-targets.js +305 -0
  316. package/lib/menu-captions.d.ts +1 -0
  317. package/lib/menu-captions.js +22 -0
  318. package/lib/parse-check-protocol.d.ts +27 -0
  319. package/lib/parse-check-protocol.js +51 -0
  320. package/lib/parse-location.d.ts +12 -0
  321. package/lib/parse-location.js +171 -0
  322. package/lib/phase-timing.d.ts +12 -0
  323. package/lib/phase-timing.js +155 -0
  324. package/lib/public-suffixes.d.ts +1 -0
  325. package/lib/public-suffixes.js +6957 -0
  326. package/lib/run-context.d.ts +2 -0
  327. package/lib/run-context.js +31 -0
  328. package/lib/runtime-messages.d.ts +10 -0
  329. package/lib/runtime-messages.js +34 -0
  330. package/lib/sandboxed-page.d.ts +5 -0
  331. package/lib/sandboxed-page.js +35 -0
  332. package/lib/script-build.d.ts +111 -0
  333. package/lib/script-build.js +787 -0
  334. package/lib/script-display.d.ts +51 -0
  335. package/lib/script-display.js +383 -0
  336. package/lib/script-icon.d.ts +16 -0
  337. package/lib/script-icon.js +43 -0
  338. package/lib/script-links.d.ts +24 -0
  339. package/lib/script-links.js +204 -0
  340. package/lib/script-source-url.d.ts +8 -0
  341. package/lib/script-source-url.js +214 -0
  342. package/lib/script-world.d.ts +6 -0
  343. package/lib/script-world.js +65 -0
  344. package/lib/sm-dev-protocol.d.ts +251 -0
  345. package/lib/sm-dev-protocol.js +84 -0
  346. package/lib/source-map.d.ts +32 -0
  347. package/lib/source-map.js +177 -0
  348. package/lib/stack-link.d.ts +15 -0
  349. package/lib/stack-link.js +138 -0
  350. package/lib/unsafe-window-use.d.ts +1 -0
  351. package/lib/unsafe-window-use.js +229 -0
  352. package/lib/url-match.d.ts +40 -0
  353. package/lib/url-match.js +734 -0
  354. package/lib/userscript-header.d.ts +33 -0
  355. package/lib/userscript-header.js +248 -0
  356. package/lib/version-compare.d.ts +6 -0
  357. package/lib/version-compare.js +141 -0
  358. package/lib/version-probe.d.ts +17 -0
  359. package/lib/version-probe.js +120 -0
  360. package/package.json +55 -3
  361. package/skills/SourceMonkey-dev/SKILL.md +868 -0
  362. package/skills/SourceMonkey-dev/references/cdp.md +293 -0
  363. package/skills/SourceMonkey-dev/references/commands.md +297 -0
  364. package/skills/SourceMonkey-dev/references/other-managers.md +63 -0
  365. package/skills/SourceMonkey-dev/references/specs.md +46 -0
  366. package/skills/SourceMonkey-friction/SKILL.md +103 -0
  367. package/skills/SourceMonkey-friction/references/template.md +59 -0
@@ -0,0 +1,407 @@
1
+ // The backup export: collections and their scripts, written as the zip
2
+ // format in docs/export-format.md#sourcemonkey. Tampermonkey's zip
3
+ // layout, so Tampermonkey and Violentmonkey can import it, plus a
4
+ // `script_manifest.json` only SourceMonkey reads.
5
+ //
6
+ // Two halves:
7
+ // - `planExportEntries` is pure: rows, values and icon bytes in, zip
8
+ // entries out. Unit-tested on its own.
9
+ // - `runExport` gathers those inputs on the collections page, builds
10
+ // the zip and hands it to Chrome's Save As.
11
+ import { MANIFEST_FILENAME } from '../collection-scan.js';
12
+ import { base64ToBytes } from '../bytes.js';
13
+ import { collectionKind, containerKind, isFileUrl, isHttpUrl, locationFsPath } from '../file-locations.js';
14
+ import { resolveScriptResource } from '../file-loader.js';
15
+ import { iconRefFromHeader } from '../script-icon.js';
16
+ import { fullSource, scriptBasename } from '../script-display.js';
17
+ import { getValue } from '../userscript-header.js';
18
+ import { effectiveDisplayName } from './collection-display.js';
19
+ import { isSmDevCollection } from './location-kinds.js';
20
+ import { isRelativeRef, mirrorPath, relativeRefPath } from './local-copy.js';
21
+ import { dataUrlContent, ensureIconForUrl } from './site-icon.js';
22
+ import { collectionUsesLocalFiles, getCollectionWithScripts, getCollections, isCollectionEnabled, isScriptEnabled, } from './script-store.js';
23
+ import { readScriptValues } from './worker-client.js';
24
+ import { buildZip } from './zip-writer.js';
25
+ export const EXPORT_FORMAT = 'sourcemonkey-export';
26
+ export const EXPORT_FORMAT_VERSION = 1;
27
+ // ---------- Pure helpers ----------
28
+ // A GM value in the type-prefixed string form Tampermonkey and
29
+ // Violentmonkey both store (docs/export-format.md#gm-value-encoding).
30
+ // `undefined` has no entry: Violentmonkey would read `u` as a delete.
31
+ export function encodeExportValue(value) {
32
+ switch (typeof value) {
33
+ case 'string': return `s${value}`;
34
+ case 'number': return `n${value}`;
35
+ case 'boolean': return `b${value}`;
36
+ case 'undefined': return undefined;
37
+ default: return `o${JSON.stringify(value)}`;
38
+ }
39
+ }
40
+ // Characters Tampermonkey or Violentmonkey would replace in a file
41
+ // name, plus control characters.
42
+ const UNSAFE_NAME_CHARS = /[\\/:*?"<>|$\u0000-\u001f]/g;
43
+ // Longest name kept, in characters, before ` (2)` and the suffix: room
44
+ // to spare under the 255-byte limit most file systems put on a name.
45
+ const MAX_NAME_CHARS = 100;
46
+ // `text` made safe as one file or folder name: characters TM or VM
47
+ // would replace turned into `-`, trimmed, leading dots dropped, and cut
48
+ // to `MAX_NAME_CHARS`. `fallback` when nothing is left.
49
+ export function safeFileName(text, fallback) {
50
+ const cleaned = text.replace(UNSAFE_NAME_CHARS, '-').trim().replace(/^\.+/, '');
51
+ return [...cleaned].slice(0, MAX_NAME_CHARS).join('').trim() || fallback;
52
+ }
53
+ // A file name as a manifest entry. An entry is read as a URL, where `#`
54
+ // would start a fragment and `%` an escape, so those two are encoded.
55
+ // Everything else stays readable: the URL parser encodes spaces and
56
+ // non-ASCII itself, and `?` never reaches a name (`exportScriptName`).
57
+ export function manifestEntryPath(fileName) {
58
+ return fileName.replace(/%/g, '%25').replace(/#/g, '%23');
59
+ }
60
+ // Where a script loads from, for the manifest: a URL, or an absolute
61
+ // file path for a local one. `null` for a location that is neither.
62
+ function scriptLocation(script) {
63
+ if (isHttpUrl(script.fileUrl))
64
+ return script.fileUrl;
65
+ if (isFileUrl(script.fileUrl))
66
+ return locationFsPath(script.fileUrl);
67
+ return null;
68
+ }
69
+ function manifestCollection(collection) {
70
+ const kind = collectionKind(collection.path);
71
+ const out = {
72
+ uuid: collection.id,
73
+ name: effectiveDisplayName(collection),
74
+ kind,
75
+ location: kind === 'scripts' ? null : collection.path,
76
+ enabled: isCollectionEnabled(collection),
77
+ local_files: collectionUsesLocalFiles(collection),
78
+ };
79
+ const builtin = containerKind(collection.path);
80
+ if (builtin)
81
+ out.builtin = builtin;
82
+ return out;
83
+ }
84
+ // Where a dependency or icon file goes, relative to its script's folder:
85
+ // - A relative ref (`lib/util.js`) at that path, where it resolves
86
+ // from the unzipped script. One that climbs out of the script's
87
+ // folder (`../x.js`) has no such place.
88
+ // - Anything else at `<host>/<path>` of its URL (a `file://` URL at
89
+ // its path), where the local-file overrides look.
90
+ export function exportFilePath(ref, url) {
91
+ return (isRelativeRef(ref) && relativeRefPath(ref)) || mirrorPath(url);
92
+ }
93
+ // The resolved URL of a script's own icon, when it is one we can bundle.
94
+ export function scriptIconUrl(script) {
95
+ const ref = iconRefFromHeader(script.header);
96
+ if (!ref)
97
+ return null;
98
+ const resolved = resolveScriptResource(ref, script.fileUrl);
99
+ // A `data:` icon is in the header itself.
100
+ return resolved && resolved.kind !== 'data' ? resolved.url : null;
101
+ }
102
+ function sameBytes(a, b) {
103
+ if (a.length !== b.length)
104
+ return false;
105
+ for (let i = 0; i < a.length; i++)
106
+ if (a[i] !== b[i])
107
+ return false;
108
+ return true;
109
+ }
110
+ // A collection's folder name: its last path segment for one named by a
111
+ // path.
112
+ function collectionFolderName(collection) {
113
+ const name = effectiveDisplayName(collection);
114
+ const last = name.split('/').filter(Boolean).pop() ?? name;
115
+ return safeFileName(last, 'collection');
116
+ }
117
+ // `base`, or `base (2)`, `base (3)`, …: the first not in `used`, which
118
+ // holds lower-cased names. The one chosen is added.
119
+ function uniqueName(base, used) {
120
+ let name = base;
121
+ for (let n = 2; used.has(name.toLowerCase()); n++)
122
+ name = `${base} (${n})`;
123
+ used.add(name.toLowerCase());
124
+ return name;
125
+ }
126
+ function scriptFiles(script, icons) {
127
+ const encoder = new TextEncoder();
128
+ const files = new Map();
129
+ const add = (ref, url, data) => {
130
+ if (!isHttpUrl(url) && !isFileUrl(url))
131
+ return;
132
+ const path = exportFilePath(ref, url);
133
+ if (path && !files.has(path))
134
+ files.set(path, { data, relative: isRelativeRef(ref) });
135
+ };
136
+ for (const r of script.requires ?? [])
137
+ add(r.ref, r.url, encoder.encode(r.source));
138
+ for (const r of script.resources ?? [])
139
+ add(r.ref, r.url, new Uint8Array(base64ToBytes(r.bytesBase64)));
140
+ const iconRef = iconRefFromHeader(script.header);
141
+ const iconUrl = scriptIconUrl(script);
142
+ const icon = iconUrl ? icons[iconUrl] : undefined;
143
+ if (iconRef && iconUrl && icon)
144
+ add(iconRef, iconUrl, icon);
145
+ return files;
146
+ }
147
+ // Every entry of the export. Each script gets a folder of its own,
148
+ // holding its `.user.js`, `.options.json` and `.storage.json`, and the
149
+ // dependency files only it uses; with more than one collection, the
150
+ // script folders sit in a folder per collection:
151
+ //
152
+ // script_manifest.json
153
+ // cdn.example.com/lib.js shared by scripts, same bytes
154
+ // My scripts/Foo/Foo.user.js
155
+ // My scripts/Foo/lib/util.js a relative ref: always here
156
+ // My scripts/Foo/cdn.example.com/x.js used by Foo only
157
+ //
158
+ // Laid out for the local-file overrides, so the unzipped folder, added
159
+ // as a manifest collection, runs every script on the bundled files.
160
+ // They search the script's own folder first, then the manifest's
161
+ // folder (docs/local-file-overrides.md#where-roots-come-from):
162
+ // - A file at the top serves every script whose own folder doesn't
163
+ // hold another version.
164
+ // - A relative ref resolves straight to a path under the script's
165
+ // folder, where the overrides don't look further, so its file is
166
+ // always there.
167
+ export function planExportEntries(input) {
168
+ const encoder = new TextEncoder();
169
+ const json = (value, pretty = false) => encoder.encode(JSON.stringify(value, null, pretty ? 2 : undefined));
170
+ // Names first: each collection's folder, and each script's.
171
+ const topNames = new Set([MANIFEST_FILENAME]);
172
+ const planned = [];
173
+ for (const { collection, scripts } of input.collections) {
174
+ const collectionFolder = input.collections.length > 1
175
+ ? `${uniqueName(collectionFolderName(collection), topNames)}/`
176
+ : '';
177
+ const scriptNames = collectionFolder ? new Set() : topNames;
178
+ for (const script of scripts) {
179
+ // Every script goes in, with whatever text it has: one that is
180
+ // empty, or never loaded, gets an empty `.user.js`.
181
+ const atName = (script.header && getValue(script.header, 'name')?.trim())
182
+ || scriptBasename(script.relativePath).replace(/\.user\.js$/i, '');
183
+ const name = uniqueName(safeFileName(atName, 'script'), scriptNames);
184
+ planned.push({
185
+ script,
186
+ collection,
187
+ atName,
188
+ folder: `${collectionFolder}${name}/`,
189
+ name,
190
+ files: input.options.files ? scriptFiles(script, input.icons) : new Map(),
191
+ });
192
+ }
193
+ }
194
+ // Which files go at the top: a path that scripts name with the same
195
+ // bytes, the version the most scripts use. Not a relative ref's file,
196
+ // and not one whose first folder would land in a collection's or
197
+ // script's folder.
198
+ const shared = new Map();
199
+ const byPath = new Map();
200
+ for (const p of planned) {
201
+ for (const [path, f] of p.files) {
202
+ if (f.relative || topNames.has(path.split('/')[0].toLowerCase()))
203
+ continue;
204
+ byPath.set(path, [...(byPath.get(path) ?? []), f.data]);
205
+ }
206
+ }
207
+ for (const [path, versions] of byPath) {
208
+ // Each copy is compared with the distinct versions seen so far,
209
+ // usually one, rather than with every other copy: fifty scripts
210
+ // sharing a large library would otherwise compare it 2,500 times.
211
+ const groups = [];
212
+ for (const v of versions) {
213
+ const group = groups.find((g) => sameBytes(g.data, v));
214
+ if (group)
215
+ group.count++;
216
+ else
217
+ groups.push({ data: v, count: 1 });
218
+ }
219
+ const best = groups.reduce((a, b) => (b.count > a.count ? b : a));
220
+ if (best.count > 1)
221
+ shared.set(path, best.data);
222
+ }
223
+ const entries = [];
224
+ const manifestScripts = [];
225
+ let position = 0;
226
+ for (const { script, collection, atName, folder, name, files } of planned) {
227
+ position++;
228
+ const location = scriptLocation(script);
229
+ const modified = script.loadedAt;
230
+ const stem = `${folder}${name}`;
231
+ entries.push({ path: `${stem}.user.js`, data: encoder.encode(fullSource(script)), modified });
232
+ const meta = { name: atName, uuid: script.id, modified };
233
+ if (location && isHttpUrl(location))
234
+ meta.file_url = location;
235
+ entries.push({
236
+ path: `${stem}.options.json`,
237
+ data: json({
238
+ meta,
239
+ // TM and VM have no collections, so a script runs there when it
240
+ // is active here.
241
+ settings: { enabled: isCollectionEnabled(collection) && isScriptEnabled(script), position },
242
+ // Required: VM skips a script without `options`. No `override`,
243
+ // which TM would take as the script's real match rules.
244
+ options: { check_for_updates: meta.file_url !== undefined },
245
+ }),
246
+ modified,
247
+ });
248
+ if (input.options.values) {
249
+ const data = {};
250
+ for (const [key, value] of Object.entries(input.values[script.id] ?? {})) {
251
+ const encoded = encodeExportValue(value);
252
+ if (encoded !== undefined)
253
+ data[key] = encoded;
254
+ }
255
+ if (Object.keys(data).length > 0) {
256
+ entries.push({ path: `${stem}.storage.json`, data: json({ ts: input.now, data }), modified });
257
+ }
258
+ }
259
+ // Its own files, unless the top holds the same bytes. A file can't
260
+ // take one of the script's own entry names.
261
+ const own = new Set([`${name}.user.js`, `${name}.options.json`, `${name}.storage.json`].map((n) => n.toLowerCase()));
262
+ for (const [path, f] of files) {
263
+ const top = shared.get(path);
264
+ if (top && !f.relative && sameBytes(top, f.data))
265
+ continue;
266
+ if (own.has(path.toLowerCase()))
267
+ continue;
268
+ entries.push({ path: `${folder}${path}`, data: f.data });
269
+ }
270
+ manifestScripts.push({
271
+ path: manifestEntryPath(`${stem}.user.js`),
272
+ uuid: script.id,
273
+ collection: collection.id,
274
+ location,
275
+ enabled: isScriptEnabled(script),
276
+ });
277
+ }
278
+ for (const [path, data] of shared)
279
+ entries.push({ path, data });
280
+ const manifest = {
281
+ created_by: 'SourceMonkey',
282
+ format: EXPORT_FORMAT,
283
+ version: EXPORT_FORMAT_VERSION,
284
+ exported_at: input.now,
285
+ collections: input.collections.map(({ collection }) => manifestCollection(collection)),
286
+ scripts: manifestScripts,
287
+ };
288
+ return {
289
+ entries: [{ path: MANIFEST_FILENAME, data: json(manifest, true) }, ...entries],
290
+ scriptCount: position,
291
+ };
292
+ }
293
+ // `sourcemonkey-backup-<iso>.zip`, the timestamp as Tampermonkey writes
294
+ // its own: `toISOString()` with `.` and `:` turned into `-`.
295
+ export function exportFileName(now) {
296
+ return `sourcemonkey-backup-${new Date(now).toISOString().replace(/[.:]/g, '-')}.zip`;
297
+ }
298
+ // Read everything the plan needs. sm-dev's test collections are left
299
+ // out: their scripts belong to a run, and only that run changes them.
300
+ export async function gatherExportInput(scope, options, now) {
301
+ const all = await getCollections();
302
+ const chosen = ('all' in scope ? all : all.filter((c) => c.id === scope.collectionId))
303
+ .filter((c) => !isSmDevCollection(c.path));
304
+ const collections = [];
305
+ for (const c of chosen) {
306
+ const got = await getCollectionWithScripts(c.id);
307
+ if (got)
308
+ collections.push(got);
309
+ }
310
+ const scripts = collections.flatMap((c) => c.scripts);
311
+ const values = options.values ? await readScriptValues(scripts.map((s) => s.id)) : {};
312
+ const icons = {};
313
+ if (options.files) {
314
+ const urls = [...new Set(scripts.map(scriptIconUrl).filter((u) => u !== null))];
315
+ await Promise.all(urls.map(async (url) => {
316
+ const dataUrl = await ensureIconForUrl(url).catch(() => null);
317
+ const content = dataUrl ? dataUrlContent(dataUrl) : null;
318
+ if (content)
319
+ icons[url] = new Uint8Array(base64ToBytes(content.base64));
320
+ }));
321
+ }
322
+ return { collections, values, icons, options, now };
323
+ }
324
+ // Message for a Save As the person cancelled. Not an error to show.
325
+ export const EXPORT_CANCELLED = 'The export was cancelled';
326
+ // Build the zip and save it through Chrome's Save As. Resolves once
327
+ // the file is written.
328
+ export async function runExport(scope, options) {
329
+ const now = Date.now();
330
+ const plan = planExportEntries(await gatherExportInput(scope, options, now));
331
+ const bytes = buildZip(plan.entries, now);
332
+ const path = await saveFile(new Blob([bytes], { type: 'application/zip' }), exportFileName(now));
333
+ return { path, scriptCount: plan.scriptCount };
334
+ }
335
+ // Chrome's reasons a download stopped, in words, for the ones a save
336
+ // to disk realistically hits. Others show Chrome's code.
337
+ const SAVE_FAILURES = {
338
+ FILE_NO_SPACE: 'The disk is full',
339
+ FILE_ACCESS_DENIED: 'Chrome was not allowed to write to that folder',
340
+ FILE_NAME_TOO_LONG: 'The file name is too long',
341
+ FILE_TOO_LARGE: 'The file is too large for that disk',
342
+ FILE_TRANSIENT_ERROR: 'The disk was busy. Try again.',
343
+ FILE_BLOCKED: 'Chrome or the system blocked the file',
344
+ };
345
+ export function saveFailureMessage(reason) {
346
+ return (reason && SAVE_FAILURES[reason]) || `Chrome stopped the save (${reason || 'no reason given'})`;
347
+ }
348
+ // The saved path once download `id` completes, from Chrome's change
349
+ // events. Checked once at the start too, for a download that finished
350
+ // before the listener was added.
351
+ function downloadFinished(id) {
352
+ return new Promise((resolve, reject) => {
353
+ const settle = (item) => {
354
+ if (!item) {
355
+ reject(new Error(EXPORT_CANCELLED));
356
+ return true;
357
+ }
358
+ if (item.state === 'complete') {
359
+ resolve(item.filename);
360
+ return true;
361
+ }
362
+ if (item.state === 'interrupted') {
363
+ reject(new Error(item.error === 'USER_CANCELED' ? EXPORT_CANCELLED : saveFailureMessage(item.error)));
364
+ return true;
365
+ }
366
+ return false;
367
+ };
368
+ const check = () => {
369
+ void chrome.downloads.search({ id }).then(([item]) => {
370
+ if (settle(item))
371
+ chrome.downloads.onChanged.removeListener(onChanged);
372
+ }, (err) => {
373
+ chrome.downloads.onChanged.removeListener(onChanged);
374
+ reject(err);
375
+ });
376
+ };
377
+ const onChanged = (delta) => {
378
+ if (delta.id === id && (delta.state || delta.error))
379
+ check();
380
+ };
381
+ chrome.downloads.onChanged.addListener(onChanged);
382
+ check();
383
+ });
384
+ }
385
+ async function saveFile(blob, fileName) {
386
+ const url = URL.createObjectURL(blob);
387
+ const saveAs = globalThis.smExportSkipSaveAs !== true;
388
+ try {
389
+ let id;
390
+ try {
391
+ id = await chrome.downloads.download({ url, filename: fileName, saveAs, conflictAction: 'uniquify' });
392
+ }
393
+ catch (err) {
394
+ // A Save As closed without choosing a file can reject here, with
395
+ // Chrome's "Download canceled".
396
+ const text = err instanceof Error ? err.message : String(err);
397
+ if (/cancel/i.test(text))
398
+ throw new Error(EXPORT_CANCELLED);
399
+ throw new Error(`Chrome refused to save the file (${text})`);
400
+ }
401
+ // No deadline: the Save As waits on the person.
402
+ return await downloadFinished(id);
403
+ }
404
+ finally {
405
+ URL.revokeObjectURL(url);
406
+ }
407
+ }
@@ -0,0 +1,142 @@
1
+ // The export dialog, opened from the collections page's ⋯ menus: what to
2
+ // include, then Chrome's Save As. It stays open to say how many scripts
3
+ // went in and where the file went. See docs/export.md.
4
+ import { describeError } from './script-store.js';
5
+ import { makeCopyButton } from './copy-button.js';
6
+ import { EXPORT_CANCELLED, runExport } from './export-backup.js';
7
+ let elements = null;
8
+ let dialogInFlight = false;
9
+ function el(tag, className, text) {
10
+ const node = document.createElement(tag);
11
+ if (className)
12
+ node.className = className;
13
+ if (text !== undefined)
14
+ node.textContent = text;
15
+ return node;
16
+ }
17
+ function checkbox(id, label, hint) {
18
+ const wrap = el('div', 'dlg-option');
19
+ const labelEl = el('label', 'dlg-check');
20
+ const input = el('input');
21
+ input.type = 'checkbox';
22
+ input.id = id;
23
+ labelEl.append(input, el('span', undefined, label));
24
+ wrap.append(labelEl, el('div', 'dlg-hint', hint));
25
+ return { wrap, input };
26
+ }
27
+ function buildDialog() {
28
+ const dialog = el('dialog', 'sm-dialog export-dialog');
29
+ const title = el('h2', 'dlg-title');
30
+ dialog.appendChild(title);
31
+ const body = el('div', 'dlg-body');
32
+ body.appendChild(el('p', 'dlg-intro', 'This saves a .zip file that SourceMonkey, Tampermonkey, Violentmonkey, and other compatible managers can import.'));
33
+ const include = el('div', 'dlg-includes');
34
+ const values = checkbox('export-values', 'Stored values', 'Each script’s GM_setValue data.');
35
+ const files = checkbox('export-files', 'Dependency files', 'The @require, @resource and @icon files each script uses.');
36
+ include.append(values.wrap, files.wrap);
37
+ body.append(el('span', 'dlg-label', 'Include'), include);
38
+ const result = el('div', 'dlg-result');
39
+ result.id = 'export-result';
40
+ result.hidden = true;
41
+ body.appendChild(result);
42
+ dialog.appendChild(body);
43
+ const errorRow = el('div', 'dlg-error-row');
44
+ const error = el('p', 'dlg-error');
45
+ error.setAttribute('role', 'alert');
46
+ errorRow.appendChild(error);
47
+ dialog.appendChild(errorRow);
48
+ const actions = el('div', 'dlg-actions');
49
+ const cancel = el('button', undefined, 'Cancel');
50
+ cancel.type = 'button';
51
+ const ok = el('button', 'primary', 'Export…');
52
+ ok.type = 'button';
53
+ ok.id = 'export-confirm-btn';
54
+ actions.append(cancel, ok);
55
+ dialog.appendChild(actions);
56
+ document.body.appendChild(dialog);
57
+ return { dialog, title, values: values.input, files: files.input, result, error, cancel, ok };
58
+ }
59
+ // Open the dialog. Resolves when it closes.
60
+ export function openExportDialog(request) {
61
+ if (!elements)
62
+ elements = buildDialog();
63
+ const ui = elements;
64
+ if (dialogInFlight)
65
+ return Promise.resolve();
66
+ dialogInFlight = true;
67
+ return new Promise((resolve) => {
68
+ let busy = false;
69
+ let done = false;
70
+ const abort = new AbortController();
71
+ const finish = () => {
72
+ abort.abort();
73
+ dialogInFlight = false;
74
+ if (ui.dialog.open)
75
+ ui.dialog.close();
76
+ resolve();
77
+ };
78
+ ui.title.textContent = 'all' in request.scope
79
+ ? 'Export all collections'
80
+ : `Export collection: ${request.collectionName ?? ''}`;
81
+ ui.values.checked = true;
82
+ ui.files.checked = false;
83
+ ui.values.disabled = false;
84
+ ui.files.disabled = false;
85
+ ui.result.hidden = true;
86
+ ui.result.replaceChildren();
87
+ ui.error.textContent = '';
88
+ ui.cancel.hidden = false;
89
+ ui.cancel.disabled = false;
90
+ ui.ok.disabled = false;
91
+ ui.ok.textContent = 'Export…';
92
+ const setBusy = (on) => {
93
+ busy = on;
94
+ ui.values.disabled = on;
95
+ ui.files.disabled = on;
96
+ ui.cancel.disabled = on;
97
+ ui.ok.disabled = on;
98
+ ui.ok.textContent = on ? 'Exporting…' : 'Export…';
99
+ };
100
+ const onOk = async () => {
101
+ if (done) {
102
+ finish();
103
+ return;
104
+ }
105
+ ui.error.textContent = '';
106
+ setBusy(true);
107
+ try {
108
+ const out = await runExport(request.scope, { values: ui.values.checked, files: ui.files.checked });
109
+ done = true;
110
+ // The saved path in code font, with a button that copies it.
111
+ const saved = el('p', 'dlg-saved');
112
+ const pathEl = el('code', 'dlg-saved-path', out.path);
113
+ pathEl.id = 'export-saved-path';
114
+ saved.append(`Exported ${out.scriptCount} ${out.scriptCount === 1 ? 'script' : 'scripts'} to `, pathEl, makeCopyButton({ label: 'Copy path to clipboard', text: () => out.path }));
115
+ ui.result.replaceChildren(saved);
116
+ ui.result.hidden = false;
117
+ setBusy(false);
118
+ ui.values.disabled = true;
119
+ ui.files.disabled = true;
120
+ ui.cancel.hidden = true;
121
+ ui.ok.textContent = 'Close';
122
+ }
123
+ catch (err) {
124
+ setBusy(false);
125
+ const message = describeError(err);
126
+ if (message !== EXPORT_CANCELLED)
127
+ ui.error.textContent = `Export failed: ${message}`;
128
+ }
129
+ };
130
+ ui.ok.addEventListener('click', () => void onOk(), { signal: abort.signal });
131
+ ui.cancel.addEventListener('click', finish, { signal: abort.signal });
132
+ // Esc while the zip is being saved would leave the save running
133
+ // behind a closed dialog.
134
+ ui.dialog.addEventListener('cancel', (ev) => {
135
+ ev.preventDefault();
136
+ if (!busy)
137
+ finish();
138
+ }, { signal: abort.signal });
139
+ ui.dialog.showModal();
140
+ ui.ok.focus();
141
+ });
142
+ }
@@ -0,0 +1,74 @@
1
+ // The "Allow access to file URLs" probe and the banner that explains a
2
+ // failed `file://` read, shared by every extension page that can hit
3
+ // one (collections page, script viewer / install preview).
4
+ //
5
+ // The toggle lives on `chrome://extensions` and is invisible from a
6
+ // fetch's point of view: with it off, reading a local file fails with
7
+ // the same opaque error a missing file produces (see
8
+ // [docs/file-access.md]). So a page that couldn't read a local file
9
+ // asks here whether the toggle is the reason, and shows a banner that
10
+ // names it and links to the page that fixes it.
11
+ //
12
+ // Page-only — `chrome.extension` is not exposed to the service worker.
13
+ // Whether the per-extension file-access toggle is on. Callback-style
14
+ // API, so it gets a promise wrapper.
15
+ //
16
+ // Never rejects. Callers use this to decide whether to *explain* a
17
+ // failure, and an explanation is not worth taking a page down for — a
18
+ // probe that throws (API renamed, unexpected context) reports "allowed"
19
+ // so the banner stays away and everything else renders as usual.
20
+ export function isFileSchemeAccessAllowed() {
21
+ return new Promise((resolve) => {
22
+ try {
23
+ chrome.extension.isAllowedFileSchemeAccess((allowed) => resolve(allowed));
24
+ }
25
+ catch {
26
+ resolve(true);
27
+ }
28
+ });
29
+ }
30
+ // The banner itself. `retry` completes "…turn it on, <retry>:" — what
31
+ // the user has to do to make the read actually happen again, which
32
+ // differs by caller: a page that reads on load wants a reload, while
33
+ // the viewer renders a stored row and doesn't re-read until Refresh.
34
+ export function renderFileAccessBanner(retry = 'then reload this page') {
35
+ const div = document.createElement('div');
36
+ div.className = 'banner';
37
+ const heading = document.createElement('strong');
38
+ heading.textContent = 'File access is disabled.';
39
+ div.appendChild(heading);
40
+ const explain = document.createElement('p');
41
+ explain.textContent =
42
+ 'SourceMonkey can\'t read local files until you enable ' +
43
+ '"Allow access to file URLs". Open the extension\'s details page, ' +
44
+ `turn it on, ${retry}:`;
45
+ div.appendChild(explain);
46
+ div.appendChild(extensionDetailsLink());
47
+ return div;
48
+ }
49
+ // chrome:// URLs are blocked from <a href> navigation, so we use a
50
+ // click handler that calls chrome.tabs.create — that path is allowed
51
+ // from extension pages. Rendered as an <a> so it looks/behaves like
52
+ // any other link.
53
+ //
54
+ // Exported because the collections page's "Allow User Scripts" banner
55
+ // points at the same details page. With `text`, it reads as a plain link
56
+ // inside a sentence instead of showing the address.
57
+ export function extensionDetailsLink(text) {
58
+ const url = `chrome://extensions/?id=${chrome.runtime.id}`;
59
+ const a = document.createElement('a');
60
+ a.href = url;
61
+ a.textContent = text ?? url;
62
+ a.className = text ? 'chrome-text-link' : 'chrome-link';
63
+ a.addEventListener('click', (event) => {
64
+ event.preventDefault();
65
+ // Caught, not dropped: an unhandled rejection in an extension page
66
+ // is filed under chrome://extensions/?errors, which is the panel
67
+ // this banner is trying to send the user to — arriving there to
68
+ // find SourceMonkey's own error would be its own kind of confusing.
69
+ // A `chrome://` target has no realistic failure, so there is
70
+ // nothing to say beyond leaving the link inert.
71
+ chrome.tabs.create({ url }).catch(() => { });
72
+ });
73
+ return a;
74
+ }