diff --git a/package.json b/package.json index 2c09dc018..61e76bfd3 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,6 +39,7 @@ "eslint-linter-browserify": "9.26.0", "eventemitter3": "^5.0.1", "i18next": "^23.16.4", + "magic-string": "^0.30.21", "monaco-editor": "^0.52.2", "react": "^18.3.1", "react-dom": "^18.3.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ff52d0907..afa1152dc 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,6 +50,9 @@ 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 @@ -782,12 +788,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 +1449,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 +2897,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==} @@ -4791,7 +4778,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 +4794,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 +5492,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 +5509,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 +5630,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 +7271,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 @@ -8383,7 +8358,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 +8583,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 diff --git a/rspack-plugins/ZipExecutionPlugin.ts b/rspack-plugins/ZipExecutionPlugin.ts new file mode 100644 index 000000000..3a9b7adcc --- /dev/null +++ b/rspack-plugins/ZipExecutionPlugin.ts @@ -0,0 +1,389 @@ +import type { Compiler, Compilation } from "@rspack/core"; +import zlib from "zlib"; + +import * as acorn from "acorn"; +import MagicString from "magic-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 { + // "zzstrs" + for (let e = 0xc0; e <= 0xff; e++) { + if (e === 0xd7 || e === 0xf7) continue; + const c = "$" + String.fromCharCode(e); + 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( + { + 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; + + 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; + const { finalSource, extracted, usedIds } = ret; + + compilation.updateAsset(filename, new compiler.webpack.sources.RawSource(finalSource)); + + console.debug(`[ZipExecutionPlugin] Processed ${filename}: ${extracted.length} unique strings extracted`); + console.debug(`[ZipExecutionPlugin] Replaced ${usedIds.size} extractions`); + } + } + ); + }); + } + + 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 (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, + }); + } + } + } else if (node.type === "TemplateLiteral") { + if (node.expressions.length === 0) { + // Static Template: treat as one unit + const quasi = node.quasis[0]; + 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: "", + }); + } + } + }); + } + } + + 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, type: "Literal" | "Template" | "Quasi", overrideVal?: string): boolean { + const content = overrideVal ?? (node.type === "Literal" ? node.value : (node.value.cooked ?? "")); + + // 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; + + // ---- Exclusions ---- + + // "use strict" + if (parent?.type === "ExpressionStatement" && content === "use strict") 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 + const isModuleSource = + parent && + (parent.type === "ImportDeclaration" || + parent.type === "ExportNamedDeclaration" || + parent.type === "ExportAllDeclaration") && + parent.source === node; + if (isModuleSource) return false; + + // Dynamic import + if (parent?.type === "ImportExpression" && parent.source === node) return false; + + return true; + } + + private normalizeValue(value: string): string { + return value.replace(/\r\n/g, "\n").replace(/\r/g, "\n"); + } +} diff --git a/rspack.config.ts b/rspack.config.ts index 6409878d5..f238bddd1 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"; @@ -47,6 +48,7 @@ export default defineConfig({ service_worker: `${src}/service_worker.ts`, offscreen: `${src}/offscreen.ts`, sandbox: `${src}/sandbox.ts`, + ff_persistent: `${src}/ff_persistent.ts`, content: `${src}/content.ts`, scripting: `${src}/scripting.ts`, inject: `${src}/inject.ts`, @@ -136,7 +138,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); }, @@ -220,6 +221,14 @@ export default defineConfig({ minify: true, chunks: ["sandbox"], }), + new rspack.HtmlRspackPlugin({ + filename: `${dist}/ext/src/ff_persistent.html`, + template: `${src}/pages/ff_persistent.html`, + inject: "head", + minify: true, + chunks: ["ff_persistent"], + }), + new ZipExecutionPlugin(), ].filter(Boolean), experiments: { css: true, @@ -243,7 +252,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 +270,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..58a5af4fb 100644 --- a/scripts/pack.js +++ b/scripts/pack.js @@ -74,13 +74,13 @@ 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; +// Firefox MV3 不支持 "background" permission +firefoxManifest.optional_permissions = firefoxManifest.optional_permissions.filter((val) => val !== "background"); 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: `{${ @@ -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/app/service/service_worker/runtime.ts b/src/app/service/service_worker/runtime.ts index 4fa3acbbc..8423e7a2e 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 @@ -838,7 +839,7 @@ export class RuntimeService { // 注意:Chrome 不支持 file.js?query retContent = [ { - id: "scriptcat-content", + id: "scriptcat-scripting", js: ["/src/scripting.js"], matches: [""], allFrames: true, @@ -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(); diff --git a/src/ff_persistent.ts b/src/ff_persistent.ts new file mode 100644 index 000000000..c0d351a00 --- /dev/null +++ b/src/ff_persistent.ts @@ -0,0 +1,46 @@ +let waitState = 0; + +let lastNow = 0; + +const dom = document.createElement("dom"); + +const runner = () => { + waitState = 1; + const c = +(dom.getAttribute("domvalue") || 0) as number; + const s = c > 255 ? "1" : `${c + 1}`; + dom.setAttribute("domvalue", s); + const now = Date.now(); + if (now - lastNow > 2000) { + lastNow = now; + // history.replaceState({ now: now }, "", `${location.pathname}?t=${now}`); + // chrome.alarms.create("ff_wakeup", { + // when: now + Math.round(Math.random() * 30 + 60), + // }); + chrome.storage.session.set({ ff_wakeup: `${now}` }); + document.title = `wakup at ${now}`; // debug + } +}; + +window.addEventListener("message", (response) => { + if (waitState === 2 && typeof response.data === "object" && response.data?.myCustomAction === "waked-up") { + runner(); + } +}); +const mutObserver = new MutationObserver(() => { + if (waitState === 1) { + waitState = 2; + // chrome.runtime.sendMessage({ myCustomAction: "wake-up-please" }, (_response) => { + // if (chrome.runtime.lastError) { + // // ignored + // } + // // nil + // top?.postMessage({ myCustomAction: "wake-up-please" }, "*"); + // }); + // top?.postMessage({ myCustomAction: "wake-up-please" }, "*"); + window?.postMessage({ myCustomAction: "waked-up" }, "*"); + } +}); +mutObserver.observe(dom, { attributes: true, attributeFilter: ["domvalue"] }); +// console.log("ff_persistent"); + +runner(); 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")}
diff --git a/src/pages/ff_persistent.html b/src/pages/ff_persistent.html new file mode 100644 index 000000000..26eca7346 --- /dev/null +++ b/src/pages/ff_persistent.html @@ -0,0 +1,11 @@ + + + + + + FF_persistent + + +
+ + diff --git a/src/persistent.ts b/src/persistent.ts new file mode 100644 index 000000000..b22e47840 --- /dev/null +++ b/src/persistent.ts @@ -0,0 +1,54 @@ +export const keepEventPageRunning = () => { + if (typeof document === "undefined") return; + if (typeof document.documentElement === "undefined") return; + if (document.getElementById("ff_persistent")) return; + // chrome.webNavigation.onHistoryStateUpdated.addListener( + // (_details) => { + // if (chrome.runtime.lastError) { + // // ignored + // } + // // console.log("ff_wakeup by webNavigation"); + // // nil + // }, + // { + // url: [{ hostEquals: new URL(chrome.runtime.getURL("/")).hostname }], + // } + // ); + // chrome.alarms.onAlarm.addListener((alarmInfo) => { + // if (chrome.runtime.lastError) { + // // ignored + // } + // if (alarmInfo.name === "ff_wakeup") { + // // console.log("ff_wakeup by Alarms"); + // // nil + // } + // }); + chrome.storage.session.onChanged.addListener((obj) => { + typeof obj.ff_wakeup !== "undefined"; + // if (typeof obj.ff_wakeup !== "undefined") { + // console.log("ff_wakeup by storage"); + // nil + // } + }); + // window.addEventListener("message", (response) => { + // if (typeof response.data === "object" && response.data?.myCustomAction === "wake-up-please") { + // (response.source)?.postMessage({ myCustomAction: "waked-up" }, "*"); + // } + // }); + // chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { + // if (chrome.runtime.lastError) { + // // ignored + // } + // if (message.myCustomAction === "wake-up-please") { + // // console.log("Received in background:", message.payload); + + // // Send response back to popup + // sendResponse({ now: Date.now() }); + // } + // return false; // true for asynchronous + // }); + const iframe = document.createElement("iframe"); + iframe.id = "ff_persistent"; + iframe.src = chrome.runtime.getURL("/src/ff_persistent.html"); + document.documentElement.appendChild(iframe); +}; diff --git a/src/service_worker.ts b/src/service_worker.ts index 62ff4d1c4..357b7b0a7 100644 --- a/src/service_worker.ts +++ b/src/service_worker.ts @@ -8,9 +8,11 @@ import { MessageQueue } from "@Packages/message/message_queue"; import { ServiceWorkerMessageSend } from "@Packages/message/window_message"; import migrate, { migrateChromeStorage } from "./app/migrate"; import { cleanInvalidKeys } from "./app/repo/resource"; +import { keepEventPageRunning } from "./persistent"; migrate(); migrateChromeStorage(); +keepEventPageRunning(); const OFFSCREEN_DOCUMENT_PATH = "src/offscreen.html";