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,375 @@
1
+ // In-memory index of every injectable userscript's matching info so the
2
+ // background service worker can find matching scripts for a URL without
3
+ // touching chrome.storage.local on every navigation. "Injectable" means
4
+ // `status: 'ok'` (see toEntry).
5
+ //
6
+ // Built once on first access (ensureLoaded()), then kept fresh by a
7
+ // chrome.storage.local.onChanged listener. Storage I/O cost scales with
8
+ // the user's script count at SW startup; per-navigation cost is just an
9
+ // in-memory loop over the cache.
10
+ //
11
+ // Enabled state: a script is considered for matching only if BOTH its
12
+ // own `enabled` flag and its parent collection's `enabled` flag are
13
+ // true. We cache every injectable script (regardless of enabled) and
14
+ // separately track the set of disabled collections, so toggling a
15
+ // collection flips every script under it in O(1) without rewalking
16
+ // storage.
17
+ import { isFileUrl, scratchRunOf } from './directories.js';
18
+ import { compareScriptNames, scriptDisplayName } from '../script-display.js';
19
+ import { scanStorageRows } from './storage-scan.js';
20
+ import { sharedRows } from './row-cache.js';
21
+ import { ensureCollectionKeysMigrated, isScriptEnabled, isCollectionEnabled, sameStoredValue, withoutAnnotations, } from './script-store.js';
22
+ import { urlMatchesScript } from '../url-match.js';
23
+ import { resolveScriptWorld } from '../script-world.js';
24
+ import { grantedSubsystems } from '../gm-api-catalog.js';
25
+ import { isManualScript } from '../script-build.js';
26
+ import { hasTag } from '../userscript-header.js';
27
+ const SCRIPT_PREFIX = 'script:';
28
+ const COLLECTION_PREFIX = 'collection:';
29
+ let cache = new Map();
30
+ // The match rules of `invalid` scripts, which `cache` leaves out. Read
31
+ // only by `findAllMatchingIds`, so the popup can list a script that
32
+ // targets the page but failed to load. Never injected or registered.
33
+ let invalidMatching = new Map();
34
+ let disabledCollections = new Set();
35
+ let loaded = null;
36
+ // While loadAll() is awaiting its full `get(null)` snapshot, any
37
+ // storage change that fires would mutate the about-to-be-replaced
38
+ // `cache`, and the subsequent `cache = nextCache` assignment would
39
+ // discard it — a lost update until the next change to that key. During
40
+ // the load window we buffer raw changes here instead of applying them,
41
+ // then replay them onto the freshly-built cache. Null when not loading.
42
+ let pendingDuringLoad = null;
43
+ // First call kicks off a single full read; later callers await the same
44
+ // promise. After it resolves, the cache is kept fresh by the onChanged
45
+ // listener installed by installStorageListener().
46
+ export function ensureLoaded() {
47
+ if (!loaded)
48
+ loaded = loadAll();
49
+ return loaded;
50
+ }
51
+ async function loadAll() {
52
+ pendingDuringLoad = [];
53
+ // Before the read, not after: a profile still on the pre-rename key
54
+ // spelling would otherwise load an empty cache and inject nothing.
55
+ await ensureCollectionKeysMigrated();
56
+ const isRow = (k) => k.startsWith(SCRIPT_PREFIX) || k.startsWith(COLLECTION_PREFIX);
57
+ // From the worker's row cache when it's on (frozen rows, read only).
58
+ const all = (await sharedRows(isRow)) ?? await scanStorageRows('match index', isRow);
59
+ const nextCache = new Map();
60
+ const nextInvalid = new Map();
61
+ const nextDisabled = new Set();
62
+ for (const [key, value] of Object.entries(all)) {
63
+ if (key.startsWith(SCRIPT_PREFIX)) {
64
+ const script = value;
65
+ const entry = toEntry(script);
66
+ if (entry)
67
+ nextCache.set(entry.id, entry);
68
+ else if (script.status === 'invalid' && script.matching)
69
+ nextInvalid.set(script.id, script.matching);
70
+ }
71
+ else if (key.startsWith(COLLECTION_PREFIX)) {
72
+ const collection = value;
73
+ if (!isCollectionEnabled(collection))
74
+ nextDisabled.add(collection.id);
75
+ }
76
+ }
77
+ cache = nextCache;
78
+ invalidMatching = nextInvalid;
79
+ disabledCollections = nextDisabled;
80
+ // Replay any changes that landed during the await on top of the
81
+ // fresh snapshot, then stop buffering. A change for a key already in
82
+ // the snapshot just re-applies the same (or newer) value; one for a
83
+ // key the snapshot missed is now captured.
84
+ const buffered = pendingDuringLoad;
85
+ pendingDuringLoad = null;
86
+ for (const changes of buffered)
87
+ applyStorageChanges(changes);
88
+ }
89
+ function toEntry(script) {
90
+ // Only `ok` scripts participate. `invalid` rows never do — their
91
+ // header may be unparsed or the file failed validation.
92
+ if (script.status !== 'ok')
93
+ return null;
94
+ const name = scriptDisplayName(script);
95
+ return {
96
+ id: script.id,
97
+ name,
98
+ matching: script.matching,
99
+ collectionId: script.collectionId,
100
+ world: resolveScriptWorld(script),
101
+ subsystems: grantedSubsystems(script.header),
102
+ enabled: isScriptEnabled(script),
103
+ local: isFileUrl(script.fileUrl),
104
+ ...smDevRunField(script.fileUrl),
105
+ ...(isManualScript(script) ? { manual: true } : {}),
106
+ ...(script.header && hasTag(script.header, 'noframes') ? { noframes: true } : {}),
107
+ };
108
+ }
109
+ function smDevRunField(fileUrl) {
110
+ const run = scratchRunOf(fileUrl);
111
+ return run === null ? {} : { smDevRun: run };
112
+ }
113
+ // Install the storage listener exactly once. Safe to call repeatedly.
114
+ let listenerInstalled = false;
115
+ export function installStorageListener() {
116
+ if (listenerInstalled)
117
+ return;
118
+ listenerInstalled = true;
119
+ chrome.storage.onChanged.addListener((changes, areaName) => {
120
+ if (areaName !== 'local')
121
+ return;
122
+ // If a full load is mid-flight, buffer this change for replay after
123
+ // the snapshot lands rather than mutating the soon-to-be-replaced
124
+ // cache (which would lose this update). See loadAll().
125
+ if (pendingDuringLoad) {
126
+ pendingDuringLoad.push(changes);
127
+ return;
128
+ }
129
+ applyStorageChanges(changes);
130
+ });
131
+ }
132
+ // Apply one storage.onChanged delta to the in-memory cache. Shared by
133
+ // the live listener and loadAll()'s post-snapshot replay.
134
+ function applyStorageChanges(changes) {
135
+ // Which rows moved, not just "something moved". The injector syncs
136
+ // incrementally off this, so a listener that only said "changed"
137
+ // would force it back to rebuilding every registration.
138
+ const scriptIds = [];
139
+ const collectionIds = [];
140
+ for (const [key, change] of Object.entries(changes)) {
141
+ if (key.startsWith(SCRIPT_PREFIX)) {
142
+ // A write that only moved an annotation (`versionProbes`, or the
143
+ // parse check's verdict) is not a change to anything downstream
144
+ // of this listener, and saying otherwise is expensive: each one
145
+ // schedules a registration sync. A "Refresh All" probes each
146
+ // script in turn and writes each result as its own row, so
147
+ // treating those as cache changes turned one refresh into a
148
+ // re-registration per probed script — measured at 23 syncs
149
+ // averaging 356ms for a 177-script profile, most of them landing
150
+ // after the refresh had already finished. A verdict cost a second
151
+ // sync after every edit.
152
+ //
153
+ // Safe for a probe because it is a display annotation and nothing
154
+ // else: `toEntry` doesn't read it, and neither does `injector.ts`
155
+ // (registration specs), `tab-status.ts`, or `background.ts`. The
156
+ // pages that DO render it — the viewer and the collections page —
157
+ // repaint from their own `chrome.storage.local.onChanged`
158
+ // listeners, not from `onCacheChanged`, so the badge still
159
+ // updates the moment a probe lands.
160
+ //
161
+ // Safe for a passing verdict for another reason: the sync reads
162
+ // it (its gate checks rows without one), but it lands just after
163
+ // the content change it checked, whose own write started a sync
164
+ // then, and the registration built from the row is the same
165
+ // either way. A failing one changes `status`, which is reported.
166
+ if (onlyAnnotationsChanged(change.oldValue, change.newValue)) {
167
+ continue;
168
+ }
169
+ const id = key.slice(SCRIPT_PREFIX.length);
170
+ scriptIds.push(id);
171
+ invalidMatching.delete(id);
172
+ if (change.newValue === undefined) {
173
+ cache.delete(id);
174
+ continue;
175
+ }
176
+ const script = change.newValue;
177
+ const entry = toEntry(script);
178
+ if (entry)
179
+ cache.set(id, entry);
180
+ else
181
+ cache.delete(id);
182
+ if (script.status === 'invalid' && script.matching)
183
+ invalidMatching.set(id, script.matching);
184
+ }
185
+ else if (key.startsWith(COLLECTION_PREFIX)) {
186
+ const id = key.slice(COLLECTION_PREFIX.length);
187
+ const next = change.newValue;
188
+ // A collection row is rewritten on every scan (`lastScannedAt`
189
+ // and friends), so reporting every write would re-register every
190
+ // script under it on every refresh. Only what moves anything
191
+ // downstream is reported: the enabled flag, and the row going
192
+ // away.
193
+ //
194
+ // Deliberately NOT its arrival. A collection appears together
195
+ // with the scripts under it, which are reported in their own
196
+ // right, and the injector fetches a collection row it hasn't seen
197
+ // when a script names one — so saying so here would buy nothing
198
+ // and cost an extra sync per collection added.
199
+ const wasDisabled = disabledCollections.has(id);
200
+ if (next === undefined) {
201
+ // Collection was removed entirely — its scripts will be deleted
202
+ // via separate script:* changes; just drop our tracking row.
203
+ disabledCollections.delete(id);
204
+ collectionIds.push(id);
205
+ }
206
+ else if (!isCollectionEnabled(next)) {
207
+ disabledCollections.add(id);
208
+ if (!wasDisabled)
209
+ collectionIds.push(id);
210
+ }
211
+ else {
212
+ disabledCollections.delete(id);
213
+ if (wasDisabled)
214
+ collectionIds.push(id);
215
+ }
216
+ }
217
+ }
218
+ if (scriptIds.length > 0 || collectionIds.length > 0) {
219
+ notifyChange({ scriptIds, collectionIds });
220
+ }
221
+ }
222
+ // Whether a `script:*` write moved nothing but its annotations.
223
+ //
224
+ // Deliberately a diff of the two stored rows rather than of the derived
225
+ // cache entries. The entry holds only what MATCHING needs, but
226
+ // `notifyChange` also drives re-registration, which reads the whole row
227
+ // — body, `@require` bodies, grants, world, run-at. Comparing entries
228
+ // would call a body-only edit "no change" and leave the old code
229
+ // registered, which is a correctness bug, not a missed optimisation.
230
+ // So the rule is inverted: assume a change matters unless it is exactly
231
+ // the fields proven not to.
232
+ //
233
+ // Exported for unit tests.
234
+ export function onlyAnnotationsChanged(before, after) {
235
+ // A creation or a deletion is never "just an annotation".
236
+ if (!before || !after)
237
+ return false;
238
+ return sameStoredValue(withoutAnnotations(before), withoutAnnotations(after));
239
+ }
240
+ const changeListeners = [];
241
+ export function onCacheChanged(fn) {
242
+ changeListeners.push(fn);
243
+ }
244
+ function notifyChange(change) {
245
+ for (const fn of changeListeners) {
246
+ try {
247
+ fn(change);
248
+ }
249
+ catch { /* swallow listener errors */ }
250
+ }
251
+ }
252
+ export function findMatchesForUrl(url, kind = 'load') {
253
+ const out = [];
254
+ for (const entry of cache.values()) {
255
+ if (!entry.enabled)
256
+ continue;
257
+ if (disabledCollections.has(entry.collectionId))
258
+ continue;
259
+ if (kind === 'load' && entry.manual)
260
+ continue;
261
+ if (kind === 'manual' && !entry.manual)
262
+ continue;
263
+ if (urlMatchesScript(url, entry.matching)) {
264
+ out.push({
265
+ id: entry.id,
266
+ name: entry.name,
267
+ matching: entry.matching,
268
+ collectionId: entry.collectionId,
269
+ world: entry.world,
270
+ ...(entry.smDevRun !== undefined ? { smDevRun: entry.smDevRun } : {}),
271
+ ...(entry.manual ? { manual: true } : {}),
272
+ ...(entry.noframes ? { noframes: true } : {}),
273
+ });
274
+ }
275
+ }
276
+ // Stable display order for the tooltip.
277
+ out.sort((a, b) => compareScriptNames(a.name, b.name));
278
+ return out;
279
+ }
280
+ // What the index knows of script `id` (any `ok` script, enabled or
281
+ // not), or undefined for one it doesn't hold.
282
+ export function indexedScript(id) {
283
+ const entry = cache.get(id);
284
+ return entry && {
285
+ world: entry.world,
286
+ ...(entry.noframes ? { noframes: true } : {}),
287
+ ...(entry.manual ? { manual: true } : {}),
288
+ };
289
+ }
290
+ // Whether `id` is a manual script (`@run-at context-menu`). False for
291
+ // an id the index doesn't hold.
292
+ export function isManualScriptId(id) {
293
+ return cache.get(id)?.manual === true;
294
+ }
295
+ // Every script whose rules match `url`, enabled or not — the popup
296
+ // lists the disabled ones too, so they can be turned on from there.
297
+ // `invalid` ones as well: one that targets the page but failed to load
298
+ // is listed as such. Ids only: the popup reads the rows it needs from
299
+ // storage.
300
+ export function findAllMatchingIds(url) {
301
+ const out = [];
302
+ for (const entry of cache.values()) {
303
+ if (urlMatchesScript(url, entry.matching))
304
+ out.push(entry.id);
305
+ }
306
+ for (const [id, matching] of invalidMatching) {
307
+ if (urlMatchesScript(url, matching))
308
+ out.push(id);
309
+ }
310
+ return out;
311
+ }
312
+ // Narrow a set of script ids to the ones loaded from `file://`.
313
+ //
314
+ // Keyed on each script's own `fileUrl`, not on its collection's
315
+ // location: a container collection holds individually-installed scripts
316
+ // of both kinds, so the collection is the wrong unit for this question.
317
+ // (`isLocalCollection` in refresh-targets.ts answers the collection-level
318
+ // version, and has to special-case containers for exactly this reason.)
319
+ //
320
+ // Ids the cache doesn't hold — a script that isn't injectable, or one
321
+ // deleted since the tab last matched — are dropped rather than assumed
322
+ // local: there is nothing to refresh either way.
323
+ export function localScriptIds(ids) {
324
+ return ids.filter((id) => cache.get(id)?.local === true);
325
+ }
326
+ // Does the profile hold ANY injectable local script?
327
+ //
328
+ // The cheap gate in front of the focus-triggered refresh: it runs on
329
+ // every tab switch and window focus change, and a profile with only
330
+ // http collections has nothing for it to do. Answering from the
331
+ // in-memory cache keeps that case off `chrome.storage.session`.
332
+ export function hasLocalScripts() {
333
+ for (const entry of cache.values())
334
+ if (entry.local)
335
+ return true;
336
+ return false;
337
+ }
338
+ export function cacheSize() {
339
+ return cache.size;
340
+ }
341
+ // The execution world a script would be injected into, or null when the
342
+ // id isn't in the injectable cache at all.
343
+ //
344
+ // Used by the SW to stamp every push it sends into a tab with the world
345
+ // the receiving script lives in. `wrapper-bridge.js` re-posts pushes
346
+ // into MAIN world, where the page can read them, so it must relay only
347
+ // what is addressed to a MAIN-world script — otherwise an isolated
348
+ // script's stored values and XHR response bodies would become
349
+ // page-visible on any page that happens to also run a MAIN-world
350
+ // script. Callers treat `null` as "not MAIN" (fail closed).
351
+ export function scriptWorld(id) {
352
+ return cache.get(id)?.world ?? null;
353
+ }
354
+ // True iff the script's `@grant` lines reach one of the given
355
+ // subsystems. The second half of the bridge gate: being a MAIN-world
356
+ // script on the page only makes the APIs it granted page-reachable, so
357
+ // a request through any other API is refused even for that script.
358
+ // Unknown ids fail closed.
359
+ export function scriptGrantsSubsystem(id, subsystems) {
360
+ const granted = cache.get(id)?.subsystems;
361
+ if (!granted)
362
+ return false;
363
+ return subsystems.some((s) => granted.has(s));
364
+ }
365
+ // Test/debug: dump the current cache contents. Lets e2e tests
366
+ // distinguish "the script never landed in the cache" from "the URL
367
+ // pattern doesn't match" when a badge classification looks wrong.
368
+ export function debugDumpCache() {
369
+ return {
370
+ entries: [...cache.values()].map((e) => ({
371
+ id: e.id, name: e.name, enabled: e.enabled, collectionId: e.collectionId, matching: e.matching,
372
+ })),
373
+ disabledCollections: [...disabledCollections],
374
+ };
375
+ }
@@ -0,0 +1,66 @@
1
+ // Explaining a script that matched a page but never started there.
2
+ //
3
+ // The badge shows such a script as MISSING. Once the page has loaded,
4
+ // the service worker also writes a Log warning for it, built here from
5
+ // what the document's other wrapper reports said (`reportedWorlds`,
6
+ // `opaqueOrigin` on TabMatchState). The main case it names:
7
+ // - A CSP `sandbox` header without `allow-scripts` turns off script
8
+ // in the page's own world, so Chrome silently skips MAIN-world
9
+ // scripts there, while USER_SCRIPT-world ones still run. The
10
+ // bridge then has them run isolated (`runBlockedMainScripts` in
11
+ // background.ts), so this hint matters only when that didn't run.
12
+ // - Such a page gets an opaque origin, which any wrapper that did run
13
+ // there reports.
14
+ // See docs/matching.md#scripts-that-never-start.
15
+ import { skippedOnDocument } from './tab-status.js';
16
+ import { INJECT_INTO_CONTENT_FIX, SANDBOX_BLOCKS_PAGE_CONTEXT } from '../sandboxed-page.js';
17
+ // The scripts expected on this document from its first load that never
18
+ // reported, and aren't already explained by the badge (REJECTED, NOT
19
+ // LOADED). See `loadMatchIds` for why only the first load's.
20
+ export function findMissingScripts(state, rejected = new Set()) {
21
+ // Nothing was injected, so no script on it is a mystery to explain.
22
+ if (state.scriptsSkipped || state.loadedWithoutUserscripts)
23
+ return [];
24
+ const atLoad = state.loadMatchIds ? new Set(state.loadMatchIds) : null;
25
+ return state.matches.filter((m) => (!atLoad || atLoad.has(m.id)) && !state.actual[m.id] && !rejected.has(m.id) && !skippedOnDocument(state, m.id));
26
+ }
27
+ // Whether the document is sandboxed, going by the wrappers' reports.
28
+ // Only on http(s): a file:// page has an opaque origin too.
29
+ function sandboxed(state) {
30
+ return state.opaqueOrigin === true && /^https?:/i.test(state.url);
31
+ }
32
+ function reportedFrom(state, world) {
33
+ return (state.reportedWorlds ?? []).includes(world);
34
+ }
35
+ // The Log warning for one missing script.
36
+ export function describeMissingScript(script, state) {
37
+ const base = `Matched ${state.url} but never started there`;
38
+ let hint;
39
+ if (script.world === 'MAIN') {
40
+ if (reportedFrom(state, 'MAIN')) {
41
+ hint = 'Other scripts in the page\'s own context did start here, so the page isn\'t blocking that context.';
42
+ }
43
+ else if (sandboxed(state)) {
44
+ hint = 'This page is sandboxed, usually by a sandbox directive in its Content-Security-Policy header. ' +
45
+ `${SANDBOX_BLOCKS_PAGE_CONTEXT}. ${INJECT_INTO_CONTENT_FIX}.`;
46
+ }
47
+ else if (!/^https?:/i.test(state.url)) {
48
+ // No response headers to point at.
49
+ return base;
50
+ }
51
+ else {
52
+ hint = 'The page may be blocking scripts in its own context. Check its Content-Security-Policy ' +
53
+ `header for a sandbox directive without allow-scripts. If it has one, ${lowerFirst(INJECT_INTO_CONTENT_FIX)}.`;
54
+ }
55
+ }
56
+ else if (reportedFrom(state, 'USER_SCRIPT')) {
57
+ hint = 'Other scripts in the isolated context did start here.';
58
+ }
59
+ else {
60
+ return base;
61
+ }
62
+ return `${base}. ${hint}`;
63
+ }
64
+ function lowerFirst(text) {
65
+ return text.charAt(0).toLowerCase() + text.slice(1);
66
+ }
@@ -0,0 +1,109 @@
1
+ // The ⋯ button and the menu it opens, shared by the toolbar popup and
2
+ // the collections page. One menu is open at a time; a click elsewhere
3
+ // or Escape closes it. Each page styles its own `button.more-menu`; the
4
+ // menu's look is `.menu` in styles.css.
5
+ let openMenuEl = null;
6
+ let openMenuBtn = null;
7
+ let listening = false;
8
+ export function moreMenuButton(label) {
9
+ const b = document.createElement('button');
10
+ b.type = 'button';
11
+ b.className = 'more-menu';
12
+ b.setAttribute('aria-label', label);
13
+ b.setAttribute('aria-haspopup', 'menu');
14
+ b.setAttribute('aria-expanded', 'false');
15
+ b.textContent = '⋯';
16
+ return b;
17
+ }
18
+ // Open `items` under `btn`, or close the menu when it is already open
19
+ // there.
20
+ export function openMenu(btn, items) {
21
+ listen();
22
+ const wasOpen = openMenuBtn === btn;
23
+ closeMenu();
24
+ if (wasOpen)
25
+ return;
26
+ const menu = document.createElement('div');
27
+ menu.className = 'menu';
28
+ menu.setAttribute('role', 'menu');
29
+ for (const item of items) {
30
+ if (item.hidden)
31
+ continue;
32
+ if (item.separator) {
33
+ menu.append(document.createElement('hr'));
34
+ continue;
35
+ }
36
+ const b = document.createElement('button');
37
+ b.type = 'button';
38
+ b.setAttribute('role', 'menuitem');
39
+ b.textContent = item.label ?? '';
40
+ b.disabled = item.disabled === true;
41
+ if (item.title)
42
+ b.title = item.title;
43
+ if (item.wire) {
44
+ item.wire(b, closeMenu);
45
+ }
46
+ else {
47
+ b.addEventListener('click', () => {
48
+ closeMenu();
49
+ if (!item.run)
50
+ return;
51
+ // Called inside the click, so the action keeps its user gesture.
52
+ try {
53
+ Promise.resolve(item.run()).catch(menuActionFailed);
54
+ }
55
+ catch (err) {
56
+ menuActionFailed(err);
57
+ }
58
+ });
59
+ }
60
+ menu.append(b);
61
+ }
62
+ document.body.append(menu);
63
+ // Under the button, right-aligned to it; above it when there's no
64
+ // room below.
65
+ const r = btn.getBoundingClientRect();
66
+ const below = r.bottom + 2;
67
+ const top = below + menu.offsetHeight > document.documentElement.clientHeight
68
+ ? Math.max(0, r.top - menu.offsetHeight - 2) : below;
69
+ menu.style.top = `${top}px`;
70
+ menu.style.right = `${document.documentElement.clientWidth - r.right}px`;
71
+ btn.setAttribute('aria-expanded', 'true');
72
+ openMenuEl = menu;
73
+ openMenuBtn = btn;
74
+ menu.querySelector('button')?.focus();
75
+ }
76
+ // Close the menu when the button it hangs from has left the page (a
77
+ // repaint replaced it), rather than leave it floating where it was.
78
+ export function closeDetachedMenu() {
79
+ if (openMenuBtn && !openMenuBtn.isConnected)
80
+ closeMenu();
81
+ }
82
+ export function closeMenu() {
83
+ openMenuEl?.remove();
84
+ openMenuBtn?.setAttribute('aria-expanded', 'false');
85
+ openMenuEl = null;
86
+ openMenuBtn = null;
87
+ }
88
+ function menuActionFailed(err) {
89
+ console.info('[SourceMonkey] menu action failed:', err);
90
+ }
91
+ function listen() {
92
+ if (listening)
93
+ return;
94
+ listening = true;
95
+ document.addEventListener('click', (ev) => {
96
+ const t = ev.target;
97
+ if (!openMenuEl || t?.closest('.menu') || t?.closest('button.more-menu') === openMenuBtn)
98
+ return;
99
+ closeMenu();
100
+ });
101
+ document.addEventListener('keydown', (ev) => {
102
+ if (ev.key === 'Escape' && openMenuEl) {
103
+ const btn = openMenuBtn;
104
+ closeMenu();
105
+ btn?.focus();
106
+ ev.preventDefault();
107
+ }
108
+ });
109
+ }