From a63390f10c064bdf6b02ccbcdcb223b64ae6cf79 Mon Sep 17 00:00:00 2001 From: Emma Segal-Grossman Date: Tue, 6 Feb 2024 21:50:12 -0500 Subject: [PATCH] Update NPM packaging, linting, and a bunch of types (#275) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Preparation for https://github.com/webrecorder/archiveweb.page/issues/198 / https://github.com/webrecorder/archiveweb.page/pull/199. ## Changes - Organizes published files more cleanly, using `dist/types` folder as the target for type declarations and sourcemaps - Configures type field in `package.json` - Updates ESLint & its config - Sets up type-informed linting rules mirroring browsertrix - Adds a bunch of other useful eslint and eslint-typescript rules, and applies auto-fixes to the codebase - Cleans up a bunch of types throughout the codebase, particularly parts of the code used by ArchiveWebPage - Changes `ItemIndex#_deleting` from `private` to `protected` so that a part of AWP that extends it can do so properly - Makes a bunch of previously-implied [non-null assertions](https://www.typescriptlang.org/docs/handbook/2/everyday-types.html#non-null-assertion-operator-postfix-) explicit - Updates `URLResource` and `ItemType` types (and renames `Item` → `ItemType` to disambiguate from the `Item` class) - Removes `node-sass` and replaces it with `sass-embedded` for much quicker installs and compilation --- .eslintrc.js | 106 ++- build/notarize.js | 3 +- index.js | 1 - package.json | 20 +- scripts/build-if-not-git-repo.js | 3 +- src/appmain.ts | 25 +- src/electron-preload.ts | 32 +- src/electron-replay-app.ts | 12 +- src/embed-receipt.ts | 4 +- src/embed.ts | 5 +- src/globals.d.ts | 46 ++ src/index.d.ts | 21 - src/{ui.ts => index.ts} | 20 +- src/item-index.ts | 45 +- src/item-info.ts | 46 +- src/item.ts | 77 +- src/loader.ts | 134 ++-- src/misc.ts | 7 +- src/pageentry.ts | 38 +- src/pages.ts | 180 ++--- src/pageutils.ts | 22 +- src/replay.ts | 28 +- src/sorter.ts | 55 +- src/story.ts | 152 ++-- src/sw.js | 29 - src/types.ts | 28 +- src/url-resources.ts | 11 +- src/utils/assertNonNullish.ts | 4 +- tsconfig.eslint.json | 8 + tsconfig.json | 5 +- webpack.config.js | 3 +- yarn.lock | 1278 +++++++----------------------- 32 files changed, 898 insertions(+), 1550 deletions(-) delete mode 100644 index.js create mode 100644 src/globals.d.ts delete mode 100644 src/index.d.ts rename src/{ui.ts => index.ts} (68%) delete mode 100644 src/sw.js create mode 100644 tsconfig.eslint.json diff --git a/.eslintrc.js b/.eslintrc.js index e3a0c20..cc79053 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -1,32 +1,110 @@ /* eslint-env node */ -/** @type { import("eslint").Linter.Config } */ +/** @type {import('eslint').Linter.Config} */ module.exports = { + parser: "@typescript-eslint/parser", env: { browser: true, - es6: true, + commonjs: true, + es2017: true, }, extends: [ "eslint:recommended", + "plugin:@typescript-eslint/eslint-recommended", + "plugin:wc/recommended", "plugin:lit/recommended", "prettier", - "plugin:@typescript-eslint/recommended", ], plugins: ["@typescript-eslint", "lit"], - parser: "@typescript-eslint/parser", parserOptions: { - ecmaVersion: 2018, - sourceType: "module", + project: ["./tsconfig.eslint.json"], + tsconfigRootDir: __dirname, }, + root: true, rules: { + /* start stylistic rules */ + "@typescript-eslint/adjacent-overload-signatures": "error", + "@typescript-eslint/array-type": "error", + "@typescript-eslint/consistent-type-imports": [ + "error", + { + fixStyle: "inline-type-imports", + }, + ], + "@typescript-eslint/prefer-readonly": "warn", + "@typescript-eslint/class-literal-property-style": ["warn", "getters"], + "@typescript-eslint/consistent-generic-constructors": "error", + "@typescript-eslint/consistent-type-assertions": "error", + "@typescript-eslint/no-confusing-non-null-assertion": "warn", + "@typescript-eslint/no-inferrable-types": "warn", + "@typescript-eslint/non-nullable-type-assertion-style": "warn", + "@typescript-eslint/prefer-for-of": "warn", + // "@typescript-eslint/prefer-nullish-coalescing": "warn", + "@typescript-eslint/prefer-optional-chain": "warn", + "@typescript-eslint/prefer-string-starts-ends-with": "error", + /* end stylistic rules */ + + /* start recommended rules */ "no-restricted-globals": [2, "event", "error"], - "linebreak-style": ["error", "unix"], + "@typescript-eslint/no-base-to-string": "warn", + "@typescript-eslint/no-duplicate-enum-values": "error", + "@typescript-eslint/no-duplicate-type-constituents": "warn", + "@typescript-eslint/no-explicit-any": "warn", + "@typescript-eslint/no-extra-non-null-assertion": "error", + // "@typescript-eslint/no-floating-promises": "warn", + "@typescript-eslint/no-for-in-array": "warn", + "no-unused-vars": "off", + "@typescript-eslint/no-unused-vars": [ + "warn", + { + argsIgnorePattern: "^_", + varsIgnorePattern: "^_", + destructuredArrayIgnorePattern: "^_", + }, + ], + "no-implied-eval": "off", + "@typescript-eslint/no-implied-eval": "error", + "no-loss-of-precision": "off", + "@typescript-eslint/no-loss-of-precision": "warn", + "@typescript-eslint/no-misused-new": "error", + "@typescript-eslint/no-misused-promises": [ + "error", + { checksVoidReturn: { arguments: false } }, + ], + "@typescript-eslint/no-non-null-asserted-nullish-coalescing": "error", + "@typescript-eslint/no-non-null-asserted-optional-chain": "warn", + "@typescript-eslint/no-redundant-type-constituents": "warn", + "@typescript-eslint/no-this-alias": "warn", + "@typescript-eslint/no-unnecessary-type-assertion": "warn", + "@typescript-eslint/no-unnecessary-type-constraint": "warn", + // "@typescript-eslint/no-unsafe-argument": "warn", + // "@typescript-eslint/no-unsafe-assignment": "warn", + // "@typescript-eslint/no-unsafe-call": "warn", + "@typescript-eslint/no-unsafe-declaration-merging": "warn", + "@typescript-eslint/no-unsafe-enum-comparison": "warn", + // "@typescript-eslint/no-unsafe-member-access": "warn", + // "@typescript-eslint/no-unsafe-return": "warn", + "@typescript-eslint/prefer-as-const": "warn", + "require-await": "off", + "@typescript-eslint/require-await": "warn", + "@typescript-eslint/restrict-template-expressions": "warn", + "@typescript-eslint/unbound-method": "off", }, - ignorePatterns: [ - "ruffle/**/*", - "build/**/*", - "/sw.js", - "/ui.js", - "dist/**/*", - ], reportUnusedDisableDirectives: true, + ignorePatterns: [ + "__generated__", + "__mocks__", + "dist", + "ruffle", + "ui.js", + "sw.js", + ], + overrides: [ + { + extends: ["plugin:@typescript-eslint/disable-type-checked"], + files: ["webpack.*.js"], + rules: { + "@typescript-eslint/no-var-requires": "off", + }, + }, + ], }; diff --git a/build/notarize.js b/build/notarize.js index 63d19fc..fcf8c38 100644 --- a/build/notarize.js +++ b/build/notarize.js @@ -1,7 +1,8 @@ +/*eslint-env node */ const { notarize } = require('electron-notarize'); exports.default = async function notarizing(context) { - const { electronPlatformName, appOutDir } = context; + const { electronPlatformName, appOutDir } = context; if (electronPlatformName !== 'darwin') { return; } diff --git a/index.js b/index.js deleted file mode 100644 index 7fbfc60..0000000 --- a/index.js +++ /dev/null @@ -1 +0,0 @@ -export * from "./ui.js"; diff --git a/package.json b/package.json index 2c576c6..bb9a76d 100644 --- a/package.json +++ b/package.json @@ -7,6 +7,8 @@ "homepage": "https://replayweb.page/", "author": "Webrecorder Software", "license": "AGPL-3.0-or-later", + "main": "ui.js", + "types": "dist/types/index.d.ts", "dependencies": { "@fortawesome/fontawesome-free": "^5.15.4", "@shoelace-style/shoelace": "^2.8.0", @@ -29,26 +31,27 @@ "stream-browserify": "^3.0.0" }, "devDependencies": { - "@types/flexsearch": "^0.7.3", - "@typescript-eslint/eslint-plugin": "^6.9.1", - "@typescript-eslint/parser": "^6.9.1", "@playwright/test": "^1.38.1", + "@types/flexsearch": "^0.7.3", + "@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": "^25.1.1", "electron-builder": "^23.0.3", "electron-notarize": "^1.1.1", - "eslint": "^8.52.0", - "eslint-config-prettier": "^9.0.0", - "eslint-plugin-lit": "^1.9.1", + "eslint": "^8.54.0", + "eslint-config-prettier": "^8.3.0", + "eslint-plugin-lit": "^1.10.1", + "eslint-plugin-wc": "^1.3.2", "http-server": "^13.0.2", "husky": "^8.0.0", "lint-staged": "^14.0.0", "mini-css-extract-plugin": "^2.3.0", - "node-sass": "^9.0.0", "playwright": "^1.38.1", "prettier": "^3.0.1", "raw-loader": "^4.0.2", + "sass-embedded": "^1.70.0", "sass-loader": "^13.3.2", "style-loader": "^3.2.1", "svg-inline-loader": "^0.8.2", @@ -62,11 +65,11 @@ }, "files": [ "/assets", + "/dist", "/src", "/ruffle", "sw.js", "ui.js", - "index.js", "index.html" ], "scripts": { @@ -102,7 +105,6 @@ "dist/*.js", "dist/prebuilds/${platform}-${arch}/*", "dist/build/*", - "index.js", "ui.js", "sw.js", "*.html", diff --git a/scripts/build-if-not-git-repo.js b/scripts/build-if-not-git-repo.js index 4d6d998..b54bd04 100644 --- a/scripts/build-if-not-git-repo.js +++ b/scripts/build-if-not-git-repo.js @@ -1,4 +1,3 @@ -/* eslint-disable @typescript-eslint/no-var-requires */ /* eslint-env node */ // Runs a webpack build when not installed in a git repository (i.e. when installed from GitHub in node_modules) @@ -26,7 +25,7 @@ const git = (args) => cp.spawnSync("git", args, { stdio: "inherit" }); // Ensure that we're inside a Git repository // If git command is not found, status is null and we should return // That's why status value needs to be checked explicitly -if (git(["rev-parse"]).status !== 0) { +if (git(["rev-parse"]).status !== 0 || process.argv.includes("--force")) { l(`git command not found, running build`); const compiler = webpack(config.map((c) => c())); diff --git a/src/appmain.ts b/src/appmain.ts index f3fbe94..f16d26f 100644 --- a/src/appmain.ts +++ b/src/appmain.ts @@ -2,6 +2,7 @@ import { LitElement, html, css, + type CSSResultGroup, type TemplateResult, type PropertyValues, } from "lit"; @@ -22,7 +23,7 @@ import { SWManager } from "./swmanager"; import "./item"; import "./item-index"; import "./chooser"; -import { LoadInfo } from "./item"; +import { type LoadInfo } from "./item"; // =========================================================================== @customElement("replay-app-main") @@ -69,10 +70,10 @@ export class ReplayWebApp extends LitElement { @property({ type: Boolean }) skipRuffle = false; - @property({ type: Object }) - swErrorMsg: TemplateResult<1> | "" | null = null; + @property({ type: String }) + swErrorMsg: TemplateResult<1> | string | null = null; - private swName?: string; + protected swName?: string; private swmanager: SWManager | null; private useRuffle = false; @@ -112,7 +113,7 @@ export class ReplayWebApp extends LitElement { }); } - private maybeStartFileDrop = (dragEvent: DragEvent) => { + private readonly maybeStartFileDrop = (dragEvent: DragEvent) => { if (this.sourceUrl) { // A source is already loaded. Don't allow dropping a file. return; @@ -133,7 +134,7 @@ export class ReplayWebApp extends LitElement { return wrapCss(ReplayWebApp.appStyles); } - static get appStyles() { + static get appStyles(): CSSResultGroup { return css` #wrlogo { max-height: 1rem; @@ -237,7 +238,7 @@ export class ReplayWebApp extends LitElement { } get mainLogo() { - return rwpLogo; + return rwpLogo as string; } renderNavBrand() { @@ -499,7 +500,7 @@ export class ReplayWebApp extends LitElement { // This is a workaround, since this app's routing doesn't permit normal // following of in-page anchors. event.preventDefault(); - (this.renderRoot.querySelector("#skip-main-target") as HTMLElement).focus(); + this.renderRoot.querySelector("#skip-main-target")?.focus(); } onNavMenu(event) { @@ -516,9 +517,7 @@ export class ReplayWebApp extends LitElement { (event) => { event.preventDefault(); this.navMenuShown = false; - ( - this.renderRoot.querySelector("#menu-button") as HTMLElement - ).focus(); + this.renderRoot.querySelector("#menu-button")?.focus(); }, { once: true }, ); @@ -528,9 +527,7 @@ export class ReplayWebApp extends LitElement { if (event.key == "Escape") { event.preventDefault(); this.navMenuShown = false; - ( - this.renderRoot.querySelector("#menu-button") as HTMLElement - ).focus(); + this.renderRoot.querySelector("#menu-button")?.focus(); } }, { once: true }, diff --git a/src/electron-preload.ts b/src/electron-preload.ts index 2ab285d..7c3e18c 100644 --- a/src/electron-preload.ts +++ b/src/electron-preload.ts @@ -1,8 +1,8 @@ /*eslint-env node */ import { CollectionLoader } from "@webrecorder/wabac/src/loaders"; +import { type IpcRendererEvent } from "electron"; -// eslint-disable-next-line @typescript-eslint/no-var-requires const { ipcRenderer, contextBridge } = require("electron"); contextBridge.exposeInMainWorld("electron", { IS_APP: true }); @@ -11,12 +11,7 @@ const dbs = {}; const loader = new CollectionLoader(); -/** - * - * @param {string} name - * @returns - */ -async function getColl(name) { +async function getColl(name: string) { if (!dbs[name]) { dbs[name] = await loader.loadColl(name); await dbs[name].initing; @@ -25,25 +20,18 @@ async function getColl(name) { return dbs[name]; } -/** - * @param {string} name - * @returns - */ -async function getDB(name) { +async function getDB(name: string) { const coll = await getColl(name); return coll.store; } -/** - * - * @param {import("electron").IpcRendererEvent} event - * @param {*} request - * @param {*} coll - * @param {*} ts - * @param {*} channel - * @returns - */ -async function getResponse(event, request, coll, ts, channel) { +async function getResponse( + event: IpcRendererEvent, + request: Request, + coll: string, + ts: string, + channel: string, +): Promise { const db = await getDB(coll); const req = { request, url: request.url, timestamp: ts }; diff --git a/src/electron-replay-app.ts b/src/electron-replay-app.ts index 3e4d584..22b6039 100644 --- a/src/electron-replay-app.ts +++ b/src/electron-replay-app.ts @@ -250,7 +250,14 @@ class ElectronReplayApp { return rv; } - async doIntercept(request, callback) { + async doIntercept( + request, + callback: (response: { + statusCode: number; + headers: Record; + data: fs.ReadStream; + }) => void, + ) { console.log(`${request.method} ${request.url} from ${request.referrer}`); // if local server @@ -303,7 +310,7 @@ class ElectronReplayApp { } // possible 'live leak' attempt, return archived version, if any - if (request.referrer && request.referrer.startsWith(REPLAY_PREFIX)) { + if (request.referrer?.startsWith(REPLAY_PREFIX)) { return await this.resolveArchiveResponse(request, callback); } @@ -483,7 +490,6 @@ class ElectronReplayApp { } getOpenUrl(argv) { - // eslint-disable-next-line @typescript-eslint/no-var-requires argv = require("minimist")(argv.slice(process.defaultApp ? 2 : 1)); const filename = diff --git a/src/embed-receipt.ts b/src/embed-receipt.ts index 28a8442..24ec311 100644 --- a/src/embed-receipt.ts +++ b/src/embed-receipt.ts @@ -10,7 +10,7 @@ import { LitElement, html, css, nothing } from "lit"; import { tsToDate } from "./pageutils"; import prettyBytes from "pretty-bytes"; import { property } from "lit/decorators.js"; -import type { Item as ItemType } from "./types"; +import type { ItemType } from "./types"; // =========================================================================== export class RWPEmbedReceipt extends LitElement { @@ -183,7 +183,7 @@ export class RWPEmbedReceipt extends LitElement { datapackageHash, publicKey, software, - } = this.collInfo?.verify || {}; + } = this.collInfo?.verify ?? {}; const sourceUrl = this.collInfo?.sourceUrl; diff --git a/src/embed.ts b/src/embed.ts index 05be002..b4c9fa2 100644 --- a/src/embed.ts +++ b/src/embed.ts @@ -2,7 +2,7 @@ import { LitElement, html, css, - TemplateResult, + type TemplateResult, type PropertyValues, } from "lit"; import { ifDefined } from "lit/directives/if-defined.js"; @@ -53,7 +53,6 @@ class Embed extends LitElement { @property({ type: String }) newWindowBase = ""; @property({ type: String }) errorMessage: - | "" | TemplateResult<1> | string | undefined; @@ -163,7 +162,7 @@ class Embed extends LitElement { } // if no storage manager or estimate, don't cache - if (!navigator.storage || !navigator.storage.estimate) { + if (!navigator.storage?.estimate) { this.noCache = true; } } diff --git a/src/globals.d.ts b/src/globals.d.ts new file mode 100644 index 0000000..7bd81e5 --- /dev/null +++ b/src/globals.d.ts @@ -0,0 +1,46 @@ +declare module "*.svg"; +declare module "*.html"; +declare module "*.scss"; +declare module "*.sass"; +declare const __SW_NAME__: string; +declare const __HELPER_PROXY__: string; +declare const __GDRIVE_CLIENT_ID__: string; +declare const __VERSION__: string; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +type TODOFixMe = any; + +// showOpenFilePicker doesn't yet exist in Typescript's DOM lib +// see https://developer.mozilla.org/en-US/docs/Web/API/Window/showOpenFilePicker for details +interface Window { + showOpenFilePicker: (options?: { + types: { description: string; accept: Record }[]; + }) => Promise<[FileSystemFileHandle]>; +} + +// This is only supported in Blink (see https://developer.mozilla.org/en-US/docs/Web/API/FileSystemHandle/requestPermission#browser_compatibility), so Typescript leaves it out of their definitions (https://github.com/microsoft/TypeScript-DOM-lib-generator/issues/1393#issuecomment-1483735315) +interface FileSystemHandle { + requestPermission: (options?: { + mode?: "read" | "readwrite"; + }) => Promise; +} + +type GAPIResponse = + | { error: string; access_token: never } + | { error: never; access_token: string }; +interface Window { + gapi: { + load: (method: string, callback: (response: GAPIResponse) => void) => void; + auth2: { + authorize: ( + config: { + client_id: string; + scope: string; + response_type: string; + prompt: unknown; + }, + callback: (response: GAPIResponse) => void, + ) => void; + }; + }; +} diff --git a/src/index.d.ts b/src/index.d.ts deleted file mode 100644 index 4b469dc..0000000 --- a/src/index.d.ts +++ /dev/null @@ -1,21 +0,0 @@ -declare module "*.svg"; -declare module "*.html"; -declare const __SW_NAME__: string; -declare const __HELPER_PROXY__: string; -declare const __GDRIVE_CLIENT_ID__: string; -declare const __VERSION__: string; - -// eslint-disable-next-line @typescript-eslint/no-explicit-any -type TODOFixMe = any; - -// showOpenFilePicker doesn't yet exist in Typescript's DOM lib -// see https://developer.mozilla.org/en-US/docs/Web/API/Window/showOpenFilePicker for details -interface Window { - showOpenFilePicker: (options?: { - types: { description: string; accept: Record }[]; - }) => Promise<[FileSystemFileHandle]>; -} - -interface Window { - gapi: TODOFixMe; -} diff --git a/src/ui.ts b/src/index.ts similarity index 68% rename from src/ui.ts rename to src/index.ts index edfbcdb..2995a80 100644 --- a/src/ui.ts +++ b/src/index.ts @@ -17,9 +17,21 @@ import "./shoelace"; export { ReplayWebApp, Chooser, - ItemIndex as CollIndex, // @todo(2023-11-06) complete rename - ItemInfo as CollInfo, // @todo(2023-11-06) complete rename - Item as Coll, // @todo(2023-11-06) complete rename + ItemIndex, + ItemInfo, + Item, + /** + * @deprecated use {@linkcode ItemIndex} + */ + ItemIndex as CollIndex, + /** + * @deprecated use {@linkcode ItemInfo} + */ + ItemInfo as CollInfo, + /** + * @deprecated use {@linkcode Item} + */ + Item as Coll, Story, GDrive, Loader, @@ -30,3 +42,5 @@ export { URLResources, Embed, }; + +export type { ItemType, URLResource } from "./types"; diff --git a/src/item-index.ts b/src/item-index.ts index 185926d..b11e1ab 100644 --- a/src/item-index.ts +++ b/src/item-index.ts @@ -7,26 +7,26 @@ import fasArrowDown from "@fortawesome/fontawesome-free/svgs/solid/angle-double- import fasSearch from "@fortawesome/fontawesome-free/svgs/solid/search.svg"; -import type { Item } from "./types"; +import type { ItemType } from "./types"; import "./item-info"; // =========================================================================== class ItemIndex extends LitElement { @property({ type: Array }) - items: Item[] = []; + items: ItemType[] = []; @property({ type: String }) query = ""; @property({ type: Array }) - filteredItems: Item[] = []; + filteredItems: ItemType[] = []; @property({ type: Array }) - sortedItems: Item[] = []; + sortedItems: ItemType[] = []; @property({ type: Boolean }) - hideHeader: boolean = false; + hideHeader = false; @property({ type: String }) dateName = "Date Loaded"; @@ -36,10 +36,14 @@ class ItemIndex extends LitElement { @state() // eslint-disable-next-line @typescript-eslint/no-explicit-any -- TODO fixme - private _deleting: any = {}; + protected _deleting: any = {}; - private typeFilter = ""; - private indexParams = ""; + private get typeFilter() { + return ""; + } + private get indexParams() { + return ""; + } constructor() { super(); @@ -84,7 +88,7 @@ class ItemIndex extends LitElement { if ( item.sourceUrl.indexOf(this.query) >= 0 || item.filename.indexOf(this.query) >= 0 || - (item.loadUrl && item.loadUrl.indexOf(this.query) >= 0) || + Boolean(item.loadUrl && item.loadUrl.indexOf(this.query) >= 0) || (item.title && item.title.indexOf(this.query) >= 0) ) { this.filteredItems.push(item); @@ -99,8 +103,8 @@ class ItemIndex extends LitElement { throw new Error("Invalid API Response, Retry"); } const json = await resp.json(); - this.items = json.colls.map((item: Item) => { - item.title = item.title || item.filename; + this.items = json.colls.map((item: ItemType) => { + item.title = item.title ?? item.filename; return item; }); @@ -285,7 +289,10 @@ class ItemIndex extends LitElement { @@ -299,15 +306,19 @@ class ItemIndex extends LitElement { ?sortDesc="${true}" .sortKeys="${this.sortKeys}" .data="${this.filteredItems}" - @sort-changed="${(e) => - (this.sortedItems = e.detail.sortedData)}" + @sort-changed="${( + e: CustomEvent<{ + sortKey: string | null; + sortDesc: boolean | null; + sortedData: ItemType[]; + }>, + ) => (this.sortedItems = e.detail.sortedData)}" >
- ${this.sortedItems && - this.sortedItems.map( + ${this.sortedItems?.map( (item, i) => html`
${this.renderItemInfo(item)} @@ -343,7 +354,7 @@ class ItemIndex extends LitElement { `; } - renderItemInfo(item: Item) { + renderItemInfo(item: ItemType) { return html``; } diff --git a/src/item-info.ts b/src/item-info.ts index e7a9f41..3f46775 100644 --- a/src/item-info.ts +++ b/src/item-info.ts @@ -4,14 +4,14 @@ import { wrapCss } from "./misc"; import { map } from "lit/directives/map.js"; import prettyBytes from "pretty-bytes"; -import type { Item } from "./types"; +import type { ItemType } from "./types"; import "./components/labeled-field"; // =========================================================================== class ItemInfo extends LitElement { @property({ type: Object }) - item!: Item | Record; + item: ItemType | Record | null = null; @property({ type: Boolean }) detailed = false; @@ -90,31 +90,31 @@ class ItemInfo extends LitElement { return html` ${item.sourceUrl && - (item.sourceUrl.startsWith("http://") || - item.sourceUrl.startsWith("https://")) - ? html` ${item.sourceUrl} ` - : html` ${item.sourceUrl}`} - ${item.sourceUrl && item.sourceUrl.startsWith("googledrive://") - ? html` (${item.filename})` + >${item!.sourceUrl && + (item!.sourceUrl.startsWith("http://") || + item!.sourceUrl.startsWith("https://")) + ? html` ${item!.sourceUrl} ` + : html` ${item!.sourceUrl}`} + ${item!.sourceUrl && item!.sourceUrl.startsWith("googledrive://") + ? html` (${item!.filename})` : nothing} ${showItemID ? html` - ${item.coll || "No ID"} + ${item!.coll || "No ID"} ` : nothing} - ${item.ctime ? new Date(item.ctime).toLocaleString() : nothing} + ${item!.ctime ? new Date(item!.ctime).toLocaleString() : nothing} - ${prettyBytes(Number(item.totalSize || item.size || 0))} + ${prettyBytes(Number(item!.totalSize || item!.size || 0))} `; } @@ -125,8 +125,8 @@ class ItemInfo extends LitElement { return html`
- ${item.name || item.title || item.filename}${item!.name || item!.title || item!.filename}
@@ -145,26 +145,26 @@ class ItemInfo extends LitElement { datapackageHash, publicKey, software, - } = this.item.verify || {}; + } = this.item!.verify || {}; const certFingerprintUrl = certFingerprint ? `https://crt.sh/?q=${certFingerprint}` : ""; return html`
- ${item.name || item.title + ${item!.name || item!.title ? html` - ${item.name || item.title} + ${item!.name || item!.title} ` : nothing} - ${item.filename} + ${item!.filename} - ${item.resources + ${item!.resources ? html`
    ${map( - item.resources, + item!.resources, (resource) => html`
  1. ${resource.name + "\n"} @@ -222,7 +222,7 @@ class ItemInfo extends LitElement { - ${item.onDemand ? "Download On-Demand" : "Fully Local"} + ${item!.onDemand ? "Download On-Demand" : "Fully Local"}
`; } diff --git a/src/item.ts b/src/item.ts index ab3d801..834aa8d 100644 --- a/src/item.ts +++ b/src/item.ts @@ -1,4 +1,10 @@ -import { LitElement, html, css, type PropertyValues } from "lit"; +import { + LitElement, + html, + css, + type PropertyValues, + type TemplateResult, +} from "lit"; import { property } from "lit/decorators.js"; import { ref, createRef, type Ref } from "lit/directives/ref.js"; import type { @@ -51,7 +57,7 @@ import fasCaretDown from "@fortawesome/fontawesome-free/svgs/solid/caret-down.sv import { RWPEmbedReceipt } from "./embed-receipt"; import Split from "split.js"; -import type { Item as ItemType } from "./types"; +import type { ItemType, URLResource } from "./types"; import type { Replay } from "./replay"; import { ifDefined } from "lit/directives/if-defined.js"; @@ -76,6 +82,7 @@ export type LoadInfo = { swError?: string; newFullImport?: unknown; name?: string; + importCollId?: string; }; // =========================================================================== @@ -175,14 +182,13 @@ class Item extends LitElement { private observer?: IntersectionObserver; - private archiveInfoDialog: Ref = createRef(); + private readonly archiveInfoDialog: Ref = createRef(); private readonly tabNames = ["pages", "story", "resources", "info"]; private readonly tabLabels = { pages: "Pages", story: "Story", resources: "URLs", - info: "Archive Info", }; constructor() { @@ -214,9 +220,7 @@ class Item extends LitElement { while ( this.editable && this.autoUpdateInterval && - (!this.itemInfo || - !this.itemInfo.pages || - this.itemInfo.pages.length < 100) + (!this.itemInfo?.pages || this.itemInfo.pages.length < 100) ) { await new Promise((resolve) => setTimeout(resolve, this.autoUpdateInterval * 1000), @@ -283,7 +287,7 @@ class Item extends LitElement { } } if (changedProperties.has("tabData")) { - if (!this.itemInfo || !this.itemInfo.coll) { + if (!this.itemInfo?.coll) { return; } const newHash = @@ -308,9 +312,8 @@ class Item extends LitElement { } else { window.location.hash = this._locationHash; if (!this.showSidebar) { - const replay = this.renderRoot.querySelector( - "wr-coll-replay", - ) as Replay; + const replay = + this.renderRoot.querySelector("wr-coll-replay"); if (replay) { replay.focus(); } @@ -338,10 +341,8 @@ class Item extends LitElement { configureSplitter() { if (this.tabData.url && this.showSidebar) { - const contents = this.renderRoot.querySelector( - "#contents", - ) as HTMLElement; - const replay = this.renderRoot.querySelector("wr-coll-replay") as Replay; + const contents = this.renderRoot.querySelector("#contents"); + const replay = this.renderRoot.querySelector("wr-coll-replay"); if (contents && replay && !this.splitter) { const opts = { @@ -378,11 +379,11 @@ class Item extends LitElement { return; } - let item = this.loadInfo && this.loadInfo.customColl; // @todo(2023-11-06) update customColl -> customItem + let item = this.loadInfo?.customColl; // @todo(2023-11-06) update customColl -> customItem if (!item) { - const res = await sourceToId(this.sourceUrl); - item = res.coll; + const res = await sourceToId(this.sourceUrl!); + item = res.item; } this.item = item; @@ -406,11 +407,7 @@ class Item extends LitElement { ...json, }; - if ( - this.loadInfo && - this.loadInfo.extraConfig && - this.loadInfo.extraConfig.headers - ) { + if (this.loadInfo?.extraConfig?.headers) { const headers = this.loadInfo.extraConfig.headers; await fetch(`${itemApiPrefix}/updateAuth`, { method: "POST", @@ -870,10 +867,9 @@ class Item extends LitElement { const isReplay = !!this.tabData.url; const isSidebar = isReplay && this.showSidebar; - if (!isReplay && this.tabData && this.tabData.view) { + if (!isReplay && this.tabData?.view) { const detail = { - // eslint-disable-next-line @typescript-eslint/no-explicit-any -- TODO fixme - title: (this.tabLabels as any)[this.tabData.view], + title: this.tabLabels[this.tabData.view], replayTitle: false, }; this.dispatchEvent( @@ -932,7 +928,7 @@ class Item extends LitElement { ts="${this.tabData.ts || ""}" @coll-tab-nav="${this.onItemTabNav}" id="replay" - @replay-loading="${(e) => + @replay-loading="${(e: CustomEvent<{ loading: boolean }>) => (this.isLoading = e.detail.loading)}" @replay-favicons="${this.onFavIcons}" > @@ -1488,14 +1484,14 @@ class Item extends LitElement { } dragStart() { - const replay = this.renderRoot.querySelector("wr-coll-replay") as Replay; + const replay = this.renderRoot.querySelector("wr-coll-replay"); if (replay) { replay.setDisablePointer(true); } } dragEnd() { - const replay = this.renderRoot.querySelector("wr-coll-replay") as Replay; + const replay = this.renderRoot.querySelector("wr-coll-replay"); if (replay) { replay.setDisablePointer(false); } @@ -1517,7 +1513,7 @@ class Item extends LitElement { >`; } - renderExtraToolbar(/*isDropdown = false*/) { + protected renderExtraToolbar(/*isDropdown = false*/): "" | TemplateResult<1> { return ""; } @@ -1582,9 +1578,7 @@ class Item extends LitElement { // This is a workaround, since this app's routing doesn't permit normal // following of in-page anchors. event.preventDefault(); - ( - this.renderRoot.querySelector("#skip-replay-target") as HTMLElement - ).focus(); + this.renderRoot.querySelector("#skip-replay-target")?.focus(); } onKeyDown(event) { @@ -1702,7 +1696,7 @@ class Item extends LitElement { onSubmit(event) { event.preventDefault(); - const input = this.renderRoot.querySelector("input") as HTMLInputElement; + const input = this.renderRoot.querySelector("input")!; if (input.value) { this.navigateTo(input.value); } else { @@ -1719,12 +1713,12 @@ class Item extends LitElement { onTimestampDropdownBtnBlur(event: MouseEvent) { const btn = event.currentTarget as HTMLButtonElement; - const dropdown = btn.closest("sl-dropdown") as SlDropdown; + const dropdown = btn.closest("sl-dropdown"); const relatedTarget = event.relatedTarget as HTMLElement; - if (relatedTarget && dropdown.contains(relatedTarget)) { + if (relatedTarget && dropdown?.contains(relatedTarget)) { return; } - if (dropdown.open) { + if (dropdown?.open) { dropdown.hide(); } } @@ -1741,9 +1735,7 @@ class Item extends LitElement { data = { url: value }; if (value === this.tabData.url) { - const replay = this.renderRoot.querySelector( - "wr-coll-replay", - ) as Replay; + const replay = this.renderRoot.querySelector("wr-coll-replay"); if (replay) { replay.refresh(); } @@ -1761,8 +1753,7 @@ class Item extends LitElement { _stringToParams(value) { const q = new URLSearchParams(value.slice(RWP_SCHEME.length)); - // eslint-disable-next-line @typescript-eslint/no-explicit-any -- TODO fixme - const data: any = {}; + const data: Partial = {}; data.url = ""; data.ts = ""; @@ -1807,7 +1798,7 @@ class Item extends LitElement { this.menuActive = false; if (this.tabData.url) { - const replay = this.renderRoot.querySelector("wr-coll-replay") as Replay; + const replay = this.renderRoot.querySelector("wr-coll-replay"); if (replay) { replay.refresh(); } diff --git a/src/loader.ts b/src/loader.ts index c8a5fdb..dcb48eb 100644 --- a/src/loader.ts +++ b/src/loader.ts @@ -5,7 +5,7 @@ import prettyBytes from "pretty-bytes"; import { parseURLSchemeHostPath } from "./pageutils"; import { property } from "lit/decorators.js"; -import { LoadInfo } from "./item"; +import type { LoadInfo } from "./item"; // =========================================================================== class Loader extends LitElement { @@ -27,7 +27,8 @@ class Loader extends LitElement { @property({ type: String }) swName?: string; pingInterval: number | NodeJS.Timer = 0; - fileHandle: FileSystemHandle | null = null; + // eslint-disable-next-line @typescript-eslint/no-explicit-any -- requestPermission() type mismatch + fileHandle: any = null; noWebWorker = false; worker?: Worker | null; @@ -54,57 +55,71 @@ class Loader extends LitElement { this.worker = navigator.serviceWorker as unknown as Worker; } - this.worker.addEventListener("message", (event) => { - switch (event.data.msg_type) { - case "collProgress": - if (event.data.name === this.coll) { - this.percent = event.data.percent; - if (event.data.error) { - this.error = event.data.error; - this.state = "errored"; - this.errorAllowRetry = true; - this.fileHandle = event.data.fileHandle; - if (this.error === "missing_local_file") { - this.tryFileHandle = false; - } else if ( - this.error === "permission_needed" && - event.data.fileHandle - ) { - this.state = "permission_needed"; - break; + this.worker.addEventListener( + "message", + ( + event: MessageEvent<{ + msg_type: string; + name: string; + percent: number; + error?: string; + fileHandle: FileSystemHandle | null; + currentSize?: number; + totalSize?: number; + extraMsg?: string; + }>, + ) => { + switch (event.data.msg_type) { + case "collProgress": + if (event.data.name === this.coll) { + this.percent = event.data.percent; + if (event.data.error) { + this.error = event.data.error; + this.state = "errored"; + this.errorAllowRetry = true; + this.fileHandle = event.data.fileHandle; + if (this.error === "missing_local_file") { + this.tryFileHandle = false; + } else if ( + this.error === "permission_needed" && + event.data.fileHandle + ) { + this.state = "permission_needed"; + break; + } } - } - if (event.data.currentSize && event.data.totalSize) { - this.currentSize = event.data.currentSize; - this.totalSize = event.data.totalSize; - } - this.extraMsg = event.data.extraMsg; - } - break; - - case "collAdded": - if (event.data.name === this.coll) { - if (!this.total) { - this.total = 100; - } - this.progress = this.total; - this.percent = 100; - this.dispatchEvent( - new CustomEvent("coll-loaded", { detail: event.data }), - ); - - if (!this.noWebWorker) { - this.worker?.terminate(); - } else { - if (this.pingInterval) { - clearInterval(this.pingInterval); + if (event.data.currentSize && event.data.totalSize) { + this.currentSize = event.data.currentSize; + this.totalSize = event.data.totalSize; } + this.extraMsg = event.data.extraMsg; } - this.worker = null; - } - break; - } - }); + break; + + case "collAdded": + if (event.data.name === this.coll) { + if (!this.total) { + this.total = 100; + } + this.progress = this.total; + this.percent = 100; + this.dispatchEvent( + new CustomEvent("coll-loaded", { detail: event.data }), + ); + + if (!this.noWebWorker) { + this.worker?.terminate(); + } else { + if (this.pingInterval) { + clearInterval(this.pingInterval); + } + } + this.worker = null; + } + break; + } + }, + ); } async doLoad() { @@ -115,7 +130,7 @@ class Loader extends LitElement { // const noSWError = getSWErrorMsg(); - if (this.loadInfo && this.loadInfo.swError) { + if (this.loadInfo?.swError) { this.state = "errored"; this.error = this.loadInfo.swError; this.errorAllowRetry = false; @@ -124,7 +139,7 @@ class Loader extends LitElement { // custom protocol handlers here... try { - const { scheme, host, path } = parseURLSchemeHostPath(sourceUrl); + const { scheme, host, path } = parseURLSchemeHostPath(sourceUrl!); switch (scheme) { case "googledrive": @@ -179,11 +194,7 @@ You can select a file to upload from the main page by clicking the 'Choose File. extraConfig = this.loadInfo.extraConfig; } // todo: too special case? - if ( - sourceUrl!.startsWith("proxy:") && - extraConfig && - extraConfig.recording - ) { + if (sourceUrl!.startsWith("proxy:") && extraConfig?.recording) { // @ts-expect-error - TS2322 - Type '"recordingproxy"' is not assignable to type 'undefined'. type = "recordingproxy"; } @@ -229,7 +240,7 @@ You can select a file to upload from the main page by clicking the 'Choose File. return this._gdWait; } - async onLoadReady(event) { + onLoadReady(event: CustomEvent) { if (this._gdResolve) { //const digest = await digestMessage(url, 'SHA-256'); //this.coll = "id-" + digest.slice(0, 12); @@ -250,7 +261,7 @@ You can select a file to upload from the main page by clicking the 'Choose File. return; } - if (navigator.serviceWorker && navigator.serviceWorker.controller) { + if (navigator.serviceWorker?.controller) { navigator.serviceWorker.controller.postMessage(msg); if (this.pingInterval) { clearInterval(this.pingInterval); @@ -260,7 +271,7 @@ You can select a file to upload from the main page by clicking the 'Choose File. updated(changedProperties: PropertyValues) { if ( - (this.sourceUrl && changedProperties.has("sourceUrl")) || + Boolean(this.sourceUrl && changedProperties.has("sourceUrl")) || changedProperties.has("tryFileHandle") ) { this.doLoad(); @@ -410,8 +421,7 @@ You can select a file to upload from the main page by clicking the 'Choose File. } async onAskPermission() { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const result = await (this.fileHandle as any)?.requestPermission({ + const result = await this.fileHandle?.requestPermission({ mode: "read", }); if (result === "granted") { diff --git a/src/misc.ts b/src/misc.ts index 8ed8ae3..56b9fc1 100644 --- a/src/misc.ts +++ b/src/misc.ts @@ -1,10 +1,9 @@ "use strict"; -import { LitElement, html, css, unsafeCSS } from "lit"; +import { LitElement, html, css, unsafeCSS, type CSSResultGroup } from "lit"; import { unsafeSVG } from "lit/directives/unsafe-svg.js"; import { styleMap } from "lit/directives/style-map.js"; -// @ts-expect-error - TS2307 - Cannot find module '../assets/main.scss' or its corresponding type declarations. import allCssRaw from "../assets/main.scss"; import rwpLogo from "../assets/logo.svg"; @@ -14,7 +13,7 @@ const replayPrefix = "./w"; // =========================================================================== const allCss = unsafeCSS(allCssRaw); -function wrapCss(custom) { +function wrapCss(custom: CSSResultGroup): CSSResultGroup { return [allCss, custom]; } @@ -22,7 +21,7 @@ const IS_APP = // @ts-expect-error - TS2339 - Property 'IS_APP' does not exist on type 'Window & typeof globalThis'. window.IS_APP || // @ts-expect-error - TS2551 - Property 'electron' does not exist on type 'Window & typeof globalThis'. Did you mean 'Electron'? | TS2551 - Property 'electron' does not exist on type 'Window & typeof globalThis'. Did you mean 'Electron'? - (window.electron && window.electron.IS_APP) || + window.electron?.IS_APP || window.matchMedia("(display-mode: standalone)").matches; const VERSION = __VERSION__; diff --git a/src/pageentry.ts b/src/pageentry.ts index b0ece99..03be52c 100644 --- a/src/pageentry.ts +++ b/src/pageentry.ts @@ -8,6 +8,7 @@ import "keyword-mark-element/lib/keyword-mark.js"; import { getReplayLink } from "./pageutils"; import { wrapCss } from "./misc"; +import type { URLResource } from "./types"; // =========================================================================== class PageEntry extends LitElement { @@ -18,8 +19,7 @@ class PageEntry extends LitElement { textSnippet: string | null = ""; @property({ type: Object }) - // eslint-disable-next-line @typescript-eslint/no-explicit-any -- TODO fixme - page: any = null; + page: URLResource | null = null; @property({ type: String }) replayPrefix = ""; @@ -191,8 +191,8 @@ class PageEntry extends LitElement { } render() { - const p = this.page; - const date = this.page.date; + const p = this.page!; + const date = this.page!.date; const hasSize = typeof p.size === "number"; @@ -231,8 +231,8 @@ class PageEntry extends LitElement { @click="${this.onReplay}" href="${getReplayLink( "pages", - this.page.url, - this.page.timestamp, + this.page!.url, + this.page!.timestamp!, )}" >
- ${this.collInfo.description + ${this.collInfo!.description ? html`
- ${this.collInfo.description} + ${this.collInfo!.description}
` : html``}`} ${this.editable @@ -638,8 +633,10 @@ class Pages extends LitElement { ? html`
` : ""} - ${this.collInfo.lists.length + ${this.collInfo!.lists.length ? html`