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,33 @@
|
|
|
1
|
+
export interface HeaderTag {
|
|
2
|
+
tag: string;
|
|
3
|
+
locale?: string;
|
|
4
|
+
value: string;
|
|
5
|
+
line?: number;
|
|
6
|
+
}
|
|
7
|
+
export interface ParsedHeader {
|
|
8
|
+
tags: HeaderTag[];
|
|
9
|
+
startLine?: number;
|
|
10
|
+
}
|
|
11
|
+
export declare const NO_METADATA_BLOCK = "No // ==UserScript== metadata block, so this is not a userscript";
|
|
12
|
+
export declare const UNTERMINATED_METADATA_BLOCK = "The // ==UserScript== metadata block is never closed and needs a // ==/UserScript== line";
|
|
13
|
+
export declare const USERSTYLE_NOT_SUPPORTED = "This is a user style (a /* ==UserStyle== block), not a userscript. User styles need a user style manager extension.";
|
|
14
|
+
export type SplitError = typeof NO_METADATA_BLOCK | typeof UNTERMINATED_METADATA_BLOCK | typeof USERSTYLE_NOT_SUPPORTED;
|
|
15
|
+
export interface SplitResult {
|
|
16
|
+
leadingText: string;
|
|
17
|
+
headerText: string;
|
|
18
|
+
bodyText: string;
|
|
19
|
+
error?: SplitError;
|
|
20
|
+
headerStartLine?: number;
|
|
21
|
+
}
|
|
22
|
+
export declare function splitHeaderBody(source: string): SplitResult;
|
|
23
|
+
export declare function joinFullSource(leadingText: string, headerText: string, bodyText: string): string;
|
|
24
|
+
export declare function parseHeader(source: string): ParsedHeader | null;
|
|
25
|
+
export declare function getValue(header: ParsedHeader, tag: string): string | undefined;
|
|
26
|
+
export declare function getValues(header: ParsedHeader, tag: string): string[];
|
|
27
|
+
export declare function getLocalized(header: ParsedHeader, tag: string): Record<string, string>;
|
|
28
|
+
export declare function hasTag(header: ParsedHeader, tag: string): boolean;
|
|
29
|
+
export declare const SUPPORTED_RUN_AT: readonly string[];
|
|
30
|
+
export declare function unsupportedRunAt(header: ParsedHeader | null | undefined): string | undefined;
|
|
31
|
+
export declare const CONTEXT_MENU_RUN_AT = "context-menu";
|
|
32
|
+
export declare const RUN_SCRIPT_LABEL = "Run this script";
|
|
33
|
+
export declare function isContextMenuRunAt(header: ParsedHeader | null | undefined): boolean;
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
// Parser for the Tampermonkey-style metadata block at the top of a
|
|
2
|
+
// userscript:
|
|
3
|
+
//
|
|
4
|
+
// // ==UserScript==
|
|
5
|
+
// // @name Hello
|
|
6
|
+
// // @match https://example.com/*
|
|
7
|
+
// // @grant none
|
|
8
|
+
// // ==/UserScript==
|
|
9
|
+
//
|
|
10
|
+
// We return a flat list of `{tag, locale?, value}` entries in source
|
|
11
|
+
// order so callers can decide which tags are single-valued (@name,
|
|
12
|
+
// @version) and which can repeat (@match, @grant, @require, …) without
|
|
13
|
+
// the parser baking that policy in. Tag names are lowercased so lookups
|
|
14
|
+
// are case-insensitive; locales like `@name:de` are kept on the side.
|
|
15
|
+
// Every line-terminator shape the metadata-block scanner recognizes.
|
|
16
|
+
// One definition so "which line is this tag on?" is counted by exactly
|
|
17
|
+
// the rule the block is split by — count and split disagreeing is how
|
|
18
|
+
// every header diagnostic in a DOS or classic-Mac file ends up one line
|
|
19
|
+
// off.
|
|
20
|
+
//
|
|
21
|
+
// Deliberately NOT the same set as `countLines` in composed-source.ts,
|
|
22
|
+
// which adds U+2028/U+2029 because V8 treats them as terminators. Those
|
|
23
|
+
// can't appear in a metadata line anyway (`TAG_LINE`'s `.` doesn't
|
|
24
|
+
// match them), and widening the split here would change what this
|
|
25
|
+
// long-standing parser considers a line.
|
|
26
|
+
const LINE_BREAK = /\r\n|\r|\n/;
|
|
27
|
+
const LINE_BREAK_G = /\r\n|\r|\n/g;
|
|
28
|
+
function countLineBreaks(text) {
|
|
29
|
+
return text.match(LINE_BREAK_G)?.length ?? 0;
|
|
30
|
+
}
|
|
31
|
+
// A marker line is `//`, optional spaces or tabs, then the marker, with
|
|
32
|
+
// anything before or after it on the line, as VM matches it. GreasyFork
|
|
33
|
+
// serves scripts opening with `/// ==UserScript==` or closing with
|
|
34
|
+
// `// ==/UserScript==////`, and TM and VM both install them. Each match
|
|
35
|
+
// takes the whole line, so text after a closing marker (part of that
|
|
36
|
+
// comment) stays in the header rather than starting the body.
|
|
37
|
+
//
|
|
38
|
+
// `[^\r\n]` and a lookbehind rather than `/m` with `.`, `^` and `$`:
|
|
39
|
+
// those also break at U+2028/U+2029, which `LINE_BREAK` deliberately
|
|
40
|
+
// doesn't treat as line breaks.
|
|
41
|
+
const BLOCK_START = /(?<![^\r\n])[^\r\n]*?\/\/[ \t]*==UserScript==[^\r\n]*/;
|
|
42
|
+
const BLOCK_END = /(?<![^\r\n])[^\r\n]*?\/\/[ \t]*==\/UserScript==[^\r\n]*/;
|
|
43
|
+
// Strip a single leading UTF-8 BOM. Most fetch paths (`response.text()`,
|
|
44
|
+
// `TextDecoder`) drop it for us, but the page-context `file://` XHR path
|
|
45
|
+
// (`directories.ts`) hands back `xhr.responseText` with the BOM intact.
|
|
46
|
+
// The marker regexes would take it as text before the `//` on line 1,
|
|
47
|
+
// leaving it in `headerText` (and GM_info's metadata string) rather than
|
|
48
|
+
// dropped. The BOM only ever appears at file position 0.
|
|
49
|
+
function stripLeadingBom(source) {
|
|
50
|
+
return source.charCodeAt(0) === 0xfeff ? source.slice(1) : source;
|
|
51
|
+
}
|
|
52
|
+
// The ways the split can fail, as the text a user reads.
|
|
53
|
+
//
|
|
54
|
+
// Named constants rather than inline literals because these strings are
|
|
55
|
+
// both the message and the value something compares against — today the
|
|
56
|
+
// corpus validator, which groups failures by message. A reworded
|
|
57
|
+
// literal would silently stop matching.
|
|
58
|
+
//
|
|
59
|
+
// Phrased to read correctly in all three places they appear: alone in
|
|
60
|
+
// the viewer's message bar, alone in the Log tab, and after the
|
|
61
|
+
// preview's `This file cannot be installed: ` prefix. Capitalized and
|
|
62
|
+
// unpunctuated per docs/diagnostics.md#wording; that still rules out a
|
|
63
|
+
// bare fragment like `No metadata block`, which needs a subject.
|
|
64
|
+
export const NO_METADATA_BLOCK = 'No // ==UserScript== metadata block, so this is not a userscript';
|
|
65
|
+
// The fix is phrased as a need rather than an instruction ("it needs",
|
|
66
|
+
// not "add"): this message carries a location, so it renders as
|
|
67
|
+
// `… — add a … line (in f.user.js, line 1)`, where the parenthetical
|
|
68
|
+
// reads as *where to add it*. Line 1 is where the block opened.
|
|
69
|
+
export const UNTERMINATED_METADATA_BLOCK = 'The // ==UserScript== metadata block is never closed and needs a // ==/UserScript== line';
|
|
70
|
+
// A file with no userscript block but a `/* ==UserStyle==` one is a
|
|
71
|
+
// user style (CSS for a style manager extension, not a userscript).
|
|
72
|
+
// GreasyFork lists them alongside userscripts, so naming what the file
|
|
73
|
+
// is beats calling it "not a userscript".
|
|
74
|
+
export const USERSTYLE_NOT_SUPPORTED = 'This is a user style (a /* ==UserStyle== block), not a userscript. User styles need a user style manager extension.';
|
|
75
|
+
const USERSTYLE_START = /\/\*[ \t]*==UserStyle==/;
|
|
76
|
+
// Split a userscript source into the (rare) text above the metadata
|
|
77
|
+
// block, the raw metadata block itself, and the body text.
|
|
78
|
+
//
|
|
79
|
+
// Failure modes:
|
|
80
|
+
// - no `==UserScript==` line → NO_METADATA_BLOCK, or
|
|
81
|
+
// USERSTYLE_NOT_SUPPORTED when there's a `/* ==UserStyle==` instead
|
|
82
|
+
// - open marker but no matching close → UNTERMINATED_METADATA_BLOCK
|
|
83
|
+
// In every case we return leadingText = '', headerText = '', and
|
|
84
|
+
// bodyText = the whole source so callers always have something to
|
|
85
|
+
// display.
|
|
86
|
+
export function splitHeaderBody(source) {
|
|
87
|
+
source = stripLeadingBom(source);
|
|
88
|
+
const startMatch = BLOCK_START.exec(source);
|
|
89
|
+
if (!startMatch) {
|
|
90
|
+
const error = USERSTYLE_START.test(source) ? USERSTYLE_NOT_SUPPORTED : NO_METADATA_BLOCK;
|
|
91
|
+
return { leadingText: '', headerText: '', bodyText: source, error };
|
|
92
|
+
}
|
|
93
|
+
const headerStartLine = countLineBreaks(source.slice(0, startMatch.index)) + 1;
|
|
94
|
+
const afterStart = source.slice(startMatch.index + startMatch[0].length);
|
|
95
|
+
const endMatch = BLOCK_END.exec(afterStart);
|
|
96
|
+
if (!endMatch) {
|
|
97
|
+
return {
|
|
98
|
+
leadingText: '',
|
|
99
|
+
headerText: '',
|
|
100
|
+
bodyText: source,
|
|
101
|
+
error: UNTERMINATED_METADATA_BLOCK,
|
|
102
|
+
headerStartLine,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
const blockEnd = startMatch.index + startMatch[0].length + endMatch.index + endMatch[0].length;
|
|
106
|
+
// Skip a single trailing newline after the closing marker so bodyText
|
|
107
|
+
// doesn't start with a stray blank line.
|
|
108
|
+
let bodyStart = blockEnd;
|
|
109
|
+
if (source[bodyStart] === '\r')
|
|
110
|
+
bodyStart++;
|
|
111
|
+
if (source[bodyStart] === '\n')
|
|
112
|
+
bodyStart++;
|
|
113
|
+
return {
|
|
114
|
+
leadingText: source.slice(0, startMatch.index),
|
|
115
|
+
headerText: source.slice(startMatch.index, blockEnd),
|
|
116
|
+
bodyText: source.slice(bodyStart),
|
|
117
|
+
headerStartLine,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
// One metadata line: `// @tag[:locale][ value]`. The value group is
|
|
121
|
+
// `.*\S` so trailing whitespace is excluded; the whole value group is
|
|
122
|
+
// optional so flag tags like `@noframes` parse with value = "".
|
|
123
|
+
const TAG_LINE = /^[ \t]*\/\/[ \t]*@([A-Za-z][\w-]*)(?::([\w-]+))?(?:[ \t]+(.*\S))?[ \t]*$/;
|
|
124
|
+
// Reassemble the original on-disk source from the three parts
|
|
125
|
+
// produced by `splitHeaderBody`. The reassembly is line-for-line
|
|
126
|
+
// faithful so that an error reported on injected line N also points
|
|
127
|
+
// at line N of this reconstructed string (which is what the viewer
|
|
128
|
+
// shows in the main tab).
|
|
129
|
+
//
|
|
130
|
+
// The `'\n'` between `headerText` and `bodyText` restores the line
|
|
131
|
+
// break `splitHeaderBody` skipped past the closing marker; with no
|
|
132
|
+
// metadata block, both `leadingText` and `headerText` are empty so
|
|
133
|
+
// the body is returned verbatim.
|
|
134
|
+
export function joinFullSource(leadingText, headerText, bodyText) {
|
|
135
|
+
if (!headerText)
|
|
136
|
+
return leadingText + bodyText;
|
|
137
|
+
return leadingText + headerText + '\n' + bodyText;
|
|
138
|
+
}
|
|
139
|
+
// Parse the metadata block. Returns null if there is no
|
|
140
|
+
// `==UserScript==` / `==/UserScript==` pair.
|
|
141
|
+
export function parseHeader(source) {
|
|
142
|
+
source = stripLeadingBom(source);
|
|
143
|
+
const startMatch = BLOCK_START.exec(source);
|
|
144
|
+
if (!startMatch)
|
|
145
|
+
return null;
|
|
146
|
+
const afterStart = source.slice(startMatch.index + startMatch[0].length);
|
|
147
|
+
const endMatch = BLOCK_END.exec(afterStart);
|
|
148
|
+
if (!endMatch)
|
|
149
|
+
return null;
|
|
150
|
+
const block = afterStart.slice(0, endMatch.index);
|
|
151
|
+
const startLine = countLineBreaks(source.slice(0, startMatch.index)) + 1;
|
|
152
|
+
const tags = [];
|
|
153
|
+
// Split on every line-terminator shape — `\r\n` (DOS), `\n` (Unix),
|
|
154
|
+
// and bare `\r` (classic Mac). `splitHeaderBody`'s marker regexes
|
|
155
|
+
// stop at all three too, so we keep parity here.
|
|
156
|
+
//
|
|
157
|
+
// `block` begins with the terminator that ended the `==UserScript==`
|
|
158
|
+
// line, so element 0 is always the empty remainder of that line and
|
|
159
|
+
// element i is `startLine + i` — no off-by-one correction needed.
|
|
160
|
+
for (const [i, rawLine] of block.split(LINE_BREAK).entries()) {
|
|
161
|
+
const m = TAG_LINE.exec(rawLine);
|
|
162
|
+
if (!m)
|
|
163
|
+
continue;
|
|
164
|
+
const [, tag, locale, value] = m;
|
|
165
|
+
const entry = {
|
|
166
|
+
tag: tag.toLowerCase(),
|
|
167
|
+
value: value ?? '',
|
|
168
|
+
line: startLine + i,
|
|
169
|
+
};
|
|
170
|
+
if (locale)
|
|
171
|
+
entry.locale = locale;
|
|
172
|
+
tags.push(entry);
|
|
173
|
+
}
|
|
174
|
+
return { tags, startLine };
|
|
175
|
+
}
|
|
176
|
+
// First non-localized value for `tag`, or undefined.
|
|
177
|
+
export function getValue(header, tag) {
|
|
178
|
+
const t = tag.toLowerCase();
|
|
179
|
+
for (const e of header.tags) {
|
|
180
|
+
if (e.tag === t && e.locale === undefined)
|
|
181
|
+
return e.value;
|
|
182
|
+
}
|
|
183
|
+
return undefined;
|
|
184
|
+
}
|
|
185
|
+
// All non-localized values for `tag`, in source order.
|
|
186
|
+
export function getValues(header, tag) {
|
|
187
|
+
const t = tag.toLowerCase();
|
|
188
|
+
const out = [];
|
|
189
|
+
for (const e of header.tags) {
|
|
190
|
+
if (e.tag === t && e.locale === undefined)
|
|
191
|
+
out.push(e.value);
|
|
192
|
+
}
|
|
193
|
+
return out;
|
|
194
|
+
}
|
|
195
|
+
// Map of locale → value for tags like `@name:de`. Excludes the
|
|
196
|
+
// unlocalized form.
|
|
197
|
+
export function getLocalized(header, tag) {
|
|
198
|
+
const t = tag.toLowerCase();
|
|
199
|
+
const out = {};
|
|
200
|
+
for (const e of header.tags) {
|
|
201
|
+
if (e.tag === t && e.locale !== undefined)
|
|
202
|
+
out[e.locale] = e.value;
|
|
203
|
+
}
|
|
204
|
+
return out;
|
|
205
|
+
}
|
|
206
|
+
// True if the tag appears at all (useful for flag-style tags like
|
|
207
|
+
// `@noframes` or `@unwrap`).
|
|
208
|
+
export function hasTag(header, tag) {
|
|
209
|
+
const t = tag.toLowerCase();
|
|
210
|
+
return header.tags.some((e) => e.tag === t);
|
|
211
|
+
}
|
|
212
|
+
// The `@run-at` values that pick a point in the page load. Anything
|
|
213
|
+
// else (a typo) runs at `document-idle`: see `normalizeDeclaredRunAt`
|
|
214
|
+
// in injector.ts. The exception is `context-menu`, which makes the
|
|
215
|
+
// script manual instead (see `CONTEXT_MENU_RUN_AT`).
|
|
216
|
+
export const SUPPORTED_RUN_AT = [
|
|
217
|
+
'document-start',
|
|
218
|
+
'document-body',
|
|
219
|
+
'document-end',
|
|
220
|
+
'document-idle',
|
|
221
|
+
];
|
|
222
|
+
// The script's declared `@run-at` when it is one we don't honor, so
|
|
223
|
+
// the caller can say it runs at `document-idle` instead. Undefined when
|
|
224
|
+
// the tag is absent, blank, supported, or `context-menu`.
|
|
225
|
+
export function unsupportedRunAt(header) {
|
|
226
|
+
if (!header)
|
|
227
|
+
return undefined;
|
|
228
|
+
const value = (getValue(header, 'run-at') ?? '').trim();
|
|
229
|
+
if (value === '' || SUPPORTED_RUN_AT.includes(value.toLowerCase()))
|
|
230
|
+
return undefined;
|
|
231
|
+
if (value.toLowerCase() === CONTEXT_MENU_RUN_AT)
|
|
232
|
+
return undefined;
|
|
233
|
+
return value;
|
|
234
|
+
}
|
|
235
|
+
// Tampermonkey's `@run-at context-menu`: don't run on page load, run
|
|
236
|
+
// from a right-click menu item instead. We call these manual scripts:
|
|
237
|
+
// they match pages as usual, but run only when the person picks "Run
|
|
238
|
+
// this script" in the popup (or sm-dev fires it). See
|
|
239
|
+
// docs/manual-scripts.md.
|
|
240
|
+
export const CONTEXT_MENU_RUN_AT = 'context-menu';
|
|
241
|
+
// The command that runs a manual script now: first in its popup row,
|
|
242
|
+
// and in sm-dev's `menu`.
|
|
243
|
+
export const RUN_SCRIPT_LABEL = 'Run this script';
|
|
244
|
+
export function isContextMenuRunAt(header) {
|
|
245
|
+
if (!header)
|
|
246
|
+
return false;
|
|
247
|
+
return (getValue(header, 'run-at') ?? '').trim().toLowerCase() === CONTEXT_MENU_RUN_AT;
|
|
248
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export declare function compareVersions(a: string, b: string): -1 | 0 | 1;
|
|
2
|
+
export declare const LOCAL_EDIT_TAG = "local";
|
|
3
|
+
export declare function localEditVersion(version: string): string;
|
|
4
|
+
export declare function isLocalEditVersion(version: string): boolean;
|
|
5
|
+
export type VersionRelation = 'same' | 'newer' | 'older' | 'edited' | 'unknown';
|
|
6
|
+
export declare function versionRelation(loaded: string | undefined, other: string | undefined): VersionRelation;
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
// `@version` ordering.
|
|
2
|
+
//
|
|
3
|
+
// Userscript versions are not semver — managers accept whatever the
|
|
4
|
+
// author writes. Tampermonkey compares them as dot-separated segments
|
|
5
|
+
// with an optional `-prerelease` tail, and that's what we implement:
|
|
6
|
+
// enough structure to order the versions real scripts use, with a
|
|
7
|
+
// deterministic answer for the ones that don't fit.
|
|
8
|
+
//
|
|
9
|
+
// Pure module — no DOM, no chrome.*, no fetching. The only consumer
|
|
10
|
+
// that needs a verdict rather than an ordering is the UI, which uses
|
|
11
|
+
// `versionRelation`.
|
|
12
|
+
// Splits `1.2.3-beta.4` into `{ main: ['1','2','3'], pre: ['beta','4'] }`.
|
|
13
|
+
// Only the FIRST `-` starts the prerelease tail, so `1.0-rc-2` has a
|
|
14
|
+
// two-segment-free prerelease of `rc-2` rather than nesting further.
|
|
15
|
+
//
|
|
16
|
+
// A `+build` tail is dropped first, per semver: build metadata carries
|
|
17
|
+
// no precedence. Without this, `1.0+b2` vs `1.0` would compare its last
|
|
18
|
+
// main segment as `0+b2` vs `0` and report the metadata-only change as
|
|
19
|
+
// a newer version — a false OUTDATED.
|
|
20
|
+
function splitVersion(v) {
|
|
21
|
+
const plus = v.indexOf('+');
|
|
22
|
+
const trimmed = (plus < 0 ? v : v.slice(0, plus)).trim();
|
|
23
|
+
const dash = trimmed.indexOf('-');
|
|
24
|
+
const mainPart = dash < 0 ? trimmed : trimmed.slice(0, dash);
|
|
25
|
+
const prePart = dash < 0 ? '' : trimmed.slice(dash + 1);
|
|
26
|
+
return {
|
|
27
|
+
main: mainPart.split('.'),
|
|
28
|
+
pre: prePart ? prePart.split('.') : [],
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
const NUMERIC_RE = /^\d+$/;
|
|
32
|
+
// Compare one segment pair. Numeric-vs-numeric compares as numbers so
|
|
33
|
+
// `10` beats `9`; anything else falls back to code-unit order, which
|
|
34
|
+
// happens to put digits below letters — the same direction semver
|
|
35
|
+
// specifies for a numeric-vs-alphanumeric identifier.
|
|
36
|
+
//
|
|
37
|
+
// A consequence worth knowing: `1.0-beta` sorts *below* `1.0` (the
|
|
38
|
+
// prerelease rule) but `1.0.beta` sorts *above* it (padded `'0'` vs
|
|
39
|
+
// `'b'` here). Both follow from the rules as stated; only the `-` form
|
|
40
|
+
// is a prerelease.
|
|
41
|
+
function compareSegment(a, b) {
|
|
42
|
+
if (NUMERIC_RE.test(a) && NUMERIC_RE.test(b)) {
|
|
43
|
+
const x = Number(a);
|
|
44
|
+
const y = Number(b);
|
|
45
|
+
return x < y ? -1 : x > y ? 1 : 0;
|
|
46
|
+
}
|
|
47
|
+
return a < b ? -1 : a > b ? 1 : 0;
|
|
48
|
+
}
|
|
49
|
+
// Missing trailing segments read as `0`, so `1.2` and `1.2.0` are the
|
|
50
|
+
// same version. Authors bump `1.2` → `1.2.1` all the time and expect
|
|
51
|
+
// exactly that.
|
|
52
|
+
function compareMain(a, b) {
|
|
53
|
+
const len = Math.max(a.length, b.length);
|
|
54
|
+
for (let i = 0; i < len; i++) {
|
|
55
|
+
const cmp = compareSegment(a[i] ?? '0', b[i] ?? '0');
|
|
56
|
+
if (cmp !== 0)
|
|
57
|
+
return cmp;
|
|
58
|
+
}
|
|
59
|
+
return 0;
|
|
60
|
+
}
|
|
61
|
+
// Prerelease tails follow semver's precedence rules: a release outranks
|
|
62
|
+
// any prerelease of the same main version (`1.0` > `1.0-rc1`), and
|
|
63
|
+
// between two prereleases the one that runs out of segments first is
|
|
64
|
+
// lower (`1.0-rc` < `1.0-rc.2`).
|
|
65
|
+
function comparePre(a, b) {
|
|
66
|
+
if (a.length === 0 && b.length === 0)
|
|
67
|
+
return 0;
|
|
68
|
+
if (a.length === 0)
|
|
69
|
+
return 1;
|
|
70
|
+
if (b.length === 0)
|
|
71
|
+
return -1;
|
|
72
|
+
const len = Math.max(a.length, b.length);
|
|
73
|
+
for (let i = 0; i < len; i++) {
|
|
74
|
+
const sa = a[i];
|
|
75
|
+
const sb = b[i];
|
|
76
|
+
if (sa === undefined)
|
|
77
|
+
return -1;
|
|
78
|
+
if (sb === undefined)
|
|
79
|
+
return 1;
|
|
80
|
+
const cmp = compareSegment(sa, sb);
|
|
81
|
+
if (cmp !== 0)
|
|
82
|
+
return cmp;
|
|
83
|
+
}
|
|
84
|
+
return 0;
|
|
85
|
+
}
|
|
86
|
+
// Total order over version strings. Always returns an ordering — there
|
|
87
|
+
// is no "incomparable" verdict, because every caller ultimately has to
|
|
88
|
+
// pick newer / older / same and a null would just push the arbitrary
|
|
89
|
+
// choice up a layer. An empty string sorts below every non-empty one.
|
|
90
|
+
export function compareVersions(a, b) {
|
|
91
|
+
const ta = a.trim();
|
|
92
|
+
const tb = b.trim();
|
|
93
|
+
if (!ta || !tb) {
|
|
94
|
+
if (ta === tb)
|
|
95
|
+
return 0;
|
|
96
|
+
return ta ? 1 : -1;
|
|
97
|
+
}
|
|
98
|
+
const va = splitVersion(ta);
|
|
99
|
+
const vb = splitVersion(tb);
|
|
100
|
+
const main = compareMain(va.main, vb.main);
|
|
101
|
+
if (main !== 0)
|
|
102
|
+
return main;
|
|
103
|
+
return comparePre(va.pre, vb.pre);
|
|
104
|
+
}
|
|
105
|
+
// The prerelease tag a local copy's `@version` carries: `13.2` copied
|
|
106
|
+
// for editing becomes `13.2-local`. Lives here, next to the comparison
|
|
107
|
+
// that has to recognise it — see `versionRelation`.
|
|
108
|
+
export const LOCAL_EDIT_TAG = 'local';
|
|
109
|
+
// The `@version` a local copy is given: the original with `-local`
|
|
110
|
+
// appended. Always a plain append — `splitVersion` keys on the FIRST
|
|
111
|
+
// `-`, so `1.0-beta` becomes `1.0-beta-local`, whose single prerelease
|
|
112
|
+
// segment `beta-local` string-compares above `beta` and stays below
|
|
113
|
+
// the `1.0` release. That copy therefore reads as newer than its
|
|
114
|
+
// source, the ordinary "local edit running ahead" case; only a source
|
|
115
|
+
// with no tail needs the `edited` relation below.
|
|
116
|
+
export function localEditVersion(version) {
|
|
117
|
+
return `${version.trim()}-${LOCAL_EDIT_TAG}`;
|
|
118
|
+
}
|
|
119
|
+
// True when `version` is a local edit per `localEditVersion`: any
|
|
120
|
+
// prerelease segment that is, or ends in, `-local`.
|
|
121
|
+
export function isLocalEditVersion(version) {
|
|
122
|
+
const { pre } = splitVersion(version.trim());
|
|
123
|
+
return pre.some((seg) => seg === LOCAL_EDIT_TAG || seg.endsWith(`-${LOCAL_EDIT_TAG}`));
|
|
124
|
+
}
|
|
125
|
+
export function versionRelation(loaded, other) {
|
|
126
|
+
if (!loaded?.trim() || !other?.trim())
|
|
127
|
+
return 'unknown';
|
|
128
|
+
const cmp = compareVersions(other, loaded);
|
|
129
|
+
if (cmp > 0)
|
|
130
|
+
return isEditOf(loaded, other) ? 'edited' : 'newer';
|
|
131
|
+
if (cmp < 0)
|
|
132
|
+
return 'older';
|
|
133
|
+
return 'same';
|
|
134
|
+
}
|
|
135
|
+
// `loaded` is a local edit of `other`: same main segments, `other` has
|
|
136
|
+
// no prerelease tail, and ours is a `-local` one.
|
|
137
|
+
function isEditOf(loaded, other) {
|
|
138
|
+
const vl = splitVersion(loaded.trim());
|
|
139
|
+
const vo = splitVersion(other.trim());
|
|
140
|
+
return vo.pre.length === 0 && compareMain(vl.main, vo.main) === 0 && isLocalEditVersion(loaded);
|
|
141
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { type ParsedHeader } from './userscript-header.js';
|
|
2
|
+
export type ProbeKind = 'update' | 'download';
|
|
3
|
+
export interface ProbeTarget {
|
|
4
|
+
url: string;
|
|
5
|
+
kinds: ProbeKind[];
|
|
6
|
+
}
|
|
7
|
+
export interface VersionProbe extends ProbeTarget {
|
|
8
|
+
version?: string;
|
|
9
|
+
error?: string;
|
|
10
|
+
checkedAt: number;
|
|
11
|
+
}
|
|
12
|
+
export type ProbePolicy = 'force' | 'if-stale';
|
|
13
|
+
export declare const PROBE_MIN_INTERVAL_MS: number;
|
|
14
|
+
export declare function probeTargets(header: ParsedHeader | null, fileUrl: string): ProbeTarget[];
|
|
15
|
+
export declare function probesAreFresh(probes: readonly VersionProbe[] | undefined, targets: readonly ProbeTarget[], now: number): boolean;
|
|
16
|
+
export declare function outdatedProbe(loadedVersion: string | undefined, probes: readonly VersionProbe[] | undefined): VersionProbe | null;
|
|
17
|
+
export declare function probeLabel(kinds: readonly ProbeKind[]): string;
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
// Update probes: "what `@version` does the script's upstream claim?"
|
|
2
|
+
//
|
|
3
|
+
// A probe is deliberately NOT a refresh. A refresh fetches the bytes we
|
|
4
|
+
// actually run, from the URL the user installed from, and can change a
|
|
5
|
+
// script's status. A probe fetches somebody else's copy, reads one
|
|
6
|
+
// header line out of it, and changes nothing but a display annotation.
|
|
7
|
+
// Keeping them apart is the whole design: a `@downloadURL` pointing at
|
|
8
|
+
// an origin the user never approved must never become an update
|
|
9
|
+
// channel, and an unreachable probe host must never disable a working
|
|
10
|
+
// script.
|
|
11
|
+
//
|
|
12
|
+
// What gets probed: only `@updateURL` / `@downloadURL` values that
|
|
13
|
+
// differ from the URL we load the script from. We already auto-update
|
|
14
|
+
// from our own collection — local files via the scan's size+mtime check,
|
|
15
|
+
// http(s) via a conditional GET — so probing it would be a slower way
|
|
16
|
+
// to learn what the refresh that just ran already established.
|
|
17
|
+
//
|
|
18
|
+
// That leaves exactly the interesting case: a copy whose official home
|
|
19
|
+
// is somewhere we don't fetch from. We can't update it automatically,
|
|
20
|
+
// so all we can do is say so. See [docs/updates.md].
|
|
21
|
+
import { getValue } from './userscript-header.js';
|
|
22
|
+
import { versionRelation } from './version-compare.js';
|
|
23
|
+
// How long a recorded probe stays fresh. Probes hit third-party hosts
|
|
24
|
+
// on somebody else's page load, and an upstream release is a
|
|
25
|
+
// once-in-weeks event, so this is deliberately hours rather than the
|
|
26
|
+
// seconds the collection-refresh gate uses.
|
|
27
|
+
export const PROBE_MIN_INTERVAL_MS = 6 * 60 * 60 * 1000;
|
|
28
|
+
// Canonical form for comparing a probe URL against the script's own
|
|
29
|
+
// location. Returns null for anything that isn't an absolute http(s)
|
|
30
|
+
// URL: a relative `@updateURL` has no meaning (updates come from a
|
|
31
|
+
// fixed home, not from wherever this copy happens to sit), and a
|
|
32
|
+
// `file://` one would turn a header line into a host-file read.
|
|
33
|
+
function canonicalProbeUrl(raw) {
|
|
34
|
+
if (!raw?.trim())
|
|
35
|
+
return null;
|
|
36
|
+
try {
|
|
37
|
+
const u = new URL(raw.trim());
|
|
38
|
+
return u.protocol === 'http:' || u.protocol === 'https:' ? u.toString() : null;
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
// The distinct upstream URLs worth asking about, in `update`-first
|
|
45
|
+
// order. Deduped by canonical URL, so the common GreasyFork shape
|
|
46
|
+
// (`@downloadURL` = the `.user.js` we installed from, `@updateURL` =
|
|
47
|
+
// the sibling `.meta.js`) yields one target, not two.
|
|
48
|
+
export function probeTargets(header, fileUrl) {
|
|
49
|
+
if (!header)
|
|
50
|
+
return [];
|
|
51
|
+
// A `file://` script canonicalizes to null and simply never matches
|
|
52
|
+
// an http(s) target, which is the right answer.
|
|
53
|
+
const own = canonicalProbeUrl(fileUrl);
|
|
54
|
+
const byUrl = new Map();
|
|
55
|
+
for (const kind of ['update', 'download']) {
|
|
56
|
+
const url = canonicalProbeUrl(getValue(header, `${kind}URL`));
|
|
57
|
+
if (!url || url === own)
|
|
58
|
+
continue;
|
|
59
|
+
const existing = byUrl.get(url);
|
|
60
|
+
if (existing)
|
|
61
|
+
existing.kinds.push(kind);
|
|
62
|
+
else
|
|
63
|
+
byUrl.set(url, { url, kinds: [kind] });
|
|
64
|
+
}
|
|
65
|
+
return [...byUrl.values()];
|
|
66
|
+
}
|
|
67
|
+
// Whether the stored probes still stand in for a fresh check. Stale in
|
|
68
|
+
// either direction, because both mean the stored set no longer
|
|
69
|
+
// describes what the script declares:
|
|
70
|
+
//
|
|
71
|
+
// - a target with no row, or a row aged past the interval — the
|
|
72
|
+
// normal case, and what makes a newly-added `@updateURL` get
|
|
73
|
+
// probed on the next check rather than in six hours;
|
|
74
|
+
// - a row whose URL is no longer a target — the script dropped that
|
|
75
|
+
// header, and leaving the row would keep reporting OUTDATED
|
|
76
|
+
// against a URL it no longer names.
|
|
77
|
+
export function probesAreFresh(probes, targets, now) {
|
|
78
|
+
const stored = probes ?? [];
|
|
79
|
+
if (targets.length === 0)
|
|
80
|
+
return stored.length === 0;
|
|
81
|
+
const targetUrls = new Set(targets.map((t) => t.url));
|
|
82
|
+
if (stored.some((p) => !targetUrls.has(p.url)))
|
|
83
|
+
return false;
|
|
84
|
+
const byUrl = new Map(stored.map((p) => [p.url, p]));
|
|
85
|
+
return targets.every((t) => {
|
|
86
|
+
const probe = byUrl.get(t.url);
|
|
87
|
+
return probe !== undefined && now - probe.checkedAt < PROBE_MIN_INTERVAL_MS;
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
// The probe that says our copy is behind, or null when none does.
|
|
91
|
+
// Picks the highest version among those ahead of us, so a script whose
|
|
92
|
+
// update and download URLs disagree is judged against the further-ahead
|
|
93
|
+
// one — being told about the smaller gap would understate it.
|
|
94
|
+
export function outdatedProbe(loadedVersion, probes) {
|
|
95
|
+
let furthestAhead = null;
|
|
96
|
+
for (const probe of probes ?? []) {
|
|
97
|
+
if (versionRelation(loadedVersion, probe.version) !== 'newer')
|
|
98
|
+
continue;
|
|
99
|
+
if (!furthestAhead || versionRelation(furthestAhead.version, probe.version) === 'newer') {
|
|
100
|
+
furthestAhead = probe;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return furthestAhead;
|
|
104
|
+
}
|
|
105
|
+
// Row label for a probe: "update URL" or "download URL".
|
|
106
|
+
//
|
|
107
|
+
// When ONE url is named by both headers the label says "update URL"
|
|
108
|
+
// rather than spelling out both. `@updateURL` is the header that means
|
|
109
|
+
// "check here for a new version", so it is the accurate name for what
|
|
110
|
+
// we did; `@downloadURL` naming the same file is a detail of the
|
|
111
|
+
// script's headers, not of the check. Spelling out both made the
|
|
112
|
+
// longest label in the panel, for a distinction the reader can't act
|
|
113
|
+
// on — the row's tooltip carries the URL either way.
|
|
114
|
+
//
|
|
115
|
+
// No ambiguity with a plain update probe: the combined form only
|
|
116
|
+
// exists when the two headers resolve to the same URL, and two probes
|
|
117
|
+
// for one URL is exactly what it's collapsing.
|
|
118
|
+
export function probeLabel(kinds) {
|
|
119
|
+
return kinds.includes('update') ? 'update URL' : 'download URL';
|
|
120
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,58 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sourcemonkey",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "0.5.0",
|
|
4
|
+
"description": "Developer tools for building and testing userscripts, using a real browser and the SourceMonkey extension: the sm-dev command-line tool, Playwright test fixture, and skills for agentic development",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"homepage": "https://github.com/jshute96/SourceMonkey",
|
|
7
|
+
"bugs": "https://github.com/jshute96/SourceMonkey/issues",
|
|
8
|
+
"keywords": [
|
|
9
|
+
"userscript",
|
|
10
|
+
"chrome-extension",
|
|
11
|
+
"playwright",
|
|
12
|
+
"tampermonkey",
|
|
13
|
+
"violentmonkey"
|
|
14
|
+
],
|
|
15
|
+
"repository": {
|
|
16
|
+
"type": "git",
|
|
17
|
+
"url": "https://github.com/jshute96/SourceMonkey.git"
|
|
18
|
+
},
|
|
19
|
+
"type": "module",
|
|
20
|
+
"exports": {
|
|
21
|
+
".": "./lib/harness/index.js",
|
|
22
|
+
"./harness": "./lib/harness/index.js",
|
|
23
|
+
"./package.json": "./package.json",
|
|
24
|
+
"./*": "./lib/*.js"
|
|
25
|
+
},
|
|
26
|
+
"files": [
|
|
27
|
+
"dist",
|
|
28
|
+
"lib",
|
|
29
|
+
"skills"
|
|
30
|
+
],
|
|
31
|
+
"bin": {
|
|
32
|
+
"sm-dev": "./lib/harness/cli.js"
|
|
33
|
+
},
|
|
34
|
+
"engines": {
|
|
35
|
+
"node": ">=22"
|
|
36
|
+
},
|
|
37
|
+
"dependencies": {
|
|
38
|
+
"playwright-core": "^1.64.1"
|
|
39
|
+
},
|
|
40
|
+
"devDependencies": {
|
|
41
|
+
"@playwright/test": "^1.64.1",
|
|
42
|
+
"@types/chrome": "^0.0.287",
|
|
43
|
+
"@types/node": "^22.20.5",
|
|
44
|
+
"@types/prismjs": "^1.26.6",
|
|
45
|
+
"fflate": "^0.8.3",
|
|
46
|
+
"prismjs": "^1.30.0",
|
|
47
|
+
"terser": "^5.51.2",
|
|
48
|
+
"typescript": "^5.9.3"
|
|
49
|
+
},
|
|
50
|
+
"peerDependencies": {
|
|
51
|
+
"@playwright/test": ">=1.48.0"
|
|
52
|
+
},
|
|
53
|
+
"peerDependenciesMeta": {
|
|
54
|
+
"@playwright/test": {
|
|
55
|
+
"optional": true
|
|
56
|
+
}
|
|
57
|
+
}
|
|
6
58
|
}
|