Add resources download button + click to download picker in replay (#469)

Fixes #467 
- Add a download button on the resource page for each entry to allow
downloading each resource no matter what type.
- Add a new 'click to download' image or media picker that allows
highlighting + clicking on image/media to download on the page (to get
around 'Save Image As...) not working. Also supports inline svgs +
background images - should work better than Save Image As...
- Location bar shows a popup indicating in 'click to download' mode,
which can be canceled. Otherwise, mode remains until user downloads
something.
- Type fixes: Untangle Page and URLResource types for better type
checking.
- Deps: bump to wabac.js 2.25.1 for new download mode support

---------
Co-authored-by: Tessa Walsh <tessa@bitarchivist.net>
This commit is contained in:
Ilya Kreymer
2025-12-05 09:21:17 -08:00
committed by GitHub
co-authored by Tessa Walsh
parent d2524ab26f
commit 916b154688
10 changed files with 497 additions and 68 deletions
+6
View File
@@ -2,6 +2,12 @@
## CHANGES
*From release 2.4.0+, changes will now be listed in the release notes on GitHub.*
*Please see [the list of releases](https://github.com/webrecorder/replayweb.page/releases) for an up-to-date
list of changes for each release.*
---
v2.3.23
- Fidelity: Fix possible regression from different JS parser in wabac.js, additional XHR improvements (viw abac.js 2.24.5)
+2 -2
View File
@@ -1,7 +1,7 @@
{
"name": "replaywebpage",
"productName": "ReplayWeb.page",
"version": "2.3.23",
"version": "2.4.0",
"description": "Serverless Web Archive Replay",
"repository": "https://github.com/webrecorder/replayweb.page",
"homepage": "https://replayweb.page/",
@@ -22,7 +22,7 @@
"dependencies": {
"@fortawesome/fontawesome-free": "^5.15.4",
"@shoelace-style/shoelace": "~2.15.1",
"@webrecorder/wabac": "^2.24.5",
"@webrecorder/wabac": "^2.25.1",
"bulma": "^0.9.3",
"electron-log": "^4.4.1",
"electron-updater": "^6.6.2",
+90 -3
View File
@@ -28,6 +28,7 @@ import fasTriangleExclamation from "@fortawesome/fontawesome-free/svgs/solid/exc
import fasBook from "@fortawesome/fontawesome-free/svgs/solid/book.svg";
import fasDownload from "@fortawesome/fontawesome-free/svgs/solid/download.svg";
import fasFileDownload from "@fortawesome/fontawesome-free/svgs/regular/arrow-alt-circle-down.svg";
import farListAlt from "@fortawesome/fontawesome-free/svgs/regular/list-alt.svg";
import farResources from "@fortawesome/fontawesome-free/svgs/solid/puzzle-piece.svg";
@@ -190,6 +191,9 @@ class Item extends LitElement {
@property({ type: Array })
multiTs?: string[] = [];
@property({ type: Boolean })
clickToDownloadMode = false;
private splitter: Split.Instance | null = null;
private _replaceLoc = false;
@@ -743,8 +747,31 @@ class Item extends LitElement {
line-height: 2;
}
#click-download-msg {
position: absolute;
right: 0.5rem;
z-index: 10;
background: #09c1ff;
top: 5px;
bottom: 5px;
margin-right: -3px;
border-radius: 8px;
display: flex;
align-items: center;
line-height: 2;
}
#hilite-sample {
width: 20px;
height: 20px;
background-color: rgba(0, 0, 255, 0.5);
border: solid 4px blue;
margin: 2px;
display: inline-flex;
}
/* Gradient to indicate URL clipping */
#datetime:before {
.loc-overlay:before {
content: "";
position: absolute;
top: 0;
@@ -988,6 +1015,8 @@ class Item extends LitElement {
id="replay"
@replay-loading="${this.onReplayLoading}"
@replay-favicons="${this.onFavIcons}"
@cancel-click-download="${() =>
(this.clickToDownloadMode = false)}"
>
</wr-coll-replay>
`
@@ -1244,7 +1273,11 @@ class Item extends LitElement {
.value="${this.url}"
placeholder="Enter text to search or a URL to replay"
/>
${isReplay ? this.renderTimestamp() : ""}
${isReplay
? this.clickToDownloadMode
? this.renderClickToDownloadNotify()
: this.renderTimestamp()
: ""}
${showFavIcon
? html` <span class="favicon icon is-small is-left">
<img src="${this.favIconUrl}" />
@@ -1258,6 +1291,23 @@ class Item extends LitElement {
<p id="skip-replay-target" tabindex="-1" class="is-sr-only">Skipped</p>`;
}
protected renderClickToDownloadNotify() {
return html`<article
id="click-download-msg"
class="loc-overlay has-background-link-light is-size-7"
>
<div class="ml-4 is-flex">
Select image or media highlighted with 🟦 (blue box) on hover to
download.
</div>
<button
class="mx-4 delete"
aria-label="delete"
@click="${this.cancelClickToDownload}"
></button>
</article>`;
}
protected renderToolbarRight() {
const isReplay = !!this.tabData.url;
@@ -1367,6 +1417,25 @@ class Item extends LitElement {
<span>Purge Cache + Full Reload</span>
</a>`
: html``}
${isReplay
? html`<hr class="dropdown-divider" />
<a
@click="${this.clickToDownload}"
role="button"
class="dropdown-item"
@keyup="${clickOnSpacebarPress}"
>
<span class="icon is-small">
<fa-icon
size="1.0em"
class="has-text-grey"
aria-hidden="true"
.svg="${fasFileDownload}"
></fa-icon>
</span>
<span>Select Media to Download</span>
</a>`
: html``}
${(!this.editable && this.downloadUrl?.startsWith("http://")) ||
this.downloadUrl?.startsWith("https://")
? html` <hr class="dropdown-divider" />
@@ -1452,7 +1521,7 @@ class Item extends LitElement {
const currDateStr = this.ts
? dateTimeFormatter.format(tsToDate(this.ts) as Date)
: "";
return html`<div id="datetime" class="control is-hidden-mobile">
return html`<div id="datetime" class="control is-hidden-mobile loc-overlay">
${timestampStrs.length > 1
? html`
<sl-dropdown placement="top-end" hoist>
@@ -1513,6 +1582,24 @@ class Item extends LitElement {
}
}
clickToDownload() {
const replay = this.renderRoot.querySelector<Replay>("wr-coll-replay");
if (replay) {
this.clickToDownloadMode = true;
replay.setClickToDownload();
}
return false;
}
cancelClickToDownload() {
const replay = this.renderRoot.querySelector<Replay>("wr-coll-replay");
if (replay) {
this.clickToDownloadMode = false;
replay.clearHilite(true);
}
return false;
}
renderItemInfo() {
if (!this.itemInfo)
return html`<sl-alert open variant="warning">
+6 -5
View File
@@ -8,7 +8,7 @@ import "keyword-mark-element/lib/keyword-mark.js";
import { getPageDateTS, getReplayLink } from "./pageutils";
import { wrapCss } from "./misc";
import type { URLResource } from "./types";
import type { Page, URLTsChange } from "./types";
import {
dateFormatter,
dateTimeFormatter,
@@ -24,7 +24,7 @@ class PageEntry extends LitElement {
textSnippet: string | null = "";
@property({ type: Object })
page: URLResource | null = null;
page: Page | null = null;
@property({ type: String })
replayPrefix = "";
@@ -241,7 +241,7 @@ class PageEntry extends LitElement {
href="${getReplayLink(
"pages",
this.page!.url,
this.page!.timestamp!,
this.page!.timestamp,
this.page!.waczhash,
)}"
>
@@ -294,7 +294,7 @@ class PageEntry extends LitElement {
private getReplayPrefix() {
const waczhash = this.page!.waczhash ? `:${this.page!.waczhash}/` : "";
const ts = this.page!.timestamp || "";
const ts = this.page!.ts || "";
return this.replayPrefix + "/" + waczhash + ts + "id_";
}
@@ -370,11 +370,12 @@ class PageEntry extends LitElement {
onReplay(event: Event, reload = false) {
event.preventDefault();
const data = {
const data: URLTsChange = {
url: this.page!.url,
ts: this.page!.timestamp,
waczhash: this.page!.waczhash,
};
this.sendChangeEvent(data, reload);
return false;
}
+21 -15
View File
@@ -18,17 +18,17 @@ import fasEdit from "@fortawesome/fontawesome-free/svgs/solid/edit.svg";
import type { Sorter } from "./sorter";
import type { PageEntry } from "./pageentry";
import type { Id, Index } from "flexsearch";
import type { ItemType, URLResource } from "./types";
import type { ItemType, Page } from "./types";
const DYNAMIC_PAGE_SIZE = 25;
// ===========================================================================
class Pages extends LitElement {
@property({ type: Array })
filteredPages: URLResource[] = [];
filteredPages: Page[] = [];
@property({ type: Array })
sortedPages: URLResource[] = [];
sortedPages: Page[] = [];
@property({ type: String })
query = "";
@@ -37,7 +37,7 @@ class Pages extends LitElement {
flex: Index | null = null;
@property({ attribute: false })
textPages: URLResource[] | null = null;
textPages: Page[] | null = null;
@property()
newQuery: string | null = null;
@@ -94,7 +94,7 @@ class Pages extends LitElement {
toDeletePages: Set<number> | number[] | null = null;
@property({ type: Object })
toDeletePage: URLResource | null = null;
toDeletePage: Page | null = null;
@property({ type: Object })
collInfo: ItemType | Record<string, never> | null = null;
@@ -181,8 +181,7 @@ class Pages extends LitElement {
this.sortKey = "date";
this.sortDesc = true;
}
const sorter =
this.renderRoot.querySelector<Sorter<URLResource>>("wr-sorter");
const sorter = this.renderRoot.querySelector<Sorter<Page>>("wr-sorter");
if (sorter) {
sorter.sortKey = this.sortKey;
sorter.sortDesc = this.sortDesc;
@@ -236,7 +235,9 @@ class Pages extends LitElement {
if (this.dynamicPagesQuery) {
if (!this.query) {
const seedPages = this.collInfo!.pages.filter((x) => x.isSeed);
const seedPages = this.collInfo!.pages.filter(
(x) => x.isSeed || x.seed,
);
this.filteredPages =
this.showAllPages || !seedPages.length
? [...this.collInfo!.pages]
@@ -256,9 +257,6 @@ class Pages extends LitElement {
// normalize the date
for (const page of this.filteredPages) {
const { timestamp, date } = getPageDateTS(page);
if (date == null) {
throw new Error("Page date is null");
}
page.timestamp = timestamp;
page.date = date;
}
@@ -292,7 +290,7 @@ class Pages extends LitElement {
const knownPages = new Set();
this.filteredPages.forEach((x) => knownPages.add(x.id));
const newPages = [];
const newPages: Page[] = [];
for (const {
id,
@@ -304,6 +302,7 @@ class Pages extends LitElement {
favIconUrl,
waczhash,
isSeed,
size,
} of json.pages) {
if (knownPages.has(id)) {
continue;
@@ -313,16 +312,20 @@ class Pages extends LitElement {
continue;
}
let tsActual;
let tsActual: number;
let date: Date;
if (typeof ts === "string") {
date = new Date(ts);
tsActual = new Date(ts).getTime();
} else {
tsActual = ts;
date = new Date(tsActual);
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const newPage: any = {
const timestamp = getTS(date.toISOString());
const newPage: Page = {
id,
url,
title,
@@ -331,6 +334,9 @@ class Pages extends LitElement {
ts: tsActual,
favIconUrl,
waczhash,
timestamp,
size,
date,
};
newPages.push(newPage);
+20 -6
View File
@@ -1,4 +1,4 @@
import type { URLResource } from "./types";
import type { Page } from "./types";
// ===========================================================================
async function digestMessage(message: string, hashtype: AlgorithmIdentifier) {
@@ -50,16 +50,17 @@ function getDateFromTS(ts: string | number) {
}
// ===========================================================================
function getPageDateTS(page: URLResource) {
function getPageDateTS(page: Page) {
let date: Date | null = null;
try {
date = new Date(page.ts || page.date);
if (page.ts > 0) {
date = new Date(page.ts);
}
} catch (e) {
// leave date unchanged in case of error
// ignore
}
const timestamp =
date && date instanceof Date ? getTS(date.toISOString()) : "";
const timestamp = date ? getTS(date.toISOString()) : "";
return { date, timestamp };
}
@@ -85,6 +86,18 @@ function getReplayLink(
return "#" + params.toString();
}
// ===========================================================================
function getDownloadLink(
replayPrefix: string,
url: string,
ts: string,
waczhash?: string,
) {
return `${replayPrefix}/${waczhash ? `:${waczhash}/` : ""}${
ts || ""
}dl_/${url}`;
}
// ===========================================================================
async function sourceToId(url: string) {
try {
@@ -137,6 +150,7 @@ export {
getPageDateTS,
getDateFromTS,
getReplayLink,
getDownloadLink,
sourceToId,
parseURLSchemeHostPath,
};
+282 -6
View File
@@ -1,9 +1,10 @@
import { LitElement, html, css, type PropertyValues } from "lit";
import { ifDefined } from "lit/directives/if-defined.js";
import { property } from "lit/decorators.js";
import { wrapCss } from "./misc";
import rwpLogo from "~assets/brand/replaywebpage-icon-color.svg";
import type { ItemType } from "./types";
import type { ItemType, URLTsChange } from "./types";
// ===========================================================================
class Replay extends LitElement {
@@ -49,10 +50,15 @@ class Replay extends LitElement {
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- requestPermission() type mismatch
authFileHandle: any = null;
@property({ type: String })
downloadResUrl = "";
private reauthWait: null | Promise<void> = null;
private _loadPoll: null | number = null;
private hiliter: HoverHiliter | null = null;
firstUpdated() {
window.addEventListener("message", (event) => this.onReplayMessage(event));
// TODO: Fix this the next time the file is edited.
@@ -143,9 +149,10 @@ class Replay extends LitElement {
(this.replayUrl && changedProperties.has("replayUrl")) ||
(this.replayTS && changedProperties.has("replayTS"))
) {
const data = {
const data: URLTsChange = {
url: this.replayUrl,
ts: this.replayTS,
waczhash: this.waczhash,
};
this.dispatchEvent(
@@ -203,7 +210,7 @@ class Replay extends LitElement {
this.replayUrl = event.data.url;
this.title = event.data.title || this.title;
this.replayNotFoundError = event.data.wb_type === "archive-not-found";
this.clearLoading(iframe.contentWindow);
this.clearLoading(iframe);
if (event.data.icons) {
const icons = event.data.icons;
@@ -265,13 +272,12 @@ class Replay extends LitElement {
(iframe.contentDocument.readyState === "complete" &&
!(iframe.contentWindow as Window & { _WBWombat: unknown })._WBWombat)
) {
this.clearLoading(iframe?.contentWindow);
this.clearLoading(iframe);
}
}, 5000);
}
// @ts-expect-error [// TODO: Fix this the next time the file is edited.] - TS7006 - Parameter 'iframeWin' implicitly has an 'any' type.
clearLoading(iframeWin) {
clearLoading(iframe: HTMLIFrameElement | null) {
this.dispatchEvent(
new CustomEvent("replay-loading", { detail: { loading: false } }),
);
@@ -281,6 +287,8 @@ class Replay extends LitElement {
this._loadPoll = null;
}
const iframeWin = iframe?.contentWindow;
if (iframeWin) {
try {
iframeWin.addEventListener("beforeunload", () => {
@@ -293,6 +301,7 @@ class Replay extends LitElement {
}
setLoading() {
this.clearHilite(true);
this.dispatchEvent(
new CustomEvent("replay-loading", { detail: { loading: true } }),
);
@@ -367,6 +376,15 @@ class Replay extends LitElement {
margin: 3em;
background-color: white;
}
.hilite-overlay {
display: none;
position: absolute;
z-index: 9999;
background-color: rgba(0, 0, 255, 0.5);
border: solid 10px blue;
cursor: crosshair;
}
`);
}
@@ -388,6 +406,16 @@ class Replay extends LitElement {
</div>
</div>`
: html`
<a
href="${this.downloadResUrl}"
@click="${this.hiliteClicked}"
class="hilite-overlay"
download="${ifDefined(
this.downloadResUrl.startsWith("blob:")
? "image.svg"
: undefined,
)}"
></a>
<div class="iframe-container">
<iframe
class="iframe-main"
@@ -444,6 +472,254 @@ class Replay extends LitElement {
</div>
`}`;
}
clearHilite(removeListeners = false) {
if (this.hiliter) {
this.hiliter.clearHilite(removeListeners);
this.hiliter = null;
}
if (removeListeners) {
this.removeEventListener(
"update-download-res-url",
this.onUpdateDownloadResUrl,
);
this.dispatchEvent(new CustomEvent("cancel-click-download"));
}
}
hiliteClicked() {
this.clearHilite(true);
return true;
}
onUpdateDownloadResUrl(e: Event) {
const { url } = (e as CustomEvent).detail;
this.downloadResUrl = url;
}
setClickToDownload() {
if (this.hiliter) {
return;
}
this.addEventListener(
"update-download-res-url",
this.onUpdateDownloadResUrl,
);
try {
this.hiliter = new HoverHiliter(this);
} catch (e) {
this.hiliter = null;
}
}
}
// ===========================================================================
class HoverHiliter {
doc: Document;
replay: Replay;
hiliteElem: HTMLElement | null = null;
hiliteOverlay: HTMLElement;
iframe: HTMLIFrameElement;
onMove: (event: MouseEvent) => void;
onRecompute: () => void;
constructor(replay: Replay) {
this.replay = replay;
const iframe = this.replay.renderRoot.querySelector("iframe");
const hiliteOverlay =
this.replay.renderRoot.querySelector(".hilite-overlay");
if (!hiliteOverlay || !iframe?.contentDocument) {
throw new Error("missing elements");
}
this.iframe = iframe;
this.hiliteOverlay = hiliteOverlay as HTMLElement;
this.onMove = (event: MouseEvent) => this.hiliteOnMove(event);
this.onRecompute = () => this.hiliteRecompute();
const doc = iframe.contentDocument;
this.doc = doc;
doc.addEventListener("mousemove", this.onMove);
doc.addEventListener("scroll", this.onRecompute);
doc.defaultView?.addEventListener("resize", this.onRecompute);
}
clearHilite(removeListeners = false) {
if (removeListeners) {
this.doc.removeEventListener("mousemove", this.onMove);
this.doc.removeEventListener("scroll", this.onRecompute);
this.doc.defaultView?.removeEventListener("scroll", this.onRecompute);
}
this.hiliteElem = null;
this.hiliteOverlay.style.display = "none";
}
hiliteRecompute() {
if (!this.hiliteElem) {
return;
}
const iframeRect = this.iframe.getBoundingClientRect();
const elemRect = this.hiliteElem.getBoundingClientRect();
const offset = 10;
const leftX = iframeRect.left + window.scrollX + elemRect.left - offset;
const topY = iframeRect.top + window.scrollY + elemRect.top - offset;
const hilite = this.hiliteOverlay;
hilite.style.left = leftX + "px";
hilite.style.top = topY + "px";
hilite.style.width = elemRect.width + offset * 2 + "px";
hilite.style.height = elemRect.height + offset * 2 + "px";
hilite.style.display = "block";
}
hiliteOnMove(event: MouseEvent) {
const elem = this.hiliteFindBestElement(event.clientX, event.clientY);
if (elem && this.hiliteElem === elem) {
return;
}
const getSrc = (elem: HTMLElement | null): string => {
if (!elem) {
return "";
}
if ((elem as HTMLImageElement).currentSrc) {
return (elem as HTMLImageElement).currentSrc;
}
if (elem.tagName === "image") {
const href = elem.getAttribute("href");
if (href) {
return href;
}
}
if (elem.tagName === "svg") {
if (!elem.getAttribute("xmlns")) {
elem.setAttribute("xmlns", "http://www.w3.org/2000/svg");
}
const buff = new TextEncoder().encode(elem.outerHTML);
const blob = new Blob([buff], { type: "image/svg+xml" });
return URL.createObjectURL(blob);
}
const src = HTMLElement.prototype.getAttribute.call(elem, "src");
if (src) {
return src;
}
if (elem.style.backgroundImage) {
return elem.style.backgroundImage.replace(
/(url\s*\(\s*[\\"']*)([^)'"]+)([\\"']*\s*\)?)/i,
"$2",
);
}
return "";
};
const src = getSrc(elem);
if (src) {
const newSrc = src.replace(
/([\d]*)([\w][\w]_)(\/(https:|http:)?\/)/,
"$1dl_$3",
);
this.replay.dispatchEvent(
new CustomEvent("update-download-res-url", { detail: { url: newSrc } }),
);
this.hiliteElem = elem;
this.hiliteRecompute();
} else {
this.clearHilite();
}
}
hiliteFindBestElement(x: number, y: number): HTMLElement | null {
const elems = this.doc.elementsFromPoint(x, y) as HTMLElement[];
if (!elems.length) {
return null;
}
const firstElem = elems[0];
const containsRect = (rect: DOMRect) => {
return (
rect.x >= firstRect.x &&
rect.y >= firstRect.y &&
rect.width <= firstRect.width &&
rect.height <= firstRect.height
);
};
// allow other elements only if they're within same bounding rect
// as the deepest element
const firstRect = firstElem.getBoundingClientRect();
let isLast = false;
for (const elem of elems) {
const rect = elem.getBoundingClientRect();
if (elem === firstElem || containsRect(rect)) {
if (
(elem as HTMLImageElement).currentSrc ||
(elem.hasAttribute("src") && !(elem instanceof HTMLIFrameElement))
) {
return elem;
}
if (elem.tagName === "image" && elem.hasAttribute("href")) {
return elem;
}
if (elem.tagName === "svg") {
return elem;
}
} else {
isLast = true;
}
const subelem =
elem.querySelector("[src]:not(script)") ||
elem.querySelector(`div[style*="background-image: url("]`);
if (subelem) {
const subRect = subelem.getBoundingClientRect();
if (
containsRect(subRect) &&
x >= subRect.left &&
y >= subRect.top &&
x < subRect.right &&
y <= subRect.bottom
) {
return subelem as HTMLElement | null;
}
}
if (isLast) {
break;
}
}
return null;
}
}
customElements.define("wr-coll-replay", Replay);
+26 -12
View File
@@ -1,21 +1,35 @@
export type URLResource = {
id: number;
title: string;
url: string;
mime: string;
status: string;
date: Date;
ts: string;
desc?: string;
timestamp?: string;
size: number;
favIconUrl: string;
text?: string;
waczhash?: string;
url: string;
};
export type Page = URLResource & {
export type Page = {
id: number;
title: string;
text?: string;
url: string;
mime: string;
status: string;
size: number;
ts: number;
timestamp: string;
date: Date | null;
favIconUrl?: string;
waczhash?: string;
seed?: boolean;
isSeed?: boolean;
desc?: string;
};
export type URLTsChange = {
url: string;
ts: string;
waczhash?: string;
};
export type ItemType = {
@@ -48,8 +62,8 @@ export type ItemType = {
};
onDemand?: boolean;
pages: Page[];
curatedPages: (URLResource & { list: string })[];
lists: URLResource[];
curatedPages: (Page & { list: string })[];
lists: Page[];
ctime?: string;
totalSize?: unknown;
size?: number | string;
+20 -1
View File
@@ -1,9 +1,10 @@
import { LitElement, html, css, type PropertyValues } from "lit";
import { wrapCss, clickOnSpacebarPress } from "./misc";
import { getReplayLink } from "./pageutils";
import { getReplayLink, getDownloadLink } from "./pageutils";
import fasSearch from "@fortawesome/fontawesome-free/svgs/solid/search.svg";
import fasDownload from "@fortawesome/fontawesome-free/svgs/solid/download.svg";
import "keyword-mark-element/lib/keyword-mark.js";
import { type ItemType } from "./types";
@@ -315,6 +316,9 @@ class URLResources extends LitElement {
.all-results .column {
word-break: break-word;
}
.dl-button {
padding: 4px;
}
div.sort-header {
padding: 10px;
margin-bottom: 0px !important;
@@ -528,6 +532,21 @@ class URLResources extends LitElement {
<tr class="columns result">
<td class="column col-url is-6">
<p class="minihead is-hidden-tablet">URL</p>
<a
class="dl-button"
href="${getDownloadLink(
this.collInfo!.replayPrefix,
result.url,
result.ts,
)}"
><fa-icon
size="1.0em"
class="has-text-black"
aria-hidden="true"
title="Download Resource"
.svg="${fasDownload}"
></fa-icon>
</a>
<a
@click="${this.onReplay}"
data-url="${result.url}"
+24 -18
View File
@@ -1075,17 +1075,17 @@
resolved "https://registry.yarnpkg.com/@webpack-cli/serve/-/serve-2.0.5.tgz#325db42395cd49fe6c14057f9a900e427df8810e"
integrity sha512-lqaoKnRYBdo1UgDX8uF24AfGMifWK19TxPmM5FHc2vAGxrJ/qtyUyFBWoY1tISZdelsQ5fBcOusifo5o5wSJxQ==
"@webrecorder/wabac@^2.24.5":
version "2.24.5"
resolved "https://registry.yarnpkg.com/@webrecorder/wabac/-/wabac-2.24.5.tgz#253b0d5a9b7691af475c7a158c6483df33f2d3ff"
integrity sha512-Wt9bdZMYrl+DYRkUdRfYAPSUfygHBQGIiZ9oMZ/DRLIeqsqeN36UBGuvbj6kmUawLNWRYVQj6kNl/ndtRZSa6w==
"@webrecorder/wabac@^2.25.1":
version "2.25.1"
resolved "https://registry.yarnpkg.com/@webrecorder/wabac/-/wabac-2.25.1.tgz#aaa99727bb2ef80746bb74ebc8d5148e03917ee9"
integrity sha512-5MvG7DTfMHYzpfFfSx/z5SDVEVTVSeyx6Nlzjt+T7nJjxh7hGmrKss86hfXorS0BaeUELrtuPL/N3yGPfCnJXw==
dependencies:
"@peculiar/asn1-ecc" "^2.3.4"
"@peculiar/asn1-schema" "^2.3.3"
"@peculiar/x509" "^1.9.2"
"@types/js-levenshtein" "^1.1.3"
"@webrecorder/wombat" "^3.10.1"
acorn-loose "^8.5.2"
"@webrecorder/wombat" "^3.10.2"
acorn "^8.15.0"
auto-js-ipfs "^2.1.1"
base64-js "^1.5.1"
brotli "^1.3.3"
@@ -1095,7 +1095,8 @@
http-status-codes "^2.1.4"
idb "^7.1.1"
js-levenshtein "^1.1.6"
js-yaml "^4.1.0"
js-yaml "^4.1.1"
mime "^4.1.0"
pako "^1.0.11"
parse5-html-rewriting-stream "^7.0.0"
parse5-sax-parser "^7.0.0"
@@ -1104,10 +1105,10 @@
stream-browserify "^3.0.0"
warcio "^2.4.7"
"@webrecorder/wombat@^3.10.1":
version "3.10.1"
resolved "https://registry.yarnpkg.com/@webrecorder/wombat/-/wombat-3.10.1.tgz#d45417f9ef61c9f02357bacf1ba930f6e09dd7fa"
integrity sha512-OzYrv3iqC1AdgjG+QD/tNQi6+yv+1izzr2PoN+40AvleL+RLsIk/AwOmdYJpz9r+L7UIOdRrK4v/JrUWPwUDzw==
"@webrecorder/wombat@^3.10.2":
version "3.10.2"
resolved "https://registry.yarnpkg.com/@webrecorder/wombat/-/wombat-3.10.2.tgz#0d8e4057be66e4b67a522b605f8be907f912d5e0"
integrity sha512-01vr0mf+ecggorX6nyxTyf8gYrFichu1UE8RQi71Ckz8DAf+mxym96ozecq9bcS80+4Bkb7vFLAGyb4h6ZRXAw==
dependencies:
warcio "^2.4.7"
@@ -1162,13 +1163,6 @@ acorn-jsx@^5.3.2:
resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz#7ed5bb55908b3b2f1bc55c6af1653bada7f07937"
integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==
acorn-loose@^8.5.2:
version "8.5.2"
resolved "https://registry.yarnpkg.com/acorn-loose/-/acorn-loose-8.5.2.tgz#a7cc7dfbb7c8f3c2e55b055db640dc657e278d26"
integrity sha512-PPvV6g8UGMGgjrMu+n/f9E/tCSkNQ2Y97eFvuVdJfG11+xdIeDcLyNdC8SHcrHbRqkfwLASdplyR6B6sKM1U4A==
dependencies:
acorn "^8.15.0"
acorn@^8.15.0:
version "8.15.0"
resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.15.0.tgz#a360898bc415edaac46c8241f6383975b930b816"
@@ -3782,6 +3776,13 @@ js-yaml@^4.1.0:
dependencies:
argparse "^2.0.1"
js-yaml@^4.1.1:
version "4.1.1"
resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.1.tgz#854c292467705b699476e1a2decc0c8a3458806b"
integrity sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==
dependencies:
argparse "^2.0.1"
jsbn@1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-1.1.0.tgz#b01307cb29b618a1ed26ec79e911f803c4da0040"
@@ -4389,6 +4390,11 @@ mime@^2.5.2:
resolved "https://registry.yarnpkg.com/mime/-/mime-2.5.2.tgz#6e3dc6cc2b9510643830e5f19d5cb753da5eeabe"
integrity sha512-tqkh47FzKeCPD2PUiPB6pkbMzsCasjxAfC62/Wap5qrUWcb+sFasXUC5I3gYM5iBM8v/Qpn4UK0x+j0iHyFPDg==
mime@^4.1.0:
version "4.1.0"
resolved "https://registry.yarnpkg.com/mime/-/mime-4.1.0.tgz#ec55df7aa21832a36d44f0bbee5c04639b27802f"
integrity sha512-X5ju04+cAzsojXKes0B/S4tcYtFAJ6tTMuSPBEn9CPGlrWr8Fiw7qYeLT0XyH80HSoAoqWCaz+MWKh22P7G1cw==
mimic-fn@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b"