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,197 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <title>SourceMonkey | Options</title>
6
+ <link rel="stylesheet" href="styles.css" />
7
+ <style>
8
+ /* Full-bleed body; the header bar and `.page-body` (styles.css)
9
+ each centre their own contents on the shared `--page-width`
10
+ column. */
11
+ body { font: 14px system-ui, sans-serif; margin: 0; color: #222; --page-width: 720px; }
12
+ /* Whole-px vertical metrics — see docs/architecture.md "Extension-page styling". */
13
+ /* Horizontal only: height, surface and states are shared across
14
+ every page (styles.css). */
15
+ button { font: inherit; padding: 0 11px; cursor: pointer; }
16
+ .settings { margin-top: 14px; }
17
+ .settings-heading { font-size: 1em; line-height: 16px; margin: 0 0 7px; }
18
+ /* Odd line-height: centering the 13px checkbox in an even line box
19
+ would put it on a half pixel. */
20
+ .settings-row { display: inline-flex; align-items: center; line-height: 15px; gap: 0.5em; cursor: pointer; }
21
+ /* Space each toggle from the hint text of the one above it. */
22
+ .settings-hint + .settings-row { margin-top: 11px; }
23
+ .settings-row input[type="checkbox"] { cursor: pointer; margin: 0; }
24
+ .settings-hint { margin: 4px 0 0; font-size: 0.85em; line-height: 14px; color: #666; }
25
+ .settings-section { margin-top: 2em; padding-top: 1em; border-top: 1px solid #eee; }
26
+ .token-row { display: flex; gap: 0.5em; align-items: center; margin: 0.5em 0; }
27
+ .token-row input[type="password"],
28
+ .token-row input[type="text"] {
29
+ flex: 1; font: inherit; padding: 0.3em 0.5em;
30
+ }
31
+ .token-status { font-size: 0.85em; color: #666; min-height: 1.2em; margin: 0.3em 0 0; }
32
+ .token-status.ok { color: #1a7f37; }
33
+ .token-status.err { color: #b00020; }
34
+ details.token-help { margin-top: 0.8em; font-size: 0.9em; }
35
+ details.token-help summary { cursor: pointer; color: #555; }
36
+ details.token-help ol { margin: 0.5em 0 0; padding-left: 1.4em; }
37
+ details.token-help li { margin: 0.3em 0; }
38
+ details.token-help code { background: #f4f4f4; padding: 0 0.25em; border-radius: 2px; }
39
+ #local-roots textarea {
40
+ width: 100%; box-sizing: border-box; font: 13px ui-monospace, monospace;
41
+ padding: 0.4em 0.5em; margin-top: 0.3em; resize: vertical;
42
+ }
43
+ .root-problems { margin: 0.3em 0 0; padding-left: 1.2em; font-size: 0.85em; color: #b00020; }
44
+ .root-problems li { margin: 0.2em 0; }
45
+ .storage-usage { margin: 0.5em 0 0; border-collapse: collapse; }
46
+ /* One gap between every pair of columns. */
47
+ .storage-usage td { padding: 2px 0 2px 2.5em; text-align: left; }
48
+ .storage-usage td:first-child { padding-left: 0; }
49
+ .storage-usage td.num { text-align: right; font-variant-numeric: tabular-nums; }
50
+ .storage-usage tr.total td { border-top: 1px solid #ddd; font-weight: 600; }
51
+ .storage-usage + .settings-hint { margin-top: 8px; }
52
+ </style>
53
+ </head>
54
+ <body>
55
+ <header class="page-header">
56
+ <div class="page-header-inner">
57
+ <a class="page-home page-icon-link" href="collections.html" target="_blank"
58
+ rel="noopener" tabindex="-1" aria-hidden="true">
59
+ <img class="page-icon" src="icons/icon-48.png" alt="" />
60
+ </a>
61
+ <h1><a class="page-home brand" href="collections.html" target="_blank" rel="noopener"
62
+ title="Open the collections page">SourceMonkey</a><span class="brand"> |</span> Options</h1>
63
+ <div class="page-header-actions">
64
+ <button id="open-collections" type="button" title="Open the collections page">Collections</button>
65
+ </div>
66
+ </div>
67
+ </header>
68
+
69
+ <div class="page-body">
70
+ <div id="settings" class="settings">
71
+ <h2 class="settings-heading">Settings</h2>
72
+ <label class="settings-row">
73
+ <input id="capture-userjs" type="checkbox" />
74
+ <span>Capture <code>.user.js</code> links</span>
75
+ </label>
76
+ <p class="settings-hint">
77
+ Opening a <code>.user.js</code> link (or a <code>.user.js</code>
78
+ file on this computer) shows SourceMonkey's install prompt
79
+ instead of leaving it to Chrome, which just shows the file's
80
+ source rather than offering to install it. On by default. Turn
81
+ this off to get Chrome's stock behavior back, including for
82
+ local files.
83
+ </p>
84
+ <label class="settings-row">
85
+ <input id="debug-logging" type="checkbox" />
86
+ <span>Enable debug logging</span>
87
+ </label>
88
+ <p class="settings-hint">Logs per-navigation match lists and per-script update checks to the service-worker console. Off by default.</p>
89
+ </div>
90
+
91
+ <div id="local-roots" class="settings-section">
92
+ <h2 class="settings-heading">Search paths for local <code>@require</code> and <code>@resource</code> files (for scripts in local files)</h2>
93
+ <p class="settings-hint">
94
+ Scripts loaded from a local file can also load resources from local files, overriding the original URL.<br/>
95
+ The script's local directory is searched first, then the
96
+ collection directory it came from.<br/>
97
+ Then the directories here are searched, in order, looking for
98
+ files matching any trailing part of the URL, site name included.<br/>
99
+ If not found locally, files are loaded from their original URL.
100
+ </p>
101
+ <p class="settings-hint">
102
+ Note: Any files under these directories can be read by local userscripts.
103
+ </p>
104
+ <textarea id="local-roots-text" rows="4" spellcheck="false"
105
+ placeholder="One absolute path per line, e.g. /home/me/dev/userscript-lib"></textarea>
106
+ <div class="token-row">
107
+ <button id="local-roots-save" type="button">Save</button>
108
+ </div>
109
+ <p id="local-roots-status" class="token-status"></p>
110
+ <ul id="local-roots-problems" class="root-problems"></ul>
111
+ <div id="local-roots-file-access"></div>
112
+ </div>
113
+
114
+ <div id="downloads-copies" class="settings-section">
115
+ <h2 class="settings-heading">Downloads folder (for local copies of scripts)</h2>
116
+ <p class="settings-hint">
117
+ <strong>Local copy…</strong> on a script's page writes an editable
118
+ copy into <code>SourceMonkey/</code> under the browser's Downloads
119
+ folder, and registers that folder as the
120
+ <strong>Scripts copied in Downloads</strong> collection.
121
+ Chrome doesn't tell extensions where Downloads is, so it is
122
+ learned from the first copy. Correct it here if Chrome's
123
+ download location has changed since.
124
+ </p>
125
+ <div class="token-row">
126
+ <input id="downloads-dir" type="text" autocomplete="off" spellcheck="false"
127
+ placeholder="Learned from the first local copy, e.g. /home/me/Downloads" />
128
+ <button id="downloads-dir-save" type="button">Save</button>
129
+ </div>
130
+ <p id="downloads-dir-status" class="token-status"></p>
131
+ </div>
132
+
133
+ <div id="github-auth" class="settings-section">
134
+ <h2 class="settings-heading">GitHub authentication</h2>
135
+ <p class="settings-hint">
136
+ Optional personal access token used when fetching from
137
+ <code>api.github.com</code> and
138
+ <code>raw.githubusercontent.com</code>. Lets you add private
139
+ repos and lifts the GitHub API limit from <strong>60</strong>
140
+ to <strong>5000</strong> requests per hour. Stored in
141
+ <code>chrome.storage.local</code> on this machine; never sent
142
+ to any non-GitHub host.
143
+ </p>
144
+ <div class="token-row">
145
+ <input id="github-token" type="password" autocomplete="off" spellcheck="false"
146
+ placeholder="ghp_… or github_pat_…" />
147
+ <button id="github-token-reveal" type="button" title="Show / hide the token">Show</button>
148
+ <button id="github-token-save" type="button">Save</button>
149
+ <button id="github-token-clear" type="button" title="Remove the saved token">Clear</button>
150
+ </div>
151
+ <p id="github-token-status" class="token-status"></p>
152
+ <details class="token-help">
153
+ <summary>How to create a token</summary>
154
+ <ol>
155
+ <li>
156
+ Open
157
+ <a href="https://github.com/settings/personal-access-tokens" target="_blank" rel="noopener">
158
+ github.com/settings/personal-access-tokens
159
+ </a>
160
+ and click <strong>Generate new token</strong> (fine-grained recommended).
161
+ </li>
162
+ <li>
163
+ <strong>Repository access</strong>: pick the repos you want
164
+ SourceMonkey to reach (or <em>All repositories</em> for
165
+ convenience). For public repos you just want higher rate
166
+ limits on, the default <em>Public repositories</em> selection
167
+ is enough.
168
+ </li>
169
+ <li>
170
+ <strong>Permissions → Repository → Contents</strong>: select
171
+ <code>Read-only</code>. That's the only scope SourceMonkey
172
+ uses; leave everything else at <em>No access</em>.
173
+ </li>
174
+ <li>Set an expiry, generate the token, and paste it above.</li>
175
+ </ol>
176
+ <p class="settings-hint">
177
+ Tip: revoke the token from the same settings page if you
178
+ stop using SourceMonkey. Tokens never expire automatically
179
+ unless you set an expiry.
180
+ </p>
181
+ </details>
182
+ </div>
183
+
184
+ <div id="storage" class="settings-section">
185
+ <h2 class="settings-heading">Chrome storage usage</h2>
186
+ <p id="storage-limit" class="settings-hint" hidden></p>
187
+ <table id="storage-usage" class="storage-usage"></table>
188
+ <p class="settings-hint">
189
+ Details: <a href="debug/storage.html" target="_blank" rel="noopener">storage by script and file</a>
190
+ · <a href="debug/icons.html" target="_blank" rel="noopener">cached icons</a>
191
+ </p>
192
+ </div>
193
+ </div>
194
+
195
+ <script src="extension/options.js" type="module"></script>
196
+ </body>
197
+ </html>
@@ -0,0 +1,51 @@
1
+ // Wire types and shared state for the parse check, imported by every
2
+ // end that can: the service worker (`parse-check.ts`), the offscreen
3
+ // relay (`offscreen.ts`), the sandbox driver (`parse-sandbox-host.ts`),
4
+ // and the page-side client (`parse-check-client.ts`).
5
+ //
6
+ // The remaining participant, `parse-sandbox.ts`, deliberately does NOT
7
+ // import this — it is loaded as a classic `<script>` in a sandboxed
8
+ // page, and any import would make tsc emit an ES module. It mirrors
9
+ // these shapes inline instead; they are small and change together.
10
+ export const PARSE_CHECK_MSG = 'sm:parse-check';
11
+ export const PARSE_CHECK_RESULT_MSG = 'sm:parse-check-result';
12
+ // Posted by the sandbox to its embedder once its listener is live.
13
+ // See the readiness note in `offscreen.ts`.
14
+ export const PARSE_SANDBOX_READY_MSG = 'sm:parse-sandbox-ready';
15
+ // ---------- The test toggle ----------
16
+ //
17
+ // `sm.setParseCheckEnabled(false)` turns the check off so a test can
18
+ // drive an unparsable script all the way to injection and exercise the
19
+ // run-time backstop.
20
+ //
21
+ // It lives in **session storage**, not in a module variable, for two
22
+ // reasons — each of which was a real bug in an earlier shape of this:
23
+ //
24
+ // - There is more than one checker now (the service worker's and one
25
+ // per extension page). A flag in the SW can't reach a page that
26
+ // compiles in its own sandbox, so the toggle would silently mean
27
+ // "half off".
28
+ // - `resetAllState` clears session storage between tests, so the
29
+ // toggle resets itself. A module variable had to be restored by
30
+ // hand, and a missed restore left the check disabled for the rest
31
+ // of that service worker's life — re-breaking the feature for
32
+ // every later spec, as a flake rather than a clean failure.
33
+ const PARSE_CHECK_DISABLED_KEY = 'sm:parse-check-disabled';
34
+ export async function setParseCheckDisabled(disabled) {
35
+ if (disabled)
36
+ await chrome.storage.session.set({ [PARSE_CHECK_DISABLED_KEY]: true });
37
+ else
38
+ await chrome.storage.session.remove(PARSE_CHECK_DISABLED_KEY);
39
+ }
40
+ // Read before every compile. Cheap (session storage is in-memory) and
41
+ // always current, which is what lets a test flip it mid-run.
42
+ export async function isParseCheckDisabled() {
43
+ try {
44
+ const got = await chrome.storage.session.get(PARSE_CHECK_DISABLED_KEY);
45
+ return got[PARSE_CHECK_DISABLED_KEY] === true;
46
+ }
47
+ catch {
48
+ // Storage unavailable is not a reason to skip the check.
49
+ return false;
50
+ }
51
+ }
@@ -0,0 +1,171 @@
1
+ // Turning a syntax error into a place: which file, which line, which
2
+ // column.
3
+ //
4
+ // The sandbox (`parse-sandbox.ts`) can only report a position in the
5
+ // text it was handed, which is the whole composed program — our own
6
+ // first line, then every `@require` body in order, then the script's
7
+ // own source. This module is the other half: pick which of the sandbox's
8
+ // two probes to believe, then map its line back through the same
9
+ // segment list the source map is built from, so the answer names the
10
+ // file the line was actually written in.
11
+ //
12
+ // It lives apart from both checkers because both need it — the service
13
+ // worker's (`background.ts`) and every extension page's
14
+ // (`parse-check-client.ts`) — and a rule this fiddly must not exist
15
+ // twice.
16
+ import { lookupGeneratedLine, } from './source-map.js';
17
+ // ---------- Choosing a probe ----------
18
+ //
19
+ // The two probes fail in opposite directions (see `locate` in
20
+ // parse-sandbox.ts), and the verdict's own message is what tells them
21
+ // apart.
22
+ //
23
+ // `new Function` is itself a wrapper — V8 compiles the body inside
24
+ // `function anonymous() { … }` — so it has the SAME blind spot as our
25
+ // wrapped probe. When a source's braces are unbalanced, the source
26
+ // escapes both wrappers, and each reports whatever it hit after the
27
+ // escape. That is exactly when the two messages diverge, which makes
28
+ // "do the messages agree?" a direct test for "was the wrapper
29
+ // absorbed?":
30
+ //
31
+ // - **agree** → the wrapper *probably* held, so the wrapped probe
32
+ // parsed the same program `new Function` judged. Trust it, subject
33
+ // to the range check below. This covers every ordinary error, plus
34
+ // the ones only the wrapped form gets right: a top-level `return`
35
+ // (legal in a function body, an error in a Script) no longer
36
+ // produces a spurious earlier position.
37
+ // - **disagree** → both wrappers were escaped, so only the raw probe
38
+ // is describing the real structure. It places the offending brace
39
+ // itself rather than the downstream error it causes.
40
+ //
41
+ // Rejecting instead of choosing was the earlier design, and it was
42
+ // worse: brace imbalance is the most common syntax error there is, and
43
+ // the messages usually can't agree for it, so those scripts would
44
+ // always have lost their position.
45
+ //
46
+ // "Probably" is doing real work in the first branch. The messages can
47
+ // agree even when the wrapper was escaped, because after escaping it
48
+ // the wrapped probe hits the closing `}` WE appended and reports
49
+ // `Unexpected token '}'` — the same text a stray `}` produces for the
50
+ // verdict. Measured, for `var a=1;\nfunction b(){}\n}` (three lines):
51
+ //
52
+ // verdict "Unexpected token '}'"
53
+ // wrapped "Unexpected token '}'" line 4 <- our WRAPPER_TAIL
54
+ // raw "Unexpected token '}'" line 3 <- the actual brace
55
+ //
56
+ // The tell is the range check: a wrapped probe that really did hold
57
+ // can only land inside the source, or on the sentinel when the source
58
+ // is genuinely unterminated. If it lands outside while the raw probe
59
+ // lands inside, the source is demonstrably NOT unterminated — the raw
60
+ // probe found a real position in it — so the out-of-range answer is
61
+ // escape fallout and the raw probe is the honest one.
62
+ export function selectParseLocation(args) {
63
+ const probes = args.probes;
64
+ if (!probes)
65
+ return null;
66
+ const wrapped = validProbe(probes.wrapped);
67
+ const raw = trustworthyRaw(validProbe(probes.raw));
68
+ // Outside the checked text means V8 ran off the end and stopped at
69
+ // our own sentinel, which happens when the source ends inside
70
+ // something unclosed. There is no line to blame there.
71
+ const inRange = (probe) => probe !== null && probe.line >= 1 && probe.line <= args.lineCount;
72
+ // `endsWith` rather than `===` so an unstripped DOM prefix on the
73
+ // probe's message (see `stripDomPrefix`) still matches the verdict's.
74
+ // It cannot match too eagerly in the direction that matters: a
75
+ // different error produces different trailing text. The non-empty
76
+ // check is load-bearing — `endsWith('')` is true of everything, so a
77
+ // verdict with no message would otherwise "agree" with any probe.
78
+ const verdictMessage = args.verdictMessage;
79
+ if (wrapped && verdictMessage && wrapped.message.endsWith(verdictMessage)) {
80
+ // Report the VERDICT's message rather than the probe's. They are
81
+ // the same error by construction, and the verdict's is the one that
82
+ // never carries a DOM prefix `stripDomPrefix` failed to strip.
83
+ if (inRange(wrapped))
84
+ return { ...wrapped, message: verdictMessage };
85
+ if (inRange(raw))
86
+ return raw;
87
+ return null;
88
+ }
89
+ // The fallback does NOT run in the other direction. A wrapped probe
90
+ // that disagreed with the verdict escaped its wrapper and is
91
+ // describing a different parse of a different program, so its
92
+ // position is not a second opinion about this error — it is an
93
+ // answer to another question.
94
+ return inRange(raw) ? raw : null;
95
+ }
96
+ // Diagnostics that only a *Script* can raise. The raw probe is a
97
+ // Script; `new Function` compiles a function body, so it can never
98
+ // produce either of these. Seeing one is proof the raw probe stopped
99
+ // at something that is perfectly legal in the entry we actually
100
+ // inject — so both its position and its message are about code that
101
+ // isn't broken, and using them would name a line the user should not
102
+ // go and look at.
103
+ //
104
+ // Verified against V8 that these two, and only these two, are
105
+ // Script-only. `Illegal break statement`, `Illegal continue
106
+ // statement` and `'super' keyword unexpected here` all read the same
107
+ // from `new Function`, so filtering them would throw away positions
108
+ // the raw probe got right.
109
+ const SCRIPT_ONLY_DIAGNOSTICS = [
110
+ 'Illegal return statement',
111
+ 'new.target expression is not allowed here',
112
+ ];
113
+ function trustworthyRaw(probe) {
114
+ if (!probe)
115
+ return null;
116
+ const scriptOnly = SCRIPT_ONLY_DIAGNOSTICS.some((d) => probe.message.endsWith(d));
117
+ return scriptOnly ? null : probe;
118
+ }
119
+ // The probes cross a postMessage boundary from a context that compiles
120
+ // untrusted source, so nothing about their shape is guaranteed.
121
+ function validProbe(probe) {
122
+ if (!probe || typeof probe.message !== 'string')
123
+ return null;
124
+ if (!Number.isFinite(probe.line) || !Number.isFinite(probe.column))
125
+ return null;
126
+ return probe;
127
+ }
128
+ // ---------- Attaching it to the verdict ----------
129
+ // Replace a verdict's raw `probes` with a resolved `location`. Called
130
+ // by both checkers, immediately after the sandbox answers and while
131
+ // the composed source that produced it is still in hand.
132
+ //
133
+ // Only ever adds detail: a verdict whose position can't be resolved
134
+ // comes back with its status and message untouched, so nothing about
135
+ // whether a script is disabled depends on any of this.
136
+ export function attachParseLocation(verdict, source) {
137
+ if (!verdict)
138
+ return null;
139
+ if (verdict.status !== 'syntax-error')
140
+ return { status: verdict.status, message: verdict.message };
141
+ const hit = selectParseLocation({
142
+ verdictMessage: verdict.message,
143
+ probes: verdict.probes,
144
+ lineCount: source.lineCount,
145
+ });
146
+ if (!hit)
147
+ return { status: verdict.status, message: verdict.message };
148
+ const origin = lookupGeneratedLine(source.segments, hit.line);
149
+ // No segment covers the line, so it belongs to code WE emitted — the
150
+ // first line we add, or a seam between concatenated files.
151
+ // Reporting a raw generated line there would point the user at a
152
+ // line of their file that has nothing to do with the error, so drop
153
+ // the position — and with it the probe's message, which only earned
154
+ // its place by describing the line we are no longer naming. What's
155
+ // left is the verdict's own message, from the compile that actually
156
+ // decided this script is broken.
157
+ if (!origin)
158
+ return { status: verdict.status, message: verdict.message };
159
+ return {
160
+ status: verdict.status,
161
+ // The chosen probe's message. `selectParseLocation` has already
162
+ // substituted the verdict's own when the wrapped probe won, so the
163
+ // two differ only when the raw probe won — and then the raw
164
+ // probe's message is the one that describes the line we're about
165
+ // to name. Pairing one compile's message with another's position
166
+ // is how you get "Unexpected token 'var'" pointing at a line
167
+ // containing no `var`.
168
+ message: hit.message,
169
+ location: { file: origin.url, line: origin.line, column: hit.column },
170
+ };
171
+ }
@@ -0,0 +1,56 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <title>SourceMonkey parse sandbox</title>
6
+ </head>
7
+ <body>
8
+ <!--
9
+ Compiles userscript source WITHOUT running it, so a syntax error can be
10
+ reported at install time instead of at injection time.
11
+
12
+ Why this page exists at all
13
+ ---------------------------
14
+ `new Function(src)` compiles a function body and returns without
15
+ executing it — exactly the check we want, and it is V8's own verdict on
16
+ the same text we are about to inject, not a third-party parser's opinion
17
+ about a similar text.
18
+
19
+ But `new Function` is `eval` as far as CSP is concerned, and the MV3
20
+ extension CSP (`script-src 'self'`) forbids it everywhere an extension
21
+ normally runs: the service worker, extension pages, and offscreen
22
+ documents alike. A **sandboxed** page is the one context that may declare
23
+ `'unsafe-eval'` (see `content_security_policy.sandbox` in manifest.json),
24
+ which is why the compile happens here and nowhere else.
25
+
26
+ Why that is safe
27
+ ----------------
28
+ - The verdict compile is never called. `new Function` returns a function
29
+ object we drop on the floor.
30
+ - Locating the error does insert a `<script>` element, which normally
31
+ means running it — so each probe has an unmatched `(` appended, making
32
+ the probe script impossible to parse and therefore impossible to
33
+ execute. A parse failure aborts the whole script before evaluation
34
+ begins, so not one statement runs. See `PROBE_SENTINEL` in
35
+ parse-sandbox.js. (This is also why this page's CSP — in manifest.json,
36
+ under content_security_policy.sandbox — allows 'unsafe-inline', which
37
+ is Chrome's own default for sandboxed pages.)
38
+ - The probe text is set with `textContent`, never `innerHTML`, so a
39
+ closing script tag inside a user's source cannot break out of the
40
+ element. Nothing in this page ever writes HTML; that is the invariant
41
+ that makes 'unsafe-inline' cost nothing here, so keep it that way.
42
+ - A sandboxed page has an opaque origin and no `chrome.*` access, so even
43
+ if some construct did execute it could not reach extension APIs, our
44
+ storage, or the user's pages.
45
+ - It talks only to its embedder (the offscreen document) over
46
+ postMessage, and the embedder is the only thing that can reach us.
47
+
48
+ Untrusted input, deliberately
49
+ -----------------------------
50
+ Every byte here is attacker-controlled in the sense that matters: a
51
+ userscript bundle can contain anything. That is the point — we are asking
52
+ "would V8 accept this?" about text we did not write.
53
+ -->
54
+ <script src="extension/parse-sandbox.js"></script>
55
+ </body>
56
+ </html>
@@ -0,0 +1,155 @@
1
+ // Phase timing for the paths a user can perceive as slow.
2
+ //
3
+ // Exists because "the refresh felt slow" is not something the rest of
4
+ // our diagnostics can answer. The Log tab records what a refresh
5
+ // DECIDED (loaded / no change / failed); nothing recorded where its
6
+ // time went, so every question about cost had to be answered by reading
7
+ // the code and guessing which phase dominated.
8
+ //
9
+ // Gated on the same debug-logging toggle as `dlog` (Options → Debug
10
+ // logging), and a disabled timer is a shared no-op object — no clock
11
+ // reads, no allocation, nothing on the hot path. `performance.now()`
12
+ // rather than `Date.now()` for sub-millisecond resolution on phases
13
+ // that are genuinely fast.
14
+ //
15
+ // **One line per phase group, not one per phase.** A refresh that
16
+ // printed a line per step would bury the comparison the reader is
17
+ // actually making — which phase is the big one — in a scroll.
18
+ //
19
+ // **Timers do not nest across contexts.** A collection Refresh runs in
20
+ // the collections PAGE, while the registration re-sync it triggers runs
21
+ // in the service worker, so their lines land in two different consoles.
22
+ // See [docs/diagnostics.md](../docs/diagnostics.md) for how to read the
23
+ // pair together.
24
+ import { isDebugLoggingEnabled } from './debug-log.js';
25
+ import { formatBytes } from './script-display.js';
26
+ const NOOP = {
27
+ mark() { },
28
+ note() { },
29
+ done() { },
30
+ };
31
+ class ActiveTimer {
32
+ label;
33
+ start = performance.now();
34
+ last = this.start;
35
+ // Time spent computing details, left out of the phases and the total:
36
+ // sizing a big read means serializing it, which would otherwise be
37
+ // charged to the very step being measured.
38
+ overhead = 0;
39
+ phases = [];
40
+ notes = [];
41
+ constructor(label) {
42
+ this.label = label;
43
+ }
44
+ mark(phase, detail) {
45
+ const now = performance.now();
46
+ const text = typeof detail === 'function' ? detail() : detail;
47
+ this.phases.push(`${phase} ${round(now - this.last)}${text ? ` [${text}]` : ''}`);
48
+ this.last = performance.now();
49
+ this.overhead += this.last - now;
50
+ }
51
+ note(text) {
52
+ this.notes.push(typeof text === 'function' ? text() : text);
53
+ }
54
+ done() {
55
+ const total = round(performance.now() - this.start - this.overhead);
56
+ // Phases are a breakdown OF the total, so they read as a
57
+ // parenthetical after it rather than as a separate list.
58
+ const breakdown = this.phases.length > 0 ? ` (${this.phases.join(', ')})` : '';
59
+ const notes = this.notes.length > 0 ? `; ${this.notes.join(', ')}` : '';
60
+ console.log(`[SourceMonkey] timing: ${this.label}: total ${total}ms${breakdown}${notes}`);
61
+ }
62
+ }
63
+ // Whole milliseconds once a phase is big enough that the fraction is
64
+ // noise, one decimal below that — a phase reported as `0ms` reads as
65
+ // "didn't run" when it means "too fast to matter", and the difference
66
+ // is exactly what a reader chasing a slow refresh needs.
67
+ function round(ms) {
68
+ return ms >= 10 ? Math.round(ms) : Math.round(ms * 10) / 10;
69
+ }
70
+ // Same rounding for a duration a caller measured itself and wants to
71
+ // put in a `note`, so the two halves of one line read alike.
72
+ export function formatMs(ms) {
73
+ return `${round(ms)}ms`;
74
+ }
75
+ // What a storage read returned or a write sent, for a `mark` detail:
76
+ // how many keys, and their size as JSON (about what Chrome stores).
77
+ // Serializes them, so it is for the small reads and writes of one
78
+ // operation, inside a detail thunk. A full scan asks Chrome for its
79
+ // size instead (storage-scan.ts).
80
+ export function storageSize(items) {
81
+ const keys = Object.keys(items).length;
82
+ return `${keys} key${keys === 1 ? '' : 's'}, ${formatBytes(JSON.stringify(items).length)}`;
83
+ }
84
+ // The clock, for a caller timing something finer-grained than a phase —
85
+ // one operation inside a phase that also contains waiting, say. Reads
86
+ // zero when logging is off, which keeps the same promise the no-op
87
+ // timer makes: nothing measured costs anything when nobody is looking.
88
+ // With the flag steady, a disabled duration comes out as 0 and the
89
+ // `note` that would print it is never called.
90
+ //
91
+ // Toggling the flag mid-operation gives that duration a meaningless
92
+ // value (one end measured against a zero origin). Deliberately not
93
+ // guarded: it needs a human flipping the setting inside a single
94
+ // refresh, and the cost is one wrong debug number.
95
+ export function timingNow() {
96
+ return isDebugLoggingEnabled() ? performance.now() : 0;
97
+ }
98
+ export function startPhaseTimer(label) {
99
+ return isDebugLoggingEnabled() ? new ActiveTimer(label) : NOOP;
100
+ }
101
+ let totals = null;
102
+ // Begin (or restart) accumulation. No-op when debug logging is off, so
103
+ // the leaf calls stay free.
104
+ export function resetPhaseTotals() {
105
+ totals = isDebugLoggingEnabled() ? new Map() : null;
106
+ }
107
+ // Run `fn`, adding its elapsed time to `label`'s bucket. Returns
108
+ // whatever `fn` returns; a throw still records the time, since work
109
+ // that failed still cost what it cost.
110
+ export async function timePhase(label, fn) {
111
+ // The map is captured, NOT re-read in the `finally`. Two things go
112
+ // wrong otherwise, and one of them is severe:
113
+ //
114
+ // - `reportPhaseTotals` nulls the global, and a concurrent pass can
115
+ // do that while this call is still awaiting. The `finally` would
116
+ // then dereference null and throw a TypeError OUT OF A FINALLY,
117
+ // which discards the wrapped call's successful result and rejects
118
+ // in its place. These wrap `attachFiles`, `buildScriptFromText`
119
+ // and the dep fetches, so a diagnostic racing itself would fail
120
+ // the whole collection refresh — with debug logging on, i.e.
121
+ // exactly when someone is diagnosing a slow refresh.
122
+ // - Even without the throw, a sample would land in whichever pass
123
+ // happened to be current when it FINISHED rather than the one it
124
+ // started in.
125
+ //
126
+ // TypeScript won't catch this: the `if (!totals)` narrowing doesn't
127
+ // survive the `await`, so reading the global in the `finally` type-
128
+ // checks fine.
129
+ const bucket = totals;
130
+ if (!bucket)
131
+ return fn();
132
+ const start = performance.now();
133
+ try {
134
+ return await fn();
135
+ }
136
+ finally {
137
+ const entry = bucket.get(label) ?? { ms: 0, n: 0 };
138
+ entry.ms += performance.now() - start;
139
+ entry.n += 1;
140
+ bucket.set(label, entry);
141
+ }
142
+ }
143
+ // Emit one line, largest bucket first — the ordering IS the finding.
144
+ export function reportPhaseTotals(label) {
145
+ // Cleared even when there is nothing to say, so a pass that recorded
146
+ // nothing doesn't leave its map open for the next one to append to.
147
+ const finished = totals;
148
+ totals = null;
149
+ if (!finished || finished.size === 0)
150
+ return;
151
+ const parts = [...finished.entries()]
152
+ .sort((a, b) => b[1].ms - a[1].ms)
153
+ .map(([name, t]) => `${name} ${round(t.ms)}ms/${t.n}`);
154
+ console.log(`[SourceMonkey] timing: ${label} cumulative (concurrent, sums past wall clock): ${parts.join(', ')}`);
155
+ }