diff --git a/CHANGES.md b/CHANGES.md index d738569..1c6e71a 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -2,13 +2,22 @@ ## CHANGES +v2.3.12 + +- App: Cleanup Electron app code and add typing +- App: fix loading files from disk on Windows, existing files should continue to load +- Fidelity: Fix meta refresh rewriting (via wabac.js 2.23.2) + v2.3.11 + - Fidelity: Fix to importmaps (via wabac.js 2.23.1) v2.3.10 + - Dependencies: Update Electron dependencies v2.3.9 + - Loading: Avoid unnecessary reloads of multiWACZ JSON, prune expired (via wabac.js 2.23.0) - Fidelity: Fix CSP issue with iframes in Firefox (via wabac.js 2.23.0) - Testing: Add testing for CSP handling and embeds in Firefox and Webkit diff --git a/package.json b/package.json index e5c4f5d..f80e8dd 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "replaywebpage", "productName": "ReplayWeb.page", - "version": "2.3.11", + "version": "2.3.12", "description": "Serverless Web Archive Replay", "repository": "https://github.com/webrecorder/replayweb.page", "homepage": "https://replayweb.page/", @@ -22,10 +22,10 @@ "dependencies": { "@fortawesome/fontawesome-free": "^5.15.4", "@shoelace-style/shoelace": "~2.15.1", - "@webrecorder/wabac": "^2.23.1", + "@webrecorder/wabac": "^2.23.2", "bulma": "^0.9.3", "electron-log": "^4.4.1", - "electron-updater": "^6.3.9", + "electron-updater": "^6.6.2", "fetch-ndjson": "^1.1.6", "flexsearch": "^0.7.31", "keyword-mark-element": "^0.2.0", @@ -33,9 +33,8 @@ "marked": "^4.0.10", "micromark": "^4.0.1", "micromark-extension-gfm-strikethrough": "^2.1.0", - "mime-types": "^2.1.32", + "mime-types": "^3.0.1", "minimist": "^1.2.5", - "node-fetch": "^3.3.0", "pretty-bytes": "^5.6.0", "process": "^0.11.10", "register-service-worker": "^1.7.2", @@ -47,11 +46,14 @@ "@playwright/test": "^1.52.0", "@types/flexsearch": "^0.7.3", "@types/lodash": "^4.17.5", + "@types/mime-types": "^2.1.4", + "@types/minimist": "^1.2.5", + "@types/node": "^22.15.18", "@typescript-eslint/eslint-plugin": "^6.12.0", "@typescript-eslint/parser": "^6.12.0", "copy-webpack-plugin": "^9.0.1", "css-loader": "^6.2.0", - "electron": "^36.2.1", + "electron": "^36.3.2", "electron-builder": "^26.0.12", "electron-notarize": "^1.2.2", "eslint": "^8.56.0", diff --git a/playwright.config.js b/playwright.config.js index 6c5bc3d..881c863 100644 --- a/playwright.config.js +++ b/playwright.config.js @@ -6,15 +6,15 @@ export default defineConfig({ projects: [ { name: "chrome", - use: { ...devices['Desktop Chrome'], channel: "chrome" }, + use: { ...devices["Desktop Chrome"], channel: "chrome" }, }, { - name: 'firefox', - use: { ...devices['Desktop Firefox'] }, + name: "firefox", + use: { ...devices["Desktop Firefox"] }, }, { - name: 'webkit', - use: { ...devices['Desktop Safari'] }, + name: "webkit", + use: { ...devices["Desktop Safari"] }, }, ], webServer: [ diff --git a/src/appmain.ts b/src/appmain.ts index ac63b7a..6aa36f6 100644 --- a/src/appmain.ts +++ b/src/appmain.ts @@ -639,13 +639,6 @@ export class ReplayWebApp extends LitElement { if (this.pageParams.get("swName")) { this.swName = this.pageParams.get("swName") || undefined; } - - if (IS_APP && this.sourceUrl.startsWith("file://")) { - this.loadInfo = { - sourceUrl: this.sourceUrl, - loadUrl: this.sourceUrl.replace("file://", "file2://"), - }; - } } // @ts-expect-error [// TODO: Fix this the next time the file is edited.] - TS7006 - Parameter 'event' implicitly has an 'any' type. diff --git a/src/chooser.ts b/src/chooser.ts index b83392f..95cc402 100644 --- a/src/chooser.ts +++ b/src/chooser.ts @@ -5,12 +5,15 @@ import fasUpload from "@fortawesome/fontawesome-free/svgs/solid/upload.svg"; import { customElement, property } from "lit/decorators.js"; export interface FileWithPath extends File { - path: string; + path?: string; } declare let window: Window & { electron?: { - getPath: (file: File) => string; + getPaths: (file: File) => { + loadUrl: string; + sourceUrl: string; + }; }; }; @@ -94,12 +97,13 @@ export class Chooser extends LitElement { setFile(file: FileWithPath) { this.file = file; - // file.path only available in electron app - if (IS_APP && window.electron?.getPath) { - this.file.path = window.electron.getPath(this.file); + if (IS_APP && window.electron?.getPaths) { + const { loadUrl, sourceUrl } = window.electron.getPaths(this.file); + this.file.path = loadUrl; + this.fileDisplayName = sourceUrl; + } else { + this.fileDisplayName = "file://" + file.name; } - - this.fileDisplayName = "file://" + (file.path || file.name); } async onChooseNativeFile() { @@ -149,9 +153,11 @@ export class Chooser extends LitElement { if (this.file) { loadInfo.isFile = true; - // file.path only available in electron app + // should only be set in Electron app if (this.file.path) { - loadInfo.loadUrl = "file2://" + this.file.path; + loadInfo.loadUrl = this.file.path; + loadInfo.sourceUrl = this.fileDisplayName; + loadInfo.name = this.file.name; loadInfo.noCache = true; } else if (this.fileHandle) { loadInfo.loadUrl = this.fileDisplayName; @@ -163,7 +169,9 @@ export class Chooser extends LitElement { loadInfo.noCache = false; } loadInfo.size = this.file.size; - loadInfo.name = this.fileDisplayName; + if (!loadInfo.name) { + loadInfo.name = this.fileDisplayName; + } } this.dispatchEvent( diff --git a/src/electron-preload.ts b/src/electron-preload.ts index b1329e4..4047171 100644 --- a/src/electron-preload.ts +++ b/src/electron-preload.ts @@ -3,12 +3,34 @@ import { CollectionLoader } from "@webrecorder/wabac/swlib"; import { type IpcRendererEvent } from "electron"; -const { ipcRenderer, contextBridge, webUtils } = require("electron"); +import { sep as pathSep } from "node:path"; + +import { ipcRenderer, contextBridge, webUtils } from "electron"; + +const FILE_SCHEME = "file://"; +const CUSTOM_FILE_SCHEME = "file2://"; + +function rewriteLoadUrl(loadUrl: string) { + if (pathSep !== "/") { + // for windows, will replace such that C:\path\to\file becomes C//path/to/file + loadUrl = loadUrl.replaceAll(pathSep, "/").replace(":", "/"); + } + return CUSTOM_FILE_SCHEME + loadUrl; +} contextBridge.exposeInMainWorld("electron", { IS_APP: true, - getPath(file: File) { - return webUtils.getPathForFile(file); + getPaths(file: File) { + const url = webUtils.getPathForFile(file); + const sourceUrl = FILE_SCHEME + url; + const loadUrl = rewriteLoadUrl(url); + return { loadUrl, sourceUrl }; + }, + getFileLoadUrl(sourceUrl: string) { + if (!sourceUrl.startsWith(FILE_SCHEME)) { + return sourceUrl; + } + return rewriteLoadUrl(sourceUrl.slice(FILE_SCHEME.length)); }, }); diff --git a/src/electron-replay-app.ts b/src/electron-replay-app.ts index 2ee1978..9687f17 100644 --- a/src/electron-replay-app.ts +++ b/src/electron-replay-app.ts @@ -1,8 +1,5 @@ /* eslint-env node */ -import fetch from "node-fetch"; -import { Headers } from "node-fetch"; - import { app, session, @@ -16,21 +13,20 @@ import { import path from "path"; import fs from "fs"; -import { ArchiveResponse, Rewriter } from "@webrecorder/wabac"; +import { ArchiveRequest, ArchiveResponse, Rewriter } from "@webrecorder/wabac"; -import { PassThrough, Readable } from "stream"; +import { Readable } from "stream"; import { autoUpdater } from "electron-updater"; import log from "electron-log"; -// @ts-expect-error [// TODO: Fix this the next time the file is edited.] - TS7016 - Could not find a declaration file for module 'mime-types'. 'node_modules/mime-types/index.js' implicitly has an 'any' type. import mime from "mime-types"; import url from "url"; -// @ts-expect-error - TS2322 - Type 'typeof Headers' is not assignable to type '{ new (init?: HeadersInit | undefined): Headers; prototype: Headers; }'. -global.Headers = Headers; -// @ts-expect-error - TS2322 - Type '(url: RequestInfo, init?: RequestInit | undefined) => Promise' is not assignable to type '(input: RequestInfo, init?: RequestInit | undefined) => Promise'. -global.fetch = fetch; +import minimist from "minimist"; + +//global.Headers = Headers; +//global.fetch = fetch; const STATIC_PREFIX = "http://localhost:5471/"; @@ -95,7 +91,7 @@ class ElectronReplayApp { ); app.quit(); } else { - app.on("second-instance", (event, commandLine /*, workingDir*/) => { + app.on("second-instance", (_, commandLine) => { // Just create a new window in case of second instance request this.createMainWindow(commandLine); }); @@ -147,9 +143,7 @@ class ElectronReplayApp { } }); - // TODO: Fix this the next time the file is edited. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - app.whenReady().then(() => this.onAppReady()); + void app.whenReady().then(() => this.onAppReady()); // Quit when all windows are closed. app.on("window-all-closed", function () { @@ -164,9 +158,7 @@ class ElectronReplayApp { autoUpdater.logger = log; // @ts-expect-error - TS2339 - Property 'transports' does not exist on type 'Logger'. autoUpdater.logger.transports.file.level = "info"; - // TODO: Fix this the next time the file is edited. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - autoUpdater.checkForUpdatesAndNotify(); + void autoUpdater.checkForUpdatesAndNotify(); } onAppReady() { @@ -178,17 +170,13 @@ class ElectronReplayApp { contents.setWindowOpenHandler(({ url }) => { // load docs in native browser for now if (url === STATIC_PREFIX + "docs") { - // TODO: Fix this the next time the file is edited. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - shell.openExternal("https://replayweb.page/docs/"); + void shell.openExternal("https://replayweb.page/docs/"); return { action: "deny" }; } // load external URLs in native browser if (!url.startsWith(STATIC_PREFIX)) { - // TODO: Fix this the next time the file is edited. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - shell.openExternal(url); + void shell.openExternal(url); return { action: "deny" }; } @@ -198,16 +186,12 @@ class ElectronReplayApp { const sesh = session.defaultSession; - // TODO: Fix this the next time the file is edited. - // eslint-disable-next-line @typescript-eslint/promise-function-async - sesh.protocol.interceptStreamProtocol("http", (request, callback) => - this.doIntercept(request, callback), + sesh.protocol.handle("http", async (request: Request) => + this.doIntercept(request), ); - // TODO: Fix this the next time the file is edited. - // eslint-disable-next-line @typescript-eslint/promise-function-async - protocol.registerStreamProtocol(FILE_PROTO, (request, callback) => - this.doHandleFile(request, callback), + protocol.handle(FILE_PROTO, async (request: Request) => + this.doHandleFile(request), ); this.origUA = sesh.getUserAgent(); @@ -215,69 +199,49 @@ class ElectronReplayApp { this.mainWindow = this.createMainWindow(process.argv); } - // @ts-expect-error [// TODO: Fix this the next time the file is edited.] - TS7006 - Parameter 'request' implicitly has an 'any' type. | TS7006 - Parameter 'callback' implicitly has an 'any' type. - async doHandleFile(request, callback) { + async doHandleFile(request: Request): Promise { //const parsedUrl = new URL(request.url); //const filename = parsedUrl.searchParams.get("filename"); if (request.url === FILE_PROTO + "://localhost") { - callback({ statusCode: 200, data: null }); - return; + return new Response("", { status: 200 }); } - // TODO: Fix this the next time the file is edited. - // eslint-disable-next-line @typescript-eslint/no-unsafe-argument - const filename = url.fileURLToPath(request.url.replace(FILE_PROTO, "file")); + let urlStr = request.url.replace(FILE_PROTO, "file"); + if (path.sep !== "/") { + urlStr = urlStr.replace(/(\/\/\w)(\/\/)/, "$1:/"); + urlStr = urlStr.replaceAll("/", path.sep); + } - const headers = { "Content-Type": "application/octet-stream" }; - // TODO: Fix this the next time the file is edited. - // eslint-disable-next-line @typescript-eslint/no-unsafe-argument + const filename = url.fileURLToPath(urlStr); + + const headers = new Headers({ "Content-Type": "application/octet-stream" }); const reqHeaders = new Headers(request.headers); if (filename) { const stat = await fs.promises.lstat(filename); if (!stat.isFile()) { - return this.notFound(filename, callback); + return this.notFound(filename); } const size = stat.size; - const { statusCode, start, end } = this.parseRange( - reqHeaders, - headers, - size, - ); + const { status, start, end } = this.parseRange(reqHeaders, headers, size); const data = request.method === "HEAD" ? null - : fs.createReadStream(filename, { start, end }); + : Readable.toWeb(fs.createReadStream(filename, { start, end })); - callback({ statusCode, headers, data }); - return; + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-explicit-any + return new Response(data as any, { status, headers }); } else { - return this.notFound("No Resource Specified", callback); + return this.notFound("No Resource Specified"); } } - // @ts-expect-error [// TODO: Fix this the next time the file is edited.] - TS7006 - Parameter 'data' implicitly has an 'any' type. - _bufferToStream(data) { - const rv = new PassThrough(); - rv.push(data); - rv.push(null); - return rv; - } - - async doIntercept( - // @ts-expect-error [// TODO: Fix this the next time the file is edited.] - TS7006 - Parameter 'request' implicitly has an 'any' type. - request, - callback: (response: { - statusCode: number; - headers: Record; - data: fs.ReadStream; - }) => void, - ) { + async doIntercept(request: Request): Promise { console.log(`${request.method} ${request.url} from ${request.referrer}`); // if local server @@ -289,8 +253,8 @@ class ElectronReplayApp { this.proxyColl = m[1]; this.proxyTS = m[2]; - request.url = m[3]; - return await this.resolveArchiveResponse(request, callback); + //request.url = m[3]; + return await this.resolveArchiveResponse(request, m[3]); } } else { // try serve static file from app dir @@ -303,8 +267,6 @@ class ElectronReplayApp { filename = "docs/index.html"; } - // TODO: Fix this the next time the file is edited. - // eslint-disable-next-line @typescript-eslint/no-unsafe-argument let ext = path.extname(filename); if (!ext) { ext = ".html"; @@ -314,157 +276,139 @@ class ElectronReplayApp { const mimeType = mime.contentType(ext); if (mimeType) { - // TODO: Fix this the next time the file is edited. - // eslint-disable-next-line @typescript-eslint/no-unsafe-argument const fullPath = path.join(this.staticContentPath, filename); console.log("fullPath: " + fullPath); const data = fs.createReadStream(fullPath); - return callback({ - statusCode: 200, + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-explicit-any + return new Response(Readable.toWeb(data) as any, { headers: { "content-type": mimeType }, - data, + status: 200, }); } } - return this.notFound(request.url, callback); + return this.notFound(request.url); } // possible 'live leak' attempt, return archived version, if any - if (request.referrer?.startsWith(REPLAY_PREFIX)) { - return await this.resolveArchiveResponse(request, callback); + if (request.referrer.startsWith(REPLAY_PREFIX)) { + return await this.resolveArchiveResponse(request); } - await this.proxyLive(request, callback); + return await this.proxyLive(request); } - // @ts-expect-error [// TODO: Fix this the next time the file is edited.] - TS7006 - Parameter 'request' implicitly has an 'any' type. | TS7006 - Parameter 'callback' implicitly has an 'any' type. - async proxyLive(request, callback) { + async proxyLive(request: Request): Promise { let headers = request.headers; - const { method, url, uploadData } = request; + const { method, url, body } = request; - const body = uploadData - ? Readable.from(readBody(uploadData, session.defaultSession)) - : null; + // const body = uploadData + // ? Readable.from(readBody(uploadData, session.defaultSession)) + // : null; if (this.origUA) { - // pass UA if origUA is set - headers["User-Agent"] = this.origUA; + headers = new Headers(headers); + headers.set("User-Agent", this.origUA); } let response; try { - // TODO: Fix this the next time the file is edited. - // eslint-disable-next-line @typescript-eslint/no-unsafe-argument response = await fetch(url, { method, headers, body }); } catch (e) { console.warn("fetch failed for: " + url); - callback({ statusCode: 502, headers: {}, data: null }); - return; + return new Response("", { status: 502 }); } const data = method === "HEAD" ? null : response.body; - const statusCode = response.status; + const status = response.status; - headers = Object.fromEntries(response.headers.entries()); - callback({ statusCode, headers, data }); + return new Response(data, { status, headers: response.headers }); } - /** - * - * @param {string} url - * @param {(props: { - * statusCode: number; - * headers: Record; - * data: unknown; - * }) => void} callback - */ - // @ts-expect-error [// TODO: Fix this the next time the file is edited.] - TS7006 - Parameter 'url' implicitly has an 'any' type. | TS7006 - Parameter 'callback' implicitly has an 'any' type. - notFound(url, callback) { + notFound(url: string) { console.log("not found: " + url); - const data = this._bufferToStream( + const headers = { "Content-Type": 'text/html; charset="utf-8"' }; + return new Response( `Sorry, the url ${url} could not be found in this archive.`, + { status: 404, headers }, ); - callback({ - statusCode: 404, - headers: { "Content-Type": 'text/html; charset="utf-8"' }, - data, - }); } - // @ts-expect-error [// TODO: Fix this the next time the file is edited.] - TS7006 - Parameter 'request' implicitly has an 'any' type. | TS7006 - Parameter 'callback' implicitly has an 'any' type. - async resolveArchiveResponse(request, callback) { - const channel = `req:${new Date().getTime()}:${request.url}`; + async resolveArchiveResponse( + request: Request, + urlOverride?: string, + ): Promise { + const url = urlOverride || request.url; + const channel = `req:${new Date().getTime()}:${url}`; - ipcMain.once(channel, async (event, status, headers, payload) => { - const url = request.url; + let resolve: (r: Response) => void; - if (status === 404 && !payload) { - return this.notFound(url, callback); - } else { - console.log("got response for: " + url); - } - - // TODO: Fix this the next time the file is edited. - // eslint-disable-next-line @typescript-eslint/no-unsafe-argument - headers = new Headers(headers); - const date = new Date(); - - let response: ArchiveResponse = new ArchiveResponse({ + const p = new Promise((r) => (resolve = r)); + ipcMain.once( + channel, + async ( + event, + status: number, + reqHeaders: Record, payload, - headers, - status, - date, - url, - }); - - const rewriter = new Rewriter({ - baseUrl: url, - prefix: "", - urlRewrite: false, - contentRewrite: true, - decode: true, - useBaseRules: true, - }); - - // TODO: Fix this the next time the file is edited. - // eslint-disable-next-line @typescript-eslint/no-unsafe-argument - request.headers = new Headers(request.headers); - - try { - response = await rewriter.rewrite(response, request); - - // TODO: Fix this the next time the file is edited. - // eslint-disable-next-line @typescript-eslint/no-unsafe-argument - headers = Object.fromEntries(response.headers.entries()); - - let data = await response.getBuffer(); - if (!data) { - data = new Uint8Array(); + ) => { + if (status === 404 && !payload) { + return this.notFound(url); + } else { + console.log("got response for: " + url); } - if (status === 206 || status === 200) { - const { statusCode, start, end } = this.parseRange( - request.headers, - headers, - data.length, - ); - if (start !== undefined) { - data = data.slice(start, end); + let headers = new Headers(reqHeaders); + const date = new Date(); + + let response: ArchiveResponse = new ArchiveResponse({ + payload, + headers, + status, + date, + url, + }); + + const rewriter = new Rewriter({ + baseUrl: url, + prefix: "", + urlRewrite: false, + contentRewrite: true, + decode: true, + useBaseRules: true, + }); + + const arRequest = new ArchiveRequest(url, request); + + try { + response = await rewriter.rewrite(response, arRequest); + + headers = response.headers; + + let data = await response.getBuffer(); + if (!data) { + data = new Uint8Array(); } - status = statusCode; + + if (status === 206 || status === 200) { + headers = new Headers(response.headers); + const res = this.parseRange(request.headers, headers, data.length); + const { start, end } = res; + status = res.status; + if (start !== undefined) { + data = data.slice(start, end); + } + } + + resolve(new Response(data, { status, headers })); + } catch (e) { + console.warn(e); } - - const result = this._bufferToStream(data); - - callback({ statusCode: status, headers, data: result }); - } catch (e) { - console.warn(e); - } - }); + }, + ); if (this.mainWindow) { this.mainWindow.webContents.send( @@ -475,45 +419,39 @@ class ElectronReplayApp { channel, ); } + + return p; } - // @ts-expect-error [// TODO: Fix this the next time the file is edited.] - TS7006 - Parameter 'reqHeaders' implicitly has an 'any' type. | TS7006 - Parameter 'headers' implicitly has an 'any' type. | TS7006 - Parameter 'size' implicitly has an 'any' type. - parseRange(reqHeaders, headers, size) { - let statusCode = 200; + parseRange(reqHeaders: Headers, headers: Headers, size: number) { + let status = 200; const range = reqHeaders.get("range"); if (!range) { - if (headers) { - headers["Content-Length"] = "" + size; - } - return { statusCode }; + headers.set("content-length", "" + size); + return { status }; } const m = range.match(/bytes=([\d]+)-([\d]*)/); if (!m) { - return { statusCode }; + return { status }; } const start = Number(m[1]); const end = m[2] ? Number(m[2]) : size - 1; - statusCode = 206; - if (headers) { - headers["Content-Range"] = `bytes ${start}-${end}/${size}`; - headers["Content-Length"] = `${end - start + 1}`; - } - return { statusCode, start, end }; + status = 206; + headers.set("content-range", `bytes ${start}-${end}/${size}`); + headers.set("content-length", `${end - start + 1}`); + return { status, start, end }; } - // @ts-expect-error [// TODO: Fix this the next time the file is edited.] - TS7006 - Parameter 'argv' implicitly has an 'any' type. - createMainWindow(argv) { + createMainWindow(argv: string[]) { const sourceString = this.getOpenUrl(argv); // Create the browser window. const theWindow = new BrowserWindow({ width: this.screenSize.width, height: this.screenSize.height, - // @ts-expect-error - TS2345 - Argument of type '{ width: any; height: any; isMaximized: boolean; show: false; webPreferences: { plugins: boolean; preload: string; nativeWindowOpen: boolean; contextIsolation: boolean; enableRemoteModule: boolean; sandbox: boolean; nodeIntegration: boolean; }; }' is not assignable to parameter of type 'BrowserWindowConstructorOptions'. - isMaximized: true, show: false, webPreferences: this.mainWindowWebPreferences, }).once("ready-to-show", () => { @@ -521,9 +459,7 @@ class ElectronReplayApp { theWindow.maximize(); }); - // TODO: Fix this the next time the file is edited. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - theWindow.loadURL(STATIC_PREFIX + this.mainWindowUrl + sourceString); + void theWindow.loadURL(STATIC_PREFIX + this.mainWindowUrl + sourceString); if (process.env.NODE_ENV === "development") { theWindow.webContents.openDevTools(); } @@ -531,15 +467,15 @@ class ElectronReplayApp { return theWindow; } - // @ts-expect-error [// TODO: Fix this the next time the file is edited.] - TS7006 - Parameter 'argv' implicitly has an 'any' type. - getOpenUrl(argv) { - argv = require("minimist")(argv.slice(process.defaultApp ? 2 : 1)); + getOpenUrl(argv: string[]) { + const parsed = minimist(argv.slice(process.defaultApp ? 2 : 1)); const filename = this.openNextFile || - argv.filename || - argv.f || - (argv._.length && argv._[0]); + parsed.filename || + parsed.f || + (parsed._.length && parsed._[0]); + this.openNextFile = null; let sourceString = ""; @@ -551,19 +487,15 @@ class ElectronReplayApp { const urlParams = new URLSearchParams(); - const openUrl = argv.url; + const openUrl = parsed.url as string | undefined; - const openTS = argv.ts || argv.timestamp; + const openTS = (parsed.ts || parsed.timestamp) as string | undefined; if (openUrl) { - // TODO: Fix this the next time the file is edited. - // eslint-disable-next-line @typescript-eslint/no-unsafe-argument urlParams.set("url", openUrl); } if (openTS) { - // TODO: Fix this the next time the file is edited. - // eslint-disable-next-line @typescript-eslint/no-unsafe-argument urlParams.set("ts", openTS); } @@ -576,15 +508,14 @@ class ElectronReplayApp { } } -// @ts-expect-error [// TODO: Fix this the next time the file is edited.] - TS7006 - Parameter 'body' implicitly has an 'any' type. | TS7006 - Parameter 'session' implicitly has an 'any' type. -async function* readBody(body, session) { - for (const chunk of body) { - if (chunk.bytes) { - yield await Promise.resolve(chunk.bytes); - } else if (chunk.blobUUID) { - yield await session.getBlobData(chunk.blobUUID); - } - } -} +// async function* readBody(body, session) { +// for (const chunk of body) { +// if (chunk.bytes) { +// yield await Promise.resolve(chunk.bytes); +// } else if (chunk.blobUUID) { +// yield await session.getBlobData(chunk.blobUUID); +// } +// } +// } export { ElectronReplayApp, STATIC_PREFIX }; diff --git a/src/loader.ts b/src/loader.ts index 9d404ff..c0fafee 100644 --- a/src/loader.ts +++ b/src/loader.ts @@ -1,11 +1,10 @@ import { LitElement, html, css, type PropertyValues, nothing } from "lit"; -import { wrapCss } from "./misc"; +import { IS_APP, wrapCss } from "./misc"; import rwpLogo from "~assets/brand/replaywebpage-icon-color.svg"; import rwpLogoAnimated from "~assets/brand/replaywebpage-icon-color-animated.svg"; import prettyBytes from "pretty-bytes"; -import { parseURLSchemeHostPath } from "./pageutils"; import { property } from "lit/decorators.js"; import type { LoadInfo } from "./item"; import { ifDefined } from "lit/directives/if-defined.js"; @@ -28,6 +27,12 @@ const NO_ANIM_STATES: LoadingState[] = [ "permission_needed", ]; +declare let window: Window & { + electron?: { + getFileLoadUrl: (sourceUrl: string) => string; + }; +}; + class Loader extends LitElement { @property({ type: String }) sourceUrl?: string; @property({ type: Object }) loadInfo: LoadInfo | null = null; @@ -46,7 +51,7 @@ class Loader extends LitElement { @property({ type: String }) extraMsg?: string; @property({ type: String }) swName?: string; - pingInterval: number | NodeJS.Timer = 0; + pingInterval: number | NodeJS.Timeout = 0; // eslint-disable-next-line @typescript-eslint/no-explicit-any -- requestPermission() type mismatch fileHandle: any = null; noWebWorker = false; @@ -161,7 +166,8 @@ class Loader extends LitElement { // custom protocol handlers here... try { - const { scheme, host, path } = parseURLSchemeHostPath(sourceUrl!); + const url = new URL(sourceUrl!); + const scheme = url.protocol.slice(0, -1); switch (scheme) { case "googledrive": @@ -174,22 +180,35 @@ class Loader extends LitElement { case "s3": source = { sourceUrl, - loadUrl: `https://${host}.s3.amazonaws.com${path}`, + loadUrl: `https://${url.hostname}.s3.amazonaws.com${url.href.slice( + url.origin.length, + )}`, name: this.sourceUrl, }; break; case "file": - if (!this.loadInfo && !this.tryFileHandle) { - this.state = "errored"; - this.error = `\ -File URLs can not be entered directly or shared. -You can select a file to upload from the main page by clicking the 'Choose File...' button.`; - this.errorAllowRetry = false; - return; - } + if (!this.loadInfo && sourceUrl && IS_APP && window.electron) { + const loadUrl = window.electron.getFileLoadUrl(sourceUrl); + const name = loadUrl.slice(loadUrl.lastIndexOf("/") + 1); + source = { + sourceUrl, + loadUrl, + name, + noCache: true, + }; + } else { + if (!this.loadInfo && !this.tryFileHandle) { + this.state = "errored"; + this.error = `\ + File URLs can not be entered directly or shared. + You can select a file to upload from the main page by clicking the 'Choose File...' button.`; + this.errorAllowRetry = false; + return; + } - source = this.loadInfo; + source = this.loadInfo; + } break; case "proxy": diff --git a/src/pageutils.ts b/src/pageutils.ts index 795ea64..592a78f 100644 --- a/src/pageutils.ts +++ b/src/pageutils.ts @@ -103,7 +103,7 @@ async function sourceToId(url: string) { // simple parse of scheme, host, rest of path // not using URL due to different browser behavior function parseURLSchemeHostPath(url: string) { - let inx = url.indexOf("://"); + let inx = url.indexOf(":"); let hostInx = 0; let scheme = ""; let host = ""; @@ -111,12 +111,11 @@ function parseURLSchemeHostPath(url: string) { if (inx >= 0) { scheme = url.slice(0, inx); - inx += 3; - } else { - inx++; - if (url.startsWith("//")) { - inx += 2; - } + url = url.slice(inx + 1); + inx += 1; + } + if (url.startsWith("//")) { + inx += 2; } hostInx = url.indexOf("/", inx); diff --git a/tests/embeds.spec.js b/tests/embeds.spec.js index ffe2b7c..b8c5447 100644 --- a/tests/embeds.spec.js +++ b/tests/embeds.spec.js @@ -57,7 +57,6 @@ test("same-domain iframe embed is loading", async ({ page }) => { const inside = res.frameLocator("iframe").locator("body"); await expect(inside).toContainText("Inside iframe"); - }); test("cross-domain iframe embed is loading", async ({ page }) => { @@ -75,7 +74,6 @@ test("cross-domain iframe embed is loading", async ({ page }) => { const inside = res.frameLocator("iframe").locator("body"); await expect(inside).toContainText("Inside iframe"); - }); test("sandbox + cross-domain embed is loading", async ({ page }) => { diff --git a/tsconfig.json b/tsconfig.json index a547c9a..a084aa1 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -2,6 +2,7 @@ "compilerOptions": { "outDir": "./dist/", "module": "esnext", + "lib": ["esnext", "dom", "dom.iterable"], "target": "es6", "moduleResolution": "Bundler", "allowJs": true, diff --git a/yarn.lock b/yarn.lock index 4e9723a..81b60d9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -726,6 +726,11 @@ resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.17.5.tgz#e6c29b58e66995d57cd170ce3e2a61926d55ee04" integrity sha512-MBIOHVZqVqgfro1euRDWX7OO0fBVUUMrN6Pwm8LQsz8cWhEpihlvR70ENj3f40j58TNxZaWv2ndSkInykNBBJw== +"@types/mime-types@^2.1.4": + version "2.1.4" + resolved "https://registry.yarnpkg.com/@types/mime-types/-/mime-types-2.1.4.tgz#93a1933e24fed4fb9e4adc5963a63efcbb3317a2" + integrity sha512-lfU4b34HOri+kAY5UheuFMWPDOI+OPceBSHZKp69gEyTL/mmJ4cnU6Y/rlme3UL3GyOn6Y42hyIEw0/q8sWx5w== + "@types/mime@*": version "3.0.1" resolved "https://registry.yarnpkg.com/@types/mime/-/mime-3.0.1.tgz#5f8f2bca0a5863cb69bc0b0acd88c96cb1d4ae10" @@ -736,6 +741,11 @@ resolved "https://registry.yarnpkg.com/@types/mime/-/mime-1.3.5.tgz#1ef302e01cf7d2b5a0fa526790c9123bf1d06690" integrity sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w== +"@types/minimist@^1.2.5": + version "1.2.5" + resolved "https://registry.yarnpkg.com/@types/minimist/-/minimist-1.2.5.tgz#ec10755e871497bcd83efe927e43ec46e8c0747e" + integrity sha512-hov8bUuiLiyFPGyFPE1lwWhmzYbirOXQNNo40+y3zow8aFVTeyn3VWL0VFFfdNddA8S4Vf0Tc062rzyNr7Paag== + "@types/ms@*": version "0.7.31" resolved "https://registry.yarnpkg.com/@types/ms/-/ms-0.7.31.tgz#31b7ca6407128a3d2bbc27fe2d21b345397f6197" @@ -753,7 +763,7 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-17.0.38.tgz#f8bb07c371ccb1903f3752872c89f44006132947" integrity sha512-5jY9RhV7c0Z4Jy09G+NIDTsCZ5G0L5n+Z+p+Y7t5VJHM30bgwzSjVtlcBxqAj+6L/swIlvtOSzr8rBk/aNyV2g== -"@types/node@^22.7.7": +"@types/node@^22.15.18", "@types/node@^22.7.7": version "22.15.18" resolved "https://registry.yarnpkg.com/@types/node/-/node-22.15.18.tgz#2f8240f7e932f571c2d45f555ba0b6c3f7a75963" integrity sha512-v1DKRfUdyW+jJhZNEI1PYy29S2YRxMV5AOO/x/SjKmW0acCIOqmbj6Haf9eHAhsPmrhlHSxEhv/1WszcLWV4cg== @@ -1102,10 +1112,10 @@ resolved "https://registry.yarnpkg.com/@webpack-cli/serve/-/serve-2.0.5.tgz#325db42395cd49fe6c14057f9a900e427df8810e" integrity sha512-lqaoKnRYBdo1UgDX8uF24AfGMifWK19TxPmM5FHc2vAGxrJ/qtyUyFBWoY1tISZdelsQ5fBcOusifo5o5wSJxQ== -"@webrecorder/wabac@^2.23.1": - version "2.23.1" - resolved "https://registry.yarnpkg.com/@webrecorder/wabac/-/wabac-2.23.1.tgz#bf51615c82954a09db19b30443b9d386ba1af5ae" - integrity sha512-9qNjelK5TvLa8adMdOPAsUC29TTRA/6vHmjhRmsi4PSKhCN/1F0YpZYL6H0LJHr2+WpE/coXMxB0nqhJKOhLLw== +"@webrecorder/wabac@^2.23.2": + version "2.23.2" + resolved "https://registry.yarnpkg.com/@webrecorder/wabac/-/wabac-2.23.2.tgz#be6447bffe90d45d4a7fbee894f7a544a4a281e9" + integrity sha512-hYtr62PvHqE9FHhzPFwi9prA6bqnbX/SXYB4IcKrCvENgB1EqB71/bEBUqZ4GbMnX2pI4YNJfpA7tXF+n1Azgw== dependencies: "@peculiar/asn1-ecc" "^2.3.4" "@peculiar/asn1-schema" "^2.3.3" @@ -1587,14 +1597,6 @@ buffer@^6.0.3: base64-js "^1.3.1" ieee754 "^1.2.1" -builder-util-runtime@9.2.10: - version "9.2.10" - resolved "https://registry.yarnpkg.com/builder-util-runtime/-/builder-util-runtime-9.2.10.tgz#a0f7d9e214158402e78b74a745c8d9f870c604bc" - integrity sha512-6p/gfG1RJSQeIbz8TK5aPNkoztgY1q5TgmGFMAXcY8itsGW6Y2ld1ALsZ5UJn8rog7hKF3zHx5iQbNQ8uLcRlw== - dependencies: - debug "^4.3.4" - sax "^1.2.4" - builder-util-runtime@9.3.1: version "9.3.1" resolved "https://registry.yarnpkg.com/builder-util-runtime/-/builder-util-runtime-9.3.1.tgz#0daedde0f6d381f2a00a50a407b166fe7dca1a67" @@ -2065,11 +2067,6 @@ cssesc@^3.0.0: resolved "https://registry.yarnpkg.com/cssesc/-/cssesc-3.0.0.tgz#37741919903b868565e1c09ea747445cd18983ee" integrity sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg== -data-uri-to-buffer@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/data-uri-to-buffer/-/data-uri-to-buffer-4.0.0.tgz#b5db46aea50f6176428ac05b73be39a57701a64b" - integrity sha512-Vr3mLBA8qWmcuschSLAOogKgQ/Jwxulv3RNE4FXnYWRGujzrRWQI4m12fQqRkwX06C0KanhLr4hK+GydchZsaA== - debug@2.6.9: version "2.6.9" resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" @@ -2360,12 +2357,12 @@ electron-to-chromium@^1.4.668: resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.733.tgz#708d51ce01a7ed3b401d5ced85e9f5d504fd4217" integrity sha512-gUI9nhI2iBGF0OaYYLKOaOtliFMl+Bt1rY7VmEjwxOxqoYLub/D9xmduPEhbw2imE6gYkJKhIE5it+KE2ulVxQ== -electron-updater@^6.3.9: - version "6.3.9" - resolved "https://registry.yarnpkg.com/electron-updater/-/electron-updater-6.3.9.tgz#e1e7f155624c58e6f3760f376c3a584028165ec4" - integrity sha512-2PJNONi+iBidkoC5D1nzT9XqsE8Q1X28Fn6xRQhO3YX8qRRyJ3mkV4F1aQsuRnYPqq6Hw+E51y27W75WgDoofw== +electron-updater@^6.6.2: + version "6.6.2" + resolved "https://registry.yarnpkg.com/electron-updater/-/electron-updater-6.6.2.tgz#3e65e044f1a99b00d61e200e24de8e709c69ce99" + integrity sha512-Cr4GDOkbAUqRHP5/oeOmH/L2Bn6+FQPxVLZtPbcmKZC63a1F3uu5EefYOssgZXG3u/zBlubbJ5PJdITdMVggbw== dependencies: - builder-util-runtime "9.2.10" + builder-util-runtime "9.3.1" fs-extra "^10.1.0" js-yaml "^4.1.0" lazy-val "^1.0.5" @@ -2374,10 +2371,10 @@ electron-updater@^6.3.9: semver "^7.6.3" tiny-typed-emitter "^2.1.0" -electron@^36.2.1: - version "36.2.1" - resolved "https://registry.yarnpkg.com/electron/-/electron-36.2.1.tgz#7c9d7c6c7076a3ddb7b1c326a851cb92c775b0b1" - integrity sha512-mm1Y+Ms46xcOTA69h8hpqfX392HfV4lga9aEkYkd/Syx1JBStvcACOIouCgGrnZpxNZPVS1jM8NTcMkNjuK6BQ== +electron@^36.3.2: + version "36.3.2" + resolved "https://registry.yarnpkg.com/electron/-/electron-36.3.2.tgz#4a60f95e8d3858d01570c03b58dc2fb2f17ee8b6" + integrity sha512-v0/j7n22CL3OYv9BIhq6JJz2+e1HmY9H4bjTk8/WzVT9JwVX/T/21YNdR7xuQ6XDSEo9gP5JnqmjOamE+CUY8Q== dependencies: "@electron/get" "^2.0.0" "@types/node" "^22.7.7" @@ -2807,14 +2804,6 @@ fd-slicer@~1.1.0: dependencies: pend "~1.2.0" -fetch-blob@^3.1.2, fetch-blob@^3.1.4: - version "3.2.0" - resolved "https://registry.yarnpkg.com/fetch-blob/-/fetch-blob-3.2.0.tgz#f09b8d4bbd45adc6f0c20b7e787e793e309dcce9" - integrity sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ== - dependencies: - node-domexception "^1.0.0" - web-streams-polyfill "^3.0.3" - fetch-ndjson@^1.1.6: version "1.1.6" resolved "https://registry.yarnpkg.com/fetch-ndjson/-/fetch-ndjson-1.1.6.tgz#541eb930a48cf5311093b8aa71bece0ed9713e7f" @@ -2915,13 +2904,6 @@ form-data@^4.0.0: combined-stream "^1.0.8" mime-types "^2.1.12" -formdata-polyfill@^4.0.10: - version "4.0.10" - resolved "https://registry.yarnpkg.com/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz#24807c31c9d402e002ab3d8c720144ceb8848423" - integrity sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g== - dependencies: - fetch-blob "^3.1.2" - forwarded@0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.2.0.tgz#2269936428aad4c15c7ebe9779a84bf0b2a81811" @@ -4306,13 +4288,25 @@ mime-db@1.52.0: resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.49.0.tgz#f3dfde60c99e9cf3bc9701d687778f537001cbed" integrity sha512-CIc8j9URtOVApSFCQIF+VBkX1RwXp/oMMOrqdyXSBXq5RWNEsRfyj1kiRnQgmNXmHxPoFIxOroKA3zcU9P+nAA== -mime-types@^2.1.12, mime-types@^2.1.27, mime-types@^2.1.31, mime-types@^2.1.32, mime-types@~2.1.17, mime-types@~2.1.24, mime-types@~2.1.34: +mime-db@^1.54.0: + version "1.54.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.54.0.tgz#cddb3ee4f9c64530dff640236661d42cb6a314f5" + integrity sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ== + +mime-types@^2.1.12, mime-types@^2.1.27, mime-types@^2.1.31, mime-types@~2.1.17, mime-types@~2.1.24, mime-types@~2.1.34: version "2.1.35" resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a" integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw== dependencies: mime-db "1.52.0" +mime-types@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-3.0.1.tgz#b1d94d6997a9b32fd69ebaed0db73de8acb519ce" + integrity sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA== + dependencies: + mime-db "^1.54.0" + mime@1.6.0, mime@^1.6.0: version "1.6.0" resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" @@ -4560,20 +4554,6 @@ node-api-version@^0.2.0: dependencies: semver "^7.3.5" -node-domexception@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/node-domexception/-/node-domexception-1.0.0.tgz#6888db46a1f71c0b76b3f7555016b63fe64766e5" - integrity sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ== - -node-fetch@^3.3.0: - version "3.3.0" - resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-3.3.0.tgz#37e71db4ecc257057af828d523a7243d651d91e4" - integrity sha512-BKwRP/O0UvoMKp7GNdwPlObhYGB5DQqwhEDQlNKuoqwVYSxkSZCSbHjnFFmUEtwSKRPU4kNK8PbDYYitwaE3QA== - dependencies: - data-uri-to-buffer "^4.0.0" - fetch-blob "^3.1.4" - formdata-polyfill "^4.0.10" - node-forge@^1: version "1.3.1" resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-1.3.1.tgz#be8da2af243b2417d5f646a770663a92b7e9ded3" @@ -6460,11 +6440,6 @@ web-component-analyzer@^2.0.0, web-component-analyzer@^2.0.0-next.5: typescript "~5.2.0" yargs "^17.7.2" -web-streams-polyfill@^3.0.3: - version "3.2.1" - resolved "https://registry.yarnpkg.com/web-streams-polyfill/-/web-streams-polyfill-3.2.1.tgz#71c2718c52b45fd49dbeee88634b3a60ceab42a6" - integrity sha512-e0MO3wdXWKrLbL0DgGnUV7WHVuw9OUvL4hjgnPkIeEvESk74gAITi5G606JtZPp39cd8HA9VQzCIvA49LpPN5Q== - webpack-cli@^5.1.4: version "5.1.4" resolved "https://registry.yarnpkg.com/webpack-cli/-/webpack-cli-5.1.4.tgz#c8e046ba7eaae4911d7e71e2b25b776fcc35759b"