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,285 @@
1
+ // The enable toggles that keep one copy of each script active: its own
2
+ // flag and its collection's both on, so it fires on the pages it
3
+ // matches.
4
+ //
5
+ // The collections page's, the viewer's and the toolbar popup's enable
6
+ // controls, and sm-dev's `collection enable`, go through here, all in
7
+ // the service worker (worker-ops.ts), so they apply the same rules:
8
+ // - Turning a copy on turns its other active copies off.
9
+ // - Turning a collection on does the same for each copy it activates.
10
+ // - Turning a collection off brings back the copies its own copies
11
+ // displaced, when nothing has changed them since.
12
+ // - "Enable all" in a collection turns on one copy per script.
13
+ // Ctrl/Cmd-click (`solo`) opts out and changes only what was clicked.
14
+ //
15
+ // Each one finds which copies are related (from the worker's index,
16
+ // row-cache.ts, or a read of every row where there is none), then
17
+ // decides which of those to change inside the write lock, from their
18
+ // stored state at that moment (`writeEnablementFrom`).
19
+ //
20
+ // The viewer works out the same single-script rule through
21
+ // `enableChanges` (script-identity.ts) itself, to paint its rows before
22
+ // the write lands. Its versions table writes through `enableWithin`
23
+ // here. Installs, moves and sm-dev's `script enable` use
24
+ // `turnOffOtherCopies` there.
25
+ //
26
+ // Design: docs/script-enablement.md.
27
+ import { isSmDevCollection } from './directories.js';
28
+ import { getAllScriptMetas, getCollections, getScriptMeta, getScriptMetas, isCollectionEnabled, isScriptEnabled, writeEnablement, writeEnablementFrom, } from './script-store.js';
29
+ import { allRelatedRows, enableChanges, findRelatedVersions, isActive, otherActiveCopies, relatedCopyIds, } from './script-identity.js';
30
+ import { compareInCollection, preferredCopy } from './copy-preference.js';
31
+ import { sameScript } from './script-keys.js';
32
+ import { effectiveDisplayName } from './collection-display.js';
33
+ import { getValue } from '../userscript-header.js';
34
+ import { startPhaseTimer } from '../phase-timing.js';
35
+ import { scriptIdsInCollection } from './row-cache.js';
36
+ // The ids of `scripts` and every copy related to any of them, marked on
37
+ // `timer` as `find copies`.
38
+ async function scopeOf(scripts, timer) {
39
+ const ids = await relatedCopyIds(scripts);
40
+ timer.mark('find copies', `${ids.size} script(s)`);
41
+ return ids;
42
+ }
43
+ // A collection, if the index lists it, and every script row naming it
44
+ // (listed by it or not), marked on `timer` as `read collection`.
45
+ async function collectionRows(id, timer) {
46
+ const collection = (await getCollections()).find((c) => c.id === id);
47
+ let scripts = [];
48
+ if (collection) {
49
+ const ids = await scriptIdsInCollection(id);
50
+ scripts = ids
51
+ ? await getScriptMetas(ids)
52
+ : (await getAllScriptMetas()).filter((s) => s.collectionId === id);
53
+ }
54
+ timer.mark('read collection', `${scripts.length} script(s)`);
55
+ return { collection, scripts };
56
+ }
57
+ // Every installed copy related to `script` (same location or same
58
+ // `@namespace` + `@name`), itself included, among `snap`'s rows.
59
+ function relatedRows(snap, script) {
60
+ return allRelatedRows(findRelatedVersions({
61
+ ...snap, location: script.fileUrl, header: script.header ?? null,
62
+ }));
63
+ }
64
+ // What `copy-preference.ts` orders a copy by.
65
+ export function copyFacts(script, collection, times) {
66
+ return {
67
+ enabled: isScriptEnabled(script),
68
+ collectionOn: collection ? isCollectionEnabled(collection) : true,
69
+ collectionName: collection ? effectiveDisplayName(collection) : '',
70
+ activeSince: times?.activeSince,
71
+ inactiveSince: times?.inactiveSince,
72
+ version: (script.header ? getValue(script.header, 'version')?.trim() : '') || null,
73
+ location: script.fileUrl,
74
+ };
75
+ }
76
+ // Turn one copy on or off. On, without `solo`, turns its other active
77
+ // copies off in the same write, when that makes it active. Ticked under
78
+ // a disabled collection it isn't active, so it displaces nothing. Off
79
+ // changes only this copy: nothing is promoted in its place.
80
+ //
81
+ // Returns the copy as stored after the write, and every row the write
82
+ // changed (the copies it turned off among them).
83
+ export async function setScriptEnabledExclusive(id, enabled, solo = false) {
84
+ const timer = startPhaseTimer(enabled ? 'enable script' : 'disable script');
85
+ let written;
86
+ if (enabled && !solo) {
87
+ const script = await getScriptMeta(id);
88
+ written = await enableWithin(id, script ? await scopeOf([script], timer) : [id], timer);
89
+ }
90
+ else {
91
+ let clicked = null;
92
+ const result = await writeEnablementFrom({ scriptIds: [id] }, (fresh) => {
93
+ clicked = fresh.scripts.find((s) => s.id === id) ?? null;
94
+ return { scripts: [{ id, enabled }] };
95
+ }, timer);
96
+ written = { ...result, clicked };
97
+ timer.note('1 change(s)');
98
+ }
99
+ timer.done();
100
+ // Nothing written when it was already in that state, and then the row
101
+ // as read inside the lock is the stored one. Not read again here: an
102
+ // await after the lock would let a later click's result reach the
103
+ // caller first.
104
+ const script = written.scripts.find((s) => s.id === id) ?? written.clicked;
105
+ return { script, changed: written.scripts };
106
+ }
107
+ // Turn copy `id` on, and every other active copy in `scope` off,
108
+ // planned inside the write lock from those rows as stored then. `scope`
109
+ // is the copies the caller counts as this one's: every copy related to
110
+ // it, or for the viewer's versions table, every copy the table shows
111
+ // (related to the script on screen, which the clicked one may not be in
112
+ // both ways). For a caller that already knows them, so it needs no read
113
+ // of its own.
114
+ //
115
+ // `clicked` is the copy as stored after the write.
116
+ export async function enableWithin(id, scope, timer) {
117
+ let count = 0;
118
+ let clicked = null;
119
+ const written = await writeEnablementFrom({ scriptIds: scope }, (fresh) => {
120
+ let changes = [{ id, enabled: true }];
121
+ const script = fresh.scripts.find((s) => s.id === id);
122
+ clicked = script ?? null;
123
+ const collection = fresh.collections.find((c) => c.id === script?.collectionId);
124
+ if (script && (!collection || isCollectionEnabled(collection))) {
125
+ changes = enableChanges(id, true, asRows(fresh))
126
+ .map((c) => ({ id: c.scriptId, enabled: c.enabled }));
127
+ }
128
+ count = changes.length;
129
+ return { scripts: changes };
130
+ }, timer);
131
+ timer.note(`${count} change(s)`);
132
+ return { ...written, clicked: written.scripts.find((s) => s.id === id) ?? clicked };
133
+ }
134
+ // Every row in `rows` as a related copy, without filtering by location
135
+ // or identity again: the caller already chose them. `enableChanges` reads
136
+ // only the row and its collection, not how it matched.
137
+ function asRows(rows) {
138
+ const byId = new Map(rows.collections.map((c) => [c.id, c]));
139
+ return rows.scripts.map((script) => ({
140
+ script,
141
+ collection: byId.get(script.collectionId) ?? null,
142
+ sameLocation: false,
143
+ sameIdentity: false,
144
+ }));
145
+ }
146
+ // Turn a collection on or off.
147
+ // - On, without `solo`: each of its copies that becomes active turns
148
+ // off its other active copies elsewhere. Two copies inside the
149
+ // collection are both left as their own boxes say.
150
+ // - Off, without `solo`: each of its active copies that displaced
151
+ // others (they went inactive at the moment it became active, and
152
+ // haven't been active since) brings them back, unless another copy
153
+ // of that script is still active.
154
+ //
155
+ // Returns the collection as written and every script row whose flag
156
+ // changed (none of them in this collection).
157
+ export async function setCollectionEnabledExclusive(id, enabled, solo = false) {
158
+ const timer = startPhaseTimer(enabled ? 'enable collection' : 'disable collection');
159
+ const { collection, scripts } = await collectionRows(id, timer);
160
+ let written;
161
+ let count = 0;
162
+ if (!collection || solo || isSmDevCollection(collection.path)) {
163
+ written = await writeEnablement({ collection: { id, enabled } }, timer);
164
+ }
165
+ else {
166
+ const scope = await scopeOf(scripts, timer);
167
+ written = await writeEnablementFrom({ scriptIds: scope, collectionIds: [id] }, (fresh) => {
168
+ const current = fresh.collections.find((c) => c.id === id);
169
+ let changes = [];
170
+ if (current && isCollectionEnabled(current) !== enabled) {
171
+ const own = fresh.scripts.filter((s) => s.collectionId === id && isScriptEnabled(s));
172
+ changes = enabled ? displacedBy(fresh, own) : restoredFor(fresh, id, own);
173
+ }
174
+ count = changes.length;
175
+ return { collection: { id, enabled }, scripts: changes };
176
+ }, timer);
177
+ }
178
+ timer.note(`${count} other cop${count === 1 ? 'y' : 'ies'} changed`);
179
+ timer.done();
180
+ return written;
181
+ }
182
+ // The copies outside the collection that `starting` (its copies about to
183
+ // become active) switch off. One that landed `invalid` does too, as
184
+ // `turnOffOtherCopies` explains.
185
+ function displacedBy(snap, starting) {
186
+ const off = new Set();
187
+ const startingCollections = new Set(starting.map((s) => s.collectionId));
188
+ for (const script of starting) {
189
+ for (const other of otherActiveCopies(relatedRows(snap, script), script.id)) {
190
+ if (!startingCollections.has(other.script.collectionId))
191
+ off.add(other.script.id);
192
+ }
193
+ }
194
+ return [...off].map((sid) => ({ id: sid, enabled: false }));
195
+ }
196
+ // The copies to bring back when collection `id` turns off and
197
+ // `stopping` (its active copies) go inactive.
198
+ function restoredFor(snap, id, stopping) {
199
+ const on = new Set();
200
+ for (const script of stopping) {
201
+ const activeSince = snap.times[script.id]?.activeSince;
202
+ if (activeSince === undefined)
203
+ continue;
204
+ const rows = relatedRows(snap, script).filter((r) => r.script.collectionId !== id);
205
+ // Another copy is still active: bringing one back would make two.
206
+ // An sm-dev run's copies don't count, as everywhere.
207
+ if (rows.some((r) => isActive(r) && !(r.collection && isSmDevCollection(r.collection.path))))
208
+ continue;
209
+ for (const r of rows) {
210
+ if (!isScriptEnabled(r.script)
211
+ && snap.times[r.script.id]?.inactiveSince === activeSince
212
+ && (r.collection ? isCollectionEnabled(r.collection) : true)
213
+ && !(r.collection && isSmDevCollection(r.collection.path)))
214
+ on.add(r.script.id);
215
+ }
216
+ }
217
+ return [...on].map((sid) => ({ id: sid, enabled: true }));
218
+ }
219
+ // The collections page's bulk toggle.
220
+ // - Off: every script in the collection off.
221
+ // - On: one copy per script on, the first by `compareInCollection`,
222
+ // and any other copy of it in the collection off. Without `solo`,
223
+ // each copy turned on also turns off its other active copies
224
+ // elsewhere, when the collection is on. With `solo`, every script.
225
+ //
226
+ // Returns every script row whose flag changed, here and elsewhere.
227
+ export async function setAllScriptsEnabled(collectionId, enabled, solo = false) {
228
+ const timer = startPhaseTimer(enabled ? 'enable all' : 'disable all');
229
+ const { collection, scripts } = await collectionRows(collectionId, timer);
230
+ if (!collection) {
231
+ timer.note('collection gone');
232
+ timer.done();
233
+ return [];
234
+ }
235
+ if (!enabled || solo || isSmDevCollection(collection.path)) {
236
+ const written = await writeEnablement({ scripts: scripts.map((s) => ({ id: s.id, enabled })) }, timer);
237
+ timer.note(`${scripts.length} script(s)`);
238
+ timer.done();
239
+ return written.scripts;
240
+ }
241
+ let count = 0;
242
+ const written = await writeEnablementFrom({ scriptIds: await scopeOf(scripts, timer), collectionIds: [collectionId] }, (fresh) => {
243
+ const current = fresh.collections.find((c) => c.id === collectionId);
244
+ if (!current)
245
+ return null;
246
+ const w = enableAllWrite(fresh, current);
247
+ count = w.scripts.length;
248
+ return w;
249
+ }, timer);
250
+ timer.note(`${count} change(s)`);
251
+ timer.done();
252
+ return written.scripts;
253
+ }
254
+ // What "Enable all" writes for `collection`, from `snap`.
255
+ function enableAllWrite(snap, collection) {
256
+ const scripts = snap.scripts.filter((s) => s.collectionId === collection.id);
257
+ const picks = pickOnePerScript(scripts, collection, snap.times);
258
+ const changes = scripts.map((s) => ({ id: s.id, enabled: picks.has(s.id) }));
259
+ // A pick that was already active and now displaces copies is stamped
260
+ // as becoming active now, so turning this collection off brings them
261
+ // back (`turnOffOtherCopies` does the same).
262
+ const started = [];
263
+ if (isCollectionEnabled(collection)) {
264
+ for (const pick of scripts.filter((s) => picks.has(s.id))) {
265
+ const off = displacedBy(snap, [pick]);
266
+ const sameCollectionOff = scripts.filter((s) => !picks.has(s.id) && isScriptEnabled(s) && sameScript(s, pick));
267
+ if (isScriptEnabled(pick) && (off.length > 0 || sameCollectionOff.length > 0))
268
+ started.push(pick.id);
269
+ changes.push(...off);
270
+ }
271
+ }
272
+ return { scripts: changes, started };
273
+ }
274
+ // One id per script among `scripts` (all in `collection`): the first by
275
+ // `compareInCollection` among each copy and the copies it shares a
276
+ // location or `@namespace` + `@name` with (`sameScript`).
277
+ export function pickOnePerScript(scripts, collection, times) {
278
+ const picks = new Set();
279
+ for (const s of scripts) {
280
+ const group = scripts.filter((o) => sameScript(o, s));
281
+ if (preferredCopy(group, (c) => copyFacts(c, collection, times[c.id]), compareInCollection) === s)
282
+ picks.add(s.id);
283
+ }
284
+ return picks;
285
+ }
@@ -0,0 +1,310 @@
1
+ // Finding *other versions of the same script* that are already
2
+ // installed.
3
+ //
4
+ // Two independent notions of "the same script" live here, and they
5
+ // answer different questions:
6
+ //
7
+ // - **Same location** — the canonicalized *absolute* URL matches.
8
+ // This is literally the same file / URL, so whatever is at the
9
+ // other end is the same bytes we would fetch.
10
+ //
11
+ // The key is `fileUrl`, not `relativePath`. A script inside a
12
+ // directory, manifest, or repo collection stores a path relative to
13
+ // its collection root, which shares no text with the full URL the
14
+ // same script carries when installed on its own — so comparing
15
+ // `relativePath` (what the preview page did) can never match a
16
+ // collection copy against a single-file copy. `fileUrl` is the
17
+ // absolute form the load pipeline already builds for every collection
18
+ // kind (`scriptFileUrl` in directories.ts), so it is the one
19
+ // spelling all collection kinds agree on.
20
+ //
21
+ // Display is unaffected: a collection copy still *shows* its
22
+ // relative path, which is what the user recognizes. Only the
23
+ // comparison uses the absolute form.
24
+ //
25
+ // - **Same identity** — `@namespace` + `@name` match. This is the
26
+ // userscript ecosystem's conventional unique ID: Greasemonkey
27
+ // documents `@namespace` as the value that, "combined with the
28
+ // name, forms a unique identifier", and both Tampermonkey and
29
+ // Violentmonkey key their install-vs-update decision on the pair.
30
+ // It catches the same script mirrored to a second host, vendored
31
+ // into a repo, or edited into a local copy — cases where the
32
+ // locations have nothing in common.
33
+ //
34
+ // SourceMonkey's own record identity stays what it was: a random UUID
35
+ // per stored row, kept stable across refreshes by `relativePath` (see
36
+ // [docs/storage.md]). Nothing here mints or rewrites an id — this
37
+ // module only *finds relatives* of a script so the UI can offer to
38
+ // jump between them.
39
+ //
40
+ // See [docs/script-identity.md].
41
+ import { containerKindForSingleFileInput, containerPath, isContainerCollection, isScratchUrl, isSmDevCollection, isSingleFileInput, } from './directories.js';
42
+ import { getAllScriptMetas, getCollections, getScriptMeta, getScriptMetas, isScriptEnabled, isCollectionEnabled, writeEnablementFrom, } from './script-store.js';
43
+ import { CONTAINER_DISPLAY_NAMES, effectiveDisplayName } from './collection-display.js';
44
+ import { getValues } from '../userscript-header.js';
45
+ import { identityKey, locationKey } from './script-keys.js';
46
+ import { startPhaseTimer } from '../phase-timing.js';
47
+ import { relatedScriptIds } from './row-cache.js';
48
+ // `identityKey` and `locationKey` live in script-keys.ts, so the store
49
+ // can use them too; re-exported here, where the rest of the lookup is.
50
+ export { identityKey, locationKey } from './script-keys.js';
51
+ export function findRelatedVersions(q) {
52
+ const loc = locationKey(q.location);
53
+ const identity = identityKey(q.header);
54
+ const collectionById = new Map(q.collections.map((s) => [s.id, s]));
55
+ const sameLocation = [];
56
+ const sameIdentity = [];
57
+ for (const script of q.scripts) {
58
+ const atLocation = locationKey(script.fileUrl) === loc;
59
+ const isIdentity = identity !== null && identityKey(script.header) === identity;
60
+ if (!atLocation && !isIdentity)
61
+ continue;
62
+ const rel = {
63
+ script,
64
+ collection: collectionById.get(script.collectionId) ?? null,
65
+ sameLocation: atLocation,
66
+ sameIdentity: isIdentity,
67
+ };
68
+ // Location is the stronger claim, so a row that matches both is
69
+ // only listed once, under location.
70
+ (atLocation ? sameLocation : sameIdentity).push(rel);
71
+ }
72
+ const byName = (a, b) => collectionSortKey(a).localeCompare(collectionSortKey(b))
73
+ || a.script.relativePath.localeCompare(b.script.relativePath);
74
+ sameLocation.sort(byName);
75
+ sameIdentity.sort(byName);
76
+ return {
77
+ sameLocation,
78
+ sameIdentity,
79
+ installOptions: installOptionsFor(loc, sameLocation, q.collections),
80
+ };
81
+ }
82
+ // The containers a single-file install of `loc` could land in, in the
83
+ // order the dropdown lists them: the built-in default for the
84
+ // location's scheme first (offered as not-yet-existing when it has no
85
+ // row), then every other container in page order. Each is marked
86
+ // `installed` when one of the same-location rows already sits in it.
87
+ // The caller appends its own "New collection…" entry.
88
+ //
89
+ // `current` names the collection the script is in now — the move
90
+ // dropdown's caller. That entry is still listed, marked `isCurrent`
91
+ // so the dropdown can label it *(current)*, disable it and pre-select
92
+ // it. Listing it rather than dropping it keeps one list shape for
93
+ // every script, and makes the opening selection somewhere the script
94
+ // already is, so nothing moves without a deliberate pick.
95
+ export function installOptionsFor(loc, sameLocation, collections, current) {
96
+ // A run's script stays in its sm-dev test collection, and nothing else
97
+ // goes into one.
98
+ if (!isSingleFileInput(loc) || isScratchUrl(loc))
99
+ return undefined;
100
+ const kind = containerKindForSingleFileInput(loc);
101
+ const defaultPath = containerPath(kind);
102
+ const holding = new Set();
103
+ for (const r of sameLocation) {
104
+ if (r.collection)
105
+ holding.add(r.collection.id);
106
+ }
107
+ const containers = collections.filter((c) => isContainerCollection(c.path) && !isSmDevCollection(c.path));
108
+ const def = containers.find((c) => c.path === defaultPath) ?? null;
109
+ const options = [{
110
+ collection: def,
111
+ path: defaultPath,
112
+ label: def ? effectiveDisplayName(def) : CONTAINER_DISPLAY_NAMES[kind],
113
+ installed: def ? holding.has(def.id) : false,
114
+ // A default that doesn't exist yet is created enabled.
115
+ collectionOff: def ? !isCollectionEnabled(def) : false,
116
+ isDefault: true,
117
+ // `def?.id === current` alone would be `undefined === undefined`
118
+ // for an install (no current) whose default has no row yet.
119
+ isCurrent: current !== undefined && def?.id === current,
120
+ }];
121
+ for (const c of containers) {
122
+ if (c.path === defaultPath)
123
+ continue;
124
+ options.push({
125
+ collection: c,
126
+ path: c.path,
127
+ label: effectiveDisplayName(c),
128
+ installed: holding.has(c.id),
129
+ collectionOff: !isCollectionEnabled(c),
130
+ isDefault: false,
131
+ isCurrent: c.id === current,
132
+ });
133
+ }
134
+ return options;
135
+ }
136
+ // One click's worth of enable/disable writes: the copy that was
137
+ // clicked, plus — when it is being turned ON — every other copy that
138
+ // is currently active. Pass an empty `rows` to opt out.
139
+ //
140
+ // Disabling is never exclusive: it leaves none active, which is an
141
+ // ordinary thing to want, and there is no other copy that should be
142
+ // promoted in its place.
143
+ //
144
+ // Nor is enabling one of an sm-dev run's scripts: the run keeps its
145
+ // scripts off other tabs itself, so they don't compete.
146
+ export function enableChanges(scriptId, enabled, rows) {
147
+ const changes = [{ scriptId, enabled }];
148
+ if (!enabled)
149
+ return changes;
150
+ const own = rows.find((r) => r.script.id === scriptId);
151
+ if (own && inRunCollection(own))
152
+ return changes;
153
+ for (const other of otherActiveCopies(rows, scriptId)) {
154
+ changes.push({ scriptId: other.script.id, enabled: false });
155
+ }
156
+ return changes;
157
+ }
158
+ // Whether a row is active: its own flag AND its collection's, so it
159
+ // fires on the pages it matches. `turnOffOtherCopies` asks it of the
160
+ // copy being enabled too: that copy has to be active itself before it
161
+ // is worth switching the others off.
162
+ export function isActive(rel) {
163
+ if (!isScriptEnabled(rel.script))
164
+ return false;
165
+ return rel.collection ? isCollectionEnabled(rel.collection) : true;
166
+ }
167
+ // The copies an exclusive enable of `keepId` has to switch off: every
168
+ // OTHER row that is currently active.
169
+ //
170
+ // Shared by both checkboxes the viewer puts on screen — the panel row's
171
+ // and the controls row's — so "enabling one copy turns the rest off"
172
+ // means the same thing wherever it is clicked.
173
+ //
174
+ // The exclusions, all deliberate:
175
+ // - **Rows that aren't active already.** Nothing to gain by
176
+ // rewriting a value they effectively hold.
177
+ // - **Rows under a disabled collection.** Not competing with the copy
178
+ // being enabled, and their own checkbox renders disabled — so a
179
+ // flip made here could not be undone from the panel at all, and
180
+ // would silently destroy the user's stored intent for whenever
181
+ // they re-enable that collection.
182
+ // - **An sm-dev run's scripts.** Only their run changes them, and it
183
+ // keeps them off other tabs itself.
184
+ export function otherActiveCopies(rows, keepId) {
185
+ return rows.filter((r) => r.script.id !== keepId && isActive(r) && !inRunCollection(r));
186
+ }
187
+ function inRunCollection(rel) {
188
+ return rel.collection !== null && isSmDevCollection(rel.collection.path);
189
+ }
190
+ // A copy that just became active follows the same exclusive rule as
191
+ // the panel's checkboxes: it is the one wanted active, and leaving an
192
+ // older copy of the same script on means both register and both run on
193
+ // every page they match. Returns the copies it turned off.
194
+ //
195
+ // For a copy that is newly active: an install, a local copy, a move
196
+ // into an enabled collection, and sm-dev's `script enable`. With no
197
+ // other copy active, it does nothing. Skipped when the copy's
198
+ // collection is disabled, so it isn't active itself.
199
+ //
200
+ // A copy that landed `invalid` still takes over: it is the one asked
201
+ // for, and leaving a working copy on beside it would make two enabled
202
+ // copies. Nothing runs until it is fixed, and then it runs without
203
+ // another click.
204
+ //
205
+ // A run's test collection is left out both ways, as by `enableChanges`.
206
+ export async function turnOffOtherCopies(scriptId) {
207
+ const timer = startPhaseTimer('turn off other copies');
208
+ const copy = await getScriptMeta(scriptId);
209
+ let related = [];
210
+ if (copy) {
211
+ const ids = await relatedCopyIds([copy]);
212
+ timer.mark('find copies', `${ids.size} script(s)`);
213
+ const [scripts, collections] = await Promise.all([getScriptMetas([...ids]), getCollections()]);
214
+ timer.mark('read copies');
215
+ related = relatedRowsOf(scripts, collections, copy);
216
+ }
217
+ let off = [];
218
+ // Skipped when this read shows nothing to turn off, the usual case
219
+ // after an install or a move, so it doesn't take the lock for nothing.
220
+ if (copy && otherActiveCopies(related, scriptId).length > 0) {
221
+ // Which copies are related comes from this read; which of them are
222
+ // active is decided again inside the write lock (`writeEnablementFrom`).
223
+ const scope = related.map((r) => r.script.id);
224
+ await writeEnablementFrom({ scriptIds: [scriptId, ...scope] }, (fresh) => {
225
+ const own = fresh.scripts.find((s) => s.id === scriptId);
226
+ if (!own)
227
+ return null;
228
+ const rows = relatedRowsOf(fresh.scripts, fresh.collections, own);
229
+ const ownRow = rows.find((r) => r.script.id === scriptId);
230
+ if (!ownRow || !isActive(ownRow) || inRunCollection(ownRow))
231
+ return null;
232
+ const others = otherActiveCopies(rows, scriptId);
233
+ if (others.length === 0)
234
+ return null;
235
+ off = others.map((r) => r.script);
236
+ // `started`: the copy became active in an earlier write, but it is
237
+ // stamped as becoming active now, the moment the copies it
238
+ // displaces go inactive (docs/script-enablement.md).
239
+ return { scripts: off.map((s) => ({ id: s.id, enabled: false })), started: [scriptId] };
240
+ }, timer);
241
+ }
242
+ timer.note(`${off.length} cop${off.length === 1 ? 'y' : 'ies'} turned off`);
243
+ timer.done();
244
+ return off;
245
+ }
246
+ // The ids of every stored copy related to any of `scripts`, themselves
247
+ // included: from the worker's index (row-cache.ts), or where there is
248
+ // none, a read of every row. Exactly the related ones, either way: the
249
+ // index keys are the ones `findRelatedVersions` compares, and a caller
250
+ // may act on every id (`enableWithin` turns off any active one).
251
+ export async function relatedCopyIds(scripts) {
252
+ const indexed = await relatedScriptIds(scripts);
253
+ if (indexed)
254
+ return indexed;
255
+ const [all, collections] = await Promise.all([getAllScriptMetas(), getCollections()]);
256
+ const ids = new Set();
257
+ for (const script of scripts) {
258
+ ids.add(script.id);
259
+ for (const r of relatedRowsOf(all, collections, script))
260
+ ids.add(r.script.id);
261
+ }
262
+ return ids;
263
+ }
264
+ // Every copy related to `copy`, itself included. Compared by the STORED
265
+ // row's location and header: what the rest of the store is compared
266
+ // against, and for a github blob URL the resolved raw one.
267
+ function relatedRowsOf(scripts, collections, copy) {
268
+ return allRelatedRows(findRelatedVersions({ scripts, collections, location: copy.fileUrl, header: copy.header ?? null }));
269
+ }
270
+ // Every related row in one list, in the order the panel draws them.
271
+ // The exclusive rule spans both sections: the point is that one copy
272
+ // of this script runs, not one copy per section.
273
+ export function allRelatedRows(related) {
274
+ return [...related.sameLocation, ...related.sameIdentity];
275
+ }
276
+ // Sorts by the name the row will actually show. Using the raw
277
+ // `displayName` would leave any collection without one — directory and
278
+ // manifest collections, which only get an inferred name — sorted under
279
+ // a string the user never sees, so the table would look unsorted.
280
+ function collectionSortKey(r) {
281
+ return r.collection ? effectiveDisplayName(r.collection) : '';
282
+ }
283
+ // What the panel's Compare column compares: a copy's own source text
284
+ // plus every `@require` body it loaded, in declaration order. Built
285
+ // from the hashes the store already keeps for each (`contentHash` on
286
+ // the row and on each require entry), so it is string assembly, not
287
+ // hashing — and it is comparable across pages: a preview's download
288
+ // gets the same shape from its text hash and the parse check's
289
+ // requires.
290
+ //
291
+ // Requires are part of it because they are part of what runs: two
292
+ // copies with the same body but a library that moved on behave
293
+ // differently. `@resource` is not: it is data the script reads, not
294
+ // code that runs.
295
+ //
296
+ // `undefined` when there is no content to compare: the row never
297
+ // loaded, or its header declares `@require`s and none is stored — a
298
+ // dependency that would not load leaves the list empty, and a preview
299
+ // has none until its parse check lands. A body-only answer there would
300
+ // call a broken copy DIFFERENT from a healthy twin with the same text,
301
+ // or two broken copies SAME.
302
+ export function contentFingerprint(script) {
303
+ if (!script.contentHash)
304
+ return undefined;
305
+ const requires = script.requires ?? [];
306
+ const declared = script.header ? getValues(script.header, 'require').length > 0 : false;
307
+ if (declared && requires.length === 0)
308
+ return undefined;
309
+ return [script.contentHash, ...requires.map((r) => r.contentHash)].join('\n');
310
+ }
@@ -0,0 +1,54 @@
1
+ // The two keys that say whether two installed rows are copies of one
2
+ // script: the same `@namespace` + `@name`, or the same absolute
3
+ // location. Kept apart from script-identity.ts (which finds the copies)
4
+ // so the store can use them without importing that module, which
5
+ // imports the store. See [docs/script-identity.md].
6
+ import { canonicalCollectionInput } from './directories.js';
7
+ import { getValue } from '../userscript-header.js';
8
+ // The ecosystem identity key: `@namespace` and `@name`, trimmed and
9
+ // joined by a separator that can't occur in either.
10
+ //
11
+ // `null` when there is no `@name` — with nothing to key on we decline
12
+ // to match rather than fold every nameless script together. A missing
13
+ // `@namespace` is NOT the same case: the empty namespace is a real
14
+ // (if unwise) value, and two scripts that both omit it and share a
15
+ // name are treated as the same script, which is exactly what TM and
16
+ // VM do. That is the convention's known weak spot, not ours to fix
17
+ // unilaterally.
18
+ //
19
+ // Comparison is exact after trimming — no case folding, no URL
20
+ // normalization of the namespace. `HTTP://Example.com/` and
21
+ // `http://example.com/` are different namespaces to every other
22
+ // manager, and quietly disagreeing would make our "same script"
23
+ // answer differ from the one the user sees in Tampermonkey.
24
+ //
25
+ // Spelled as an escape, never as a literal NUL byte: a raw NUL makes
26
+ // grep treat the whole source file as binary and print nothing for
27
+ // every match in it.
28
+ const IDENTITY_SEP = '\u0000';
29
+ export function identityKey(header) {
30
+ if (!header)
31
+ return null;
32
+ const name = getValue(header, 'name')?.trim();
33
+ if (!name)
34
+ return null;
35
+ const namespace = getValue(header, 'namespace')?.trim() ?? '';
36
+ return `${namespace}${IDENTITY_SEP}${name}`;
37
+ }
38
+ // The location key: the one true spelling of an absolute script
39
+ // location. `canonicalCollectionInput` is the same helper
40
+ // `addCollection` uses, so a `file://` URL and the bare path spelling
41
+ // of the same file collapse together — which is what lets a directory
42
+ // collection's `fileUrl` (always a `file://` URL) match a container
43
+ // row's bare path, and what makes a capture's URL match a row stored
44
+ // before canonicalization existed.
45
+ export function locationKey(absoluteUrl) {
46
+ return canonicalCollectionInput(absoluteUrl);
47
+ }
48
+ // Whether two rows are copies of one script, by either key.
49
+ export function sameScript(a, b) {
50
+ if (locationKey(a.fileUrl) === locationKey(b.fileUrl))
51
+ return true;
52
+ const ia = identityKey(a.header ?? null);
53
+ return ia !== null && ia === identityKey(b.header ?? null);
54
+ }