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,131 @@
1
+ // Tabs whose page doesn't answer, found before `sm-dev start` connects.
2
+ //
3
+ // Playwright's connection isn't ready until it has set up every tab in
4
+ // the browser, and a tab showing a dialog (alert, confirm, prompt, or a
5
+ // "leave site?" prompt) answers nothing until the dialog is closed: not
6
+ // even the call that would report the dialog. So one such tab of the
7
+ // person's holds up the run until Playwright gives up.
8
+ //
9
+ // No call reports a dialog that opened before the connection asking,
10
+ // or answers one (Page.handleJavaScriptDialog says none is showing), so
11
+ // each tab is asked for something trivial, on a connection of its own,
12
+ // and one that stays quiet is named. That can also be a page busy in a
13
+ // long script. (A page paused in the debugger still answers.)
14
+ // How long a page gets to answer, and the browser to take the side
15
+ // connection.
16
+ const ANSWER_MS = 2000;
17
+ const CONNECT_MS = 5000;
18
+ // How often the tabs that didn't are asked again.
19
+ const RECHECK_MS = 1000;
20
+ /**
21
+ * The browser's tabs whose pages don't answer, of `only` when given.
22
+ * Throws when the browser itself can't be reached: the caller's own
23
+ * connect then says so.
24
+ */
25
+ export async function stuckTabs(endpoint, only) {
26
+ // A `ws://` endpoint is the browser's own connection already.
27
+ const url = /^wss?:/i.test(endpoint) ? endpoint
28
+ : (await (await fetch(`${endpoint}/json/version`, { signal: AbortSignal.timeout(CONNECT_MS) })).json()).webSocketDebuggerUrl;
29
+ const ws = new WebSocket(url);
30
+ await new Promise((resolve, reject) => {
31
+ const timer = setTimeout(() => reject(new Error('The browser\'s debugging connection did not open')), CONNECT_MS);
32
+ ws.onopen = () => {
33
+ clearTimeout(timer);
34
+ resolve(undefined);
35
+ };
36
+ ws.onerror = () => {
37
+ clearTimeout(timer);
38
+ reject(new Error('The browser\'s debugging connection could not be opened'));
39
+ };
40
+ }).catch((err) => {
41
+ ws.close();
42
+ throw err;
43
+ });
44
+ let nextId = 1;
45
+ const pending = new Map();
46
+ // Dropped, the connection answers nothing more, which would look like
47
+ // every tab not responding: the check fails instead.
48
+ let dropped = false;
49
+ ws.onclose = () => { dropped = true; };
50
+ ws.onmessage = (e) => {
51
+ const msg = JSON.parse(String(e.data));
52
+ if (msg.id !== undefined)
53
+ pending.get(msg.id)?.(msg);
54
+ };
55
+ // A command's reply, or null when none came within `ms`.
56
+ const call = (method, params, sessionId, ms = ANSWER_MS) => new Promise((resolve) => {
57
+ const id = nextId++;
58
+ const timer = setTimeout(() => {
59
+ pending.delete(id);
60
+ resolve(null);
61
+ }, ms);
62
+ pending.set(id, (msg) => {
63
+ clearTimeout(timer);
64
+ pending.delete(id);
65
+ resolve(msg);
66
+ });
67
+ ws.send(JSON.stringify({ id, method, params, ...(sessionId ? { sessionId } : {}) }));
68
+ });
69
+ try {
70
+ const listed = await call('Target.getTargets', {});
71
+ const targets = (listed?.result?.targetInfos ?? []);
72
+ const pages = targets.filter((t) => t.type === 'page' && (!only || only.has(t.targetId)));
73
+ const stuck = await Promise.all(pages.map(async (t) => {
74
+ const attached = await call('Target.attachToTarget', { targetId: t.targetId, flatten: true });
75
+ const sessionId = attached?.result?.sessionId;
76
+ // Closed meanwhile, or not attachable: Playwright won't wait on it.
77
+ if (!sessionId)
78
+ return null;
79
+ const answer = await call('Runtime.evaluate', { expression: '1' }, sessionId);
80
+ void call('Target.detachFromTarget', { sessionId }, undefined, 500);
81
+ return answer ? null : { targetId: t.targetId, title: t.title, url: t.url };
82
+ }));
83
+ if (dropped)
84
+ throw new Error('The browser\'s debugging connection closed');
85
+ return stuck.filter((t) => t !== null);
86
+ }
87
+ finally {
88
+ ws.close();
89
+ }
90
+ }
91
+ /**
92
+ * Waits until every tab's page answers, saying which don't and why they
93
+ * might not. True at once when they all do, and when the browser can't
94
+ * be reached (the connect that follows says so). False when `until`
95
+ * resolves first (--stop-after-seconds, or the run's output closing).
96
+ */
97
+ export async function waitForStuckTabs(endpoint, out, until) {
98
+ let stuck;
99
+ try {
100
+ stuck = await stuckTabs(endpoint);
101
+ }
102
+ catch {
103
+ return true;
104
+ }
105
+ if (stuck.length === 0)
106
+ return true;
107
+ let stopped = false;
108
+ void until?.then(() => { stopped = true; });
109
+ out(stuck.length === 1
110
+ ? 'A tab is not responding, so sm-dev can\'t connect to the browser over CDP:'
111
+ : 'Some tabs are not responding, so sm-dev can\'t connect to the browser over CDP:');
112
+ // Chrome titles a page without one by its address.
113
+ for (const t of stuck)
114
+ out(` ${t.title && !t.url.endsWith(t.title) ? `${t.title} ` : ''}${t.url}`);
115
+ out(stuck.length === 1
116
+ ? 'It may have a dialog open, or a busy page. Answer the dialog or close the tab to continue (Ctrl-C to abort).'
117
+ : 'They may have a dialog open, or a busy page. Answer the dialogs or close the tabs to continue (Ctrl-C to abort).');
118
+ while (stuck.length > 0) {
119
+ await Promise.race([new Promise((r) => setTimeout(r, RECHECK_MS)), until]);
120
+ if (stopped)
121
+ return false;
122
+ try {
123
+ stuck = await stuckTabs(endpoint, new Set(stuck.map((t) => t.targetId)));
124
+ }
125
+ catch {
126
+ return true;
127
+ }
128
+ }
129
+ out('Every tab is responding now. Connecting.');
130
+ return true;
131
+ }
@@ -0,0 +1,20 @@
1
+ import type { Dialog } from '@playwright/test';
2
+ import type { ConsoleContext, ControlledTab } from './console-commands.js';
3
+ /** Where the page commands have no tab to act on; also `start --notab`'s start. */
4
+ export declare const NO_TABS = "[sm-dev] No tabs are active. Use `newtab` to open one.";
5
+ export declare const TAB_ARG: RegExp;
6
+ export declare function tabName(tab: ControlledTab): string;
7
+ export declare function currentTab(ctx: ConsoleContext): ControlledTab | undefined;
8
+ /**
9
+ * The tab a typed argument names: `T2`, or its CDP target id (or the
10
+ * start of one, if only one open tab's id starts that way), bare or as
11
+ * the output shows it (`[targetId=...]`).
12
+ */
13
+ export declare function pickTab(ctx: ConsoleContext, input: string): ControlledTab | null;
14
+ export declare function dialogKind(dialog: Dialog): string;
15
+ /**
16
+ * What to say when `waiting` has a dialog open and a command can't go
17
+ * on until it's answered. `accept` and `dismiss` act on the current tab
18
+ * (`current`), so another tab's needs picking first.
19
+ */
20
+ export declare function dialogWaitingMessage(waiting: ControlledTab, current: ControlledTab | undefined): string;
@@ -0,0 +1,80 @@
1
+ // Naming and picking a run's tabs, for the commands that take one
2
+ // (console-commands.ts, wait.ts): `T2`, its target id,
3
+ // the current tab, and what to say when one is stopped on a dialog.
4
+ import { MISTAKE_PREFIX, quoted } from './output-style.js';
5
+ /** Where the page commands have no tab to act on; also `start --notab`'s start. */
6
+ export const NO_TABS = '[sm-dev] No tabs are active. Use `newtab` to open one.';
7
+ // A tab named before a page command's own words: `T2`, or an id as the
8
+ // output shows one. A bare hex prefix isn't taken, since it could be a
9
+ // selector.
10
+ export const TAB_ARG = /^(\[?targetId=\S+|[Tt]\d+)$/i;
11
+ export function tabName(tab) {
12
+ return `T${tab.n}`;
13
+ }
14
+ // The tab the page commands act on. One that has closed gives way to
15
+ // T1, said once so a command doesn't land somewhere unexpected.
16
+ export function currentTab(ctx) {
17
+ const open = ctx.tabs();
18
+ if (ctx.tab && !open.includes(ctx.tab)) {
19
+ // With none left, the command's own reply says so.
20
+ if (open[0])
21
+ ctx.out(`[sm-dev] ${tabName(ctx.tab)} has closed, so the page commands now act on ${tabName(open[0])}`);
22
+ ctx.tab = undefined;
23
+ }
24
+ // Remembered, so the tab closing later is noted as above.
25
+ ctx.tab ??= open[0];
26
+ return ctx.tab;
27
+ }
28
+ // Enough of a target id to be taken for one rather than a slip.
29
+ const MIN_TARGET_ID_PREFIX = 6;
30
+ /**
31
+ * The tab a typed argument names: `T2`, or its CDP target id (or the
32
+ * start of one, if only one open tab's id starts that way), bare or as
33
+ * the output shows it (`[targetId=...]`).
34
+ */
35
+ export function pickTab(ctx, input) {
36
+ // As copied from the output: `targetId=ABC...`, brackets and all.
37
+ const arg = input.replace(/^\[?targetId=([0-9a-f]+)\]?$/i, '$1');
38
+ if (arg.length >= MIN_TARGET_ID_PREFIX && /^[0-9a-f]+$/i.test(arg)) {
39
+ const upper = arg.toUpperCase();
40
+ const hits = ctx.tabs().filter((t) => t.targetId?.toUpperCase().startsWith(upper));
41
+ if (hits.length === 1)
42
+ return hits[0];
43
+ if (hits.length > 1) {
44
+ ctx.out(`[sm-dev] Multiple tabs have target ids starting with ${arg} (${hits.map(tabName).join(', ')}). Give more of it.`);
45
+ return null;
46
+ }
47
+ // Only a number could still be a tab slip (`2` for `T2`).
48
+ if (!/^\d+$/.test(arg)) {
49
+ ctx.out(`[sm-dev] No open tab's target id starts with ${quoted(arg)}. Use ${quoted('tabs')} to list them.`);
50
+ return null;
51
+ }
52
+ }
53
+ const m = /^[Tt](\d+)$/.exec(arg);
54
+ if (!m) {
55
+ // A bare number is the likely slip, and has an obvious fix.
56
+ ctx.out(/^\d+$/.test(arg)
57
+ ? `${MISTAKE_PREFIX}Tabs are named T1, T2 and so on. Did you mean ${quoted(`T${arg}`)}?`
58
+ : `${MISTAKE_PREFIX}${quoted(arg)} is not a tab. Tabs are named T1, T2 and so on; use ${quoted('tabs')} to list them.`);
59
+ return null;
60
+ }
61
+ const tab = ctx.tabs().find((t) => t.n === Number(m[1]));
62
+ if (!tab)
63
+ ctx.out(`[sm-dev] There is no open tab T${m[1]}. Use ${quoted('tabs')} to list them.`);
64
+ return tab ?? null;
65
+ }
66
+ export function dialogKind(dialog) {
67
+ const type = dialog.type();
68
+ return `${/^[aeiou]/.test(type) ? 'an' : 'a'} ${type}`;
69
+ }
70
+ /**
71
+ * What to say when `waiting` has a dialog open and a command can't go
72
+ * on until it's answered. `accept` and `dismiss` act on the current tab
73
+ * (`current`), so another tab's needs picking first.
74
+ */
75
+ export function dialogWaitingMessage(waiting, current) {
76
+ const kind = dialogKind(waiting.dialogs.current);
77
+ return waiting === current
78
+ ? `The page is waiting on ${kind} dialog. Use ${quoted('accept')} or ${quoted('dismiss')} first.`
79
+ : `${tabName(waiting)} is waiting on ${kind} dialog. Use ${quoted(`accept ${tabName(waiting)}`)} or ${quoted(`dismiss ${tabName(waiting)}`)} first.`;
80
+ }
@@ -0,0 +1,71 @@
1
+ import { type ExternalManager, type ExternalScript, type InstallOptions } from './external-manager.js';
2
+ /** Tampermonkey in one browser, over a hidden page of its own until `close`. */
3
+ export declare class Tampermonkey implements ExternalManager {
4
+ private readonly page;
5
+ readonly extensionId: string;
6
+ readonly version: string;
7
+ readonly name: "tampermonkey";
8
+ readonly label: string;
9
+ private readonly ids;
10
+ private readonly layouts;
11
+ private listed?;
12
+ private constructor();
13
+ /**
14
+ * Connects to TM in the browser at `endpoint`. Throws saying why when
15
+ * it isn't installed and enabled there. `browserArg`: the flags
16
+ * that name the browser in the advice (see `browserArg` in run.ts).
17
+ */
18
+ static open(endpoint: string, browserArg?: string): Promise<Tampermonkey>;
19
+ /**
20
+ * Makes one call. TM gives no answer to a call it doesn't know, so
21
+ * one without an answer fails naming the call and TM's version.
22
+ */
23
+ call<R>(method: string, fields?: Record<string, unknown>): Promise<R>;
24
+ private list;
25
+ private listAnew;
26
+ private changed;
27
+ private numberOf;
28
+ private uuidOf;
29
+ scripts(): Promise<ExternalScript[]>;
30
+ texts(ids: number[]): Promise<Map<number, string>>;
31
+ install(code: string, options?: InstallOptions): Promise<{
32
+ id: number;
33
+ isNew: boolean;
34
+ }>;
35
+ private mustSaveQuietly;
36
+ setEnabled(id: number, enabled: boolean): Promise<void>;
37
+ uninstall(id: number): Promise<void>;
38
+ remove(ids: number[]): Promise<void>;
39
+ applied(): Promise<boolean>;
40
+ setApplied(on: boolean): Promise<void>;
41
+ allowFilePages(): Promise<boolean>;
42
+ matching(url: string): Promise<number[]>;
43
+ scriptIdOf(url: string): number | undefined;
44
+ get scriptUrlPrefix(): string;
45
+ /**
46
+ * Notes the `@require` files one of the run's scripts was installed
47
+ * with, in order, as `remapStack` needs them: TM runs a script's code
48
+ * after one line of its own and its `@require` files, joined by
49
+ * newlines, with the script's first line on their last.
50
+ */
51
+ noteRequires(id: number, requires: {
52
+ url: string;
53
+ source: string;
54
+ }[]): void;
55
+ /** `text` with the run's scripts' stack lines pointed at the lines of their own files. */
56
+ remapStack(text: string): string;
57
+ close(): Promise<void>;
58
+ private settings;
59
+ private scriptOf;
60
+ }
61
+ /**
62
+ * `code` as TM is given it. A script saved from TM's dashboard has no
63
+ * URL of its own, so TM can't resolve a relative `@require` or
64
+ * `@resource`; each one is rewritten to the URL it resolves to from
65
+ * `url`, a local file's under `sm-dev.invalid`. Each line rewritten is
66
+ * kept as it was in a comment at the end, before sm-dev's mark, which
67
+ * `restoredText` puts back.
68
+ */
69
+ export declare function rewrittenText(code: string, url: string): string;
70
+ /** `text` with the header lines `rewrittenText` changed as they were, and without its comments. */
71
+ export declare function restoredText(text: string): string;
@@ -0,0 +1,356 @@
1
+ // Tampermonkey (TM), driven by the messages its own dashboard sends its
2
+ // background: `{method, ...}` through `chrome.runtime.sendMessage`,
3
+ // from a hidden page of TM's own (`options.html`), since TM answers no
4
+ // other sender. Not a published API: what each call takes and gives is
5
+ // listed in docs/sm-dev-tampermonkey.md, which the version check
6
+ // (tests/tm-api/tm-api.spec.ts) tries against a fetched TM.
7
+ import { installedManagers, openHiddenPage } from './browser-extensions.js';
8
+ import { installUrlOf } from './external-manager.js';
9
+ import { MANAGERS } from './managers.js';
10
+ import { withoutMark } from './script-mark.js';
11
+ import { TmIds } from './tm-ids.js';
12
+ import { getValue, getValues, parseHeader } from '../userscript-header.js';
13
+ import { quoted } from './output-style.js';
14
+ const OPTIONS_PAGE = 'options.html';
15
+ // How long one listing of every script, code included, answers again.
16
+ // A command asks for the list, then the texts, then the count, so this
17
+ // saves reading them all three times. Any call that changes something
18
+ // drops it.
19
+ const LIST_REUSE_MS = 1000;
20
+ // The calls that only read, so keep the listing.
21
+ const READS = new Set(['loadTree', 'determineScriptsToRun']);
22
+ // The comment keeping a header line sm-dev rewrote, as it was, by its
23
+ // line's index: `// sm-dev was: 5 // @require lib.js`. Written before
24
+ // sm-dev's mark (script-mark.ts), at the end, so no line moves.
25
+ // `[^\n]`, not `.`, so a line's `\r` (a CRLF file) stays with it.
26
+ const WAS = /^\/\/ sm-dev was: (\d+) ([^\n]*)$/;
27
+ /** Tampermonkey in one browser, over a hidden page of its own until `close`. */
28
+ export class Tampermonkey {
29
+ page;
30
+ extensionId;
31
+ version;
32
+ name = 'tampermonkey';
33
+ label = MANAGERS.tampermonkey.label;
34
+ ids = new TmIds();
35
+ // How far each of the run's scripts' lines are moved, by uuid (see `noteRequires`).
36
+ layouts = new Map();
37
+ listed;
38
+ constructor(page, extensionId, version) {
39
+ this.page = page;
40
+ this.extensionId = extensionId;
41
+ this.version = version;
42
+ }
43
+ /**
44
+ * Connects to TM in the browser at `endpoint`. Throws saying why when
45
+ * it isn't installed and enabled there. `browserArg`: the flags
46
+ * that name the browser in the advice (see `browserArg` in run.ts).
47
+ */
48
+ static async open(endpoint, browserArg = '') {
49
+ const label = MANAGERS.tampermonkey.label;
50
+ const copies = (await installedManagers(endpoint)).filter((m) => m.manager === 'tampermonkey');
51
+ const copy = copies.find((m) => m.enabled);
52
+ if (!copy) {
53
+ throw new Error(`${label} is ${copies.length > 0 ? 'turned off' : 'not installed'} in this browser. Use ${quoted(`sm-dev${browserArg} --manager tm manager ${copies.length > 0 ? 'enable' : 'install'}`)}.`);
54
+ }
55
+ let page;
56
+ try {
57
+ page = await openHiddenPage(endpoint, `chrome-extension://${copy.id}/${OPTIONS_PAGE}`, 'document.readyState === "complete" && typeof chrome?.runtime?.sendMessage === "function"');
58
+ }
59
+ catch (err) {
60
+ throw new Error(`${label} ${copy.version}'s dashboard could not be opened: ${err.message}`, { cause: err });
61
+ }
62
+ return new Tampermonkey(page, copy.id, copy.version);
63
+ }
64
+ /**
65
+ * Makes one call. TM gives no answer to a call it doesn't know, so
66
+ * one without an answer fails naming the call and TM's version.
67
+ */
68
+ async call(method, fields = {}) {
69
+ if (!READS.has(method))
70
+ this.listed = undefined;
71
+ let answer;
72
+ try {
73
+ answer = await this.page.evaluate(`chrome.runtime.sendMessage(${JSON.stringify({ method, ...fields })})`);
74
+ }
75
+ catch (err) {
76
+ throw new Error(`${this.label} ${this.version}'s ${method} call failed: ${err.message.replace(/^Error: /, '')}`, { cause: err });
77
+ }
78
+ if (answer === undefined || answer === null) {
79
+ throw new Error(`${this.label} ${this.version} doesn't answer its ${method} call, which sm-dev uses. This version may have changed it.`);
80
+ }
81
+ const error = answer.error;
82
+ if (error)
83
+ throw new Error(`${this.label} ${this.version}'s ${method} call failed: ${String(error)}`);
84
+ return answer;
85
+ }
86
+ // Every installed script, with its text as TM keeps it.
87
+ list() {
88
+ if (this.listed && Date.now() - this.listed.at < LIST_REUSE_MS)
89
+ return this.listed.scripts;
90
+ const scripts = this.listAnew();
91
+ this.listed = { at: Date.now(), scripts };
92
+ // A failed listing isn't kept for the next.
93
+ scripts.catch(() => {
94
+ if (this.listed?.scripts === scripts)
95
+ this.listed = undefined;
96
+ });
97
+ return scripts;
98
+ }
99
+ async listAnew() {
100
+ const tree = await this.call('loadTree', { referrer: 'options.scripts.userscripts', complete: true });
101
+ if (!Array.isArray(tree.items))
102
+ throw this.changed('loadTree', 'no script list');
103
+ // The rest are the dashboard's own entries.
104
+ const scripts = tree.items.filter((i) => typeof i?.uuid === 'string');
105
+ this.ids.numbersOf(scripts.map((s) => s.uuid));
106
+ return scripts;
107
+ }
108
+ changed(method, what) {
109
+ return new Error(`${this.label} ${this.version}'s ${method} call gave ${what}, which sm-dev reads. This version may have changed it.`);
110
+ }
111
+ numberOf(uuid) {
112
+ return this.ids.numberOf(uuid) ?? this.ids.numbersOf([uuid]).get(uuid);
113
+ }
114
+ uuidOf(id) {
115
+ const uuid = this.ids.uuidOf(id);
116
+ if (!uuid)
117
+ throw new Error(`There is no script E${id} in ${this.label}`);
118
+ return uuid;
119
+ }
120
+ async scripts() {
121
+ return (await this.list()).map((s) => this.scriptOf(s));
122
+ }
123
+ async texts(ids) {
124
+ if (ids.length === 0)
125
+ return new Map();
126
+ const wanted = new Set(ids);
127
+ return new Map((await this.list()).flatMap((s) => {
128
+ const id = this.numberOf(s.uuid);
129
+ return wanted.has(id) && typeof s.code === 'string' ? [[id, restoredText(s.code)]] : [];
130
+ }));
131
+ }
132
+ async install(code, options = {}) {
133
+ await this.mustSaveQuietly();
134
+ const header = parseHeader(code);
135
+ const name = header ? getValue(header, 'name') : undefined;
136
+ // TM keeps any number of scripts of one name and namespace; sm-dev
137
+ // replaces as Violentmonkey does, keeping to one.
138
+ let uuid = options.id !== undefined ? this.uuidOf(options.id) : undefined;
139
+ if (uuid === undefined && header && name !== undefined) {
140
+ const namespace = getValue(header, 'namespace') ?? '';
141
+ uuid = (await this.list()).find((s) => s.name === name && (s.namespace ?? '') === namespace)?.uuid;
142
+ }
143
+ const text = options.url ? rewrittenText(code, options.url) : code;
144
+ const saved = await this.call('saveScript', {
145
+ ...(uuid !== undefined ? { uuid } : { uuid: 'new', new_script: true }),
146
+ code: text,
147
+ reload: false,
148
+ });
149
+ if (typeof saved.uuid !== 'string' || !saved.installed)
150
+ throw this.changed('saveScript', 'no installed script');
151
+ for (const [url, source] of Object.entries(options.files?.requires ?? {})) {
152
+ await this.call('saveExternal', { uuid: saved.uuid, url, code: source, mimetype: 'text/javascript' });
153
+ }
154
+ // A resource is kept as a string of its bytes, one character each.
155
+ for (const [url, { contentType, base64 }] of Object.entries(options.files?.resources ?? {})) {
156
+ await this.call('saveExternal', { uuid: saved.uuid, url, code: Buffer.from(base64, 'base64').toString('latin1'), mimetype: contentType || 'application/octet-stream' });
157
+ }
158
+ return { id: this.numberOf(saved.uuid), isNew: uuid === undefined };
159
+ }
160
+ // TM saves without asking only with its Easy save setting on, as it
161
+ // is by default; otherwise each save waits on a page no one sees.
162
+ async mustSaveQuietly() {
163
+ if ((await this.settings()).editor_easySave === false) {
164
+ throw new Error(`${this.label}'s Easy save setting is off, so it would ask before each save. Turn it on in ${this.label}'s settings (Editor, shown in the Advanced config mode), then try again.`);
165
+ }
166
+ }
167
+ async setEnabled(id, enabled) {
168
+ await this.call('modifyScriptOptions', { uuid: this.uuidOf(id), enabled, reload: false });
169
+ }
170
+ async uninstall(id) {
171
+ await this.call('saveScript', { uuid: this.uuidOf(id), code: null, reload: false });
172
+ }
173
+ async remove(ids) {
174
+ if (ids.length === 0)
175
+ return;
176
+ await this.call('purgeScripts', { uuids: ids.map((id) => this.uuidOf(id)) });
177
+ }
178
+ async applied() {
179
+ return (await this.settings()).enabled !== false;
180
+ }
181
+ async setApplied(on) {
182
+ await this.call('setOption', { name: 'enabled', value: on });
183
+ }
184
+ async allowFilePages() {
185
+ // Chrome's own file access setting decides, which sm-dev leaves.
186
+ return false;
187
+ }
188
+ async matching(url) {
189
+ const { scripts } = await this.call('determineScriptsToRun', { url });
190
+ if (!Array.isArray(scripts))
191
+ throw this.changed('determineScriptsToRun', 'no script list');
192
+ return scripts.filter((s) => typeof s === 'string').map((uuid) => this.numberOf(uuid));
193
+ }
194
+ scriptIdOf(url) {
195
+ // `chrome-extension://<id>/userscript.html?name=<name>.user.js&id=<uuid>`,
196
+ // as a frame's URL or in a stack's text.
197
+ const uuid = new RegExp(`chrome-extension://${this.extensionId}/userscript\\.html\\?\\S*?\\bid=([0-9a-f-]{36})`).exec(url)?.[1];
198
+ return uuid === undefined ? undefined : this.ids.numberOf(uuid);
199
+ }
200
+ get scriptUrlPrefix() {
201
+ return `chrome-extension://${this.extensionId}/`;
202
+ }
203
+ /**
204
+ * Notes the `@require` files one of the run's scripts was installed
205
+ * with, in order, as `remapStack` needs them: TM runs a script's code
206
+ * after one line of its own and its `@require` files, joined by
207
+ * newlines, with the script's first line on their last.
208
+ */
209
+ noteRequires(id, requires) {
210
+ const joined = requires.map((r) => r.source).join('\n');
211
+ this.layouts.set(this.uuidOf(id), {
212
+ offset: 1 + newlines(joined),
213
+ requires: requires.map((r) => ({ url: r.url, lines: newlines(r.source) + 1 })),
214
+ });
215
+ }
216
+ /** `text` with the run's scripts' stack lines pointed at the lines of their own files. */
217
+ remapStack(text) {
218
+ if (this.layouts.size === 0)
219
+ return text;
220
+ const ref = new RegExp(`(chrome-extension://${this.extensionId}/userscript\\.html\\?\\S*?\\bid=([0-9a-f-]{36})):(\\d+):(\\d+)`, 'g');
221
+ return text.replace(ref, (all, url, uuid, line, col) => {
222
+ const layout = this.layouts.get(uuid);
223
+ if (!layout)
224
+ return all;
225
+ const n = Number(line);
226
+ if (n - layout.offset >= 1)
227
+ return `${url}:${n - layout.offset}:${col}`;
228
+ // In a `@require` file: its line, counted from the line after TM's own.
229
+ let at = n - 1;
230
+ for (const r of layout.requires) {
231
+ if (at >= 1 && at <= r.lines)
232
+ return `${r.url}:${at}:${col}`;
233
+ at -= r.lines;
234
+ }
235
+ return all;
236
+ });
237
+ }
238
+ close() {
239
+ return this.page.close();
240
+ }
241
+ async settings() {
242
+ const tree = await this.call('loadTree', { referrer: 'options' });
243
+ if (!tree.options)
244
+ throw this.changed('loadTree', 'no settings');
245
+ return tree.options;
246
+ }
247
+ scriptOf(s) {
248
+ const id = this.numberOf(s.uuid);
249
+ // The dependencies as the script's own header gives them, before
250
+ // sm-dev's rewrite.
251
+ const header = typeof s.code === 'string' ? parseHeader(restoredText(s.code)) : null;
252
+ const runAt = s.options?.run_at ?? s.options?.override?.orig_run_at;
253
+ return {
254
+ id,
255
+ name: s.name ?? s.uuid,
256
+ ...(s.namespace ? { namespace: s.namespace } : {}),
257
+ ...(s.version ? { version: s.version } : {}),
258
+ ...(s.description ? { description: s.description } : {}),
259
+ ...(s.author ? { author: s.author } : {}),
260
+ enabled: s.enabled !== false,
261
+ match: s.matches ?? [],
262
+ include: s.includes ?? [],
263
+ exclude: s.excludes ?? [],
264
+ excludeMatch: [],
265
+ ...(runAt ? { runAt } : {}),
266
+ grants: s.grant ?? [],
267
+ requires: header ? getValues(header, 'require') : [],
268
+ resources: header ? getValues(header, 'resource').flatMap((v) => {
269
+ const [name, url] = v.split(/\s+/, 2);
270
+ return name && url ? [{ name, url }] : [];
271
+ }) : [],
272
+ ...(s.fileURL || s.downloadURL ? { installUrl: (s.fileURL || s.downloadURL) } : {}),
273
+ ...(s.lastModified ? { updatedAt: s.lastModified } : {}),
274
+ };
275
+ }
276
+ }
277
+ function newlines(text) {
278
+ return text.split('\n').length - 1;
279
+ }
280
+ /**
281
+ * `code` as TM is given it. A script saved from TM's dashboard has no
282
+ * URL of its own, so TM can't resolve a relative `@require` or
283
+ * `@resource`; each one is rewritten to the URL it resolves to from
284
+ * `url`, a local file's under `sm-dev.invalid`. Each line rewritten is
285
+ * kept as it was in a comment at the end, before sm-dev's mark, which
286
+ * `restoredText` puts back.
287
+ */
288
+ export function rewrittenText(code, url) {
289
+ const body = withoutMark(code);
290
+ const markLine = code.slice(body.length);
291
+ const lines = body.split('\n');
292
+ const was = [];
293
+ let inHeader = false;
294
+ for (let i = 0; i < lines.length; i++) {
295
+ // Without a CRLF file's `\r`, which goes back after.
296
+ const cr = lines[i].endsWith('\r') ? '\r' : '';
297
+ const line = lines[i].slice(0, lines[i].length - cr.length);
298
+ // The same loose markers `userscript-header.ts` accepts.
299
+ if (/\/\/[ \t]*==UserScript==/.test(line))
300
+ inHeader = true;
301
+ else if (/\/\/[ \t]*==\/UserScript==/.test(line))
302
+ break;
303
+ if (!inHeader)
304
+ continue;
305
+ const tag = /^(\s*\/\/\s*@(require|resource)\s+)(\S+\s+)?(\S+)(.*)$/.exec(line);
306
+ if (!tag)
307
+ continue;
308
+ const [, lead, kind, name = '', value, rest] = tag;
309
+ // `@require` takes no name: a second word there is the URL's own.
310
+ if (kind === 'require' && name)
311
+ continue;
312
+ const resolved = resolvedUrl(value, url);
313
+ if (resolved === value)
314
+ continue;
315
+ lines[i] = `${lead}${name}${resolved}${rest}${cr}`;
316
+ was.push(`// sm-dev was: ${i} ${line}${cr}`);
317
+ }
318
+ if (was.length === 0)
319
+ return code;
320
+ const rewritten = lines.join('\n');
321
+ return `${rewritten}${rewritten.endsWith('\n') ? '' : '\n'}${was.join('\n')}\n${markLine}`;
322
+ }
323
+ // Where a dependency's URL resolves to from the script's, a local
324
+ // file's under sm-dev.invalid. The same as given when it can't be
325
+ // resolved, which TM then reports itself.
326
+ function resolvedUrl(value, base) {
327
+ let href;
328
+ try {
329
+ href = new URL(value, base).href;
330
+ }
331
+ catch {
332
+ return value;
333
+ }
334
+ return installUrlOf(href);
335
+ }
336
+ /** `text` with the header lines `rewrittenText` changed as they were, and without its comments. */
337
+ export function restoredText(text) {
338
+ const lines = text.split('\n');
339
+ const kept = [];
340
+ const was = new Map();
341
+ for (const line of lines) {
342
+ const m = WAS.exec(line);
343
+ if (m)
344
+ was.set(Number(m[1]), m[2]);
345
+ else
346
+ kept.push(line);
347
+ }
348
+ if (was.size === 0)
349
+ return text;
350
+ // The comments come after every line they name, so dropping them
351
+ // moves none of those.
352
+ for (const [i, line] of was)
353
+ if (i < kept.length)
354
+ kept[i] = line;
355
+ return kept.join('\n');
356
+ }
@@ -0,0 +1,18 @@
1
+ /** The numbers of Tampermonkey's scripts, by uuid, read from and kept in sm-dev's cache. */
2
+ export declare class TmIds {
3
+ private saved;
4
+ private readonly byNumber;
5
+ constructor();
6
+ private index;
7
+ /**
8
+ * The numbers of `uuids`, the scripts installed now, giving each new
9
+ * one the next number. Saves the file when anything changed, read
10
+ * again first so another sm-dev's numbers aren't lost.
11
+ */
12
+ numbersOf(uuids: string[]): Map<string, number>;
13
+ /** The number of `uuid`, when it has one. */
14
+ numberOf(uuid: string): number | undefined;
15
+ /** The uuid number `n` was given, when it's still kept. */
16
+ uuidOf(n: number): string | undefined;
17
+ private save;
18
+ }