Skip to content

Commit 8e1445c

Browse files
committed
Large refactor: save progress
1 parent ebeec8f commit 8e1445c

27 files changed

+613
-333
lines changed

.github/workflows/tests.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,5 +29,8 @@ jobs:
2929
- name: NPM install
3030
run: npm ci
3131

32+
- name: TypeScript type check
33+
run: npm run test:types
34+
3235
- name: Vitest
3336
run: npm test

biome.json

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
2-
"$schema": "https://biomejs.dev/schemas/1.4.1/schema.json",
2+
"$schema": "./node_modules/@biomejs/biome/configuration_schema.json",
33
"organizeImports": {
44
"enabled": true
55
},
@@ -16,7 +16,10 @@
1616
},
1717
"complexity": {
1818
"noForEach": "off"
19-
}
19+
},
20+
"suspicious": {
21+
"noConfusingLabels": "off"
22+
}
2023
}
2124
},
2225
"formatter": {

package-lock.json

Lines changed: 27 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,8 @@
2121
"lint:fix": "biome check src scripts --apply",
2222
"ci": "biome ci src scripts",
2323
"test": "vitest run --typecheck",
24-
"test:watch": "vitest watch"
24+
"test:watch": "vitest watch",
25+
"test:types": "tsc --noEmit"
2526
},
2627
"author": "tim-we",
2728
"license": "MIT",
@@ -47,6 +48,7 @@
4748
"dependencies": {
4849
"@babel/parser": "^7.26.9",
4950
"@babel/traverse": "^7.26.9",
51+
"@preact/signals": "^2.0.1",
5052
"@zip.js/zip.js": "^2.7.47",
5153
"acorn": "^8.12.1",
5254
"acorn-walk": "^8.3.3",

src/extension/Extension.ts

Lines changed: 31 additions & 122 deletions
Original file line numberDiff line numberDiff line change
@@ -1,39 +1,23 @@
11
import * as zip from "@zip.js/zip.js";
22
import prettyBytes from "pretty-bytes";
3-
import Runner from "../runner/Runner";
43
import { sum } from "../utilities/iterators";
5-
import createUniqueId from "../utilities/unique-id";
64
import { createFileSystem } from "./FileSystem";
75
import { FSFolder } from "./FileSystem";
8-
import type { ExtensionData } from "./types/ExtensionData";
6+
import type { ExtensionSummary } from "./types/ExtensionSummary";
97
import type { Manifest } from "./types/Manifest";
108
import type { Translations } from "./types/Translations";
119

1210
export default class Extension {
13-
readonly id: string;
1411
readonly manifest: Readonly<Manifest>;
1512
readonly files: FSFolder;
16-
readonly #objectURLs = new Map<string, string>();
1713
readonly #translations: Map<string, Translations>;
1814

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>) {
2616
/* Some of the initialization happens in the static create method because the constructor is not async. */
2717

28-
this.id = createUniqueId();
2918
this.files = root;
3019
this.manifest = Object.freeze(manifest);
3120
this.#translations = translations;
32-
33-
this.#objectURLs.set("download", URL.createObjectURL(blob));
34-
if (icon) {
35-
this.#objectURLs.set("icon", icon);
36-
}
3721
}
3822

3923
/**
@@ -71,82 +55,7 @@ export default class Extension {
7155
}
7256
}
7357

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);
15059
}
15160

15261
i18n(
@@ -242,8 +151,34 @@ export default class Extension {
242151
};
243152
}
244153

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+
};
247182
}
248183

249184
/**
@@ -289,32 +224,6 @@ export default class Extension {
289224
}
290225
}
291226

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-
318227
function isHostPermission(permission: string): boolean {
319228
if (permission === "<all_urls>") {
320229
return true;

src/extension/sources/AMO.ts

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
/**
2+
* API docs:
3+
* https://addons-server.readthedocs.io/en/latest/topics/api/addons.html#detail
4+
*
5+
* TODO: Can we use the frozen v4 API instead?
6+
*/
7+
8+
const API = "https://addons.mozilla.org/api/v5";
9+
10+
export type Details = {
11+
id: number;
12+
authors: ExtensionAuthor[];
13+
current_version: Version;
14+
guid: string;
15+
icon_url: string;
16+
name: TranslatedField | null;
17+
last_updated: string;
18+
created: string;
19+
slug: string;
20+
url: string;
21+
};
22+
23+
type ExtensionAuthor = {
24+
id: number;
25+
name: string;
26+
url: string;
27+
username: string;
28+
};
29+
30+
type Version = {
31+
id: number;
32+
channel: "unlisted" | "listed";
33+
file: File;
34+
version: string;
35+
compatibility: unknown;
36+
};
37+
38+
type File = {
39+
id: number;
40+
created: string;
41+
hash: string;
42+
is_mozilla_signed_extension: boolean;
43+
optional_permissions: string[];
44+
permissions: string[];
45+
size: number;
46+
status: string;
47+
url: string;
48+
};
49+
50+
type TranslatedField = Partial<
51+
Record<Lowercase<string> | `${Lowercase<string>}-${string}`, string>
52+
>;
53+
54+
export async function getInfo(slug: string): Promise<Details> {
55+
const response = await fetch(`${API}/addons/addon/${slug}/`);
56+
return response.json();
57+
}

src/extension/sources/CWS.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
export function getProxiedDownloadURL(extId: string): string {
2+
throw new Error("TODO");
3+
}
4+
5+
export function getDownloadURL(extId: string): string {
6+
const host = "clients2.google.com";
7+
const path = "/service/update2/crx";
8+
const params = `response=redirect&prodversion=${__CHROME_VERSION__}&x=id%3D${extId}%26installsource%3Dondemand%26uc&nacl_arch=x86-64&acceptformat=crx2,crx3`;
9+
return `https://${host}${path}?${params}`;
10+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
export type ExtensionSource =
2+
| {
3+
type: "amo" | "cws";
4+
id: string;
5+
}
6+
| { type: "file"; file: File }
7+
| { type: "url"; url: string };
Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
1-
export type ExtensionData = {
2-
id: string;
3-
downloadUrl: string;
1+
import type { ExtensionSource } from "../sources/ExtensionSource";
42

3+
export type ExtensionSummary = {
54
meta: {
65
name: string;
76
version: string;
8-
source: "amo" | "cws" | "file";
7+
source: ExtensionSource["type"];
98
author?: string;
109
icon?: string;
1110
created?: string;

0 commit comments

Comments
 (0)