webextension-store-meta
Version:
Get browser extension(webextension) item meta from Chrome Web Store, Firefox add-ons, and Microsoft Edge Add-ons.
473 lines (394 loc) • 12.4 kB
text/typescript
import { parse, stringify } from "node:querystring";
import { DomHandler, type Node } from "domhandler";
import { Parser } from "htmlparser2/lib/Parser";
import type { RequestInit } from "undici";
import { findOne, getText } from "../utils/dom";
import { fetchText } from "../utils/fetch-text";
import { isPlainObject } from "../utils/parse";
export type { RequestInit };
export interface EdgeAddonsImage {
caption?: string;
imagePurpose?: string;
uri?: string;
[key: string]: unknown;
}
export interface EdgeAddonsPrivacyData {
privacyPolicyRequired?: boolean;
dataUsageList?: unknown[];
disclosureList?: unknown[];
[key: string]: unknown;
}
export interface EdgeAddonsMeta {
availability: string[] | null;
activeInstallCount: number | null;
storeProductId: string | null;
name: string | null;
logoUrl: string | null;
thumbnailUrl: string | null;
description: string | null;
developer: string | null;
category: string | null;
isInstalled: boolean | null;
crxId: string | null;
manifest: string | null;
isHavingMatureContent: boolean | null;
version: string | null;
lastUpdateDate: number | null;
privacyUrl: string | null;
availabilityId: string | null;
skuId: string | null;
locale: string | null;
market: string | null;
averageRating: number | null;
ratingCount: number | null;
availableLanguages: string[] | null;
metadata: Record<string, unknown> | null;
shortDescription: string | null;
searchKeywords: string | null;
screenshots: EdgeAddonsImage[] | null;
videos: EdgeAddonsImage[] | null;
largePromotionImage: EdgeAddonsImage | null;
publisherWebsiteUri: string | null;
isBadgedAsFeatured: boolean | null;
privacyData: EdgeAddonsPrivacyData | null;
url: string | null;
}
export interface EdgeAddonsOptions {
/**
* Microsoft Edge Add-ons extension ID
* @example "cnlefmmeadmemmdciolhbnfeacpdfbkd"
*/
id: string;
/**
* `undici.fetch` options
* @see {@link https://undici.nodejs.org/#/?id=undicifetchinput-init-promise}
*/
options?: RequestInit;
/**
* Query string
*/
qs?: Record<string, string> | string;
}
type EdgeAddonsApiData = Partial<
Omit<EdgeAddonsMeta, "url" | "logoUrl" | "thumbnailUrl">
> & {
logoUrl?: string | null;
thumbnailUrl?: string | null;
};
export class EdgeAddons {
public id: string;
public options?: RequestInit;
public qs: string;
private _data: EdgeAddonsApiData | null = null;
private _dom: Node[] | null = null;
private _url: string | null = null;
private _cache = new Map<keyof EdgeAddonsMeta, unknown>();
public constructor({ id, options, qs }: EdgeAddonsOptions) {
this.id = id;
this.options = options;
this.qs = stringify(
typeof qs === "string" ? parse(qs.replace(/^\?/, "")) : qs || {},
);
}
static async load(options: EdgeAddonsOptions): Promise<EdgeAddons> {
const instance = new EdgeAddons(options);
await instance.load();
return instance;
}
async load(): Promise<EdgeAddons> {
const apiUrl = `https://microsoftedge.microsoft.com/addons/getproductdetailsbycrxid/${this.id}${this.queryString}`;
try {
this._data = JSON.parse(await fetchText(apiUrl, this.options));
this._url = this.detailUrl;
this._dom = [];
return this;
} catch (_) {
this._data = {};
}
try {
const html = await fetchText(
`${this.detailUrl}${this.queryString}`,
this.options,
);
const handler = new DomHandler();
new Parser(handler).end(html);
this._dom = handler.dom;
this._url = this.detailUrl;
} catch (_) {
this._dom = [];
this._url = null;
}
return this;
}
meta(): EdgeAddonsMeta {
return {
availability: this.availability(),
activeInstallCount: this.activeInstallCount(),
storeProductId: this.storeProductId(),
name: this.name(),
logoUrl: this.logoUrl(),
thumbnailUrl: this.thumbnailUrl(),
description: this.description(),
developer: this.developer(),
category: this.category(),
isInstalled: this.isInstalled(),
crxId: this.crxId(),
manifest: this.manifest(),
isHavingMatureContent: this.isHavingMatureContent(),
version: this.version(),
lastUpdateDate: this.lastUpdateDate(),
privacyUrl: this.privacyUrl(),
availabilityId: this.availabilityId(),
skuId: this.skuId(),
locale: this.locale(),
market: this.market(),
averageRating: this.averageRating(),
ratingCount: this.ratingCount(),
availableLanguages: this.availableLanguages(),
metadata: this.metadata(),
shortDescription: this.shortDescription(),
searchKeywords: this.searchKeywords(),
screenshots: this.screenshots(),
videos: this.videos(),
largePromotionImage: this.largePromotionImage(),
publisherWebsiteUri: this.publisherWebsiteUri(),
isBadgedAsFeatured: this.isBadgedAsFeatured(),
privacyData: this.privacyData(),
url: this.url(),
};
}
public availability(): string[] | null {
return this._array("availability");
}
public activeInstallCount(): number | null {
return this._number("activeInstallCount", () =>
this._parseMetaNumber("userInteractionCount"),
);
}
public storeProductId(): string | null {
return this._string("storeProductId");
}
public name(): string | null {
return this._string("name", () => this._title());
}
public logoUrl(): string | null {
return this._urlString("logoUrl");
}
public thumbnailUrl(): string | null {
return this._urlString("thumbnailUrl");
}
public description(): string | null {
return this._string("description");
}
public developer(): string | null {
return this._string("developer");
}
public category(): string | null {
return this._string("category");
}
public isInstalled(): boolean | null {
return this._boolean("isInstalled");
}
public crxId(): string | null {
return this._string("crxId");
}
public manifest(): string | null {
return this._string("manifest");
}
public isHavingMatureContent(): boolean | null {
return this._boolean("isHavingMatureContent");
}
public version(): string | null {
return this._string("version");
}
public lastUpdateDate(): number | null {
return this._number("lastUpdateDate");
}
public privacyUrl(): string | null {
return this._string("privacyUrl");
}
public availabilityId(): string | null {
return this._string("availabilityId");
}
public skuId(): string | null {
return this._string("skuId");
}
public locale(): string | null {
return this._string("locale");
}
public market(): string | null {
return this._string("market");
}
public averageRating(): number | null {
return this._number("averageRating", () =>
this._parseMetaNumber("ratingValue"),
);
}
public ratingCount(): number | null {
return this._number("ratingCount", () =>
this._parseMetaNumber("ratingCount"),
);
}
public availableLanguages(): string[] | null {
return this._array("availableLanguages");
}
public metadata(): Record<string, unknown> | null {
return this._object("metadata");
}
public shortDescription(): string | null {
return this._string("shortDescription");
}
public searchKeywords(): string | null {
return this._string("searchKeywords");
}
public screenshots(): EdgeAddonsImage[] | null {
return this._images("screenshots");
}
public videos(): EdgeAddonsImage[] | null {
return this._images("videos");
}
public largePromotionImage(): EdgeAddonsImage | null {
const value = this._value("largePromotionImage");
if (!isPlainObject(value)) return null;
return normalizeImage(value);
}
public publisherWebsiteUri(): string | null {
return this._string("publisherWebsiteUri");
}
public isBadgedAsFeatured(): boolean | null {
return this._boolean("isBadgedAsFeatured");
}
public privacyData(): EdgeAddonsPrivacyData | null {
return this._object("privacyData") as EdgeAddonsPrivacyData | null;
}
public url(): string | null {
if (!this._cache.has("url")) {
this._cache.set("url", this._url);
}
return (this._cache.get("url") as string | null) ?? null;
}
private get queryString(): string {
return this.qs ? `?${this.qs}` : "";
}
private get detailUrl(): string {
return `https://microsoftedge.microsoft.com/addons/detail/${this.id}`;
}
private _value(key: keyof EdgeAddonsMeta): unknown {
if (!this._data) {
throw new Error(
"Item not loaded. Please run `await instance.load()` first.`",
);
}
return this._data[key as keyof EdgeAddonsApiData] ?? null;
}
private _string(
key: keyof EdgeAddonsMeta,
fallback?: () => string | null,
): string | null {
if (!this._cache.has(key)) {
const value = this._value(key);
this._cache.set(
key,
typeof value === "string" && value !== ""
? value
: (fallback?.() ?? null),
);
}
return (this._cache.get(key) as string | null) ?? null;
}
private _urlString(key: "logoUrl" | "thumbnailUrl"): string | null {
if (!this._cache.has(key)) {
const value = this._value(key);
this._cache.set(
key,
typeof value === "string" && value !== "" ? normalizeUrl(value) : null,
);
}
return (this._cache.get(key) as string | null) ?? null;
}
private _number(
key: keyof EdgeAddonsMeta,
fallback?: () => number | null,
): number | null {
if (!this._cache.has(key)) {
const value = this._value(key);
this._cache.set(
key,
typeof value === "number" ? value : (fallback?.() ?? null),
);
}
return (this._cache.get(key) as number | null) ?? null;
}
private _boolean(key: keyof EdgeAddonsMeta): boolean | null {
if (!this._cache.has(key)) {
const value = this._value(key);
this._cache.set(key, typeof value === "boolean" ? value : null);
}
return (this._cache.get(key) as boolean | null) ?? null;
}
private _array(key: keyof EdgeAddonsMeta): string[] | null {
if (!this._cache.has(key)) {
const value = this._value(key);
this._cache.set(
key,
Array.isArray(value) && value.every((item) => typeof item === "string")
? value
: null,
);
}
return (this._cache.get(key) as string[] | null) ?? null;
}
private _images(key: "screenshots" | "videos"): EdgeAddonsImage[] | null {
if (!this._cache.has(key)) {
const value = this._value(key);
this._cache.set(
key,
Array.isArray(value)
? value.filter(isPlainObject).map(normalizeImage)
: null,
);
}
return (this._cache.get(key) as EdgeAddonsImage[] | null) ?? null;
}
private _object(key: keyof EdgeAddonsMeta): Record<string, unknown> | null {
if (!this._cache.has(key)) {
const value = this._value(key);
this._cache.set(key, isPlainObject(value) ? value : null);
}
return (this._cache.get(key) as Record<string, unknown> | null) ?? null;
}
private _title(): string | null {
const title = getText(findOne((el) => el.name === "title", this.dom));
return title.replace(/\s*-\s*Microsoft Edge Add-ons\s*$/, "") || null;
}
private _parseMetaNumber(itemprop: string): number | null {
const value = findOne(
(el) =>
el.name === "meta" &&
el.attribs.itemprop?.toLowerCase() === itemprop.toLowerCase(),
this.dom,
)?.attribs.content;
if (!value) return null;
const number = Number(value.replace(/,/g, ""));
return Number.isNaN(number) ? null : number;
}
private get dom() {
if (!this._dom) {
throw new Error(
"Item not loaded. Please run `await instance.load()` first.`",
);
}
return this._dom;
}
}
export default EdgeAddons;
function normalizeUrl(url: string): string {
return url.startsWith("//") ? `https:${url}` : url;
}
function normalizeImage(image: Record<string, unknown>): EdgeAddonsImage {
const result: EdgeAddonsImage = { ...image };
if (typeof image.uri === "string") {
result.uri = normalizeUrl(image.uri);
}
return result;
}