From df16075421b638d05eaf757cdc1f511e8ecd69c8 Mon Sep 17 00:00:00 2001 From: StanPonomarev Date: Sun, 30 Aug 2026 21:00:05 +0200 Subject: [PATCH] chromium support --- .gitea/workflows/release.yml | 10 +-- README.md | 31 +++++++- background.js | 73 +++++++++++++++---- content.js | 11 +-- icons/icon-128.png | Bin 0 -> 8393 bytes icons/icon-16.png | Bin 0 -> 623 bytes icons/icon-32.png | Bin 0 -> 1396 bytes icons/icon-48.png | Bin 0 -> 2289 bytes icons/icon-96.png | Bin 0 -> 5146 bytes package.json | 6 +- popup.js | 8 ++- scripts/package-extension.js | 123 ++++++++++++++++++++++++++------ tests/background.test.js | 61 ++++++++++++++-- tests/package-extension.test.js | 84 +++++++++++++++++++++- 14 files changed, 350 insertions(+), 57 deletions(-) create mode 100644 icons/icon-128.png create mode 100644 icons/icon-16.png create mode 100644 icons/icon-32.png create mode 100644 icons/icon-48.png create mode 100644 icons/icon-96.png diff --git a/.gitea/workflows/release.yml b/.gitea/workflows/release.yml index b075a66..649f2aa 100644 --- a/.gitea/workflows/release.yml +++ b/.gitea/workflows/release.yml @@ -1,4 +1,4 @@ -name: Release unsigned Firefox extension +name: Release Firefox and Chromium extensions on: release: @@ -11,7 +11,7 @@ permissions: jobs: package-and-attach: - name: Package and attach unsigned XPI + name: Package and attach browser extensions runs-on: ubuntu-latest timeout-minutes: 10 @@ -30,14 +30,14 @@ jobs: - name: Run automated checks run: npm test - - name: Build unsigned release archive + - name: Build unsigned release archives env: GITEA_RELEASE_TAG: ${{ gitea.event.release.tag_name }} run: npm run package -- --version "${GITEA_RELEASE_TAG}" --output-dir dist - name: Verify release checksum working-directory: dist - run: sha256sum --check *.xpi.sha256 + run: sha256sum --check *.sha256 - name: Attach release assets env: @@ -47,7 +47,7 @@ jobs: GITEA_RELEASE_ID: ${{ gitea.event.release.id }} run: | set -eu - for asset in dist/*.xpi dist/*.xpi.sha256; do + for asset in dist/*.xpi dist/*.xpi.sha256 dist/*-chromium.zip dist/*-chromium.zip.sha256; do name="$(basename "${asset}")" curl --fail-with-body --silent --show-error \ --request POST \ diff --git a/README.md b/README.md index 28fc926..9dbd3f2 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,13 @@ # Glagolify -Glagolify is a small Firefox extension that turns Russian Cyrillic text on webpages into Glagolitic. Switch it off and the original text comes back—no reload required. +Glagolify is a small Firefox and Chromium extension that turns Russian Cyrillic text on webpages into Glagolitic. Switch it off and the original text comes back—no reload required. Choose from four letter styles: **Round**, **Angular Sans**, **Historical Angular**, and **Missal Angular**. Glagolify also handles text added after a page loads while leaving text fields and editors alone. ## Install +### Firefox + Glagolify requires Firefox 142 or newer. 1. Download the latest `.xpi` from [Releases](https://git.krtss.de/StanPonomarev/glagolify/releases). @@ -15,8 +17,33 @@ Glagolify requires Firefox 142 or newer. Release builds are currently unsigned, so regular Firefox installs them temporarily and removes them when Firefox restarts. +### Chromium + +Glagolify requires Chromium 102 or newer. + +1. Download and extract the latest `-chromium.zip` from [Releases](https://git.krtss.de/StanPonomarev/glagolify/releases). +2. Open `chrome://extensions` in Chromium and enable **Developer mode**. +3. Select **Load unpacked** and choose the extracted directory. + ## Use Open a webpage, click the Glagolify toolbar icon, choose a style, and select **Enable Glagolitic**. Select **Restore Cyrillic** when you want the original text back. -Glagolify transliterates letters; it does not translate language. Firefox settings pages, browser UI, and other protected pages cannot be changed. +Glagolify transliterates letters; it does not translate language. Browser settings pages, browser UI, and other protected pages cannot be changed. + +## Build + +Node.js 22 or newer is required. The build has no third-party dependencies. + +```sh +npm test +npm run package +``` + +`npm run package` creates both `dist/glagolify-.xpi` for Firefox and `dist/glagolify--chromium.zip`, with a `.sha256` file beside each archive. Use `npm run package:firefox` or `npm run package:chromium` to build only one target. A release version and output directory can be supplied to any command, for example: + +```sh +npm run package:chromium -- --version v1.2.3 --output-dir dist +``` + +The checked-in manifest remains the Firefox Manifest V2 source. The Chromium package command converts it to Manifest V3 and uses the shared runtime files. diff --git a/background.js b/background.js index 6d0118a..3b2514e 100644 --- a/background.js +++ b/background.js @@ -2,6 +2,7 @@ "use strict"; const FONT_STYLE_KEY = "glagolifyFontStyle"; + const TAB_STATES_KEY = "glagolifyTabStates"; const FONT_STYLES = new Set(["round", "angular-sans", "angular", "missal-angular"]); const DEFAULT_FONT_STYLE = "round"; @@ -13,14 +14,35 @@ if (stored && FONT_STYLES.has(stored[FONT_STYLE_KEY])) { fontStyle = stored[FONT_STYLE_KEY]; } - return fontStyle; }) - .catch(() => fontStyle); + .catch(() => undefined); + const tabStatesReady = Promise.resolve(api.storage.session.get(TAB_STATES_KEY)) + .then((stored) => { + const states = stored && stored[TAB_STATES_KEY]; + if (!states || typeof states !== "object" || Array.isArray(states)) { + return; + } + + for (const [storedTabId, enabled] of Object.entries(states)) { + const tabId = Number(storedTabId); + if (validTabId(tabId) && typeof enabled === "boolean") { + tabStates.set(tabId, enabled); + } + } + }) + .catch(() => undefined); + const controllerReady = Promise.all([fontStyleReady, tabStatesReady]); function validTabId(tabId) { return Number.isInteger(tabId) && tabId >= 0; } + async function persistTabStates() { + await api.storage.session.set({ + [TAB_STATES_KEY]: Object.fromEntries(tabStates) + }); + } + async function broadcast(tabId, enabled) { await fontStyleReady; try { @@ -35,14 +57,14 @@ } } - api.runtime.onMessage.addListener((message, sender) => { + function handleMessage(message, sender) { if (!message || typeof message.type !== "string") { return undefined; } if (message.type === "glagolify:document-ready") { const tabId = sender && sender.tab && sender.tab.id; - return fontStyleReady.then(() => ({ + return controllerReady.then(() => ({ enabled: validTabId(tabId) && tabStates.get(tabId) === true, fontStyle })); @@ -50,7 +72,7 @@ if (message.type === "glagolify:get-tab-state") { const tabId = message.tabId; - return fontStyleReady.then(async () => { + return controllerReady.then(async () => { if (!validTabId(tabId)) { return { enabled: false, available: false, fontStyle }; } @@ -64,15 +86,16 @@ if (message.type === "glagolify:set-tab-enabled") { const tabId = message.tabId; if (!validTabId(tabId) || typeof message.enabled !== "boolean") { - return fontStyleReady.then(() => ({ + return controllerReady.then(() => ({ enabled: false, available: false, fontStyle })); } - return fontStyleReady.then(async () => { + return controllerReady.then(async () => { tabStates.set(tabId, message.enabled); + await persistTabStates(); const available = await broadcast(tabId, message.enabled); return { enabled: message.enabled, @@ -84,10 +107,10 @@ if (message.type === "glagolify:set-font-style") { if (!FONT_STYLES.has(message.fontStyle)) { - return fontStyleReady.then(() => ({ fontStyle, accepted: false })); + return controllerReady.then(() => ({ fontStyle, accepted: false })); } - return fontStyleReady.then(async () => { + return controllerReady.then(async () => { await api.storage.local.set({ [FONT_STYLE_KEY]: message.fontStyle }); fontStyle = message.fontStyle; await Promise.all( @@ -100,24 +123,46 @@ } return undefined; + } + + api.runtime.onMessage.addListener((message, sender, sendResponse) => { + const response = handleMessage(message, sender); + if (response === undefined) { + return false; + } + + Promise.resolve(response).then( + (value) => sendResponse(value), + () => sendResponse(undefined) + ); + return true; }); - api.tabs.onRemoved.addListener((tabId) => { - tabStates.delete(tabId); + api.tabs.onRemoved.addListener(async (tabId) => { + await tabStatesReady; + if (tabStates.delete(tabId)) { + await persistTabStates(); + } }); return { tabStates, broadcast, - fontStyleReady, + controllerReady, getFontStyle: () => fontStyle }; } const exported = Object.freeze({ createBackgroundController }); - if (typeof browser === "object" && browser.runtime && browser.tabs) { - createBackgroundController(browser); + const extensionApi = + typeof browser === "object" + ? browser + : typeof chrome === "object" + ? chrome + : null; + if (extensionApi && extensionApi.runtime && extensionApi.tabs) { + createBackgroundController(extensionApi); } if (typeof module === "object" && module.exports) { diff --git a/content.js b/content.js index e81ded9..8ee4e7c 100644 --- a/content.js +++ b/content.js @@ -1,6 +1,8 @@ (function () { "use strict"; + const api = typeof browser === "object" ? browser : chrome; + const EXCLUDED_ANCESTORS = "head, script, style, noscript, template, textarea, select, option"; const FONT_FAMILIES = Object.freeze({ round: '"Glagolify Shafarik Round"', @@ -330,14 +332,15 @@ return { enabled }; } - browser.runtime.onMessage.addListener((message) => { + api.runtime.onMessage.addListener((message, sender, sendResponse) => { if (!message || message.type !== "glagolify:set-enabled" || typeof message.enabled !== "boolean") { - return undefined; + return false; } - return Promise.resolve(setEnabled(message.enabled, message.fontStyle)); + sendResponse(setEnabled(message.enabled, message.fontStyle)); + return false; }); - browser.runtime.sendMessage({ type: "glagolify:document-ready" }) + api.runtime.sendMessage({ type: "glagolify:document-ready" }) .then((response) => { setEnabled(Boolean(response && response.enabled), response && response.fontStyle); }) diff --git a/icons/icon-128.png b/icons/icon-128.png new file mode 100644 index 0000000000000000000000000000000000000000..3bfc3e5018bfdea7cf94f84805893852e870f9de GIT binary patch literal 8393 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 literal 0 HcmV?d00001 diff --git a/icons/icon-16.png b/icons/icon-16.png new file mode 100644 index 0000000000000000000000000000000000000000..47146ee32afda3d9db543fc035985a206d276d0f GIT binary patch literal 623 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 LNkvXXu0mjfdy;XC literal 0 HcmV?d00001 diff --git a/icons/icon-96.png b/icons/icon-96.png new file mode 100644 index 0000000000000000000000000000000000000000..06a93f8a8ea71408b14499a98558e3180b3638b0 GIT binary patch literal 5146 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 { @@ -104,7 +106,7 @@ async function initialize() { try { - const tabs = await browser.tabs.query({ active: true, currentWindow: true }); + const tabs = await api.tabs.query({ active: true, currentWindow: true }); if (!tabs[0] || typeof tabs[0].id !== "number") { showUnavailable(); return; diff --git a/scripts/package-extension.js b/scripts/package-extension.js index ade3463..37b6d44 100644 --- a/scripts/package-extension.js +++ b/scripts/package-extension.js @@ -5,7 +5,7 @@ const { mkdir, readFile, writeFile } = require("node:fs/promises"); const path = require("node:path"); const { deflateRawSync } = require("node:zlib"); -const ARCHIVE_FILES = Object.freeze([ +const FIREFOX_ARCHIVE_FILES = Object.freeze([ "background.js", "content.css", "content.js", @@ -23,6 +23,14 @@ const ARCHIVE_FILES = Object.freeze([ "popup.js", "transliterate.js" ]); +const CHROMIUM_ARCHIVE_FILES = Object.freeze([ + ...FIREFOX_ARCHIVE_FILES.map((relativePath) => + relativePath.endsWith(".svg") ? relativePath.replace(/\.svg$/, ".png") : relativePath + ), + "icons/icon-128.png", + "icons/icon-48.svg" +]); +const TARGETS = new Set(["firefox", "chromium"]); const VERSION_PATTERN = /^(0|[1-9][0-9]{0,8})(\.(0|[1-9][0-9]{0,8})){0,3}$/; const UTF8_FLAG = 0x0800; @@ -46,6 +54,47 @@ function normalizeVersion(value) { return version; } +function createManifest(contents, version, target) { + const manifest = JSON.parse(contents.toString("utf8")); + manifest.version = version; + + if (target === "firefox") { + return manifest; + } + + if (version.split(".").some((part) => Number(part) > 65535)) { + throw new Error(`Chromium extension version parts cannot exceed 65535: ${version}`); + } + + manifest.manifest_version = 3; + manifest.minimum_chrome_version = "102"; + delete manifest.browser_specific_settings; + manifest.host_permissions = manifest.permissions.filter( + (permission) => permission === "" || permission.includes("://") + ); + manifest.permissions = manifest.permissions.filter( + (permission) => !manifest.host_permissions.includes(permission) + ); + manifest.background = { service_worker: "background.js" }; + manifest.action = manifest.browser_action; + delete manifest.browser_action; + manifest.web_accessible_resources = [ + { + resources: manifest.web_accessible_resources, + matches: [""] + } + ]; + + for (const icons of [manifest.icons, manifest.action.default_icon]) { + for (const size of Object.keys(icons)) { + icons[size] = icons[size].replace(/\.svg$/, ".png"); + } + } + manifest.icons["128"] = "icons/icon-128.png"; + + return manifest; +} + function crc32(buffer) { let crc = 0xffffffff; @@ -122,19 +171,29 @@ function createZip(entries) { return Buffer.concat([...localRecords, centralDirectory, endRecord]); } -async function packageExtension({ rootDirectory, outputDirectory, version: requestedVersion }) { +async function packageExtension({ + rootDirectory, + outputDirectory, + target = "firefox", + version: requestedVersion +}) { + if (!TARGETS.has(target)) { + throw new Error(`Unknown extension target: ${target}`); + } + const packageMetadata = JSON.parse( await readFile(path.join(rootDirectory, "package.json"), "utf8") ); const version = normalizeVersion(requestedVersion ?? packageMetadata.version); + const archiveFiles = + target === "firefox" ? FIREFOX_ARCHIVE_FILES : CHROMIUM_ARCHIVE_FILES; const entries = []; - for (const relativePath of ARCHIVE_FILES) { + for (const relativePath of archiveFiles) { let contents = await readFile(path.join(rootDirectory, relativePath)); if (relativePath === "manifest.json") { - const manifest = JSON.parse(contents.toString("utf8")); - manifest.version = version; + const manifest = createManifest(contents, version, target); contents = Buffer.from(`${JSON.stringify(manifest, null, 2)}\n`); } @@ -142,7 +201,10 @@ async function packageExtension({ rootDirectory, outputDirectory, version: reque } const archive = createZip(entries); - const archiveName = `glagolify-${version}.xpi`; + const archiveName = + target === "firefox" + ? `glagolify-${version}.xpi` + : `glagolify-${version}-chromium.zip`; const archivePath = path.join(outputDirectory, archiveName); const checksumPath = `${archivePath}.sha256`; const digest = createHash("sha256").update(archive).digest("hex"); @@ -151,17 +213,22 @@ async function packageExtension({ rootDirectory, outputDirectory, version: reque await writeFile(archivePath, archive); await writeFile(checksumPath, `${digest} ${archiveName}\n`); - return { archiveName, archivePath, checksumPath, digest, version }; + return { archiveName, archivePath, checksumPath, digest, target, version }; } function parseArguments(arguments_) { let outputDirectory = "dist"; + let target = "all"; let version; for (let index = 0; index < arguments_.length; index += 1) { const argument = arguments_[index]; - if (argument === "--version" || argument === "--output-dir") { + if ( + argument === "--version" || + argument === "--output-dir" || + argument === "--target" + ) { const value = arguments_[index + 1]; if (!value || value.startsWith("--")) { throw new Error(`${argument} requires a value.`); @@ -169,8 +236,10 @@ function parseArguments(arguments_) { if (argument === "--version") { version = value; - } else { + } else if (argument === "--output-dir") { outputDirectory = value; + } else { + target = value; } index += 1; } else { @@ -178,22 +247,31 @@ function parseArguments(arguments_) { } } - return { outputDirectory, version }; + if (target !== "all" && !TARGETS.has(target)) { + throw new Error(`Unknown extension target: ${target}`); + } + + return { outputDirectory, target, version }; } async function main() { const rootDirectory = path.resolve(__dirname, ".."); const options = parseArguments(process.argv.slice(2)); - const result = await packageExtension({ - rootDirectory, - outputDirectory: path.resolve(rootDirectory, options.outputDirectory), - version: options.version - }); + const targets = options.target === "all" ? [...TARGETS] : [options.target]; - process.stdout.write( - `Created ${path.relative(rootDirectory, result.archivePath)} for version ${result.version}\n` + - `SHA-256 ${result.digest}\n` - ); + for (const target of targets) { + const result = await packageExtension({ + rootDirectory, + outputDirectory: path.resolve(rootDirectory, options.outputDirectory), + target, + version: options.version + }); + + process.stdout.write( + `Created ${path.relative(rootDirectory, result.archivePath)} for ${target} ` + + `version ${result.version}\nSHA-256 ${result.digest}\n` + ); + } } if (require.main === module) { @@ -203,4 +281,9 @@ if (require.main === module) { }); } -module.exports = { ARCHIVE_FILES, normalizeVersion, packageExtension }; +module.exports = { + CHROMIUM_ARCHIVE_FILES, + FIREFOX_ARCHIVE_FILES, + normalizeVersion, + packageExtension +}; diff --git a/tests/background.test.js b/tests/background.test.js index afccd14..4e8fc61 100644 --- a/tests/background.test.js +++ b/tests/background.test.js @@ -4,12 +4,14 @@ const test = require("node:test"); const assert = require("node:assert/strict"); const { createBackgroundController } = require("../background.js"); -function makeBrowser(initialStorage = {}) { +function makeBrowser(initialStorage = {}, initialSessionStorage = {}) { let messageListener; let removedListener; const broadcasts = []; const stored = { ...initialStorage }; + const sessionStored = { ...initialSessionStorage }; const storageWrites = []; + const sessionWrites = []; const api = { runtime: { onMessage: { @@ -27,6 +29,15 @@ function makeBrowser(initialStorage = {}) { storageWrites.push({ ...values }); Object.assign(stored, values); } + }, + session: { + async get(key) { + return { [key]: sessionStored[key] }; + }, + async set(values) { + sessionWrites.push(structuredClone(values)); + Object.assign(sessionStored, structuredClone(values)); + } } }, tabs: { @@ -45,13 +56,24 @@ function makeBrowser(initialStorage = {}) { return { api, broadcasts, + sessionStored, + sessionWrites, stored, storageWrites, send(message, sender = {}) { - return messageListener(message, sender); + return new Promise((resolve) => { + let responded = false; + const keepChannelOpen = messageListener(message, sender, (response) => { + responded = true; + resolve(response); + }); + if (!keepChannelOpen && !responded) { + resolve(undefined); + } + }); }, remove(tabId) { - removedListener(tabId); + return removedListener(tabId); } }; } @@ -91,10 +113,41 @@ test("owns absolute state per tab and supplies it to new frames", async () => { { enabled: false, fontStyle: "round" } ); - fake.remove(17); + await fake.remove(17); assert.equal(controller.tabStates.has(17), false); }); +test("restores tab state after an MV3 background worker restart", async () => { + const firstWorker = makeBrowser(); + createBackgroundController(firstWorker.api); + await firstWorker.send({ + type: "glagolify:set-tab-enabled", + tabId: 23, + enabled: true + }); + + assert.deepEqual(firstWorker.sessionStored, { + glagolifyTabStates: { 23: true } + }); + + const restartedWorker = makeBrowser({}, firstWorker.sessionStored); + createBackgroundController(restartedWorker.api); + assert.deepEqual( + await restartedWorker.send( + { type: "glagolify:document-ready" }, + { tab: { id: 23 } } + ), + { enabled: true, fontStyle: "round" } + ); + assert.deepEqual( + await restartedWorker.send({ + type: "glagolify:get-tab-state", + tabId: 23 + }), + { enabled: true, available: true, fontStyle: "round" } + ); +}); + test("keeps states isolated between tabs and reports protected tabs", async () => { const fake = makeBrowser(); createBackgroundController(fake.api); diff --git a/tests/package-extension.test.js b/tests/package-extension.test.js index 535e860..3d1798d 100644 --- a/tests/package-extension.test.js +++ b/tests/package-extension.test.js @@ -8,7 +8,8 @@ const os = require("node:os"); const path = require("node:path"); const { inflateRawSync } = require("node:zlib"); const { - ARCHIVE_FILES, + CHROMIUM_ARCHIVE_FILES, + FIREFOX_ARCHIVE_FILES, normalizeVersion, packageExtension } = require("../scripts/package-extension.js"); @@ -72,8 +73,12 @@ test("builds a deterministic unsigned XPI with the release version and runtime f const entries = readZipEntries(firstArchive); assert.equal(first.archiveName, "glagolify-9.8.7.xpi"); - assert.deepEqual([...entries.keys()], [...ARCHIVE_FILES]); - assert.equal(JSON.parse(entries.get("manifest.json")).version, "9.8.7"); + assert.deepEqual([...entries.keys()], [...FIREFOX_ARCHIVE_FILES]); + 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); @@ -84,3 +89,76 @@ test("builds a deterministic unsigned XPI with the release version and runtime f `${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, [""]); + 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: [""] + } + ]); + assert.equal(manifest.icons["128"], "icons/icon-128.png"); + + 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/ + ); +});