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,276 @@
|
|
|
1
|
+
// Deleting a local copy — the script file itself, and optionally the
|
|
2
|
+
// dependency files that were copied beside it — off the disk.
|
|
3
|
+
//
|
|
4
|
+
// The counterpart of `local-copy.ts`, and it can only undo what that
|
|
5
|
+
// module did: `chrome.downloads.removeFile` deletes a file the browser
|
|
6
|
+
// itself downloaded, found by its absolute path in Chrome's download
|
|
7
|
+
// history. Nothing else an extension can call deletes a file, so a copy
|
|
8
|
+
// whose history entry is gone can only be reported, not removed.
|
|
9
|
+
//
|
|
10
|
+
// See docs/local-delete.md.
|
|
11
|
+
import { isFileUrl, locationFsPath } from '../file-locations.js';
|
|
12
|
+
import { describeError, } from './script-store.js';
|
|
13
|
+
import { dlog } from '../debug-log.js';
|
|
14
|
+
import { downloadsCopiesDir, readDownloadsCopiesSetting, } from './local-copy.js';
|
|
15
|
+
// The copies setting, or null when it can't be read — a surface that
|
|
16
|
+
// asks "is this row a local copy?" while painting must not be taken
|
|
17
|
+
// down by a storage failure, and null is the safe answer (no row
|
|
18
|
+
// offers a file deletion). One helper so the pages that ask can't
|
|
19
|
+
// drift on what a failure means.
|
|
20
|
+
export async function readCopiesSetting() {
|
|
21
|
+
try {
|
|
22
|
+
return await readDownloadsCopiesSetting();
|
|
23
|
+
}
|
|
24
|
+
catch (err) {
|
|
25
|
+
dlog(`[SourceMonkey] could not read the local-copies setting: ${describeError(err)}`);
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
// ---------- Which scripts can be deleted from disk ----------
|
|
30
|
+
// The absolute path of `script`'s file when it is a copy we wrote, and
|
|
31
|
+
// null when it isn't. `collection` is the row's own collection.
|
|
32
|
+
//
|
|
33
|
+
// Both halves are required, and the collection is the important one:
|
|
34
|
+
// the row has to sit in the directory collection AT the copies
|
|
35
|
+
// directory. That is what makes the rest of this module's reasoning
|
|
36
|
+
// true — a file in that directory is shared only within that
|
|
37
|
+
// collection, so its scripts are the whole set that could be reading
|
|
38
|
+
// it, and `copyDeletePlan` can answer "does anything else need this?"
|
|
39
|
+
// from one collection's rows.
|
|
40
|
+
//
|
|
41
|
+
// A row somewhere else whose file happens to be in that directory — an
|
|
42
|
+
// individual script the user added by hand at that path — is refused.
|
|
43
|
+
// Deleting through it would compute the shared/own split against the
|
|
44
|
+
// wrong set of scripts and could take a library another copy is
|
|
45
|
+
// loading. Individual-file rows, overlapping directories and manifests
|
|
46
|
+
// all break the assumption in the same way; none of them is worth the
|
|
47
|
+
// mess (see TODO.md).
|
|
48
|
+
//
|
|
49
|
+
export function deletableCopyPath(script, collection, setting) {
|
|
50
|
+
if (!setting || !collection)
|
|
51
|
+
return null;
|
|
52
|
+
const dir = downloadsCopiesDir(setting.downloadsDir);
|
|
53
|
+
if (!isCopiesDir(collection.path, dir))
|
|
54
|
+
return null;
|
|
55
|
+
const path = locationFsPath(script.fileUrl);
|
|
56
|
+
return path.startsWith(`${dir}/`) ? path : null;
|
|
57
|
+
}
|
|
58
|
+
// Is this collection the one at the copies directory? Compared as
|
|
59
|
+
// paths, so the collection is recognized however it was created — the
|
|
60
|
+
// one our first copy made, or one the user had already added there and
|
|
61
|
+
// we adopted (see local-copy.md).
|
|
62
|
+
function isCopiesDir(collectionPath, copiesDir) {
|
|
63
|
+
return locationFsPath(collectionPath).replace(/\/+$/, '') === copiesDir;
|
|
64
|
+
}
|
|
65
|
+
// What deleting `script` would remove, given every script in its
|
|
66
|
+
// collection (`siblings`, which may include `script` itself).
|
|
67
|
+
//
|
|
68
|
+
// Only siblings are consulted because only they can be reading these
|
|
69
|
+
// files: a local stand-in is searched for under the script's own folder
|
|
70
|
+
// and its collection root (see local-file-overrides.md), so a file
|
|
71
|
+
// under the copies directory is invisible to a script outside it.
|
|
72
|
+
export function copyDeletePlan(script, siblings, copiesDir) {
|
|
73
|
+
// Which files another script in the collection is loading. Only
|
|
74
|
+
// whether, not which scripts: the dialog doesn't name them — a
|
|
75
|
+
// userscript name is long and there can be several.
|
|
76
|
+
const others = new Set();
|
|
77
|
+
for (const other of siblings) {
|
|
78
|
+
if (other.id === script.id)
|
|
79
|
+
continue;
|
|
80
|
+
for (const path of localDependencyPaths(other, copiesDir).keys())
|
|
81
|
+
others.add(path);
|
|
82
|
+
}
|
|
83
|
+
const own = [];
|
|
84
|
+
const shared = [];
|
|
85
|
+
for (const [path, url] of localDependencyPaths(script, copiesDir)) {
|
|
86
|
+
const dep = {
|
|
87
|
+
path,
|
|
88
|
+
relativePath: path.slice(copiesDir.length + 1),
|
|
89
|
+
url,
|
|
90
|
+
};
|
|
91
|
+
(others.has(path) ? shared : own).push(dep);
|
|
92
|
+
}
|
|
93
|
+
const path = locationFsPath(script.fileUrl);
|
|
94
|
+
return {
|
|
95
|
+
path,
|
|
96
|
+
relativePath: path.startsWith(`${copiesDir}/`) ? path.slice(copiesDir.length + 1) : path,
|
|
97
|
+
own,
|
|
98
|
+
shared,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
// The files under `copiesDir` a script is currently loading its
|
|
102
|
+
// `@require` / `@resource` entries from, as path → declared URL.
|
|
103
|
+
//
|
|
104
|
+
// What the load pipeline actually read is the authority, so this lists
|
|
105
|
+
// files in use rather than paths a copy might once have written: the
|
|
106
|
+
// `localUrl` of a stand-in for a web URL, or the `file://` URL itself
|
|
107
|
+
// of a relative ref. Deduped by path — one file can serve two
|
|
108
|
+
// declarations.
|
|
109
|
+
function localDependencyPaths(script, copiesDir) {
|
|
110
|
+
const out = new Map();
|
|
111
|
+
for (const file of [...(script.requires ?? []), ...(script.resources ?? [])]) {
|
|
112
|
+
const readFrom = file.localUrl ?? (isFileUrl(file.url) ? file.url : null);
|
|
113
|
+
if (!readFrom)
|
|
114
|
+
continue;
|
|
115
|
+
const path = locationFsPath(readFrom);
|
|
116
|
+
if (!path.startsWith(`${copiesDir}/`) || out.has(path))
|
|
117
|
+
continue;
|
|
118
|
+
out.set(path, file.url);
|
|
119
|
+
}
|
|
120
|
+
return out;
|
|
121
|
+
}
|
|
122
|
+
// ---------- Which files the browser can still be asked about ----------
|
|
123
|
+
// Every completed download whose file is this path AND that Chrome
|
|
124
|
+
// still believes is on disk. `filename` is an exact match on the
|
|
125
|
+
// absolute path, and the record outlives the session — it is cleared
|
|
126
|
+
// with the user's browsing data, not on restart.
|
|
127
|
+
//
|
|
128
|
+
// `exists` is the important half. Chrome refuses `removeFile` with
|
|
129
|
+
// *Download file already deleted* for a record it has marked gone, and
|
|
130
|
+
// that mark is sticky: it is set by Chrome's own existence checks, so a
|
|
131
|
+
// copy edited by a tool that saves via write-to-temp-and-rename can
|
|
132
|
+
// carry it while the file is sitting right there. Filtering here means
|
|
133
|
+
// the dialog's lists and the deletion agree — a file Chrome won't act
|
|
134
|
+
// on is shown as one we can't delete, before anything is pressed,
|
|
135
|
+
// rather than failing half way through.
|
|
136
|
+
function completedDownloads(path) {
|
|
137
|
+
return chrome.downloads.search({ filename: path, state: 'complete', exists: true });
|
|
138
|
+
}
|
|
139
|
+
// The subset of `paths` Chrome still holds a download record for — the
|
|
140
|
+
// only ones `removeFile` can act on. Asked up front so the dialog can
|
|
141
|
+
// say which files are going and which are being left, rather than
|
|
142
|
+
// finding out one failure at a time.
|
|
143
|
+
//
|
|
144
|
+
// A search that throws counts as "no record": the answer is only used
|
|
145
|
+
// to decide what to attempt, and attempting a file we can't ask about
|
|
146
|
+
// would fail anyway.
|
|
147
|
+
export async function trackedFiles(paths) {
|
|
148
|
+
const tracked = new Set();
|
|
149
|
+
await Promise.all(paths.map(async (path) => {
|
|
150
|
+
try {
|
|
151
|
+
if ((await completedDownloads(path)).length > 0)
|
|
152
|
+
tracked.add(path);
|
|
153
|
+
}
|
|
154
|
+
catch (err) {
|
|
155
|
+
dlog(`[SourceMonkey] could not look up the download record for ${path}: ${describeError(err)}`);
|
|
156
|
+
}
|
|
157
|
+
}));
|
|
158
|
+
return tracked;
|
|
159
|
+
}
|
|
160
|
+
// ---------- Deleting ----------
|
|
161
|
+
// Chrome holds no usable download record for the file, so `removeFile`
|
|
162
|
+
// has nothing to act on — the user cleared their downloads, or Chrome
|
|
163
|
+
// has marked the file gone (see `completedDownloads`). Two sentences,
|
|
164
|
+
// both punctuated: this lands in the dialog's error slot and in the
|
|
165
|
+
// viewer's message line, never spliced after a prefix
|
|
166
|
+
// (docs/diagnostics.md#wording).
|
|
167
|
+
//
|
|
168
|
+
// `subject` names what could not be deleted. The script's own file is
|
|
169
|
+
// `SCRIPT_FILE`, with no path in it: the dialog is already showing the
|
|
170
|
+
// file and its full path two rows up, and a relative path with a
|
|
171
|
+
// subdirectory in it makes the sentence hard to read. A dependency
|
|
172
|
+
// file, reported away from any list of its own, carries its path.
|
|
173
|
+
//
|
|
174
|
+
// `alsoOthers` when the same block would have taken other files with
|
|
175
|
+
// it — the copy's own dependencies. They are all still on disk, so the
|
|
176
|
+
// second line names them all rather than just the one it reports on.
|
|
177
|
+
//
|
|
178
|
+
// The second line says what to do next, not just that we can't. With
|
|
179
|
+
// `watched`, the dialog is showing this for the script file with its
|
|
180
|
+
// Delete disabled, and the row has no other trash can (a directory
|
|
181
|
+
// collection's rows are removed by refreshing, which scans the file
|
|
182
|
+
// straight back in) — so without it the user is looking at a dead
|
|
183
|
+
// end. The dialog watches for the file going and then offers to
|
|
184
|
+
// remove the entry (`watchDisk` in the dialog), which is what the
|
|
185
|
+
// line promises. Without `watched` — a dependency reported after the
|
|
186
|
+
// script has already gone, or a dialog that can't watch because file
|
|
187
|
+
// access is off — the line promises nothing.
|
|
188
|
+
export const SCRIPT_FILE = 'Script file';
|
|
189
|
+
export function noDownloadRecord(subject, alsoOthers = false, watched = false) {
|
|
190
|
+
const files = alsoOthers ? 'files' : 'file';
|
|
191
|
+
return `${subject} is no longer in Chrome download history, so SourceMonkey cannot delete it.\n`
|
|
192
|
+
+ (watched
|
|
193
|
+
? `Delete the ${files} by hand, and Delete will then remove this script.`
|
|
194
|
+
: `You can delete the ${files} by hand.`);
|
|
195
|
+
}
|
|
196
|
+
// Ends with a period, unlike most messages: it is joined with the
|
|
197
|
+
// other per-file reports into one line (the viewer's not-found detail,
|
|
198
|
+
// the collections page's error line), where an unpunctuated sentence
|
|
199
|
+
// runs into the next one. Nothing splices it after a prefix.
|
|
200
|
+
function deleteFailed(name, detail) {
|
|
201
|
+
return `Deleting ${name} failed: ${detail}.`;
|
|
202
|
+
}
|
|
203
|
+
// Delete one file the browser downloaded, by its absolute path.
|
|
204
|
+
// Resolves with null on success, or the message to show.
|
|
205
|
+
//
|
|
206
|
+
// `removeFile` deletes the bytes; `erase` then drops the history row,
|
|
207
|
+
// which would otherwise sit in the user's downloads list pointing at a
|
|
208
|
+
// file that is gone. Erasing is best effort — the file is already gone
|
|
209
|
+
// by then, which is what was asked for.
|
|
210
|
+
//
|
|
211
|
+
// **Nothing is erased unless the file went.** The record is the only
|
|
212
|
+
// handle there is on that file: erasing one we failed to delete with
|
|
213
|
+
// would leave the file on disk and take away the one way of ever
|
|
214
|
+
// removing it.
|
|
215
|
+
async function deleteDownloadedFile(path, name, kind = 'File', alsoOthers = false) {
|
|
216
|
+
let items;
|
|
217
|
+
try {
|
|
218
|
+
items = await completedDownloads(path);
|
|
219
|
+
}
|
|
220
|
+
catch (err) {
|
|
221
|
+
return deleteFailed(name, describeError(err));
|
|
222
|
+
}
|
|
223
|
+
if (items.length === 0) {
|
|
224
|
+
return noDownloadRecord(kind === 'Script' ? SCRIPT_FILE : `File ${name}`, alsoOthers);
|
|
225
|
+
}
|
|
226
|
+
// Newest first: one path can have been written several times over
|
|
227
|
+
// (every Replace adds a record), and the latest is the one whose
|
|
228
|
+
// file is on disk now. Older records for the same path are erased
|
|
229
|
+
// too, so the downloads list doesn't keep rows for a file that has
|
|
230
|
+
// gone.
|
|
231
|
+
const ordered = [...items].sort((a, b) => (b.startTime ?? '').localeCompare(a.startTime ?? ''));
|
|
232
|
+
let lastError = '';
|
|
233
|
+
for (const item of ordered) {
|
|
234
|
+
try {
|
|
235
|
+
await chrome.downloads.removeFile(item.id);
|
|
236
|
+
}
|
|
237
|
+
catch (err) {
|
|
238
|
+
// Chrome refuses a record it believes is already gone. Another
|
|
239
|
+
// record for the same path may still hold the live one, so this
|
|
240
|
+
// is only fatal once every record has been tried — and the
|
|
241
|
+
// record we couldn't delete with is left alone.
|
|
242
|
+
lastError = describeError(err);
|
|
243
|
+
continue;
|
|
244
|
+
}
|
|
245
|
+
// The file is gone, so every record for that path now points at
|
|
246
|
+
// nothing. Erased only from here.
|
|
247
|
+
for (const stale of ordered) {
|
|
248
|
+
try {
|
|
249
|
+
await chrome.downloads.erase({ id: stale.id });
|
|
250
|
+
}
|
|
251
|
+
catch (err) {
|
|
252
|
+
dlog(`[SourceMonkey] could not erase the download record for ${path}: ${describeError(err)}`);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
return null;
|
|
256
|
+
}
|
|
257
|
+
return deleteFailed(name, lastError || 'The file could not be removed');
|
|
258
|
+
}
|
|
259
|
+
export async function deleteCopyFiles(request) {
|
|
260
|
+
if (request.script) {
|
|
261
|
+
const scriptError = await deleteDownloadedFile(request.script.path, request.script.relativePath, 'Script',
|
|
262
|
+
// The dependencies were going with it and are staying too, so the
|
|
263
|
+
// message says so — the same answer the dialog gives when it knows
|
|
264
|
+
// up front that the script's record has gone.
|
|
265
|
+
request.dependencies.length > 0);
|
|
266
|
+
if (scriptError)
|
|
267
|
+
return { scriptError, dependencyErrors: [] };
|
|
268
|
+
}
|
|
269
|
+
const dependencyErrors = [];
|
|
270
|
+
for (const dep of request.dependencies) {
|
|
271
|
+
const err = await deleteDownloadedFile(dep.path, dep.relativePath);
|
|
272
|
+
if (err)
|
|
273
|
+
dependencyErrors.push(err);
|
|
274
|
+
}
|
|
275
|
+
return { scriptError: null, dependencyErrors };
|
|
276
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
// Pure naming helpers for script and collection locations, split from
|
|
2
|
+
// directories.ts so pages that only label locations (the toolbar popup,
|
|
3
|
+
// via collection-display.ts) don't load the scanning and fetch code.
|
|
4
|
+
// directories.ts re-exports all of it.
|
|
5
|
+
import { isHttpUrl } from '../file-locations.js';
|
|
6
|
+
// Classify a script location by scheme: which built-in default
|
|
7
|
+
// container it would install into, and which scheme chip its row wears
|
|
8
|
+
// inside a container. github URLs here mean both the human-facing
|
|
9
|
+
// `github.com/...` form and the already-normalized
|
|
10
|
+
// `raw.githubusercontent.com/...` form, since `normalizeCollectionInput`
|
|
11
|
+
// may have rewritten the input before this runs.
|
|
12
|
+
export function containerKindForSingleFileInput(input) {
|
|
13
|
+
if (/^https?:\/\/(www\.)?github\.com\//i.test(input))
|
|
14
|
+
return 'github';
|
|
15
|
+
if (/^https?:\/\/raw\.githubusercontent\.com\//i.test(input))
|
|
16
|
+
return 'github';
|
|
17
|
+
if (isHttpUrl(input))
|
|
18
|
+
return 'http';
|
|
19
|
+
return 'file';
|
|
20
|
+
}
|
|
21
|
+
// ---------- Scratch scripts ----------
|
|
22
|
+
//
|
|
23
|
+
// A script whose only copy is its text in extension storage, with no
|
|
24
|
+
// file or URL behind it. Only sm-dev, the development tool, writes
|
|
25
|
+
// them, over the worker's test surface (docs/sm-dev-design.md).
|
|
26
|
+
//
|
|
27
|
+
// - Each sm-dev run has its own collection of them, its test
|
|
28
|
+
// collection, at `container:sm-dev:<run>`, where `<run>` is the run's
|
|
29
|
+
// unique id.
|
|
30
|
+
// - Each "location" is a `scratch:///<run>/<path>.user.js` URL that
|
|
31
|
+
// names the storage row holding the text, so the ordinary refresh
|
|
32
|
+
// pipeline reads it back like any other body and a rewrite of the
|
|
33
|
+
// same name is picked up as a content change.
|
|
34
|
+
// - The run in the URL is what lets the script run: only in tabs that
|
|
35
|
+
// run's claim holds (sm-dev-link.ts).
|
|
36
|
+
const SM_DEV_CONTAINER_PREFIX = 'container:sm-dev:';
|
|
37
|
+
// Three slashes: with an empty host the run and name are the URL's
|
|
38
|
+
// path, so the basename every label is cut from is the script's own
|
|
39
|
+
// name rather than the whole URL.
|
|
40
|
+
const SCRATCH_SCHEME = 'scratch:///';
|
|
41
|
+
export const SCRATCH_TEXT_KEY_PREFIX = 'scratch:';
|
|
42
|
+
export const SCRATCH_FILES_KEY_PREFIX = 'scratchFiles:';
|
|
43
|
+
export const SCRATCH_SOURCE_KEY_PREFIX = 'scratchSource:';
|
|
44
|
+
// Kept to a URL path segment's safe characters.
|
|
45
|
+
function safeSegment(text) {
|
|
46
|
+
return text.replace(/[^A-Za-z0-9._-]+/g, '-');
|
|
47
|
+
}
|
|
48
|
+
export function isScratchUrl(input) {
|
|
49
|
+
return input.startsWith(SCRATCH_SCHEME);
|
|
50
|
+
}
|
|
51
|
+
// The location for run `run`'s scratch script called `name`: a name,
|
|
52
|
+
// or the path sm-dev was given, relative or absolute, kept whole so
|
|
53
|
+
// two files of the same name stay apart. Ends in `.user.js` so every
|
|
54
|
+
// "is this a script" check reads it as one, and so its basename labels
|
|
55
|
+
// it in the viewer and the logs.
|
|
56
|
+
//
|
|
57
|
+
// Its segments are percent-encoded, as a file:// URL's would be, so the
|
|
58
|
+
// name reads back as written wherever it is shown decoded (the viewer,
|
|
59
|
+
// stack lines): `My%20Script.user.js` stays that, not `My-20Script`.
|
|
60
|
+
export function scratchUrl(run, name) {
|
|
61
|
+
// A lone surrogate has no encoding (encodeURIComponent throws on it),
|
|
62
|
+
// so it becomes U+FFFD, as a decoder would show it.
|
|
63
|
+
const whole = name.replace(/[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]/g, '\uFFFD');
|
|
64
|
+
const segments = whole.split('/').map((segment) => encodeURIComponent(segment));
|
|
65
|
+
const base = (segments.pop() ?? '').replace(/\.user\.js$/i, '') || 'script';
|
|
66
|
+
return `${SCRATCH_SCHEME}${safeSegment(run)}/${[...segments, `${base}.user.js`].join('/')}`;
|
|
67
|
+
}
|
|
68
|
+
// The run a scratch script belongs to. Null for a script that isn't one.
|
|
69
|
+
export function scratchRunOf(url) {
|
|
70
|
+
if (!isScratchUrl(url))
|
|
71
|
+
return null;
|
|
72
|
+
const rest = url.slice(SCRATCH_SCHEME.length);
|
|
73
|
+
const slash = rest.indexOf('/');
|
|
74
|
+
return slash < 0 ? '' : rest.slice(0, slash);
|
|
75
|
+
}
|
|
76
|
+
export function scratchTextKey(url) {
|
|
77
|
+
return SCRATCH_TEXT_KEY_PREFIX + url;
|
|
78
|
+
}
|
|
79
|
+
// The storage row holding the `@require` / `@resource` bodies sm-dev
|
|
80
|
+
// pushed with a scratch script: the ones it read from disk, which the
|
|
81
|
+
// extension couldn't (see `PushedFile` in external-files.ts).
|
|
82
|
+
export function scratchFilesKey(url) {
|
|
83
|
+
return SCRATCH_FILES_KEY_PREFIX + url;
|
|
84
|
+
}
|
|
85
|
+
// The storage row naming the file on the developer's disk a scratch
|
|
86
|
+
// script came from (`ScratchSource`), for sm-dev's `script list`.
|
|
87
|
+
export function scratchSourceKey(url) {
|
|
88
|
+
return SCRATCH_SOURCE_KEY_PREFIX + url;
|
|
89
|
+
}
|
|
90
|
+
// The path of run `run`'s test collection.
|
|
91
|
+
export function smDevContainerPath(run) {
|
|
92
|
+
return SM_DEV_CONTAINER_PREFIX + safeSegment(run);
|
|
93
|
+
}
|
|
94
|
+
// An sm-dev test collection, which only its run changes: no script is
|
|
95
|
+
// moved into or out of one, or installed into one, by hand.
|
|
96
|
+
export function isSmDevCollection(path) {
|
|
97
|
+
return smDevRunOfContainer(path) !== null;
|
|
98
|
+
}
|
|
99
|
+
// The run a test collection belongs to, or null for any other
|
|
100
|
+
// collection.
|
|
101
|
+
export function smDevRunOfContainer(path) {
|
|
102
|
+
return path.startsWith(SM_DEV_CONTAINER_PREFIX) ? path.slice(SM_DEV_CONTAINER_PREFIX.length) : null;
|
|
103
|
+
}
|
|
104
|
+
// The scheme chip's kind for a container script: where it came from.
|
|
105
|
+
// A scratch script has no location; its only copy is in storage.
|
|
106
|
+
export function scriptSchemeKind(location) {
|
|
107
|
+
return isScratchUrl(location) ? 'scratch' : containerKindForSingleFileInput(location);
|
|
108
|
+
}
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
// Formatting for the per-script event log.
|
|
2
|
+
//
|
|
3
|
+
// Two consumers, one set of strings:
|
|
4
|
+
// - the viewer's Log pane table (Time / Status / Source / Message)
|
|
5
|
+
// - the CSV the pane's Save and Copy buttons hand out
|
|
6
|
+
//
|
|
7
|
+
// Keeping both here is what makes an exported row read the same as the
|
|
8
|
+
// row on screen: the timestamp spelling and the `tab 17 ·
|
|
9
|
+
// script-wrapper` source label are written once.
|
|
10
|
+
//
|
|
11
|
+
// The Message column is the exception, and deliberately so. A message
|
|
12
|
+
// carrying a stack trace only becomes readable after its refs are
|
|
13
|
+
// mapped back through the injected entry's source map — which needs
|
|
14
|
+
// the viewer's tabs and the script's current layout, neither of which
|
|
15
|
+
// belongs in a pure module. So `formatLogCsv` takes the renderer as an
|
|
16
|
+
// argument: the viewer passes the one behind its own table, and the
|
|
17
|
+
// default here is the honest fallback for a caller that has no source
|
|
18
|
+
// map to consult.
|
|
19
|
+
//
|
|
20
|
+
// Pure (no DOM, no chrome.*) so unit tests can drive it directly.
|
|
21
|
+
import { describeDiagnostic } from '../diagnostic-location.js';
|
|
22
|
+
import { formatScriptStatusLabel, logIdSuffix } from '../script-display.js';
|
|
23
|
+
// Local wall-clock time, sortable spelling: `2026-08-05 14:03:22.017`.
|
|
24
|
+
// Not ISO/UTC — the reader is looking at their own machine's log and
|
|
25
|
+
// correlating it with what they just saw happen in a tab.
|
|
26
|
+
export function formatLogTimestamp(ms) {
|
|
27
|
+
const d = new Date(ms);
|
|
28
|
+
const yyyy = String(d.getFullYear());
|
|
29
|
+
const mo = String(d.getMonth() + 1).padStart(2, '0');
|
|
30
|
+
const dd = String(d.getDate()).padStart(2, '0');
|
|
31
|
+
const hh = String(d.getHours()).padStart(2, '0');
|
|
32
|
+
const mm = String(d.getMinutes()).padStart(2, '0');
|
|
33
|
+
const ss = String(d.getSeconds()).padStart(2, '0');
|
|
34
|
+
const mmm = String(d.getMilliseconds()).padStart(3, '0');
|
|
35
|
+
return `${yyyy}-${mo}-${dd} ${hh}:${mm}:${ss}.${mmm}`;
|
|
36
|
+
}
|
|
37
|
+
// The Source cell: `tab 17 · script-wrapper` for anything that came
|
|
38
|
+
// from injected code in a specific tab, the bare label otherwise.
|
|
39
|
+
export function formatLogSource(entry) {
|
|
40
|
+
return entry.tabId !== undefined ? `tab ${entry.tabId} · ${entry.source}` : entry.source;
|
|
41
|
+
}
|
|
42
|
+
// The Status cell: the state the entry moved the script into, in the
|
|
43
|
+
// Status badge's own words (`REFRESH FAILED`, `INVALID`, `OK`), or the
|
|
44
|
+
// bare level (`info` / `warning` / `error`) for an entry about
|
|
45
|
+
// something the script itself did. See `LogEntry.state`.
|
|
46
|
+
export function formatLogStatus(entry) {
|
|
47
|
+
return entry.state !== undefined ? formatScriptStatusLabel(entry.state) : entry.level;
|
|
48
|
+
}
|
|
49
|
+
// The CSS class the pane colours the Status cell by: the display state
|
|
50
|
+
// when the entry carries one, the level otherwise (`userscript.html`
|
|
51
|
+
// defines both families).
|
|
52
|
+
export function logStatusClass(entry) {
|
|
53
|
+
return entry.state !== undefined ? `log-state-${entry.state}` : `log-level-${entry.level}`;
|
|
54
|
+
}
|
|
55
|
+
// The Message cell as plain text, with a derived location flattened
|
|
56
|
+
// into the same sentence the pane renders as a link.
|
|
57
|
+
//
|
|
58
|
+
// Correct for every entry SourceMonkey wrote about a script, and wrong
|
|
59
|
+
// only for the stack refs inside an entry the script itself produced —
|
|
60
|
+
// see the note at the top of the file, and `logMessageText` in
|
|
61
|
+
// `userscript.ts` for the renderer that handles those.
|
|
62
|
+
export function formatLogMessage(entry) {
|
|
63
|
+
return describeDiagnostic(entry.message, entry.location);
|
|
64
|
+
}
|
|
65
|
+
// Column headings, in order — the pane's `<th>` row and the CSV's
|
|
66
|
+
// header row are the same four.
|
|
67
|
+
export const LOG_COLUMNS = ['Time', 'Status', 'Source', 'Message'];
|
|
68
|
+
// ---------- CSV ----------
|
|
69
|
+
// Quote when the value would otherwise be ambiguous or awkward to read
|
|
70
|
+
// back: the separator, the quote character itself, and any whitespace —
|
|
71
|
+
// the last of which covers leading/trailing spaces a parser would
|
|
72
|
+
// otherwise be free to trim.
|
|
73
|
+
const NEEDS_QUOTING = /[",\s]/;
|
|
74
|
+
export function csvField(value) {
|
|
75
|
+
if (!NEEDS_QUOTING.test(value))
|
|
76
|
+
return value;
|
|
77
|
+
return `"${value.replace(/"/g, '""')}"`;
|
|
78
|
+
}
|
|
79
|
+
// Fold a value onto one physical line: `\n`, `\r` as their two-
|
|
80
|
+
// character escapes.
|
|
81
|
+
//
|
|
82
|
+
// CSV would allow the newline inside quotes, and a spreadsheet would
|
|
83
|
+
// read it back — but this file is called `.log`, and one record per
|
|
84
|
+
// line is what makes it work with the tools a log is actually reached
|
|
85
|
+
// for. A stack trace spanning eight physical lines means `grep` finds
|
|
86
|
+
// a fragment with no timestamp on it, `wc -l` counts frames instead of
|
|
87
|
+
// events, and `tail -n` cuts an entry in half.
|
|
88
|
+
//
|
|
89
|
+
// Backslash is escaped first, and only for that reason: log messages
|
|
90
|
+
// carry Windows paths and regex sources, so without it a literal `\n`
|
|
91
|
+
// in the text and a real newline would decode to the same thing, and
|
|
92
|
+
// the escape wouldn't be reversible.
|
|
93
|
+
export function foldToOneLine(value) {
|
|
94
|
+
return value
|
|
95
|
+
.replace(/\\/g, '\\\\')
|
|
96
|
+
.replace(/\n/g, '\\n')
|
|
97
|
+
.replace(/\r/g, '\\r');
|
|
98
|
+
}
|
|
99
|
+
// One record, one line — by construction, not by the caller
|
|
100
|
+
// remembering to flatten first.
|
|
101
|
+
export function csvRow(fields) {
|
|
102
|
+
return fields.map((f) => csvField(foldToOneLine(f))).join(',');
|
|
103
|
+
}
|
|
104
|
+
// The whole log as CSV text, header row first, one entry per line.
|
|
105
|
+
//
|
|
106
|
+
// Rows are newest-first, matching the pane's table rather than the
|
|
107
|
+
// append order in storage: this is an export of what the reader is
|
|
108
|
+
// looking at, and a log whose top line is the event they just saw is
|
|
109
|
+
// the point of the ordering in both places.
|
|
110
|
+
//
|
|
111
|
+
// `renderMessage` is how the caller supplies its own Message column;
|
|
112
|
+
// see the note at the top of the file.
|
|
113
|
+
export function formatLogCsv(entries, renderMessage = formatLogMessage) {
|
|
114
|
+
const lines = [csvRow(LOG_COLUMNS)];
|
|
115
|
+
for (let i = entries.length - 1; i >= 0; i--) {
|
|
116
|
+
const e = entries[i];
|
|
117
|
+
lines.push(csvRow([
|
|
118
|
+
formatLogTimestamp(e.timestamp),
|
|
119
|
+
formatLogStatus(e),
|
|
120
|
+
formatLogSource(e),
|
|
121
|
+
renderMessage(e),
|
|
122
|
+
]));
|
|
123
|
+
}
|
|
124
|
+
return `${lines.join('\n')}\n`;
|
|
125
|
+
}
|
|
126
|
+
// Suggested filename for a saved log: the script's log ID with its
|
|
127
|
+
// `.user.js` / `.js` extension swapped for `.log`, keeping its `#E4`
|
|
128
|
+
// (or `#N`) suffix so two scripts sharing a basename don't suggest the
|
|
129
|
+
// same file. `bold-on-hover.user.js#E4` → `bold-on-hover#E4.log`.
|
|
130
|
+
//
|
|
131
|
+
// `.log` rather than `.csv` even though the contents are CSV: this is
|
|
132
|
+
// a log first, and a double-click should open a text editor rather
|
|
133
|
+
// than a spreadsheet.
|
|
134
|
+
export function logFileName(logId) {
|
|
135
|
+
// Suffix first, extension second — `logIdSuffix` explains
|
|
136
|
+
// why the `#N` match is anchored, and `script-source-url.ts` explains
|
|
137
|
+
// why the order matters (`Chapter #2.user.js`).
|
|
138
|
+
const trimmed = sanitizeFilename(logId).trim();
|
|
139
|
+
const dupe = logIdSuffix(trimmed);
|
|
140
|
+
// One alternation rather than two `.replace`s: chaining them strips
|
|
141
|
+
// twice for a name like `bar.js.user.js`, eating a `.js` that is part
|
|
142
|
+
// of the name.
|
|
143
|
+
const base = (dupe ? trimmed.slice(0, -dupe.length) : trimmed)
|
|
144
|
+
.replace(/\.(user\.)?js$/i, '');
|
|
145
|
+
// Fallback for a script we have no label for at all — the viewer
|
|
146
|
+
// asks for a filename whether or not the log ID map has landed.
|
|
147
|
+
return `${base || 'script'}${dupe}.log`;
|
|
148
|
+
}
|
|
149
|
+
// A log ID is a decoded on-disk filename, which is attacker-controlled
|
|
150
|
+
// for an untrusted script bundle. Path separators are already gone
|
|
151
|
+
// (`scriptBasename` takes the leaf twice), so what is left is control
|
|
152
|
+
// characters, which have no business in a filename the browser is
|
|
153
|
+
// about to write. Same substitution as `sanitizeLabel` in
|
|
154
|
+
// `script-source-url.ts`, for the same reason.
|
|
155
|
+
function sanitizeFilename(name) {
|
|
156
|
+
// eslint-disable-next-line no-control-regex
|
|
157
|
+
return name.replace(/[\u0000-\u001f\u007f\u2028\u2029]/g, '_');
|
|
158
|
+
}
|