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,607 @@
|
|
|
1
|
+
// A run's connection to the real SourceMonkey in its browser: the calls
|
|
2
|
+
// it makes into the extension's service worker, the tabs it claims, and
|
|
3
|
+
// the events the worker streams back. The extension's side is
|
|
4
|
+
// src/extension/sm-dev-link.ts; the design is docs/sm-dev-design.md.
|
|
5
|
+
//
|
|
6
|
+
// - Calls go through `self.SourceMonkey` in the worker, with Playwright's
|
|
7
|
+
// `worker.evaluate`.
|
|
8
|
+
// - Chrome stops an idle worker and starts a new one later. A call made
|
|
9
|
+
// while the known one is stopped goes to the new one, which is
|
|
10
|
+
// listened to and subscribed to again; the run is told, since events
|
|
11
|
+
// in between are lost.
|
|
12
|
+
// - The run's claims and its subscription are a lease, renewed here
|
|
13
|
+
// while the run lasts, and released at its end. A renewal also claims
|
|
14
|
+
// again any of the run's tabs whose claim the extension lost.
|
|
15
|
+
// - A run's scripts go into a test collection of its own,
|
|
16
|
+
// which the extension removes when the run is released, or a while
|
|
17
|
+
// after its lease runs out.
|
|
18
|
+
// - It marks the browser's pages, so a `document-start` wrapper knows
|
|
19
|
+
// whether it can run without asking the extension
|
|
20
|
+
// (`markBrowser`, `setVouch`; docs/sm-dev-design.md#making-wrappers-ask).
|
|
21
|
+
import { randomBytes } from 'node:crypto';
|
|
22
|
+
import { SM_DEV_EVENT, SM_DEV_MARKERS, SM_DEV_PROTOCOL, } from '../sm-dev-protocol.js';
|
|
23
|
+
import { DROP_BROWSER_MODE, findExtensionWorker, isSourceMonkeyWorker, sourceMonkeyExtensionId, sourceMonkeyOrigin, } from './extension-backend.js';
|
|
24
|
+
import { rebuildAdvice } from './rebuild-advice.js';
|
|
25
|
+
// How long a claim lasts unrenewed, and how often it's renewed. A killed
|
|
26
|
+
// run's tabs run the extension's scripts again within the lease.
|
|
27
|
+
const LEASE_MS = 30_000;
|
|
28
|
+
const RENEW_MS = 10_000;
|
|
29
|
+
// How long the extension watches for the navigation that names a tab.
|
|
30
|
+
const NAMING_MS = 10_000;
|
|
31
|
+
// The world the per-tab marker's listener runs in, out of the page's
|
|
32
|
+
// sight. A listener on `window` hears events dispatched from any world.
|
|
33
|
+
const VOUCH_WORLD = 'sourcemonkey-sm-dev';
|
|
34
|
+
// A marker: a window listener that cancels events of its type.
|
|
35
|
+
const markerSource = (type) => `window.addEventListener(${JSON.stringify(type)}, function (e) { e.preventDefault(); });`;
|
|
36
|
+
// The contexts whose pages carry the marker every page does.
|
|
37
|
+
const markedContexts = new WeakSet();
|
|
38
|
+
/**
|
|
39
|
+
* Marks every page of the browser as one sm-dev is connected to, so a
|
|
40
|
+
* wrapper there asks the extension before running, since any tab may be
|
|
41
|
+
* claimed. Through Playwright's context init script, which Playwright
|
|
42
|
+
* adds to a new tab before letting it load, a tab a page opens
|
|
43
|
+
* included. Once per context: it lasts as long as the connection.
|
|
44
|
+
*/
|
|
45
|
+
async function markBrowser(context) {
|
|
46
|
+
if (markedContexts.has(context))
|
|
47
|
+
return;
|
|
48
|
+
try {
|
|
49
|
+
await context.addInitScript({ content: markerSource(SM_DEV_MARKERS.present) });
|
|
50
|
+
}
|
|
51
|
+
catch (err) {
|
|
52
|
+
throw new Error(`Could not mark the browser's pages as sm-dev's, so SourceMonkey's scripts would run in the run's tabs: ${firstLine(err)}`);
|
|
53
|
+
}
|
|
54
|
+
markedContexts.add(context);
|
|
55
|
+
}
|
|
56
|
+
// How to bring each side up to date, for the messages below.
|
|
57
|
+
const UPDATE_EXTENSION = `Update it: rebuild it and reload it in chrome://extensions, or ${DROP_BROWSER_MODE}, which loads this sm-dev's copy.`;
|
|
58
|
+
const updateSmDev = () => `Update sm-dev: ${rebuildAdvice()} Or ${DROP_BROWSER_MODE}, which loads this sm-dev's copy of SourceMonkey.`;
|
|
59
|
+
/** Said when the extension in the browser has an older protocol major, or none. */
|
|
60
|
+
const EXTENSION_TOO_OLD = `SourceMonkey in this browser is older than this sm-dev, so the two can't work together. ${UPDATE_EXTENSION}`;
|
|
61
|
+
/**
|
|
62
|
+
* How the extension's protocol version compares with sm-dev's
|
|
63
|
+
* (`SM_DEV_PROTOCOL`): an error for a major that differs, a note for a
|
|
64
|
+
* minor that does, or null when they match.
|
|
65
|
+
*/
|
|
66
|
+
export function compareProtocol(extension, ours = SM_DEV_PROTOCOL) {
|
|
67
|
+
// Built before the version existed.
|
|
68
|
+
if (!extension || extension.major < ours.major)
|
|
69
|
+
return { error: EXTENSION_TOO_OLD };
|
|
70
|
+
if (extension.major > ours.major) {
|
|
71
|
+
return { error: `This sm-dev is older than SourceMonkey in this browser, so the two can't work together. ${updateSmDev()}` };
|
|
72
|
+
}
|
|
73
|
+
if (extension.minor < ours.minor) {
|
|
74
|
+
return { note: `SourceMonkey in this browser is older than this sm-dev, so some of sm-dev's newer features won't work there. ${UPDATE_EXTENSION}` };
|
|
75
|
+
}
|
|
76
|
+
if (extension.minor > ours.minor) {
|
|
77
|
+
return { note: `This sm-dev is older than SourceMonkey in this browser, so it lacks some newer features. ${updateSmDev()}` };
|
|
78
|
+
}
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* The worker's protocol version, compared with sm-dev's: throws for a
|
|
83
|
+
* major that differs, and gives the note for a minor that does.
|
|
84
|
+
*/
|
|
85
|
+
async function checkedProtocol(worker) {
|
|
86
|
+
const protocol = await worker.evaluate(() => self.SourceMonkey?.smDevProtocol);
|
|
87
|
+
const compared = compareProtocol(protocol);
|
|
88
|
+
if (compared?.error)
|
|
89
|
+
throw new Error(compared.error);
|
|
90
|
+
return { protocol, note: compared?.note ?? null };
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Throws, naming `feature`, when the extension's protocol minor is older
|
|
94
|
+
* than `minor`.
|
|
95
|
+
*/
|
|
96
|
+
function requireMinorOf(extension, minor, feature) {
|
|
97
|
+
if ((extension?.minor ?? 0) < minor) {
|
|
98
|
+
throw new Error(`${feature} needs a newer SourceMonkey in this browser. ${UPDATE_EXTENSION}`);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
/** Said when Chrome won't let the extension register user scripts. */
|
|
102
|
+
function userScriptsOff(extensionId) {
|
|
103
|
+
return 'SourceMonkey in this browser does not have "Allow User Scripts" on, so it can\'t run sm-dev\'s scripts. '
|
|
104
|
+
+ `Turn it on in SourceMonkey's details page, chrome://extensions/?id=${extensionId}, or ${DROP_BROWSER_MODE}, which has it on.`;
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* A run's id in the extension: sm-dev's run id (`D2`, or a named one)
|
|
108
|
+
* with a random tail, since run ids are reused once a run ends. Kept to
|
|
109
|
+
* the characters a URL path segment allows, since its scripts' URLs
|
|
110
|
+
* carry it.
|
|
111
|
+
*/
|
|
112
|
+
export function linkRunId(runId) {
|
|
113
|
+
const head = (runId ?? 'run').replace(/[^A-Za-z0-9._-]+/g, '-');
|
|
114
|
+
return `${head}-${randomBytes(2).toString('hex')}`;
|
|
115
|
+
}
|
|
116
|
+
export class ExtensionLink {
|
|
117
|
+
context;
|
|
118
|
+
runId;
|
|
119
|
+
workerUrl;
|
|
120
|
+
origin;
|
|
121
|
+
worker = null;
|
|
122
|
+
// Workers already listened to, so none gets a second listener.
|
|
123
|
+
heard = new WeakSet();
|
|
124
|
+
// Workers that have stopped, whose handles fail every call.
|
|
125
|
+
stopped = new WeakSet();
|
|
126
|
+
listeners = new Set();
|
|
127
|
+
restartListeners = new Set();
|
|
128
|
+
renewTimer = null;
|
|
129
|
+
// The tabs the run holds, for a renewal to claim again if lost.
|
|
130
|
+
tabs = new Map();
|
|
131
|
+
// The tabs the run claimed itself, each with the CDP session that
|
|
132
|
+
// marks it, and the marker's script while it has one (`setVouch`).
|
|
133
|
+
vouches = new Map();
|
|
134
|
+
closed = false;
|
|
135
|
+
// Set as `close` begins, so no renewal starts after it; every caller
|
|
136
|
+
// of `close` waits on it.
|
|
137
|
+
closing = null;
|
|
138
|
+
// The renewals in flight (the timer's and a new worker's can overlap),
|
|
139
|
+
// which `close` lets finish before it releases, so one landing late
|
|
140
|
+
// can't claim the run's tabs again.
|
|
141
|
+
renewing = new Set();
|
|
142
|
+
// A worker has been adopted before, so the next is a restart.
|
|
143
|
+
adopted = false;
|
|
144
|
+
/** The extension's protocol version, as `open` read it. */
|
|
145
|
+
extensionProtocol;
|
|
146
|
+
/** For the run to print: how the extension's protocol minor differs from sm-dev's, if it does. */
|
|
147
|
+
versionNote = null;
|
|
148
|
+
// Set for a run without SourceMonkey (`without`): what every call
|
|
149
|
+
// into the extension throws.
|
|
150
|
+
absent = null;
|
|
151
|
+
// The tab ids such a run makes up for its tabs, counting down from -1
|
|
152
|
+
// so they can't be taken for Chrome's.
|
|
153
|
+
madeUpTabId = 0;
|
|
154
|
+
onWorker = (worker) => {
|
|
155
|
+
if (isSourceMonkeyWorker(worker.url(), this.origin))
|
|
156
|
+
void this.adopt(worker).catch(() => undefined);
|
|
157
|
+
};
|
|
158
|
+
/**
|
|
159
|
+
* Throws, naming `feature`, when the extension's protocol minor is
|
|
160
|
+
* older than `minor`: for a call or event added in that minor, before
|
|
161
|
+
* using it.
|
|
162
|
+
*/
|
|
163
|
+
requireMinor(minor, feature) {
|
|
164
|
+
requireMinorOf(this.extensionProtocol, minor, feature);
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Whether this run has a SourceMonkey link: for output that depends
|
|
168
|
+
* on what the extension does. False for a run with another manager.
|
|
169
|
+
*/
|
|
170
|
+
get linked() {
|
|
171
|
+
return this.absent === null;
|
|
172
|
+
}
|
|
173
|
+
constructor(context,
|
|
174
|
+
/**
|
|
175
|
+
* The run's unique id (`linkRunId`), which its claims and its test
|
|
176
|
+
* collection carry.
|
|
177
|
+
*/
|
|
178
|
+
runId,
|
|
179
|
+
/** The extension's service worker, whose console carries the events. */
|
|
180
|
+
workerUrl,
|
|
181
|
+
/** The origin of the extension's pages and its scripts' world. */
|
|
182
|
+
origin) {
|
|
183
|
+
this.context = context;
|
|
184
|
+
this.runId = runId;
|
|
185
|
+
this.workerUrl = workerUrl;
|
|
186
|
+
this.origin = origin;
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* Connects to the extension in `context`'s browser, for a run. Its
|
|
190
|
+
* tabs run its own scripts, and it hears the extension's events.
|
|
191
|
+
* Throws when the extension's protocol major differs from sm-dev's
|
|
192
|
+
* (`compareProtocol`), or `userScriptsOff` when Chrome won't let it
|
|
193
|
+
* run scripts. A minor that differs leaves `versionNote` set.
|
|
194
|
+
*/
|
|
195
|
+
static async open(context, runId) {
|
|
196
|
+
const worker = await findExtensionWorker(context);
|
|
197
|
+
const link = new ExtensionLink(context, runId, worker.url(), await sourceMonkeyOrigin());
|
|
198
|
+
const checked = await checkedProtocol(worker);
|
|
199
|
+
link.extensionProtocol = checked.protocol;
|
|
200
|
+
link.versionNote = checked.note;
|
|
201
|
+
// Chrome lists `chrome.userScripts` once the manifest asks for it,
|
|
202
|
+
// but every call fails until the user turns on "Allow User
|
|
203
|
+
// Scripts", which would leave the run's scripts silently unrun.
|
|
204
|
+
// The collections page's banner probes the same way.
|
|
205
|
+
const userScriptsOn = await worker.evaluate(async () => {
|
|
206
|
+
try {
|
|
207
|
+
await chrome.userScripts.getScripts();
|
|
208
|
+
return true;
|
|
209
|
+
}
|
|
210
|
+
catch {
|
|
211
|
+
return false;
|
|
212
|
+
}
|
|
213
|
+
});
|
|
214
|
+
if (!userScriptsOn)
|
|
215
|
+
throw new Error(userScriptsOff(await sourceMonkeyExtensionId()));
|
|
216
|
+
// Before any claim, so no claimed tab's page goes unmarked.
|
|
217
|
+
await markBrowser(context);
|
|
218
|
+
await link.adopt(worker);
|
|
219
|
+
context.on('serviceworker', link.onWorker);
|
|
220
|
+
link.renewTimer = setInterval(() => { void link.renew().catch(() => undefined); }, RENEW_MS);
|
|
221
|
+
return link;
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* A link for a run whose manager isn't SourceMonkey: no extension to
|
|
225
|
+
* call, no tabs claimed, no events heard. Its tabs are plain browser
|
|
226
|
+
* tabs, and every call into the extension throws `reason`.
|
|
227
|
+
*/
|
|
228
|
+
static without(context, runId, reason) {
|
|
229
|
+
const link = new ExtensionLink(context, runId, '', 'chrome-extension://none');
|
|
230
|
+
link.absent = reason;
|
|
231
|
+
return link;
|
|
232
|
+
}
|
|
233
|
+
// Makes `worker` the one calls go to: listened to once, and the run
|
|
234
|
+
// subscribed in it.
|
|
235
|
+
async adopt(worker) {
|
|
236
|
+
if (this.closed || worker === this.worker)
|
|
237
|
+
return;
|
|
238
|
+
// Not `this.worker !== null`: a stopped worker is cleared as it
|
|
239
|
+
// closes, before the new one starts.
|
|
240
|
+
const replaced = this.adopted;
|
|
241
|
+
this.adopted = true;
|
|
242
|
+
this.worker = worker;
|
|
243
|
+
if (!this.heard.has(worker)) {
|
|
244
|
+
this.heard.add(worker);
|
|
245
|
+
worker.on('close', () => {
|
|
246
|
+
this.stopped.add(worker);
|
|
247
|
+
if (this.worker === worker)
|
|
248
|
+
this.worker = null;
|
|
249
|
+
});
|
|
250
|
+
worker.on('console', (message) => {
|
|
251
|
+
if (message.type() !== 'debug')
|
|
252
|
+
return;
|
|
253
|
+
const text = message.text();
|
|
254
|
+
if (!text.startsWith(`${SM_DEV_EVENT} `))
|
|
255
|
+
return;
|
|
256
|
+
let event;
|
|
257
|
+
try {
|
|
258
|
+
event = JSON.parse(text.slice(SM_DEV_EVENT.length + 1));
|
|
259
|
+
}
|
|
260
|
+
catch {
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
for (const listener of this.listeners)
|
|
264
|
+
listener(event);
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
const restored = await this.renewIn(worker);
|
|
268
|
+
if (replaced)
|
|
269
|
+
for (const listener of this.restartListeners)
|
|
270
|
+
listener(restored);
|
|
271
|
+
}
|
|
272
|
+
/** Hears each event the extension sends. */
|
|
273
|
+
onEvent(listener) {
|
|
274
|
+
this.listeners.add(listener);
|
|
275
|
+
}
|
|
276
|
+
/**
|
|
277
|
+
* Told when a new worker replaced the one before, whose events in
|
|
278
|
+
* between are lost, with how many of the run's tab claims had to be
|
|
279
|
+
* made again.
|
|
280
|
+
*/
|
|
281
|
+
onRestart(listener) {
|
|
282
|
+
this.restartListeners.add(listener);
|
|
283
|
+
}
|
|
284
|
+
// The worker to call: the current one, or the one running now if it
|
|
285
|
+
// stopped.
|
|
286
|
+
async current() {
|
|
287
|
+
if (this.worker && !this.stopped.has(this.worker))
|
|
288
|
+
return this.worker;
|
|
289
|
+
const worker = await findExtensionWorker(this.context);
|
|
290
|
+
await this.adopt(worker);
|
|
291
|
+
return worker;
|
|
292
|
+
}
|
|
293
|
+
/**
|
|
294
|
+
* Runs `fn` in the worker. Tried again once in a new worker when the
|
|
295
|
+
* one known stopped meanwhile; an error of the call's own is thrown,
|
|
296
|
+
* not repeated, since a call may not be safe to make twice.
|
|
297
|
+
*/
|
|
298
|
+
async call(fn, arg) {
|
|
299
|
+
if (this.absent !== null)
|
|
300
|
+
throw new Error(this.absent);
|
|
301
|
+
// Playwright's argument types don't follow a generic `A` through.
|
|
302
|
+
const evaluate = (worker) => worker.evaluate(fn, arg);
|
|
303
|
+
const worker = await this.current();
|
|
304
|
+
try {
|
|
305
|
+
return await evaluate(worker);
|
|
306
|
+
}
|
|
307
|
+
catch (err) {
|
|
308
|
+
if (this.closed)
|
|
309
|
+
throw err;
|
|
310
|
+
// The worker's `close` can come just after the call it failed.
|
|
311
|
+
for (let i = 0; i < 10 && !this.stopped.has(worker); i++)
|
|
312
|
+
await new Promise((r) => setTimeout(r, 25));
|
|
313
|
+
if (!this.stopped.has(worker))
|
|
314
|
+
throw err;
|
|
315
|
+
return evaluate(await this.current());
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
/**
|
|
319
|
+
* Claims `page`'s tab for the run before it loads the run's page. The
|
|
320
|
+
* tab is named to the extension by a navigation it watches for, to
|
|
321
|
+
* `about:blank#<fragment>`, and the claim is recorded before this
|
|
322
|
+
* returns. Leaves the tab on `about:blank`, for the run to load its
|
|
323
|
+
* page in. Returns the tab's id. Throws an error naming the step that
|
|
324
|
+
* failed. A link `without` SourceMonkey claims nothing, and makes up
|
|
325
|
+
* an id.
|
|
326
|
+
*/
|
|
327
|
+
async claimPage(page, scriptsOff = false) {
|
|
328
|
+
if (this.absent !== null)
|
|
329
|
+
return --this.madeUpTabId;
|
|
330
|
+
// Twice at most: the watch lives in the worker's memory, so a worker
|
|
331
|
+
// Chrome restarted in between has none, and the next one watches anew.
|
|
332
|
+
let tabId = await this.nameTab(page);
|
|
333
|
+
if (tabId === null)
|
|
334
|
+
tabId = await this.nameTab(page);
|
|
335
|
+
if (tabId === null)
|
|
336
|
+
throw new Error('SourceMonkey did not see the tab load about:blank, so it could not tell which tab it is');
|
|
337
|
+
await this.call(({ id, runId, ttl }) => self.SourceMonkey.claimTab(id, runId, ttl), { id: tabId, runId: this.runId, ttl: LEASE_MS });
|
|
338
|
+
if (scriptsOff)
|
|
339
|
+
await this.setTabScriptsOff(tabId, true);
|
|
340
|
+
this.tabs.set(tabId, { scriptsOff });
|
|
341
|
+
try {
|
|
342
|
+
const session = await this.context.newCDPSession(page);
|
|
343
|
+
this.vouches.set(tabId, { session, script: null });
|
|
344
|
+
// Without it, the tab's new documents don't run the marker.
|
|
345
|
+
await session.send('Page.enable');
|
|
346
|
+
await this.setVouch(tabId, !scriptsOff);
|
|
347
|
+
}
|
|
348
|
+
catch {
|
|
349
|
+
// Unmarked, the run's scripts ask the extension before running,
|
|
350
|
+
// which answers the same, later.
|
|
351
|
+
await this.forgetVouch(tabId);
|
|
352
|
+
}
|
|
353
|
+
// The fragment off by history, not a navigation: one to `about:blank`
|
|
354
|
+
// would swap the tab's main frame, and Playwright would then prefix
|
|
355
|
+
// the page's snapshot refs (`f1e5`) as if it were an iframe.
|
|
356
|
+
await page.evaluate(() => history.replaceState(null, '', location.href.split('#')[0]))
|
|
357
|
+
.catch(() => page.goto('about:blank').catch(() => undefined));
|
|
358
|
+
return tabId;
|
|
359
|
+
}
|
|
360
|
+
// The tab's `chrome.tabs` id, learned from a navigation the worker
|
|
361
|
+
// watches for (`watchForNamingNavigation`). Null when it heard none.
|
|
362
|
+
async nameTab(page) {
|
|
363
|
+
const fragment = `sm-dev-${randomBytes(8).toString('hex')}`;
|
|
364
|
+
await this.call(({ f, t }) => self.SourceMonkey.watchForNamingNavigation(f, t), { f: fragment, t: NAMING_MS });
|
|
365
|
+
try {
|
|
366
|
+
await page.goto(`about:blank#${fragment}`);
|
|
367
|
+
}
|
|
368
|
+
catch (err) {
|
|
369
|
+
// The watch ends on its own, as it times out.
|
|
370
|
+
throw new Error(`The tab could not load about:blank, to tell SourceMonkey which tab it is: ${firstLine(err)}`);
|
|
371
|
+
}
|
|
372
|
+
return this.call((f) => self.SourceMonkey.namedTab(f), fragment);
|
|
373
|
+
}
|
|
374
|
+
/**
|
|
375
|
+
* A tab the extension claimed for the run itself (one a run tab
|
|
376
|
+
* opened), for renewals to keep.
|
|
377
|
+
*/
|
|
378
|
+
holdTab(tabId, scriptsOff = false) {
|
|
379
|
+
if (!this.tabs.has(tabId))
|
|
380
|
+
this.tabs.set(tabId, { scriptsOff });
|
|
381
|
+
}
|
|
382
|
+
/** The run no longer holds the tab (it closed). */
|
|
383
|
+
forgetTab(tabId) {
|
|
384
|
+
this.tabs.delete(tabId);
|
|
385
|
+
void this.forgetVouch(tabId);
|
|
386
|
+
}
|
|
387
|
+
/**
|
|
388
|
+
* Marks a tab the run claimed, for every document it loads from now
|
|
389
|
+
* on, as one whose scripts on the run's own get `run`, or takes the
|
|
390
|
+
* marker away. Through a CDP script, which runs ahead of every
|
|
391
|
+
* extension script in a new document. It only reaches the tab: the
|
|
392
|
+
* tabs it opens, and its subframes, ask the extension. The marker is
|
|
393
|
+
* gone with the session, as the run closes or its connection drops.
|
|
394
|
+
*/
|
|
395
|
+
async setVouch(tabId, on) {
|
|
396
|
+
const vouch = this.vouches.get(tabId);
|
|
397
|
+
if (!vouch || on === (vouch.script !== null))
|
|
398
|
+
return;
|
|
399
|
+
if (on) {
|
|
400
|
+
const { identifier } = await vouch.session.send('Page.addScriptToEvaluateOnNewDocument', {
|
|
401
|
+
source: markerSource(SM_DEV_MARKERS.vouchPrefix + this.runId), worldName: VOUCH_WORLD,
|
|
402
|
+
});
|
|
403
|
+
vouch.script = identifier;
|
|
404
|
+
}
|
|
405
|
+
else {
|
|
406
|
+
await vouch.session.send('Page.removeScriptToEvaluateOnNewDocument', { identifier: vouch.script });
|
|
407
|
+
vouch.script = null;
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
/** Turns a claimed tab's scripts off or back on, for `--noscript` tabs. */
|
|
411
|
+
async setTabScriptsOff(tabId, off) {
|
|
412
|
+
// Off: the marker goes first, so no page in between runs a script.
|
|
413
|
+
if (off)
|
|
414
|
+
await this.setVouch(tabId, false).catch(() => this.forgetVouch(tabId));
|
|
415
|
+
await this.call(({ t, o }) => self.SourceMonkey.setTabScriptsOff(t, o), { t: tabId, o: off });
|
|
416
|
+
const held = this.tabs.get(tabId);
|
|
417
|
+
if (held)
|
|
418
|
+
held.scriptsOff = off;
|
|
419
|
+
if (!off)
|
|
420
|
+
await this.setVouch(tabId, true).catch(() => undefined);
|
|
421
|
+
}
|
|
422
|
+
// Drops a tab's marker session, and the marker with it.
|
|
423
|
+
async forgetVouch(tabId) {
|
|
424
|
+
const vouch = this.vouches.get(tabId);
|
|
425
|
+
this.vouches.delete(tabId);
|
|
426
|
+
await vouch?.session.detach().catch(() => undefined);
|
|
427
|
+
}
|
|
428
|
+
async renew() {
|
|
429
|
+
if (this.closing)
|
|
430
|
+
return;
|
|
431
|
+
const worker = await this.current();
|
|
432
|
+
// A worker started meanwhile was renewed as it was adopted.
|
|
433
|
+
if (worker === this.worker)
|
|
434
|
+
await this.renewIn(worker);
|
|
435
|
+
}
|
|
436
|
+
// Renews the run's lease in `worker`. Returns how many of its tab
|
|
437
|
+
// claims had to be made again.
|
|
438
|
+
renewIn(worker) {
|
|
439
|
+
if (this.closing)
|
|
440
|
+
return Promise.resolve(0);
|
|
441
|
+
const tabs = [...this.tabs].map(([tabId, t]) => ({ tabId, scriptsOff: t.scriptsOff }));
|
|
442
|
+
const renewal = worker.evaluate(({ runId, ttl, held }) => self.SourceMonkey.renewRun(runId, ttl, held), { runId: this.runId, ttl: LEASE_MS, held: tabs });
|
|
443
|
+
this.renewing.add(renewal);
|
|
444
|
+
void renewal.finally(() => this.renewing.delete(renewal)).catch(() => undefined);
|
|
445
|
+
return renewal;
|
|
446
|
+
}
|
|
447
|
+
/**
|
|
448
|
+
* Installs or replaces one of the run's scripts in its test collection,
|
|
449
|
+
* with the dependency bodies read from disk (`pushedFiles`).
|
|
450
|
+
*/
|
|
451
|
+
/**
|
|
452
|
+
* `source`: the file's `file://` URL, which `script list` shows. An
|
|
453
|
+
* extension before protocol 1.2 ignores it.
|
|
454
|
+
*/
|
|
455
|
+
async push(text, name, files = {}, source) {
|
|
456
|
+
return this.call(({ run, t, n, f, s }) => self.SourceMonkey.writeRunScript({ run, text: t, name: n, files: f, ...(s ? { source: s } : {}) }), { run: this.runId, t: text, n: name, f: files, s: source });
|
|
457
|
+
}
|
|
458
|
+
/** Which document `tabId` shows (see the worker's `tabDocument`), or null if it can't be read. */
|
|
459
|
+
async tabDocument(tabId) {
|
|
460
|
+
return this.call((id) => self.SourceMonkey.tabDocument(id), tabId);
|
|
461
|
+
}
|
|
462
|
+
/** How the script's injected entry maps back to its files; see `remapStackText`. */
|
|
463
|
+
async stackLayout(scriptId) {
|
|
464
|
+
return this.call((id) => self.SourceMonkey.getStackLayout(id), scriptId);
|
|
465
|
+
}
|
|
466
|
+
/**
|
|
467
|
+
* A tab's frames, live documents only, as Chrome lists them to the
|
|
468
|
+
* extension: straight from `chrome.webNavigation`, which every
|
|
469
|
+
* extension sm-dev works with has, so no protocol version gates it.
|
|
470
|
+
* Null when Chrome can't list them (the tab is gone).
|
|
471
|
+
*/
|
|
472
|
+
async allFrames(tabId) {
|
|
473
|
+
return this.call((id) => chrome.webNavigation.getAllFrames({ tabId: id }).then((frames) => frames?.map((f) => ({ frameId: f.frameId, parentFrameId: f.parentFrameId, documentId: f.documentId, url: f.url })) ?? null, () => null), tabId);
|
|
474
|
+
}
|
|
475
|
+
/** A script's stored values, in the wire form the extension keeps. */
|
|
476
|
+
async values(scriptId) {
|
|
477
|
+
return this.call((id) => self.SourceMonkey.getGmValues(id), scriptId);
|
|
478
|
+
}
|
|
479
|
+
/** A script's Log entries, oldest first, as its Log tab shows them. */
|
|
480
|
+
async scriptLog(scriptId) {
|
|
481
|
+
return this.call((id) => self.SourceMonkey.getScriptLog(id), scriptId);
|
|
482
|
+
}
|
|
483
|
+
/** Writes values (wire form) as another tab of the script would. */
|
|
484
|
+
async writeValues(scriptId, writes) {
|
|
485
|
+
await this.call(({ id, w }) => self.SourceMonkey.writeGmValues(id, w), { id: scriptId, w: writes });
|
|
486
|
+
}
|
|
487
|
+
/** The scripts the toolbar badge lists for a tab, with their status and collection. */
|
|
488
|
+
async scriptsOnTab(tabId) {
|
|
489
|
+
return this.call((id) => self.SourceMonkey.scriptsOnTab(id), tabId);
|
|
490
|
+
}
|
|
491
|
+
/**
|
|
492
|
+
* Fires a menu command in a tab, as clicking it in the toolbar menu
|
|
493
|
+
* does: in `documentId`, the document that registered it, when given.
|
|
494
|
+
* An extension before protocol 2.1 ignores it, firing the first match.
|
|
495
|
+
*/
|
|
496
|
+
async fireMenu(tabId, scriptId, commandId, documentId) {
|
|
497
|
+
const result = await this.call(({ t, s, c, d }) => self.SourceMonkey.fireActionContextMenuItem(t, s, c, d), { t: tabId, s: scriptId, c: commandId, d: documentId });
|
|
498
|
+
return result.ok;
|
|
499
|
+
}
|
|
500
|
+
/**
|
|
501
|
+
* Runs a manual script (`@run-at context-menu`) in a tab, as its popup
|
|
502
|
+
* "Run this script" does. Throws with the extension's reason when it
|
|
503
|
+
* can't.
|
|
504
|
+
*/
|
|
505
|
+
async runManualScript(tabId, scriptId) {
|
|
506
|
+
const result = await this.call(({ t, s }) => self.SourceMonkey.runManualScript(t, s), { t: tabId, s: scriptId });
|
|
507
|
+
if (!result.ok)
|
|
508
|
+
throw new Error(result.error ?? 'Internal error: SourceMonkey did not say why the script did not run');
|
|
509
|
+
}
|
|
510
|
+
/** The collections and installed scripts, for the `collection` and `script` commands. */
|
|
511
|
+
manager = managerControl((fn, arg) => this.call(fn, arg));
|
|
512
|
+
/** Clicks a shown notification, as the person would. False when it isn't showing. */
|
|
513
|
+
async clickNotification(notificationId) {
|
|
514
|
+
return this.call((id) => self.SourceMonkey.clickNotification(id), notificationId);
|
|
515
|
+
}
|
|
516
|
+
/**
|
|
517
|
+
* Answers a connection prompt, as the person would in its window.
|
|
518
|
+
* False when it isn't waiting.
|
|
519
|
+
*/
|
|
520
|
+
async answerConnectPrompt(promptId, choice) {
|
|
521
|
+
return this.call(({ p, c }) => self.SourceMonkey.answerConnectPrompt(p, c), { p: promptId, c: choice });
|
|
522
|
+
}
|
|
523
|
+
/** Closes a shown notification, as the person would. */
|
|
524
|
+
async closeNotification(notificationId) {
|
|
525
|
+
return this.call((id) => new Promise((resolve) => chrome.notifications.clear(id, resolve)), notificationId);
|
|
526
|
+
}
|
|
527
|
+
/**
|
|
528
|
+
* Releases the run's claims and subscription, which removes its test
|
|
529
|
+
* collection, and stops renewing.
|
|
530
|
+
*/
|
|
531
|
+
close() {
|
|
532
|
+
this.closing ??= this.release();
|
|
533
|
+
return this.closing;
|
|
534
|
+
}
|
|
535
|
+
async release() {
|
|
536
|
+
if (this.renewTimer)
|
|
537
|
+
clearInterval(this.renewTimer);
|
|
538
|
+
this.context.off('serviceworker', this.onWorker);
|
|
539
|
+
await Promise.allSettled(this.renewing);
|
|
540
|
+
// The markers go before the claims, so no page in between runs a
|
|
541
|
+
// script on the strength of one.
|
|
542
|
+
await Promise.allSettled([...this.vouches.values()].map((v) => v.session.detach()));
|
|
543
|
+
this.vouches.clear();
|
|
544
|
+
// Best effort: an unreleased claim runs out with its lease, and the
|
|
545
|
+
// collection goes a while later.
|
|
546
|
+
await this.call((runId) => self.SourceMonkey.releaseRun(runId), this.runId)
|
|
547
|
+
.catch(() => undefined);
|
|
548
|
+
this.closed = true;
|
|
549
|
+
this.listeners.clear();
|
|
550
|
+
this.restartListeners.clear();
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
/** An error's first line, without the call Playwright names at its start. */
|
|
554
|
+
export function firstLine(err) {
|
|
555
|
+
const line = err.message.split('\n')[0].replace(/^[\w.]+: /, '');
|
|
556
|
+
return line.charAt(0).toUpperCase() + line.slice(1);
|
|
557
|
+
}
|
|
558
|
+
/**
|
|
559
|
+
* The collection and script commands' connection to SourceMonkey, with
|
|
560
|
+
* no run: no tabs claimed, no events heard. For one command, made just
|
|
561
|
+
* after the worker is found, so it doesn't look for a new one.
|
|
562
|
+
* - Throws as `ExtensionLink.open` does for a protocol major that
|
|
563
|
+
* differs.
|
|
564
|
+
* - Doesn't need "Allow User Scripts", since nothing here runs a script.
|
|
565
|
+
*/
|
|
566
|
+
export async function openManagerLink(context) {
|
|
567
|
+
const worker = await findExtensionWorker(context);
|
|
568
|
+
const { note } = await checkedProtocol(worker);
|
|
569
|
+
return {
|
|
570
|
+
versionNote: note,
|
|
571
|
+
// Playwright's argument types don't follow a generic through.
|
|
572
|
+
manager: managerControl((fn, arg) => worker.evaluate(fn, arg)),
|
|
573
|
+
};
|
|
574
|
+
}
|
|
575
|
+
// The manager calls, made with `invoke` (in the worker).
|
|
576
|
+
function managerControl(invoke) {
|
|
577
|
+
// `self.SourceMonkey.managed`'s calls. A change it refuses fails with
|
|
578
|
+
// its message alone, without the `worker.evaluate: Error: `
|
|
579
|
+
// Playwright puts before it.
|
|
580
|
+
const managed = async (fn, arg) => {
|
|
581
|
+
try {
|
|
582
|
+
return await invoke(fn, arg);
|
|
583
|
+
}
|
|
584
|
+
catch (err) {
|
|
585
|
+
throw new Error(firstLine(err).replace(/^Error: /, ''), { cause: err });
|
|
586
|
+
}
|
|
587
|
+
};
|
|
588
|
+
return {
|
|
589
|
+
label: 'SourceMonkey',
|
|
590
|
+
collections: () => invoke(() => self.SourceMonkey.managedCollections(), undefined),
|
|
591
|
+
scripts: () => invoke(() => self.SourceMonkey.managedScripts(), undefined),
|
|
592
|
+
onDisk: () => invoke(() => self.SourceMonkey.managed.onDisk(), undefined),
|
|
593
|
+
setScriptEnabled: (id, enabled) => managed(({ i, e }) => self.SourceMonkey.managed.setScriptEnabled(i, e), { i: id, e: enabled }),
|
|
594
|
+
setCollectionEnabled: (id, enabled) => managed(({ i, e }) => self.SourceMonkey.managed.setCollectionEnabled(i, e), { i: id, e: enabled }),
|
|
595
|
+
refreshScript: (id) => managed((i) => self.SourceMonkey.managed.refreshScript(i), id),
|
|
596
|
+
refreshCollection: (id) => managed((i) => self.SourceMonkey.managed.refreshCollection(i), id),
|
|
597
|
+
installScript: (location, collectionId) => managed(({ l, c }) => self.SourceMonkey.managed.installScript(l, c), { l: location, c: collectionId }),
|
|
598
|
+
uninstallScript: (id) => managed((i) => self.SourceMonkey.managed.uninstallScript(i), id),
|
|
599
|
+
moveScript: (id, collectionId) => managed(({ i, c }) => self.SourceMonkey.managed.moveScript(i, c), { i: id, c: collectionId }),
|
|
600
|
+
renameCollection: (id, name) => managed(({ i, n }) => self.SourceMonkey.managed.renameCollection(i, n), { i: id, n: name }),
|
|
601
|
+
scriptDetails: (id) => managed((i) => self.SourceMonkey.managed.scriptDetails(i), id),
|
|
602
|
+
scriptText: (id) => managed((i) => self.SourceMonkey.managed.scriptText(i), id),
|
|
603
|
+
scriptFiles: (id) => managed((i) => self.SourceMonkey.managed.scriptFiles(i), id),
|
|
604
|
+
// Async, so a failure rejects, which `show` passes over.
|
|
605
|
+
scriptIcons: async (id, load) => managed(({ i, l }) => self.SourceMonkey.managed.scriptIcons(i, l), { i: id, l: load }),
|
|
606
|
+
};
|
|
607
|
+
}
|