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,199 @@
1
+ // How one extension page opens another.
2
+ //
3
+ // Two of our pages are *named tabs*: Collections and Options each have
4
+ // at most one tab by default, and every link to them reuses it rather
5
+ // than stacking up copies. A viewer tab is not named — it is per-script,
6
+ // and having several open at once is the point.
7
+ //
8
+ // The rules this module exists to keep in one place:
9
+ // - A link to a named page opens in a NEW tab, never in the tab the
10
+ // user is reading. Leaving the viewer to look at a collection and
11
+ // losing the viewer is the behaviour this replaces.
12
+ // - "New tab" means the named one where there already is one: focus
13
+ // it and tell it where to scroll, instead of opening a second.
14
+ // - A modified click (Ctrl/Cmd/Shift/middle) is the user overriding
15
+ // us, and is left to the browser — which is why every one of these
16
+ // links is a real anchor with a real `href`, not a button.
17
+ //
18
+ // Usable from an extension page and from the service worker: both can
19
+ // call `chrome.tabs` and `chrome.runtime.getContexts`, and the worker
20
+ // passes the window it wants explicitly (see `openCollectionsPage` in
21
+ // src/extension/background.ts).
22
+ const PAGE_FILES = {
23
+ collections: 'collections.html',
24
+ options: 'options.html',
25
+ };
26
+ // Sent to a named tab that was reused rather than opened. A tab that is
27
+ // already loaded does not act on a fragment we write into its URL, so
28
+ // the scroll has to be asked for directly.
29
+ export const NAMED_TAB_FOCUS_MESSAGE = 'sm:named-tab-focus';
30
+ // The page-relative URL of a named page, for an anchor's `href`. Every
31
+ // extension page lives at the extension root, so the browser resolves
32
+ // it against the current page and the `chrome-extension://<id>/` prefix
33
+ // stays out of the markup.
34
+ export function namedPageUrl(page, hash) {
35
+ return hash ? `${PAGE_FILES[page]}#${hash}` : PAGE_FILES[page];
36
+ }
37
+ // Focus the named tab for `page` — reusing the open one where there is
38
+ // one — and land it on `hash`.
39
+ export async function openNamedTab(page, opts = {}) {
40
+ const hash = opts.hash ?? '';
41
+ const url = chrome.runtime.getURL(namedPageUrl(page, hash));
42
+ const windowId = opts.windowId !== undefined ? opts.windowId : await currentWindowId();
43
+ const existing = await findNamedTab(page, windowId, opts.sameWindowOnly === true);
44
+ if (!existing) {
45
+ await chrome.tabs.create(windowId == null ? { url } : { url, windowId });
46
+ return;
47
+ }
48
+ await chrome.tabs.update(existing.tabId, { active: true });
49
+ // Only when it is somewhere else: the current window is already
50
+ // focused, and asking again is a no-op at best.
51
+ if (existing.windowId != null && existing.windowId !== windowId) {
52
+ try {
53
+ await chrome.windows.update(existing.windowId, { focused: true });
54
+ }
55
+ catch {
56
+ // A window that closed between the lookup and here. The tab
57
+ // activation above is the part that mattered.
58
+ }
59
+ }
60
+ if (hash || !opts.preserveScroll)
61
+ await notifyNamedTabFocus(page, hash);
62
+ }
63
+ // The open tab showing `page`, preferring one in `windowId`.
64
+ //
65
+ // getContexts, not tabs.query({url}): filtering a query by URL needs
66
+ // permission for that URL, and `<all_urls>` does not cover the
67
+ // `chrome-extension:` scheme — the query comes back empty for our own
68
+ // pages and we'd open a duplicate tab every time. getContexts needs no
69
+ // permission and reports tabId/windowId directly. It only sees loaded
70
+ // contexts, so a named tab still navigating is missed; the cost of that
71
+ // is one extra tab.
72
+ async function findNamedTab(page, windowId, sameWindowOnly) {
73
+ const base = chrome.runtime.getURL(PAGE_FILES[page]);
74
+ let contexts;
75
+ try {
76
+ contexts = await chrome.runtime.getContexts({ contextTypes: [chrome.runtime.ContextType.TAB] });
77
+ }
78
+ catch {
79
+ // Nothing to reuse that we can prove; opening a tab is the safe
80
+ // failure.
81
+ return null;
82
+ }
83
+ const matches = contexts.filter((ctx) => (ctx.tabId != null && ctx.tabId >= 0
84
+ && typeof ctx.documentUrl === 'string' && isPageUrl(ctx.documentUrl, base)));
85
+ const here = windowId == null ? undefined : matches.find((ctx) => ctx.windowId === windowId);
86
+ const pick = here ?? (sameWindowOnly ? undefined : matches[0]);
87
+ return pick ? { tabId: pick.tabId, windowId: pick.windowId } : null;
88
+ }
89
+ // Whether a document URL is this page, with or without a query string
90
+ // and a fragment — `?first_run=1` and `#collection-…` variants are the
91
+ // same tab. The delimiter check is what keeps a prefix match from
92
+ // claiming a future `collections-archive.html`.
93
+ function isPageUrl(documentUrl, base) {
94
+ if (!documentUrl.startsWith(base))
95
+ return false;
96
+ const rest = documentUrl.slice(base.length);
97
+ return rest === '' || rest.startsWith('?') || rest.startsWith('#');
98
+ }
99
+ // This page's window, so a link prefers the named tab the user can
100
+ // already see. Null in the worker, and on any page Chrome won't name a
101
+ // tab for.
102
+ async function currentWindowId() {
103
+ try {
104
+ const tab = await chrome.tabs.getCurrent();
105
+ return tab?.windowId ?? null;
106
+ }
107
+ catch {
108
+ return null;
109
+ }
110
+ }
111
+ // Tell the reused tab where to go. Broadcast rather than addressed at
112
+ // the tab: a page cannot name itself as a message target, and the only
113
+ // listeners are the named pages themselves — where a second Collections
114
+ // tab exists, both scrolling to the same collection is the harmless
115
+ // answer.
116
+ async function notifyNamedTabFocus(page, hash) {
117
+ const message = { type: NAMED_TAB_FOCUS_MESSAGE, page, hash };
118
+ try {
119
+ await chrome.runtime.sendMessage(message);
120
+ }
121
+ catch {
122
+ // No listener answered — the tab is mid-load and will read the
123
+ // fragment out of its own URL instead. Not worth reporting.
124
+ }
125
+ }
126
+ // Register a named page's handler for "you were reused; go here". The
127
+ // argument is the fragment without its `#`, empty for the top.
128
+ export function onNamedTabFocus(page, handler) {
129
+ chrome.runtime.onMessage.addListener((message) => {
130
+ const msg = message;
131
+ if (!msg || msg.type !== NAMED_TAB_FOCUS_MESSAGE || msg.page !== page)
132
+ return undefined;
133
+ handler(typeof msg.hash === 'string' ? msg.hash : '');
134
+ // No reply, and no `true`: holding the channel open for an answer
135
+ // we never send makes the sender wait for the timeout.
136
+ return undefined;
137
+ });
138
+ }
139
+ // Turn an anchor into a link to a named page: a real href (so the
140
+ // browser's own open-in-new-tab gestures work and the URL is
141
+ // copyable), and a plain left click routed through `openNamedTab`.
142
+ export function wireNamedTabLink(a, page, hash) {
143
+ a.href = namedPageUrl(page, hash);
144
+ // The href alone would open a new tab; the handler below is what
145
+ // makes it the NAMED one. `target` is what a modified click and a
146
+ // pre-script click both fall back to.
147
+ a.target = '_blank';
148
+ a.rel = 'noopener';
149
+ a.addEventListener('click', (ev) => {
150
+ if (!isPlainClick(ev))
151
+ return;
152
+ ev.preventDefault();
153
+ void openNamedTab(page, { hash });
154
+ });
155
+ }
156
+ // The header bar's product name and icon, on every page that is not the
157
+ // collections page, are a link home. One delegated listener rather than
158
+ // per-element wiring: the viewer repaints its heading (and the link
159
+ // inside it) on every in-place navigation.
160
+ export function installHomeLinkHandler() {
161
+ document.addEventListener('click', (ev) => {
162
+ if (!isPlainClick(ev))
163
+ return;
164
+ const target = ev.target;
165
+ if (!(target instanceof Element))
166
+ return;
167
+ if (!target.closest('a.page-home'))
168
+ return;
169
+ ev.preventDefault();
170
+ void openNamedTab('collections');
171
+ });
172
+ }
173
+ // A click we are allowed to reinterpret: primary button, no modifier.
174
+ // Anything else is the user asking the browser for a new tab, a new
175
+ // window, or a download, and the anchor's own href serves that.
176
+ function isPlainClick(ev) {
177
+ return !ev.defaultPrevented && ev.button === 0
178
+ && !ev.ctrlKey && !ev.metaKey && !ev.shiftKey && !ev.altKey;
179
+ }
180
+ // Open an extension page in a plain new tab — no reuse, because the
181
+ // destination is per-script (a viewer or an install preview) and two of
182
+ // them is a normal thing to want.
183
+ //
184
+ // Returns the promise rather than dropping it: an unhandled rejection
185
+ // in an extension page is reported into chrome://extensions/?errors,
186
+ // where it reads as SourceMonkey being broken.
187
+ export function openExtensionTab(url) {
188
+ return chrome.tabs.create({ url }).then(() => undefined);
189
+ }
190
+ // Open an extension page in a new background tab, as a middle click or
191
+ // Ctrl/Cmd-click on a link does.
192
+ export function openBackgroundTab(url) {
193
+ return chrome.tabs.create({ url, active: false }).then(() => undefined);
194
+ }
195
+ // Open an extension page in a new window, as a Shift-click on a link
196
+ // does.
197
+ export function openExtensionWindow(url) {
198
+ return chrome.windows.create({ url }).then(() => undefined);
199
+ }
@@ -0,0 +1,70 @@
1
+ // Page-side compile-only parse check, for the install preview: it
2
+ // compiles a script it hasn't added yet, before the user decides, so
3
+ // there's no stored row for the store's checker to run on.
4
+ //
5
+ // Every stored row is checked in the service worker, which runs every
6
+ // refresh, add and install (docs/worker-writes.md), through the
7
+ // offscreen document.
8
+ //
9
+ // A page has a DOM, so it embeds its own `parse-sandbox.html` and
10
+ // compiles locally — `parse-sandbox-host.ts` does the driving, exactly
11
+ // as it does for the offscreen document on the service worker's
12
+ // behalf. The service worker is not involved: no wake, and no copy of
13
+ // the script across contexts, before the user has even chosen to
14
+ // install.
15
+ //
16
+ // Only the transport differs from the service worker's checker. Both
17
+ // compose the checked text with `buildParseCheckSource`, so both judge
18
+ // the same program.
19
+ //
20
+ // Fails open like every other leg of this feature (see
21
+ // [docs/parse-check.md]): a sandbox that won't load, a timeout, a
22
+ // verdict in an unrecognised shape, or a throw anywhere in between all
23
+ // resolve to `null` = "couldn't check", never to "didn't parse".
24
+ import { buildParseCheckSource } from '../composed-source.js';
25
+ import { dlog } from '../debug-log.js';
26
+ import { attachParseLocation } from '../parse-location.js';
27
+ import { isParseCheckDisabled } from '../parse-check-protocol.js';
28
+ import { createParseSandboxHost } from './parse-sandbox-host.js';
29
+ export const checkScriptParses = async (script) => {
30
+ if (await isParseCheckDisabled())
31
+ return null;
32
+ try {
33
+ const source = buildParseCheckSource(script);
34
+ const verdicts = await ensureHost().compile([source.text]);
35
+ // Resolved here rather than by the caller: `source` is what maps a
36
+ // reported line back to a file, and this is the last point where
37
+ // we still hold it.
38
+ return verdicts ? attachParseLocation(verdicts[0], source) : null;
39
+ }
40
+ catch (err) {
41
+ dlog(`[SourceMonkey] parse check unavailable in this page: ${String(err)}`);
42
+ return null;
43
+ }
44
+ };
45
+ // One frame per page, created on the first check rather than at
46
+ // startup: a page that never adds or refreshes anything (the common
47
+ // case for the viewer) never pays for it. Once created it stays for
48
+ // the life of the page — checks come in bursts (a Refresh All is one
49
+ // per script), and re-creating the frame per check would pay the load
50
+ // and readiness handshake every time.
51
+ let host = null;
52
+ function ensureHost() {
53
+ if (host)
54
+ return host;
55
+ const frame = document.createElement('iframe');
56
+ // `src`, not `srcdoc`: the URL has to resolve to the page listed in
57
+ // the manifest's `sandbox.pages`, or the frame inherits this page's
58
+ // CSP and loses the `'unsafe-eval'` that is the entire point.
59
+ frame.src = 'parse-sandbox.html';
60
+ // Belt-and-braces, mirroring offscreen.html: the page is already
61
+ // sandboxed by the manifest key, and this keeps the frame from
62
+ // silently becoming a privileged extension page — with `chrome.*`
63
+ // reachable from whatever it compiles — if that key is ever dropped.
64
+ frame.setAttribute('sandbox', 'allow-scripts');
65
+ frame.hidden = true;
66
+ frame.style.display = 'none';
67
+ document.body.append(frame);
68
+ host = createParseSandboxHost(frame);
69
+ return host;
70
+ }
@@ -0,0 +1,161 @@
1
+ // Service-worker side of the compile-only parse check.
2
+ //
3
+ // ---------- What this answers ----------
4
+ //
5
+ // "Would V8 accept the code we are about to register?" — asked at
6
+ // install / refresh time rather than discovered at injection time.
7
+ //
8
+ // Before this existed, a `@require` with a syntax error (a truncated
9
+ // CDN response, an HTML error page that squeaked past the content-type
10
+ // gate) made the whole concatenated entry fail to parse. The seal
11
+ // entry caught it and logged "Script source failed to parse (a
12
+ // @require or the script body has a syntax error); nothing was run" —
13
+ // but only on a page the script matched. Checking up front turns that
14
+ // into an `invalid` status on the collections page, next to the script,
15
+ // the moment it is added or refreshed.
16
+ //
17
+ // The verdict is about the whole composed program, so on its own it
18
+ // says nothing about WHERE. The sandbox answers that separately, by
19
+ // compiling the same text a second way to get a line and column out of
20
+ // V8, and `parse-location.ts` maps that line back to the `@require`
21
+ // (or the body) it was written in.
22
+ //
23
+ // This is the SERVICE WORKER's route to the sandbox. Extension pages
24
+ // have a DOM and embed their own sandbox frame instead — see
25
+ // `parse-check-client.ts`. Both share `parse-sandbox-host.ts`.
26
+ //
27
+ // ---------- Why the SW route takes three contexts ----------
28
+ //
29
+ // `new Function(src)` compiles without executing, which is exactly the
30
+ // check — and it is V8's own verdict on the same text we inject, not a
31
+ // bundled parser's opinion about a similar text. But it is `eval` as
32
+ // far as CSP is concerned, and MV3's extension CSP forbids that in the
33
+ // service worker, in extension pages, and in offscreen documents.
34
+ //
35
+ // A **sandboxed page** may declare `'unsafe-eval'`, and is the only
36
+ // context that may. It also has an opaque origin and no `chrome.*`, so
37
+ // it cannot be messaged by the service worker directly. Hence:
38
+ //
39
+ // service worker → offscreen document → sandboxed iframe
40
+ // (chrome.*, (chrome.* + DOM, ('unsafe-eval',
41
+ // no DOM, no eval) no chrome.*)
42
+ // no eval)
43
+ //
44
+ // Considered and rejected: bundling a real parser (acorn, vendored the
45
+ // way Prism is). It needs no offscreen machinery and runs synchronously
46
+ // in the worker — but it parses a *Program* where our injected code is
47
+ // a *function body*, so it disagrees with V8 on top-level `return`,
48
+ // `await` and `new.target`. Those appear in real userscripts, and a
49
+ // parser that rejects code V8 accepts would flip working scripts to
50
+ // invalid. Being wrong in that direction is much worse than the setup
51
+ // cost here.
52
+ //
53
+ // ---------- Failing open ----------
54
+ //
55
+ // Every failure of this machinery itself — no `chrome.offscreen`, the
56
+ // document won't open, the sandbox never answers — resolves to "I
57
+ // don't know", never to "didn't parse". A script that works must not
58
+ // be marked invalid because our plumbing broke. The seal entry is
59
+ // still in place as the backstop, so a parse failure we miss here is
60
+ // reported at run time exactly as it was before.
61
+ import { dlog } from '../debug-log.js';
62
+ import { PARSE_CHECK_MSG } from '../parse-check-protocol.js';
63
+ const OFFSCREEN_URL = 'offscreen.html';
64
+ // How long to wait for the sandbox before giving up on a batch. The
65
+ // compile itself is fast (V8 parsing a few hundred KB), so this is a
66
+ // guard against the offscreen document being torn down mid-flight
67
+ // rather than a realistic budget. Kept short because opportunistic
68
+ // refresh runs on the navigation path: a wedged check should cost a
69
+ // moment, not ten seconds per pass.
70
+ const PARSE_TIMEOUT_MS = 3_000;
71
+ // After this many consecutive failures, stop trying for the rest of
72
+ // the service-worker's life. Without it, a permanently broken sandbox
73
+ // costs every refresh a full timeout per script forever — a
74
+ // degraded-capability path that gets slower the more scripts you have
75
+ // is worse than one that turns itself off.
76
+ const FAILURE_LIMIT = 3;
77
+ let consecutiveFailures = 0;
78
+ // `chrome.offscreen.createDocument` rejects if one already exists, and
79
+ // there is no "create if absent" call. Serializing through one promise
80
+ // keeps two concurrent refreshes from racing into that rejection.
81
+ let creating = null;
82
+ // True when this browser can run the check at all. Chrome gained
83
+ // `chrome.offscreen` in 109; we don't support anything near that old,
84
+ // but the guard also covers test environments and any future context
85
+ // where the API is absent.
86
+ export function parseCheckAvailable() {
87
+ return typeof chrome !== 'undefined' && !!chrome.offscreen;
88
+ }
89
+ // Reset the circuit breaker. Exposed for tests, which need a clean
90
+ // slate between cases that deliberately break the machinery.
91
+ export function resetParseCheckFailures() {
92
+ consecutiveFailures = 0;
93
+ }
94
+ async function ensureOffscreenDocument() {
95
+ if (creating)
96
+ return creating;
97
+ creating = (async () => {
98
+ // hasDocument() landed alongside the API; getContexts() is the
99
+ // newer spelling. Prefer whichever exists rather than assuming.
100
+ const existing = await hasOffscreenDocument();
101
+ if (existing)
102
+ return;
103
+ await chrome.offscreen.createDocument({
104
+ url: OFFSCREEN_URL,
105
+ reasons: [chrome.offscreen.Reason.IFRAME_SCRIPTING],
106
+ justification: 'Compile userscript source in a sandboxed iframe to detect syntax errors without running it.',
107
+ });
108
+ })().finally(() => {
109
+ creating = null;
110
+ });
111
+ return creating;
112
+ }
113
+ async function hasOffscreenDocument() {
114
+ const runtime = chrome.runtime;
115
+ if (runtime.getContexts) {
116
+ const contexts = await runtime.getContexts({ contextTypes: ['OFFSCREEN_DOCUMENT'] });
117
+ return contexts.length > 0;
118
+ }
119
+ const offscreen = chrome.offscreen;
120
+ return offscreen.hasDocument ? offscreen.hasDocument() : false;
121
+ }
122
+ // Compile each source independently. Returns one verdict per input, or
123
+ // null when the check could not be performed at all — callers must
124
+ // treat null as "unknown", never as failure. See "Failing open" above.
125
+ export async function checkParses(sources) {
126
+ if (sources.length === 0)
127
+ return [];
128
+ if (!parseCheckAvailable())
129
+ return null;
130
+ if (consecutiveFailures >= FAILURE_LIMIT)
131
+ return null;
132
+ try {
133
+ await ensureOffscreenDocument();
134
+ // Correlating a reply with its request is the sandbox host's job
135
+ // (`parse-sandbox-host.ts`) — this leg is a single awaited
136
+ // sendMessage, so there is nothing here to mix up.
137
+ const reply = await withTimeout(chrome.runtime.sendMessage({ type: PARSE_CHECK_MSG, sources }), PARSE_TIMEOUT_MS);
138
+ const verdicts = reply?.verdicts;
139
+ if (!Array.isArray(verdicts) || verdicts.length !== sources.length) {
140
+ dlog('[SourceMonkey] parse check returned no usable verdicts');
141
+ consecutiveFailures++;
142
+ return null;
143
+ }
144
+ consecutiveFailures = 0;
145
+ return verdicts;
146
+ }
147
+ catch (err) {
148
+ // console.info, not .warn/.error: this is a degraded-capability
149
+ // path, not an extension bug, and Chrome forwards warn/error into
150
+ // chrome://extensions/?errors.
151
+ console.info('[SourceMonkey] parse check unavailable', err);
152
+ consecutiveFailures++;
153
+ return null;
154
+ }
155
+ }
156
+ function withTimeout(promise, ms) {
157
+ return new Promise((resolve, reject) => {
158
+ const timer = setTimeout(() => reject(new Error('parse check timed out')), ms);
159
+ promise.then((v) => { clearTimeout(timer); resolve(v); }, (e) => { clearTimeout(timer); reject(e); });
160
+ });
161
+ }
@@ -0,0 +1,172 @@
1
+ // Drives a sandboxed parse page over `postMessage`, for any context
2
+ // that has a DOM.
3
+ //
4
+ // The compile itself must happen in a sandboxed page — `new Function`
5
+ // is `eval` as far as CSP is concerned, and a sandboxed page is the
6
+ // only place allowed to declare `'unsafe-eval'` (see
7
+ // [docs/parse-check.md]). A sandboxed page has an opaque origin and no
8
+ // `chrome.*`, so it can only be reached by whoever embeds it.
9
+ //
10
+ // Two contexts embed one:
11
+ //
12
+ // - `offscreen.ts`, on behalf of the service worker. The SW has no
13
+ // DOM, so it cannot host an iframe at all; the offscreen document
14
+ // exists to be the DOM it lacks.
15
+ // - extension pages (`parse-check-client.ts`). A page already has a
16
+ // DOM, so it embeds its own frame and skips the service worker
17
+ // entirely — no wake, no cross-context clone of the script, and no
18
+ // dependency on the SW being alive mid-request.
19
+ //
20
+ // Both get the same behaviour from here: readiness, correlation,
21
+ // timeouts, verdict validation, and the circuit breaker.
22
+ import { PARSE_CHECK_MSG, PARSE_CHECK_RESULT_MSG, PARSE_SANDBOX_READY_MSG, } from '../parse-check-protocol.js';
23
+ // Per-request ceiling. The compile is fast (V8 parsing a few hundred
24
+ // KB), so this guards against the frame being torn down mid-flight
25
+ // rather than budgeting real work. It also stops `pending` from
26
+ // growing a dead resolver per abandoned request — an offscreen
27
+ // document outlives service-worker restarts, so "leaks slowly,
28
+ // forever" is a real shape there.
29
+ //
30
+ // Longer than the service worker's own `PARSE_TIMEOUT_MS`
31
+ // (`parse-check.ts`), deliberately: the SW is on the navigation path
32
+ // and gives up early, this is the last resort that keeps `pending`
33
+ // bounded. On the SW route that means the SW usually abandons a stuck
34
+ // request before this fires, so the breaker below mostly counts
35
+ // failures on the page route.
36
+ const REQUEST_TIMEOUT_MS = 8_000;
37
+ // Readiness cannot be observed from outside. A sandboxed frame is
38
+ // opaque-origin, so `frame.contentDocument` is ALWAYS null and its
39
+ // `readyState` is unreadable — an "is it loaded?" fast path looks
40
+ // reasonable and can never fire. That leaves two signals, and we take
41
+ // whichever arrives first:
42
+ //
43
+ // - the sandbox's own ready ping, sent as the last thing it does;
44
+ // - the iframe's `load` event, in case the ping was posted before
45
+ // the listener existed.
46
+ //
47
+ // The timeout is the third leg, and the important one: without it a
48
+ // missed signal wedges readiness forever and every check silently
49
+ // times out for the rest of the session. Resolving anyway lets the
50
+ // request through — the sandbox is almost certainly up, and if it
51
+ // isn't, the per-request timeout reports "couldn't check" rather than
52
+ // lying.
53
+ const FRAME_READY_TIMEOUT_MS = 2_000;
54
+ // After this many consecutive failures, stop trying. Without it a
55
+ // permanently broken sandbox costs every refresh a full timeout per
56
+ // script forever — a degraded-capability path that gets slower the
57
+ // more scripts you have is worse than one that turns itself off.
58
+ //
59
+ // Per host, not global: one wedged page no longer speaks for the whole
60
+ // extension, which is part of what embedding per-context buys.
61
+ const FAILURE_LIMIT = 3;
62
+ // ...but the trip is not permanent. The offscreen document is never
63
+ // torn down and is reused for the life of the browser session, so a
64
+ // sticky breaker there would mean three unlucky timeouts disable the
65
+ // check until the browser restarts — including after the sandbox
66
+ // recovers. Instead a tripped host lets one probe request through per
67
+ // cooldown: if it succeeds the breaker resets, if it fails the host
68
+ // goes quiet for another cooldown. The cost of staying wedged stays
69
+ // bounded, and recovery costs one timeout every 30s rather than never
70
+ // happening.
71
+ const BREAKER_COOLDOWN_MS = 30_000;
72
+ export function createParseSandboxHost(frame) {
73
+ // Requests waiting on the sandbox, by correlation id.
74
+ const pending = new Map();
75
+ let consecutiveFailures = 0;
76
+ let breakerTrippedAt = 0;
77
+ const frameReady = new Promise((resolve) => {
78
+ // Named and removed rather than inline: readiness is answered once,
79
+ // but this listener would otherwise keep type-checking every
80
+ // message reaching the context for as long as it lives.
81
+ const onReadyMessage = (event) => {
82
+ if (event.source !== frame.contentWindow)
83
+ return;
84
+ if (event.data?.type === PARSE_SANDBOX_READY_MSG)
85
+ done();
86
+ };
87
+ const done = () => {
88
+ window.removeEventListener('message', onReadyMessage);
89
+ resolve();
90
+ };
91
+ window.addEventListener('message', onReadyMessage);
92
+ frame.addEventListener('load', done, { once: true });
93
+ setTimeout(done, FRAME_READY_TIMEOUT_MS);
94
+ });
95
+ window.addEventListener('message', (event) => {
96
+ // Only the sandboxed frame can reach an offscreen document in
97
+ // practice, but a normal extension page is a richer target — check
98
+ // the source so nothing else can resolve a request.
99
+ if (event.source !== frame.contentWindow)
100
+ return;
101
+ const data = event.data;
102
+ if (!data || data.type !== PARSE_CHECK_RESULT_MSG)
103
+ return;
104
+ if (typeof data.id !== 'string' || !Array.isArray(data.verdicts))
105
+ return;
106
+ const resolve = pending.get(data.id);
107
+ if (!resolve)
108
+ return;
109
+ pending.delete(data.id);
110
+ // A verdict we don't recognise is not a verdict. `applyParseCheck`
111
+ // treats anything that isn't `ok`/`unknown` as `invalid`, so an
112
+ // unexpected status string would DISABLE the script — exactly what
113
+ // the three-state design exists to prevent.
114
+ resolve(data.verdicts.every(isParseVerdict) ? data.verdicts : null);
115
+ });
116
+ // Stamp the trip on the failure that reaches the limit, so the
117
+ // cooldown runs from when the host went quiet.
118
+ const noteFailure = () => {
119
+ consecutiveFailures++;
120
+ if (consecutiveFailures === FAILURE_LIMIT)
121
+ breakerTrippedAt = Date.now();
122
+ };
123
+ return {
124
+ async compile(sources) {
125
+ if (sources.length === 0)
126
+ return [];
127
+ if (consecutiveFailures >= FAILURE_LIMIT) {
128
+ if (Date.now() - breakerTrippedAt < BREAKER_COOLDOWN_MS)
129
+ return null;
130
+ // Probe: let this one through, and re-arm the cooldown now so a
131
+ // second caller during the probe doesn't get a second probe.
132
+ breakerTrippedAt = Date.now();
133
+ }
134
+ await frameReady;
135
+ const win = frame.contentWindow;
136
+ if (!win) {
137
+ // The frame went away (document teardown racing a request).
138
+ noteFailure();
139
+ return null;
140
+ }
141
+ // Random rather than a counter: an offscreen document outlives
142
+ // service-worker restarts, and a counter restarts at zero with
143
+ // it. A reused id can resolve a NEW request with an OLD
144
+ // request's verdicts, which is the one way this machinery could
145
+ // mark a good script invalid.
146
+ const id = crypto.randomUUID();
147
+ const verdicts = await new Promise((resolve) => {
148
+ const timer = setTimeout(() => {
149
+ pending.delete(id);
150
+ resolve(null);
151
+ }, REQUEST_TIMEOUT_MS);
152
+ pending.set(id, (v) => { clearTimeout(timer); resolve(v); });
153
+ win.postMessage({ type: PARSE_CHECK_MSG, id, sources }, '*');
154
+ });
155
+ if (!verdicts || verdicts.length !== sources.length) {
156
+ noteFailure();
157
+ return null;
158
+ }
159
+ consecutiveFailures = 0;
160
+ return verdicts;
161
+ },
162
+ };
163
+ }
164
+ const PARSE_STATUSES = ['ok', 'syntax-error', 'unknown'];
165
+ // Exported for the unit test: this predicate is the last thing between
166
+ // an unrecognised status string and `applyParseCheck` disabling the
167
+ // script it describes, so it is worth testing directly rather than
168
+ // only through a sandbox that would have to be made to misbehave.
169
+ export function isParseVerdict(value) {
170
+ const status = value?.status;
171
+ return PARSE_STATUSES.includes(status);
172
+ }