diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index a188c38..88ba63d 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -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: @@ -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' diff --git a/packages/webextension/README.md b/packages/webextension/README.md index 61e9c5e..1d6bad4 100644 --- a/packages/webextension/README.md +++ b/packages/webextension/README.md @@ -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. diff --git a/packages/webextension/app/manifest.json b/packages/webextension/app/manifest.json index c175a68..0b0872c 100644 --- a/packages/webextension/app/manifest.json +++ b/packages/webextension/app/manifest.json @@ -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", @@ -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" @@ -34,11 +32,22 @@ "default_popup": "pages/popup.html" }, "web_accessible_resources": [ - "scripts/pageScript.js" + { + "resources": [ + "scripts/pageScript.js" + ], + "matches": [ + "" + ] + } ], - "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": [ "" ], "__firefox__applications": { diff --git a/packages/webextension/app/scripts/InstalledApp/StateContext.tsx b/packages/webextension/app/scripts/InstalledApp/StateContext.tsx index 31866f8..df006dd 100644 --- a/packages/webextension/app/scripts/InstalledApp/StateContext.tsx +++ b/packages/webextension/app/scripts/InstalledApp/StateContext.tsx @@ -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()); diff --git a/packages/webextension/app/scripts/InstalledApp/component/InstalledTextlintList.tsx b/packages/webextension/app/scripts/InstalledApp/component/InstalledTextlintList.tsx index 728ab39..6e4398e 100644 --- a/packages/webextension/app/scripts/InstalledApp/component/InstalledTextlintList.tsx +++ b/packages/webextension/app/scripts/InstalledApp/component/InstalledTextlintList.tsx @@ -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"; diff --git a/packages/webextension/app/scripts/background.ts b/packages/webextension/app/scripts/background.ts index 726083d..5fb377e 100644 --- a/packages/webextension/app/scripts/background.ts +++ b/packages/webextension/app/scripts/background.ts @@ -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"; @@ -25,7 +24,10 @@ listenOnTextlintWorkerJsUrl({ type ThenArg = T extends PromiseLike ? U : T; type DataBase = ThenArg>; -export type BackgroundToContentObject = LintEngineAPI; +export type BackgroundToContentObject = LintEngineAPI & { + // Get scripts + getScripts(): Promise; +}; export type BackgroundToPopupObject = { findScriptsWithPatten: DataBase["findScriptsWithPatten"]; findScriptsWithName: DataBase["findScriptsWithName"]; @@ -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 { - 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 { + 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 { throw new Error("No implement ignoreText on background"); + }, + async getScripts(): Promise { + 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"); }); diff --git a/packages/webextension/app/scripts/background/database.ts b/packages/webextension/app/scripts/background/database.ts index 035b3d0..af2ddfc 100644 --- a/packages/webextension/app/scripts/background/database.ts +++ b/packages/webextension/app/scripts/background/database.ts @@ -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); }; diff --git a/packages/webextension/app/scripts/background/onTextlintWorker.ts b/packages/webextension/app/scripts/background/onTextlintWorker.ts index 0d904b0..b507acd 100644 --- a/packages/webextension/app/scripts/background/onTextlintWorker.ts +++ b/packages/webextension/app/scripts/background/onTextlintWorker.ts @@ -1,4 +1,4 @@ -import { browser } from "webextension-polyfill-ts"; +import browser from "webextension-polyfill"; const isTextlintWorkerUrl = (urlString: string): boolean => { try { diff --git a/packages/webextension/app/scripts/contentScript.ts b/packages/webextension/app/scripts/contentScript.ts index 4e6476e..c55d173 100644 --- a/packages/webextension/app/scripts/contentScript.ts +++ b/packages/webextension/app/scripts/contentScript.ts @@ -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 @@ -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); @@ -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 { + 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; @@ -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 = []; +}); diff --git a/packages/webextension/app/scripts/background/scriptWorkerSet.ts b/packages/webextension/app/scripts/contentScript/scriptWorkerSet.ts similarity index 95% rename from packages/webextension/app/scripts/background/scriptWorkerSet.ts rename to packages/webextension/app/scripts/contentScript/scriptWorkerSet.ts index 42b4dd3..f5bf792 100644 --- a/packages/webextension/app/scripts/background/scriptWorkerSet.ts +++ b/packages/webextension/app/scripts/contentScript/scriptWorkerSet.ts @@ -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(); diff --git a/packages/webextension/app/scripts/background/textlint.ts b/packages/webextension/app/scripts/contentScript/textlint.ts similarity index 99% rename from packages/webextension/app/scripts/background/textlint.ts rename to packages/webextension/app/scripts/contentScript/textlint.ts index df9741f..d27b9f7 100644 --- a/packages/webextension/app/scripts/background/textlint.ts +++ b/packages/webextension/app/scripts/contentScript/textlint.ts @@ -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) => { diff --git a/packages/webextension/app/scripts/install-dialog.tsx b/packages/webextension/app/scripts/install-dialog.tsx index 3068ef3..b17eba4 100644 --- a/packages/webextension/app/scripts/install-dialog.tsx +++ b/packages/webextension/app/scripts/install-dialog.tsx @@ -1,7 +1,7 @@ -import { createEndpoint } from "comlink-extension"; +import { createEndpoint } from "../../comlink-extension/src"; import * as Comlink from "comlink"; import type { BackgroundToPopupObject } from "./background"; -import { browser } from "webextension-polyfill-ts"; +import browser from "webextension-polyfill"; import { parseMetadata, TextlintScriptMetadata } from "@textlint/script-parser"; import { logger } from "./utils/logger"; import * as React from "react"; diff --git a/packages/webextension/app/scripts/utils/script.ts b/packages/webextension/app/scripts/utils/script.ts new file mode 100644 index 0000000..978412f --- /dev/null +++ b/packages/webextension/app/scripts/utils/script.ts @@ -0,0 +1,19 @@ +export type Script = { + namespace: string; + name: string; + scriptUrl: string; + homepage: string; + version: string; + code: string; + ext: string; + textlintrc: string; + matchPattern: string; +}; + +/** + * Create unique key of Script + * @param script + */ +export const keyOfScript = (script: { name: string; namespace: string }): string => { + return `${script.namespace}@${script.name}`; +}; diff --git a/packages/webextension/diff.patch b/packages/webextension/diff.patch new file mode 100644 index 0000000..b59d8d9 --- /dev/null +++ b/packages/webextension/diff.patch @@ -0,0 +1,22 @@ +diff --git a/comlink-extension/src/adapter.ts b/comlink-extension/src/adapter.ts +index 11038b3..2b50a15 100644 +--- a/comlink-extension/src/adapter.ts ++++ b/comlink-extension/src/adapter.ts +@@ -27,7 +27,7 @@ export function createEndpoint( + + function serialize(data: any): void { + if (Array.isArray(data)) { +- data.forEach((value, i) => { ++ data.forEach((value) => { + serialize(value); + }); + } else if (data && typeof data === "object") { +@@ -97,7 +97,7 @@ export function createEndpoint( + } + + return { +- postMessage: (message, transfer: MessagePort[]) => { ++ postMessage: (message) => { + serialize(message); + port.postMessage(message); + }, diff --git a/packages/webextension/webextension-toolbox.config.js b/packages/webextension/webextension-toolbox.config.js index 0923952..f7cb921 100644 --- a/packages/webextension/webextension-toolbox.config.js +++ b/packages/webextension/webextension-toolbox.config.js @@ -37,5 +37,5 @@ module.exports = { // Important: return the modified config return config; }, - copyIgnore: ["**/*.js", "**/*.json", "**/*.ts", "**/*.tsx"] + copyIgnore: ["**/*.js", "**/*.ts", "**/*.tsx"] };