From 1320cc3d6a6fba03987a9b12703e88d16b2fff0f Mon Sep 17 00:00:00 2001 From: cyfung1031 <44498510+cyfung1031@users.noreply.github.com> Date: Sun, 8 Feb 2026 01:57:37 +0900 Subject: [PATCH 01/18] enhanced support for cross-browser --- rspack.config.ts | 12 +++++++++--- scripts/pack.js | 13 ++++++++++--- src/pkg/utils/monaco-editor/index.ts | 12 ++++++++---- src/pkg/utils/monaco-editor/utils.ts | 23 +++++++++++++++++++++++ 4 files changed, 50 insertions(+), 10 deletions(-) diff --git a/rspack.config.ts b/rspack.config.ts index 6409878d5..4a3b90857 100644 --- a/rspack.config.ts +++ b/rspack.config.ts @@ -62,7 +62,12 @@ export default defineConfig({ }, output: { path: `${dist}/ext/src`, - filename: "[name].js", + filename(pathData, _assetInfo) { + if (pathData.runtime === "ts.worker") { + return "[name].js.bin"; + } + return "[name].js"; + }, clean: true, }, resolve: { @@ -229,6 +234,7 @@ export default defineConfig({ optimization: { minimizer: [ new rspack.SwcJsMinimizerRspackPlugin({ + test: /\.[cm]?js(\.bin)?(\?.*)?$/, minimizerOptions: { minify: !isDev, mangle: { @@ -243,7 +249,7 @@ export default defineConfig({ passes: 2, drop_console: false, drop_debugger: !isDev, - ecma: 2020, + ecma: 2022, arrows: true, dead_code: true, ie8: false, @@ -261,7 +267,7 @@ export default defineConfig({ format: { comments: false, beautify: false, - ecma: 2020, + ecma: 2022, }, }, }), diff --git a/scripts/pack.js b/scripts/pack.js index 26d0f79a2..85f66307a 100644 --- a/scripts/pack.js +++ b/scripts/pack.js @@ -89,6 +89,15 @@ firefoxManifest.browser_specific_settings = { // https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/userScripts#browser_compatibility // Firefox 136 (Released 2025-03-04) strict_min_version: "136.0", + data_collection_permissions: { + required: [ + "none", // 没有必须传送至第三方的资料。安装转页没有记录用户何时何地安装了什么。 + ], + optional: [ + "authenticationInfo", // 使用 Cloud Backup / Import 时,有传送用户的资料至第三方作登入验证 + "personallyIdentifyingInfo", // 使用 电邮 或 帐密 让第三方识别个人身份进行 Cloud Backup / Import + ], + }, }, }; @@ -126,10 +135,8 @@ firefox.file("manifest.json", JSON.stringify(firefoxManifest)); await Promise.all([ addDir(chrome, "./dist/ext", "", ["manifest.json"]), - addDir(firefox, "./dist/ext", "", ["manifest.json", "ts.worker.js"]), + addDir(firefox, "./dist/ext", "", ["manifest.json"]), ]); -// 添加ts.worker.js名字为gz -firefox.file("src/ts.worker.js.gz", await fs.readFile("./dist/ext/src/ts.worker.js", { encoding: "utf8" })); // 导出zip包 chrome diff --git a/src/pkg/utils/monaco-editor/index.ts b/src/pkg/utils/monaco-editor/index.ts index 6c91de174..8a055c44f 100644 --- a/src/pkg/utils/monaco-editor/index.ts +++ b/src/pkg/utils/monaco-editor/index.ts @@ -1,7 +1,7 @@ import { globalCache, systemConfig } from "@App/pages/store/global"; import EventEmitter from "eventemitter3"; import { languages } from "monaco-editor"; -import { findGlobalInsertionInfo, updateGlobalCommentLine } from "./utils"; +import { findGlobalInsertionInfo, getEditorWorkerPromise, getTsWorkerPromise, updateGlobalCommentLine } from "./utils"; // 注册eslint const linterWorker = new Worker("/src/linter.worker.js"); @@ -455,12 +455,16 @@ type Prompt = (typeof langs)["zh-CN"]["prompt"]; type LangEntry = (typeof langs)["zh-CN"]; export default function registerEditor() { + const editorWorker = getEditorWorkerPromise(); + const tsWorkerPromise = getTsWorkerPromise(); window.MonacoEnvironment = { - getWorkerUrl(moduleId: any, label: any) { + // https://microsoft.github.io/monaco-editor/typedoc/interfaces/Environment.html#getWorker + // Returns Worker | Promise + getWorker(workerId: string, label: string) { if (label === "typescript" || label === "javascript") { - return "/src/ts.worker.js"; + return tsWorkerPromise; } - return "/src/editor.worker.js"; + return editorWorker; }, }; function asLangEntry(key: T) { diff --git a/src/pkg/utils/monaco-editor/utils.ts b/src/pkg/utils/monaco-editor/utils.ts index 978742ffb..7f0e9fc72 100644 --- a/src/pkg/utils/monaco-editor/utils.ts +++ b/src/pkg/utils/monaco-editor/utils.ts @@ -1,5 +1,28 @@ import type { editor } from "monaco-editor"; +export const getEditorWorkerPromise = () => { + return Promise.resolve( + new Worker("/src/editor.worker.js", { + credentials: "omit", + name: "editorWorker", + type: "module", + }) + ); +}; + +export const getTsWorkerPromise = () => { + return fetch(chrome.runtime.getURL(`/src/ts.worker.js.bin`)) + .then((resp) => (resp.ok ? resp.blob() : null)) + .catch(() => null) + .then(async (blob) => { + if (blob) { + const blobUrl = URL.createObjectURL(new Blob([blob], { type: "text/javascript" })); + return new Worker(blobUrl, { credentials: "omit", name: "tsWorker", type: "module" }); + } + throw new Error("no ts.worker.js"); + }); +}; + export const findGlobalInsertionInfo = (model: editor.ITextModel) => { const lineCount = model.getLineCount(); From 75e9813cd69c366a009c0437b29d329825193c4c Mon Sep 17 00:00:00 2001 From: cyfung1031 <44498510+cyfung1031@users.noreply.github.com> Date: Sun, 8 Feb 2026 22:33:02 +0900 Subject: [PATCH 02/18] Update utils.ts --- src/pkg/utils/monaco-editor/utils.ts | 26 +++++++++++++++----------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/src/pkg/utils/monaco-editor/utils.ts b/src/pkg/utils/monaco-editor/utils.ts index 7f0e9fc72..9b9c3b7d6 100644 --- a/src/pkg/utils/monaco-editor/utils.ts +++ b/src/pkg/utils/monaco-editor/utils.ts @@ -10,17 +10,21 @@ export const getEditorWorkerPromise = () => { ); }; -export const getTsWorkerPromise = () => { - return fetch(chrome.runtime.getURL(`/src/ts.worker.js.bin`)) - .then((resp) => (resp.ok ? resp.blob() : null)) - .catch(() => null) - .then(async (blob) => { - if (blob) { - const blobUrl = URL.createObjectURL(new Blob([blob], { type: "text/javascript" })); - return new Worker(blobUrl, { credentials: "omit", name: "tsWorker", type: "module" }); - } - throw new Error("no ts.worker.js"); - }); +export const getTsWorkerPromise = (): Promise => { + return new Promise((resolve, reject) => { + fetch(chrome.runtime.getURL(`/src/ts.worker.js.bin`)) + .then((resp) => (resp.ok ? resp.blob() : null)) + .catch(() => null) + .then((blob) => { + if (blob) { + const blobUrl = URL.createObjectURL(new Blob([blob], { type: "text/javascript" })); + const worker = new Worker(blobUrl, { credentials: "omit", name: "tsWorker", type: "module" }); + resolve(worker); + } else { + reject(new Error("no ts.worker")); + } + }); + }); }; export const findGlobalInsertionInfo = (model: editor.ITextModel) => { From 58ceb19bd038e5e2b5f11271a7f38f66a6c4cea0 Mon Sep 17 00:00:00 2001 From: cyfung1031 <44498510+cyfung1031@users.noreply.github.com> Date: Sun, 8 Feb 2026 22:33:25 +0900 Subject: [PATCH 03/18] Update utils.ts --- src/pkg/utils/monaco-editor/utils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pkg/utils/monaco-editor/utils.ts b/src/pkg/utils/monaco-editor/utils.ts index 9b9c3b7d6..223299d76 100644 --- a/src/pkg/utils/monaco-editor/utils.ts +++ b/src/pkg/utils/monaco-editor/utils.ts @@ -1,6 +1,6 @@ import type { editor } from "monaco-editor"; -export const getEditorWorkerPromise = () => { +export const getEditorWorkerPromise = (): Promise => { return Promise.resolve( new Worker("/src/editor.worker.js", { credentials: "omit", From 044c7783207d3354be57fa98d50d9c6249611daf Mon Sep 17 00:00:00 2001 From: cyfung1031 <44498510+cyfung1031@users.noreply.github.com> Date: Sun, 8 Feb 2026 22:51:41 +0900 Subject: [PATCH 04/18] blob -> arrayBuffer to force removing of MIME --- src/pkg/utils/monaco-editor/utils.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/pkg/utils/monaco-editor/utils.ts b/src/pkg/utils/monaco-editor/utils.ts index 223299d76..88cde2d0a 100644 --- a/src/pkg/utils/monaco-editor/utils.ts +++ b/src/pkg/utils/monaco-editor/utils.ts @@ -13,11 +13,11 @@ export const getEditorWorkerPromise = (): Promise => { export const getTsWorkerPromise = (): Promise => { return new Promise((resolve, reject) => { fetch(chrome.runtime.getURL(`/src/ts.worker.js.bin`)) - .then((resp) => (resp.ok ? resp.blob() : null)) + .then((resp) => (resp.ok ? resp.arrayBuffer() : null)) .catch(() => null) - .then((blob) => { - if (blob) { - const blobUrl = URL.createObjectURL(new Blob([blob], { type: "text/javascript" })); + .then((rawData) => { + if (rawData) { + const blobUrl = URL.createObjectURL(new Blob([rawData], { type: "text/javascript" })); const worker = new Worker(blobUrl, { credentials: "omit", name: "tsWorker", type: "module" }); resolve(worker); } else { From 033d71b4cbe0df28f36cd1c8687f8715ecf96a3b Mon Sep 17 00:00:00 2001 From: cyfung1031 <44498510+cyfung1031@users.noreply.github.com> Date: Sun, 8 Feb 2026 23:48:20 +0900 Subject: [PATCH 05/18] fix `content_security_policy` --- rspack.config.ts | 1 - scripts/pack.js | 2 -- src/manifest.json | 3 +++ 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/rspack.config.ts b/rspack.config.ts index 4a3b90857..4e39b9794 100644 --- a/rspack.config.ts +++ b/rspack.config.ts @@ -141,7 +141,6 @@ export default defineConfig({ const manifest = JSON.parse(content.toString()); if (isDev || isBeta) { manifest.name = "__MSG_scriptcat_beta__"; - // manifest.content_security_policy = "script-src 'self' https://cdn.crowdin.com; object-src 'self'"; } return JSON.stringify(manifest); }, diff --git a/scripts/pack.js b/scripts/pack.js index 85f66307a..f3784c8c3 100644 --- a/scripts/pack.js +++ b/scripts/pack.js @@ -74,13 +74,11 @@ execSync("npm run build", { stdio: "inherit" }); const firefoxManifest = { ...manifest, background: { ...manifest.background } }; const chromeManifest = { ...manifest, background: { ...manifest.background } }; -delete chromeManifest.content_security_policy; chromeManifest.optional_permissions = chromeManifest.optional_permissions.filter((val) => val !== "userScripts"); delete chromeManifest.background.scripts; delete firefoxManifest.background.service_worker; delete firefoxManifest.sandbox; -// firefoxManifest.content_security_policy = "script-src 'self' blob:; object-src 'self' blob:"; firefoxManifest.browser_specific_settings = { gecko: { id: `{${ diff --git a/src/manifest.json b/src/manifest.json index b0ef98365..98080f632 100644 --- a/src/manifest.json +++ b/src/manifest.json @@ -55,6 +55,9 @@ "src/sandbox.html" ] }, + "content_security_policy": { + "extension_pages": "script-src 'self' blob:; worker-src 'self' blob:;" + }, "web_accessible_resources": [ { "resources": [ From 54f929b4d46be9777faefdb2bfac2a95ff597781 Mon Sep 17 00:00:00 2001 From: cyfung1031 <44498510+cyfung1031@users.noreply.github.com> Date: Tue, 10 Feb 2026 23:32:55 +0900 Subject: [PATCH 06/18] =?UTF-8?q?=E6=94=B9=E4=B8=BA=20ZipExecutionPlugin?= =?UTF-8?q?=20=E5=81=9A=E4=BB=A3=E7=A0=81=E5=8E=8B=E7=BC=A9=EF=BC=8C?= =?UTF-8?q?=E4=BD=BF=20ts.worker.js=20=E5=A4=A7=E5=B0=8F=E7=BC=A9=E5=87=8F?= =?UTF-8?q?=E8=87=B3=204MB=20(=20<=205MB=20)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- rspack-plugins/ZipExecutionPlugin.ts | 252 +++++++++++++++++++++++++++ rspack.config.ts | 7 +- src/pkg/utils/monaco-editor/index.ts | 12 +- src/pkg/utils/monaco-editor/utils.ts | 27 --- 4 files changed, 259 insertions(+), 39 deletions(-) create mode 100644 rspack-plugins/ZipExecutionPlugin.ts diff --git a/rspack-plugins/ZipExecutionPlugin.ts b/rspack-plugins/ZipExecutionPlugin.ts new file mode 100644 index 000000000..9b17ed6f5 --- /dev/null +++ b/rspack-plugins/ZipExecutionPlugin.ts @@ -0,0 +1,252 @@ +import type { Compiler, Compilation } from "@rspack/core"; +import pako from "pako"; + +import * as acorn from "acorn"; +import MagicString from "magic-string"; + +export function compileDecodeSource(templateCode: string, base64Data: string, vName: string) { + return ` + const $encodedBase64 = "${base64Data}"; + + // -------------- See https://github.com/js-vanilla/inflate-raw/ -------------- + + const $inflateRaw = (()=>{const t=Uint8Array,r=t.fromBase64?.bind(t)??(r=>{const e=atob(r);let n=e.length;const l=new t(n);for(;n--;)l[n]=e.charCodeAt(n);return l}); + return e=>{const n=r(e);let l=4*n.length;l<32768&&(l=32768);let s=new t(l),o=0;const a=r=>{let e=s.length;const n=o+r;if(n>e){do{e=3*e>>>1}while(e{for(;g<16&&d{A();const r=w&(1<>>=t,g-=t,r},k=[16,17,18,0,8,7,9,6,10,5,11,4,12,3,13,2,14,1,15], + m=[3,4,5,6,7,8,9,10,11,13,15,17,19,23,27,31,35,43,51,59,67,83,99,115,131,163,195,227,258],p=[0,0,0,0,0,0,0,0,1,1,1,1,2,2,2,2,3,3,3,3,4,4,4,4,5,5,5,5,0], + v=[1,2,3,4,5,7,9,13,17,25,33,49,65,97,129,193,257,385,513,769,1025,1537,2049,3073,4097,6145,8193,12289,16385,24577], + x=[0,0,0,0,1,1,2,2,3,3,4,4,5,5,6,6,7,7,8,8,9,9,10,10,11,11,12,12,13,13],B=h.subarray(0,16),C=h.subarray(16,32),D=(t,r)=>{const e=B.fill(0); + let n=0;for(let r=0;r0&&(e[l]++,l>n&&(n=l))}const l=1<0&&(c[o[t[r]]++]=r);let f=0,i=0;for(let t=1;t<=n;t++){const r=1<>=1;f^=n}}return s},I=t=>{A();const r=t.length-1,e=t[w&r],n=e>>>9;return w>>>=n,g-=n,511&e},R=new t(320), + T=R.subarray(0,19);let W=!1,j=0;for(;!j;){const t=U(3);j=1&t;const r=t>>1;if(0===r){w=g=0;const t=n[d++]|n[d++]<<8;d+=2,a(t),s.set(n.subarray(d,d+t),o),o+=t,d+=t}else{ + let t,e;if(1===r){if(!W){W=!0;let t=65856;const r=R.subarray(0,288);r.fill(8,0,144),r.fill(9,144,256),r.fill(7,256,280),r.fill(8,280,288),y=c.subarray(t,t+=512),D(r,y); + const e=R.subarray(0,32).fill(5);b=c.subarray(t,t+=32),D(e,b)}t=y,e=b}else{const r=U(14),n=257+(31&r),l=1+(r>>5&31),s=4+(r>>10&15);T.fill(0); + for(let t=0;t { + // "zzstrs" + for (let e = 0xc0; e <= 0xff; e++) { + if (e === 0xd7 || e === 0xf7) continue; + const c = "$" + String.fromCharCode(e); + if (source.includes(c)) continue; + return c; + } + throw new Error("Unable to compress"); +}; + +export class ZipExecutionPlugin { + apply(compiler: Compiler) { + compiler.hooks.thisCompilation.tap("ZipExecutionPlugin", (compilation: Compilation) => { + compilation.hooks.processAssets.tapPromise( + { + name: "ZipExecutionPlugin", + stage: compiler.webpack.Compilation.PROCESS_ASSETS_STAGE_SUMMARIZE, // after all compressions + }, + async (assets) => { + for (const [filename, asset] of Object.entries(assets)) { + if (!filename.includes("ts.worker.js")) continue; + + const source = asset.source().toString(); + + const vName = findAvailableVarName(source); + + // ────────────────────────────────────────────────────────────── + // 1. Parse (no regex hacks!) + let ast: acorn.Node; + try { + ast = acorn.parse(source, { + ecmaVersion: "latest", + sourceType: "module", + ranges: true, + }); + } catch (err) { + console.warn(`[ZipExec] Parse failed ${filename}:`, (err as Error).message); + continue; + } + + // ────────────────────────────────────────────────────────────── + // 2. Collect candidates (robust walker + context) + const candidates = this.collectCandidates(ast); + + if (candidates.length === 0) continue; + + // ────────────────────────────────────────────────────────────── + // 3. Normalise values + deduplicate (huge strings are rarely identical, but helps) + const extracted: string[] = []; + const operations: Candidate[] = []; + + const candidatesFreq = new Map(); + const mapped = candidates.map((c) => { + const d = this.normalizeValue(c.value); + let q = candidatesFreq.get(d); + if (!q) candidatesFreq.set(d, (q = [0, 0])); + q[0] += 1; + return [c, d, q] as const; + }); + const sorted = [...candidatesFreq.entries()].sort((a, b) => b[1][0] - a[1][0]); + let i = 0; + for (const [d, q] of sorted) { + q[1] = i++; + extracted.push(d); + } + candidatesFreq.clear(); + + mapped.forEach(([c, _d, q]: any) => { + const id = q[1] as number; + operations.push({ ...c, id }); + }); + mapped.length = 0; + + // ────────────────────────────────────────────────────────────── + // 4. Replace bottom-up (safe offsets) + operations.sort((a, b) => b.start - a.start); + + const ms = new MagicString(source); + + for (const op of operations) { + if (op.type === "Template") { + // `content` → `${zzstrs[N]}` + if (op.expressions === 0) { + ms.overwrite(op.start - 1, op.end + 1, `${vName}[${op.id}]`); + } else if (op.expressions) { + throw "not implemented yet"; + // ms.overwrite(op.start, op.end, `\${${vName}[${op.id}]}`); + } + } else { + // "content" or 'content' → zzstrs[N] + ms.overwrite(op.start, op.end, `(${vName}[${op.id}])`); // bracket is required. might be some literals are mixed with other expr. + } + } + + // ────────────────────────────────────────────────────────────── + // 5. Compress + const json = JSON.stringify(extracted); + const deflated = pako.deflateRaw(Buffer.from(json, "utf8"), { level: 6 }); + const base64 = Buffer.from(deflated).toString("base64"); + + // ────────────────────────────────────────────────────────────── + // 6. Wrap + const finalSource = compileDecodeSource(ms.toString(), base64, vName); + + compilation.updateAsset(filename, new compiler.webpack.sources.RawSource(finalSource)); + + console.log(`[ZipExecutionPlugin] Processed ${filename}: ${extracted.length} unique strings extracted`); + } + } + ); + }); + } + + private collectCandidates(ast: acorn.Node): Omit[] { + const results: Omit[] = []; + + const walk = (node: any, parent: any = null) => { + if (!node || typeof node !== "object") return; + + if (this.isExtractable(node, parent)) { + if (node.type === "Literal" && typeof node.value === "string") { + results.push({ + type: "Literal", + start: node.start!, + end: node.end!, + value: node.value, + }); + return; // no children + } + + // for node.expressions.length > 0, not implemented yet + if (node.type === "TemplateLiteral" && node.expressions.length === 0) { + const quasi = node.quasis[0]; + const value = quasi.value.cooked ?? quasi.value.raw; + results.push({ + type: "Template", + start: quasi.start!, + end: quasi.end!, + expressions: node.expressions.length, + value, + }); + return; + } + } + + // Safe child traversal + for (const key of Object.keys(node)) { + if (["parent", "loc", "range", "start", "end"].includes(key)) continue; + const child = node[key]; + if (Array.isArray(child)) child.forEach((c) => walk(c, node)); + else if (child && typeof child === "object" && child.type) walk(child, node); + } + }; + + walk(ast); + return results; + } + + private isExtractable(node: any, parent: any): boolean { + const isLiteral = node.type === "Literal" && typeof node.value === "string"; + const isTemplate = node.type === "TemplateLiteral"; + + if (!isLiteral && !isTemplate) return false; + + // Length filter (runtime value) + const content = isLiteral ? node.value : (node.quasis[0].value.cooked ?? ""); + if (isTemplate && node.expressions?.length === 0) { + if (content.length < 7) return false; // `1234567` => $S[6789] + } else { + // isLiteral or isTemplate with node.expressions + if (content.length < 9) return false; // "123456789" => ($S[6789]) + } + + // ── Exclusions ───────────────────────────────────────────────────── + // "use strict" + if (parent?.type === "ExpressionStatement" && node.value === "use strict") return false; + + // Tagged templates + if (parent?.type === "TaggedTemplateExpression" && parent.quasi === node) return false; + + // Object keys (non-computed) + if (parent?.type === "Property" && parent.key === node && !parent.computed) return false; + + // Import/export sources + if ( + parent && + (parent.type === "ImportDeclaration" || + parent.type === "ExportNamedDeclaration" || + parent.type === "ExportAllDeclaration") && + parent.source === node + ) + return false; + + // Dynamic import + if (parent?.type === "ImportExpression" && parent.source === node) return false; + + return true; + } + + private normalizeValue(value: string): string { + // Only standardise line endings – never escape anything + return value.includes("\r") ? value.replace(/\r\n/g, "\n").replace(/\r/g, "\n") : value; + } +} diff --git a/rspack.config.ts b/rspack.config.ts index 4e39b9794..0c9f7f640 100644 --- a/rspack.config.ts +++ b/rspack.config.ts @@ -1,6 +1,7 @@ import * as path from "path"; import { defineConfig } from "@rspack/cli"; import { rspack } from "@rspack/core"; +import { ZipExecutionPlugin } from "./rspack-plugins/ZipExecutionPlugin"; import { readFileSync } from "fs"; import { NormalModule } from "@rspack/core"; import { v4 as uuidv4 } from "uuid"; @@ -62,10 +63,7 @@ export default defineConfig({ }, output: { path: `${dist}/ext/src`, - filename(pathData, _assetInfo) { - if (pathData.runtime === "ts.worker") { - return "[name].js.bin"; - } + filename(_pathData, _assetInfo) { return "[name].js"; }, clean: true, @@ -224,6 +222,7 @@ export default defineConfig({ minify: true, chunks: ["sandbox"], }), + new ZipExecutionPlugin(), ].filter(Boolean), experiments: { css: true, diff --git a/src/pkg/utils/monaco-editor/index.ts b/src/pkg/utils/monaco-editor/index.ts index 8a055c44f..6c91de174 100644 --- a/src/pkg/utils/monaco-editor/index.ts +++ b/src/pkg/utils/monaco-editor/index.ts @@ -1,7 +1,7 @@ import { globalCache, systemConfig } from "@App/pages/store/global"; import EventEmitter from "eventemitter3"; import { languages } from "monaco-editor"; -import { findGlobalInsertionInfo, getEditorWorkerPromise, getTsWorkerPromise, updateGlobalCommentLine } from "./utils"; +import { findGlobalInsertionInfo, updateGlobalCommentLine } from "./utils"; // 注册eslint const linterWorker = new Worker("/src/linter.worker.js"); @@ -455,16 +455,12 @@ type Prompt = (typeof langs)["zh-CN"]["prompt"]; type LangEntry = (typeof langs)["zh-CN"]; export default function registerEditor() { - const editorWorker = getEditorWorkerPromise(); - const tsWorkerPromise = getTsWorkerPromise(); window.MonacoEnvironment = { - // https://microsoft.github.io/monaco-editor/typedoc/interfaces/Environment.html#getWorker - // Returns Worker | Promise - getWorker(workerId: string, label: string) { + getWorkerUrl(moduleId: any, label: any) { if (label === "typescript" || label === "javascript") { - return tsWorkerPromise; + return "/src/ts.worker.js"; } - return editorWorker; + return "/src/editor.worker.js"; }, }; function asLangEntry(key: T) { diff --git a/src/pkg/utils/monaco-editor/utils.ts b/src/pkg/utils/monaco-editor/utils.ts index 88cde2d0a..978742ffb 100644 --- a/src/pkg/utils/monaco-editor/utils.ts +++ b/src/pkg/utils/monaco-editor/utils.ts @@ -1,32 +1,5 @@ import type { editor } from "monaco-editor"; -export const getEditorWorkerPromise = (): Promise => { - return Promise.resolve( - new Worker("/src/editor.worker.js", { - credentials: "omit", - name: "editorWorker", - type: "module", - }) - ); -}; - -export const getTsWorkerPromise = (): Promise => { - return new Promise((resolve, reject) => { - fetch(chrome.runtime.getURL(`/src/ts.worker.js.bin`)) - .then((resp) => (resp.ok ? resp.arrayBuffer() : null)) - .catch(() => null) - .then((rawData) => { - if (rawData) { - const blobUrl = URL.createObjectURL(new Blob([rawData], { type: "text/javascript" })); - const worker = new Worker(blobUrl, { credentials: "omit", name: "tsWorker", type: "module" }); - resolve(worker); - } else { - reject(new Error("no ts.worker")); - } - }); - }); -}; - export const findGlobalInsertionInfo = (model: editor.ITextModel) => { const lineCount = model.getLineCount(); From 75b484ca8d45a23d2644c1e3d826d89d0fe7d21d Mon Sep 17 00:00:00 2001 From: cyfung1031 <44498510+cyfung1031@users.noreply.github.com> Date: Tue, 10 Feb 2026 23:34:31 +0900 Subject: [PATCH 07/18] revert changes --- rspack.config.ts | 5 +---- src/manifest.json | 3 --- 2 files changed, 1 insertion(+), 7 deletions(-) diff --git a/rspack.config.ts b/rspack.config.ts index 0c9f7f640..6894b57a6 100644 --- a/rspack.config.ts +++ b/rspack.config.ts @@ -63,9 +63,7 @@ export default defineConfig({ }, output: { path: `${dist}/ext/src`, - filename(_pathData, _assetInfo) { - return "[name].js"; - }, + filename: "[name].js", clean: true, }, resolve: { @@ -232,7 +230,6 @@ export default defineConfig({ optimization: { minimizer: [ new rspack.SwcJsMinimizerRspackPlugin({ - test: /\.[cm]?js(\.bin)?(\?.*)?$/, minimizerOptions: { minify: !isDev, mangle: { diff --git a/src/manifest.json b/src/manifest.json index 98080f632..b0ef98365 100644 --- a/src/manifest.json +++ b/src/manifest.json @@ -55,9 +55,6 @@ "src/sandbox.html" ] }, - "content_security_policy": { - "extension_pages": "script-src 'self' blob:; worker-src 'self' blob:;" - }, "web_accessible_resources": [ { "resources": [ From 648eb5ff2cde3b6ad9bf64ff7ecd1935cf5648ce Mon Sep 17 00:00:00 2001 From: cyfung1031 <44498510+cyfung1031@users.noreply.github.com> Date: Tue, 10 Feb 2026 23:39:29 +0900 Subject: [PATCH 08/18] =?UTF-8?q?=E8=A1=A5=E5=9B=9E=20packages=20-=20acorn?= =?UTF-8?q?,=20magic-string,=20pako?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 3 +++ pnpm-lock.yaml | 61 ++++++++++++++++++-------------------------------- 2 files changed, 25 insertions(+), 39 deletions(-) diff --git a/package.json b/package.json index 2c09dc018..0df81cbc6 100644 --- a/package.json +++ b/package.json @@ -30,6 +30,7 @@ "@dnd-kit/modifiers": "^9.0.0", "@dnd-kit/sortable": "^10.0.0", "@dnd-kit/utilities": "^3.2.2", + "acorn": "^8.15.0", "chardet": "^2.1.1", "cron": "^4.4.0", "crypto-js": "^4.2.0", @@ -38,7 +39,9 @@ "eslint-linter-browserify": "9.26.0", "eventemitter3": "^5.0.1", "i18next": "^23.16.4", + "magic-string": "^0.30.21", "monaco-editor": "^0.52.2", + "pako": "^2.1.0", "react": "^18.3.1", "react-dom": "^18.3.1", "react-dropzone": "^14.3.8", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ff52d0907..15cd06d86 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -23,6 +23,9 @@ importers: '@dnd-kit/utilities': specifier: ^3.2.2 version: 3.2.2(react@18.3.1) + acorn: + specifier: ^8.15.0 + version: 8.15.0 chardet: specifier: ^2.1.1 version: 2.1.1 @@ -47,9 +50,15 @@ importers: i18next: specifier: ^23.16.4 version: 23.16.4 + magic-string: + specifier: ^0.30.21 + version: 0.30.21 monaco-editor: specifier: ^0.52.2 version: 0.52.2 + pako: + specifier: ^2.1.0 + version: 2.1.0 react: specifier: ^18.3.1 version: 18.3.1 @@ -782,12 +791,6 @@ packages: '@jridgewell/source-map@0.3.10': resolution: {integrity: sha512-0pPkgz9dY+bijgistcTTJ5mR+ocqRXLuhXHYdzoMmmoJ2C9S46RCm2GMUbatPEUK9Yjy26IrAy8D/M00lLkv+Q==} - '@jridgewell/sourcemap-codec@1.5.0': - resolution: {integrity: sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==} - - '@jridgewell/sourcemap-codec@1.5.4': - resolution: {integrity: sha512-VT2+G1VQs/9oz078bLrYbecdZKs912zQlkelYpuf+SXF+QvZDYJlbx/LSx+meSAwdDFnF8FVXW92AVjjkVmgFw==} - '@jridgewell/sourcemap-codec@1.5.5': resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} @@ -1449,16 +1452,6 @@ packages: resolution: {integrity: sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==} engines: {node: '>=0.4.0'} - acorn@8.13.0: - resolution: {integrity: sha512-8zSiw54Oxrdym50NlZ9sUusyO1Z1ZchgRLWRaK6c86XJFClyCgFKetdowBg5bKxyp/u+CDBJG4Mpp0m3HLZl9w==} - engines: {node: '>=0.4.0'} - hasBin: true - - acorn@8.14.1: - resolution: {integrity: sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==} - engines: {node: '>=0.4.0'} - hasBin: true - acorn@8.15.0: resolution: {integrity: sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==} engines: {node: '>=0.4.0'} @@ -2907,9 +2900,6 @@ packages: resolution: {integrity: sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==} hasBin: true - magic-string@0.30.17: - resolution: {integrity: sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==} - magic-string@0.30.21: resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} @@ -3175,6 +3165,9 @@ packages: pako@1.0.11: resolution: {integrity: sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==} + pako@2.1.0: + resolution: {integrity: sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==} + parent-module@1.0.1: resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} engines: {node: '>=6'} @@ -4791,7 +4784,7 @@ snapshots: '@jridgewell/gen-mapping@0.3.12': dependencies: - '@jridgewell/sourcemap-codec': 1.5.4 + '@jridgewell/sourcemap-codec': 1.5.5 '@jridgewell/trace-mapping': 0.3.29 '@jridgewell/remapping@2.3.5': @@ -4807,21 +4800,17 @@ snapshots: '@jridgewell/trace-mapping': 0.3.29 optional: true - '@jridgewell/sourcemap-codec@1.5.0': {} - - '@jridgewell/sourcemap-codec@1.5.4': {} - '@jridgewell/sourcemap-codec@1.5.5': {} '@jridgewell/trace-mapping@0.3.29': dependencies: '@jridgewell/resolve-uri': 3.1.2 - '@jridgewell/sourcemap-codec': 1.5.4 + '@jridgewell/sourcemap-codec': 1.5.5 '@jridgewell/trace-mapping@0.3.9': dependencies: '@jridgewell/resolve-uri': 3.1.2 - '@jridgewell/sourcemap-codec': 1.5.0 + '@jridgewell/sourcemap-codec': 1.5.5 '@jsonjoy.com/base64@1.1.2(tslib@2.8.1)': dependencies: @@ -5509,7 +5498,7 @@ snapshots: dependencies: '@vitest/spy': 3.2.4 estree-walker: 3.0.3 - magic-string: 0.30.17 + magic-string: 0.30.21 optionalDependencies: vite: 7.0.2(@types/node@22.16.0)(jiti@2.6.1)(terser@5.43.1)(tsx@4.19.2)(yaml@2.8.1) @@ -5526,7 +5515,7 @@ snapshots: '@vitest/snapshot@3.2.4': dependencies: '@vitest/pretty-format': 3.2.4 - magic-string: 0.30.17 + magic-string: 0.30.21 pathe: 2.0.3 '@vitest/spy@3.2.4': @@ -5647,11 +5636,7 @@ snapshots: acorn-walk@8.3.4: dependencies: - acorn: 8.14.1 - - acorn@8.13.0: {} - - acorn@8.14.1: {} + acorn: 8.15.0 acorn@8.15.0: {} @@ -7292,10 +7277,6 @@ snapshots: lz-string@1.5.0: {} - magic-string@0.30.17: - dependencies: - '@jridgewell/sourcemap-codec': 1.5.4 - magic-string@0.30.21: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 @@ -7538,6 +7519,8 @@ snapshots: pako@1.0.11: {} + pako@2.1.0: {} + parent-module@1.0.1: dependencies: callsites: 3.1.0 @@ -8383,7 +8366,7 @@ snapshots: '@tsconfig/node14': 1.0.3 '@tsconfig/node16': 1.0.4 '@types/node': 22.16.0 - acorn: 8.13.0 + acorn: 8.15.0 acorn-walk: 8.3.4 arg: 4.1.3 create-require: 1.1.1 @@ -8608,7 +8591,7 @@ snapshots: chai: 5.2.0 debug: 4.4.1 expect-type: 1.2.2 - magic-string: 0.30.17 + magic-string: 0.30.21 pathe: 2.0.3 picomatch: 4.0.2 std-env: 3.9.0 From 09afa0f75d97075960c88d8a6abd90324e340ba4 Mon Sep 17 00:00:00 2001 From: cyfung1031 <44498510+cyfung1031@users.noreply.github.com> Date: Tue, 10 Feb 2026 23:52:14 +0900 Subject: [PATCH 09/18] code adjusted --- rspack-plugins/ZipExecutionPlugin.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/rspack-plugins/ZipExecutionPlugin.ts b/rspack-plugins/ZipExecutionPlugin.ts index 9b17ed6f5..c0f2fe7f7 100644 --- a/rspack-plugins/ZipExecutionPlugin.ts +++ b/rspack-plugins/ZipExecutionPlugin.ts @@ -136,7 +136,7 @@ export class ZipExecutionPlugin { } } else { // "content" or 'content' → zzstrs[N] - ms.overwrite(op.start, op.end, `(${vName}[${op.id}])`); // bracket is required. might be some literals are mixed with other expr. + ms.overwrite(op.start, op.end, ` ${vName}[${op.id}] `); } } @@ -144,6 +144,7 @@ export class ZipExecutionPlugin { // 5. Compress const json = JSON.stringify(extracted); const deflated = pako.deflateRaw(Buffer.from(json, "utf8"), { level: 6 }); + if (!deflated) throw new Error("Pako Compression Failed"); const base64 = Buffer.from(deflated).toString("base64"); // ────────────────────────────────────────────────────────────── From 98f5f0488efa039553dc1436742b5c84f81e5eda Mon Sep 17 00:00:00 2001 From: cyfung1031 <44498510+cyfung1031@users.noreply.github.com> Date: Wed, 11 Feb 2026 00:14:53 +0900 Subject: [PATCH 10/18] =?UTF-8?q?=E4=BF=AE=E8=AE=A2=E4=BB=A3=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- rspack-plugins/ZipExecutionPlugin.ts | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/rspack-plugins/ZipExecutionPlugin.ts b/rspack-plugins/ZipExecutionPlugin.ts index c0f2fe7f7..185b236d9 100644 --- a/rspack-plugins/ZipExecutionPlugin.ts +++ b/rspack-plugins/ZipExecutionPlugin.ts @@ -129,14 +129,23 @@ export class ZipExecutionPlugin { if (op.type === "Template") { // `content` → `${zzstrs[N]}` if (op.expressions === 0) { - ms.overwrite(op.start - 1, op.end + 1, `${vName}[${op.id}]`); + const c1 = ms.slice(op.start - 2, op.start - 1); + const c2 = ms.slice(op.end + 1, op.end + 2); + const s1 = /[\w"'`]/.test(c1) ? " " : ""; + const s2 = /[\w"'`]/.test(c2) ? " " : ""; + ms.overwrite(op.start - 1, op.end + 1, `${s1}${vName}[${op.id}]${s2}`); } else if (op.expressions) { throw "not implemented yet"; // ms.overwrite(op.start, op.end, `\${${vName}[${op.id}]}`); } } else { // "content" or 'content' → zzstrs[N] - ms.overwrite(op.start, op.end, ` ${vName}[${op.id}] `); + // note: case"123456789" -> case $X[123] + const c1 = ms.slice(op.start - 1, op.start); + const c2 = ms.slice(op.end, op.end + 1); + const s1 = /[\w"'`]/.test(c1) ? " " : ""; + const s2 = /[\w"'`]/.test(c2) ? " " : ""; + ms.overwrite(op.start, op.end, `${s1}${vName}[${op.id}]${s2}`); } } From 3cb05c0ece23b40ef7ca13b09ec8746348eb5a6c Mon Sep 17 00:00:00 2001 From: cyfung1031 <44498510+cyfung1031@users.noreply.github.com> Date: Wed, 11 Feb 2026 12:25:49 +0900 Subject: [PATCH 11/18] =?UTF-8?q?=E4=BF=AE=E8=AE=A2=E4=BB=A3=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- rspack-plugins/ZipExecutionPlugin.ts | 465 +++++++++++++++++---------- 1 file changed, 296 insertions(+), 169 deletions(-) diff --git a/rspack-plugins/ZipExecutionPlugin.ts b/rspack-plugins/ZipExecutionPlugin.ts index 185b236d9..3a9b7adcc 100644 --- a/rspack-plugins/ZipExecutionPlugin.ts +++ b/rspack-plugins/ZipExecutionPlugin.ts @@ -1,48 +1,57 @@ import type { Compiler, Compilation } from "@rspack/core"; -import pako from "pako"; +import zlib from "zlib"; import * as acorn from "acorn"; import MagicString from "magic-string"; -export function compileDecodeSource(templateCode: string, base64Data: string, vName: string) { +const trimCode = (code: string) => { + return code.trim(); +}; + +export function compileDecodeSource(templateCode: string, base64Data: string, pName: string) { + // ------------------------------------------ inflate-raw ------------------------------------------ + // lightweight implementation of the DEFLATE decompression algorithm (RFC 1951) + // * See https://github.com/js-vanilla/inflate-raw/ + const inflateRawCode = trimCode(` + (()=>{let _=Uint8Array,e=_.fromBase64?.bind(_)??(e=>{let l=atob(e),$=l.length,r=new _($);for(;$--;)r[$]=l.charCodeAt($);return r}), + l=l=>{let $=e(l),r=4*$.length;r<32768&&(r=32768);let t=new _(r),a=0,f=e=>{let l=t.length,$=a+e;if($>l){do l=3*l>>>1;while(l<$);let r=new _(l);r.set(t),t=r}}, + s=new Uint16Array(66400),n=s.subarray(0,32768),u=s.subarray(32768,65536),b=s.subarray(65536,65856),i,o,y=new Int32Array(48),h=0,w=0,g=0, + d=()=>{for(;w<16&&g<$.length;)h|=$[g++]<{d();let e=h&(1<<_)-1;return h>>>=_,w-=_,e},F=[16,17,18,0,8,7,9,6,10,5,11,4,12,3,13,2,14,1,15], + k=[3,4,5,6,7,8,9,10,11,13,15,17,19,23,27,31,35,43,51,59,67,83,99,115,131,163,195,227,258],m=[0,0,0,0,0,0,0,0,1,1,1,1,2,2,2,2,3,3,3,3,4,4,4,4,5,5,5,5,0], + p=[1,2,3,4,5,7,9,13,17,25,33,49,65,97,129,193,257,385,513,769,1025,1537,2049,3073,4097,6145,8193,12289,16385,24577], + v=[0,0,0,0,1,1,2,2,3,3,4,4,5,5,6,6,7,7,8,8,9,9,10,10,11,11,12,12,13,13],x=y.subarray(0,16),A=y.subarray(16,32),B=(_,e)=>{let l=x.fill(0),$=0; + for(let r=0;r<_.length;r++){let t=_[r];t>0&&(l[t]++,t>$&&($=t))}let a=1<<$,f=e.subarray(0,a),s=A,n=0;for(let u=1;u<=$;u++)s[u]=n,n+=l[u];let i=b; + for(let o=0;o<_.length;o++)_[o]>0&&(i[s[_[o]]++]=o);let y=0,h=0;for(let w=1;w<=$;w++){let g=1<>=1;y^=p}}return f},C=_=>{d();let e=_.length-1,l=_[h&e],$=l>>>9;return h>>>=$,w-=$,511&l}, + R=new _(320),W=R.subarray(0,19),j=!1,q=0;for(;!q;){let z=c(3);q=1&z;let D=z>>1;if(0===D){h=w=0;let E=$[g++]|$[g++]<<8;g+=2,f(E),t.set($.subarray(g,g+E),a),a+=E, + g+=E}else{let G,H;if(1===D){if(!j){j=!0;let I=65856,J=R.subarray(0,288);J.fill(8,0,144),J.fill(9,144,256),J.fill(7,256,280),J.fill(8,280,288),B(J,i=s.subarray(I,I+=512)); + let K=R.subarray(0,32).fill(5);B(K,o=s.subarray(I,I+=32))}G=i,H=o}else{let L=c(14),M=(31&L)+257,N=(L>>5&31)+1,O=(L>>10&15)+4;W.fill(0);for(let P=0;P{const t=Uint8Array,r=t.fromBase64?.bind(t)??(r=>{const e=atob(r);let n=e.length;const l=new t(n);for(;n--;)l[n]=e.charCodeAt(n);return l}); - return e=>{const n=r(e);let l=4*n.length;l<32768&&(l=32768);let s=new t(l),o=0;const a=r=>{let e=s.length;const n=o+r;if(n>e){do{e=3*e>>>1}while(e{for(;g<16&&d{A();const r=w&(1<>>=t,g-=t,r},k=[16,17,18,0,8,7,9,6,10,5,11,4,12,3,13,2,14,1,15], - m=[3,4,5,6,7,8,9,10,11,13,15,17,19,23,27,31,35,43,51,59,67,83,99,115,131,163,195,227,258],p=[0,0,0,0,0,0,0,0,1,1,1,1,2,2,2,2,3,3,3,3,4,4,4,4,5,5,5,5,0], - v=[1,2,3,4,5,7,9,13,17,25,33,49,65,97,129,193,257,385,513,769,1025,1537,2049,3073,4097,6145,8193,12289,16385,24577], - x=[0,0,0,0,1,1,2,2,3,3,4,4,5,5,6,6,7,7,8,8,9,9,10,10,11,11,12,12,13,13],B=h.subarray(0,16),C=h.subarray(16,32),D=(t,r)=>{const e=B.fill(0); - let n=0;for(let r=0;r0&&(e[l]++,l>n&&(n=l))}const l=1<0&&(c[o[t[r]]++]=r);let f=0,i=0;for(let t=1;t<=n;t++){const r=1<>=1;f^=n}}return s},I=t=>{A();const r=t.length-1,e=t[w&r],n=e>>>9;return w>>>=n,g-=n,511&e},R=new t(320), - T=R.subarray(0,19);let W=!1,j=0;for(;!j;){const t=U(3);j=1&t;const r=t>>1;if(0===r){w=g=0;const t=n[d++]|n[d++]<<8;d+=2,a(t),s.set(n.subarray(d,d+t),o),o+=t,d+=t}else{ - let t,e;if(1===r){if(!W){W=!0;let t=65856;const r=R.subarray(0,288);r.fill(8,0,144),r.fill(9,144,256),r.fill(7,256,280),r.fill(8,280,288),y=c.subarray(t,t+=512),D(r,y); - const e=R.subarray(0,32).fill(5);b=c.subarray(t,t+=32),D(e,b)}t=y,e=b}else{const r=U(14),n=257+(31&r),l=1+(r>>5&31),s=4+(r>>10&15);T.fill(0); - for(let t=0;t { @@ -50,13 +59,160 @@ const findAvailableVarName = (source: string) => { for (let e = 0xc0; e <= 0xff; e++) { if (e === 0xd7 || e === 0xf7) continue; const c = "$" + String.fromCharCode(e); - if (source.includes(c)) continue; - return c; + if (!source.includes(c)) return c; } throw new Error("Unable to compress"); }; +const findShortName = (source: string) => { + // $H + const candidates = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" + .split("") + .map((c) => [c, source.split("$" + c).filter((w, i) => i === 0 || !/[\w$]/.test(w[0])).length] as const); + candidates.sort((a, b) => a[1] - b[1]); + const [candidateChar, _candidatesFreq] = candidates[0]; + const pName = "$" + candidateChar; + return pName; +}; + export class ZipExecutionPlugin { + processFn(source: string, filename: string = "") { + const vName = findAvailableVarName(source); + const pName = findShortName(source); + source = source.replaceAll(pName, vName); + + // 1. Parse + let ast: acorn.Node; + try { + ast = acorn.parse(source, { + ecmaVersion: "latest", + sourceType: "module", + ranges: true, + }); + } catch (err) { + console.warn(`[ZipExec] Parse failed ${filename}:`, (err as Error).message); + return false; + } + + // 2. Collect candidates (robust walker + context) + const candidates = this.collectCandidates(ast, source); + if (candidates.length === 0) return false; + + // Normalization & Deduplication + const extracted: string[] = []; + const operations: Candidate[] = []; + const candidatesFreq = new Map(); + + let mapped = candidates.map((c) => { + const d = this.normalizeValue(c.value); + if (c.zz) { + let q = candidatesFreq.get(d); + if (!q) candidatesFreq.set(d, (q = [0, 0, d.length])); + q[0] += 1; + return [c, d, q] as const; + } else { + return [c, d, [0, 0, 0]] as const; + } + }); + + mapped = mapped.filter(([c, d, q]) => { + if (q[0] === 1) { + // for freq === 1, if the size difference is small, replacement will make the compressed coding longer. + if (d.length < 14) { + q[0] = 0; + q[1] = 0; + q[2] = 0; + c.zz = false; + } + } + return true; + }); + + const sorted = [...candidatesFreq.entries()].sort((a, b) => b[1][0] - a[1][0]); + let i = 0; + for (const [d, q] of sorted) { + if (q[0] > 0) { + q[1] = i++; + extracted.push(d); + } + } + + mapped.forEach(([c, d, q]) => { + operations.push({ ...c, d: d, id: q[1], freq: q[0] }); + }); + + // Replace bottom-up (safe offsets) + operations.sort((a, b) => b.start - a.start); + const ms = new MagicString(source); + const usedIds = new Set(); + + for (const op of operations) { + let doZZ = false; + const p = op.type === "Template" ? op.start - 1 : op.start; + const q = op.type === "Template" ? op.end + 1 : op.end; + if (op.zz) { + const freq = op.freq || 0; + if (freq === 0) throw new Error("invalid freq"); + const newValue = `${pName}[${op.id}]`; + + let oldSize; + + let r; + if (op.type === "Template") { + // Static template: removes backticks + // someFn(`1234567`) -> someFn($X[1234]) + r = `${op.prefix}${newValue}${op.suffix}`; + oldSize = op.end - op.start + 2; // opValue = targetString + } else if (op.type === "Quasi") { + // Quasi: stays inside backticks + // someFn(`...${123456789}...`) -> someFn(`...${$X[1234]}...`) + r = `\${${newValue}}`; + oldSize = op.end - op.start; // opValue = targetString + } else { + // Literal: removes quotes + // someFn("1234567") -> someFn($X[1234]) + // note: case"12345678" -> case $X[1234] + r = `${op.prefix}${newValue}${op.suffix}`; + oldSize = op.end - op.start; // opValue = "targetString" + } + + const newSize = r.length; + if (newSize > oldSize) { + //@ts-ignore : ignore empty value + extracted[op.id] = 0; // No replacement to $X. Just keep the id in $X + } else { + doZZ = true; + usedIds.add(op.id); + ms.overwrite(p, q, r); + } + } + if (!doZZ) { + // Handling non-compressed strings (like those with newlines) + const old = op.value; + if (/[\r\n]/.test(old)) { + if (op.type === "Template") { + ms.overwrite(op.start - 1, op.end + 1, JSON.stringify(op.d)); + } else if (op.type === "Quasi" && /^[\r\n\w$.=*,?:!(){}[\]@#%^&*/ '"+-]+$/.test(old)) { + ms.overwrite(op.start, op.end, op.d.replace(/\n/g, "\\n")); + } + } + } + } + + // Compress + const json = JSON.stringify(extracted); + // const deflated = pako.deflateRaw(Buffer.from(json, "utf8"), { level: 6 }); + const deflated = zlib.deflateRawSync(Buffer.from(json, "utf8"), { level: 6 }); + if (!deflated) throw new Error("Compression Failed"); + const base64 = Buffer.from(deflated).toString("base64"); + + // Wrap + const finalSource = compileDecodeSource(ms.toString(), base64, pName); + // testing: + // const finalSource = `var ${vName}=JSON.parse(new TextDecoder().decode(require('pako').inflateRaw(Buffer.from("${base64}","base64"))));\n${ms.toString()}`; + + return { finalSource, source, extracted, usedIds }; + } apply(compiler: Compiler) { compiler.hooks.thisCompilation.tap("ZipExecutionPlugin", (compilation: Compilation) => { compilation.hooks.processAssets.tapPromise( @@ -68,140 +224,119 @@ export class ZipExecutionPlugin { for (const [filename, asset] of Object.entries(assets)) { if (!filename.includes("ts.worker.js")) continue; - const source = asset.source().toString(); - - const vName = findAvailableVarName(source); - - // ────────────────────────────────────────────────────────────── - // 1. Parse (no regex hacks!) - let ast: acorn.Node; - try { - ast = acorn.parse(source, { - ecmaVersion: "latest", - sourceType: "module", - ranges: true, - }); - } catch (err) { - console.warn(`[ZipExec] Parse failed ${filename}:`, (err as Error).message); - continue; - } - - // ────────────────────────────────────────────────────────────── - // 2. Collect candidates (robust walker + context) - const candidates = this.collectCandidates(ast); - - if (candidates.length === 0) continue; + let source = asset.source().toString(); - // ────────────────────────────────────────────────────────────── - // 3. Normalise values + deduplicate (huge strings are rarely identical, but helps) - const extracted: string[] = []; - const operations: Candidate[] = []; - - const candidatesFreq = new Map(); - const mapped = candidates.map((c) => { - const d = this.normalizeValue(c.value); - let q = candidatesFreq.get(d); - if (!q) candidatesFreq.set(d, (q = [0, 0])); - q[0] += 1; - return [c, d, q] as const; - }); - const sorted = [...candidatesFreq.entries()].sort((a, b) => b[1][0] - a[1][0]); - let i = 0; - for (const [d, q] of sorted) { - q[1] = i++; - extracted.push(d); - } - candidatesFreq.clear(); - - mapped.forEach(([c, _d, q]: any) => { - const id = q[1] as number; - operations.push({ ...c, id }); - }); - mapped.length = 0; - - // ────────────────────────────────────────────────────────────── - // 4. Replace bottom-up (safe offsets) - operations.sort((a, b) => b.start - a.start); - - const ms = new MagicString(source); - - for (const op of operations) { - if (op.type === "Template") { - // `content` → `${zzstrs[N]}` - if (op.expressions === 0) { - const c1 = ms.slice(op.start - 2, op.start - 1); - const c2 = ms.slice(op.end + 1, op.end + 2); - const s1 = /[\w"'`]/.test(c1) ? " " : ""; - const s2 = /[\w"'`]/.test(c2) ? " " : ""; - ms.overwrite(op.start - 1, op.end + 1, `${s1}${vName}[${op.id}]${s2}`); - } else if (op.expressions) { - throw "not implemented yet"; - // ms.overwrite(op.start, op.end, `\${${vName}[${op.id}]}`); - } - } else { - // "content" or 'content' → zzstrs[N] - // note: case"123456789" -> case $X[123] - const c1 = ms.slice(op.start - 1, op.start); - const c2 = ms.slice(op.end, op.end + 1); - const s1 = /[\w"'`]/.test(c1) ? " " : ""; - const s2 = /[\w"'`]/.test(c2) ? " " : ""; - ms.overwrite(op.start, op.end, `${s1}${vName}[${op.id}]${s2}`); - } - } + // const ss = this.processFn("switch(e){case\"string_Case1_string\":33;case\"string_Case2_string\":44};\nconst p={s:\"string_Var1_string\",f:`string_Var2_string`,e:\"string_Var3_string\"};"); + // console.log(21399, ss); - // ────────────────────────────────────────────────────────────── - // 5. Compress - const json = JSON.stringify(extracted); - const deflated = pako.deflateRaw(Buffer.from(json, "utf8"), { level: 6 }); - if (!deflated) throw new Error("Pako Compression Failed"); - const base64 = Buffer.from(deflated).toString("base64"); + // await new Promise(r => setTimeout(r, 300000)); - // ────────────────────────────────────────────────────────────── - // 6. Wrap - const finalSource = compileDecodeSource(ms.toString(), base64, vName); + const ret = this.processFn(source, filename); + if (ret === false) continue; + source = ret.source; + const { finalSource, extracted, usedIds } = ret; compilation.updateAsset(filename, new compiler.webpack.sources.RawSource(finalSource)); - console.log(`[ZipExecutionPlugin] Processed ${filename}: ${extracted.length} unique strings extracted`); + console.debug(`[ZipExecutionPlugin] Processed ${filename}: ${extracted.length} unique strings extracted`); + console.debug(`[ZipExecutionPlugin] Replaced ${usedIds.size} extractions`); } } ); }); } - private collectCandidates(ast: acorn.Node): Omit[] { - const results: Omit[] = []; + private collectCandidates(ast: acorn.Node, source: string): Omit[] { + const results: Omit[] = []; + + const getPadding = (start: number, end: number) => { + //xy"abcd"jk + //3 7 + //s[3-1] = s[2] = " + //s[7+1] = s[8] = j + const c1 = source[start - 1] || ""; + const c2 = source[end] || ""; + const isWord = /[\w$"'`]/; + return { + prefix: isWord.test(c1) ? " " : "", + suffix: isWord.test(c2) ? " " : "", + }; + }; const walk = (node: any, parent: any = null) => { if (!node || typeof node !== "object") return; - if (this.isExtractable(node, parent)) { - if (node.type === "Literal" && typeof node.value === "string") { - results.push({ - type: "Literal", - start: node.start!, - end: node.end!, - value: node.value, - }); - return; // no children + if (node.type === "Literal" && typeof node.value === "string") { + if (this.isExtractable(node, parent, "Literal")) { + const { prefix, suffix } = getPadding(node.start, node.end); + const oriLen = node.end - node.start; // "targetString" + // someFn("123456") -> someFn($X[1234]) + // note: case"1234567" -> case $X[1234] + const isZZ = oriLen >= 8 + prefix.length + suffix.length; + if (isZZ) { + results.push({ + type: "Literal", + start: node.start, + end: node.end, + value: node.value, + zz: true, + prefix, + suffix, + }); + } } - - // for node.expressions.length > 0, not implemented yet - if (node.type === "TemplateLiteral" && node.expressions.length === 0) { + } else if (node.type === "TemplateLiteral") { + if (node.expressions.length === 0) { + // Static Template: treat as one unit const quasi = node.quasis[0]; - const value = quasi.value.cooked ?? quasi.value.raw; - results.push({ - type: "Template", - start: quasi.start!, - end: quasi.end!, - expressions: node.expressions.length, - value, + const val = quasi.value.cooked ?? quasi.value.raw; + if (this.isExtractable(quasi, parent, "Template")) { + // Templates overwrite backticks, so peek 1 char further out + // someFn(`123456`) -> someFn($X[1234]) + // node = `Template` + // quasi = Template + const { prefix, suffix } = getPadding(quasi.start - 1, quasi.end + 1); + const oriLen = quasi.end - quasi.start; // targetString + const isZZ = oriLen >= 6 + prefix.length + suffix.length; + const hasNewline = val.includes("\n") || val.includes("\r"); + if (isZZ || hasNewline) { + results.push({ + type: "Template", + start: quasi.start, + end: quasi.end, + value: val, + zz: isZZ, + prefix: isZZ ? prefix : "", + suffix: isZZ ? suffix : "", + }); + } + } + } else { + // Complex Template: extract individual quasis + node.quasis.forEach((quasi: any) => { + const val = quasi.value.cooked ?? quasi.value.raw; + if (val && this.isExtractable(quasi, parent, "Quasi", val)) { + // Quasis are inside `${}`, usually don't need padding relative to word boundaries + // `${...}123456789ab${...}` -> `${...}${$X[1234]}${...}` + const oriLen = quasi.end - quasi.start; // `${...}targetString${...}` + const isZZ = oriLen >= 11; + const hasNewline = val.includes("\n") || val.includes("\r"); + if (isZZ || hasNewline) { + results.push({ + type: "Quasi", + start: quasi.start, + end: quasi.end, + value: val, + zz: isZZ, + prefix: "", + suffix: "", + }); + } + } }); - return; } } - // Safe child traversal for (const key of Object.keys(node)) { if (["parent", "loc", "range", "start", "end"].includes(key)) continue; const child = node[key]; @@ -214,40 +349,33 @@ export class ZipExecutionPlugin { return results; } - private isExtractable(node: any, parent: any): boolean { - const isLiteral = node.type === "Literal" && typeof node.value === "string"; - const isTemplate = node.type === "TemplateLiteral"; + private isExtractable(node: any, parent: any, type: "Literal" | "Template" | "Quasi", overrideVal?: string): boolean { + const content = overrideVal ?? (node.type === "Literal" ? node.value : (node.value.cooked ?? "")); - if (!isLiteral && !isTemplate) return false; + // Thresholds: Quasis need more length because they add `${}` (3 chars) + // if (type === "Quasi" && content.length < 12) return false; + // if (type === "Template" && content.length < 7) return false; + // if (type === "Literal" && content.length < 9) return false; - // Length filter (runtime value) - const content = isLiteral ? node.value : (node.quasis[0].value.cooked ?? ""); - if (isTemplate && node.expressions?.length === 0) { - if (content.length < 7) return false; // `1234567` => $S[6789] - } else { - // isLiteral or isTemplate with node.expressions - if (content.length < 9) return false; // "123456789" => ($S[6789]) - } + // ---- Exclusions ---- - // ── Exclusions ───────────────────────────────────────────────────── // "use strict" - if (parent?.type === "ExpressionStatement" && node.value === "use strict") return false; + if (parent?.type === "ExpressionStatement" && content === "use strict") return false; - // Tagged templates - if (parent?.type === "TaggedTemplateExpression" && parent.quasi === node) return false; + // Tagged templates but type is not "Quasi" + if (parent?.type === "TaggedTemplateExpression" && type !== "Quasi") return false; // Object keys (non-computed) if (parent?.type === "Property" && parent.key === node && !parent.computed) return false; // Import/export sources - if ( + const isModuleSource = parent && (parent.type === "ImportDeclaration" || parent.type === "ExportNamedDeclaration" || parent.type === "ExportAllDeclaration") && - parent.source === node - ) - return false; + parent.source === node; + if (isModuleSource) return false; // Dynamic import if (parent?.type === "ImportExpression" && parent.source === node) return false; @@ -256,7 +384,6 @@ export class ZipExecutionPlugin { } private normalizeValue(value: string): string { - // Only standardise line endings – never escape anything - return value.includes("\r") ? value.replace(/\r\n/g, "\n").replace(/\r/g, "\n") : value; + return value.replace(/\r\n/g, "\n").replace(/\r/g, "\n"); } } From 7bfacbbeb85ac60bbb94ad575676810391614e99 Mon Sep 17 00:00:00 2001 From: cyfung1031 <44498510+cyfung1031@users.noreply.github.com> Date: Wed, 11 Feb 2026 12:48:04 +0900 Subject: [PATCH 12/18] No "background" permission in Firefox MV3 --- scripts/pack.js | 2 + src/pages/components/RuntimeSetting/index.tsx | 77 +++++++++++-------- 2 files changed, 46 insertions(+), 33 deletions(-) diff --git a/scripts/pack.js b/scripts/pack.js index f3784c8c3..58a5af4fb 100644 --- a/scripts/pack.js +++ b/scripts/pack.js @@ -77,6 +77,8 @@ const chromeManifest = { ...manifest, background: { ...manifest.background } }; chromeManifest.optional_permissions = chromeManifest.optional_permissions.filter((val) => val !== "userScripts"); delete chromeManifest.background.scripts; +// Firefox MV3 不支持 "background" permission +firefoxManifest.optional_permissions = firefoxManifest.optional_permissions.filter((val) => val !== "background"); delete firefoxManifest.background.service_worker; delete firefoxManifest.sandbox; firefoxManifest.browser_specific_settings = { diff --git a/src/pages/components/RuntimeSetting/index.tsx b/src/pages/components/RuntimeSetting/index.tsx index 60e7d2c7a..b96477f71 100644 --- a/src/pages/components/RuntimeSetting/index.tsx +++ b/src/pages/components/RuntimeSetting/index.tsx @@ -5,6 +5,7 @@ import FileSystemParams from "../FileSystemParams"; import { systemConfig } from "@App/pages/store/global"; import type { FileSystemType } from "@Packages/filesystem/factory"; import FileSystemFactory from "@Packages/filesystem/factory"; +import { isFirefox } from "@App/pkg/utils/utils"; const CollapseItem = Collapse.Item; @@ -24,52 +25,62 @@ const RuntimeSetting: React.FC = () => { setFilesystemType(res.filesystem); setFilesystemParam(res.params[res.filesystem] || {}); }); - chrome.permissions.contains({ permissions: ["background"] }, (result) => { - if (chrome.runtime.lastError) { - console.error(chrome.runtime.lastError); - return; - } - setEnableBackgroundState(result); - }); - }, []); - - const setEnableBackground = (enable: boolean) => { - if (enable) { - chrome.permissions.request({ permissions: ["background"] }, (granted) => { - if (chrome.runtime.lastError) { - console.error(chrome.runtime.lastError); - Message.error(t("enable_background.enable_failed")!); - return; - } - setEnableBackgroundState(granted); - }); + if (isFirefox()) { + // no background permission } else { - chrome.permissions.remove({ permissions: ["background"] }, (removed) => { + chrome.permissions.contains({ permissions: ["background"] }, (result) => { if (chrome.runtime.lastError) { console.error(chrome.runtime.lastError); - Message.error(t("enable_background.disable_failed")!); return; } - setEnableBackgroundState(!removed); + setEnableBackgroundState(result); }); } + }, []); + + const setEnableBackground = (enable: boolean) => { + if (isFirefox()) { + // no background permission + } else { + if (enable) { + chrome.permissions.request({ permissions: ["background"] }, (granted) => { + if (chrome.runtime.lastError) { + console.error(chrome.runtime.lastError); + Message.error(t("enable_background.enable_failed")!); + return; + } + setEnableBackgroundState(granted); + }); + } else { + chrome.permissions.remove({ permissions: ["background"] }, (removed) => { + if (chrome.runtime.lastError) { + console.error(chrome.runtime.lastError); + Message.error(t("enable_background.disable_failed")!); + return; + } + setEnableBackgroundState(!removed); + }); + } + } }; return (
-
- - { - setEnableBackground(!enableBackground); - }} - > - {t("enable_background.title")} - -
+ {!isFirefox() && ( +
+ + { + setEnableBackground(!enableBackground); + }} + > + {t("enable_background.title")} + +
+ )} {t("enable_background.description")}
From 17a3988ef54eb0d07e00ed1bfa369e39128b0b4e Mon Sep 17 00:00:00 2001 From: cyfung1031 <44498510+cyfung1031@users.noreply.github.com> Date: Wed, 11 Feb 2026 13:16:49 +0900 Subject: [PATCH 13/18] drop pako --- package.json | 1 - pnpm-lock.yaml | 8 -------- 2 files changed, 9 deletions(-) diff --git a/package.json b/package.json index 0df81cbc6..61e76bfd3 100644 --- a/package.json +++ b/package.json @@ -41,7 +41,6 @@ "i18next": "^23.16.4", "magic-string": "^0.30.21", "monaco-editor": "^0.52.2", - "pako": "^2.1.0", "react": "^18.3.1", "react-dom": "^18.3.1", "react-dropzone": "^14.3.8", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 15cd06d86..afa1152dc 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -56,9 +56,6 @@ importers: monaco-editor: specifier: ^0.52.2 version: 0.52.2 - pako: - specifier: ^2.1.0 - version: 2.1.0 react: specifier: ^18.3.1 version: 18.3.1 @@ -3165,9 +3162,6 @@ packages: pako@1.0.11: resolution: {integrity: sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==} - pako@2.1.0: - resolution: {integrity: sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==} - parent-module@1.0.1: resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} engines: {node: '>=6'} @@ -7519,8 +7513,6 @@ snapshots: pako@1.0.11: {} - pako@2.1.0: {} - parent-module@1.0.1: dependencies: callsites: 3.1.0 From 58d01b9b8bb92e706fcca525f3f409e6a4492774 Mon Sep 17 00:00:00 2001 From: cyfung1031 <44498510+cyfung1031@users.noreply.github.com> Date: Wed, 11 Feb 2026 16:03:02 +0900 Subject: [PATCH 14/18] =?UTF-8?q?=E7=B4=A7=E6=80=A5=E4=BF=AE=E5=A4=8D?= =?UTF-8?q?=EF=BC=9A=20=E7=8E=AF=E5=A2=83=E5=88=9D=E5=A7=8B=E5=8C=96?= =?UTF-8?q?=E6=97=B6=E5=8F=8D=E6=B3=A8=E5=86=8C=E6=9C=AA=E6=AD=A3=E7=A1=AE?= =?UTF-8?q?=E6=89=A7=E8=A1=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/service/service_worker/runtime.ts | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/app/service/service_worker/runtime.ts b/src/app/service/service_worker/runtime.ts index 4fa3acbbc..6e073fe63 100644 --- a/src/app/service/service_worker/runtime.ts +++ b/src/app/service/service_worker/runtime.ts @@ -667,9 +667,10 @@ export class RuntimeService { } // 取消脚本注册 - async unregisterUserscripts() { + async unregisterUserscripts(forced: boolean = false) { // 检查 registered 避免重复操作增加系统开支 - if (runtimeGlobal.registered) { + // ( registerUserscripts 的环境初始化时必须强制执行 ) + if (forced || runtimeGlobal.registered) { runtimeGlobal.registered = false; // 重置 flag 避免取消注册失败 // 即使注册失败,通过重置 flag 可避免错误地呼叫已取消注册的Script @@ -905,8 +906,9 @@ export class RuntimeService { this.logger.warn("registered = true but scriptcat-content/scriptcat-inject not exists, re-register userscripts."); runtimeGlobal.registered = false; // 异常时强制反注册 } - // 删除旧注册 - await this.unregisterUserscripts(); + // runtimeGlobal.registered 已重置为 false + // 删除旧注册 (registered已重置为 false。因此必须强制执行) + await this.unregisterUserscripts(true); // 使注册时重新注入 chrome.runtime try { await chrome.userScripts.resetWorldConfiguration(); From 3fcea642bf4c5c25a38e27975cad70d7c0df15df Mon Sep 17 00:00:00 2001 From: cyfung1031 <44498510+cyfung1031@users.noreply.github.com> Date: Wed, 11 Feb 2026 16:09:54 +0900 Subject: [PATCH 15/18] =?UTF-8?q?v1.3=20=E4=BF=AE=E6=AD=A3=E5=90=8D?= =?UTF-8?q?=E5=AD=97=EF=BC=9A=20scriptcat-content=20->=20scriptcat-scripti?= =?UTF-8?q?ng?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/service/service_worker/runtime.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/service/service_worker/runtime.ts b/src/app/service/service_worker/runtime.ts index 6e073fe63..8423e7a2e 100644 --- a/src/app/service/service_worker/runtime.ts +++ b/src/app/service/service_worker/runtime.ts @@ -839,7 +839,7 @@ export class RuntimeService { // 注意:Chrome 不支持 file.js?query retContent = [ { - id: "scriptcat-content", + id: "scriptcat-scripting", js: ["/src/scripting.js"], matches: [""], allFrames: true, From 6fcc2106751c1a309d8bd0672dc17609c18e2884 Mon Sep 17 00:00:00 2001 From: cyfung1031 <44498510+cyfung1031@users.noreply.github.com> Date: Wed, 11 Feb 2026 17:21:38 +0900 Subject: [PATCH 16/18] =?UTF-8?q?v1.3=20=E4=BF=AE=E6=AD=A3=E5=8F=8D?= =?UTF-8?q?=E6=B3=A8=E5=86=8C=E6=9C=AA=E6=AD=A3=E7=A1=AE=E6=89=A7=E8=A1=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/service/service_worker/runtime.ts | 41 +++++++++++++++-------- 1 file changed, 27 insertions(+), 14 deletions(-) diff --git a/src/app/service/service_worker/runtime.ts b/src/app/service/service_worker/runtime.ts index 8423e7a2e..8ea36659e 100644 --- a/src/app/service/service_worker/runtime.ts +++ b/src/app/service/service_worker/runtime.ts @@ -53,9 +53,20 @@ import { scriptToMenu, type TPopupPageLoadInfo } from "./popup_scriptmenu"; const ORIGINAL_URLMATCH_SUFFIX = "{ORIGINAL}"; // 用于标记原始URLPatterns的后缀 +const RuntimeRegisterCode = { + UNSET: 0, + REGISTER_DONE: 1, + UNREGISTER_DONE: 2, +} as const; + +type RuntimeRegisterCode = ValueOf; + const runtimeGlobal = { - registered: false, + registerState: RuntimeRegisterCode.UNSET, messageFlag: "PENDING", +} as { + registerState: RuntimeRegisterCode; + messageFlag: string; }; export type TTabInfo = { @@ -314,15 +325,15 @@ export class RuntimeService { await cacheInstance.set("runtimeStartFlag", true); } - let registered = false; + let count = 0; try { const res = await chrome.userScripts?.getScripts({ ids: ["scriptcat-inject"] }); - registered = res?.length === 1; + count = res?.length; } catch { // 该错误为预期内情况,无需记录 debug 日志 } finally { // 考虑 UserScripts API 不可使用等情况 - runtimeGlobal.registered = registered; + runtimeGlobal.registerState = count === 1 ? RuntimeRegisterCode.REGISTER_DONE : RuntimeRegisterCode.UNSET; } } @@ -667,11 +678,11 @@ export class RuntimeService { } // 取消脚本注册 - async unregisterUserscripts(forced: boolean = false) { + async unregisterUserscripts() { // 检查 registered 避免重复操作增加系统开支 - // ( registerUserscripts 的环境初始化时必须强制执行 ) - if (forced || runtimeGlobal.registered) { - runtimeGlobal.registered = false; + // 已成功注册(true)或是未知有无注册(null)的情况下执行 + if (runtimeGlobal.registerState !== RuntimeRegisterCode.UNREGISTER_DONE) { + runtimeGlobal.registerState = RuntimeRegisterCode.UNREGISTER_DONE; // 重置 flag 避免取消注册失败 // 即使注册失败,通过重置 flag 可避免错误地呼叫已取消注册的Script await Promise.allSettled([chrome.userScripts?.unregister(), chrome.scripting.unregisterContentScripts()]); @@ -894,7 +905,7 @@ export class RuntimeService { if (!this.isUserScriptsAvailable || !this.isLoadScripts) return; // 判断是否已经注册过 - if (runtimeGlobal.registered) { + if (runtimeGlobal.registerState === RuntimeRegisterCode.REGISTER_DONE) { // 异常情况 // 检查scriptcat-content和scriptcat-inject是否存在 const res = await chrome.userScripts.getScripts({ ids: ["scriptcat-inject"] }); @@ -904,11 +915,10 @@ export class RuntimeService { // scriptcat-content/scriptcat-inject不存在的情况 // 走一次重新注册的流程 this.logger.warn("registered = true but scriptcat-content/scriptcat-inject not exists, re-register userscripts."); - runtimeGlobal.registered = false; // 异常时强制反注册 + runtimeGlobal.registerState = RuntimeRegisterCode.UNSET; // 异常时强制反注册 } - // runtimeGlobal.registered 已重置为 false - // 删除旧注册 (registered已重置为 false。因此必须强制执行) - await this.unregisterUserscripts(true); + // 删除旧注册 + await this.unregisterUserscripts(); // 使注册时重新注入 chrome.runtime try { await chrome.userScripts.resetWorldConfiguration(); @@ -928,7 +938,7 @@ export class RuntimeService { const list: chrome.userScripts.RegisteredUserScript[] = [...particularScriptList, ...injectScriptList]; - runtimeGlobal.registered = true; + let failed = false; try { await chrome.userScripts.register(list); } catch (e: any) { @@ -943,6 +953,7 @@ export class RuntimeService { try { await chrome.userScripts.update([script]); } catch (e) { + failed = true; this.logger.error("update error", Logger.E(e)); } } else { @@ -955,9 +966,11 @@ export class RuntimeService { try { await chrome.scripting.registerContentScripts(contentScriptList); } catch (e: any) { + failed = true; this.logger.error("register content.js error", Logger.E(e)); } } + runtimeGlobal.registerState = failed ? RuntimeRegisterCode.UNSET : RuntimeRegisterCode.REGISTER_DONE; } // 给指定tab发送消息 From 30cb5ca48edfba218f8e3bd5644ab97d60c452ea Mon Sep 17 00:00:00 2001 From: cyfung1031 <44498510+cyfung1031@users.noreply.github.com> Date: Thu, 12 Feb 2026 00:16:36 +0900 Subject: [PATCH 17/18] =?UTF-8?q?inflateRaw=20=E4=BB=A3=E7=A0=81=E6=9B=B4?= =?UTF-8?q?=E6=96=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- rspack-plugins/ZipExecutionPlugin.ts | 33 ++++++++++++++-------------- 1 file changed, 16 insertions(+), 17 deletions(-) diff --git a/rspack-plugins/ZipExecutionPlugin.ts b/rspack-plugins/ZipExecutionPlugin.ts index 3a9b7adcc..3b18e0f14 100644 --- a/rspack-plugins/ZipExecutionPlugin.ts +++ b/rspack-plugins/ZipExecutionPlugin.ts @@ -5,7 +5,7 @@ import * as acorn from "acorn"; import MagicString from "magic-string"; const trimCode = (code: string) => { - return code.trim(); + return code.replace(/[\r\n]\s+/g, "").trim(); }; export function compileDecodeSource(templateCode: string, base64Data: string, pName: string) { @@ -14,22 +14,21 @@ export function compileDecodeSource(templateCode: string, base64Data: string, pN // * See https://github.com/js-vanilla/inflate-raw/ const inflateRawCode = trimCode(` (()=>{let _=Uint8Array,e=_.fromBase64?.bind(_)??(e=>{let l=atob(e),$=l.length,r=new _($);for(;$--;)r[$]=l.charCodeAt($);return r}), - l=l=>{let $=e(l),r=4*$.length;r<32768&&(r=32768);let t=new _(r),a=0,f=e=>{let l=t.length,$=a+e;if($>l){do l=3*l>>>1;while(l<$);let r=new _(l);r.set(t),t=r}}, - s=new Uint16Array(66400),n=s.subarray(0,32768),u=s.subarray(32768,65536),b=s.subarray(65536,65856),i,o,y=new Int32Array(48),h=0,w=0,g=0, - d=()=>{for(;w<16&&g<$.length;)h|=$[g++]<{d();let e=h&(1<<_)-1;return h>>>=_,w-=_,e},F=[16,17,18,0,8,7,9,6,10,5,11,4,12,3,13,2,14,1,15], - k=[3,4,5,6,7,8,9,10,11,13,15,17,19,23,27,31,35,43,51,59,67,83,99,115,131,163,195,227,258],m=[0,0,0,0,0,0,0,0,1,1,1,1,2,2,2,2,3,3,3,3,4,4,4,4,5,5,5,5,0], - p=[1,2,3,4,5,7,9,13,17,25,33,49,65,97,129,193,257,385,513,769,1025,1537,2049,3073,4097,6145,8193,12289,16385,24577], - v=[0,0,0,0,1,1,2,2,3,3,4,4,5,5,6,6,7,7,8,8,9,9,10,10,11,11,12,12,13,13],x=y.subarray(0,16),A=y.subarray(16,32),B=(_,e)=>{let l=x.fill(0),$=0; - for(let r=0;r<_.length;r++){let t=_[r];t>0&&(l[t]++,t>$&&($=t))}let a=1<<$,f=e.subarray(0,a),s=A,n=0;for(let u=1;u<=$;u++)s[u]=n,n+=l[u];let i=b; - for(let o=0;o<_.length;o++)_[o]>0&&(i[s[_[o]]++]=o);let y=0,h=0;for(let w=1;w<=$;w++){let g=1<>=1;y^=p}}return f},C=_=>{d();let e=_.length-1,l=_[h&e],$=l>>>9;return h>>>=$,w-=$,511&l}, - R=new _(320),W=R.subarray(0,19),j=!1,q=0;for(;!q;){let z=c(3);q=1&z;let D=z>>1;if(0===D){h=w=0;let E=$[g++]|$[g++]<<8;g+=2,f(E),t.set($.subarray(g,g+E),a),a+=E, - g+=E}else{let G,H;if(1===D){if(!j){j=!0;let I=65856,J=R.subarray(0,288);J.fill(8,0,144),J.fill(9,144,256),J.fill(7,256,280),J.fill(8,280,288),B(J,i=s.subarray(I,I+=512)); - let K=R.subarray(0,32).fill(5);B(K,o=s.subarray(I,I+=32))}G=i,H=o}else{let L=c(14),M=(31&L)+257,N=(L>>5&31)+1,O=(L>>10&15)+4;W.fill(0);for(let P=0;P{let $=e(l),r=4*$.length;r<32768&&(r=32768);let t=new _(r),a=0,f=e=>{let l=t.length,$=a+e;if($>l){do l=3*l>>>1;while(l<$); + let r=new _(l);r.set(t),t=r}},s=new Uint16Array(66400),u=s.subarray(0,32768),b=s.subarray(32768,65536),n=s.subarray(65536,65856),i, + o,y=new Int32Array(48),h=0,w=0,g=0,d=()=>{for(;w<16&&g<$.length;)h|=$[g++]<{d();let e=h&(1<<_)-1;return h>>>=_,w-=_,e}, + F=[16,17,18,0,8,7,9,6,10,5,11,4,12,3,13,2,14,1,15],k=[3,4,5,6,7,8,9,10,11,13,15,17,19,23,27,31,35,43,51,59,67,83,99,115,131,163,195,227,258], + m=[0,0,0,0,0,0,0,0,1,1,1,1,2,2,2,2,3,3,3,3,4,4,4,4,5,5,5,5,0],v=[1,2,3,4,5,7,9,13,17,25,33,49,65,97,129,193,257,385,513,769,1025,1537,2049,3073,4097,6145,8193,12289,16385,24577], + x=[0,0,0,0,1,1,2,2,3,3,4,4,5,5,6,6,7,7,8,8,9,9,10,10,11,11,12,12,13,13],A=y.subarray(0,16),B=y.subarray(16,32),C=(_,e)=>{let l=A.fill(0),$=0;for(let r=0;r<_.length;r++){ + let t=_[r];t>0&&(l[t]++,t>$&&($=t))}let a=1<<$,f=e.subarray(0,a),s=B,u=0;for(let b=1;b<=$;b++)s[b]=u,u+=l[b];let i=n;for(let o=0;o<_.length;o++)_[o]>0&&(i[s[_[o]]++]=o); + let y=0,h=0;for(let w=1;w<=$;w++){let g=1<>=1;y^=v}}return f}, + R=_=>{d();let e=_.length-1,l=_[h&e],$=l>>>9;return h>>>=$,w-=$,511&l},j=new _(320),p=j.subarray(0,19),q=!1,z=0;for(;!z;){let D=c(3);z=1&D;let E=D>>1;if(0===E){ + h=w=0;let G=$[g++]|$[g++]<<8;g+=2,f(G),t.set($.subarray(g,g+G),a),a+=G,g+=G}else{let H,I;if(1===E){if(!q){q=!0;let J=65856,K=j.subarray(0,288);K.fill(8,0,144),K.fill(9,144,256), + K.fill(7,256,280),K.fill(8,280,288),C(K,i=s.subarray(J,J+=512));let L=j.subarray(0,32).fill(5);C(L,o=s.subarray(J,J+=32))}H=i,I=o}else{let M=c(14),N=(31&M)+257,O=(M>>5&31)+1, + P=(M>>10&15)+4;p.fill(0);for(let Q=0;Q0;){ + let _t=a-_r;_e<_t&&(_t=_e),t.set(t.subarray(_r,_r+_t),a),a+=_t,_e-=_t}}}}}return new TextDecoder().decode(t.subarray(0,a))};return l})(); `); // ------------------------------------------------------------------------------------------------- return ` From a085cba1627f84010d897308de79b46e6d4b4c03 Mon Sep 17 00:00:00 2001 From: cyfung1031 <44498510+cyfung1031@users.noreply.github.com> Date: Thu, 12 Feb 2026 00:55:09 +0900 Subject: [PATCH 18/18] =?UTF-8?q?ZipExecutionPlugin=E4=BB=A3=E7=A0=81?= =?UTF-8?q?=E8=B0=83=E6=95=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- rspack-plugins/ZipExecutionPlugin.ts | 27 +++++++++++++-------------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/rspack-plugins/ZipExecutionPlugin.ts b/rspack-plugins/ZipExecutionPlugin.ts index 3b18e0f14..cafd1bf63 100644 --- a/rspack-plugins/ZipExecutionPlugin.ts +++ b/rspack-plugins/ZipExecutionPlugin.ts @@ -65,9 +65,10 @@ const findAvailableVarName = (source: string) => { const findShortName = (source: string) => { // $H - const candidates = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" - .split("") - .map((c) => [c, source.split("$" + c).filter((w, i) => i === 0 || !/[\w$]/.test(w[0])).length] as const); + const filterFn = (w: string, i: number) => i === 0 || !/[\w$]/.test(w[0]); + const candidates = [..."abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"].map( + (c) => [c, source.split("$" + c).filter(filterFn).length] as const + ); candidates.sort((a, b) => a[1] - b[1]); const [candidateChar, _candidatesFreq] = candidates[0]; const pName = "$" + candidateChar; @@ -136,9 +137,9 @@ export class ZipExecutionPlugin { } } - mapped.forEach(([c, d, q]) => { + for (const [c, d, q] of mapped) { operations.push({ ...c, d: d, id: q[1], freq: q[0] }); - }); + } // Replace bottom-up (safe offsets) operations.sort((a, b) => b.start - a.start); @@ -225,11 +226,6 @@ export class ZipExecutionPlugin { let source = asset.source().toString(); - // const ss = this.processFn("switch(e){case\"string_Case1_string\":33;case\"string_Case2_string\":44};\nconst p={s:\"string_Var1_string\",f:`string_Var2_string`,e:\"string_Var3_string\"};"); - // console.log(21399, ss); - - // await new Promise(r => setTimeout(r, 300000)); - const ret = this.processFn(source, filename); if (ret === false) continue; source = ret.source; @@ -312,7 +308,7 @@ export class ZipExecutionPlugin { } } else { // Complex Template: extract individual quasis - node.quasis.forEach((quasi: any) => { + for (const quasi of node.quasis) { const val = quasi.value.cooked ?? quasi.value.raw; if (val && this.isExtractable(quasi, parent, "Quasi", val)) { // Quasis are inside `${}`, usually don't need padding relative to word boundaries @@ -332,14 +328,14 @@ export class ZipExecutionPlugin { }); } } - }); + } } } for (const key of Object.keys(node)) { if (["parent", "loc", "range", "start", "end"].includes(key)) continue; const child = node[key]; - if (Array.isArray(child)) child.forEach((c) => walk(c, node)); + if (Array.isArray(child)) for (const c of child) walk(c, node); else if (child && typeof child === "object" && child.type) walk(child, node); } }; @@ -383,6 +379,9 @@ export class ZipExecutionPlugin { } private normalizeValue(value: string): string { - return value.replace(/\r\n/g, "\n").replace(/\r/g, "\n"); + if (value.includes("\r")) { + value = value.replace(/\r\n|\r/g, "\n"); + } + return value; } }