|
1 | 1 | import * as zip from "@zip.js/zip.js"; |
2 | 2 | import prettyBytes from "pretty-bytes"; |
3 | | -import Runner from "../runner/Runner"; |
4 | 3 | import { sum } from "../utilities/iterators"; |
5 | | -import createUniqueId from "../utilities/unique-id"; |
6 | 4 | import { createFileSystem } from "./FileSystem"; |
7 | 5 | import { FSFolder } from "./FileSystem"; |
8 | | -import type { ExtensionData } from "./types/ExtensionData"; |
| 6 | +import type { ExtensionSummary } from "./types/ExtensionSummary"; |
9 | 7 | import type { Manifest } from "./types/Manifest"; |
10 | 8 | import type { Translations } from "./types/Translations"; |
11 | 9 |
|
12 | 10 | export default class Extension { |
13 | | - readonly id: string; |
14 | 11 | readonly manifest: Readonly<Manifest>; |
15 | 12 | readonly files: FSFolder; |
16 | | - readonly #objectURLs = new Map<string, string>(); |
17 | 13 | readonly #translations: Map<string, Translations>; |
18 | 14 |
|
19 | | - private constructor( |
20 | | - blob: Blob, |
21 | | - root: FSFolder, |
22 | | - manifest: Manifest, |
23 | | - translations: Map<string, Translations>, |
24 | | - icon?: string |
25 | | - ) { |
| 15 | + private constructor(root: FSFolder, manifest: Manifest, translations: Map<string, Translations>) { |
26 | 16 | /* Some of the initialization happens in the static create method because the constructor is not async. */ |
27 | 17 |
|
28 | | - this.id = createUniqueId(); |
29 | 18 | this.files = root; |
30 | 19 | this.manifest = Object.freeze(manifest); |
31 | 20 | this.#translations = translations; |
32 | | - |
33 | | - this.#objectURLs.set("download", URL.createObjectURL(blob)); |
34 | | - if (icon) { |
35 | | - this.#objectURLs.set("icon", icon); |
36 | | - } |
37 | 21 | } |
38 | 22 |
|
39 | 23 | /** |
@@ -71,82 +55,7 @@ export default class Extension { |
71 | 55 | } |
72 | 56 | } |
73 | 57 |
|
74 | | - // Icon |
75 | | - const iconPath = getIconPath(manifest, files); |
76 | | - const iconURL = iconPath |
77 | | - ? URL.createObjectURL(await files.getFile(iconPath).asBlob()) |
78 | | - : undefined; |
79 | | - |
80 | | - return new Extension(zipData, files, manifest, translations, iconURL); |
81 | | - } |
82 | | - |
83 | | - getSummary(): ExtensionData { |
84 | | - const manifest = this.manifest; |
85 | | - |
86 | | - const hostPermissions = [ |
87 | | - ...(manifest.permissions ?? []), |
88 | | - ...(manifest.optional_permissions ?? []) |
89 | | - ].filter(isHostPermission).length; |
90 | | - |
91 | | - const files = { |
92 | | - javascript: this.files.countFiles(/\.(js|mjs)$/), |
93 | | - html: this.files.countFiles(/\.(htm|html)$/), |
94 | | - css: this.files.countFiles(/\.css$/), |
95 | | - json: this.files.countFiles(/\.json$/) |
96 | | - }; |
97 | | - |
98 | | - const backgroundScripts = ((bg) => { |
99 | | - if (bg === undefined) { |
100 | | - return false; |
101 | | - } |
102 | | - if (bg.page !== undefined) { |
103 | | - return true; |
104 | | - } |
105 | | - if (bg.scripts?.length > 0) { |
106 | | - return true; |
107 | | - } |
108 | | - return false; |
109 | | - })(manifest.background); |
110 | | - |
111 | | - const messageKeys = new Set(this.#translations.values().flatMap((t) => Object.keys(t))).size; |
112 | | - const translatedMessages = sum(this.#translations.values().map((t) => Object.keys(t).length)); |
113 | | - |
114 | | - return { |
115 | | - id: this.id, |
116 | | - downloadUrl: this.#objectURLs.get("download")!, |
117 | | - meta: { |
118 | | - name: this.#__MSG_i18n(this.manifest.name), |
119 | | - version: manifest.version, |
120 | | - icon: this.#objectURLs.get("icon"), |
121 | | - source: "file", // FIXME |
122 | | - author: this.#getAuthor(), |
123 | | - manifestVersion: manifest.manifest_version, |
124 | | - size: prettyBytes(this.files.uncompressedSize) |
125 | | - }, |
126 | | - permissions: { |
127 | | - required: manifest.permissions?.length ?? 0, |
128 | | - optional: manifest.optional_permissions?.length ?? 0, |
129 | | - host: hostPermissions |
130 | | - }, |
131 | | - files: { |
132 | | - ...files, |
133 | | - other: this.files.numFiles - (files.javascript + files.html + files.css + files.json) |
134 | | - }, |
135 | | - dynamicAnalysis: { |
136 | | - supported: Runner.supports(this), |
137 | | - background: backgroundScripts, |
138 | | - jsType: Runner.supports(this) ? "classic" : undefined |
139 | | - }, |
140 | | - translations: { |
141 | | - locales: Array.from(this.#translations.keys()), |
142 | | - messages: messageKeys, |
143 | | - defaultLocale: manifest.default_locale, |
144 | | - percentage: |
145 | | - this.#translations.size * messageKeys > 0 |
146 | | - ? translatedMessages / (this.#translations.size * messageKeys) |
147 | | - : undefined |
148 | | - } |
149 | | - }; |
| 58 | + return new Extension(files, manifest, translations); |
150 | 59 | } |
151 | 60 |
|
152 | 61 | i18n( |
@@ -242,8 +151,34 @@ export default class Extension { |
242 | 151 | }; |
243 | 152 | } |
244 | 153 |
|
245 | | - free() { |
246 | | - this.#objectURLs.forEach((url) => URL.revokeObjectURL(url)); |
| 154 | + getLocales() { |
| 155 | + return [...this.#translations.keys()]; |
| 156 | + } |
| 157 | + |
| 158 | + get meta(): Omit<ExtensionSummary["meta"], "icon"> { |
| 159 | + return { |
| 160 | + name: this.#__MSG_i18n(this.manifest.name), |
| 161 | + version: this.manifest.version, |
| 162 | + source: "file", // FIXME |
| 163 | + author: this.#getAuthor(), |
| 164 | + manifestVersion: this.manifest.manifest_version, |
| 165 | + size: prettyBytes(this.files.uncompressedSize) |
| 166 | + }; |
| 167 | + } |
| 168 | + |
| 169 | + get translationInfo(): ExtensionSummary["translations"] { |
| 170 | + const messageKeys = new Set(this.#translations.values().flatMap((t) => Object.keys(t))).size; |
| 171 | + const translatedMessages = sum(this.#translations.values().map((t) => Object.keys(t).length)); |
| 172 | + |
| 173 | + return { |
| 174 | + locales: this.getLocales(), |
| 175 | + messages: messageKeys, |
| 176 | + defaultLocale: this.manifest.default_locale, |
| 177 | + percentage: |
| 178 | + this.#translations.size * messageKeys > 0 |
| 179 | + ? translatedMessages / (this.#translations.size * messageKeys) |
| 180 | + : undefined |
| 181 | + }; |
247 | 182 | } |
248 | 183 |
|
249 | 184 | /** |
@@ -289,32 +224,6 @@ export default class Extension { |
289 | 224 | } |
290 | 225 | } |
291 | 226 |
|
292 | | -function getIconPath(manifest: Manifest, root: FSFolder): string | undefined { |
293 | | - if (!manifest.icons) { |
294 | | - return; |
295 | | - } |
296 | | - |
297 | | - const sizes = Object.entries(manifest.icons).map(([size, path]) => ({ |
298 | | - size: Number.parseInt(size, 10), |
299 | | - path |
300 | | - })); |
301 | | - |
302 | | - if (sizes.length === 0) { |
303 | | - return; |
304 | | - } |
305 | | - |
306 | | - // sort sizes descending |
307 | | - sizes.sort((a, b) => b.size - a.size); |
308 | | - |
309 | | - const optimalSizes = sizes |
310 | | - .filter(({ path }) => root.getFile(path, true)) |
311 | | - .filter(({ size }) => size >= 48 && size <= 96); |
312 | | - |
313 | | - const { path } = optimalSizes[0] ?? sizes[0]; |
314 | | - |
315 | | - return path; |
316 | | -} |
317 | | - |
318 | 227 | function isHostPermission(permission: string): boolean { |
319 | 228 | if (permission === "<all_urls>") { |
320 | 229 | return true; |
|
0 commit comments