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,458 @@
1
+ // Waiting: the `wait` command, and the settling that it and the waits
2
+ // after `start`, loads and actions share.
3
+ //
4
+ // A tab has settled when nothing sm-dev can see is still
5
+ // happening in it. A browser has no signal for "the scripts are done":
6
+ // JavaScript runs on events, and a page can keep timers, intervals and
7
+ // sockets going for good. So settled is a set of signals all quiet
8
+ // together for a short window, counted from when the wait began:
9
+ // - the document has loaded;
10
+ // - each script the document runs whose rules cover the page has
11
+ // reported (started, skipped or thrown);
12
+ // - no `GM_xmlhttpRequest` of its scripts is in flight;
13
+ // - no request of the page's is in flight;
14
+ // - its DOM hasn't changed;
15
+ // - and then its main thread has a moment with nothing to do.
16
+ // Requests open over a few seconds are taken for long-lived ones (a
17
+ // socket, a stream, a long poll), which would keep a page from ever
18
+ // settling. Timers are not waited for: they are set up to run later. A
19
+ // short one that matters changes the DOM when it fires, which the
20
+ // window catches, since it starts no earlier than the wait.
21
+ import { MAX_TIMER_SECONDS, TIMEOUT_NEEDS_SECONDS, timeoutSecondsMs } from './command-list.js';
22
+ import { MISTAKE_PREFIX, quoted, WAIT_FAILED } from './output-style.js';
23
+ import { countOf, pickFrame, refFrameProblem } from './page-commands.js';
24
+ import { explainMatch, preparedScriptName } from './prepare.js';
25
+ import { ALL_LOG_SOURCES, foundAt, inSources, linesLost, parseLogSources, parsePosition, searchedUpTo, streamText, textMatcher } from './run-logs.js';
26
+ import { refProblem } from './snapshot.js';
27
+ import { currentTab, dialogWaitingMessage, NO_TABS, pickTab, TAB_ARG, tabName } from './tab-refs.js';
28
+ /** How long every signal must stay quiet. */
29
+ const QUIET_MS = 200;
30
+ /** How long a wait lasts before it says what is still busy. */
31
+ export const SETTLE_TIMEOUT_MS = 5000;
32
+ // A request open this long counts as long-lived.
33
+ const LONG_REQUEST_MS = 3000;
34
+ // The longest one look into the page may take: a page busy in a long
35
+ // script doesn't answer, which counts as busy.
36
+ const LOOK_MS = 1000;
37
+ const POLL_MS = 50;
38
+ // How often `wait selector` looks again, and the longest one look may
39
+ // take: a page busy in a long script, or stopped on a dialog, doesn't
40
+ // answer.
41
+ const SELECTOR_POLL_MS = 100;
42
+ const SELECTOR_LOOK_MS = 1000;
43
+ // How the replies of the waits with something to match start, so each
44
+ // says plainly whether it got it.
45
+ const WAIT_DONE = '[sm-dev] Wait done: ';
46
+ const LOG_MATCHED = '[sm-dev] Wait matched log line: ';
47
+ /** Waits for `tab` to settle, up to `timeoutMs`. */
48
+ export async function settleTab(tab, host, timeoutMs = SETTLE_TIMEOUT_MS) {
49
+ const started = Date.now();
50
+ const deadline = started + timeoutMs;
51
+ // Each look bounded by the time left too, so a short timeout holds.
52
+ const look = () => Math.max(POLL_MS, Math.min(LOOK_MS, deadline - Date.now()));
53
+ for (;;) {
54
+ if (tab.page.isClosed())
55
+ return { settled: false, closed: true };
56
+ if (tab.dialogs?.current)
57
+ return { settled: false, dialog: true };
58
+ let busy = await busyWith(tab, host, look(), started);
59
+ if (busy.length === 0) {
60
+ // Last, once the rest is quiet: the main thread free, and nothing
61
+ // changed while it got there. A document that went away meanwhile
62
+ // just makes the next look busy.
63
+ await within(tab.link.idle(look()).catch(() => undefined), look());
64
+ busy = await busyWith(tab, host, look(), started);
65
+ if (busy.length === 0)
66
+ return { settled: true };
67
+ }
68
+ if (Date.now() >= deadline) {
69
+ return tab.page.isClosed() ? { settled: false, closed: true } : { settled: false, busy };
70
+ }
71
+ await sleep(Math.min(POLL_MS, Math.max(0, deadline - Date.now())));
72
+ }
73
+ }
74
+ /**
75
+ * What still keeps `tab` from counting as settled, each starting with
76
+ * words of ours, lowercase, as the message lists them; empty when
77
+ * nothing does. `started`: when the wait began, which the quiet window
78
+ * can't start before, so what an action sets off a moment later is
79
+ * waited for even on a page that was quiet.
80
+ */
81
+ async function busyWith(tab, host, lookMs, started) {
82
+ const busy = [];
83
+ const { link } = tab;
84
+ const activity = await within(link.activity().catch(() => null), lookMs);
85
+ if (activity === 'timeout')
86
+ return ['the page\'s JavaScript is busy'];
87
+ if (activity === null)
88
+ return tab.page.isClosed() ? [] : ['the page has no document yet'];
89
+ if (activity.readyState !== 'complete')
90
+ busy.push('the page is still loading');
91
+ // The scripts the document runs report once there; one whose rules
92
+ // don't cover the page may say nothing (as on about:blank), and has
93
+ // nothing to wait for anyway. Reports from the document's first
94
+ // span count, so one the back/forward cache restored, where the
95
+ // scripts don't run again, isn't waited on.
96
+ const url = tab.page.url();
97
+ const scripts = link.scriptsInDocument().filter((s) => s.id && explainMatch(s.matching, url).matches);
98
+ const waiting = host.unreported(link, scripts.map((s) => s.id), link.documentFirstStart());
99
+ if (waiting.length > 0) {
100
+ busy.push(`scripts not started yet: ${scripts.filter((s) => waiting.includes(s.id)).map(preparedScriptName).join(', ')}`);
101
+ }
102
+ const now = Date.now();
103
+ const xhrs = host.reports.requests.filter((r) => !r.outcome && host.pageOf(r) === link
104
+ && r.at >= link.documentFirstStart() && now - r.at < LONG_REQUEST_MS);
105
+ if (xhrs.length > 0)
106
+ busy.push(`${count(xhrs.length, 'GM_xmlhttpRequest')} pending (${urls(xhrs.map((r) => r.url))})`);
107
+ const network = link.network();
108
+ const pending = network.pending.filter((r) => r.ms < LONG_REQUEST_MS);
109
+ if (pending.length > 0)
110
+ busy.push(`${count(pending.length, 'request')} pending (${urls(pending.map((r) => r.url))})`);
111
+ else if (network.sinceLast < QUIET_MS)
112
+ busy.push('a request just finished');
113
+ if (activity.sinceDom < QUIET_MS)
114
+ busy.push(`the DOM is still changing${activity.lastChanged ? ` (${activity.lastChanged})` : ''}`);
115
+ // Nothing else to say, but the window hasn't passed since the wait
116
+ // began (only a timeout shorter than the window shows it).
117
+ if (busy.length === 0 && now - started < QUIET_MS)
118
+ busy.push('the quiet moment it waits for hasn\'t passed yet');
119
+ return busy;
120
+ }
121
+ /**
122
+ * After a load, an action or `start`: each tab settling, all at once.
123
+ * One that doesn't in time is said, without failing anything, since
124
+ * what came before worked. A dialog or a closed tab says so elsewhere,
125
+ * and a tab whose load already gave up (`stalled`) isn't waited on
126
+ * again.
127
+ */
128
+ export async function settleTabs(host, tabs, out, timeoutMs = SETTLE_TIMEOUT_MS) {
129
+ // Only real tabs: a caller's `newTab` (a test's) may return none.
130
+ await Promise.all(tabs.filter((t) => t?.page && !t.stalled).map(async (t) => {
131
+ const outcome = await settleTab(t, host, timeoutMs);
132
+ if (!outcome.settled && 'busy' in outcome)
133
+ out(`[sm-dev] ${notSettledMessage(t, timeoutMs, outcome.busy)}`);
134
+ }));
135
+ }
136
+ /** How long a load (`open`, `reload`, the tabs `start` opens) may take. */
137
+ export const LOAD_TIMEOUT_MS = 30_000;
138
+ /**
139
+ * A command's `--timeout-seconds` in ms, for each wait it does in place
140
+ * of that wait's default: undefined when not given, null (said why)
141
+ * when not a number of seconds.
142
+ */
143
+ export function timeoutFlagMs(flags, out) {
144
+ const timeout = flags['--timeout-seconds'];
145
+ if (timeout === undefined)
146
+ return undefined;
147
+ const ms = timeoutSecondsMs(timeout);
148
+ if (ms === null)
149
+ out(`${MISTAKE_PREFIX}${TIMEOUT_NEEDS_SECONDS}`);
150
+ return ms;
151
+ }
152
+ // How often a load looks for a page that stopped itself.
153
+ const STOPPED_POLL_MS = 100;
154
+ /** A navigation whose page didn't finish loading in time. */
155
+ export class PageNotLoaded extends Error {
156
+ }
157
+ /**
158
+ * Runs a navigation (`page.goto`, `page.reload`) until the new page
159
+ * has loaded. A page that stops its own load with `window.stop()`
160
+ * counts as loaded: its readyState goes to complete, but it never
161
+ * fires `load`, which a plain `goto` waits for until it times out.
162
+ * Rejects with the navigation's own error, or when the page doesn't
163
+ * load in time.
164
+ */
165
+ export async function loadPage(page, navigate, timeoutMs = LOAD_TIMEOUT_MS) {
166
+ const deadline = Date.now() + timeoutMs;
167
+ await navigate({ waitUntil: 'commit', timeout: timeoutMs });
168
+ let ended = false;
169
+ const load = page.waitForLoadState('load', { timeout: Math.max(1, deadline - Date.now()) }).then(() => true, (err) => {
170
+ if (err.name === 'TimeoutError')
171
+ return false;
172
+ throw err;
173
+ }).finally(() => { ended = true; });
174
+ // Reads readyState in a later task than the one that set it, so a
175
+ // load that did fire has run its listeners by then too.
176
+ const stopped = (async () => {
177
+ while (!ended && Date.now() < deadline) {
178
+ if (await page.evaluate(() => document.readyState).catch(() => null) === 'complete')
179
+ return true;
180
+ await sleep(STOPPED_POLL_MS);
181
+ }
182
+ return false;
183
+ })();
184
+ try {
185
+ if (!await Promise.race([load, stopped]))
186
+ throw new PageNotLoaded(`The page did not finish loading within ${seconds(timeoutMs)}`);
187
+ }
188
+ finally {
189
+ ended = true;
190
+ load.catch(() => undefined);
191
+ }
192
+ }
193
+ /** The message when `tab` didn't settle in time, as every wait says it. */
194
+ export function notSettledMessage(tab, timeoutMs, busy) {
195
+ const list = busy.join('; ');
196
+ return `T${tab.n} did not settle within ${seconds(timeoutMs)}: ${list.charAt(0).toUpperCase()}${list.slice(1)}`;
197
+ }
198
+ // ---------- The wait command ----------
199
+ /**
200
+ * `wait`: until a tab settles, an element appears or goes, or an output
201
+ * line shows up; or a plain delay. `parsed`: its words, as its syntax
202
+ * reads them (command-args.ts). False when it gave up or was invalid,
203
+ * so a line of several commands stops there.
204
+ */
205
+ export async function waitFor(ctx, parsed) {
206
+ const mistake = (text) => {
207
+ ctx.out(`${MISTAKE_PREFIX}${text}`);
208
+ return false;
209
+ };
210
+ const timeout = parsed.flags['--timeout-seconds'];
211
+ const given = timeoutFlagMs(parsed.flags, ctx.out);
212
+ if (given === null)
213
+ return false;
214
+ const timeoutMs = given ?? SETTLE_TIMEOUT_MS;
215
+ if (parsed.form === 'log') {
216
+ if (parsed.tab !== undefined)
217
+ return mistake('The `wait log` command takes no tab, since it watches all of the run\'s output');
218
+ return waitForLog(ctx, parsed, timeoutMs);
219
+ }
220
+ // A plain delay. A tab or `all` before it (`wait T2 3s`) is allowed
221
+ // and has no effect: a delay waits the same whichever tab is named.
222
+ // So is --timeout-seconds, which a long delay gets out of habit, as
223
+ // long as it doesn't cut the delay short.
224
+ // A named tab must still exist, so a mistyped one is said.
225
+ const delayWord = parsed.words.length === 2 && parsed.words[0] === 'all' && parsed.tab === undefined
226
+ ? parsed.words[1] : parsed.words.length === 1 ? parsed.words[0] : undefined;
227
+ const delay = delayWord !== undefined ? parseDelay(delayWord) : null;
228
+ if (delay !== null) {
229
+ if (delay > MAX_TIMER_SECONDS * 1000)
230
+ return mistake(`A delay can be at most ${MAX_TIMER_SECONDS}s`);
231
+ if (timeout !== undefined && delay > timeoutMs)
232
+ return mistake(`The delay ${delayWord} is longer than --timeout-seconds ${timeout}. Give a timeout at least as long, or none.`);
233
+ if (parsed.tab !== undefined && !pickTab(ctx, parsed.tab))
234
+ return false;
235
+ await new Promise((resolve) => setTimeout(resolve, delay));
236
+ return true;
237
+ }
238
+ const [word, ...more] = parsed.words;
239
+ if (more.length > 0) {
240
+ // `wait all selector .x`: `all` isn't a tab, so the form wasn't taken.
241
+ if (word === 'all' && (more[0] === 'selector' || more[0] === 'selector-gone')) {
242
+ return mistake(`The wait ${more[0]} command looks in one tab. Name it (T2), or leave it out for the current one.`);
243
+ }
244
+ const valid = word === 'all' || parseDelay(word) !== null;
245
+ return mistake(`The ${quoted('wait')} command takes a tab, ${quoted('all')}, ${quoted('selector')}, ${quoted('selector-gone')}, ${quoted('log')}, or a delay such as ${quoted('500ms')} or ${quoted('2s')}, not ${quoted(valid ? more.join(' ') : word)}`);
246
+ }
247
+ // The tab, or all of them.
248
+ let tabs;
249
+ if (parsed.tab !== undefined) {
250
+ const picked = pickTab(ctx, parsed.tab);
251
+ if (!picked)
252
+ return false;
253
+ tabs = [picked];
254
+ }
255
+ else if (word === 'all') {
256
+ tabs = ctx.tabs();
257
+ }
258
+ else {
259
+ const current = currentTab(ctx);
260
+ tabs = current ? [current] : [];
261
+ }
262
+ if (tabs.length === 0) {
263
+ ctx.out(NO_TABS);
264
+ return false;
265
+ }
266
+ const kind = parsed.form;
267
+ if (kind === 'selector' || kind === 'selector-gone') {
268
+ const selector = parsed.text ?? '';
269
+ if (!selector)
270
+ return mistake(`The ${quoted(`wait ${kind}`)} command needs a selector, e.g. ${quoted(`wait ${kind} '#results'`)}`);
271
+ const [first, ...more] = selector.split(/\s+/);
272
+ if (more.length > 0 && TAB_ARG.test(first))
273
+ return mistake(`The tab goes before the word ${kind}: ${quoted(`wait ${first} ${kind} <css>`)}`);
274
+ const frame = await pickFrame(parsed.flags['--frame'], tabs[0], (line) => ctx.out(`[sm-dev] ${line}`));
275
+ if (frame === null)
276
+ return false;
277
+ return waitForSelector(ctx, tabs[0], selector, kind === 'selector', timeoutMs, frame);
278
+ }
279
+ if (word === undefined || word === 'all')
280
+ return waitToSettle(ctx, tabs, timeoutMs);
281
+ return mistake(`The ${quoted('wait')} command takes a tab, ${quoted('all')}, ${quoted('selector')}, ${quoted('selector-gone')}, ${quoted('log')}, or a delay such as ${quoted('500ms')} or ${quoted('2s')}, not ${quoted(word)}`);
282
+ }
283
+ // `wait [T<n>|all]`: each tab settling, all at once.
284
+ async function waitToSettle(ctx, tabs, timeoutMs) {
285
+ const started = Date.now();
286
+ const outcomes = await Promise.all(tabs.map((t) => settleTab(t, ctx.host, timeoutMs)));
287
+ let ok = true;
288
+ outcomes.forEach((outcome, i) => {
289
+ if (outcome.settled)
290
+ return;
291
+ ok = false;
292
+ const t = tabs[i];
293
+ ctx.out('dialog' in outcome ? `${WAIT_FAILED}${dialogWaitingMessage(t, currentTab(ctx))}`
294
+ : 'closed' in outcome ? `${WAIT_FAILED}${tabName(t)} closed`
295
+ : `[sm-dev] ${notSettledMessage(t, timeoutMs, outcome.busy)}`);
296
+ });
297
+ if (ok) {
298
+ const took = seconds(Date.now() - started);
299
+ ctx.out(`[sm-dev] ${tabs.length === 1 ? `${tabName(tabs[0])} settled` : 'All tabs settled'} in ${took}`);
300
+ }
301
+ return ok;
302
+ }
303
+ // `wait selector` / `wait selector-gone`: looks again until it holds.
304
+ // Each look is bounded, so the timeout holds, and a dialog that opens
305
+ // meanwhile ends the wait rather than stopping it for good.
306
+ async function waitForSelector(ctx, tab, selector, present, timeoutMs, frame) {
307
+ // In an iframe, the frame on its tab: `T1.F3`.
308
+ const name = `${tabName(tab)}${frame ? `.F${tab.link.host.frameIds.number(frame)}` : ''}`;
309
+ // A ref from a snapshot of another page matches nothing, which would
310
+ // pass for gone.
311
+ const problem = await within((async () => await refProblem(tab.page, selector)
312
+ ?? await refFrameProblem(tab.page, selector, frame, tab.link.host.frameIds))().catch(() => null), SELECTOR_LOOK_MS);
313
+ if (typeof problem === 'string' && problem !== 'timeout') {
314
+ ctx.out(`${WAIT_FAILED}${problem}`);
315
+ return false;
316
+ }
317
+ const deadline = Date.now() + timeoutMs;
318
+ for (;;) {
319
+ if (tab.page.isClosed()) {
320
+ ctx.out(`${WAIT_FAILED}${tabName(tab)} closed`);
321
+ return false;
322
+ }
323
+ if (frame?.isDetached()) {
324
+ ctx.out(`${WAIT_FAILED}${name} has gone from the page`);
325
+ return false;
326
+ }
327
+ if (tab.dialogs?.current) {
328
+ ctx.out(`${WAIT_FAILED}${dialogWaitingMessage(tab, currentTab(ctx))}`);
329
+ return false;
330
+ }
331
+ const count = await within(countOf(tab.page, selector, frame), Math.max(1, Math.min(SELECTOR_LOOK_MS, deadline - Date.now())));
332
+ if (typeof count === 'string' && count !== 'timeout') {
333
+ ctx.out(`${WAIT_FAILED}The selector ${selector} can't be used: ${count}`);
334
+ return false;
335
+ }
336
+ if (typeof count === 'number' && (present ? count > 0 : count === 0)) {
337
+ ctx.out(`${WAIT_DONE}The selector ${selector} ${present ? 'matches' : 'matches nothing'} on ${name}`);
338
+ return true;
339
+ }
340
+ if (Date.now() >= deadline) {
341
+ const limit = seconds(timeoutMs);
342
+ ctx.out(`${WAIT_FAILED}The selector ${selector} ${present ? `matched nothing on ${name} within` : `still matched on ${name} after`} ${limit}`);
343
+ return false;
344
+ }
345
+ await sleep(SELECTOR_POLL_MS);
346
+ }
347
+ }
348
+ // `wait log`: a log line (see run-logs.ts) of the sources asked for (by
349
+ // default all of them), with the text or matching the /regex/, logged
350
+ // since the previous command began (or since `--since`) or from now on.
351
+ // Matched in the form the stream prints it, whole.
352
+ async function waitForLog(ctx, parsed, timeoutMs) {
353
+ const mistake = (text) => {
354
+ ctx.out(`${MISTAKE_PREFIX}${text}`);
355
+ return false;
356
+ };
357
+ const { flags } = parsed;
358
+ const typed = parsed.text ?? '';
359
+ const flag = flags['--find'];
360
+ if (typed && typeof flag === 'string')
361
+ return mistake('The `wait log` command takes its text either after `log` or with `--find`, not both');
362
+ const text = typeof flag === 'string' ? flag : typed;
363
+ if (!text)
364
+ return mistake('The `wait log` command needs the text to look for, e.g. `wait log ready`');
365
+ const logs = ctx.logs;
366
+ if (!logs) {
367
+ ctx.out(`${WAIT_FAILED}This run cannot watch its log`);
368
+ return false;
369
+ }
370
+ const matcher = textMatcher(text, '');
371
+ if ('error' in matcher)
372
+ return mistake(matcher.error);
373
+ const sources = typeof flags['--log-source'] === 'string' ? parseLogSources(flags['--log-source']) : ALL_LOG_SOURCES;
374
+ if ('error' in sources)
375
+ return mistake(sources.error);
376
+ const given = typeof flags['--since'] === 'string' ? parsePosition(flags['--since']) : undefined;
377
+ if (typeof given === 'object')
378
+ return mistake(given.error);
379
+ const since = given ?? ctx.previousStart ?? 0;
380
+ const fits = (record) => inSources(record, sources) && matcher.matches(streamText(record, true));
381
+ const found = (record) => {
382
+ ctx.out(`${LOG_MATCHED}${streamText(record, true)}`);
383
+ ctx.out(foundAt(record.id));
384
+ return true;
385
+ };
386
+ if (given !== undefined) {
387
+ const lost = logs.lostAfter(given);
388
+ if (lost !== undefined)
389
+ ctx.out(linesLost(lost));
390
+ }
391
+ const earlier = logs.since(since).find(fits);
392
+ if (earlier)
393
+ return found(earlier);
394
+ const later = await new Promise((resolve) => {
395
+ const timer = setTimeout(() => {
396
+ stop();
397
+ resolve(null);
398
+ }, timeoutMs);
399
+ const stop = logs.watch((record) => {
400
+ if (!fits(record))
401
+ return;
402
+ clearTimeout(timer);
403
+ stop();
404
+ resolve(record);
405
+ });
406
+ });
407
+ // Seen as it streamed, if its source streams; said again, so the
408
+ // reply says it was found.
409
+ if (later)
410
+ return found(later);
411
+ ctx.out(`${WAIT_FAILED}No log line matched ${matcher.shown} within ${seconds(timeoutMs)}`);
412
+ ctx.out(searchedUpTo(logs.position()));
413
+ return false;
414
+ }
415
+ // ---------- Helpers ----------
416
+ /**
417
+ * A plain delay as typed, `100ms` or `5s` (`1.5s` too), in milliseconds;
418
+ * null for anything else.
419
+ */
420
+ export function parseDelay(text) {
421
+ const m = /^(\d+(?:\.\d+)?)(ms|s)$/.exec(text);
422
+ if (!m)
423
+ return null;
424
+ return Math.round(Number(m[1]) * (m[2] === 's' ? 1000 : 1));
425
+ }
426
+ /** A duration as the messages say it: `5s`, `0.3s`. */
427
+ export function seconds(ms) {
428
+ return `${Number((ms / 1000).toFixed(1))}s`;
429
+ }
430
+ function count(n, noun) {
431
+ return `${n} ${noun}${n === 1 ? '' : 's'}`;
432
+ }
433
+ // A few URLs, each shortened to its path, and a count of the rest.
434
+ function urls(list) {
435
+ const shown = list.slice(0, 3).map((url) => {
436
+ try {
437
+ const u = new URL(url);
438
+ return u.protocol.startsWith('http') ? `${u.pathname}${u.search}`.slice(0, 80) : url.slice(0, 80);
439
+ }
440
+ catch {
441
+ return url.slice(0, 80);
442
+ }
443
+ });
444
+ return list.length > 3 ? `${shown.join(', ')}, and ${list.length - 3} more` : shown.join(', ');
445
+ }
446
+ /** `work`'s result, or `timeout` once `ms` has passed. */
447
+ export async function within(work, ms) {
448
+ let timer;
449
+ try {
450
+ return await Promise.race([work, new Promise((resolve) => { timer = setTimeout(() => resolve('timeout'), ms); })]);
451
+ }
452
+ finally {
453
+ clearTimeout(timer);
454
+ }
455
+ }
456
+ function sleep(ms) {
457
+ return new Promise((resolve) => setTimeout(resolve, ms));
458
+ }
@@ -0,0 +1,30 @@
1
+ export declare const HTTP_READ_MESSAGE = "sm:http-read";
2
+ export interface HttpReadRequest {
3
+ type: typeof HTTP_READ_MESSAGE;
4
+ url: string;
5
+ as: 'text' | 'bytes';
6
+ headers?: Record<string, string>;
7
+ maxBytes?: number;
8
+ timeoutMs?: number;
9
+ }
10
+ export type HttpReadReply = {
11
+ kind: 'response';
12
+ status: number;
13
+ contentType: string;
14
+ etag: string | null;
15
+ lastModified: string | null;
16
+ finalUrl: string;
17
+ text?: string;
18
+ bodyBase64?: string;
19
+ } | {
20
+ kind: 'error';
21
+ reason: 'timeout' | 'no-response' | 'oversize';
22
+ message?: string;
23
+ };
24
+ export declare function handleHttpReadRequest(req: HttpReadRequest): Promise<HttpReadReply>;
25
+ export declare function mustHopToWorker(): boolean;
26
+ export declare function readHttpViaWorker(req: Omit<HttpReadRequest, 'type'>): Promise<HttpReadReply | null>;
27
+ export declare function replyBytes(reply: {
28
+ text?: string;
29
+ bodyBase64?: string;
30
+ }): ArrayBuffer;
@@ -0,0 +1,142 @@
1
+ // One http(s) read, made where it is safe to make it.
2
+ //
3
+ // ---------- Why this module exists ----------
4
+ //
5
+ // A `fetch()` issued by a DOCUMENT hands that document the response's
6
+ // `Link: <…>; rel=preload; as=script` headers, and Chrome acts on them
7
+ // — it tries to preload the named script into the page that asked. Our
8
+ // pages are extension pages, whose CSP is `script-src 'self'`, so the
9
+ // preload is refused, and Chrome files the refusal under
10
+ // `chrome://extensions/?errors`: an uncatchable error, about a script
11
+ // the page never asked for, that reads as SourceMonkey being broken.
12
+ //
13
+ // Nothing exotic triggers it. Any site that preloads its own bundles
14
+ // sends the header on every page, and we fetch site HTML (the site-icon
15
+ // probe) and arbitrary script URLs (Add / Refresh / the install
16
+ // preview) from pages all the time. A response that turns out to be a
17
+ // login page — the case `fetchHttpText` already has a branch for — is
18
+ // among the most likely of all to carry one.
19
+ //
20
+ // A service worker has no document for a hint to apply to, so the same
21
+ // read there is inert. Hence this module: pages hand the read to the
22
+ // worker and get the response back over `chrome.runtime`. Both
23
+ // directions are pinned by `tests/e2e/preload-hints.spec.ts`.
24
+ //
25
+ // ---------- Shape ----------
26
+ //
27
+ // The worker performs the fetch and reports what came back; every
28
+ // caller keeps its own rules about what the response MEANS (which
29
+ // statuses are failures, which content types are acceptable, how a 304
30
+ // is reported). That split is deliberate: the two callers
31
+ // (`fetchResourceBytes` for bytes, `fetchHttpText` for script and
32
+ // manifest text) disagree about all of it, and a handler that tried to
33
+ // decide for both would have to grow their union.
34
+ //
35
+ // `as: 'text'` decodes in the worker via `Response.text()` rather than
36
+ // shipping bytes for the page to decode, so a response's charset is
37
+ // honoured exactly as it was before the hop existed.
38
+ import { base64ToBytes, bytesToBase64 } from './bytes.js';
39
+ import { isPageContext } from './run-context.js';
40
+ import { ensureGithubAuthTokenLoaded, githubAuthHeaders } from './github-auth.js';
41
+ export const HTTP_READ_MESSAGE = 'sm:http-read';
42
+ // Service-worker side. `background.ts` owns the listener (and the
43
+ // sender check that keeps this from being an open fetch proxy — the
44
+ // extension holds `<all_urls>`).
45
+ export async function handleHttpReadRequest(req) {
46
+ // The worker may have woken up to answer this, with its startup token
47
+ // read still in flight — a private GitHub raw URL would 404 on a race
48
+ // the page-side fetch never had.
49
+ await ensureGithubAuthTokenLoaded();
50
+ const headers = { ...(req.headers ?? {}), ...githubAuthHeaders(req.url) };
51
+ const controller = new AbortController();
52
+ const timer = req.timeoutMs != null
53
+ ? setTimeout(() => controller.abort(), req.timeoutMs)
54
+ : null;
55
+ let response;
56
+ try {
57
+ response = await fetch(req.url, {
58
+ cache: 'no-store',
59
+ redirect: 'follow',
60
+ referrerPolicy: 'no-referrer',
61
+ signal: controller.signal,
62
+ headers,
63
+ });
64
+ }
65
+ catch (err) {
66
+ // Our own timer is the only thing that aborts this request, so an
67
+ // aborted signal identifies the timeout exactly rather than leaving
68
+ // it merged with a network failure.
69
+ return controller.signal.aborted
70
+ ? { kind: 'error', reason: 'timeout' }
71
+ : { kind: 'error', reason: 'no-response', message: errorText(err) };
72
+ }
73
+ finally {
74
+ if (timer != null)
75
+ clearTimeout(timer);
76
+ }
77
+ // Size checks are for a body we are about to read. A 304 has none —
78
+ // its `Content-Length`, where a server sends one at all, describes the
79
+ // entity that WOULD have been sent. Rejecting on it would fail a
80
+ // conditional read as "too large" for a file that didn't change, and
81
+ // only on this leg: the local path tests 304 before any size check, so
82
+ // the two would disagree about the same response.
83
+ const contentLength = response.headers.get('content-length');
84
+ if (response.status !== 304 &&
85
+ req.maxBytes != null && contentLength && Number(contentLength) > req.maxBytes) {
86
+ return { kind: 'error', reason: 'oversize' };
87
+ }
88
+ const base = {
89
+ kind: 'response',
90
+ status: response.status,
91
+ // Verbatim, not lowercased: callers quote it back as evidence of
92
+ // what the server actually sent.
93
+ contentType: response.headers.get('content-type') ?? '',
94
+ etag: response.headers.get('ETag'),
95
+ lastModified: response.headers.get('Last-Modified'),
96
+ finalUrl: response.url || req.url,
97
+ };
98
+ try {
99
+ if (req.as === 'text')
100
+ return { ...base, text: await response.text() };
101
+ const body = await response.arrayBuffer();
102
+ if (req.maxBytes != null && body.byteLength > req.maxBytes) {
103
+ return { kind: 'error', reason: 'oversize' };
104
+ }
105
+ return { ...base, bodyBase64: bytesToBase64(new Uint8Array(body)) };
106
+ }
107
+ catch (err) {
108
+ // The body never finished arriving (connection dropped mid-read).
109
+ return { kind: 'error', reason: 'no-response', message: errorText(err) };
110
+ }
111
+ }
112
+ // True when this context must not fetch http(s) itself — i.e. it is a
113
+ // page rather than the service worker.
114
+ export function mustHopToWorker() {
115
+ return isPageContext();
116
+ }
117
+ // Page side. Returns null — not an error — when the request got no
118
+ // answer at all, so the caller can fall back to fetching locally. That
119
+ // reopens the preload leak for that one read, which beats failing a
120
+ // read the user asked for.
121
+ export async function readHttpViaWorker(req) {
122
+ let reply;
123
+ try {
124
+ reply = await chrome.runtime.sendMessage({
125
+ type: HTTP_READ_MESSAGE,
126
+ ...req,
127
+ });
128
+ }
129
+ catch {
130
+ return null; // no receiving end
131
+ }
132
+ return reply ?? null;
133
+ }
134
+ // The bytes of a `kind: 'response'` reply, whichever form it arrived in.
135
+ export function replyBytes(reply) {
136
+ if (reply.bodyBase64 != null)
137
+ return base64ToBytes(reply.bodyBase64);
138
+ return new TextEncoder().encode(reply.text ?? '').buffer;
139
+ }
140
+ function errorText(err) {
141
+ return err instanceof Error ? err.message : String(err);
142
+ }
@@ -0,0 +1,35 @@
1
+ import { type DirectoryListingCache } from './file-locations.js';
2
+ export declare const LOCAL_ROOTS_KEY = "smLocalFileRoots";
3
+ export interface ParsedRoot {
4
+ lineNumber: number;
5
+ text: string;
6
+ url?: string;
7
+ error?: string;
8
+ }
9
+ export declare function parseLocalRootLines(text: string): ParsedRoot[];
10
+ export declare function rootsFromText(text: string): string[] | null;
11
+ export declare function textFromRoots(roots: readonly string[]): string;
12
+ export declare function loadLocalRootsSetting(): Promise<void>;
13
+ export declare function localRootsReady(): Promise<void>;
14
+ export declare function installLocalRootsListener(): void;
15
+ export declare function readRootsSetting(raw: unknown): string[];
16
+ export declare function setConfiguredLocalRoots(roots: readonly string[]): void;
17
+ export declare function _setLocalRootsForTests(roots: readonly string[]): void;
18
+ export interface LocalFileScope {
19
+ enabled: boolean;
20
+ collectionPath?: string;
21
+ lookup?: LocalLookup;
22
+ }
23
+ export type LocalLookup = 'known' | 'search';
24
+ export declare function localSearchRoots(scriptFileUrl: string, scope: LocalFileScope): string[];
25
+ export declare function collectionSupportsLocalFiles(collectionPath: string): boolean;
26
+ export declare function localFileCandidates(declaredUrl: string, roots: readonly string[]): string[];
27
+ export interface LocalFileCandidate {
28
+ url: string;
29
+ root: string;
30
+ firstSegment: string;
31
+ isLeaf: boolean;
32
+ }
33
+ export declare function localFileCandidatesDetailed(declaredUrl: string, roots: readonly string[]): LocalFileCandidate[];
34
+ export declare function presentLocalFileCandidates(declaredUrl: string, roots: readonly string[], listings: DirectoryListingCache): Promise<string[]>;
35
+ export declare function localCandidateMayExist(c: LocalFileCandidate, listings: DirectoryListingCache): Promise<boolean>;