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,39 @@
|
|
|
1
|
+
import type { Page } from '@playwright/test';
|
|
2
|
+
import { type PreparedScript } from './prepare.js';
|
|
3
|
+
/** How a userscript is named in a saved file's header: its @name and @version. */
|
|
4
|
+
export declare function userscriptLabel(script: PreparedScript): string;
|
|
5
|
+
/**
|
|
6
|
+
* The page's current DOM as a standalone HTML document.
|
|
7
|
+
*
|
|
8
|
+
* A `<base>` keeps relative URLs pointing at the site. The page's own
|
|
9
|
+
* JavaScript is disabled (`type="text/plain"`, which keeps it readable
|
|
10
|
+
* while the browser skips it): served again it would re-fetch,
|
|
11
|
+
* redirect to a login, or rebuild the DOM the file was saved to
|
|
12
|
+
* preserve. What a clone cannot carry (typed form values, canvas
|
|
13
|
+
* pixels, shadow roots) is not in the file either.
|
|
14
|
+
*
|
|
15
|
+
* The header records the page's URL, for `--from-saved` to serve it
|
|
16
|
+
* there, and each of `userscripts` (see `userscriptLabel`), the ones
|
|
17
|
+
* running in the page, whose changes are in the file.
|
|
18
|
+
*/
|
|
19
|
+
export declare function pageHtml(page: Page, userscripts?: string[]): Promise<string>;
|
|
20
|
+
/**
|
|
21
|
+
* The header comment lines for a page saved from `url` with
|
|
22
|
+
* `userscripts` running. Only `-->` (or `--!>`) ends a comment, and a
|
|
23
|
+
* URL the browser reports has any `>` percent-encoded, so the URL goes
|
|
24
|
+
* in as it is (`xn--` hosts included). A name gets its `>` escaped
|
|
25
|
+
* after `--`, which no real name has.
|
|
26
|
+
*/
|
|
27
|
+
export declare function savedHeaderLines(url: string, userscripts: string[]): string[];
|
|
28
|
+
/** `pageHtml`, written to `file`. Returns the HTML. */
|
|
29
|
+
export declare function savePageHtml(page: Page, file: string, userscripts?: string[]): Promise<string>;
|
|
30
|
+
/** What a saved file's header says: its URL, if any, and the userscripts that were active. */
|
|
31
|
+
export interface SavedPageHeader {
|
|
32
|
+
url: string | null;
|
|
33
|
+
userscripts: string[];
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Reads a saved file's header. Only what comes before `<html` is
|
|
37
|
+
* looked at, so a comment in the page's own content never counts.
|
|
38
|
+
*/
|
|
39
|
+
export declare function readSavedHeader(html: string): SavedPageHeader;
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
// Saving a page's DOM as a file, with a header saying where it came
|
|
2
|
+
// from; and reading that header back (see saved-pages.ts for serving
|
|
3
|
+
// the file).
|
|
4
|
+
//
|
|
5
|
+
// Shared by the `savehtml` run command (page-commands.ts) and the
|
|
6
|
+
// fixture's `saveHtml`, so a developer recording a page by hand and a
|
|
7
|
+
// test recording one produce the same file.
|
|
8
|
+
import { writeFile } from 'node:fs/promises';
|
|
9
|
+
import { preparedScriptName } from './prepare.js';
|
|
10
|
+
// The header lines a saved file starts with, after its doctype. The
|
|
11
|
+
// URL line is the "saved from" comment Internet Explorer's Mark of the
|
|
12
|
+
// Web introduced, and Chrome's Save Page As still writes, so a page
|
|
13
|
+
// saved from the browser's menu is read the same way. The number is
|
|
14
|
+
// the URL's length, which nothing here relies on.
|
|
15
|
+
const SAVED_FROM = /<!--\s*saved from url=\(\d+\)(\S+?)\s*-->/i;
|
|
16
|
+
const USERSCRIPT_PREFIX = 'SourceMonkey: userscript active: ';
|
|
17
|
+
const USERSCRIPT_LINE = new RegExp(`<!-- ${USERSCRIPT_PREFIX}(.*?) -->`, 'g');
|
|
18
|
+
/** How a userscript is named in a saved file's header: its @name and @version. */
|
|
19
|
+
export function userscriptLabel(script) {
|
|
20
|
+
const version = (script.script.header?.tags ?? []).find((t) => t.tag === 'version')?.value;
|
|
21
|
+
return version ? `${preparedScriptName(script)} ${version}` : preparedScriptName(script);
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* The page's current DOM as a standalone HTML document.
|
|
25
|
+
*
|
|
26
|
+
* A `<base>` keeps relative URLs pointing at the site. The page's own
|
|
27
|
+
* JavaScript is disabled (`type="text/plain"`, which keeps it readable
|
|
28
|
+
* while the browser skips it): served again it would re-fetch,
|
|
29
|
+
* redirect to a login, or rebuild the DOM the file was saved to
|
|
30
|
+
* preserve. What a clone cannot carry (typed form values, canvas
|
|
31
|
+
* pixels, shadow roots) is not in the file either.
|
|
32
|
+
*
|
|
33
|
+
* The header records the page's URL, for `--from-saved` to serve it
|
|
34
|
+
* there, and each of `userscripts` (see `userscriptLabel`), the ones
|
|
35
|
+
* running in the page, whose changes are in the file.
|
|
36
|
+
*/
|
|
37
|
+
export async function pageHtml(page, userscripts = []) {
|
|
38
|
+
const url = page.url();
|
|
39
|
+
const { doctype, body } = await page.evaluate((base) => {
|
|
40
|
+
const doc = document.documentElement.cloneNode(true);
|
|
41
|
+
const head = doc.querySelector('head');
|
|
42
|
+
if (head && !head.querySelector('base')) {
|
|
43
|
+
const el = document.createElement('base');
|
|
44
|
+
el.href = base;
|
|
45
|
+
head.prepend(el);
|
|
46
|
+
}
|
|
47
|
+
for (const script of doc.querySelectorAll('script'))
|
|
48
|
+
script.setAttribute('type', 'text/plain');
|
|
49
|
+
const dt = document.doctype;
|
|
50
|
+
return {
|
|
51
|
+
doctype: dt
|
|
52
|
+
? `<!DOCTYPE ${dt.name}${dt.publicId ? ` PUBLIC "${dt.publicId}"` : ''}${dt.systemId ? ` "${dt.systemId}"` : ''}>`
|
|
53
|
+
: '',
|
|
54
|
+
body: doc.outerHTML,
|
|
55
|
+
};
|
|
56
|
+
}, url);
|
|
57
|
+
return [...(doctype ? [doctype] : []), ...savedHeaderLines(url, userscripts), body].join('\n');
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* The header comment lines for a page saved from `url` with
|
|
61
|
+
* `userscripts` running. Only `-->` (or `--!>`) ends a comment, and a
|
|
62
|
+
* URL the browser reports has any `>` percent-encoded, so the URL goes
|
|
63
|
+
* in as it is (`xn--` hosts included). A name gets its `>` escaped
|
|
64
|
+
* after `--`, which no real name has.
|
|
65
|
+
*/
|
|
66
|
+
export function savedHeaderLines(url, userscripts) {
|
|
67
|
+
return [
|
|
68
|
+
`<!-- saved from url=(${String(url.length).padStart(4, '0')})${url} -->`,
|
|
69
|
+
...userscripts.map((name) => `<!-- ${USERSCRIPT_PREFIX}${name.replace(/--(!?)>/g, '--$1>')} -->`),
|
|
70
|
+
];
|
|
71
|
+
}
|
|
72
|
+
/** `pageHtml`, written to `file`. Returns the HTML. */
|
|
73
|
+
export async function savePageHtml(page, file, userscripts = []) {
|
|
74
|
+
const html = await pageHtml(page, userscripts);
|
|
75
|
+
await writeFile(file, html);
|
|
76
|
+
return html;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Reads a saved file's header. Only what comes before `<html` is
|
|
80
|
+
* looked at, so a comment in the page's own content never counts.
|
|
81
|
+
*/
|
|
82
|
+
export function readSavedHeader(html) {
|
|
83
|
+
const end = html.search(/<html[\s>]/i);
|
|
84
|
+
const head = end >= 0 ? html.slice(0, end) : html;
|
|
85
|
+
return {
|
|
86
|
+
url: SAVED_FROM.exec(head)?.[1] ?? null,
|
|
87
|
+
userscripts: [...head.matchAll(USERSCRIPT_LINE)].map((m) => m[1]),
|
|
88
|
+
};
|
|
89
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import type { Page, Request } from '@playwright/test';
|
|
2
|
+
import { type SavedPageHeader } from './save-html.js';
|
|
3
|
+
/**
|
|
4
|
+
* The browser reports `https://example.test` as `https://example.test/`
|
|
5
|
+
* and never reports a fragment, so both sides are normalized the same
|
|
6
|
+
* way before comparing.
|
|
7
|
+
*/
|
|
8
|
+
export declare function comparableUrl(raw: string): string;
|
|
9
|
+
/** What `routeSavedPage` serves, as it stands when a request is made. */
|
|
10
|
+
export interface SavedPageServe {
|
|
11
|
+
/** The URL the page answers, matched exactly, query string included, fragment ignored. */
|
|
12
|
+
url: string;
|
|
13
|
+
/** The page's HTML. Read per request, so an edited file is served on the next load. */
|
|
14
|
+
html: () => Promise<string>;
|
|
15
|
+
/** Other requests to abort, rather than send on. */
|
|
16
|
+
block?: (request: Request) => boolean;
|
|
17
|
+
/** How the file is named when it can't be read. */
|
|
18
|
+
name: string;
|
|
19
|
+
/** Told why, when the file can't be read; the page shows it too. */
|
|
20
|
+
onError?: (message: string) => void;
|
|
21
|
+
/** The files saved beside the page, served from disk (see `companionFiles`). */
|
|
22
|
+
companion?: CompanionFiles;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* The folder a browser's "Webpage, Complete" save puts beside the page
|
|
26
|
+
* (Chrome and Firefox both name it `<name>_files`), and the URL its
|
|
27
|
+
* files are asked for at: the page links them relatively
|
|
28
|
+
* (`./<name>_files/app.css`), so they resolve against the page's URL.
|
|
29
|
+
*/
|
|
30
|
+
export interface CompanionFiles {
|
|
31
|
+
/** The folder's URL path, decoded, on `origin`. */
|
|
32
|
+
origin: string;
|
|
33
|
+
path: string;
|
|
34
|
+
dir: string;
|
|
35
|
+
}
|
|
36
|
+
/** The companion folder for a saved `file` served at `url`, whether or not it exists. */
|
|
37
|
+
export declare function companionFiles(file: string, url: string): CompanionFiles | undefined;
|
|
38
|
+
/**
|
|
39
|
+
* Serves `current()` on `page`: its URL gets its HTML, and every other
|
|
40
|
+
* request falls through to the page's other routes and the network,
|
|
41
|
+
* unless `block` says to abort it. Undefined serves nothing, so a
|
|
42
|
+
* caller can change or end what is served without routing again.
|
|
43
|
+
*/
|
|
44
|
+
export declare function routeSavedPage(page: Page, current: () => SavedPageServe | undefined): Promise<void>;
|
|
45
|
+
/**
|
|
46
|
+
* For `--offline`: blocks the requests a page makes while it shows the
|
|
47
|
+
* saved page at `url`. A load of a new page is never blocked, so
|
|
48
|
+
* following a link off it still works.
|
|
49
|
+
*/
|
|
50
|
+
export declare function offlineBlock(page: Page, url: string): (request: Request) => boolean;
|
|
51
|
+
/**
|
|
52
|
+
* A saved page as sm-dev serves it in a tab: `file` at `url`, `offline`
|
|
53
|
+
* blocking the rest. `given`: the file as it was named, for messages.
|
|
54
|
+
*/
|
|
55
|
+
export interface SavedPageFile {
|
|
56
|
+
file: string;
|
|
57
|
+
given: string;
|
|
58
|
+
url: string;
|
|
59
|
+
offline: boolean;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* What `routeSavedPage` serves for `saved` in `page`, for `--from-saved`:
|
|
63
|
+
* the file read fresh on each load, and a read failure told to `out` as
|
|
64
|
+
* well as shown.
|
|
65
|
+
*/
|
|
66
|
+
export declare function savedPageServe(page: Page, saved: SavedPageFile, out: (line: string) => void): SavedPageServe;
|
|
67
|
+
/** How `start`, `open` and `newtab` say a tab is loading a saved page. */
|
|
68
|
+
export declare function openingSavedLine(given: string, url: string, tab: string): string;
|
|
69
|
+
/** `--offline` without `--from-saved`. */
|
|
70
|
+
export declare const OFFLINE_NEEDS_SAVED = "The --offline flag goes with --from-saved";
|
|
71
|
+
/** `--url` without `--from-saved` and exactly one saved page. */
|
|
72
|
+
export declare const URL_NEEDS_ONE_SAVED = "The --url flag gives the URL for one saved page, so it goes with --from-saved and a single .html file";
|
|
73
|
+
/** `--from-saved` with no page to open from a file. */
|
|
74
|
+
export declare const NO_SAVED_FILE = "The --from-saved flag opens .html files, and none was given";
|
|
75
|
+
/** How sm-dev's commands say to give a URL for a saved page. */
|
|
76
|
+
export declare const GIVE_SAVED_URL = "Open it by itself, with --url.";
|
|
77
|
+
/**
|
|
78
|
+
* The URL to serve a saved page at: `url`, else the one its header
|
|
79
|
+
* records. Or why there is none: no URL anywhere, or one a route
|
|
80
|
+
* can't answer (`about:blank`, `file://`). `remedy` is a sentence
|
|
81
|
+
* saying how to give one where it was named.
|
|
82
|
+
*/
|
|
83
|
+
export declare function savedPageUrl(given: string, header: SavedPageHeader, url: string | undefined, remedy: string): {
|
|
84
|
+
url: string;
|
|
85
|
+
} | {
|
|
86
|
+
problem: string;
|
|
87
|
+
};
|
|
88
|
+
/** A saved page to open (`--from-saved`), with the userscripts its header lists. */
|
|
89
|
+
export type SavedPageStart = SavedPageFile & {
|
|
90
|
+
userscripts: string[];
|
|
91
|
+
};
|
|
92
|
+
/** One page a command opens: a URL, or with `saved`, a saved page served at it. */
|
|
93
|
+
export interface PageToOpen {
|
|
94
|
+
url: string;
|
|
95
|
+
saved?: SavedPageStart;
|
|
96
|
+
}
|
|
97
|
+
/** A page a command was given: the word as typed, and the URL it stands for. */
|
|
98
|
+
export interface PageWord {
|
|
99
|
+
word: string;
|
|
100
|
+
url: string;
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* With `--from-saved` (`fromSaved`): each word naming a local `.html`
|
|
104
|
+
* file is a saved page, at the URL it records or `--url`; the others
|
|
105
|
+
* stay URLs. Without, `--url` and `--offline` are refused. Throws a
|
|
106
|
+
* `UsageError` naming the file for any saved page that can't be opened,
|
|
107
|
+
* before anything opens.
|
|
108
|
+
*/
|
|
109
|
+
export declare function pagesToOpen(words: PageWord[], opts: {
|
|
110
|
+
fromSaved: boolean;
|
|
111
|
+
url?: string;
|
|
112
|
+
offline: boolean;
|
|
113
|
+
}, cwd?: string): Promise<PageToOpen[]>;
|
|
114
|
+
/** Reads a saved file for serving, with its header. */
|
|
115
|
+
export declare function loadSavedPage(file: string): Promise<{
|
|
116
|
+
html: string;
|
|
117
|
+
header: SavedPageHeader;
|
|
118
|
+
}>;
|
|
119
|
+
/** Why a saved page named with `--from-saved` could not be read. */
|
|
120
|
+
export declare function savedReadMessage(given: string, err: NodeJS.ErrnoException): string;
|
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
// Serving a saved page (from save-html.ts, or a browser's own Save Page
|
|
2
|
+
// As) back at the URL it was saved from, so scripts match and run on it
|
|
3
|
+
// as on the live site.
|
|
4
|
+
//
|
|
5
|
+
// Shared by `--from-saved` on `start`, `open` and `newtab`
|
|
6
|
+
// (console-commands.ts, run.ts, cli.ts) and the fixture's `serveHtml`,
|
|
7
|
+
// so both serve one the same way.
|
|
8
|
+
import { readFile } from 'node:fs/promises';
|
|
9
|
+
import path from 'node:path';
|
|
10
|
+
import { fileURLToPath } from 'node:url';
|
|
11
|
+
import { UsageError } from './checks.js';
|
|
12
|
+
import { isDirectoryMessage, noSuchFileMessage } from './prepare.js';
|
|
13
|
+
import { readSavedHeader } from './save-html.js';
|
|
14
|
+
/**
|
|
15
|
+
* The browser reports `https://example.test` as `https://example.test/`
|
|
16
|
+
* and never reports a fragment, so both sides are normalized the same
|
|
17
|
+
* way before comparing.
|
|
18
|
+
*/
|
|
19
|
+
export function comparableUrl(raw) {
|
|
20
|
+
try {
|
|
21
|
+
const u = new URL(raw);
|
|
22
|
+
u.hash = '';
|
|
23
|
+
return u.href;
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
return raw;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
/** The companion folder for a saved `file` served at `url`, whether or not it exists. */
|
|
30
|
+
export function companionFiles(file, url) {
|
|
31
|
+
const stem = path.basename(file).replace(/\.x?html?$/i, '');
|
|
32
|
+
let at;
|
|
33
|
+
try {
|
|
34
|
+
// Built by the URL parser, so it's encoded as the browser encodes
|
|
35
|
+
// the page's own relative links.
|
|
36
|
+
at = new URL(`./${stem}_files/`, url);
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
return undefined;
|
|
40
|
+
}
|
|
41
|
+
// Resolved, so the check that a request stays inside it compares like with like.
|
|
42
|
+
return { origin: at.origin, path: decodedPath(at) ?? at.pathname, dir: path.resolve(path.dirname(file), `${stem}_files`) };
|
|
43
|
+
}
|
|
44
|
+
// Where on disk a request for a companion file is, or null when it
|
|
45
|
+
// isn't one. Never outside the folder, whatever `..` the URL has.
|
|
46
|
+
function companionPath(companion, raw) {
|
|
47
|
+
let url;
|
|
48
|
+
try {
|
|
49
|
+
url = new URL(raw);
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
const wanted = decodedPath(url);
|
|
55
|
+
if (url.origin !== companion.origin || !wanted?.startsWith(companion.path))
|
|
56
|
+
return null;
|
|
57
|
+
const file = path.resolve(companion.dir, wanted.slice(companion.path.length));
|
|
58
|
+
return file.startsWith(companion.dir + path.sep) ? file : null;
|
|
59
|
+
}
|
|
60
|
+
function decodedPath(url) {
|
|
61
|
+
try {
|
|
62
|
+
return decodeURIComponent(url.pathname);
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
// By extension: a stylesheet or module script served with the wrong
|
|
69
|
+
// type is refused. Anything else is left for the browser to sniff.
|
|
70
|
+
const CONTENT_TYPES = {
|
|
71
|
+
'.css': 'text/css', '.js': 'text/javascript', '.mjs': 'text/javascript', '.json': 'application/json',
|
|
72
|
+
'.html': 'text/html', '.htm': 'text/html', '.svg': 'image/svg+xml', '.png': 'image/png',
|
|
73
|
+
'.jpg': 'image/jpeg', '.jpeg': 'image/jpeg', '.gif': 'image/gif', '.webp': 'image/webp', '.avif': 'image/avif',
|
|
74
|
+
'.ico': 'image/x-icon', '.woff': 'font/woff', '.woff2': 'font/woff2', '.ttf': 'font/ttf', '.otf': 'font/otf',
|
|
75
|
+
};
|
|
76
|
+
/**
|
|
77
|
+
* Serves `current()` on `page`: its URL gets its HTML, and every other
|
|
78
|
+
* request falls through to the page's other routes and the network,
|
|
79
|
+
* unless `block` says to abort it. Undefined serves nothing, so a
|
|
80
|
+
* caller can change or end what is served without routing again.
|
|
81
|
+
*/
|
|
82
|
+
export async function routeSavedPage(page, current) {
|
|
83
|
+
await bypassServiceWorker(page);
|
|
84
|
+
await page.route('**/*', async (route) => {
|
|
85
|
+
const saved = current();
|
|
86
|
+
const request = route.request();
|
|
87
|
+
if (saved && comparableUrl(request.url()) === comparableUrl(saved.url)) {
|
|
88
|
+
let body;
|
|
89
|
+
try {
|
|
90
|
+
body = await saved.html();
|
|
91
|
+
}
|
|
92
|
+
catch (err) {
|
|
93
|
+
// Shown as the page too, where the person is looking.
|
|
94
|
+
const message = savedReadMessage(saved.name, err);
|
|
95
|
+
saved.onError?.(message);
|
|
96
|
+
await route.fulfill({ status: 500, contentType: 'text/plain; charset=utf-8', body: message });
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
await route.fulfill({ status: 200, contentType: 'text/html; charset=utf-8', body });
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
// A file saved beside the page, while it shows, even with
|
|
103
|
+
// `--offline`. One that isn't there is a 404, not the live site's:
|
|
104
|
+
// the site never had it at that URL.
|
|
105
|
+
const showing = saved !== undefined && comparableUrl(page.url()) === comparableUrl(saved.url);
|
|
106
|
+
const local = showing && saved.companion ? companionPath(saved.companion, request.url()) : null;
|
|
107
|
+
if (local) {
|
|
108
|
+
const body = await readFile(local).catch(() => null);
|
|
109
|
+
const type = CONTENT_TYPES[path.extname(local).toLowerCase()];
|
|
110
|
+
await route.fulfill(body ? { status: 200, body, ...(type ? { contentType: type } : {}) } : { status: 404, body: '' });
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
if (saved?.block?.(request)) {
|
|
114
|
+
await route.abort();
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
// `fallback`, not `continue`: Playwright runs route handlers newest
|
|
118
|
+
// first, and only `fallback` hands a request to the next one. That
|
|
119
|
+
// lets a second saved page, or a HAR, sit beside this route.
|
|
120
|
+
await route.fallback();
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* For `--offline`: blocks the requests a page makes while it shows the
|
|
125
|
+
* saved page at `url`. A load of a new page is never blocked, so
|
|
126
|
+
* following a link off it still works.
|
|
127
|
+
*/
|
|
128
|
+
export function offlineBlock(page, url) {
|
|
129
|
+
return (request) => {
|
|
130
|
+
if (comparableUrl(page.url()) !== comparableUrl(url))
|
|
131
|
+
return false;
|
|
132
|
+
try {
|
|
133
|
+
return !(request.isNavigationRequest() && request.frame() === page.mainFrame());
|
|
134
|
+
}
|
|
135
|
+
catch {
|
|
136
|
+
// A service worker's request has no frame.
|
|
137
|
+
return true;
|
|
138
|
+
}
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* What `routeSavedPage` serves for `saved` in `page`, for `--from-saved`:
|
|
143
|
+
* the file read fresh on each load, and a read failure told to `out` as
|
|
144
|
+
* well as shown.
|
|
145
|
+
*/
|
|
146
|
+
export function savedPageServe(page, saved, out) {
|
|
147
|
+
return {
|
|
148
|
+
url: saved.url,
|
|
149
|
+
html: () => readFile(saved.file, 'utf8'),
|
|
150
|
+
block: saved.offline ? offlineBlock(page, saved.url) : undefined,
|
|
151
|
+
name: saved.given,
|
|
152
|
+
onError: (message) => out(`[sm-dev] ${message}`),
|
|
153
|
+
companion: companionFiles(saved.file, saved.url),
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
/** How `start`, `open` and `newtab` say a tab is loading a saved page. */
|
|
157
|
+
export function openingSavedLine(given, url, tab) {
|
|
158
|
+
return `[sm-dev] Opening ${given} as ${url} in ${tab}`;
|
|
159
|
+
}
|
|
160
|
+
/** `--offline` without `--from-saved`. */
|
|
161
|
+
export const OFFLINE_NEEDS_SAVED = 'The --offline flag goes with --from-saved';
|
|
162
|
+
/** `--url` without `--from-saved` and exactly one saved page. */
|
|
163
|
+
export const URL_NEEDS_ONE_SAVED = 'The --url flag gives the URL for one saved page, so it goes with --from-saved and a single .html file';
|
|
164
|
+
/** `--from-saved` with no page to open from a file. */
|
|
165
|
+
export const NO_SAVED_FILE = 'The --from-saved flag opens .html files, and none was given';
|
|
166
|
+
/** How sm-dev's commands say to give a URL for a saved page. */
|
|
167
|
+
export const GIVE_SAVED_URL = 'Open it by itself, with --url.';
|
|
168
|
+
/**
|
|
169
|
+
* The URL to serve a saved page at: `url`, else the one its header
|
|
170
|
+
* records. Or why there is none: no URL anywhere, or one a route
|
|
171
|
+
* can't answer (`about:blank`, `file://`). `remedy` is a sentence
|
|
172
|
+
* saying how to give one where it was named.
|
|
173
|
+
*/
|
|
174
|
+
export function savedPageUrl(given, header, url, remedy) {
|
|
175
|
+
const at = url ?? header.url;
|
|
176
|
+
if (!at)
|
|
177
|
+
return { problem: `${given} does not say what URL it was saved from. ${remedy}` };
|
|
178
|
+
if (!/^https?:/i.test(at))
|
|
179
|
+
return { problem: `${given} can only be served at an http or https URL, not ${at}. ${remedy}` };
|
|
180
|
+
return { url: at };
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* With `--from-saved` (`fromSaved`): each word naming a local `.html`
|
|
184
|
+
* file is a saved page, at the URL it records or `--url`; the others
|
|
185
|
+
* stay URLs. Without, `--url` and `--offline` are refused. Throws a
|
|
186
|
+
* `UsageError` naming the file for any saved page that can't be opened,
|
|
187
|
+
* before anything opens.
|
|
188
|
+
*/
|
|
189
|
+
export async function pagesToOpen(words, opts, cwd = process.cwd()) {
|
|
190
|
+
if (!opts.fromSaved) {
|
|
191
|
+
if (opts.url !== undefined)
|
|
192
|
+
throw new UsageError(URL_NEEDS_ONE_SAVED);
|
|
193
|
+
if (opts.offline)
|
|
194
|
+
throw new UsageError(OFFLINE_NEEDS_SAVED);
|
|
195
|
+
return words.map(({ url }) => ({ url }));
|
|
196
|
+
}
|
|
197
|
+
const files = words.map((w) => savedPageFile(w, cwd));
|
|
198
|
+
const count = files.filter(Boolean).length;
|
|
199
|
+
if (count === 0)
|
|
200
|
+
throw new UsageError(NO_SAVED_FILE);
|
|
201
|
+
if (opts.url !== undefined && count > 1)
|
|
202
|
+
throw new UsageError(URL_NEEDS_ONE_SAVED);
|
|
203
|
+
return Promise.all(words.map(async ({ word, url }, i) => {
|
|
204
|
+
const file = files[i];
|
|
205
|
+
if (!file)
|
|
206
|
+
return { url };
|
|
207
|
+
let header;
|
|
208
|
+
try {
|
|
209
|
+
({ header } = await loadSavedPage(file));
|
|
210
|
+
}
|
|
211
|
+
catch (err) {
|
|
212
|
+
// A name that's no file is a mistake in the command; one that
|
|
213
|
+
// can't be read for another reason isn't.
|
|
214
|
+
const code = err.code;
|
|
215
|
+
const message = savedReadMessage(word, err);
|
|
216
|
+
throw code === 'ENOENT' || code === 'EISDIR' ? new UsageError(message) : new Error(message);
|
|
217
|
+
}
|
|
218
|
+
const found = savedPageUrl(word, header, opts.url, GIVE_SAVED_URL);
|
|
219
|
+
if ('problem' in found)
|
|
220
|
+
throw new UsageError(found.problem);
|
|
221
|
+
return { url: found.url, saved: { file, given: word, url: found.url, offline: opts.offline, userscripts: header.userscripts } };
|
|
222
|
+
}));
|
|
223
|
+
}
|
|
224
|
+
// The file a page names, when it's a local `.html` file: what
|
|
225
|
+
// `--from-saved` serves. A word ending `.html` with no `://` is one
|
|
226
|
+
// even where it would read as a host (`pgae.html`), so a typo is
|
|
227
|
+
// said to be no file rather than opened live; `localhost:8080/x.html`
|
|
228
|
+
// keeps its port, and so its host.
|
|
229
|
+
function savedPageFile({ word, url }, cwd) {
|
|
230
|
+
if (url.startsWith('file:'))
|
|
231
|
+
return /\.x?html?$/i.test(url) ? fileURLToPath(url) : null;
|
|
232
|
+
const first = word.split('/')[0];
|
|
233
|
+
if (word.includes('://') || /:\d+$/.test(first) || !/\.x?html?$/i.test(word))
|
|
234
|
+
return null;
|
|
235
|
+
return path.resolve(cwd, word);
|
|
236
|
+
}
|
|
237
|
+
// A site's service worker answers its requests before Playwright's
|
|
238
|
+
// routes see them, so a site that has one would get its live page
|
|
239
|
+
// instead of the saved one. Bypassing it takes the network domain
|
|
240
|
+
// enabled on a session of our own, kept for the page's life. Best
|
|
241
|
+
// effort: a browser that refuses still serves pages without one.
|
|
242
|
+
const bypassed = new WeakMap();
|
|
243
|
+
function bypassServiceWorker(page) {
|
|
244
|
+
let session = bypassed.get(page);
|
|
245
|
+
if (!session) {
|
|
246
|
+
session = (async () => {
|
|
247
|
+
try {
|
|
248
|
+
const cdp = await page.context().newCDPSession(page);
|
|
249
|
+
await cdp.send('Network.enable');
|
|
250
|
+
await cdp.send('Network.setBypassServiceWorker', { bypass: true });
|
|
251
|
+
return cdp;
|
|
252
|
+
}
|
|
253
|
+
catch {
|
|
254
|
+
return null;
|
|
255
|
+
}
|
|
256
|
+
})();
|
|
257
|
+
bypassed.set(page, session);
|
|
258
|
+
}
|
|
259
|
+
return session;
|
|
260
|
+
}
|
|
261
|
+
/** Reads a saved file for serving, with its header. */
|
|
262
|
+
export async function loadSavedPage(file) {
|
|
263
|
+
const html = await readFile(file, 'utf8');
|
|
264
|
+
return { html, header: readSavedHeader(html) };
|
|
265
|
+
}
|
|
266
|
+
/** Why a saved page named with `--from-saved` could not be read. */
|
|
267
|
+
export function savedReadMessage(given, err) {
|
|
268
|
+
if (err.code === 'ENOENT')
|
|
269
|
+
return noSuchFileMessage(given);
|
|
270
|
+
if (err.code === 'EISDIR')
|
|
271
|
+
return isDirectoryMessage(given, 'Name a saved .html file.');
|
|
272
|
+
if (err.code === 'EACCES')
|
|
273
|
+
return `The saved page ${given} could not be read: Permission denied`;
|
|
274
|
+
return `The saved page ${given} could not be read: ${err.message}`;
|
|
275
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/** What the comment says. */
|
|
2
|
+
export interface ScriptMark {
|
|
3
|
+
/** A run's script: the run's id (`D1`). */
|
|
4
|
+
run?: string;
|
|
5
|
+
/** A run's script: when the run started, which tells it from an older run with the same id. */
|
|
6
|
+
started?: string;
|
|
7
|
+
/** The file it was installed from. */
|
|
8
|
+
file?: string;
|
|
9
|
+
}
|
|
10
|
+
/** `text` with `mark` as its last line, in place of any it had. */
|
|
11
|
+
export declare function markedText(text: string, mark: ScriptMark): string;
|
|
12
|
+
/** What the comment in `text` says, or null when it has none. */
|
|
13
|
+
export declare function readMark(text: string): ScriptMark | null;
|
|
14
|
+
/** `text` without the comment, as it was before it was added. */
|
|
15
|
+
export declare function withoutMark(text: string): string;
|
|
16
|
+
/** Whether two texts are the same script, the comment and trailing white space aside. */
|
|
17
|
+
export declare function sameScript(a: string, b: string): boolean;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
// The comment sm-dev adds as the last line of a script it installs in
|
|
2
|
+
// another manager (Violentmonkey, Tampermonkey), saying where
|
|
3
|
+
// it came from, and for a run's script, which run:
|
|
4
|
+
//
|
|
5
|
+
// // sm-dev: run=D1 started=2026-10-04T12:00:00.000Z file=/home/me/foo.user.js
|
|
6
|
+
//
|
|
7
|
+
// In the script's own text, since that's what every manager keeps as
|
|
8
|
+
// given, so no manager's internals are relied on. Last, so it moves no
|
|
9
|
+
// line numbers. See docs/sm-dev-violentmonkey.md.
|
|
10
|
+
const PREFIX = '// sm-dev: ';
|
|
11
|
+
/** `text` with `mark` as its last line, in place of any it had. */
|
|
12
|
+
export function markedText(text, mark) {
|
|
13
|
+
const body = withoutMark(text);
|
|
14
|
+
// The file last, since a path can hold spaces.
|
|
15
|
+
const fields = [
|
|
16
|
+
...(mark.run !== undefined ? [`run=${mark.run}`] : []),
|
|
17
|
+
...(mark.started !== undefined ? [`started=${mark.started}`] : []),
|
|
18
|
+
...(mark.file !== undefined ? [`file=${mark.file}`] : []),
|
|
19
|
+
];
|
|
20
|
+
if (fields.length === 0)
|
|
21
|
+
return body;
|
|
22
|
+
return `${body}${body.endsWith('\n') || body === '' ? '' : '\n'}${PREFIX}${fields.join(' ')}\n`;
|
|
23
|
+
}
|
|
24
|
+
// The last line that has anything on it, and where it starts.
|
|
25
|
+
function lastLine(text) {
|
|
26
|
+
const trimmed = text.replace(/\s+$/, '');
|
|
27
|
+
const start = trimmed.lastIndexOf('\n') + 1;
|
|
28
|
+
return { line: trimmed.slice(start), start };
|
|
29
|
+
}
|
|
30
|
+
/** What the comment in `text` says, or null when it has none. */
|
|
31
|
+
export function readMark(text) {
|
|
32
|
+
const { line } = lastLine(text);
|
|
33
|
+
if (!line.startsWith(PREFIX))
|
|
34
|
+
return null;
|
|
35
|
+
const mark = {};
|
|
36
|
+
const rest = line.slice(PREFIX.length);
|
|
37
|
+
const file = /(?:^| )file=(.*)$/.exec(rest);
|
|
38
|
+
if (file)
|
|
39
|
+
mark.file = file[1];
|
|
40
|
+
for (const [, key, value] of (file ? rest.slice(0, file.index) : rest).matchAll(/(\w+)=(\S*)/g)) {
|
|
41
|
+
if (key === 'run')
|
|
42
|
+
mark.run = value;
|
|
43
|
+
else if (key === 'started')
|
|
44
|
+
mark.started = value;
|
|
45
|
+
}
|
|
46
|
+
return mark;
|
|
47
|
+
}
|
|
48
|
+
/** `text` without the comment, as it was before it was added. */
|
|
49
|
+
export function withoutMark(text) {
|
|
50
|
+
const { line, start } = lastLine(text);
|
|
51
|
+
return line.startsWith(PREFIX) ? text.slice(0, start) : text;
|
|
52
|
+
}
|
|
53
|
+
/** Whether two texts are the same script, the comment and trailing white space aside. */
|
|
54
|
+
export function sameScript(a, b) {
|
|
55
|
+
return withoutMark(a).trimEnd() === withoutMark(b).trimEnd();
|
|
56
|
+
}
|