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,787 @@
|
|
|
1
|
+
// Building a script row from its text: parsing and validating the
|
|
2
|
+
// metadata block, deriving its matching rules and startup notes,
|
|
3
|
+
// loading its `@require` / `@resource` files, and the parse check.
|
|
4
|
+
//
|
|
5
|
+
// The half of the install pipeline that touches no storage, so the
|
|
6
|
+
// development harness (src/harness/prepare.ts) can run it under Node
|
|
7
|
+
// without the store around it. script-store.ts calls it for every add
|
|
8
|
+
// and refresh, and re-exports what its callers know it for.
|
|
9
|
+
import { getValue, getValues, isContextMenuRunAt, parseHeader, splitHeaderBody, unsupportedRunAt, NO_METADATA_BLOCK, } from './userscript-header.js';
|
|
10
|
+
import { attachFiles, checkScriptIcon, distillRequires, distillResources, } from './external-files.js';
|
|
11
|
+
import { localSearchRoots, localRootsReady } from './local-file-overrides.js';
|
|
12
|
+
import { iconDeclFromHeader } from './script-icon.js';
|
|
13
|
+
import { canonicalizeGrant, findApi, resolveGrants } from './gm-api-catalog.js';
|
|
14
|
+
import { hasNoConnectRules } from './gm-connect.js';
|
|
15
|
+
import { sourceUsesUnsafeWindow } from './unsafe-window-use.js';
|
|
16
|
+
import { resolveWorld } from './script-world.js';
|
|
17
|
+
import { ignoredMatchMessage, matchPatternProblem, unusableMatchMessage, } from './url-match.js';
|
|
18
|
+
import { validLocation, } from './diagnostic-location.js';
|
|
19
|
+
import { sha256Hex } from './bytes.js';
|
|
20
|
+
import { dlog } from './debug-log.js';
|
|
21
|
+
import { timePhase } from './phase-timing.js';
|
|
22
|
+
// Compute the universal change-detection token for a fetched script
|
|
23
|
+
// source. Uses the Web Crypto SubtleCrypto SHA-256 — available
|
|
24
|
+
// uniformly in service workers, page contexts, and Node ≥19, so the
|
|
25
|
+
// same helper works in production and in unit tests with no
|
|
26
|
+
// per-environment polyfill. Async by necessity (SubtleCrypto has no
|
|
27
|
+
// sync form on the web platform); buildScriptFromText and
|
|
28
|
+
// rebuildCollection (script-store.ts) are async in turn.
|
|
29
|
+
export async function hashSource(text) {
|
|
30
|
+
return sha256Hex(text);
|
|
31
|
+
}
|
|
32
|
+
// Pull `@match` / `@exclude` / `@include` lists out of a parsed header.
|
|
33
|
+
// These feed the future page-matching/injection logic.
|
|
34
|
+
export function distillMatching(header) {
|
|
35
|
+
if (!header)
|
|
36
|
+
return { matches: [], excludes: [], includes: [] };
|
|
37
|
+
return {
|
|
38
|
+
matches: getValues(header, 'match'),
|
|
39
|
+
excludes: getValues(header, 'exclude'),
|
|
40
|
+
includes: getValues(header, 'include'),
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
// Derive parse-time diagnostics from the metadata block (plus one fact
|
|
44
|
+
// about the body: whether it names `unsafeWindow`). Producers
|
|
45
|
+
// so far: unknown `@grant` names, `unsafeWindow` in a script that asks
|
|
46
|
+
// to run isolated, `GM_xmlhttpRequest` with no
|
|
47
|
+
// `@connect`, an unsupported `@run-at`, and bad `@match` patterns. Add others here as they come up (e.g.
|
|
48
|
+
// deprecated header tags, soft `@resource` failures,
|
|
49
|
+
// `@grant`-but-never-called dead grants). Anything that blocks
|
|
50
|
+
// `validateHeader` belongs in there instead — error rows skip the
|
|
51
|
+
// startup-logs attachment in `buildScriptFromText`.
|
|
52
|
+
// Said when a script asks for `unsafeWindow` (by `@grant` or by using
|
|
53
|
+
// the name) and also asks to run isolated, naming the header that did
|
|
54
|
+
// (`@inject-into content` or `@sandbox DOM`). Exported so the tests and
|
|
55
|
+
// any other surface recognize it by identity rather than by matching
|
|
56
|
+
// its wording.
|
|
57
|
+
export function isolatedUnsafeWindowWarning(cause) {
|
|
58
|
+
return 'This script cannot reach the page through unsafeWindow. '
|
|
59
|
+
+ `${cause} makes the script run isolated from main page content, so the `
|
|
60
|
+
+ `page's variables read as undefined. Remove ${cause} to reach them.`;
|
|
61
|
+
}
|
|
62
|
+
// The header line that puts a script in the isolated world, in the form
|
|
63
|
+
// a message names it, or undefined when it runs in the page.
|
|
64
|
+
function isolatingHeader(header) {
|
|
65
|
+
if (resolveWorld(header) !== 'USER_SCRIPT')
|
|
66
|
+
return undefined;
|
|
67
|
+
const injectInto = (getValue(header, 'inject-into') ?? '').trim().toLowerCase();
|
|
68
|
+
return injectInto === 'content' ? '@inject-into content' : '@sandbox DOM';
|
|
69
|
+
}
|
|
70
|
+
// Said when a script grants an API that `@connect` gates (today only
|
|
71
|
+
// `GM_xmlhttpRequest`) but declares no `@connect` line: each other site
|
|
72
|
+
// it contacts needs the user's answer to a prompt (see gm-connect.ts).
|
|
73
|
+
// `grant` is the name as the script wrote it (`GM.xmlHttpRequest` or the
|
|
74
|
+
// legacy form).
|
|
75
|
+
export function noConnectInfo(grant) {
|
|
76
|
+
return `This script has no @connect lines, so SourceMonkey asks before its ${grant} requests reach another site. `
|
|
77
|
+
+ 'Add @connect for each host it contacts, or @connect * to allow any host.';
|
|
78
|
+
}
|
|
79
|
+
// A manual script (`@run-at context-menu`): it matches pages like any
|
|
80
|
+
// other, but never runs on page load, only when the person asks for it
|
|
81
|
+
// in the popup or sm-dev fires it. See docs/manual-scripts.md.
|
|
82
|
+
export function isManualScript(script) {
|
|
83
|
+
return isContextMenuRunAt(script.header);
|
|
84
|
+
}
|
|
85
|
+
// Said for a `@run-at` value we don't honor (a typo). `context-menu`
|
|
86
|
+
// isn't one: it makes the script manual (see `CONTEXT_MENU_RUN_AT`).
|
|
87
|
+
export function unsupportedRunAtWarning(value) {
|
|
88
|
+
return `@run-at ${value} is not supported. The script runs at document-idle instead.`;
|
|
89
|
+
}
|
|
90
|
+
export function extractStartupLogs(header, relativePath,
|
|
91
|
+
// `sourceUsesUnsafeWindow(bodyText)` — the body names `unsafeWindow`,
|
|
92
|
+
// grant or no grant. Passed in rather than derived here so the scan
|
|
93
|
+
// happens once per load, on the same text the row stores.
|
|
94
|
+
usesUnsafeWindow = false) {
|
|
95
|
+
if (!header)
|
|
96
|
+
return [];
|
|
97
|
+
const logs = [];
|
|
98
|
+
const grants = getValues(header, 'grant');
|
|
99
|
+
const resolved = resolveGrants(grants);
|
|
100
|
+
const isolatedBy = isolatingHeader(header);
|
|
101
|
+
// The two ways a script asks for the page's window: the grant line,
|
|
102
|
+
// and naming `unsafeWindow` in its body without one. Everything below
|
|
103
|
+
// treats them alike — what the script needs is the same either way.
|
|
104
|
+
const wantsUnsafeWindow = resolved.granted.has('unsafeWindow') || usesUnsafeWindow;
|
|
105
|
+
if (grants.length > 0) {
|
|
106
|
+
// `none` wins over every other `@grant` line in `resolveGrants`: no
|
|
107
|
+
// bindings and no sandbox `window`. The author
|
|
108
|
+
// probably meant the other lines, so say what happened to them.
|
|
109
|
+
// Only the grants we'd otherwise have honored; a typo'd name gets
|
|
110
|
+
// its own "unsupported" line below and shouldn't be counted twice.
|
|
111
|
+
const overridden = grants
|
|
112
|
+
.map((g) => g.trim())
|
|
113
|
+
.filter((g) => g !== '' && g.toLowerCase() !== 'none' && canonicalizeGrant(g) !== null);
|
|
114
|
+
if (resolved.hasNone && overridden.length > 0) {
|
|
115
|
+
const log = {
|
|
116
|
+
level: 'warn',
|
|
117
|
+
message: `@grant none overrides @grant ${overridden.join(', ')}, so the script runs in the ` +
|
|
118
|
+
'page with no GM APIs. Remove the none line to use them.',
|
|
119
|
+
};
|
|
120
|
+
const line = headerTagLine(header, 'grant', 'none');
|
|
121
|
+
if (line !== undefined)
|
|
122
|
+
log.location = { file: relativePath, line };
|
|
123
|
+
logs.push(log);
|
|
124
|
+
}
|
|
125
|
+
if (resolved.unknown.length > 0) {
|
|
126
|
+
const log = {
|
|
127
|
+
level: 'warn',
|
|
128
|
+
message: `Ignoring unsupported @grant ${resolved.unknown.join(', ')}`,
|
|
129
|
+
};
|
|
130
|
+
// One message can name several bad grants, and they may be spread
|
|
131
|
+
// over several `@grant` lines — so the location is the FIRST one,
|
|
132
|
+
// which is where a reader fixing them starts. The others are named
|
|
133
|
+
// in the message and are a line or two away.
|
|
134
|
+
const line = headerTagLine(header, 'grant', resolved.unknown[0]);
|
|
135
|
+
if (line !== undefined)
|
|
136
|
+
log.location = { file: relativePath, line };
|
|
137
|
+
logs.push(log);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
// `unsafeWindow` means "the page's real window". That only exists in
|
|
141
|
+
// MAIN world, and `@inject-into content` / `@sandbox DOM` pin the
|
|
142
|
+
// script to the isolated USER_SCRIPT world, where `unsafeWindow` can
|
|
143
|
+
// only be the isolated realm's own `window`: page globals read as
|
|
144
|
+
// `undefined`. We keep the binding (matching Violentmonkey's content
|
|
145
|
+
// mode) but say so out loud, because the alternative is a script that
|
|
146
|
+
// quietly does nothing. See docs/injection.md#execution-world.
|
|
147
|
+
if (wantsUnsafeWindow && isolatedBy) {
|
|
148
|
+
logs.push({ level: 'warn', message: isolatedUnsafeWindowWarning(isolatedBy) });
|
|
149
|
+
}
|
|
150
|
+
// A granted API that needs `@connect`, with none declared. One row
|
|
151
|
+
// even when both spellings are granted: they are one API.
|
|
152
|
+
if (hasNoConnectRules(header)) {
|
|
153
|
+
const needsConnect = grants
|
|
154
|
+
.map((g) => g.trim())
|
|
155
|
+
.find((g) => {
|
|
156
|
+
const name = canonicalizeGrant(g);
|
|
157
|
+
return name !== null && resolved.granted.has(name)
|
|
158
|
+
&& (findApi(name)?.requires ?? []).includes('connect');
|
|
159
|
+
});
|
|
160
|
+
if (needsConnect) {
|
|
161
|
+
const log = { level: 'info', message: noConnectInfo(needsConnect) };
|
|
162
|
+
const line = headerTagLine(header, 'grant', needsConnect);
|
|
163
|
+
if (line !== undefined)
|
|
164
|
+
log.location = { file: relativePath, line };
|
|
165
|
+
logs.push(log);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
const runAt = unsupportedRunAt(header);
|
|
169
|
+
if (runAt !== undefined) {
|
|
170
|
+
const log = { level: 'warn', message: unsupportedRunAtWarning(runAt) };
|
|
171
|
+
const line = headerTagLine(header, 'run-at', runAt);
|
|
172
|
+
if (line !== undefined)
|
|
173
|
+
log.location = { file: relativePath, line };
|
|
174
|
+
logs.push(log);
|
|
175
|
+
}
|
|
176
|
+
// A `@match` Chrome's grammar refuses. One of these would take the
|
|
177
|
+
// whole registration down (Chrome refuses the spec, not the pattern),
|
|
178
|
+
// so `buildRegistrationSpec` drops it — and this is what says so, on
|
|
179
|
+
// the line that declared it.
|
|
180
|
+
//
|
|
181
|
+
// One row per bad pattern, unlike the grants above: each has its own
|
|
182
|
+
// reason and its own line, so collapsing them would leave the
|
|
183
|
+
// location pointing at one of several unrelated problems.
|
|
184
|
+
for (const pattern of getValues(header, 'match')) {
|
|
185
|
+
const problem = matchPatternProblem(pattern);
|
|
186
|
+
if (!problem)
|
|
187
|
+
continue;
|
|
188
|
+
const log = { level: 'warn', message: ignoredMatchMessage(problem) };
|
|
189
|
+
const line = headerTagLine(header, 'match', pattern);
|
|
190
|
+
if (line !== undefined)
|
|
191
|
+
log.location = { file: relativePath, line };
|
|
192
|
+
logs.push(log);
|
|
193
|
+
}
|
|
194
|
+
return logs;
|
|
195
|
+
}
|
|
196
|
+
// The line of the `@<tag>` declaration whose value is `value`.
|
|
197
|
+
//
|
|
198
|
+
// No "near enough" fallback to the first `@<tag>` line: that line is
|
|
199
|
+
// usually a perfectly good declaration, and pointing at valid code is
|
|
200
|
+
// worse than pointing at nothing — it sends the reader somewhere with
|
|
201
|
+
// nothing wrong with it. `HeaderTag.value` is trimmed by `TAG_LINE`
|
|
202
|
+
// and the caller reports the trimmed text, so the match holds.
|
|
203
|
+
function headerTagLine(header, tag, value) {
|
|
204
|
+
for (const t of header.tags) {
|
|
205
|
+
if (t.tag !== tag || t.locale)
|
|
206
|
+
continue;
|
|
207
|
+
if (t.value.trim() === value)
|
|
208
|
+
return t.line;
|
|
209
|
+
}
|
|
210
|
+
return undefined;
|
|
211
|
+
}
|
|
212
|
+
// The messages `validateHeader` can return. Exported for the same
|
|
213
|
+
// reason the split errors are (see `userscript-header.ts`): the corpus
|
|
214
|
+
// validator groups failures by message, so these are the strings it
|
|
215
|
+
// matches against rather than literals maintained in two places.
|
|
216
|
+
//
|
|
217
|
+
// Same phrasing rule too — each has to read correctly alone in the
|
|
218
|
+
// viewer's message bar and after the preview's `This file cannot be
|
|
219
|
+
// installed: ` prefix.
|
|
220
|
+
export const NO_NAME = 'No @name in the metadata block';
|
|
221
|
+
export const NO_MATCH_OR_INCLUDE = 'No @match or @include in the metadata block, so this script would never run on any page';
|
|
222
|
+
// No longer produced: manual scripts are supported now. Kept so
|
|
223
|
+
// `retiredHeaderProblemIds` can find the rows an older build refused
|
|
224
|
+
// with it.
|
|
225
|
+
export const RETIRED_CONTEXT_MENU_UNSUPPORTED = '@run-at context-menu (run from the right-click menu) is not supported, so this script can\'t run';
|
|
226
|
+
// Opens every parse-check verdict (`Syntax error: <what V8 said>`).
|
|
227
|
+
// Exported for the same reason as the constants above: the corpus
|
|
228
|
+
// validator groups its survey by the message AFTER this prefix, so it
|
|
229
|
+
// has to strip exactly what we wrote. It used to carry its own regex,
|
|
230
|
+
// which stopped matching the moment this string was recased — and a
|
|
231
|
+
// stale recognizer mislabels a whole bucket instead of failing.
|
|
232
|
+
export const SYNTAX_ERROR_PREFIX = 'Syntax error: ';
|
|
233
|
+
// Verify the parsed header has the fields we require to consider a
|
|
234
|
+
// script `ok`. Returns null on success, otherwise a human-readable
|
|
235
|
+
// problem. Required: an `@name` plus at least one usable
|
|
236
|
+
// `@match` / `@include` (otherwise the script can never match a page).
|
|
237
|
+
//
|
|
238
|
+
// The missing-tag failures are about something that ISN'T there, so
|
|
239
|
+
// none of them has a line of its own. They point at the
|
|
240
|
+
// `// ==UserScript==` marker instead: that's the block the missing tag
|
|
241
|
+
// belongs in, and where the reader has to go to add it. The unusable
|
|
242
|
+
// `@match` case is the exception — the offending text is right there in
|
|
243
|
+
// the file, so it gets that line.
|
|
244
|
+
export function validateHeader(header) {
|
|
245
|
+
// No block at all — the whole file is the answer, so no line.
|
|
246
|
+
if (!header)
|
|
247
|
+
return { message: NO_METADATA_BLOCK };
|
|
248
|
+
const line = header.startLine;
|
|
249
|
+
if (!getValue(header, 'name'))
|
|
250
|
+
return { message: NO_NAME, line };
|
|
251
|
+
const matches = getValues(header, 'match');
|
|
252
|
+
const includes = getValues(header, 'include');
|
|
253
|
+
if (matches.length === 0 && includes.length === 0) {
|
|
254
|
+
return { message: NO_MATCH_OR_INCLUDE, line };
|
|
255
|
+
}
|
|
256
|
+
// Patterns Chrome's grammar refuses are dropped rather than run (see
|
|
257
|
+
// `matchPatternProblem`), so a script whose every `@match` is one of
|
|
258
|
+
// those — with no `@include` to fall back on — is in the same
|
|
259
|
+
// position as one that declared no pattern at all: it can never match
|
|
260
|
+
// a page. Say so with the FIRST bad pattern's own reason, which is
|
|
261
|
+
// more use than the generic sentence above and points at a real line.
|
|
262
|
+
//
|
|
263
|
+
// Only the first: the others' reasons ride on `startupLogs` for an
|
|
264
|
+
// `ok` script, and an `invalid` row doesn't carry those. A reader who
|
|
265
|
+
// fixes this one sees the next on the following load, which is the
|
|
266
|
+
// same one-at-a-time path every other header failure takes.
|
|
267
|
+
if (includes.length === 0) {
|
|
268
|
+
const problems = matches.map((m) => matchPatternProblem(m));
|
|
269
|
+
const first = problems[0];
|
|
270
|
+
if (first && problems.every((p) => p !== null)) {
|
|
271
|
+
return {
|
|
272
|
+
message: unusableMatchMessage(first),
|
|
273
|
+
line: headerTagLine(header, 'match', first.pattern) ?? line,
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
return null;
|
|
278
|
+
}
|
|
279
|
+
// Compose a `StoredScript` from freshly fetched source text. Always
|
|
280
|
+
// records the new content (so the viewer reflects what's on disk),
|
|
281
|
+
// then determines status by running it through split + parse +
|
|
282
|
+
// validate. Any failure along that chain lands in `errorMessage` while
|
|
283
|
+
// the content fields stay populated. Async because hashing the source
|
|
284
|
+
// for the universal change-detection token uses SubtleCrypto.
|
|
285
|
+
export async function buildScriptFromText(args) {
|
|
286
|
+
const split = splitHeaderBody(args.text);
|
|
287
|
+
const header = parseHeader(args.text);
|
|
288
|
+
const matching = distillMatching(header);
|
|
289
|
+
// UTF-16 string length ≠ file byte length for non-ASCII content;
|
|
290
|
+
// TextEncoder gives us the wire-accurate UTF-8 byte count.
|
|
291
|
+
const size = new TextEncoder().encode(args.text).length;
|
|
292
|
+
const contentHash = await hashSource(args.text);
|
|
293
|
+
let status = 'ok';
|
|
294
|
+
let errorMessage;
|
|
295
|
+
let errorLine;
|
|
296
|
+
if (split.error) {
|
|
297
|
+
status = 'invalid';
|
|
298
|
+
errorMessage = split.error;
|
|
299
|
+
// UNTERMINATED_METADATA_BLOCK gets the line that opened the block
|
|
300
|
+
// that never closed. NO_METADATA_BLOCK gets nothing — there is no
|
|
301
|
+
// marker to point at, and the answer is the file as a whole.
|
|
302
|
+
errorLine = split.headerStartLine;
|
|
303
|
+
}
|
|
304
|
+
else {
|
|
305
|
+
const problem = validateHeader(header);
|
|
306
|
+
if (problem) {
|
|
307
|
+
status = 'invalid';
|
|
308
|
+
errorMessage = problem.message;
|
|
309
|
+
errorLine = problem.line;
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
// Same bytes as last time → preserve the prior load timestamp so
|
|
313
|
+
// the collections page's "what changed" highlight stays accurate even
|
|
314
|
+
// when the cheap skip paths didn't fire. Status still reflects the
|
|
315
|
+
// current parse outcome (parser fixes between refreshes are rare
|
|
316
|
+
// but shouldn't be silently masked).
|
|
317
|
+
const unchanged = args.prior?.contentHash === contentHash;
|
|
318
|
+
const loadedAt = unchanged && args.prior ? args.prior.loadedAt : args.now;
|
|
319
|
+
const entry = {
|
|
320
|
+
id: args.id,
|
|
321
|
+
collectionId: args.collectionId,
|
|
322
|
+
relativePath: args.relativePath,
|
|
323
|
+
fileUrl: args.fileUrl,
|
|
324
|
+
status,
|
|
325
|
+
size,
|
|
326
|
+
contentHash,
|
|
327
|
+
headerText: split.headerText,
|
|
328
|
+
bodyText: split.bodyText,
|
|
329
|
+
header,
|
|
330
|
+
matching,
|
|
331
|
+
loadedAt,
|
|
332
|
+
// New rows default enabled; existing rows preserve their prior
|
|
333
|
+
// value via the merge in rebuildCollection / refreshScript
|
|
334
|
+
// (script-store.ts).
|
|
335
|
+
enabled: true,
|
|
336
|
+
};
|
|
337
|
+
if (errorMessage) {
|
|
338
|
+
setScriptError(entry, errorMessage, locationIn(args.relativePath, errorLine));
|
|
339
|
+
}
|
|
340
|
+
if (args.httpEtag)
|
|
341
|
+
entry.httpEtag = args.httpEtag;
|
|
342
|
+
if (args.httpLastModified)
|
|
343
|
+
entry.httpLastModified = args.httpLastModified;
|
|
344
|
+
// Only persist leadingText when non-empty so the vast majority of
|
|
345
|
+
// scripts (which start at `// ==UserScript==`) don't carry an
|
|
346
|
+
// empty-string field.
|
|
347
|
+
if (split.leadingText)
|
|
348
|
+
entry.leadingText = split.leadingText;
|
|
349
|
+
// Parse-time diagnostics. Only attached when status is `ok` — error
|
|
350
|
+
// rows don't run and never reach the wrapper, so the diagnostics
|
|
351
|
+
// would just be dead bytes in storage. Same shape filter for the
|
|
352
|
+
// empty case keeps the persisted row free of empty arrays.
|
|
353
|
+
if (status === 'ok') {
|
|
354
|
+
// Derived once, here, from the text this row stores — the world
|
|
355
|
+
// decision and the emitted bindings both read the persisted flag.
|
|
356
|
+
const usesUnsafeWindow = sourceUsesUnsafeWindow(split.bodyText);
|
|
357
|
+
if (usesUnsafeWindow)
|
|
358
|
+
entry.usesUnsafeWindow = true;
|
|
359
|
+
const startupLogs = extractStartupLogs(header, args.relativePath, usesUnsafeWindow);
|
|
360
|
+
if (startupLogs.length > 0)
|
|
361
|
+
entry.startupLogs = startupLogs;
|
|
362
|
+
}
|
|
363
|
+
return entry;
|
|
364
|
+
}
|
|
365
|
+
// Apply a hard fetch failure (network error, 404, unreadable file,
|
|
366
|
+
// disconnected mount) to an existing entry, preserving the prior
|
|
367
|
+
// content either way.
|
|
368
|
+
//
|
|
369
|
+
// - prior was `ok`: this is a *refresh* failure, not a content
|
|
370
|
+
// problem — we have a last-known-good copy. Keep it running
|
|
371
|
+
// (`status: 'ok'`) and record `refreshError` so the UI can flag
|
|
372
|
+
// REFRESH FAILED. A transient logout / network blip must never
|
|
373
|
+
// silently disable a working script.
|
|
374
|
+
// - prior was anything else (already invalid): no good copy to
|
|
375
|
+
// protect, so flip to `invalid` with the message as before.
|
|
376
|
+
export function applyFetchError(existing, errorMessage) {
|
|
377
|
+
if (existing.status === 'ok') {
|
|
378
|
+
return { ...existing, refreshError: errorMessage };
|
|
379
|
+
}
|
|
380
|
+
const next = { ...existing, status: 'invalid' };
|
|
381
|
+
// No location: we never got the bytes, so there is no content this
|
|
382
|
+
// message is about. Routed through the setter so the prior row's
|
|
383
|
+
// location can't survive into a message it has nothing to do with.
|
|
384
|
+
setScriptError(next, errorMessage, undefined);
|
|
385
|
+
return next;
|
|
386
|
+
}
|
|
387
|
+
// Write a row's error message and its location together. They are one
|
|
388
|
+
// fact stored in two fields, and every path that changes one has to
|
|
389
|
+
// change the other: a location left behind by a previous failure would
|
|
390
|
+
// point the reader at a line that has nothing to do with what the row
|
|
391
|
+
// now says. Passing `undefined` for the message clears both.
|
|
392
|
+
//
|
|
393
|
+
// The location is validated on the way in rather than at each of the
|
|
394
|
+
// surfaces that renders it — these rows are persisted and reloaded, and
|
|
395
|
+
// a click target is not the place to discover a malformed line number.
|
|
396
|
+
export function setScriptError(entry, message, location) {
|
|
397
|
+
if (message === undefined)
|
|
398
|
+
delete entry.errorMessage;
|
|
399
|
+
else
|
|
400
|
+
entry.errorMessage = message;
|
|
401
|
+
const valid = validLocation(location);
|
|
402
|
+
if (valid)
|
|
403
|
+
entry.errorLocation = valid;
|
|
404
|
+
else
|
|
405
|
+
delete entry.errorLocation;
|
|
406
|
+
}
|
|
407
|
+
// A place in the script's own source. `file` is the relative path
|
|
408
|
+
// because that is the identity the source map gives a script's own body
|
|
409
|
+
// (`buildParseCheckSource` passes it as `mainUrl`) and what the viewer
|
|
410
|
+
// matches its main tab against — see `DiagnosticLocation`.
|
|
411
|
+
//
|
|
412
|
+
// Returns a location even with no line: "this file" is still somewhere
|
|
413
|
+
// to go, and it is the honest answer for a file with no metadata block
|
|
414
|
+
// at all.
|
|
415
|
+
export function locationIn(file, line) {
|
|
416
|
+
return line === undefined ? { file } : { file, line };
|
|
417
|
+
}
|
|
418
|
+
// Build an `invalid` entry for a brand-new (never-loaded-successfully)
|
|
419
|
+
// script whose first fetch failed. No content to preserve, so the
|
|
420
|
+
// content fields stay empty and size is null.
|
|
421
|
+
export function buildInitialInvalidEntry(args) {
|
|
422
|
+
return {
|
|
423
|
+
id: args.id,
|
|
424
|
+
collectionId: args.collectionId,
|
|
425
|
+
relativePath: args.relativePath,
|
|
426
|
+
fileUrl: args.fileUrl,
|
|
427
|
+
status: 'invalid',
|
|
428
|
+
errorMessage: args.errorMessage,
|
|
429
|
+
size: null,
|
|
430
|
+
headerText: '',
|
|
431
|
+
bodyText: '',
|
|
432
|
+
header: null,
|
|
433
|
+
matching: { matches: [], excludes: [], includes: [] },
|
|
434
|
+
loadedAt: args.now,
|
|
435
|
+
enabled: true,
|
|
436
|
+
};
|
|
437
|
+
}
|
|
438
|
+
// Resolve + fetch a script's `@require` bodies AND `@resource` blobs,
|
|
439
|
+
// attaching both lists onto the StoredScript. On any failure preserves
|
|
440
|
+
// the prior's last-known-good state and flips status to 'invalid' with a
|
|
441
|
+
// message naming the offending URL — mirrors `applyFetchError`'s
|
|
442
|
+
// content-preservation policy for body-fetch failures, so a flaky
|
|
443
|
+
// dependency doesn't clobber the user's working version. Without
|
|
444
|
+
// declared deps a userscript usually breaks worse than not injecting,
|
|
445
|
+
// so erroring out is safer than running headless.
|
|
446
|
+
//
|
|
447
|
+
// `recheck === 'preserve'` reuses every cached prior verbatim and only
|
|
448
|
+
// fetches refs the script newly declares. `recheck === 'all'` refetches
|
|
449
|
+
// every declared file (used by explicit Refresh). The fetchCache is
|
|
450
|
+
// shared across all scripts in a single refresh pass so two scripts
|
|
451
|
+
// referencing the same canonical URL share one fetch — and a require
|
|
452
|
+
// + a resource pointing at the same URL also share a single fetch.
|
|
453
|
+
//
|
|
454
|
+
// Exported for the development harness (`src/harness/prepare.ts`),
|
|
455
|
+
// which runs this half of the pipeline under Node for `sm-dev` and the
|
|
456
|
+
// corpus validator. The point is to exercise the production path, so
|
|
457
|
+
// it calls this rather than restating the sequence.
|
|
458
|
+
export async function applyFiles(built, prior, recheck, fetchCache, now,
|
|
459
|
+
// What this script's collection says about local stand-ins — whether
|
|
460
|
+
// they're allowed, and the collection's own root. Defaults to
|
|
461
|
+
// disabled so a caller outside the extension keeps the plain
|
|
462
|
+
// declared-URL behavior.
|
|
463
|
+
localFiles = { enabled: false },
|
|
464
|
+
// Whether to wait for the parse verdict before returning. Defaults to
|
|
465
|
+
// 'await' so every caller that hasn't thought about it keeps today's
|
|
466
|
+
// behavior; see `ParseCheckMode`.
|
|
467
|
+
parseCheck = 'await',
|
|
468
|
+
// A scratch script's pushed bodies (sm-dev's local files), which the
|
|
469
|
+
// store reads from the script's `scratchFiles:` row on every pass,
|
|
470
|
+
// like a local stand-in, since a push may have changed them.
|
|
471
|
+
pushedFiles) {
|
|
472
|
+
const priorRequires = new Map((prior?.requires ?? []).map((r) => [r.url, r]));
|
|
473
|
+
const priorResources = new Map((prior?.resources ?? []).map((r) => [r.url, r]));
|
|
474
|
+
// Configured roots come from storage, so make sure that read has
|
|
475
|
+
// landed before asking — a refresh racing SW startup would otherwise
|
|
476
|
+
// resolve as if none were set and persist that.
|
|
477
|
+
if (localFiles.enabled)
|
|
478
|
+
await localRootsReady();
|
|
479
|
+
const localRoots = localSearchRoots(built.fileUrl, localFiles);
|
|
480
|
+
// Before the shortcut below, which returns early: the icon has its
|
|
481
|
+
// own rule for when to look again. Not for a scratch script: sm-dev
|
|
482
|
+
// pushes its `@require` / `@resource` files but not its icon, and
|
|
483
|
+
// has already checked the icon on disk itself.
|
|
484
|
+
if (!pushedFiles)
|
|
485
|
+
await applyIconCheck(built, prior, recheck, fetchCache, localRoots, localFiles.lookup);
|
|
486
|
+
// `recheck: 'preserve'` short-circuits when both lists are identical
|
|
487
|
+
// to the prior's: no resolve work, no fetch, just keep what we had.
|
|
488
|
+
// This is the hot path on opportunistic refresh.
|
|
489
|
+
//
|
|
490
|
+
// Skipped entirely when the script has local search roots: a stand-in
|
|
491
|
+
// has to be re-read every pass or an edit on disk wouldn't surface
|
|
492
|
+
// until the user clicked Refresh, which is the workflow the whole
|
|
493
|
+
// feature exists for. The per-entry loaders still preserve cached
|
|
494
|
+
// REMOTE entries in that case, so only local reads are paid for.
|
|
495
|
+
if (recheck === 'preserve' && prior && localRoots.length === 0 && !pushedFiles) {
|
|
496
|
+
const newReqRefs = distillRequires(built.header);
|
|
497
|
+
const sameReqs = newReqRefs.length === (prior.requires?.length ?? 0) &&
|
|
498
|
+
newReqRefs.every((ref, i) => prior.requires?.[i]?.ref === ref);
|
|
499
|
+
const newResDecls = distillResources(built.header);
|
|
500
|
+
const sameRess = newResDecls.length === (prior.resources?.length ?? 0) &&
|
|
501
|
+
newResDecls.every((d, i) => {
|
|
502
|
+
const p = prior.resources?.[i];
|
|
503
|
+
return p && p.name === d.name && p.ref === d.ref;
|
|
504
|
+
});
|
|
505
|
+
if (sameReqs && sameRess) {
|
|
506
|
+
if (prior.requires)
|
|
507
|
+
built.requires = prior.requires;
|
|
508
|
+
else
|
|
509
|
+
delete built.requires;
|
|
510
|
+
if (prior.resources)
|
|
511
|
+
built.resources = prior.resources;
|
|
512
|
+
else
|
|
513
|
+
delete built.resources;
|
|
514
|
+
// Still route through the parse check. `built` is a fresh row,
|
|
515
|
+
// so returning here directly would drop `parseCheckedHash` and
|
|
516
|
+
// make every later refresh re-compile content that never moved.
|
|
517
|
+
// The check itself is usually free on this path: the deps are
|
|
518
|
+
// the prior's by construction, so the fingerprint matches unless
|
|
519
|
+
// the BODY changed — which is exactly when a re-check is right.
|
|
520
|
+
return timePhase('parse-check', () => applyParseCheck(built, prior, parseCheck));
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
const result = await timePhase('deps', () => attachFiles({
|
|
524
|
+
scriptFileUrl: built.fileUrl,
|
|
525
|
+
header: built.header,
|
|
526
|
+
priorRequires,
|
|
527
|
+
priorResources,
|
|
528
|
+
recheck,
|
|
529
|
+
fetchCache,
|
|
530
|
+
now,
|
|
531
|
+
localRoots,
|
|
532
|
+
...(localFiles.lookup ? { localLookup: localFiles.lookup } : {}),
|
|
533
|
+
...(pushedFiles ? { pushedFiles } : {}),
|
|
534
|
+
}));
|
|
535
|
+
if (result.errorMessage) {
|
|
536
|
+
// Preserve the last-known-good script state when we have one.
|
|
537
|
+
// This rolls the *entire* refresh back to the prior — the new
|
|
538
|
+
// body isn't applied either, because injecting a fresh body
|
|
539
|
+
// against partial/stale dependencies would be unsafe.
|
|
540
|
+
//
|
|
541
|
+
// A failed @require/@resource fetch against a prior `ok` row is a
|
|
542
|
+
// *refresh* failure, not a content problem: keep the script
|
|
543
|
+
// running on its cached deps (`status: 'ok'`) and record
|
|
544
|
+
// `refreshError` so the UI flags REFRESH FAILED. A flaky dep host
|
|
545
|
+
// (or a logged-out CDN) must never silently disable a working
|
|
546
|
+
// script. Note we can only catch dep *fetch* failures this way —
|
|
547
|
+
// a 200 returning a login page in place of a @require is caught by
|
|
548
|
+
// the require markup gate (HTML rejected) in external-files;
|
|
549
|
+
// @resource bodies are arbitrary bytes and can't be validated.
|
|
550
|
+
if (prior && prior.status === 'ok') {
|
|
551
|
+
return { ...prior, refreshError: result.errorMessage };
|
|
552
|
+
}
|
|
553
|
+
built.status = 'invalid';
|
|
554
|
+
// The file that failed has no content to point into, so the
|
|
555
|
+
// location is the `@require` / `@resource` line that named it —
|
|
556
|
+
// which is the line the reader has to change anyway.
|
|
557
|
+
setScriptError(built, result.errorMessage, locationIn(built.relativePath, result.errorLine));
|
|
558
|
+
delete built.requires;
|
|
559
|
+
delete built.resources;
|
|
560
|
+
return built;
|
|
561
|
+
}
|
|
562
|
+
if (result.requires.length > 0)
|
|
563
|
+
built.requires = result.requires;
|
|
564
|
+
else
|
|
565
|
+
delete built.requires;
|
|
566
|
+
if (result.resources.length > 0)
|
|
567
|
+
built.resources = result.resources;
|
|
568
|
+
else
|
|
569
|
+
delete built.resources;
|
|
570
|
+
// A dependency whose content changed is fresh content for the script
|
|
571
|
+
// too: what runs is the composed body. Everything that asks "is there
|
|
572
|
+
// a newer copy than the page ran" compares `loadedAt` (the reload
|
|
573
|
+
// swap, marking open tabs for a reload, an old wrapper's report), so
|
|
574
|
+
// without this a library edit reached none of them until the next
|
|
575
|
+
// reload after the re-registration.
|
|
576
|
+
if (prior && dependenciesChanged(prior, built))
|
|
577
|
+
built.loadedAt = Math.max(built.loadedAt, now);
|
|
578
|
+
return timePhase('parse-check', () => applyParseCheck(built, prior, parseCheck));
|
|
579
|
+
}
|
|
580
|
+
// Whether the `@require` / `@resource` content differs between two rows.
|
|
581
|
+
// Compared on the content hashes, so a re-read of the same bytes, or the
|
|
582
|
+
// same bytes from another place, is no change.
|
|
583
|
+
function dependenciesChanged(a, b) {
|
|
584
|
+
const key = (s) => JSON.stringify([
|
|
585
|
+
(s.requires ?? []).map((r) => [r.url, r.contentHash]),
|
|
586
|
+
(s.resources ?? []).map((r) => [r.name, r.url, r.contentHash]),
|
|
587
|
+
]);
|
|
588
|
+
return key(a) !== key(b);
|
|
589
|
+
}
|
|
590
|
+
// Record the `@icon` check on `built`, and a problem it found as a
|
|
591
|
+
// load-time warning on the `@icon` line.
|
|
592
|
+
async function applyIconCheck(built, prior, recheck, fetchCache, localRoots, localLookup) {
|
|
593
|
+
// A row reused whole (the "unchanged" refresh path starts from a copy
|
|
594
|
+
// of the prior) already carries the last check's warning, so drop it
|
|
595
|
+
// before adding this one.
|
|
596
|
+
const stale = iconCheckLog(built);
|
|
597
|
+
if (stale && built.startupLogs) {
|
|
598
|
+
const kept = built.startupLogs.filter((l) => l.message !== stale.message);
|
|
599
|
+
if (kept.length > 0)
|
|
600
|
+
built.startupLogs = kept;
|
|
601
|
+
else
|
|
602
|
+
delete built.startupLogs;
|
|
603
|
+
}
|
|
604
|
+
delete built.iconCheck;
|
|
605
|
+
// Invalid rows don't run and carry no startup logs; see `startupLogs`.
|
|
606
|
+
if (built.status !== 'ok')
|
|
607
|
+
return;
|
|
608
|
+
const check = await timePhase('icon-check', () => checkScriptIcon({
|
|
609
|
+
header: built.header,
|
|
610
|
+
scriptFileUrl: built.fileUrl,
|
|
611
|
+
prior: prior?.iconCheck,
|
|
612
|
+
recheck,
|
|
613
|
+
fetchCache,
|
|
614
|
+
localRoots,
|
|
615
|
+
...(localLookup ? { localLookup } : {}),
|
|
616
|
+
}));
|
|
617
|
+
if (!check)
|
|
618
|
+
return;
|
|
619
|
+
built.iconCheck = check;
|
|
620
|
+
const log = iconCheckLog(built);
|
|
621
|
+
if (log)
|
|
622
|
+
built.startupLogs = [...(built.startupLogs ?? []), log];
|
|
623
|
+
}
|
|
624
|
+
// A stored row's startup logs, derived again: the header checks, plus
|
|
625
|
+
// the stored `@icon` warning. That one comes from a download, not the
|
|
626
|
+
// header, so it is carried over rather than re-derived. For the
|
|
627
|
+
// backfills that rebuild a row's logs without rebuilding the row.
|
|
628
|
+
export function rederivedStartupLogs(script, usesUnsafeWindow) {
|
|
629
|
+
const logs = [...extractStartupLogs(script.header, script.relativePath, usesUnsafeWindow)];
|
|
630
|
+
const iconLog = iconCheckLog(script);
|
|
631
|
+
if (iconLog)
|
|
632
|
+
logs.push(iconLog);
|
|
633
|
+
return logs;
|
|
634
|
+
}
|
|
635
|
+
// The load-time warning for a stored `@icon` problem, or null when
|
|
636
|
+
// there is none.
|
|
637
|
+
function iconCheckLog(script) {
|
|
638
|
+
const message = script.iconCheck?.message;
|
|
639
|
+
if (!message)
|
|
640
|
+
return null;
|
|
641
|
+
return {
|
|
642
|
+
level: 'warn',
|
|
643
|
+
message,
|
|
644
|
+
location: locationIn(script.relativePath, iconDeclFromHeader(script.header)?.line),
|
|
645
|
+
};
|
|
646
|
+
}
|
|
647
|
+
// Exported (read-only by convention) for script-store.ts's rechecks.
|
|
648
|
+
export let parseChecker = null;
|
|
649
|
+
export function setParseChecker(fn) {
|
|
650
|
+
parseChecker = fn;
|
|
651
|
+
}
|
|
652
|
+
// Fingerprint of everything that can affect whether the injected entry
|
|
653
|
+
// parses: the body, each `@require` body in order, and our own
|
|
654
|
+
// extension version.
|
|
655
|
+
//
|
|
656
|
+
// The body's `contentHash` covers the WHOLE file including the header,
|
|
657
|
+
// so `@grant` / `@require` / `@inject-into` edits all move the key.
|
|
658
|
+
//
|
|
659
|
+
// The version leg is there because how WE compose and wrap the user
|
|
660
|
+
// program decides what parses, and that can change between releases
|
|
661
|
+
// with nothing about the script itself changing. (Moving the user
|
|
662
|
+
// program into its own nested scope, for one, turned scripts that
|
|
663
|
+
// redeclare a `GM_*` name from failures into passes; see
|
|
664
|
+
// `wrapCombinedEntry` in injector.ts.) Without the version in the key,
|
|
665
|
+
// a cached verdict would survive the upgrade.
|
|
666
|
+
export function parseCheckKey(script) {
|
|
667
|
+
const reqs = (script.requires ?? []).map((r) => r.contentHash).join(',');
|
|
668
|
+
return `${extensionVersion()}|${script.contentHash ?? ''}|${reqs}`;
|
|
669
|
+
}
|
|
670
|
+
// Guarded because this module is also loaded by the unit suite, which
|
|
671
|
+
// runs the compiled output under plain Node with no `chrome` global.
|
|
672
|
+
// An empty version there just means the version leg contributes
|
|
673
|
+
// nothing to the key, which is correct for a context that never runs
|
|
674
|
+
// the check anyway.
|
|
675
|
+
function extensionVersion() {
|
|
676
|
+
if (typeof chrome === 'undefined' || !chrome.runtime?.getManifest)
|
|
677
|
+
return '';
|
|
678
|
+
return chrome.runtime.getManifest().version;
|
|
679
|
+
}
|
|
680
|
+
// Compile the script's user program without running it, and flip the
|
|
681
|
+
// row to `invalid` if V8 refuses it.
|
|
682
|
+
//
|
|
683
|
+
// Why here: this is the last point in the load pipeline where the body
|
|
684
|
+
// and every `@require` are both present and final. Before this
|
|
685
|
+
// existed, a syntax error anywhere in the concatenated program was
|
|
686
|
+
// only discovered at injection time — on a page the script matched,
|
|
687
|
+
// reported by the seal entry, and only as "a @require or the script
|
|
688
|
+
// body has a syntax error". See [docs/parse-check.md].
|
|
689
|
+
//
|
|
690
|
+
// `invalid` is deliberate rather than `refreshError`: unlike a failed
|
|
691
|
+
// dependency fetch, this is not transient and re-running won't help —
|
|
692
|
+
// the content we hold genuinely cannot run. Callers upstream still
|
|
693
|
+
// apply their own preservation policy on top; `rebuildCollection`
|
|
694
|
+
// (script-store.ts) keeps
|
|
695
|
+
// a prior good copy running when an *http* refresh comes back invalid
|
|
696
|
+
// (a login page served in place of the script), while a file:// edit
|
|
697
|
+
// flags immediately, which is what a local dev wants.
|
|
698
|
+
//
|
|
699
|
+
// Fails open. `checkParses` returns null when the machinery is
|
|
700
|
+
// unavailable, and null must never become `invalid` — a working script
|
|
701
|
+
// must not break because our offscreen document didn't open. The seal
|
|
702
|
+
// entry remains the backstop for anything missed here.
|
|
703
|
+
export async function applyParseCheck(built, prior, mode = 'await') {
|
|
704
|
+
// Nothing to check on a row that already failed for another reason
|
|
705
|
+
// (missing file, bad header) — there may be no body at all.
|
|
706
|
+
if (built.status !== 'ok')
|
|
707
|
+
return built;
|
|
708
|
+
const key = parseCheckKey(built);
|
|
709
|
+
// Unchanged since the last check, so the previous verdict still
|
|
710
|
+
// holds. This is what keeps opportunistic refreshes from paying a
|
|
711
|
+
// round trip per script for content that didn't move. Applies in
|
|
712
|
+
// 'defer' mode too — reusing a cached verdict costs nothing, so a
|
|
713
|
+
// deferring caller only leaves the row unchecked when the content
|
|
714
|
+
// really moved.
|
|
715
|
+
if (prior?.parseCheckedHash === key && prior.status === 'ok') {
|
|
716
|
+
built.parseCheckedHash = key;
|
|
717
|
+
return built;
|
|
718
|
+
}
|
|
719
|
+
// Deferred: return the row with no `parseCheckedHash`, which is the
|
|
720
|
+
// same state a row is in when the check couldn't run. The caller runs
|
|
721
|
+
// `checkParsesInBackground` after its write, and `verifyParseChecks`
|
|
722
|
+
// catches the row before registration either way (both in
|
|
723
|
+
// script-store.ts).
|
|
724
|
+
if (mode === 'defer')
|
|
725
|
+
return built;
|
|
726
|
+
if (!parseChecker)
|
|
727
|
+
return built;
|
|
728
|
+
let verdict;
|
|
729
|
+
try {
|
|
730
|
+
verdict = await parseChecker(built);
|
|
731
|
+
}
|
|
732
|
+
catch (err) {
|
|
733
|
+
// The checker itself failed (a storage read for a storage-granting
|
|
734
|
+
// script's baked-in values, the offscreen document refusing to
|
|
735
|
+
// open). Not the script's fault; skip rather than accuse it.
|
|
736
|
+
dlog(`[SourceMonkey] parse check skipped for ${built.relativePath}: ${describeError(err)}`);
|
|
737
|
+
return built;
|
|
738
|
+
}
|
|
739
|
+
if (!verdict)
|
|
740
|
+
return built;
|
|
741
|
+
// `unknown` is the machinery saying it couldn't decide — a stack
|
|
742
|
+
// overflow in the parser, a sandbox that lost its CSP. It must not
|
|
743
|
+
// become `invalid`, and it must not be cached as a pass either.
|
|
744
|
+
if (verdict.status === 'unknown') {
|
|
745
|
+
dlog(`[SourceMonkey] parse check inconclusive for ${built.relativePath}: ${verdict.message ?? ''}`);
|
|
746
|
+
return built;
|
|
747
|
+
}
|
|
748
|
+
if (verdict.status === 'ok') {
|
|
749
|
+
built.parseCheckedHash = key;
|
|
750
|
+
return built;
|
|
751
|
+
}
|
|
752
|
+
built.status = 'invalid';
|
|
753
|
+
// The message carries no `(in …)` suffix any more — the location
|
|
754
|
+
// rides alongside it so surfaces can make it clickable, and each of
|
|
755
|
+
// them appends the suffix itself via `describeDiagnostic`. Anything
|
|
756
|
+
// reading `errorMessage` on its own therefore has to pair it with
|
|
757
|
+
// `errorLocation` to say the whole thing.
|
|
758
|
+
setScriptError(built,
|
|
759
|
+
// No "script source has a…" prefix: `errorLocation` names the file,
|
|
760
|
+
// and that file is routinely a `@require` rather than the script.
|
|
761
|
+
`${SYNTAX_ERROR_PREFIX}${verdict.message ?? 'unknown'}`, verdict.location);
|
|
762
|
+
// Don't record the fingerprint: the row is invalid, and a future
|
|
763
|
+
// refresh that produces the same bytes should re-derive that rather
|
|
764
|
+
// than inherit a stale pass. (`prior` above only short-circuits on a
|
|
765
|
+
// prior `ok`.)
|
|
766
|
+
delete built.parseCheckedHash;
|
|
767
|
+
return built;
|
|
768
|
+
}
|
|
769
|
+
// The fields a parse verdict owns, copied from the checked row onto
|
|
770
|
+
// the stored one. Everything else on the stored row is left as it is
|
|
771
|
+
// found — the check has no opinion about it.
|
|
772
|
+
export function applyVerdictFields(current, checked) {
|
|
773
|
+
const merged = { ...current, status: checked.status };
|
|
774
|
+
setScriptError(merged, checked.errorMessage, checked.errorLocation);
|
|
775
|
+
if (checked.parseCheckedHash)
|
|
776
|
+
merged.parseCheckedHash = checked.parseCheckedHash;
|
|
777
|
+
else
|
|
778
|
+
delete merged.parseCheckedHash;
|
|
779
|
+
return merged;
|
|
780
|
+
}
|
|
781
|
+
// Coerce a caught throw into a printable string. Exported so other
|
|
782
|
+
// modules (notably the injector) don't have to duplicate the helper.
|
|
783
|
+
export function describeError(err) {
|
|
784
|
+
if (err instanceof Error)
|
|
785
|
+
return err.message;
|
|
786
|
+
return String(err);
|
|
787
|
+
}
|