| 1 | export interface ThirdPartyLicenseEntry { |
| 2 | readonly bundles: ReadonlyArray<string>; |
| 3 | readonly kind: "custom" | "package"; |
| 4 | readonly license: string; |
| 5 | readonly name: string; |
| 6 | readonly noticeText: string; |
| 7 | readonly sourceUrl: string | null; |
| 8 | readonly version: string | null; |
| 9 | } |
| 10 | |
| 11 | export interface ThirdPartyLicenseManifest { |
| 12 | readonly schemaVersion: 1; |
| 13 | readonly entries: ReadonlyArray<ThirdPartyLicenseEntry>; |
| 14 | } |
| 15 | |
| 16 | function isRecord(value: unknown): value is Record<string, unknown> { |
| 17 | return typeof value === "object" && value !== null && !Array.isArray(value); |
| 18 | } |
| 19 | |
| 20 | function isStringArray(value: unknown): value is ReadonlyArray<string> { |
| 21 | return Array.isArray(value) && value.every((entry) => typeof entry === "string"); |
| 22 | } |
| 23 | |
| 24 | function isHttpUrl(value: unknown): value is string { |
| 25 | if (typeof value !== "string") return false; |
| 26 | try { |
| 27 | const protocol = new URL(value).protocol; |
| 28 | return protocol === "http:" || protocol === "https:"; |
| 29 | } catch { |
| 30 | return false; |
| 31 | } |
| 32 | } |
| 33 | |
| 34 | function decodeEntry(value: unknown, index: number): ThirdPartyLicenseEntry { |
| 35 | if (!isRecord(value)) { |
| 36 | throw new Error(`License entry ${String(index + 1)} is not an object.`); |
| 37 | } |
| 38 | if ( |
| 39 | !isStringArray(value.bundles) || |
| 40 | value.bundles.length === 0 || |
| 41 | (value.kind !== "custom" && value.kind !== "package") || |
| 42 | typeof value.license !== "string" || |
| 43 | typeof value.name !== "string" || |
| 44 | typeof value.noticeText !== "string" || |
| 45 | (value.sourceUrl !== null && !isHttpUrl(value.sourceUrl)) || |
| 46 | (value.version !== null && typeof value.version !== "string") |
| 47 | ) { |
| 48 | throw new Error(`License entry ${String(index + 1)} has an invalid shape.`); |
| 49 | } |
| 50 | return { |
| 51 | bundles: value.bundles, |
| 52 | kind: value.kind, |
| 53 | license: value.license, |
| 54 | name: value.name, |
| 55 | noticeText: value.noticeText, |
| 56 | sourceUrl: value.sourceUrl, |
| 57 | version: value.version, |
| 58 | }; |
| 59 | } |
| 60 | |
| 61 | export function decodeThirdPartyLicenseManifest(value: unknown): ThirdPartyLicenseManifest { |
| 62 | if (!isRecord(value) || value.schemaVersion !== 1 || !Array.isArray(value.entries)) { |
| 63 | throw new Error("The open-source license manifest has an unsupported format."); |
| 64 | } |
| 65 | const entries = value.entries.map(decodeEntry); |
| 66 | const entryKeys = new Set<string>(); |
| 67 | for (const entry of entries) { |
| 68 | const key = thirdPartyLicenseEntryKey(entry); |
| 69 | if (entryKeys.has(key)) { |
| 70 | throw new Error(`The open-source license manifest contains a duplicate entry: ${key}`); |
| 71 | } |
| 72 | entryKeys.add(key); |
| 73 | } |
| 74 | return { |
| 75 | schemaVersion: 1, |
| 76 | entries, |
| 77 | }; |
| 78 | } |
| 79 | |
| 80 | export function filterThirdPartyLicenseEntries( |
| 81 | entries: ReadonlyArray<ThirdPartyLicenseEntry>, |
| 82 | query: string, |
| 83 | ): ReadonlyArray<ThirdPartyLicenseEntry> { |
| 84 | const terms = query |
| 85 | .trim() |
| 86 | .toLowerCase() |
| 87 | .split(/\s+/) |
| 88 | .filter((term) => term.length > 0); |
| 89 | if (terms.length === 0) return entries; |
| 90 | return entries.filter((entry) => { |
| 91 | const searchable = [entry.name, entry.version, entry.license, ...entry.bundles] |
| 92 | .filter((value): value is string => value !== null) |
| 93 | .join(" ") |
| 94 | .toLowerCase(); |
| 95 | return terms.every((term) => searchable.includes(term)); |
| 96 | }); |
| 97 | } |
| 98 | |
| 99 | const BUNDLE_LABELS: Readonly<Record<string, string>> = { |
| 100 | android: "Android", |
| 101 | assets: "Assets", |
| 102 | desktop: "Desktop", |
| 103 | "device-tools": "Device tools", |
| 104 | ios: "iOS", |
| 105 | mobile: "Mobile", |
| 106 | server: "Server", |
| 107 | web: "Web", |
| 108 | }; |
| 109 | |
| 110 | export function formatLicenseBundles(bundles: ReadonlyArray<string>): string { |
| 111 | return bundles |
| 112 | .map((bundle) => |
| 113 | Object.prototype.hasOwnProperty.call(BUNDLE_LABELS, bundle) ? BUNDLE_LABELS[bundle] : bundle, |
| 114 | ) |
| 115 | .join(", "); |
| 116 | } |
| 117 | |
| 118 | export function thirdPartyLicenseEntryKey(entry: ThirdPartyLicenseEntry): string { |
| 119 | return encodeURIComponent(JSON.stringify([entry.kind, entry.name, entry.version])); |
| 120 | } |
| 121 | |
| 122 | export function findThirdPartyLicenseEntry( |
| 123 | entries: ReadonlyArray<ThirdPartyLicenseEntry>, |
| 124 | key: string, |
| 125 | ): ThirdPartyLicenseEntry | undefined { |
| 126 | return entries.find((entry) => thirdPartyLicenseEntryKey(entry) === key); |
| 127 | } |