Skip to content

Commit

Permalink
fix: Chrome extension Manifest v3 support for webextension
Browse files Browse the repository at this point in the history
  • Loading branch information
massongit committed Dec 10, 2024
1 parent 383442d commit 738e948
Show file tree
Hide file tree
Showing 15 changed files with 175 additions and 120 deletions.
6 changes: 6 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ jobs:
steps:
- name: checkout
uses: actions/checkout@v4
with:
submodules: true
- name: setup Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4
with:
Expand All @@ -24,6 +26,10 @@ jobs:
run: yarn install
- name: Test
run: yarn test
- name: Apply patch
run: patch -p1 < diff.patch
if: matrix.os == 'ubuntu-latest'
working-directory: packages/webextension
- name: Build webextension (Chrome)
run: npm run dist chrome
if: matrix.os == 'ubuntu-latest'
Expand Down
3 changes: 3 additions & 0 deletions packages/webextension/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ textlint editor
yarn install
yarn run build

# on this dir
patch -p1 < diff.patch

### Install textlint scripts

textlint editor install your textlint scripts like Greasemonkey.
Expand Down
25 changes: 17 additions & 8 deletions packages/webextension/app/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"short_name": "__MSG_appShortName__",
"description": "__MSG_appDescription__",
"version": "0.12.6",
"manifest_version": 2,
"manifest_version": 3,
"default_locale": "en",
"icons": {
"16": "images/icon-16.png",
Expand All @@ -21,11 +21,9 @@
}
],
"background": {
"scripts": [
"scripts/background.js"
]
"service_worker": "scripts/background.js"
},
"browser_action": {
"action": {
"default_icon": {
"19": "images/icon-19.png",
"38": "images/icon-38.png"
Expand All @@ -34,11 +32,22 @@
"default_popup": "pages/popup.html"
},
"web_accessible_resources": [
"scripts/pageScript.js"
{
"resources": [
"scripts/pageScript.js"
],
"matches": [
"<all_urls>"
]
}
],
"content_security_policy": "script-src 'self'; object-src 'self'; worker-src 'self' blob:",
"content_security_policy": {
"extension_pages": "script-src 'self'; object-src 'self'; worker-src 'self'"
},
"permissions": [
"tabs",
"tabs"
],
"host_permissions": [
"<all_urls>"
],
"__firefox__applications": {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
// src/contexts/AppStateContext.tsx
import React, { Dispatch, SetStateAction, useContext, useState } from "react";
import { browser } from "webextension-polyfill-ts";
import browser from "webextension-polyfill";
import * as Comlink from "comlink";
import { forward } from "comlink-extension";
import { forward } from "../../../comlink-extension/src";
import { BackgroundToPopupObject } from "../background";
const { port1, port2 } = new MessageChannel();
forward(port1, browser.runtime.connect());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { useAsyncList } from "@react-stately/data";
import React from "react";
import { ActionGroup, Flex, Item, ListBox, Text } from "@adobe/react-spectrum";
import { usePort } from "../StateContext";
import { Script } from "../../background/database";
import { Script } from "../../utils/script";
import FileCode from "@spectrum-icons/workflow/FileCode";
import { logger } from "../../utils/logger";

Expand Down
94 changes: 16 additions & 78 deletions packages/webextension/app/scripts/background.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
import { browser } from "webextension-polyfill-ts";
import { createBackgroundEndpoint, isMessagePort } from "comlink-extension";
import browser from "webextension-polyfill";
import { createBackgroundEndpoint, isMessagePort } from "../../comlink-extension/src";
import * as Comlink from "comlink";
import { createTextlintWorker } from "./background/textlint";
import { keyOfScript, openDatabase, Script } from "./background/database";
import { openDatabase } from "./background/database";
import { Script } from "./utils/script";
import { LintEngineAPI } from "textchecker-element";
import { TextlintResult } from "@textlint/types";
import { scriptWorkerSet } from "./background/scriptWorkerSet";
import { logger } from "./utils/logger";
import { listenOnTextlintWorkerJsUrl } from "./background/onTextlintWorker";

Expand All @@ -25,7 +24,10 @@ listenOnTextlintWorkerJsUrl({

type ThenArg<T> = T extends PromiseLike<infer U> ? U : T;
type DataBase = ThenArg<ReturnType<typeof openDatabase>>;
export type BackgroundToContentObject = LintEngineAPI;
export type BackgroundToContentObject = LintEngineAPI & {
// Get scripts
getScripts(): Promise<Script[]>;
};
export type BackgroundToPopupObject = {
findScriptsWithPatten: DataBase["findScriptsWithPatten"];
findScriptsWithName: DataBase["findScriptsWithName"];
Expand Down Expand Up @@ -80,85 +82,21 @@ browser.runtime.onConnect.addListener(async (port) => {
};
return Comlink.expose(exports, createBackgroundEndpoint(port));
}
// release after close connection
let scripts = await db.findScriptsWithPatten(originUrl);
const getWorker = (script: Script) => {
const runningWorker = scriptWorkerSet.get(script);
if (runningWorker) {
return {
worker: runningWorker,
ext: script.ext
};
}
logger.log("Start worker", keyOfScript(script));
const textlintWorker = createTextlintWorker(script);
scriptWorkerSet.set({ script, worker: textlintWorker });
return {
worker: textlintWorker,
ext: script.ext
};
};
// get script workers which are ready to work
const readyScriptWorkers = async () => {
const scriptWorkers = scripts.map((script) => {
return getWorker(script);
});
await Promise.all(scriptWorkers.map((worker) => worker.worker.ready()));
return scriptWorkers;
};
const closeScriptWorkers = () => {
scripts.forEach((script) => {
const deleted = scriptWorkerSet.delete({ script });
if (deleted) {
logger.log("Success to delete worker", keyOfScript(script));
} else {
logger.log("Fail to delete worker", keyOfScript(script));
}
});
};
// Support multiple workers
const lintEngine: LintEngineAPI = {
async lintText({ text }: { text: string }): Promise<TextlintResult[]> {
logger.log("text:", text);
const scriptWorkers = await readyScriptWorkers();
const allLintResults = await Promise.all(
scriptWorkers.map(({ worker, ext }) => {
return worker.createLintEngine({ ext }).lintText({ text });
})
);
logger.log("lintText", allLintResults);
return allLintResults.flat();
const lintEngine: BackgroundToContentObject = {
async lintText(): Promise<TextlintResult[]> {
throw new Error("No implement lintText on background");
},
async fixText({ text }): Promise<{ output: string }> {
let output = text;
const scriptWorkers = await readyScriptWorkers();
for (const { worker, ext } of scriptWorkers) {
await worker
.createLintEngine({ ext })
.fixText({ text: output, messages: [] })
.then((result) => {
output = result.output;
return result;
});
}
return {
output
};
async fixText(): Promise<{ output: string }> {
throw new Error("No implement fixText on background");
},
async ignoreText(): Promise<boolean> {
throw new Error("No implement ignoreText on background");
},
async getScripts(): Promise<Script[]> {
return await db.findScriptsWithPatten(originUrl);
}
};
port.onDisconnect.addListener(async () => {
logger.log("dispose worker - close workers");
// When some tab close, the related worker will disposed.
// It aims to reduce memory leak
// https://github.com/textlint/editor/issues/52
// scriptWorker will re-start when call `lint` or `fix` api automatically
closeScriptWorkers();
// Release reference - force GC
scripts = [];
});
Comlink.expose(lintEngine, createBackgroundEndpoint(port));
port.postMessage("textlint-editor-boot");
});
20 changes: 1 addition & 19 deletions packages/webextension/app/scripts/background/database.ts
Original file line number Diff line number Diff line change
@@ -1,28 +1,10 @@
import { kvsEnvStorage } from "@kvs/env";
import minimatch from "minimatch";

export type Script = {
namespace: string;
name: string;
scriptUrl: string;
homepage: string;
version: string;
code: string;
ext: string;
textlintrc: string;
matchPattern: string;
};
import { keyOfScript, type Script } from "../utils/script";

export type TextlintDBSchema = {
scripts: Script[];
};
/**
* Create unique key of Script
* @param script
*/
export const keyOfScript = (script: { name: string; namespace: string }): string => {
return `${script.namespace}@${script.name}`;
};
const equalScript = (a: { name: string; namespace: string }, b: { name: string; namespace: string }): boolean => {
return keyOfScript(a) === keyOfScript(b);
};
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { browser } from "webextension-polyfill-ts";
import browser from "webextension-polyfill";

const isTextlintWorkerUrl = (urlString: string): boolean => {
try {
Expand Down
88 changes: 82 additions & 6 deletions packages/webextension/app/scripts/contentScript.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
import { LintEngineAPI } from "textchecker-element";
import { browser } from "webextension-polyfill-ts";
import { createEndpoint } from "comlink-extension";
import browser from "webextension-polyfill";
import { TextlintResult } from "@textlint/types";
import { createEndpoint } from "../../comlink-extension/src";
import * as Comlink from "comlink";
import type { BackgroundToContentObject } from "./background";
import { scriptWorkerSet } from "./contentScript/scriptWorkerSet";
import { createTextlintWorker } from "./contentScript/textlint";
import { nonRandomKey } from "./shared/page-contents-shared";
import { logger } from "./utils/logger";
import { keyOfScript, type Script } from "./utils/script";

const rawPort = browser.runtime.connect();
// content-script <-> background page
Expand All @@ -14,7 +18,7 @@ rawPort.onMessage.addListener((event) => {
logger.log("[ContentScript]", "boot event received");
// Inject page-script
try {
const script = browser.extension.getURL("scripts/pageScript.js");
const script = browser.runtime.getURL("scripts/pageScript.js");
const pageScript = document.createElement("script");
pageScript.src = script;
document.body.append(pageScript);
Expand All @@ -23,17 +27,69 @@ rawPort.onMessage.addListener((event) => {
}
}
});
// release after close connection
let scripts: Script[] = [];
// page-script <-> content-script
window.addEventListener("message", (event) => {
window.addEventListener("message", async (event) => {
if (
event.source == window &&
event.data &&
event.data.direction == "from-page-script" &&
event.data.nonRandomKey === nonRandomKey
) {
const getWorker = (script: Script) => {
const runningWorker = scriptWorkerSet.get(script);
if (runningWorker) {
return {
worker: runningWorker,
ext: script.ext
};
}
logger.log("Start worker", keyOfScript(script));
const textlintWorker = createTextlintWorker(script);
scriptWorkerSet.set({ script, worker: textlintWorker });
return {
worker: textlintWorker,
ext: script.ext
};
};
scripts = await port.getScripts();
// get script workers which are ready to work
const readyScriptWorkers = async () => {
const scriptWorkers = scripts.map((script) => {
return getWorker(script);
});
await Promise.all(scriptWorkers.map((worker) => worker.worker.ready()));
return scriptWorkers;
};
const lintEngine: LintEngineAPI = {
lintText: port.lintText,
fixText: port.fixText,
async lintText({ text }: { text: string }): Promise<TextlintResult[]> {
logger.log("text:", text);
const scriptWorkers = await readyScriptWorkers();
const allLintResults = await Promise.all(
scriptWorkers.map(({ worker, ext }) => {
return worker.createLintEngine({ ext }).lintText({ text });
})
);
logger.log("lintText", allLintResults);
return allLintResults.flat();
},
async fixText({ text }): Promise<{ output: string }> {
let output = text;
const scriptWorkers = await readyScriptWorkers();
for (const { worker, ext } of scriptWorkers) {
await worker
.createLintEngine({ ext })
.fixText({ text: output, messages: [] })
.then((result: { output: string }) => {
output = result.output;
return result;
});
}
return {
output
};
},
ignoreText: port.ignoreText
};
const command = event.data.command as keyof typeof lintEngine;
Expand All @@ -54,3 +110,23 @@ window.addEventListener("message", (event) => {
}
}
});
const closeScriptWorkers = () => {
scripts.forEach((script) => {
const deleted = scriptWorkerSet.delete({ script });
if (deleted) {
logger.log("Success to delete worker", keyOfScript(script));
} else {
logger.log("Fail to delete worker", keyOfScript(script));
}
});
};
rawPort.onDisconnect.addListener(async () => {
logger.log("dispose worker - close workers");
// When some tab close, the related worker will disposed.
// It aims to reduce memory leak
// https://github.com/textlint/editor/issues/52
// scriptWorker will re-start when call `lint` or `fix` api automatically
closeScriptWorkers();
// Release reference - force GC
scripts = [];
});
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { TextlintWorker } from "./textlint";
import { keyOfScript, Script } from "./database";
import { keyOfScript, Script } from "../utils/script";
import { logger } from "../utils/logger";

const workerMap = new Map<string, TextlintWorker>();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import {
TextlintWorkerCommandResponse
} from "@textlint/script-compiler";
import type { TextlintRcConfig } from "@textlint/config-loader";
import { Script } from "./database";
import { Script } from "../utils/script";
import { logger } from "../utils/logger";

const waiterForInit = (worker: Worker) => {
Expand Down
Loading

0 comments on commit 738e948

Please sign in to comment.