Release Firefox and Chromium extensions / Package and attach browser extensions (release) Successful in 4s
185 lines
6.7 KiB
JavaScript
185 lines
6.7 KiB
JavaScript
"use strict";
|
|
|
|
const test = require("node:test");
|
|
const assert = require("node:assert/strict");
|
|
const { createHash } = require("node:crypto");
|
|
const { mkdtemp, readFile, rm } = require("node:fs/promises");
|
|
const os = require("node:os");
|
|
const path = require("node:path");
|
|
const { inflateRawSync } = require("node:zlib");
|
|
const {
|
|
CHROMIUM_ARCHIVE_FILES,
|
|
FIREFOX_ARCHIVE_FILES,
|
|
normalizeVersion,
|
|
packageExtension
|
|
} = require("../scripts/package-extension.js");
|
|
|
|
function readZipEntries(archive) {
|
|
const entries = new Map();
|
|
let offset = 0;
|
|
|
|
while (archive.readUInt32LE(offset) === 0x04034b50) {
|
|
const method = archive.readUInt16LE(offset + 8);
|
|
const compressedSize = archive.readUInt32LE(offset + 18);
|
|
const nameLength = archive.readUInt16LE(offset + 26);
|
|
const extraLength = archive.readUInt16LE(offset + 28);
|
|
const nameStart = offset + 30;
|
|
const dataStart = nameStart + nameLength + extraLength;
|
|
const name = archive.subarray(nameStart, nameStart + nameLength).toString("utf8");
|
|
const compressed = archive.subarray(dataStart, dataStart + compressedSize);
|
|
|
|
assert.equal(method, 8, `${name} should use DEFLATE compression`);
|
|
entries.set(name, inflateRawSync(compressed));
|
|
offset = dataStart + compressedSize;
|
|
}
|
|
|
|
assert.equal(archive.readUInt32LE(offset), 0x02014b50, "central directory is present");
|
|
return entries;
|
|
}
|
|
|
|
test("normalizes release tags and rejects versions Firefox cannot publish", () => {
|
|
assert.equal(normalizeVersion("1.2.3"), "1.2.3");
|
|
assert.equal(normalizeVersion("v2.0"), "2.0");
|
|
assert.equal(normalizeVersion("V3"), "3");
|
|
|
|
for (const version of ["", "v", "1.2.3.4.5", "1.02", "1.0-beta", "1000000000"]) {
|
|
assert.throws(() => normalizeVersion(version), /Invalid Mozilla extension version|required/);
|
|
}
|
|
});
|
|
|
|
test("builds a deterministic unsigned XPI with the release version and runtime files only", async (context) => {
|
|
const rootDirectory = path.resolve(__dirname, "..");
|
|
const firstOutput = await mkdtemp(path.join(os.tmpdir(), "glagolify-package-"));
|
|
const secondOutput = await mkdtemp(path.join(os.tmpdir(), "glagolify-package-"));
|
|
context.after(async () => {
|
|
await Promise.all([
|
|
rm(firstOutput, { recursive: true, force: true }),
|
|
rm(secondOutput, { recursive: true, force: true })
|
|
]);
|
|
});
|
|
|
|
const first = await packageExtension({
|
|
rootDirectory,
|
|
outputDirectory: firstOutput,
|
|
version: "v9.8.7"
|
|
});
|
|
const second = await packageExtension({
|
|
rootDirectory,
|
|
outputDirectory: secondOutput,
|
|
version: "9.8.7"
|
|
});
|
|
const firstArchive = await readFile(first.archivePath);
|
|
const secondArchive = await readFile(second.archivePath);
|
|
const entries = readZipEntries(firstArchive);
|
|
|
|
assert.equal(first.archiveName, "glagolify-9.8.7.xpi");
|
|
assert.deepEqual([...entries.keys()], [...FIREFOX_ARCHIVE_FILES]);
|
|
assert.deepEqual(
|
|
entries.get("content.css"),
|
|
await readFile(path.join(rootDirectory, "content.css"))
|
|
);
|
|
const manifest = JSON.parse(entries.get("manifest.json"));
|
|
assert.equal(manifest.version, "9.8.7");
|
|
assert.equal(manifest.manifest_version, 2);
|
|
assert.equal(manifest.background.persistent, true);
|
|
assert.equal(manifest.browser_action.default_popup, "popup.html");
|
|
assert.equal(entries.has("package.json"), false);
|
|
assert.equal(entries.has("tests/package-extension.test.js"), false);
|
|
assert.deepEqual(firstArchive, secondArchive);
|
|
|
|
const digest = createHash("sha256").update(firstArchive).digest("hex");
|
|
assert.equal(
|
|
await readFile(first.checksumPath, "utf8"),
|
|
`${digest} glagolify-9.8.7.xpi\n`
|
|
);
|
|
});
|
|
|
|
test("builds a Chromium MV3 ZIP with Chromium manifest fields and raster icons", async (context) => {
|
|
const rootDirectory = path.resolve(__dirname, "..");
|
|
const outputDirectory = await mkdtemp(path.join(os.tmpdir(), "glagolify-chromium-"));
|
|
context.after(() => rm(outputDirectory, { recursive: true, force: true }));
|
|
|
|
const result = await packageExtension({
|
|
rootDirectory,
|
|
outputDirectory,
|
|
target: "chromium",
|
|
version: "v4.5.6"
|
|
});
|
|
const archive = await readFile(result.archivePath);
|
|
const entries = readZipEntries(archive);
|
|
const manifest = JSON.parse(entries.get("manifest.json"));
|
|
|
|
assert.equal(result.archiveName, "glagolify-4.5.6-chromium.zip");
|
|
assert.deepEqual([...entries.keys()], [...CHROMIUM_ARCHIVE_FILES]);
|
|
assert.equal(manifest.manifest_version, 3);
|
|
assert.equal(manifest.minimum_chrome_version, "102");
|
|
assert.equal(manifest.version, "4.5.6");
|
|
assert.deepEqual(manifest.permissions, ["activeTab", "storage"]);
|
|
assert.deepEqual(manifest.host_permissions, ["<all_urls>"]);
|
|
assert.deepEqual(manifest.background, { service_worker: "background.js" });
|
|
assert.equal(manifest.action.default_popup, "popup.html");
|
|
assert.equal("browser_action" in manifest, false);
|
|
assert.equal("browser_specific_settings" in manifest, false);
|
|
assert.deepEqual(manifest.web_accessible_resources, [
|
|
{
|
|
resources: [
|
|
"fonts/NotoSansGlagolitic-Regular.ttf",
|
|
"fonts/Shafarik-Regular.ttf"
|
|
],
|
|
matches: ["<all_urls>"]
|
|
}
|
|
]);
|
|
assert.equal(manifest.icons["128"], "icons/icon-128.png");
|
|
|
|
const contentCss = entries.get("content.css").toString("utf8");
|
|
const fontUrls = [...contentCss.matchAll(/src: url\("([^"]+)"\)/g)].map(
|
|
(match) => match[1]
|
|
);
|
|
const extensionUrl = "chrome-extension://__MSG_@@extension_id__/";
|
|
assert.equal(fontUrls.length, 4);
|
|
for (const fontUrl of fontUrls) {
|
|
assert.equal(fontUrl.startsWith(extensionUrl), true);
|
|
const relativePath = fontUrl.slice(extensionUrl.length);
|
|
assert.equal(entries.has(relativePath), true);
|
|
assert.equal(
|
|
manifest.web_accessible_resources[0].resources.includes(relativePath),
|
|
true
|
|
);
|
|
}
|
|
|
|
for (const relativePath of CHROMIUM_ARCHIVE_FILES.filter((name) =>
|
|
name.endsWith(".png")
|
|
)) {
|
|
assert.deepEqual(
|
|
entries.get(relativePath).subarray(0, 8),
|
|
Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a])
|
|
);
|
|
}
|
|
const popup = entries.get("popup.html").toString("utf8");
|
|
for (const [, relativePath] of popup.matchAll(/\b(?:href|src)="([^"]+)"/g)) {
|
|
assert.equal(
|
|
entries.has(relativePath),
|
|
true,
|
|
`${relativePath} referenced by popup.html should be packaged`
|
|
);
|
|
}
|
|
|
|
const digest = createHash("sha256").update(archive).digest("hex");
|
|
assert.equal(
|
|
await readFile(result.checksumPath, "utf8"),
|
|
`${digest} glagolify-4.5.6-chromium.zip\n`
|
|
);
|
|
});
|
|
|
|
test("rejects Chromium version components outside the MV3 manifest range", async () => {
|
|
await assert.rejects(
|
|
packageExtension({
|
|
rootDirectory: path.resolve(__dirname, ".."),
|
|
outputDirectory: os.tmpdir(),
|
|
target: "chromium",
|
|
version: "65536"
|
|
}),
|
|
/cannot exceed 65535/
|
|
);
|
|
});
|