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,155 @@
1
+ // Per-script "log ID" assignment.
2
+ //
3
+ // Goal: every script that appears in a log line or UI label can be
4
+ // referred to by a short, file-derived handle: its basename and its
5
+ // script number (`bold-on-hover.user.js#E4`), the number sm-dev names
6
+ // it by (see sticky-numbers.ts). The number keeps two scripts sharing a
7
+ // basename apart, and never shifts when other scripts come and go.
8
+ //
9
+ // A script with no number yet (one just added, before the service
10
+ // worker numbers it, or one in an sm-dev test collection) is labelled
11
+ // by its basename alone, or `#1`, `#2`, ... among other unnumbered
12
+ // scripts sharing it.
13
+ //
14
+ // The basename comes from `scriptBasename`, so a script added by URL
15
+ // contributes its decoded filename (`My Script.user.js`), not the
16
+ // percent-encoded URL tail. `script-source-url.ts` re-encodes the
17
+ // label on its way into a `//# sourceURL=` directive.
18
+ //
19
+ // Unnumbered dupe ordering: sorted by their stable UUID `id`, so the
20
+ // assignment is deterministic, though adding or removing another such
21
+ // script can reshuffle them.
22
+ import { scriptBasename } from '../script-display.js';
23
+ import { numberMap, numbersFrom, SCRIPT_NUMBERS_KEY } from './sticky-numbers.js';
24
+ import { scanStorageRows } from './storage-scan.js';
25
+ import { sharedRows } from './row-cache.js';
26
+ const SCRIPT_PREFIX = 'script:';
27
+ // ---------- Pure helpers ----------
28
+ /**
29
+ * Each script's log ID, by id. `numbers`: the script numbers (from the
30
+ * `scriptNumbers` row); a script with one is `<basename>#E<number>`.
31
+ */
32
+ export function computeLogIds(scripts, numbers = new Map()) {
33
+ const groups = new Map();
34
+ const out = new Map();
35
+ for (const s of scripts) {
36
+ const base = scriptBasename(s.relativePath);
37
+ const n = numbers.get(s.id);
38
+ if (n !== undefined) {
39
+ out.set(s.id, `${base}#E${n}`);
40
+ continue;
41
+ }
42
+ const arr = groups.get(base);
43
+ if (arr)
44
+ arr.push(s);
45
+ else
46
+ groups.set(base, [s]);
47
+ }
48
+ for (const [base, arr] of groups) {
49
+ if (arr.length === 1) {
50
+ out.set(arr[0].id, base);
51
+ continue;
52
+ }
53
+ // Plain byte-order comparison rather than localeCompare so the
54
+ // assignment is locale-independent — a Turkish-locale SW must agree
55
+ // with a US-locale collections page, and UUIDs are ASCII anyway.
56
+ arr.sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0));
57
+ arr.forEach((s, i) => out.set(s.id, `${base}#${i + 1}`));
58
+ }
59
+ return out;
60
+ }
61
+ // ---------- SW-side runtime ----------
62
+ //
63
+ // The SW maintains its own copy of {id → relativePath} for every
64
+ // stored script (regardless of status — error/missing scripts can
65
+ // still show up in update-check log lines). One full read at startup,
66
+ // then kept fresh via chrome.storage.onChanged.
67
+ let allInfo = new Map();
68
+ let numbers = new Map();
69
+ let logIds = new Map();
70
+ let loaded = null;
71
+ export function ensureLogIdsLoaded() {
72
+ if (!loaded)
73
+ loaded = loadAll();
74
+ return loaded;
75
+ }
76
+ async function loadAll() {
77
+ // From the worker's row cache when it's on (frozen rows, read only),
78
+ // with the numbers row, which it doesn't keep, read beside it.
79
+ const cached = await sharedRows((k) => k.startsWith(SCRIPT_PREFIX));
80
+ const all = cached
81
+ ? { ...cached, ...await chrome.storage.local.get(SCRIPT_NUMBERS_KEY) }
82
+ : await scanStorageRows('log id table', (k) => k.startsWith(SCRIPT_PREFIX) || k === SCRIPT_NUMBERS_KEY);
83
+ const next = new Map();
84
+ for (const [key, value] of Object.entries(all)) {
85
+ if (!key.startsWith(SCRIPT_PREFIX))
86
+ continue;
87
+ const s = value;
88
+ next.set(s.id, { id: s.id, relativePath: s.relativePath });
89
+ }
90
+ allInfo = next;
91
+ numbers = numberMap(numbersFrom(all[SCRIPT_NUMBERS_KEY]));
92
+ recompute();
93
+ }
94
+ function recompute() {
95
+ logIds = computeLogIds([...allInfo.values()], numbers);
96
+ }
97
+ let listenerInstalled = false;
98
+ export function installScriptLogIdListener() {
99
+ if (listenerInstalled)
100
+ return;
101
+ listenerInstalled = true;
102
+ chrome.storage.onChanged.addListener((changes, areaName) => {
103
+ if (areaName !== 'local')
104
+ return;
105
+ let touched = false;
106
+ if (SCRIPT_NUMBERS_KEY in changes) {
107
+ numbers = numberMap(numbersFrom(changes[SCRIPT_NUMBERS_KEY].newValue));
108
+ touched = true;
109
+ }
110
+ for (const [key, change] of Object.entries(changes)) {
111
+ if (!key.startsWith(SCRIPT_PREFIX))
112
+ continue;
113
+ const id = key.slice(SCRIPT_PREFIX.length);
114
+ if (change.newValue === undefined) {
115
+ if (allInfo.delete(id))
116
+ touched = true;
117
+ continue;
118
+ }
119
+ const s = change.newValue;
120
+ const prior = allInfo.get(id);
121
+ if (!prior || prior.relativePath !== s.relativePath) {
122
+ allInfo.set(id, { id: s.id, relativePath: s.relativePath });
123
+ touched = true;
124
+ }
125
+ }
126
+ if (touched)
127
+ recompute();
128
+ });
129
+ }
130
+ /**
131
+ * The script numbers, straight from the service worker's numbering, so
132
+ * a label asked for right after a sync already has its `#E<n>`, rather
133
+ * than waiting for the storage change event to bring it.
134
+ */
135
+ export function noteScriptNumbers(next) {
136
+ numbers = next;
137
+ recompute();
138
+ }
139
+ // Short human-readable label for SW log messages. Falls back to the
140
+ // first 8 chars of the UUID when the script isn't tracked — happens
141
+ // only for a brief window before ensureLogIdsLoaded() resolves, or
142
+ // for late sm:* reports from scripts that have since been removed.
143
+ export function getScriptLogLabel(id) {
144
+ return logIds.get(id) ?? id.slice(0, 8);
145
+ }
146
+ // Test/debug hook. Note: we do NOT flip listenerInstalled back to
147
+ // false — chrome.storage.onChanged.addListener can't be cleanly
148
+ // undone for an anonymous listener, and re-installing would queue
149
+ // duplicate callbacks.
150
+ export function _resetScriptLogIdsForTests() {
151
+ allInfo = new Map();
152
+ numbers = new Map();
153
+ logIds = new Map();
154
+ loaded = null;
155
+ }
@@ -0,0 +1,290 @@
1
+ // Per-script event log.
2
+ //
3
+ // Each StoredScript gets its own log keyed by script ID and stored in
4
+ // chrome.storage.session (so logs survive SW restarts but are cleared
5
+ // when Chrome shuts down — same lifetime as the per-tab badge state).
6
+ //
7
+ // We chose a single per-script array (rather than one storage key per
8
+ // entry) because chrome.storage.session is a flat key-value store and
9
+ // scanning N keys to read N log lines would dominate the cost of
10
+ // rendering the log pane. The array IS a read/modify/write append, so
11
+ // concurrent appendScriptLog calls for the same id are serialized
12
+ // through an in-memory promise chain to keep them from clobbering each
13
+ // other. Cross-id appends still run concurrently.
14
+ //
15
+ // Entries are append-only with a fixed cap; once the count passes
16
+ // LOG_CAP the oldest entries are dropped. The cap is generous enough
17
+ // to cover a normal day of refreshes + a few crashy injections but
18
+ // small enough that a runaway error loop can't fill storage.
19
+ //
20
+ // Alongside the log we maintain a sibling `loglevel:<scriptId>` row
21
+ // recording the highest level any SCRIPT-ORIGINATED entry has carried
22
+ // so far — a runtime error, a refused `GM_download`, a parse-time
23
+ // diagnostic.
24
+ // Bumped only when an append carries a strictly higher level; cleared
25
+ // on `clearScriptLog` and by an entry that `resetsLogLevel` (new content
26
+ // loaded). Entries that record a load-state transition
27
+ // (`state` set — see `LogEntry.state`) never touch it: their badge
28
+ // comes from the row and clears with it. It lives in session storage
29
+ // (not on the StoredScript row) because:
30
+ // - Lifetime matches the log itself — Chrome shutdown drops both
31
+ // together, so a WARNING/ERROR badge can't outlive its entries.
32
+ // - One bulk `get(null)` yields every script's level in O(scripts)
33
+ // rather than O(scripts × entries) — the collections page needs this
34
+ // to render the Status column cheaply.
35
+ import { validLocation } from '../diagnostic-location.js';
36
+ import { isPageContext } from '../run-context.js';
37
+ import { isLoadState } from '../script-display.js';
38
+ // Ordinal compare for LogLevel: error > warning > info. Used to decide
39
+ // whether an append should bump the stored max.
40
+ const LEVEL_RANK = { info: 1, warning: 2, error: 3 };
41
+ function isHigher(candidate, prior) {
42
+ if (prior === undefined)
43
+ return true;
44
+ return LEVEL_RANK[candidate] > LEVEL_RANK[prior];
45
+ }
46
+ // Hard cap on retained entries per script. Older entries are dropped
47
+ // on append. 500 × ~200 bytes ≈ 100 KB per script — well inside the
48
+ // chrome.storage.session quota (10 MB total).
49
+ export const LOG_CAP = 500;
50
+ const logKey = (scriptId) => `log:${scriptId}`;
51
+ const levelKey = (scriptId) => `loglevel:${scriptId}`;
52
+ // Prefix used by the bulk-read path. Kept here so callers don't have
53
+ // to hardcode it.
54
+ export const LOG_LEVEL_KEY_PREFIX = 'loglevel:';
55
+ // Per-script promise chain. chrome.storage.session has no atomic
56
+ // append, so two concurrent appendScriptLog calls for the same id
57
+ // would both read the same prior array and the second .set would
58
+ // overwrite the first append. Chaining serializes them; the map entry
59
+ // is dropped once its chain drains so long-running sessions don't
60
+ // accumulate dead Promises.
61
+ //
62
+ // The map is module state, so the chain only orders writes made in THIS
63
+ // context. Only the worker writes the log today: it logs every
64
+ // injection and refresh event, and the viewer's Clear button goes
65
+ // through the worker's ops port. Pages share `script-store` code with
66
+ // the worker, though, and a page that logged through it would run a
67
+ // second chain on the same key, losing entries or resurrecting ones a
68
+ // clear had just removed. So, as a guard, a MUTATION made in a page
69
+ // hops to the worker (`writeViaWorker`) and the worker's chain stays
70
+ // the only one that touches the key. Reads don't hop: they aren't
71
+ // read-modify-write.
72
+ //
73
+ // What that guarantees is "the worker's chain orders everything that
74
+ // reaches it", NOT "a page's appends are ordered": two writes from one
75
+ // page are two messages, ordered by the IPC rather than by a chain.
76
+ const queues = new Map();
77
+ function enqueue(scriptId, task) {
78
+ const prev = queues.get(scriptId) ?? Promise.resolve();
79
+ // .then(task, task) so a thrown task doesn't poison the chain — the
80
+ // next queued task still runs against the post-throw state.
81
+ const next = prev.then(task, task);
82
+ queues.set(scriptId, next);
83
+ void next.finally(() => {
84
+ if (queues.get(scriptId) === next)
85
+ queues.delete(scriptId);
86
+ });
87
+ return next;
88
+ }
89
+ // Told of each entry the worker writes, for the events sm-dev runs
90
+ // hear (sm-dev-link.ts). Set by the worker only.
91
+ let entryListener = null;
92
+ export function onScriptLogEntry(listener) {
93
+ entryListener = listener;
94
+ }
95
+ // Append an entry. Every caller that reaches here observed something,
96
+ // and the Log tab is a transcript of observations, so the entry is
97
+ // always written. A caller that fires on its own schedule rather than
98
+ // on an observation — the injector's per-sync registration pass — is
99
+ // responsible for deciding whether anything happened before it calls.
100
+ export async function appendScriptLog(scriptId, input) {
101
+ if (isPageContext())
102
+ return writeViaWorker({ op: 'append', scriptId, input });
103
+ return enqueue(scriptId, () => writeEntry(scriptId, input));
104
+ }
105
+ // ---------- Page → worker relay ----------
106
+ export const SCRIPT_LOG_WRITE_MESSAGE = 'sm:script-log-write';
107
+ // Worker side. Runs the write on the worker's own per-script chain,
108
+ // which is the point of the hop.
109
+ export async function handleScriptLogWrite(req) {
110
+ if (req.op === 'append')
111
+ return enqueue(req.scriptId, () => writeEntry(req.scriptId, req.input));
112
+ return enqueue(req.scriptId, () => removeEntries(req.scriptId));
113
+ }
114
+ // Page side. Only the worker writes storage (docs/worker-writes.md).
115
+ // No page code logs today; this is the guard for one that does, through
116
+ // code it shares with the worker (see `queues` above).
117
+ // An entry the worker didn't ACKNOWLEDGE is sent once more: a worker
118
+ // stopped mid-message closes the channel unanswered, and the second
119
+ // message wakes a new one. Then it is dropped, and said so in this
120
+ // page's console.
121
+ //
122
+ // The ack has to be READ, not just awaited: a message nothing handled,
123
+ // and a handler that threw, both RESOLVE `sendMessage` (with
124
+ // `undefined`); only an unreachable worker rejects it. So `true` (sent
125
+ // by the listener once the write has landed) is the one answer that
126
+ // means it was written. A retry after a write that landed unacknowledged
127
+ // can log the entry twice, which beats losing it.
128
+ async function writeViaWorker(req) {
129
+ for (let attempt = 0; attempt < 2; attempt++) {
130
+ try {
131
+ const ack = await chrome.runtime.sendMessage({ type: SCRIPT_LOG_WRITE_MESSAGE, ...req });
132
+ if (ack === true)
133
+ return;
134
+ }
135
+ catch { /* no receiving end */ }
136
+ }
137
+ console.info(`[SourceMonkey] the background worker did not save a log ${req.op} for script ${req.scriptId}`);
138
+ }
139
+ // Shared read-modify-write body. Always called from inside `enqueue`.
140
+ async function writeEntry(scriptId, input) {
141
+ const lk = logKey(scriptId);
142
+ const mk = levelKey(scriptId);
143
+ const got = await chrome.storage.session.get([lk, mk]);
144
+ const prior = Array.isArray(got[lk]) ? got[lk] : [];
145
+ const priorLevel = got[mk];
146
+ if (input.collapse) {
147
+ const { key, message } = input.collapse;
148
+ let at = prior.length - 1;
149
+ while (at >= 0 && prior[at].collapseKey !== key)
150
+ at--;
151
+ if (at >= 0) {
152
+ const old = prior[at];
153
+ const count = (old.count ?? 1) + 1;
154
+ const level = isHigher(input.level, old.level) ? input.level : old.level;
155
+ const next = prior.slice();
156
+ next[at] = { ...old, level, count, message: message(count) };
157
+ const toSet = { [lk]: next };
158
+ if (isHigher(input.level, priorLevel))
159
+ toSet[mk] = input.level;
160
+ // Not a new entry, so the entry listener isn't told.
161
+ await chrome.storage.session.set(toSet);
162
+ return;
163
+ }
164
+ }
165
+ const entry = {
166
+ timestamp: input.timestamp ?? Date.now(),
167
+ level: input.level,
168
+ source: input.source,
169
+ message: input.collapse ? input.collapse.message(1) : input.message,
170
+ };
171
+ if (input.tabId !== undefined)
172
+ entry.tabId = input.tabId;
173
+ if (input.collapse)
174
+ entry.collapseKey = input.collapse.key;
175
+ const location = validLocation(input.location);
176
+ if (location)
177
+ entry.location = location;
178
+ // Checked like `location`: the input may have crossed the page →
179
+ // worker message boundary, and a bad value here would render an
180
+ // empty badge.
181
+ if (isLoadState(input.state))
182
+ entry.state = input.state;
183
+ if (input.resetsLogLevel === true)
184
+ entry.resetsLogLevel = true;
185
+ const next = prior.length >= LOG_CAP
186
+ ? [...prior.slice(prior.length - LOG_CAP + 1), entry]
187
+ : [...prior, entry];
188
+ const toSet = { [lk]: next };
189
+ // A load-state transition is badged by the row, not the sentinel —
190
+ // otherwise a `refresh-failed` entry would leave WARNING behind for
191
+ // the rest of the session after the row had already recovered. A
192
+ // resetting entry doesn't count itself either: it's the boundary,
193
+ // not the first event past it.
194
+ const counts = entry.state === undefined && !entry.resetsLogLevel;
195
+ if (counts && isHigher(input.level, priorLevel))
196
+ toSet[mk] = input.level;
197
+ await chrome.storage.session.set(toSet);
198
+ try {
199
+ entryListener?.(scriptId, entry, input.fromReport === true);
200
+ }
201
+ catch {
202
+ // A listener's failure is no reason to lose the entry's other effects.
203
+ }
204
+ // The reset rides on the same queued write as the entry, so it lands
205
+ // exactly between the rows before it and the rows after it: parse
206
+ // diagnostics replayed for the new version come next and bump the
207
+ // sentinel afresh. Skipped when there is nothing to clear — the
208
+ // common case, since most new content lands on a clean badge.
209
+ if (entry.resetsLogLevel && priorLevel !== undefined)
210
+ await chrome.storage.session.remove(mk);
211
+ }
212
+ export async function getScriptLog(scriptId) {
213
+ const key = logKey(scriptId);
214
+ const got = await chrome.storage.session.get(key);
215
+ return Array.isArray(got[key]) ? got[key] : [];
216
+ }
217
+ export async function clearScriptLog(scriptId) {
218
+ if (isPageContext())
219
+ return writeViaWorker({ op: 'clear', scriptId });
220
+ return enqueue(scriptId, () => removeEntries(scriptId));
221
+ }
222
+ // Shared removal body, the counterpart of `writeEntry`. Always called
223
+ // from inside `enqueue`.
224
+ async function removeEntries(scriptId) {
225
+ await chrome.storage.session.remove([logKey(scriptId), levelKey(scriptId)]);
226
+ }
227
+ // Drop the logs of scripts whose rows are going away — otherwise an
228
+ // orphaned session-storage entry would outlive the script it described
229
+ // and confuse any later renderer that scans log levels. Goes through
230
+ // `clearScriptLog` (and therefore the per-script queue, in whichever
231
+ // context owns it), so an append still in flight can't land after the
232
+ // removal and resurrect the row.
233
+ // Best-effort: a script whose log was never written isn't an error, and
234
+ // nothing that deletes a row should fail because of its log.
235
+ export async function forgetScriptLogs(scriptIds) {
236
+ await Promise.all(scriptIds.map((id) => clearScriptLog(id).catch(() => { })));
237
+ }
238
+ // Read the persisted "highest log level seen" sentinel for one script.
239
+ // Returns undefined when no script-originated entry has been appended
240
+ // since the last reset — a `clearScriptLog`, a `resetsLogLevel` entry
241
+ // (new content loaded), or the start of the Chrome session. Load-state
242
+ // entries don't count (see `LogEntry.state`). Between resets it only
243
+ // goes up — LOG_CAP rotation does NOT decrease it. Once a script has
244
+ // hit ERROR, rotation that drops the only error entry leaves the badge
245
+ // at ERROR until the next reset. We accept that drift because
246
+ // recomputing on every rotation would dominate the cost of an append
247
+ // and the log itself is session-scoped anyway.
248
+ export async function getMaxLogLevel(scriptId) {
249
+ const key = levelKey(scriptId);
250
+ const got = await chrome.storage.session.get(key);
251
+ const v = got[key];
252
+ return v === 'info' || v === 'warning' || v === 'error' ? v : undefined;
253
+ }
254
+ // Read every script's max log level in one storage round trip. The
255
+ // collections page uses this to render the Status column without N
256
+ // per-script reads. Returns a Map keyed by scriptId.
257
+ export async function getAllMaxLogLevels() {
258
+ const all = await chrome.storage.session.get(null);
259
+ const out = new Map();
260
+ for (const [key, value] of Object.entries(all)) {
261
+ if (!key.startsWith(LOG_LEVEL_KEY_PREFIX))
262
+ continue;
263
+ if (value === 'info' || value === 'warning' || value === 'error') {
264
+ out.set(key.slice(LOG_LEVEL_KEY_PREFIX.length), value);
265
+ }
266
+ }
267
+ return out;
268
+ }
269
+ // Convenience: fire-and-forget. Used by call sites that already have a
270
+ // console.log next to the event and don't want to await an extra
271
+ // promise. Errors swallow silently — the log itself isn't load-bearing
272
+ // and a chrome.storage.session failure shouldn't break the refresh
273
+ // path that's emitting the event.
274
+ export function logScriptEvent(scriptId, input) {
275
+ appendScriptLog(scriptId, input).catch(() => { });
276
+ }
277
+ // Test/debug hook to drain in-flight appends before asserting on the
278
+ // stored log. Used by unit tests directly, and by the SW's e2e
279
+ // `getScriptLog` accessor — both would otherwise race `logScriptEvent`,
280
+ // which returns before its write lands.
281
+ export async function _flushScriptLogQueueForTests(scriptId) {
282
+ // Loop rather than awaiting once: an append that chains on while we
283
+ // wait becomes the new tail, and returning after the old one would
284
+ // leave exactly the entry the caller is about to read still unwritten.
285
+ let awaited;
286
+ for (let tail = queues.get(scriptId); tail && tail !== awaited; tail = queues.get(scriptId)) {
287
+ awaited = tail;
288
+ await tail.catch(() => { });
289
+ }
290
+ }
@@ -0,0 +1,104 @@
1
+ // Per-script record of a registration `chrome.userScripts` refused.
2
+ //
3
+ // A rejected spec is a state of its own: the script loaded fine (it has
4
+ // valid content, a valid header, and a status of `ok`), but Chrome
5
+ // would not accept the registration — a `@match` pattern it considers
6
+ // malformed is the usual cause — so the script silently never runs.
7
+ // The collections page and the viewer render that as REJECTED. See
8
+ // [docs/diagnostics.md](../docs/diagnostics.md#rejected-registrations).
9
+ //
10
+ // Rarer than it used to be: `matchPatternProblem` (src/url-match.ts)
11
+ // catches the malformed shapes we are sure about at parse time, so what
12
+ // reaches here is the rest of Chrome's grammar — a malformed port, a
13
+ // host its canonicalizer rejects, or whatever it tightens next.
14
+ //
15
+ // Written only by `src/extension/injector.ts` — `registerWithFallback` and the
16
+ // targeted retry are the only places that ever learn a spec was
17
+ // refused. Read by the collections page and the viewer for the row's
18
+ // status, and by `src/extension/background.ts` for the tab badge; all three
19
+ // watch `chrome.storage.session.onChanged` so they repaint without a
20
+ // reload.
21
+ //
22
+ // Why `chrome.storage.session`:
23
+ // - The fact describes Chrome's CURRENT registration set, which is
24
+ // itself dropped on browser shutdown. Persisting it to local
25
+ // storage would let a REJECTED badge outlive the registration
26
+ // attempt that produced it.
27
+ // - It has to outlive the service worker: MV3 evicts the SW freely
28
+ // and `syncRegistrations` runs again on every wake-up, so an
29
+ // in-memory Map would lose the state on every eviction.
30
+ //
31
+ // Why one map key rather than a key per script (the shape
32
+ // `script-log.ts` uses for log levels): the injector rewrites the whole
33
+ // set on every sync so that a script which left the active registration
34
+ // set — disabled, deleted, or newly INVALID — can't leave a stale
35
+ // REJECTED badge behind. Authoritative-set semantics want a single
36
+ // read/write; the alternative would need a full session scan to find
37
+ // the strays.
38
+ import { assertWorkerContext } from '../run-context.js';
39
+ // Session-storage key holding the whole `RejectionMap`.
40
+ export const REJECTIONS_KEY = 'injector-rejections';
41
+ // The one sentence every surface uses for a refused registration: the
42
+ // Log row, and the viewer's message bar. One message per problem —
43
+ // two copies would drift the moment either is reworded.
44
+ //
45
+ // `reason` is Chrome's own wording, appended rather than rephrased
46
+ // because it names the offending header value. See
47
+ // [docs/diagnostics.md](../docs/diagnostics.md#rejected-registrations).
48
+ export function describeRejection(reason) {
49
+ return `Chrome rejected this script's registration, so it will not run: ${reason}`;
50
+ }
51
+ // Coerce an unknown storage value into a RejectionMap, dropping any
52
+ // entry whose reason isn't a string. Used for both the direct read and
53
+ // the `storage.onChanged` newValue, so the two paths can't disagree
54
+ // about what a well-formed record looks like.
55
+ export function coerceRejectionMap(value) {
56
+ if (!value || typeof value !== 'object')
57
+ return {};
58
+ const out = {};
59
+ for (const [id, message] of Object.entries(value)) {
60
+ if (typeof message === 'string')
61
+ out[id] = message;
62
+ }
63
+ return out;
64
+ }
65
+ // Every registration the last sync could not place. Empty when nothing
66
+ // is rejected, which is the overwhelmingly common case.
67
+ export async function readRejections() {
68
+ try {
69
+ const got = await chrome.storage.session.get(REJECTIONS_KEY);
70
+ return coerceRejectionMap(got[REJECTIONS_KEY]);
71
+ }
72
+ catch {
73
+ // Session storage being unavailable shouldn't break a sync; the
74
+ // worst case is a status cell that doesn't say REJECTED.
75
+ return {};
76
+ }
77
+ }
78
+ // Replace the stored map with `next`, but only when it actually differs
79
+ // from `prior`. The no-rejections case — every sync on a healthy
80
+ // profile — then costs one read and no write, and no needless
81
+ // `storage.onChanged` event fans out to the open UI pages.
82
+ //
83
+ // Note this is about the STORAGE write, not about logging: a rejection
84
+ // that persists across syncs writes nothing here but still gets a fresh
85
+ // Log row each time, because "we tried again and Chrome said no again"
86
+ // is a fact about this sync. See `registerWithFallback`.
87
+ export async function writeRejections(prior, next) {
88
+ if (sameRejections(prior, next))
89
+ return;
90
+ assertWorkerContext('write storage');
91
+ try {
92
+ await chrome.storage.session.set({ [REJECTIONS_KEY]: next });
93
+ }
94
+ catch { /* best-effort: the badge isn't load-bearing */ }
95
+ }
96
+ function sameRejections(a, b) {
97
+ const keys = Object.keys(a);
98
+ if (keys.length !== Object.keys(b).length)
99
+ return false;
100
+ for (const id of keys)
101
+ if (a[id] !== b[id])
102
+ return false;
103
+ return true;
104
+ }