1 Commits
Author SHA1 Message Date
StanPonomarev df16075421 chromium support
Release Firefox and Chromium extensions / Package and attach browser extensions (release) Successful in 54s
2026-08-30 21:00:05 +02:00
14 changed files with 350 additions and 57 deletions
+5 -5
View File
@@ -1,4 +1,4 @@
name: Release unsigned Firefox extension name: Release Firefox and Chromium extensions
on: on:
release: release:
@@ -11,7 +11,7 @@ permissions:
jobs: jobs:
package-and-attach: package-and-attach:
name: Package and attach unsigned XPI name: Package and attach browser extensions
runs-on: ubuntu-latest runs-on: ubuntu-latest
timeout-minutes: 10 timeout-minutes: 10
@@ -30,14 +30,14 @@ jobs:
- name: Run automated checks - name: Run automated checks
run: npm test run: npm test
- name: Build unsigned release archive - name: Build unsigned release archives
env: env:
GITEA_RELEASE_TAG: ${{ gitea.event.release.tag_name }} GITEA_RELEASE_TAG: ${{ gitea.event.release.tag_name }}
run: npm run package -- --version "${GITEA_RELEASE_TAG}" --output-dir dist run: npm run package -- --version "${GITEA_RELEASE_TAG}" --output-dir dist
- name: Verify release checksum - name: Verify release checksum
working-directory: dist working-directory: dist
run: sha256sum --check *.xpi.sha256 run: sha256sum --check *.sha256
- name: Attach release assets - name: Attach release assets
env: env:
@@ -47,7 +47,7 @@ jobs:
GITEA_RELEASE_ID: ${{ gitea.event.release.id }} GITEA_RELEASE_ID: ${{ gitea.event.release.id }}
run: | run: |
set -eu 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}")" name="$(basename "${asset}")"
curl --fail-with-body --silent --show-error \ curl --fail-with-body --silent --show-error \
--request POST \ --request POST \
+29 -2
View File
@@ -1,11 +1,13 @@
# Glagolify # 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. 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 ## Install
### Firefox
Glagolify requires Firefox 142 or newer. Glagolify requires Firefox 142 or newer.
1. Download the latest `.xpi` from [Releases](https://git.krtss.de/StanPonomarev/glagolify/releases). 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. 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 ## 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. 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-<version>.xpi` for Firefox and `dist/glagolify-<version>-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.
+59 -14
View File
@@ -2,6 +2,7 @@
"use strict"; "use strict";
const FONT_STYLE_KEY = "glagolifyFontStyle"; const FONT_STYLE_KEY = "glagolifyFontStyle";
const TAB_STATES_KEY = "glagolifyTabStates";
const FONT_STYLES = new Set(["round", "angular-sans", "angular", "missal-angular"]); const FONT_STYLES = new Set(["round", "angular-sans", "angular", "missal-angular"]);
const DEFAULT_FONT_STYLE = "round"; const DEFAULT_FONT_STYLE = "round";
@@ -13,14 +14,35 @@
if (stored && FONT_STYLES.has(stored[FONT_STYLE_KEY])) { if (stored && FONT_STYLES.has(stored[FONT_STYLE_KEY])) {
fontStyle = 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) { function validTabId(tabId) {
return Number.isInteger(tabId) && tabId >= 0; 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) { async function broadcast(tabId, enabled) {
await fontStyleReady; await fontStyleReady;
try { try {
@@ -35,14 +57,14 @@
} }
} }
api.runtime.onMessage.addListener((message, sender) => { function handleMessage(message, sender) {
if (!message || typeof message.type !== "string") { if (!message || typeof message.type !== "string") {
return undefined; return undefined;
} }
if (message.type === "glagolify:document-ready") { if (message.type === "glagolify:document-ready") {
const tabId = sender && sender.tab && sender.tab.id; const tabId = sender && sender.tab && sender.tab.id;
return fontStyleReady.then(() => ({ return controllerReady.then(() => ({
enabled: validTabId(tabId) && tabStates.get(tabId) === true, enabled: validTabId(tabId) && tabStates.get(tabId) === true,
fontStyle fontStyle
})); }));
@@ -50,7 +72,7 @@
if (message.type === "glagolify:get-tab-state") { if (message.type === "glagolify:get-tab-state") {
const tabId = message.tabId; const tabId = message.tabId;
return fontStyleReady.then(async () => { return controllerReady.then(async () => {
if (!validTabId(tabId)) { if (!validTabId(tabId)) {
return { enabled: false, available: false, fontStyle }; return { enabled: false, available: false, fontStyle };
} }
@@ -64,15 +86,16 @@
if (message.type === "glagolify:set-tab-enabled") { if (message.type === "glagolify:set-tab-enabled") {
const tabId = message.tabId; const tabId = message.tabId;
if (!validTabId(tabId) || typeof message.enabled !== "boolean") { if (!validTabId(tabId) || typeof message.enabled !== "boolean") {
return fontStyleReady.then(() => ({ return controllerReady.then(() => ({
enabled: false, enabled: false,
available: false, available: false,
fontStyle fontStyle
})); }));
} }
return fontStyleReady.then(async () => { return controllerReady.then(async () => {
tabStates.set(tabId, message.enabled); tabStates.set(tabId, message.enabled);
await persistTabStates();
const available = await broadcast(tabId, message.enabled); const available = await broadcast(tabId, message.enabled);
return { return {
enabled: message.enabled, enabled: message.enabled,
@@ -84,10 +107,10 @@
if (message.type === "glagolify:set-font-style") { if (message.type === "glagolify:set-font-style") {
if (!FONT_STYLES.has(message.fontStyle)) { 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 }); await api.storage.local.set({ [FONT_STYLE_KEY]: message.fontStyle });
fontStyle = message.fontStyle; fontStyle = message.fontStyle;
await Promise.all( await Promise.all(
@@ -100,24 +123,46 @@
} }
return undefined; 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) => { api.tabs.onRemoved.addListener(async (tabId) => {
tabStates.delete(tabId); await tabStatesReady;
if (tabStates.delete(tabId)) {
await persistTabStates();
}
}); });
return { return {
tabStates, tabStates,
broadcast, broadcast,
fontStyleReady, controllerReady,
getFontStyle: () => fontStyle getFontStyle: () => fontStyle
}; };
} }
const exported = Object.freeze({ createBackgroundController }); const exported = Object.freeze({ createBackgroundController });
if (typeof browser === "object" && browser.runtime && browser.tabs) { const extensionApi =
createBackgroundController(browser); typeof browser === "object"
? browser
: typeof chrome === "object"
? chrome
: null;
if (extensionApi && extensionApi.runtime && extensionApi.tabs) {
createBackgroundController(extensionApi);
} }
if (typeof module === "object" && module.exports) { if (typeof module === "object" && module.exports) {
+7 -4
View File
@@ -1,6 +1,8 @@
(function () { (function () {
"use strict"; "use strict";
const api = typeof browser === "object" ? browser : chrome;
const EXCLUDED_ANCESTORS = "head, script, style, noscript, template, textarea, select, option"; const EXCLUDED_ANCESTORS = "head, script, style, noscript, template, textarea, select, option";
const FONT_FAMILIES = Object.freeze({ const FONT_FAMILIES = Object.freeze({
round: '"Glagolify Shafarik Round"', round: '"Glagolify Shafarik Round"',
@@ -330,14 +332,15 @@
return { enabled }; 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") { 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) => { .then((response) => {
setEnabled(Boolean(response && response.enabled), response && response.fontStyle); setEnabled(Boolean(response && response.enabled), response && response.fontStyle);
}) })
Binary file not shown.

After

Width:  |  Height:  |  Size: 8.2 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 623 B

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

+4 -2
View File
@@ -1,10 +1,12 @@
{ {
"name": "glagolify-firefox-extension", "name": "glagolify-extension",
"version": "1.1.0", "version": "1.1.0",
"private": true, "private": true,
"description": "A dependency-free Firefox extension for toggling modern Russian text to Glagolitic.", "description": "A dependency-free Firefox and Chromium extension for toggling modern Russian text to Glagolitic.",
"scripts": { "scripts": {
"package": "node scripts/package-extension.js", "package": "node scripts/package-extension.js",
"package:firefox": "node scripts/package-extension.js --target firefox",
"package:chromium": "node scripts/package-extension.js --target chromium",
"test": "node --test" "test": "node --test"
} }
} }
+5 -3
View File
@@ -1,6 +1,8 @@
(function () { (function () {
"use strict"; "use strict";
const api = typeof browser === "object" ? browser : chrome;
const status = document.getElementById("status"); const status = document.getElementById("status");
const toggle = document.getElementById("toggle"); const toggle = document.getElementById("toggle");
const fontStyleInputs = [...document.querySelectorAll('input[name="font-style"]')]; const fontStyleInputs = [...document.querySelectorAll('input[name="font-style"]')];
@@ -39,11 +41,11 @@
toggle.disabled = true; toggle.disabled = true;
toggle.setAttribute("aria-pressed", "false"); toggle.setAttribute("aria-pressed", "false");
toggle.textContent = "Unavailable on this page"; toggle.textContent = "Unavailable on this page";
status.textContent = "Firefox protects this page from extensions."; status.textContent = "The browser protects this page from extensions.";
} }
async function askBackground(type, extra = {}) { async function askBackground(type, extra = {}) {
return browser.runtime.sendMessage({ type, ...extra }); return api.runtime.sendMessage({ type, ...extra });
} }
toggle.addEventListener("click", async () => { toggle.addEventListener("click", async () => {
@@ -104,7 +106,7 @@
async function initialize() { async function initialize() {
try { 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") { if (!tabs[0] || typeof tabs[0].id !== "number") {
showUnavailable(); showUnavailable();
return; return;
+96 -13
View File
@@ -5,7 +5,7 @@ const { mkdir, readFile, writeFile } = require("node:fs/promises");
const path = require("node:path"); const path = require("node:path");
const { deflateRawSync } = require("node:zlib"); const { deflateRawSync } = require("node:zlib");
const ARCHIVE_FILES = Object.freeze([ const FIREFOX_ARCHIVE_FILES = Object.freeze([
"background.js", "background.js",
"content.css", "content.css",
"content.js", "content.js",
@@ -23,6 +23,14 @@ const ARCHIVE_FILES = Object.freeze([
"popup.js", "popup.js",
"transliterate.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 VERSION_PATTERN = /^(0|[1-9][0-9]{0,8})(\.(0|[1-9][0-9]{0,8})){0,3}$/;
const UTF8_FLAG = 0x0800; const UTF8_FLAG = 0x0800;
@@ -46,6 +54,47 @@ function normalizeVersion(value) {
return version; 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 === "<all_urls>" || 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: ["<all_urls>"]
}
];
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) { function crc32(buffer) {
let crc = 0xffffffff; let crc = 0xffffffff;
@@ -122,19 +171,29 @@ function createZip(entries) {
return Buffer.concat([...localRecords, centralDirectory, endRecord]); 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( const packageMetadata = JSON.parse(
await readFile(path.join(rootDirectory, "package.json"), "utf8") await readFile(path.join(rootDirectory, "package.json"), "utf8")
); );
const version = normalizeVersion(requestedVersion ?? packageMetadata.version); const version = normalizeVersion(requestedVersion ?? packageMetadata.version);
const archiveFiles =
target === "firefox" ? FIREFOX_ARCHIVE_FILES : CHROMIUM_ARCHIVE_FILES;
const entries = []; const entries = [];
for (const relativePath of ARCHIVE_FILES) { for (const relativePath of archiveFiles) {
let contents = await readFile(path.join(rootDirectory, relativePath)); let contents = await readFile(path.join(rootDirectory, relativePath));
if (relativePath === "manifest.json") { if (relativePath === "manifest.json") {
const manifest = JSON.parse(contents.toString("utf8")); const manifest = createManifest(contents, version, target);
manifest.version = version;
contents = Buffer.from(`${JSON.stringify(manifest, null, 2)}\n`); 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 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 archivePath = path.join(outputDirectory, archiveName);
const checksumPath = `${archivePath}.sha256`; const checksumPath = `${archivePath}.sha256`;
const digest = createHash("sha256").update(archive).digest("hex"); 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(archivePath, archive);
await writeFile(checksumPath, `${digest} ${archiveName}\n`); await writeFile(checksumPath, `${digest} ${archiveName}\n`);
return { archiveName, archivePath, checksumPath, digest, version }; return { archiveName, archivePath, checksumPath, digest, target, version };
} }
function parseArguments(arguments_) { function parseArguments(arguments_) {
let outputDirectory = "dist"; let outputDirectory = "dist";
let target = "all";
let version; let version;
for (let index = 0; index < arguments_.length; index += 1) { for (let index = 0; index < arguments_.length; index += 1) {
const argument = arguments_[index]; const argument = arguments_[index];
if (argument === "--version" || argument === "--output-dir") { if (
argument === "--version" ||
argument === "--output-dir" ||
argument === "--target"
) {
const value = arguments_[index + 1]; const value = arguments_[index + 1];
if (!value || value.startsWith("--")) { if (!value || value.startsWith("--")) {
throw new Error(`${argument} requires a value.`); throw new Error(`${argument} requires a value.`);
@@ -169,8 +236,10 @@ function parseArguments(arguments_) {
if (argument === "--version") { if (argument === "--version") {
version = value; version = value;
} else { } else if (argument === "--output-dir") {
outputDirectory = value; outputDirectory = value;
} else {
target = value;
} }
index += 1; index += 1;
} else { } 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() { async function main() {
const rootDirectory = path.resolve(__dirname, ".."); const rootDirectory = path.resolve(__dirname, "..");
const options = parseArguments(process.argv.slice(2)); const options = parseArguments(process.argv.slice(2));
const targets = options.target === "all" ? [...TARGETS] : [options.target];
for (const target of targets) {
const result = await packageExtension({ const result = await packageExtension({
rootDirectory, rootDirectory,
outputDirectory: path.resolve(rootDirectory, options.outputDirectory), outputDirectory: path.resolve(rootDirectory, options.outputDirectory),
target,
version: options.version version: options.version
}); });
process.stdout.write( process.stdout.write(
`Created ${path.relative(rootDirectory, result.archivePath)} for version ${result.version}\n` + `Created ${path.relative(rootDirectory, result.archivePath)} for ${target} ` +
`SHA-256 ${result.digest}\n` `version ${result.version}\nSHA-256 ${result.digest}\n`
); );
}
} }
if (require.main === module) { 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
};
+57 -4
View File
@@ -4,12 +4,14 @@ const test = require("node:test");
const assert = require("node:assert/strict"); const assert = require("node:assert/strict");
const { createBackgroundController } = require("../background.js"); const { createBackgroundController } = require("../background.js");
function makeBrowser(initialStorage = {}) { function makeBrowser(initialStorage = {}, initialSessionStorage = {}) {
let messageListener; let messageListener;
let removedListener; let removedListener;
const broadcasts = []; const broadcasts = [];
const stored = { ...initialStorage }; const stored = { ...initialStorage };
const sessionStored = { ...initialSessionStorage };
const storageWrites = []; const storageWrites = [];
const sessionWrites = [];
const api = { const api = {
runtime: { runtime: {
onMessage: { onMessage: {
@@ -27,6 +29,15 @@ function makeBrowser(initialStorage = {}) {
storageWrites.push({ ...values }); storageWrites.push({ ...values });
Object.assign(stored, 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: { tabs: {
@@ -45,13 +56,24 @@ function makeBrowser(initialStorage = {}) {
return { return {
api, api,
broadcasts, broadcasts,
sessionStored,
sessionWrites,
stored, stored,
storageWrites, storageWrites,
send(message, sender = {}) { 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) { 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" } { enabled: false, fontStyle: "round" }
); );
fake.remove(17); await fake.remove(17);
assert.equal(controller.tabStates.has(17), false); 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 () => { test("keeps states isolated between tabs and reports protected tabs", async () => {
const fake = makeBrowser(); const fake = makeBrowser();
createBackgroundController(fake.api); createBackgroundController(fake.api);
+81 -3
View File
@@ -8,7 +8,8 @@ const os = require("node:os");
const path = require("node:path"); const path = require("node:path");
const { inflateRawSync } = require("node:zlib"); const { inflateRawSync } = require("node:zlib");
const { const {
ARCHIVE_FILES, CHROMIUM_ARCHIVE_FILES,
FIREFOX_ARCHIVE_FILES,
normalizeVersion, normalizeVersion,
packageExtension packageExtension
} = require("../scripts/package-extension.js"); } = 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); const entries = readZipEntries(firstArchive);
assert.equal(first.archiveName, "glagolify-9.8.7.xpi"); assert.equal(first.archiveName, "glagolify-9.8.7.xpi");
assert.deepEqual([...entries.keys()], [...ARCHIVE_FILES]); assert.deepEqual([...entries.keys()], [...FIREFOX_ARCHIVE_FILES]);
assert.equal(JSON.parse(entries.get("manifest.json")).version, "9.8.7"); 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("package.json"), false);
assert.equal(entries.has("tests/package-extension.test.js"), false); assert.equal(entries.has("tests/package-extension.test.js"), false);
assert.deepEqual(firstArchive, secondArchive); 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` `${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");
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/
);
});