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,878 @@
|
|
|
1
|
+
// Prefetched external files referenced by a userscript: `@require`
|
|
2
|
+
// libraries and `@resource` data blobs. Both flow through the same
|
|
3
|
+
// resolution + fetch + dedup pipeline; the diff is the typed shape
|
|
4
|
+
// each produces and how they're consumed downstream.
|
|
5
|
+
//
|
|
6
|
+
// `@require`:
|
|
7
|
+
// - JS text prepended before the script body at injection time
|
|
8
|
+
// (the registered userScript carries the concatenated source so
|
|
9
|
+
// no runtime fetch is needed at injection time — Chrome's
|
|
10
|
+
// userScripts API can't fetch on its own).
|
|
11
|
+
// - Any URL, with or without a `.js` suffix (CDN shorthands like
|
|
12
|
+
// `cdn.jsdelivr.net/npm/sweetalert2@11` have none, and TM and VM
|
|
13
|
+
// don't gate on it). Strict UTF-8 decode on the fetched bytes —
|
|
14
|
+
// non-UTF-8 fails the script outright. We never inject binary as
|
|
15
|
+
// JS.
|
|
16
|
+
//
|
|
17
|
+
// `@resource`:
|
|
18
|
+
// - Arbitrary bytes (any media type). Stored as base64 so the
|
|
19
|
+
// chrome.storage.local JSON layer can carry them.
|
|
20
|
+
// - Exposed to userscripts at runtime via GM_getResourceText
|
|
21
|
+
// (UTF-8 decode on demand, mojibake-tolerant) and
|
|
22
|
+
// GM_getResourceURL (blob: URL minted in the page with the
|
|
23
|
+
// fetched content-type).
|
|
24
|
+
//
|
|
25
|
+
// Shared semantics:
|
|
26
|
+
// - resolveScriptResource rules from file-loader: absolute
|
|
27
|
+
// http(s):// or relative-against-script. file:// only reachable
|
|
28
|
+
// via relative resolution, and a local script's relative ref may
|
|
29
|
+
// not climb out of its own folder. data: rejected.
|
|
30
|
+
// - Same byte fetcher with size cap.
|
|
31
|
+
// - Same per-refresh cross-script fetch cache: two scripts (and
|
|
32
|
+
// two refs of either kind) pointing at the same URL share one
|
|
33
|
+
// fetch. The typed assembly happens after the cache hit.
|
|
34
|
+
// - Any failure (resolve-time or fetch-time) returns an error
|
|
35
|
+
// message; the caller flips the script to status:'invalid' with
|
|
36
|
+
// the message and preserves prior-known-good state.
|
|
37
|
+
import { fetchResourceBytes, isNotModified, resolveScriptResourceDetailed, } from './file-loader.js';
|
|
38
|
+
import { localCandidateMayExist, localFileCandidatesDetailed, } from './local-file-overrides.js';
|
|
39
|
+
import { splitResourceValue } from './script-links.js';
|
|
40
|
+
import { sha256Hex, bytesToBase64, base64ToBytes } from './bytes.js';
|
|
41
|
+
import { timePhase } from './phase-timing.js';
|
|
42
|
+
import { getValues } from './userscript-header.js';
|
|
43
|
+
import { iconDeclFromHeader, isIconContentType, MAX_ICON_BYTES } from './script-icon.js';
|
|
44
|
+
import { isMarkupContentType, looksLikeMarkup, markupResponseMessage, } from './markup-response.js';
|
|
45
|
+
// Per-file limit. Big enough for jQuery (~90 KB) and the 1-2 MB
|
|
46
|
+
// libraries scripts pin (MathJax, video.js, babel-standalone, the
|
|
47
|
+
// stockfish engines); small enough to keep a runaway from draining the
|
|
48
|
+
// 10 MB chrome.storage.local quota in one row. Applies to both
|
|
49
|
+
// @require and @resource.
|
|
50
|
+
export const MAX_FILE_BYTES = 2 * 1024 * 1024;
|
|
51
|
+
// Rendered in whole MB when the cap divides evenly, so messages say
|
|
52
|
+
// "1 MB" rather than "1024 KB".
|
|
53
|
+
function byteLimitText(maxBytes) {
|
|
54
|
+
return maxBytes % (1024 * 1024) === 0
|
|
55
|
+
? `${maxBytes / (1024 * 1024)} MB`
|
|
56
|
+
: `${Math.round(maxBytes / 1024)} KB`;
|
|
57
|
+
}
|
|
58
|
+
// Why a `data:` URL dependency won't load. Exported so the corpus
|
|
59
|
+
// validator can recognize it without matching the wording.
|
|
60
|
+
export const DATA_URL_NOT_SUPPORTED = 'URLs with the data: scheme are not supported';
|
|
61
|
+
// Split out so `isFetchFailureMessage` can recognize the one message
|
|
62
|
+
// whose text isn't fixed at module load, without either side spelling
|
|
63
|
+
// the prefix twice.
|
|
64
|
+
const HTTP_STATUS_PREFIX = 'The server returned HTTP ';
|
|
65
|
+
// Why a dependency wouldn't load, as a sentence of its own that lands
|
|
66
|
+
// after the subject: `@require ./lib.js: ` + one of these.
|
|
67
|
+
//
|
|
68
|
+
// These used to be bare predicates (`is empty`, `could not be fetched:
|
|
69
|
+
// HTTP 404`) that only parsed as English once the subject was glued on.
|
|
70
|
+
// Each now carries its own subject so it reads as a sentence wherever
|
|
71
|
+
// it lands, and takes the capital every message takes — see
|
|
72
|
+
// [docs/diagnostics.md](../docs/diagnostics.md#wording). Dropping the
|
|
73
|
+
// `could not be fetched: ` half also drops a second colon from a string
|
|
74
|
+
// that already arrives after one.
|
|
75
|
+
//
|
|
76
|
+
// These read as facts rather than as a list of possibilities because
|
|
77
|
+
// the fetcher now reports which case it hit. It used to return a bare
|
|
78
|
+
// `null`, so a single message had to cover "no response", "an HTTP
|
|
79
|
+
// error", and "over the limit" at once — and the corpus validator
|
|
80
|
+
// reconstructed the real cause from a fetch log to make its report
|
|
81
|
+
// useful.
|
|
82
|
+
//
|
|
83
|
+
// `maxBytes` is the limit the fetch was made with, for the oversize
|
|
84
|
+
// message: `@require` / `@resource` by default, smaller for an `@icon`.
|
|
85
|
+
export function describeFetchFailure(failure, maxBytes = MAX_FILE_BYTES) {
|
|
86
|
+
switch (failure.reason) {
|
|
87
|
+
case 'unsupported-scheme':
|
|
88
|
+
// Unreachable for a dependency — resolution rejects anything
|
|
89
|
+
// that isn't http(s) or a contained relative path long before
|
|
90
|
+
// the fetch. Kept for totality.
|
|
91
|
+
return 'That URL scheme cannot be fetched';
|
|
92
|
+
case 'no-response':
|
|
93
|
+
return 'No response from the server';
|
|
94
|
+
case 'timeout':
|
|
95
|
+
return 'The request timed out';
|
|
96
|
+
case 'unreadable-file':
|
|
97
|
+
// The one fetch failure that still names two causes, because a
|
|
98
|
+
// file:// read has no HTTP layer: "isn't there" and "can't be
|
|
99
|
+
// read" arrive identically. Not a network problem either way,
|
|
100
|
+
// which is what this case used to be reported as.
|
|
101
|
+
return 'The file may not exist, or may not be readable';
|
|
102
|
+
case 'http-error':
|
|
103
|
+
return `${HTTP_STATUS_PREFIX}${failure.status}`;
|
|
104
|
+
case 'wrong-content-type':
|
|
105
|
+
// Only the icon fetches ask for a content type.
|
|
106
|
+
return 'The file is not an image';
|
|
107
|
+
case 'empty':
|
|
108
|
+
return 'The file is empty';
|
|
109
|
+
case 'oversize':
|
|
110
|
+
return `The file is larger than the ${byteLimitText(maxBytes)} limit`;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
// Every `FetchFailReason`, so the predicate below can't silently stop
|
|
114
|
+
// covering one. `FetchFailure` is a discriminated union, not an enum,
|
|
115
|
+
// so there is no runtime list to derive this from.
|
|
116
|
+
//
|
|
117
|
+
// A `Record` keyed by the union rather than an array: a `Record` is
|
|
118
|
+
// missing-key-checked, so adding a `FetchFailReason` without a line
|
|
119
|
+
// here fails the build. An array — even `as const satisfies
|
|
120
|
+
// readonly FetchFailure['reason'][]` — only checks that each entry IS a
|
|
121
|
+
// reason, never that none is missing, which is the direction that
|
|
122
|
+
// matters. A missed reason wouldn't throw; it would quietly stop being
|
|
123
|
+
// recognized, and the validator would mislabel that bucket.
|
|
124
|
+
const FETCH_FAILURE_REASONS = {
|
|
125
|
+
'unsupported-scheme': true,
|
|
126
|
+
'no-response': true,
|
|
127
|
+
'timeout': true,
|
|
128
|
+
'unreadable-file': true,
|
|
129
|
+
'http-error': true,
|
|
130
|
+
'wrong-content-type': true,
|
|
131
|
+
'empty': true,
|
|
132
|
+
'oversize': true,
|
|
133
|
+
};
|
|
134
|
+
// Did this text come out of `describeFetchFailure`?
|
|
135
|
+
//
|
|
136
|
+
// Exists for the corpus validator
|
|
137
|
+
// (`tests/greasyfork-examples/validate-corpus.mjs`), which substitutes
|
|
138
|
+
// its own wording when the offline mirror's 404 is an artifact of the
|
|
139
|
+
// mirror rather than a real server answer — it has to recognize a
|
|
140
|
+
// fetch-layer failure to know when that substitution applies.
|
|
141
|
+
//
|
|
142
|
+
// A predicate rather than a regex over the wording: these strings are
|
|
143
|
+
// user-facing and get reworded, and a pattern living in the validator
|
|
144
|
+
// would go stale silently (it did — it matched a `could not be
|
|
145
|
+
// fetched: ` prefix that no longer exists).
|
|
146
|
+
export function isFetchFailureMessage(text) {
|
|
147
|
+
// `http-error` first and separately: its text carries the status, so
|
|
148
|
+
// it's the one case that can't be compared against a fixed string.
|
|
149
|
+
if (text.startsWith(HTTP_STATUS_PREFIX))
|
|
150
|
+
return true;
|
|
151
|
+
const reasons = Object.keys(FETCH_FAILURE_REASONS);
|
|
152
|
+
return reasons.some((reason) => {
|
|
153
|
+
if (reason === 'http-error')
|
|
154
|
+
return false;
|
|
155
|
+
return describeFetchFailure({ failed: true, reason }) === text;
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
// Why a ref couldn't be resolved to something fetchable. Same shape:
|
|
159
|
+
// a sentence that follows the subject, not a predicate glued to it.
|
|
160
|
+
export function describeResolveFailure(failure) {
|
|
161
|
+
switch (failure.reason) {
|
|
162
|
+
case 'empty':
|
|
163
|
+
// Unreachable — `distillRequires` / `distillResources` drop empty
|
|
164
|
+
// refs before a loader sees one. Kept for totality.
|
|
165
|
+
return 'The tag has no reference after it';
|
|
166
|
+
case 'malformed-url':
|
|
167
|
+
return 'The reference is not a valid URL';
|
|
168
|
+
case 'unsupported-scheme':
|
|
169
|
+
// Names what the author actually wrote. `file:` reaches this on
|
|
170
|
+
// purpose — an absolute local path is not accepted even from a
|
|
171
|
+
// local script (see file-loader.ts's module header). The scheme
|
|
172
|
+
// is parenthesized rather than leading, so the sentence can open
|
|
173
|
+
// with a capital of ours instead of a verbatim value.
|
|
174
|
+
return `That URL scheme (${failure.scheme}) is not supported; ` +
|
|
175
|
+
'use an http(s) URL, or a relative path resolved against the script\'s own location';
|
|
176
|
+
case 'outside-script-folder':
|
|
177
|
+
// Only reachable for a script loaded from `file://`: a relative
|
|
178
|
+
// ref in a script served over http(s) may climb, the way it does
|
|
179
|
+
// in Violentmonkey. So the message names the case it applies to
|
|
180
|
+
// rather than stating a rule that isn't universal.
|
|
181
|
+
return 'That path points outside the script\'s own folder; ' +
|
|
182
|
+
'a relative path in a local script has to stay inside it';
|
|
183
|
+
case 'relative-in-scratch':
|
|
184
|
+
// A scratch script is text alone, without the files beside it.
|
|
185
|
+
return 'A scratch script can use a relative path only to a file pushed along with its text. ' +
|
|
186
|
+
'Use an http(s) URL, or push the file with the script.';
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
function isFailure(v) {
|
|
190
|
+
return v.failed === true;
|
|
191
|
+
}
|
|
192
|
+
// ---------- Header distillation ----------
|
|
193
|
+
// Unique `@require` refs in declaration order. Localized variants
|
|
194
|
+
// (`@require:de`) are ignored. Empty / whitespace-only refs dropped.
|
|
195
|
+
// Duplicate refs collapse to first occurrence — TM does the same.
|
|
196
|
+
export function distillRequires(header) {
|
|
197
|
+
if (!header)
|
|
198
|
+
return [];
|
|
199
|
+
const out = [];
|
|
200
|
+
const seen = new Set();
|
|
201
|
+
for (const raw of getValues(header, 'require')) {
|
|
202
|
+
const trimmed = raw.trim();
|
|
203
|
+
if (!trimmed)
|
|
204
|
+
continue;
|
|
205
|
+
if (seen.has(trimmed))
|
|
206
|
+
continue;
|
|
207
|
+
seen.add(trimmed);
|
|
208
|
+
out.push(trimmed);
|
|
209
|
+
}
|
|
210
|
+
return out;
|
|
211
|
+
}
|
|
212
|
+
// Unique `@resource` declarations in declaration order. Each line is
|
|
213
|
+
// `<name> <url>`; we split via the same helper the viewer uses so
|
|
214
|
+
// linkification and load semantics stay aligned. Lines with no URL
|
|
215
|
+
// portion (no whitespace gap) are dropped silently. Duplicate names
|
|
216
|
+
// keep the first declaration — TM/VM raise an error; being lenient
|
|
217
|
+
// means a typo only loses one resource rather than failing the
|
|
218
|
+
// whole script.
|
|
219
|
+
export function distillResources(header) {
|
|
220
|
+
if (!header)
|
|
221
|
+
return [];
|
|
222
|
+
const out = [];
|
|
223
|
+
const seenName = new Set();
|
|
224
|
+
for (const raw of getValues(header, 'resource')) {
|
|
225
|
+
const trimmed = raw.trim();
|
|
226
|
+
if (!trimmed)
|
|
227
|
+
continue;
|
|
228
|
+
const parts = splitResourceValue(trimmed);
|
|
229
|
+
if (!parts)
|
|
230
|
+
continue;
|
|
231
|
+
const name = parts.prefix.trim();
|
|
232
|
+
const ref = parts.url.trim();
|
|
233
|
+
if (!name || !ref)
|
|
234
|
+
continue;
|
|
235
|
+
if (seenName.has(name))
|
|
236
|
+
continue;
|
|
237
|
+
seenName.add(name);
|
|
238
|
+
out.push({ name, ref });
|
|
239
|
+
}
|
|
240
|
+
return out;
|
|
241
|
+
}
|
|
242
|
+
// Find the header line that declared a file, so a load failure can
|
|
243
|
+
// point at it. Matching is on the declaration's own text rather than
|
|
244
|
+
// on the resolved URL: resolution is exactly what may have failed, and
|
|
245
|
+
// the line the user has to edit is the one they wrote.
|
|
246
|
+
//
|
|
247
|
+
// Localized forms (`@require:de`) are skipped for the same reason
|
|
248
|
+
// `getValues` skips them — they are not the declarations that were
|
|
249
|
+
// loaded.
|
|
250
|
+
function declarationLine(header, tag, matches) {
|
|
251
|
+
if (!header)
|
|
252
|
+
return undefined;
|
|
253
|
+
for (const t of header.tags) {
|
|
254
|
+
if (t.tag !== tag || t.locale)
|
|
255
|
+
continue;
|
|
256
|
+
if (matches(t.value.trim()))
|
|
257
|
+
return t.line;
|
|
258
|
+
}
|
|
259
|
+
return undefined;
|
|
260
|
+
}
|
|
261
|
+
export async function attachFiles(args) {
|
|
262
|
+
const requireRefs = distillRequires(args.header);
|
|
263
|
+
const resourceDecls = distillResources(args.header);
|
|
264
|
+
if (requireRefs.length === 0 && resourceDecls.length === 0) {
|
|
265
|
+
return { requires: [], resources: [] };
|
|
266
|
+
}
|
|
267
|
+
const requires = [];
|
|
268
|
+
const resources = [];
|
|
269
|
+
// @require dedup is at URL granularity (TM/VM also do this).
|
|
270
|
+
// @resource dedup is at NAME granularity, handled in distillResources —
|
|
271
|
+
// two names pointing at the same URL produce two distinct entries.
|
|
272
|
+
const seenRequireUrls = new Set();
|
|
273
|
+
const localRoots = args.localRoots ?? [];
|
|
274
|
+
const localLookup = args.localLookup ?? 'search';
|
|
275
|
+
// All files load at once, rather than one after another: each is a
|
|
276
|
+
// round trip of its own (a stand-in read, a network fetch), and on a
|
|
277
|
+
// page-load check they decide whether the reload's window is met.
|
|
278
|
+
// Started in declaration order, and `loadRequire` claims its URL in
|
|
279
|
+
// `seenUrls` before its first await, so the first of two refs to one
|
|
280
|
+
// URL is still the one that loads. Failures are reported for the
|
|
281
|
+
// first failing declaration, as the serial loop did.
|
|
282
|
+
const requireLoads = requireRefs.map((ref) => loadRequire({
|
|
283
|
+
ref,
|
|
284
|
+
scriptFileUrl: args.scriptFileUrl,
|
|
285
|
+
prior: args.priorRequires,
|
|
286
|
+
recheck: args.recheck,
|
|
287
|
+
fetchCache: args.fetchCache,
|
|
288
|
+
now: args.now,
|
|
289
|
+
seenUrls: seenRequireUrls,
|
|
290
|
+
localRoots,
|
|
291
|
+
localLookup,
|
|
292
|
+
pushed: args.pushedFiles?.[ref],
|
|
293
|
+
}));
|
|
294
|
+
const resourceLoads = resourceDecls.map((decl) => loadResource({
|
|
295
|
+
decl,
|
|
296
|
+
scriptFileUrl: args.scriptFileUrl,
|
|
297
|
+
prior: args.priorResources,
|
|
298
|
+
recheck: args.recheck,
|
|
299
|
+
fetchCache: args.fetchCache,
|
|
300
|
+
now: args.now,
|
|
301
|
+
localRoots,
|
|
302
|
+
localLookup,
|
|
303
|
+
pushed: args.pushedFiles?.[decl.ref],
|
|
304
|
+
}));
|
|
305
|
+
const [requireResults, resourceResults] = await Promise.all([
|
|
306
|
+
Promise.all(requireLoads), Promise.all(resourceLoads),
|
|
307
|
+
]);
|
|
308
|
+
for (const [i, res] of requireResults.entries()) {
|
|
309
|
+
if (res.errorMessage) {
|
|
310
|
+
const ref = requireRefs[i];
|
|
311
|
+
return {
|
|
312
|
+
requires: [],
|
|
313
|
+
resources: [],
|
|
314
|
+
errorMessage: res.errorMessage,
|
|
315
|
+
errorLine: declarationLine(args.header, 'require', (v) => v === ref),
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
if (res.entry)
|
|
319
|
+
requires.push(res.entry);
|
|
320
|
+
}
|
|
321
|
+
for (const [i, res] of resourceResults.entries()) {
|
|
322
|
+
if (res.errorMessage) {
|
|
323
|
+
const decl = resourceDecls[i];
|
|
324
|
+
return {
|
|
325
|
+
requires: [],
|
|
326
|
+
resources: [],
|
|
327
|
+
errorMessage: res.errorMessage,
|
|
328
|
+
// Matched on NAME, which is what `distillResources` dedups on,
|
|
329
|
+
// so the line found is the declaration that was actually
|
|
330
|
+
// loaded rather than a later one the loader ignored.
|
|
331
|
+
errorLine: declarationLine(args.header, 'resource', (v) => splitResourceValue(v)?.prefix.trim() === decl.name),
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
if (res.entry)
|
|
335
|
+
resources.push(res.entry);
|
|
336
|
+
}
|
|
337
|
+
return { requires, resources };
|
|
338
|
+
}
|
|
339
|
+
// Check that the script's `@icon` can be shown: that it resolves,
|
|
340
|
+
// downloads, is an image, and is within `MAX_ICON_BYTES`. The pages
|
|
341
|
+
// that paint icons apply the same rules and just show a globe when one
|
|
342
|
+
// fails, so this is the only place the reason is reported.
|
|
343
|
+
//
|
|
344
|
+
// Never makes the script invalid: the icon is cosmetic. Downloads on
|
|
345
|
+
// the same terms as `@require`:
|
|
346
|
+
// - `recheck: 'preserve'` keeps the prior answer for an unchanged
|
|
347
|
+
// reference, so a background refresh doesn't download it again.
|
|
348
|
+
// Not one that may pass on its own (no response, a timeout).
|
|
349
|
+
// - A local stand-in is re-read every pass, like a `@require`'s.
|
|
350
|
+
//
|
|
351
|
+
// Returns null when there is nothing to check: no `@icon`, or an inline
|
|
352
|
+
// `data:` icon, which has nothing to download.
|
|
353
|
+
export async function checkScriptIcon(args) {
|
|
354
|
+
const decl = iconDeclFromHeader(args.header);
|
|
355
|
+
if (!decl || decl.ref.startsWith('data:'))
|
|
356
|
+
return null;
|
|
357
|
+
const { ref } = decl;
|
|
358
|
+
const problem = (reason) => ({ ref, message: `@icon ${ref}: ${reason}` });
|
|
359
|
+
const resolved = resolveScriptResourceDetailed(ref, args.scriptFileUrl);
|
|
360
|
+
if ('failed' in resolved)
|
|
361
|
+
return problem(describeResolveFailure(resolved));
|
|
362
|
+
const { url } = resolved;
|
|
363
|
+
// A stand-in the page could show wins. One it couldn't (too big, not
|
|
364
|
+
// an image) is skipped, as the page skips it for the declared URL.
|
|
365
|
+
// Read through the shared fetch, which has no icon rules, so they
|
|
366
|
+
// are applied to the bytes here.
|
|
367
|
+
const prior = args.prior;
|
|
368
|
+
// A 'known' lookup goes by what the last check of this same reference
|
|
369
|
+
// found. A row that recorded a stand-in without its URL is searched.
|
|
370
|
+
const last = args.localLookup === 'known' && prior?.ref === ref && prior.url === url &&
|
|
371
|
+
(!prior.local || prior.localUrl)
|
|
372
|
+
? { localUrl: prior.localUrl } : undefined;
|
|
373
|
+
const local = await readLocalStandIn(url, args.localRoots, args.fetchCache, last);
|
|
374
|
+
if (local?.localUrl && !isFailure(local.got) &&
|
|
375
|
+
local.got.bytes.byteLength <= MAX_ICON_BYTES &&
|
|
376
|
+
(!local.got.contentType || isIconContentType(local.got.contentType))) {
|
|
377
|
+
return { ref, url, local: true, localUrl: local.localUrl };
|
|
378
|
+
}
|
|
379
|
+
if (args.recheck === 'preserve' && prior?.ref === ref && prior.url === url &&
|
|
380
|
+
!prior.local && !prior.retry) {
|
|
381
|
+
return prior;
|
|
382
|
+
}
|
|
383
|
+
// Not through the shared fetch cache: that one fetches up to the
|
|
384
|
+
// `@require` limit, and an icon has its own limit and type rule.
|
|
385
|
+
const res = await fetchResourceBytes(url, {
|
|
386
|
+
maxBytes: MAX_ICON_BYTES,
|
|
387
|
+
acceptContentType: isIconContentType,
|
|
388
|
+
});
|
|
389
|
+
if ('failed' in res) {
|
|
390
|
+
const transient = res.reason === 'no-response' || res.reason === 'timeout';
|
|
391
|
+
return {
|
|
392
|
+
...problem(describeFetchFailure(res, MAX_ICON_BYTES)),
|
|
393
|
+
url,
|
|
394
|
+
failure: res.reason,
|
|
395
|
+
...(transient ? { retry: true } : {}),
|
|
396
|
+
};
|
|
397
|
+
}
|
|
398
|
+
return { ref, url };
|
|
399
|
+
}
|
|
400
|
+
// Fetch a single `@require` the way `attachFiles` would, for one
|
|
401
|
+
// preview TAB: the tabs are built from the header and render one body
|
|
402
|
+
// each, so a tab can't use the batch orchestrator (which returns all
|
|
403
|
+
// files or an error). The preview's parse check does use `attachFiles`
|
|
404
|
+
// — this is the per-tab path only, and the shared `fetchCache` means
|
|
405
|
+
// whichever runs first pays for the fetch. Routing through the same
|
|
406
|
+
// `loadRequire` keeps the validation and the error wording identical
|
|
407
|
+
// to what installing the script would produce.
|
|
408
|
+
//
|
|
409
|
+
// Throws (rather than returning an error field) because the caller is
|
|
410
|
+
// a lazy tab body, and a rejected promise is what the tab view renders
|
|
411
|
+
// as a load failure.
|
|
412
|
+
export async function fetchRequireForPreview(ref, scriptFileUrl, fetchCache, now, localRoots = []) {
|
|
413
|
+
const res = await loadRequire({
|
|
414
|
+
ref,
|
|
415
|
+
scriptFileUrl,
|
|
416
|
+
prior: new Map(),
|
|
417
|
+
recheck: 'all',
|
|
418
|
+
fetchCache,
|
|
419
|
+
now,
|
|
420
|
+
localRoots,
|
|
421
|
+
localLookup: 'search',
|
|
422
|
+
// Fresh per call: dedup across refs is the batch loader's job, and
|
|
423
|
+
// the preview shows one tab per declared ref.
|
|
424
|
+
seenUrls: new Set(),
|
|
425
|
+
});
|
|
426
|
+
if (res.errorMessage)
|
|
427
|
+
throw new Error(res.errorMessage);
|
|
428
|
+
// Defensive: `loadRequire` returns neither an entry nor an error only
|
|
429
|
+
// when it skipped the fetch (a URL already seen this pass, or a
|
|
430
|
+
// preserved prior), and this call can do neither — fresh `seenUrls`,
|
|
431
|
+
// empty `prior`, `recheck: 'all'`. So it says nothing about the file.
|
|
432
|
+
if (!res.entry)
|
|
433
|
+
throw new Error(`@require ${ref}: Internal error: no content and no reason`);
|
|
434
|
+
return res.entry;
|
|
435
|
+
}
|
|
436
|
+
// The stored fields that say a stand-in was read, and its stat. Empty
|
|
437
|
+
// on the ordinary path, so nothing is written for a declared-URL fetch.
|
|
438
|
+
function standInFields(loaded) {
|
|
439
|
+
if (!loaded.localUrl)
|
|
440
|
+
return {};
|
|
441
|
+
return {
|
|
442
|
+
localUrl: loaded.localUrl,
|
|
443
|
+
...(loaded.fsMtime !== undefined ? { fsMtime: loaded.fsMtime } : {}),
|
|
444
|
+
};
|
|
445
|
+
}
|
|
446
|
+
// A pushed body as what was read for its ref. A relative ref's body is
|
|
447
|
+
// the declared file itself, so only a stand-in for a remote URL counts
|
|
448
|
+
// as one.
|
|
449
|
+
function pushedBytes(pushed) {
|
|
450
|
+
return {
|
|
451
|
+
...(pushed.localUrl ? { localUrl: pushed.localUrl } : {}),
|
|
452
|
+
got: { bytes: base64ToBytes(pushed.bytesBase64), contentType: pushed.contentType },
|
|
453
|
+
};
|
|
454
|
+
}
|
|
455
|
+
// Read a declared file, preferring a local stand-in when one exists.
|
|
456
|
+
//
|
|
457
|
+
// Candidates are tried in order and the first that READS wins — the
|
|
458
|
+
// probe and the real read are the same fetch, so a hit costs nothing
|
|
459
|
+
// extra. A root is listed only when the search reaches it, to rule out
|
|
460
|
+
// candidates under it that can't exist (see `localCandidateMayExist`),
|
|
461
|
+
// so a hit in an early root never lists the later ones. `ensureFetch`
|
|
462
|
+
// caches the reads for the rest of the pass, misses included.
|
|
463
|
+
//
|
|
464
|
+
// `last` is what the last check found, passed by the page-load checks
|
|
465
|
+
// (a listing per root costs more than the whole rest of such a check):
|
|
466
|
+
// - its stand-in, when still a candidate under these roots, is read
|
|
467
|
+
// first and no root is listed. A path that no longer reads falls
|
|
468
|
+
// through to the search.
|
|
469
|
+
// - no stand-in last time means none is looked for: one appearing is
|
|
470
|
+
// rare, and searching would list every root on every page load.
|
|
471
|
+
// Either can miss a copy that appeared since, nearer or at all; the
|
|
472
|
+
// searching checks (focus, Refresh) find it.
|
|
473
|
+
//
|
|
474
|
+
// Returns `null` when no stand-in was found, so the caller can take its
|
|
475
|
+
// `recheck: 'preserve'` shortcut on the declared URL. That ordering is
|
|
476
|
+
// deliberate: a stand-in is checked on EVERY refresh, including
|
|
477
|
+
// opportunistic ones, so editing a local library shows up on the next
|
|
478
|
+
// navigation — while a remote dependency still gets the cached-and-left-
|
|
479
|
+
// alone treatment that keeps per-navigation cost bounded.
|
|
480
|
+
//
|
|
481
|
+
// A `null` here does NOT mean the caller can preserve any prior: a prior
|
|
482
|
+
// that itself carries a `localUrl` describes a file we just failed to
|
|
483
|
+
// find, so keeping it would serve a deleted stand-in indefinitely. Each
|
|
484
|
+
// caller's preserve shortcut checks for that.
|
|
485
|
+
async function readLocalStandIn(declaredUrl, localRoots, fetchCache, last) {
|
|
486
|
+
const candidates = localFileCandidatesDetailed(declaredUrl, localRoots);
|
|
487
|
+
if (last && !last.localUrl)
|
|
488
|
+
return null;
|
|
489
|
+
const known = last?.localUrl;
|
|
490
|
+
if (known && candidates.some((c) => c.url === known)) {
|
|
491
|
+
const got = await ensureFetch(fetchCache, known);
|
|
492
|
+
if (!isFailure(got))
|
|
493
|
+
return standInRead(known, got);
|
|
494
|
+
}
|
|
495
|
+
const listings = listingsFor(fetchCache);
|
|
496
|
+
for (const candidate of candidates) {
|
|
497
|
+
if (!await localCandidateMayExist(candidate, listings))
|
|
498
|
+
continue;
|
|
499
|
+
const got = await ensureFetch(fetchCache, candidate.url);
|
|
500
|
+
// A candidate that doesn't read isn't a failure — it just isn't
|
|
501
|
+
// there. Only an actual hit ends the search.
|
|
502
|
+
if (!isFailure(got))
|
|
503
|
+
return standInRead(candidate.url, got);
|
|
504
|
+
}
|
|
505
|
+
return null;
|
|
506
|
+
}
|
|
507
|
+
// A stand-in that read, with its modified time for the "modified" chip.
|
|
508
|
+
// The time comes from the read itself (`Last-Modified`), in the units a
|
|
509
|
+
// listing of its folder would give.
|
|
510
|
+
function standInRead(localUrl, got) {
|
|
511
|
+
return { localUrl, got, ...(got.fsMtime !== undefined ? { fsMtime: got.fsMtime } : {}) };
|
|
512
|
+
}
|
|
513
|
+
// The root-directory listings the stand-in search gates on,
|
|
514
|
+
// given the same lifetime as the fetch cache they sit beside: one
|
|
515
|
+
// refresh pass, or the page for the install preview's page-lifetime
|
|
516
|
+
// cache, which memoizes misses for that long anyway. Keyed off the fetch cache
|
|
517
|
+
// rather than threaded as another parameter because every entry point
|
|
518
|
+
// that builds a `FileFetchCache` would otherwise have to build this one
|
|
519
|
+
// too, and the two must never outlive each other — a listing that
|
|
520
|
+
// outlived its pass would hide a stand-in created after it was taken.
|
|
521
|
+
const listingCaches = new WeakMap();
|
|
522
|
+
// A pass that already keeps directory listings (the navigation
|
|
523
|
+
// `RefreshBatch`) registers them here, so the stat it took of the
|
|
524
|
+
// script's own folder and the gate's read of that same folder are one
|
|
525
|
+
// listing rather than two.
|
|
526
|
+
export function bindListingCache(fetchCache, listings) {
|
|
527
|
+
listingCaches.set(fetchCache, listings);
|
|
528
|
+
}
|
|
529
|
+
function listingsFor(fetchCache) {
|
|
530
|
+
let listings = listingCaches.get(fetchCache);
|
|
531
|
+
if (!listings) {
|
|
532
|
+
listings = new Map();
|
|
533
|
+
listingCaches.set(fetchCache, listings);
|
|
534
|
+
}
|
|
535
|
+
return listings;
|
|
536
|
+
}
|
|
537
|
+
// What the last check found for this file, under a 'known' lookup. A
|
|
538
|
+
// file the last check didn't load (newly declared) has nothing known,
|
|
539
|
+
// so it is searched for.
|
|
540
|
+
function lastLookup(lookup, prior) {
|
|
541
|
+
return lookup === 'known' ? prior : undefined;
|
|
542
|
+
}
|
|
543
|
+
// Start reading the stand-ins a script's last check found, into the
|
|
544
|
+
// pass's fetch cache, so a check can overlap them with reading the
|
|
545
|
+
// script itself. The loaders then take them from the cache. Only for a
|
|
546
|
+
// 'known' lookup, which reads these paths first anyway.
|
|
547
|
+
export function prefetchKnownStandIns(fetchCache, prior) {
|
|
548
|
+
for (const entry of [...(prior.requires ?? []), ...(prior.resources ?? [])]) {
|
|
549
|
+
if (entry.localUrl)
|
|
550
|
+
void ensureFetch(fetchCache, entry.localUrl);
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
// Where a ref resolves: the pushed body's URL when there is one, which
|
|
554
|
+
// the pusher already resolved and checked, else the ref resolved
|
|
555
|
+
// against the script.
|
|
556
|
+
function resolveRef(ref, scriptFileUrl, pushed) {
|
|
557
|
+
if (pushed)
|
|
558
|
+
return { url: pushed.url };
|
|
559
|
+
const resolved = resolveScriptResourceDetailed(ref, scriptFileUrl);
|
|
560
|
+
if ('failed' in resolved)
|
|
561
|
+
return { failure: describeResolveFailure(resolved) };
|
|
562
|
+
if (resolved.kind === 'data')
|
|
563
|
+
return { failure: DATA_URL_NOT_SUPPORTED };
|
|
564
|
+
return { url: resolved.url };
|
|
565
|
+
}
|
|
566
|
+
async function loadRequire(args) {
|
|
567
|
+
const resolved = resolveRef(args.ref, args.scriptFileUrl, args.pushed);
|
|
568
|
+
if ('failure' in resolved) {
|
|
569
|
+
return { errorMessage: `@require ${args.ref}: ${resolved.failure}` };
|
|
570
|
+
}
|
|
571
|
+
if (args.seenUrls.has(resolved.url))
|
|
572
|
+
return {};
|
|
573
|
+
args.seenUrls.add(resolved.url);
|
|
574
|
+
const prior = args.prior.get(resolved.url);
|
|
575
|
+
// Local stand-in first, and before the preserve shortcut — see
|
|
576
|
+
// `readLocalStandIn` for why an overridden file is never cached past
|
|
577
|
+
// a refresh. A pushed body is read every time for the same reason.
|
|
578
|
+
const local = args.pushed ? pushedBytes(args.pushed)
|
|
579
|
+
: await timePhase('local-standin', () => readLocalStandIn(resolved.url, args.localRoots, args.fetchCache, lastLookup(args.localLookup, prior)));
|
|
580
|
+
if (!local && !args.pushed && args.recheck === 'preserve' && prior && !prior.localUrl) {
|
|
581
|
+
return { entry: prior };
|
|
582
|
+
}
|
|
583
|
+
const loaded = local ?? {
|
|
584
|
+
got: await ensureFetch(args.fetchCache, resolved.url, priorCopyOf(prior, priorRequireBytes(prior))),
|
|
585
|
+
};
|
|
586
|
+
const got = loaded.got;
|
|
587
|
+
if (isFailure(got)) {
|
|
588
|
+
return { errorMessage: `@require ${args.ref}: ${got.errorMessage}` };
|
|
589
|
+
}
|
|
590
|
+
// The server answered 304 against OUR validator, so the copy we hold
|
|
591
|
+
// IS the current one — reuse it whole.
|
|
592
|
+
//
|
|
593
|
+
// Not merely an optimisation (though it does drop a SHA-256 per
|
|
594
|
+
// dependency per manual refresh). Rebuilding from the reconstructed
|
|
595
|
+
// bytes would be wrong: `source` was decoded with a BOM-stripping
|
|
596
|
+
// decoder, so re-encoding it does not reproduce the original bytes
|
|
597
|
+
// for a BOM-prefixed file. That would hash differently, bump
|
|
598
|
+
// `loadedAt`, shrink the stored `size` by three, and report a content
|
|
599
|
+
// change the server just told us did not happen.
|
|
600
|
+
//
|
|
601
|
+
// Matched on the hash rather than on "did we send a validator",
|
|
602
|
+
// because the shared cache may have resolved this URL from ANOTHER
|
|
603
|
+
// script's prior. Equal hashes mean equal content, so reusing this
|
|
604
|
+
// caller's own row is right; unequal means these are someone else's
|
|
605
|
+
// bytes and the normal path below has to run.
|
|
606
|
+
if (prior && got.priorContentHash !== undefined && got.priorContentHash === prior.contentHash) {
|
|
607
|
+
return { entry: { ...prior, ref: args.ref, url: resolved.url, ...validatorFields(got) } };
|
|
608
|
+
}
|
|
609
|
+
// @require-specific markup gate. Applied here (not in the shared
|
|
610
|
+
// fetch) so a @require + @resource pair pointing at the same URL
|
|
611
|
+
// share one fetch without the require's stricter rule poisoning the
|
|
612
|
+
// resource's cache entry — a `@resource` may legitimately BE an HTML
|
|
613
|
+
// file.
|
|
614
|
+
//
|
|
615
|
+
// Only markup is rejected. `application/octet-stream` and other
|
|
616
|
+
// vague types are accepted on purpose — they are common and
|
|
617
|
+
// legitimate for JavaScript (GitHub serves every release asset that
|
|
618
|
+
// way, and release URLs are a normal thing to pin a library to), and
|
|
619
|
+
// the strict-UTF-8 decode below is what actually closes the
|
|
620
|
+
// "anything binary gets through" hole. Tampermonkey does no
|
|
621
|
+
// content-type gating on `@require` at all.
|
|
622
|
+
if (isMarkupContentType(got.contentType)) {
|
|
623
|
+
return {
|
|
624
|
+
errorMessage: `@require ${args.ref}: ${markupResponseMessage({
|
|
625
|
+
...(loaded.localUrl ? { localUrl: loaded.localUrl } : {}),
|
|
626
|
+
contentType: got.contentType,
|
|
627
|
+
})}`,
|
|
628
|
+
};
|
|
629
|
+
}
|
|
630
|
+
// Strict UTF-8 decode: catches binary regardless of what the server
|
|
631
|
+
// claimed the type was (a misconfigured CDN serving a PNG, or the
|
|
632
|
+
// generic octet-stream the markup gate deliberately lets
|
|
633
|
+
// through). Previously we used fatal:false and the bytes were
|
|
634
|
+
// mojibake'd into JS source, where they'd produce a confusing
|
|
635
|
+
// SyntaxError at eval. This is the gate that closes that hole.
|
|
636
|
+
let source;
|
|
637
|
+
try {
|
|
638
|
+
source = new TextDecoder('utf-8', { fatal: true }).decode(got.bytes);
|
|
639
|
+
}
|
|
640
|
+
catch {
|
|
641
|
+
return { errorMessage: `@require ${args.ref}: The response is not valid UTF-8 (binary or wrong-encoding file)` };
|
|
642
|
+
}
|
|
643
|
+
// Second half of the same gate, on the bytes rather than the header.
|
|
644
|
+
// The Content-Type test above can't fire when there is no
|
|
645
|
+
// Content-Type — the common `file://` case, which is exactly where a
|
|
646
|
+
// directory-listing HTML page comes from — nor when a server labels
|
|
647
|
+
// its login page `text/plain`.
|
|
648
|
+
if (looksLikeMarkup(source)) {
|
|
649
|
+
return {
|
|
650
|
+
errorMessage: `@require ${args.ref}: ${markupResponseMessage({
|
|
651
|
+
...(loaded.localUrl ? { localUrl: loaded.localUrl } : {}),
|
|
652
|
+
})}`,
|
|
653
|
+
};
|
|
654
|
+
}
|
|
655
|
+
const u8 = new Uint8Array(got.bytes);
|
|
656
|
+
const contentHash = await sha256Hex(got.bytes);
|
|
657
|
+
const unchanged = prior?.contentHash === contentHash;
|
|
658
|
+
const loadedAt = unchanged && prior ? prior.loadedAt : args.now;
|
|
659
|
+
return {
|
|
660
|
+
entry: {
|
|
661
|
+
ref: args.ref,
|
|
662
|
+
url: resolved.url,
|
|
663
|
+
source,
|
|
664
|
+
size: u8.byteLength,
|
|
665
|
+
contentHash,
|
|
666
|
+
contentType: got.contentType,
|
|
667
|
+
loadedAt,
|
|
668
|
+
// Spread rather than assigned so the field is ABSENT (not
|
|
669
|
+
// `undefined`) on the ordinary path — stored rows are compared
|
|
670
|
+
// and JSON-round-tripped, and an explicit `undefined` would
|
|
671
|
+
// survive as a key in some of those comparisons.
|
|
672
|
+
...standInFields(loaded),
|
|
673
|
+
...validatorFields(got),
|
|
674
|
+
},
|
|
675
|
+
};
|
|
676
|
+
}
|
|
677
|
+
// The prior copy's bytes, for a conditional request. `source` was
|
|
678
|
+
// decoded from those bytes with a strict UTF-8 decoder, so re-encoding
|
|
679
|
+
// reproduces them exactly.
|
|
680
|
+
function priorRequireBytes(prior) {
|
|
681
|
+
if (!prior)
|
|
682
|
+
return null;
|
|
683
|
+
return new TextEncoder().encode(prior.source).buffer;
|
|
684
|
+
}
|
|
685
|
+
// The prior copy's bytes, decoded back from storage.
|
|
686
|
+
function priorResourceBytes(prior) {
|
|
687
|
+
if (!prior)
|
|
688
|
+
return null;
|
|
689
|
+
return base64ToBytes(prior.bytesBase64);
|
|
690
|
+
}
|
|
691
|
+
// Assemble the conditional-request input, or nothing when this entry
|
|
692
|
+
// can't support one.
|
|
693
|
+
//
|
|
694
|
+
// Withheld for a local stand-in (`localUrl`): the bytes we hold came
|
|
695
|
+
// off disk, and the validators — if any survived — describe the remote
|
|
696
|
+
// copy, so pairing them would ask the server about one file and answer
|
|
697
|
+
// with another.
|
|
698
|
+
function priorCopyOf(prior, bytes) {
|
|
699
|
+
if (!prior || !bytes)
|
|
700
|
+
return undefined;
|
|
701
|
+
if (prior.localUrl)
|
|
702
|
+
return undefined;
|
|
703
|
+
if (!prior.httpEtag && !prior.httpLastModified)
|
|
704
|
+
return undefined;
|
|
705
|
+
// Without a Content-Type the prior can't fully stand in for a
|
|
706
|
+
// response. `StoredRequire.contentType` is optional — rows written
|
|
707
|
+
// before it existed don't carry one — and a 304 resolved from such a
|
|
708
|
+
// row would hand `''` to every sharer of that URL through the fetch
|
|
709
|
+
// cache, including a `@resource` whose blob URL needs a real MIME.
|
|
710
|
+
// Refetching unconditionally costs one body and keeps that honest.
|
|
711
|
+
if (!prior.contentType)
|
|
712
|
+
return undefined;
|
|
713
|
+
return {
|
|
714
|
+
bytes,
|
|
715
|
+
contentType: prior.contentType,
|
|
716
|
+
contentHash: prior.contentHash,
|
|
717
|
+
...(prior.httpEtag ? { etag: prior.httpEtag } : {}),
|
|
718
|
+
...(prior.httpLastModified ? { lastModified: prior.httpLastModified } : {}),
|
|
719
|
+
};
|
|
720
|
+
}
|
|
721
|
+
// Validators onto the stored entry, absent rather than `undefined` for
|
|
722
|
+
// the same reason `localUrl` is spread above.
|
|
723
|
+
function validatorFields(got) {
|
|
724
|
+
return {
|
|
725
|
+
...(got.etag ? { httpEtag: got.etag } : {}),
|
|
726
|
+
...(got.lastModified ? { httpLastModified: got.lastModified } : {}),
|
|
727
|
+
};
|
|
728
|
+
}
|
|
729
|
+
async function loadResource(args) {
|
|
730
|
+
// A `@resource` line carries a name AND a ref, and the name alone
|
|
731
|
+
// doesn't say what failed to load. `@require` names its ref because
|
|
732
|
+
// that is all it has; this names both.
|
|
733
|
+
const subject = `@resource ${args.decl.name} (${args.decl.ref})`;
|
|
734
|
+
const resolved = resolveRef(args.decl.ref, args.scriptFileUrl, args.pushed);
|
|
735
|
+
if ('failure' in resolved) {
|
|
736
|
+
return { errorMessage: `${subject}: ${resolved.failure}` };
|
|
737
|
+
}
|
|
738
|
+
// Resources can be any media type.
|
|
739
|
+
// No URL-level dedup: two `@resource` lines with different
|
|
740
|
+
// names pointing at the same URL are intentional — we want two
|
|
741
|
+
// distinct StoredResource entries so each name resolves to its own
|
|
742
|
+
// GM_getResource* lookup. The shared FileFetchCache still ensures
|
|
743
|
+
// we only fetch the URL once.
|
|
744
|
+
// Prior lookup by URL.
|
|
745
|
+
const prior = args.prior.get(resolved.url);
|
|
746
|
+
// Local stand-in first, and before the preserve shortcut — same rule
|
|
747
|
+
// and same reasoning as `@require`; see `readLocalStandIn`.
|
|
748
|
+
const local = args.pushed ? pushedBytes(args.pushed)
|
|
749
|
+
: await timePhase('local-standin', () => readLocalStandIn(resolved.url, args.localRoots, args.fetchCache, lastLookup(args.localLookup, prior)));
|
|
750
|
+
if (!local && args.recheck === 'preserve' &&
|
|
751
|
+
prior && prior.name === args.decl.name && !prior.localUrl) {
|
|
752
|
+
return { entry: prior };
|
|
753
|
+
}
|
|
754
|
+
// Resources accept anything — including HTML. A script that wants
|
|
755
|
+
// to embed a fragment of HTML as a string via GM_getResourceText
|
|
756
|
+
// shouldn't be blocked.
|
|
757
|
+
const loaded = local ?? {
|
|
758
|
+
got: await ensureFetch(args.fetchCache, resolved.url, priorCopyOf(prior, priorResourceBytes(prior))),
|
|
759
|
+
};
|
|
760
|
+
const got = loaded.got;
|
|
761
|
+
if (isFailure(got)) {
|
|
762
|
+
return { errorMessage: `${subject}: ${got.errorMessage}` };
|
|
763
|
+
}
|
|
764
|
+
// Same 304 reuse as `loadRequire`. The name has to match too: a
|
|
765
|
+
// `@resource` renamed to the same URL is a changed declaration even
|
|
766
|
+
// though the body is identical, and the normal path below is what
|
|
767
|
+
// stamps the new name and bumps `loadedAt`.
|
|
768
|
+
if (prior && prior.name === args.decl.name &&
|
|
769
|
+
got.priorContentHash !== undefined && got.priorContentHash === prior.contentHash) {
|
|
770
|
+
return {
|
|
771
|
+
entry: { ...prior, ref: args.decl.ref, url: resolved.url, ...validatorFields(got) },
|
|
772
|
+
};
|
|
773
|
+
}
|
|
774
|
+
const u8 = new Uint8Array(got.bytes);
|
|
775
|
+
const contentHash = await sha256Hex(got.bytes);
|
|
776
|
+
const unchanged = prior && prior.contentHash === contentHash && prior.name === args.decl.name;
|
|
777
|
+
const loadedAt = unchanged && prior ? prior.loadedAt : args.now;
|
|
778
|
+
return {
|
|
779
|
+
entry: {
|
|
780
|
+
ref: args.decl.ref,
|
|
781
|
+
url: resolved.url,
|
|
782
|
+
name: args.decl.name,
|
|
783
|
+
bytesBase64: bytesToBase64(u8),
|
|
784
|
+
size: u8.byteLength,
|
|
785
|
+
contentHash,
|
|
786
|
+
contentType: got.contentType,
|
|
787
|
+
loadedAt,
|
|
788
|
+
...standInFields(loaded),
|
|
789
|
+
...validatorFields(got),
|
|
790
|
+
},
|
|
791
|
+
};
|
|
792
|
+
}
|
|
793
|
+
// ---------- Shared byte fetcher ----------
|
|
794
|
+
// `prior` is used only by the FIRST caller to ask for a given URL —
|
|
795
|
+
// whoever populates the cache entry. A later caller sharing the URL
|
|
796
|
+
// takes that entry as-is.
|
|
797
|
+
//
|
|
798
|
+
// That is safe even when the two hold different copies, because a 304
|
|
799
|
+
// resolves to the prior's BYTES rather than to a "nothing changed"
|
|
800
|
+
// marker. The cache entry always means "the current bytes for this
|
|
801
|
+
// URL", however they were obtained, so a script with no prior of its
|
|
802
|
+
// own still gets a body it can build from.
|
|
803
|
+
function ensureFetch(cache, url, prior) {
|
|
804
|
+
// The cache stores raw bytes + Content-Type, no kind-specific
|
|
805
|
+
// filtering applied. Per-kind acceptance (the @require Content-Type
|
|
806
|
+
// gate, the @require UTF-8 fatal decode, etc.) happens at the
|
|
807
|
+
// typed call site after the cache hit. This way a @require + a
|
|
808
|
+
// @resource pointing at the same URL share one fetch without
|
|
809
|
+
// either's rules biasing the other's outcome.
|
|
810
|
+
const cached = cache.get(url);
|
|
811
|
+
if (cached)
|
|
812
|
+
return cached;
|
|
813
|
+
const promise = timePhase('dep-fetch', () => fetchFileBytes(url, prior));
|
|
814
|
+
cache.set(url, promise);
|
|
815
|
+
return promise;
|
|
816
|
+
}
|
|
817
|
+
async function fetchFileBytes(url, prior) {
|
|
818
|
+
try {
|
|
819
|
+
const res = await fetchResourceBytes(url, {
|
|
820
|
+
maxBytes: MAX_FILE_BYTES,
|
|
821
|
+
// Only conditional when we hold a validator to offer. A bare
|
|
822
|
+
// prior with neither would send no header and just refetch.
|
|
823
|
+
...(prior && (prior.etag ?? prior.lastModified)
|
|
824
|
+
? { conditional: { etag: prior.etag, lastModified: prior.lastModified } }
|
|
825
|
+
: {}),
|
|
826
|
+
});
|
|
827
|
+
if ('failed' in res) {
|
|
828
|
+
return { failed: true, errorMessage: describeFetchFailure(res) };
|
|
829
|
+
}
|
|
830
|
+
if (isNotModified(res)) {
|
|
831
|
+
// Unreachable without a prior — `conditional` is only set above
|
|
832
|
+
// when one exists — but the type doesn't know that, and resolving
|
|
833
|
+
// to the prior's bytes is what makes the shared cache entry mean
|
|
834
|
+
// the same thing on both paths.
|
|
835
|
+
if (!prior) {
|
|
836
|
+
return {
|
|
837
|
+
failed: true,
|
|
838
|
+
errorMessage: 'Internal error: the server reported no change for a file we do not hold',
|
|
839
|
+
};
|
|
840
|
+
}
|
|
841
|
+
// Validators carried forward, not re-read: a 304 need not repeat
|
|
842
|
+
// them, and the ones we sent are by definition still current.
|
|
843
|
+
const out = {
|
|
844
|
+
bytes: prior.bytes,
|
|
845
|
+
contentType: prior.contentType,
|
|
846
|
+
priorContentHash: prior.contentHash,
|
|
847
|
+
};
|
|
848
|
+
if (prior.etag)
|
|
849
|
+
out.etag = prior.etag;
|
|
850
|
+
if (prior.lastModified)
|
|
851
|
+
out.lastModified = prior.lastModified;
|
|
852
|
+
return out;
|
|
853
|
+
}
|
|
854
|
+
const out = { bytes: res.body, contentType: res.contentType };
|
|
855
|
+
if (res.etag)
|
|
856
|
+
out.etag = res.etag;
|
|
857
|
+
if (res.lastModified)
|
|
858
|
+
out.lastModified = res.lastModified;
|
|
859
|
+
if (res.fsMtime !== undefined)
|
|
860
|
+
out.fsMtime = res.fsMtime;
|
|
861
|
+
return out;
|
|
862
|
+
}
|
|
863
|
+
catch (err) {
|
|
864
|
+
return {
|
|
865
|
+
failed: true,
|
|
866
|
+
// NOT a `describeFetchFailure` string, deliberately.
|
|
867
|
+
// `fetchResourceBytes` reports every fetch outcome as a
|
|
868
|
+
// `FetchFailure` return and does not throw, so reaching here means
|
|
869
|
+
// our own code broke its contract — which is what the reader is
|
|
870
|
+
// being told, and why `isFetchFailureMessage` correctly declines
|
|
871
|
+
// it. Phrasing it as one of the fetch failures would both invite
|
|
872
|
+
// the corpus validator to substitute mirror wording for a bug of
|
|
873
|
+
// ours, and put a second `could not be fetched: ` colon into a
|
|
874
|
+
// string that already lands after `@require <ref>: `.
|
|
875
|
+
errorMessage: `Internal error: the file fetcher failed unexpectedly (${err instanceof Error ? err.message : String(err)})`,
|
|
876
|
+
};
|
|
877
|
+
}
|
|
878
|
+
}
|