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.
- package/LICENSE +21 -0
- package/README.md +87 -2
- package/dist/bytes.js +37 -0
- package/dist/channel-placeholder.js +5 -0
- package/dist/collection-scan.js +17 -0
- package/dist/collections.html +353 -0
- package/dist/compare.html +358 -0
- package/dist/composed-source.js +205 -0
- package/dist/connect-prompt.html +47 -0
- package/dist/debug/icons.html +211 -0
- package/dist/debug/storage.html +163 -0
- package/dist/debug-log.js +75 -0
- package/dist/diagnostic-location.js +64 -0
- package/dist/eval-hook.js +308 -0
- package/dist/extension/background.js +5263 -0
- package/dist/extension/channel.js +133 -0
- package/dist/extension/code-links.js +187 -0
- package/dist/extension/code-tabs.js +460 -0
- package/dist/extension/collection-dialog.js +619 -0
- package/dist/extension/collection-display.js +322 -0
- package/dist/extension/collections.js +2905 -0
- package/dist/extension/compare.js +867 -0
- package/dist/extension/concurrency.js +186 -0
- package/dist/extension/connect-grants.js +109 -0
- package/dist/extension/connect-prompt-page.js +70 -0
- package/dist/extension/connect-prompt.js +266 -0
- package/dist/extension/copy-button.js +63 -0
- package/dist/extension/copy-dialog.js +502 -0
- package/dist/extension/copy-preference.js +66 -0
- package/dist/extension/debug/icons.js +203 -0
- package/dist/extension/debug/storage.js +365 -0
- package/dist/extension/deep-link.js +77 -0
- package/dist/extension/delete-dialog.js +432 -0
- package/dist/extension/delete-icon.js +40 -0
- package/dist/extension/details-tooltips.js +66 -0
- package/dist/extension/directories.js +679 -0
- package/dist/extension/export-backup.js +407 -0
- package/dist/extension/export-dialog.js +142 -0
- package/dist/extension/file-access.js +74 -0
- package/dist/extension/file-changes.js +346 -0
- package/dist/extension/frame-info.js +85 -0
- package/dist/extension/github-provider.js +336 -0
- package/dist/extension/glyph.js +84 -0
- package/dist/extension/gm-bindings.js +601 -0
- package/dist/extension/gm-download.js +419 -0
- package/dist/extension/gm-menu.js +228 -0
- package/dist/extension/gm-notification.js +141 -0
- package/dist/extension/gm-originator-map.js +23 -0
- package/dist/extension/gm-tab-data.js +121 -0
- package/dist/extension/gm-tab.js +160 -0
- package/dist/extension/gm-url-change.js +111 -0
- package/dist/extension/gm-url-scheme.js +27 -0
- package/dist/extension/gm-values.js +176 -0
- package/dist/extension/gm-window-control.js +104 -0
- package/dist/extension/gm-xhr-headers.js +333 -0
- package/dist/extension/gm-xhr.js +584 -0
- package/dist/extension/injector.js +3145 -0
- package/dist/extension/install-capture.js +277 -0
- package/dist/extension/line-diff.js +233 -0
- package/dist/extension/local-copy.js +560 -0
- package/dist/extension/local-delete.js +276 -0
- package/dist/extension/location-kinds.js +108 -0
- package/dist/extension/log-format.js +158 -0
- package/dist/extension/managed-changes.js +361 -0
- package/dist/extension/managed-items.js +57 -0
- package/dist/extension/match-index.js +375 -0
- package/dist/extension/missing-scripts.js +66 -0
- package/dist/extension/more-menu.js +109 -0
- package/dist/extension/move-dialog.js +197 -0
- package/dist/extension/offscreen.js +37 -0
- package/dist/extension/options.js +504 -0
- package/dist/extension/page-heading.js +170 -0
- package/dist/extension/page-links.js +199 -0
- package/dist/extension/parse-check-client.js +70 -0
- package/dist/extension/parse-check.js +161 -0
- package/dist/extension/parse-sandbox-host.js +172 -0
- package/dist/extension/parse-sandbox.js +175 -0
- package/dist/extension/popup-commands-open.js +41 -0
- package/dist/extension/popup-view.js +387 -0
- package/dist/extension/popup.js +703 -0
- package/dist/extension/prism-userscript.js +53 -0
- package/dist/extension/refresh-error.js +136 -0
- package/dist/extension/refresh-targets.js +160 -0
- package/dist/extension/related-versions.js +1144 -0
- package/dist/extension/resize-observer-loop.js +19 -0
- package/dist/extension/row-cache.js +387 -0
- package/dist/extension/script-blobs.js +413 -0
- package/dist/extension/script-details.js +658 -0
- package/dist/extension/script-enablement.js +285 -0
- package/dist/extension/script-identity.js +310 -0
- package/dist/extension/script-keys.js +54 -0
- package/dist/extension/script-log-id.js +155 -0
- package/dist/extension/script-log.js +290 -0
- package/dist/extension/script-rejection.js +104 -0
- package/dist/extension/script-sites.js +158 -0
- package/dist/extension/script-store.js +3507 -0
- package/dist/extension/script-updater.js +991 -0
- package/dist/extension/script-url-hash.js +61 -0
- package/dist/extension/select-all.js +37 -0
- package/dist/extension/select-open-guard.js +90 -0
- package/dist/extension/site-icon.js +1261 -0
- package/dist/extension/sm-dev-link.js +302 -0
- package/dist/extension/solo-click.js +58 -0
- package/dist/extension/sticky-numbers.js +188 -0
- package/dist/extension/storage-breakdown.js +253 -0
- package/dist/extension/storage-scan.js +86 -0
- package/dist/extension/storage-usage.js +194 -0
- package/dist/extension/structural-keys.js +29 -0
- package/dist/extension/sw-teardown.js +36 -0
- package/dist/extension/tab-status.js +403 -0
- package/dist/extension/toolbar-summary.js +105 -0
- package/dist/extension/user-scripts-access.js +43 -0
- package/dist/extension/userscript.js +3672 -0
- package/dist/extension/value-store.js +204 -0
- package/dist/extension/version-probe-fetch.js +68 -0
- package/dist/extension/worker-client.js +109 -0
- package/dist/extension/worker-ops.js +171 -0
- package/dist/extension/zip-writer.js +24 -0
- package/dist/external-files.js +878 -0
- package/dist/file-loader.js +436 -0
- package/dist/file-locations.js +439 -0
- package/dist/generated-wrappers.js +5 -0
- package/dist/github-auth.js +148 -0
- package/dist/github-urls.js +46 -0
- package/dist/gm-api-catalog.js +217 -0
- package/dist/gm-connect.js +273 -0
- package/dist/gm-xhr-types.js +5 -0
- package/dist/http-read.js +142 -0
- package/dist/icons/icon-128.png +0 -0
- package/dist/icons/icon-16.png +0 -0
- package/dist/icons/icon-48.png +0 -0
- package/dist/local-file-overrides.js +558 -0
- package/dist/manifest.json +38 -0
- package/dist/markup-response.js +126 -0
- package/dist/match-targets.js +305 -0
- package/dist/menu-captions.js +22 -0
- package/dist/offscreen.html +39 -0
- package/dist/options.html +197 -0
- package/dist/parse-check-protocol.js +51 -0
- package/dist/parse-location.js +171 -0
- package/dist/parse-sandbox.html +56 -0
- package/dist/phase-timing.js +155 -0
- package/dist/popup.html +197 -0
- package/dist/public-suffixes.js +6957 -0
- package/dist/run-context.js +31 -0
- package/dist/runtime-messages.js +34 -0
- package/dist/sandboxed-page.js +35 -0
- package/dist/script-build.js +787 -0
- package/dist/script-display.js +383 -0
- package/dist/script-icon.js +43 -0
- package/dist/script-links.js +204 -0
- package/dist/script-source-url.js +214 -0
- package/dist/script-world.js +65 -0
- package/dist/sm-dev-protocol.js +84 -0
- package/dist/source-map.js +177 -0
- package/dist/stack-link.js +138 -0
- package/dist/styles.css +825 -0
- package/dist/unsafe-window-use.js +229 -0
- package/dist/url-match.js +734 -0
- package/dist/userscript-header.js +248 -0
- package/dist/userscript.html +1165 -0
- package/dist/vendor/fflate.LICENSE +21 -0
- package/dist/vendor/fflate.js +2692 -0
- package/dist/vendor/prism/prism-line-numbers.css +1 -0
- package/dist/vendor/prism/prism-line-numbers.js +1 -0
- package/dist/vendor/prism/prism.css +1 -0
- package/dist/vendor/prism/prism.js +1946 -0
- package/dist/version-compare.js +141 -0
- package/dist/version-probe.js +120 -0
- package/lib/build-info.json +1 -0
- package/lib/bytes.d.ts +3 -0
- package/lib/bytes.js +37 -0
- package/lib/channel-placeholder.d.ts +1 -0
- package/lib/channel-placeholder.js +5 -0
- package/lib/collection-scan.d.ts +8 -0
- package/lib/collection-scan.js +17 -0
- package/lib/composed-source.d.ts +28 -0
- package/lib/composed-source.js +205 -0
- package/lib/debug-log.d.ts +9 -0
- package/lib/debug-log.js +75 -0
- package/lib/diagnostic-location.d.ts +8 -0
- package/lib/diagnostic-location.js +64 -0
- package/lib/eval-hook.d.ts +34 -0
- package/lib/eval-hook.js +308 -0
- package/lib/external-files.d.ts +111 -0
- package/lib/external-files.js +878 -0
- package/lib/file-loader.d.ts +48 -0
- package/lib/file-loader.js +436 -0
- package/lib/file-locations.d.ts +44 -0
- package/lib/file-locations.js +439 -0
- package/lib/generated-wrappers.d.ts +11 -0
- package/lib/generated-wrappers.js +5 -0
- package/lib/github-auth.d.ts +12 -0
- package/lib/github-auth.js +148 -0
- package/lib/github-urls.d.ts +1 -0
- package/lib/github-urls.js +46 -0
- package/lib/gm-api-catalog.d.ts +24 -0
- package/lib/gm-api-catalog.js +217 -0
- package/lib/gm-connect.d.ts +34 -0
- package/lib/gm-connect.js +273 -0
- package/lib/gm-xhr-types.d.ts +68 -0
- package/lib/gm-xhr-types.js +5 -0
- package/lib/harness/browser-extensions.d.ts +73 -0
- package/lib/harness/browser-extensions.js +285 -0
- package/lib/harness/browser-lang.d.ts +20 -0
- package/lib/harness/browser-lang.js +86 -0
- package/lib/harness/browser-launch.d.ts +14 -0
- package/lib/harness/browser-launch.js +184 -0
- package/lib/harness/checks.d.ts +110 -0
- package/lib/harness/checks.js +321 -0
- package/lib/harness/cli.d.ts +2 -0
- package/lib/harness/cli.js +2259 -0
- package/lib/harness/command-args.d.ts +91 -0
- package/lib/harness/command-args.js +369 -0
- package/lib/harness/command-lines.d.ts +80 -0
- package/lib/harness/command-lines.js +180 -0
- package/lib/harness/command-list.d.ts +206 -0
- package/lib/harness/command-list.js +1475 -0
- package/lib/harness/console-commands.d.ts +463 -0
- package/lib/harness/console-commands.js +3216 -0
- package/lib/harness/download-watch.d.ts +21 -0
- package/lib/harness/download-watch.js +156 -0
- package/lib/harness/extension-backend.d.ts +41 -0
- package/lib/harness/extension-backend.js +100 -0
- package/lib/harness/extension-host.d.ts +112 -0
- package/lib/harness/extension-host.js +552 -0
- package/lib/harness/extension-launch.d.ts +67 -0
- package/lib/harness/extension-launch.js +285 -0
- package/lib/harness/extension-link.d.ts +315 -0
- package/lib/harness/extension-link.js +607 -0
- package/lib/harness/external-manager.d.ts +146 -0
- package/lib/harness/external-manager.js +414 -0
- package/lib/harness/fixture.d.ts +165 -0
- package/lib/harness/fixture.js +461 -0
- package/lib/harness/frame-sessions.d.ts +71 -0
- package/lib/harness/frame-sessions.js +248 -0
- package/lib/harness/frames.d.ts +68 -0
- package/lib/harness/frames.js +216 -0
- package/lib/harness/help-format.d.ts +17 -0
- package/lib/harness/help-format.js +127 -0
- package/lib/harness/host.d.ts +352 -0
- package/lib/harness/host.js +423 -0
- package/lib/harness/index.d.ts +8 -0
- package/lib/harness/index.js +10 -0
- package/lib/harness/input-file.d.ts +18 -0
- package/lib/harness/input-file.js +61 -0
- package/lib/harness/json-output.d.ts +565 -0
- package/lib/harness/json-output.js +71 -0
- package/lib/harness/manager-actions.d.ts +21 -0
- package/lib/harness/manager-actions.js +129 -0
- package/lib/harness/manager-commands.d.ts +142 -0
- package/lib/harness/manager-commands.js +1196 -0
- package/lib/harness/manager-fetch.d.ts +33 -0
- package/lib/harness/manager-fetch.js +239 -0
- package/lib/harness/managers.d.ts +24 -0
- package/lib/harness/managers.js +27 -0
- package/lib/harness/output-style.d.ts +125 -0
- package/lib/harness/output-style.js +322 -0
- package/lib/harness/page-commands.d.ts +134 -0
- package/lib/harness/page-commands.js +1141 -0
- package/lib/harness/page-output.d.ts +67 -0
- package/lib/harness/page-output.js +139 -0
- package/lib/harness/page-watch.d.ts +171 -0
- package/lib/harness/page-watch.js +607 -0
- package/lib/harness/parse-check-node.d.ts +3 -0
- package/lib/harness/parse-check-node.js +132 -0
- package/lib/harness/path-args.d.ts +22 -0
- package/lib/harness/path-args.js +148 -0
- package/lib/harness/playwright-chromium.d.ts +16 -0
- package/lib/harness/playwright-chromium.js +86 -0
- package/lib/harness/prepare.d.ts +168 -0
- package/lib/harness/prepare.js +364 -0
- package/lib/harness/prompt-editor.d.ts +17 -0
- package/lib/harness/prompt-editor.js +354 -0
- package/lib/harness/prompt-history.d.ts +13 -0
- package/lib/harness/prompt-history.js +89 -0
- package/lib/harness/rebuild-advice.d.ts +6 -0
- package/lib/harness/rebuild-advice.js +39 -0
- package/lib/harness/run-log.d.ts +47 -0
- package/lib/harness/run-log.js +140 -0
- package/lib/harness/run-logs.d.ts +132 -0
- package/lib/harness/run-logs.js +241 -0
- package/lib/harness/run.d.ts +194 -0
- package/lib/harness/run.js +1973 -0
- package/lib/harness/runs.d.ts +229 -0
- package/lib/harness/runs.js +726 -0
- package/lib/harness/save-html.d.ts +39 -0
- package/lib/harness/save-html.js +89 -0
- package/lib/harness/saved-pages.d.ts +120 -0
- package/lib/harness/saved-pages.js +275 -0
- package/lib/harness/script-mark.d.ts +17 -0
- package/lib/harness/script-mark.js +56 -0
- package/lib/harness/snapshot.d.ts +143 -0
- package/lib/harness/snapshot.js +474 -0
- package/lib/harness/stuck-tabs.d.ts +19 -0
- package/lib/harness/stuck-tabs.js +131 -0
- package/lib/harness/tab-refs.d.ts +20 -0
- package/lib/harness/tab-refs.js +80 -0
- package/lib/harness/tampermonkey.d.ts +71 -0
- package/lib/harness/tampermonkey.js +356 -0
- package/lib/harness/tm-ids.d.ts +18 -0
- package/lib/harness/tm-ids.js +88 -0
- package/lib/harness/violentmonkey.d.ts +34 -0
- package/lib/harness/violentmonkey.js +162 -0
- package/lib/harness/wait.d.ts +77 -0
- package/lib/harness/wait.js +458 -0
- package/lib/http-read.d.ts +30 -0
- package/lib/http-read.js +142 -0
- package/lib/local-file-overrides.d.ts +35 -0
- package/lib/local-file-overrides.js +558 -0
- package/lib/manifest.json +38 -0
- package/lib/markup-response.d.ts +11 -0
- package/lib/markup-response.js +126 -0
- package/lib/match-targets.d.ts +18 -0
- package/lib/match-targets.js +305 -0
- package/lib/menu-captions.d.ts +1 -0
- package/lib/menu-captions.js +22 -0
- package/lib/parse-check-protocol.d.ts +27 -0
- package/lib/parse-check-protocol.js +51 -0
- package/lib/parse-location.d.ts +12 -0
- package/lib/parse-location.js +171 -0
- package/lib/phase-timing.d.ts +12 -0
- package/lib/phase-timing.js +155 -0
- package/lib/public-suffixes.d.ts +1 -0
- package/lib/public-suffixes.js +6957 -0
- package/lib/run-context.d.ts +2 -0
- package/lib/run-context.js +31 -0
- package/lib/runtime-messages.d.ts +10 -0
- package/lib/runtime-messages.js +34 -0
- package/lib/sandboxed-page.d.ts +5 -0
- package/lib/sandboxed-page.js +35 -0
- package/lib/script-build.d.ts +111 -0
- package/lib/script-build.js +787 -0
- package/lib/script-display.d.ts +51 -0
- package/lib/script-display.js +383 -0
- package/lib/script-icon.d.ts +16 -0
- package/lib/script-icon.js +43 -0
- package/lib/script-links.d.ts +24 -0
- package/lib/script-links.js +204 -0
- package/lib/script-source-url.d.ts +8 -0
- package/lib/script-source-url.js +214 -0
- package/lib/script-world.d.ts +6 -0
- package/lib/script-world.js +65 -0
- package/lib/sm-dev-protocol.d.ts +251 -0
- package/lib/sm-dev-protocol.js +84 -0
- package/lib/source-map.d.ts +32 -0
- package/lib/source-map.js +177 -0
- package/lib/stack-link.d.ts +15 -0
- package/lib/stack-link.js +138 -0
- package/lib/unsafe-window-use.d.ts +1 -0
- package/lib/unsafe-window-use.js +229 -0
- package/lib/url-match.d.ts +40 -0
- package/lib/url-match.js +734 -0
- package/lib/userscript-header.d.ts +33 -0
- package/lib/userscript-header.js +248 -0
- package/lib/version-compare.d.ts +6 -0
- package/lib/version-compare.js +141 -0
- package/lib/version-probe.d.ts +17 -0
- package/lib/version-probe.js +120 -0
- package/package.json +55 -3
- package/skills/SourceMonkey-dev/SKILL.md +868 -0
- package/skills/SourceMonkey-dev/references/cdp.md +293 -0
- package/skills/SourceMonkey-dev/references/commands.md +297 -0
- package/skills/SourceMonkey-dev/references/other-managers.md +63 -0
- package/skills/SourceMonkey-dev/references/specs.md +46 -0
- package/skills/SourceMonkey-friction/SKILL.md +103 -0
- 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
|
+
}
|