import validator from "validator"; import type { GeneratorOptions, RunAtOption } from "../generate.js"; import { uniquify } from "../utils/common.js"; import type { PackageInfo } from "../utils/package.js"; import type { NetworkSiteInfo } from "../utils/scraper.js"; import type { RequiredProps } from "../utils/types.js"; import { explodePaths } from "../utils/urls.js"; import type { CommonHeaders } from "./common/index.js"; import type { GreasemonkeyGrantOptions } from "./greasemonkey/types.js"; import type { TampermonkeyGrantOptions } from "./tampermonkey/types.js"; import type { ViolentmonkeyGrantOptions } from "./violentmonkey/types.js"; export type GrantOptions = | GreasemonkeyGrantOptions | TampermonkeyGrantOptions | ViolentmonkeyGrantOptions | "all"; export type UserScriptManagerName = | "tampermonkey" | "violentmonkey" | "greasemonkey"; export type HeaderGenerator = ( info: PackageInfo, options: RequiredProps, "spaces"> ) => Promise; export type HeaderEntry = [keyof T & string, string]; export type HeaderEntries = HeaderEntry[]; /** * @summary abstract '@grant' header generator */ export const generateGrantHeaders = < T extends CommonHeaders, U extends GrantOptions, >( grantMap: Record, grants: U[], ) => { if (grants.find(g => g === "all")) { return Object.entries(grantMap).map(([, v]) => [ "grant", v, ]) as HeaderEntries; } const headers: HeaderEntries = grants.map(g => ["grant", grantMap[g]]); return headers.length ? headers : ([["grant", "none"]] as HeaderEntries>); }; /** * @summary abstract '@exclude' header generator * @param excludes list of patterns to exclude */ export const generateExcludeHeaders = ( excludes: string[], ): HeaderEntries => { return excludes.flatMap(explodePaths).map(uri => ["exclude", uri]); }; /** * @summary abstract '@exclude-match' header generator * @param excludes list of patterns to exclude */ export const generateExcludeMatchHeaders = ( excludes: string[], ): HeaderEntries => { return excludes.flatMap(explodePaths).map(uri => ["exclude-match", uri]); }; /** * @summary abstract '@match' header generator */ export const generateMatchHeaders = async ( matches: string[], networkSiteScraper: () => Promise, collapse = true, ): Promise> => { if (matches.includes("all")) { const match = matches.find(m => m.includes("domain")) || "https://domain/*"; const sites = (await networkSiteScraper()) // only inclue meta sites if requested .filter(({ isMeta }) => matches.includes("meta") || !isMeta); const all = sites.map(({ site }) => { const domain = collapse && site.includes("stackexchange") ? "*.stackexchange.com" : site; return match.replace("domain", domain); }); return generateMatchHeaders(uniquify(all), networkSiteScraper); } return matches.flatMap(explodePaths).map(uri => ["match", uri]); }; /** * @summary abstract '@run-at' header generator */ export const generateRunAtHeaders = ( runAtMap: { [P in RunAtOption]?: T["run-at"] } & Record, runAt: T["run-at"], ) => { const runsAt = runAtMap[runAt]; return runsAt ? ([["run-at", runsAt]] as HeaderEntries>) : []; }; /** * @summary abstract '@require' header generator */ export const generateRequireHeaders = ( requires: string[], ): HeaderEntries<{ require: string }> => { return requires .filter(url => validator.isURL(url, { allow_protocol_relative_urls: true, })) .flatMap(explodePaths) .map(url => ["require", url]); };