Skip to content

Commit 9ec116f

Browse files
authored
⬆️ 修复 Firefox 中未能使用格式化、TSWorker等问题 (#1005)
1 parent d987ace commit 9ec116f

16 files changed

Lines changed: 225 additions & 228 deletions

build/pack.js

Lines changed: 46 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,41 @@ const semver = require("semver");
66
const manifest = require("../src/manifest.json");
77
const package = require("../package.json");
88

9+
// --- utils ---
10+
11+
const MAX_CHUNK_SIZE = 3 * 1024 * 1024; // 3 MiB
12+
13+
function addFileInChunks(zip, filePath, toDir, baseName, maxChunkSize = MAX_CHUNK_SIZE) {
14+
const buffer = fs.readFileSync(filePath);
15+
let offset = 0;
16+
17+
const chunks = [];
18+
while (offset < buffer.length) {
19+
const end = Math.min(offset + maxChunkSize, buffer.length);
20+
const chunk = buffer.subarray(offset, end);
21+
chunks.push(chunk);
22+
offset = end;
23+
}
24+
const len = chunks.length;
25+
26+
for (let idx = 0; idx < len; idx += 1) {
27+
const chunk = chunks[idx];
28+
// e.g. src/ts.worker.js.part30, src/ts.worker.js.part31, ...
29+
const chunkPath = `${toDir}${baseName}.part${idx}`;
30+
zip.file(chunkPath, chunk);
31+
}
32+
}
33+
34+
const createJSZip = () => {
35+
const currDate = new Date();
36+
const dateWithOffset = new Date(currDate.getTime() - currDate.getTimezoneOffset() * 60000);
37+
// replace the default date with dateWithOffset
38+
JSZip.defaults.date = dateWithOffset;
39+
return new JSZip();
40+
};
41+
42+
// --- utils ---
43+
944
// 判断是否为beta版本
1045
const version = semver.parse(package.version);
1146
if (version.prerelease.length) {
@@ -63,8 +98,8 @@ const chromeManifest = { ...manifest };
6398
delete chromeManifest.content_security_policy;
6499

65100
delete firefoxManifest.sandbox;
66-
// firefoxManifest.content_security_policy =
67-
// "script-src 'self' blob:; object-src 'self' blob:";
101+
// firefoxManifest.content_security_policy 是为了支持动态组合的 ts.worker.js Blob URL
102+
firefoxManifest.content_security_policy = "script-src 'self' blob:; object-src 'self' blob:";
68103
firefoxManifest.browser_specific_settings = {
69104
gecko: {
70105
id: `{${
@@ -95,8 +130,8 @@ firefoxManifest.permissions = firefoxManifest.permissions.filter(
95130
(permission) => permission !== "background"
96131
);
97132

98-
const chrome = new JSZip();
99-
const firefox = new JSZip();
133+
const chrome = createJSZip();
134+
const firefox = createJSZip();
100135

101136
function addDir(zip, localDir, toDir, filters) {
102137
const files = fs.readdirSync(localDir);
@@ -120,10 +155,13 @@ firefox.file("manifest.json", JSON.stringify(firefoxManifest));
120155

121156
addDir(chrome, "./dist/ext", "", ["manifest.json"]);
122157
addDir(firefox, "./dist/ext", "", ["manifest.json", "ts.worker.js"]);
123-
// 添加ts.worker.js名字为gz
124-
firefox.file(
125-
"src/ts.worker.js.gz",
126-
fs.readFileSync("./dist/ext/src/ts.worker.js")
158+
159+
// Now split ts.worker.js into chunks (<4MB each) for Firefox
160+
addFileInChunks(
161+
firefox,
162+
"./dist/ext/src/ts.worker.js", // source file on disk
163+
"src/", // folder path inside zip
164+
"ts.worker.js" // base name for chunked file
127165
);
128166

129167
// 导出zip包

package-lock.json

Lines changed: 0 additions & 7 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,11 @@
99
"lint": "eslint --ext .js,.ts,.tsx src/",
1010
"lint-fix": "eslint --ext .js,.ts,.tsx src/ --fix",
1111
"dev": "concurrently \"webpack --mode development --config ./webpack/webpack.dev.ts\" \"webpack --mode development --config ./webpack/webpack.inject.dev.ts\"",
12-
"build": "webpack --mode production --config ./webpack/webpack.prod.ts && concurrently \"npm run build:linter\" \"npm run build:inject && npm run build:no-split\"",
12+
"build": "webpack --mode production --config ./webpack/webpack.prod.ts && concurrently \"npm run build:linter\" \"npm run build:monaco\" \"npm run build:inject && npm run build:content\"",
1313
"build:linter": "webpack --mode production --config ./webpack/webpack.linter.ts",
1414
"build:inject": "webpack --mode production --config ./webpack/webpack.inject.ts",
15-
"build:no-split": "webpack --mode production --config ./webpack/webpack.no.split.ts",
15+
"build:content": "webpack --mode production --config ./webpack/webpack.content.ts",
16+
"build:monaco": "webpack --mode production --config ./webpack/webpack.monaco.ts",
1617
"dev:linter": "webpack --mode development --config ./webpack/webpack.linter.dev.ts",
1718
"pack": "node ./build/pack.js",
1819
"changlog": "gitmoji-changelog",
@@ -41,7 +42,6 @@
4142
"lodash": "^4.17.21",
4243
"monaco-editor": "^0.37.1",
4344
"monaco-vim": "^0.3.4",
44-
"pako": "^2.0.4",
4545
"react": "^18.3.1",
4646
"react-dom": "^18.3.1",
4747
"react-i18next": "^13.1.0",

src/pages/components/CodeEditor/index.tsx

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,6 @@ const CodeEditor: React.ForwardRefRenderFunction<
3030
return () => {};
3131
}
3232
let edit: editor.IStandaloneDiffEditor | editor.IStandaloneCodeEditor;
33-
// @ts-ignore
34-
const ts = window.tsUrl ? 0 : 200;
3533
setTimeout(() => {
3634
const div = document.getElementById(id) as HTMLDivElement;
3735
if (diffCode) {
@@ -70,7 +68,7 @@ const CodeEditor: React.ForwardRefRenderFunction<
7068

7169
setEditor(edit);
7270
}
73-
}, ts);
71+
}, 0);
7472
return () => {
7573
if (edit) {
7674
edit.dispose();

src/pkg/utils/monaco-editor.ts

Lines changed: 66 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,83 @@
1+
/* eslint-disable no-await-in-loop */
12
// @ts-ignore
23
// eslint-disable-next-line import/no-unresolved
34
import dts from "@App/types/scriptcat";
45
import Hook from "@App/app/service/hook";
56
import { languages } from "monaco-editor";
6-
import pako from "pako";
77
import Cache from "@App/app/cache";
8-
import { isFirefox } from "./utils";
98

109
// 注册eslint
1110
const linterWorker = new Worker("/src/linter.worker.js");
11+
const editorWorker = new Worker("/src/editor.worker.js", { type: "module" });
12+
13+
const getPartialBlob = (idx: number): Promise<Blob | null> => fetch(
14+
chrome.runtime.getURL(`/src/ts.worker.js.part${idx}`)
15+
).then((resp) => (resp.ok ? resp.blob() : null))
16+
.catch(() => null);
17+
const combineBlobsToUrl = async (blobs: Blob[], defaultType?: string): Promise<string> => {
18+
const arrayBuffers: ArrayBuffer[] = [];
19+
let totalLength = 0;
20+
21+
// Read all blobs into ArrayBuffers and compute total length
22+
for (const blob of blobs) {
23+
const arrayBuffer = await blob.arrayBuffer();
24+
arrayBuffers.push(arrayBuffer);
25+
totalLength += arrayBuffer.byteLength; // <-- sum, don't overwrite
26+
}
27+
28+
// Allocate a single Uint8Array large enough for everything
29+
const combined = new Uint8Array(totalLength);
30+
31+
// Copy each buffer into the combined array
32+
let offset = 0;
33+
for (const buffer of arrayBuffers) {
34+
combined.set(new Uint8Array(buffer), offset);
35+
offset += buffer.byteLength;
36+
}
37+
38+
// Create a single Blob out of the combined data
39+
const type = defaultType || blobs[0]?.type || "application/octet-stream";
40+
const combinedBlob = new Blob([combined], { type });
41+
42+
// Create a Blob URL
43+
const blobUrl = URL.createObjectURL(combinedBlob);
44+
// 注意:此处生成的 Blob URL 在整个应用生命周期内用于 Worker,不会被释放。
45+
// 如果未来 Worker 支持销毁重建,请在销毁时调用 URL.revokeObjectURL(blobUrl) 释放资源。
46+
return blobUrl;
47+
}
48+
49+
50+
const tsWorkerPromise = fetch(chrome.runtime.getURL("/src/ts.worker.js.part0")).then((resp) => {
51+
return resp.ok ? resp.blob() : null;
52+
}).catch(() => { return null }).then(async (blob) => {
53+
let worker: Worker;
54+
if (blob) {
55+
// 有分割
56+
const blobs: Blob[] = [];
57+
let idx = 0;
58+
do {
59+
blobs.push(blob);
60+
blob = await getPartialBlob(++idx);
61+
} while (blob);
62+
const url = await combineBlobsToUrl(blobs, "text/javascript");
63+
worker = new Worker(url, { type: "module" });
64+
} else {
65+
// 沒分割
66+
worker = new Worker("/src/ts.worker.js", { type: "module" });
67+
}
68+
return worker;
69+
});
1270

1371
export default function registerEditor() {
14-
// @ts-ignore
15-
window.tsUrl = "";
16-
17-
fetch(chrome.runtime.getURL(`/src/ts.worker.js${isFirefox() ? ".gz" : ""}`))
18-
.then((resp) => resp.blob())
19-
.then(async (blob) => {
20-
const result = pako.inflate(await blob.arrayBuffer());
21-
// @ts-ignore
22-
window.tsUrl = URL.createObjectURL(new Blob([result]));
23-
});
24-
// @ts-ignore
72+
2573
window.MonacoEnvironment = {
26-
getWorkerUrl(moduleId: any, label: any) {
74+
// https://microsoft.github.io/monaco-editor/typedoc/interfaces/Environment.html#getWorker
75+
// Returns Worker | Promise<Worker>
76+
getWorker(workerId: string, label: string) {
2777
if (label === "typescript" || label === "javascript") {
28-
// return "/src/ts.worker.js";
29-
// @ts-ignore
30-
return window.tsUrl;
78+
return tsWorkerPromise;
3179
}
32-
return "/src/editor.worker.js";
80+
return editorWorker;
3381
},
3482
};
3583

webpack.config.ts

Lines changed: 60 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ const dist = `${__dirname}/dist`;
1717
const assets = `${__dirname}/build/assets`;
1818
const template = `${assets}/template`;
1919

20-
const config: Configuration = {
20+
const configCommon: Configuration = {
2121
entry: {
2222
options: `${src}/pages/options/main.tsx`,
2323
install: `${src}/pages/install/main.tsx`,
@@ -209,4 +209,62 @@ const config: Configuration = {
209209
},
210210
};
211211

212-
export default config;
212+
const configWebWorker: Configuration = {
213+
...configCommon,
214+
215+
optimization: {
216+
minimize: true,
217+
splitChunks: false,
218+
runtimeChunk: false,
219+
minimizer: [
220+
new TerserPlugin({
221+
extractComments: false, // 避免额外产生 .LICENSE.txt
222+
terserOptions: {
223+
format: {
224+
// 输出只用 ASCII,非 ASCII 变成 \uXXXX
225+
ascii_only: true,
226+
},
227+
},
228+
}),
229+
],
230+
},
231+
232+
// 移除插件
233+
plugins: [],
234+
} satisfies Configuration;
235+
236+
const configInjectScript: Configuration = {
237+
...configCommon,
238+
239+
optimization: {
240+
minimize: true,
241+
splitChunks: false,
242+
runtimeChunk: false,
243+
minimizer: [
244+
new TerserPlugin({
245+
extractComments: false, // 避免额外产生 .LICENSE.txt
246+
terserOptions: {
247+
format: {
248+
// 输出只用 ASCII,非 ASCII 变成 \uXXXX
249+
ascii_only: true,
250+
},
251+
},
252+
}),
253+
],
254+
},
255+
256+
// 移除插件
257+
plugins: configCommon.plugins!.filter(
258+
(plugin) =>
259+
!(
260+
plugin instanceof HtmlWebpackPlugin ||
261+
plugin instanceof CopyPlugin ||
262+
plugin instanceof CleanWebpackPlugin
263+
)
264+
),
265+
266+
} satisfies Configuration;
267+
268+
269+
270+
export { configCommon, configWebWorker, configInjectScript };

webpack/webpack.content.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
/* eslint-disable import/no-extraneous-dependencies */
2+
import path from "path";
3+
import merge from "webpack-merge";
4+
import { configInjectScript as common } from "../webpack.config";
5+
6+
const src = path.resolve(__dirname, "../src");
7+
const dist = path.resolve(__dirname, "../dist");
8+
9+
// 不要分割的文件
10+
common.entry = {
11+
content: path.join(src, "content.ts"),
12+
};
13+
14+
common.output = {
15+
path: path.join(dist, "ext/src"),
16+
filename: "[name].js",
17+
clean: false,
18+
};
19+
20+
export default merge(common, {});

webpack/webpack.dev.ts

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,8 @@
22
import path from "path";
33
import NodePolyfillPlugin from "node-polyfill-webpack-plugin";
44
import merge from "webpack-merge";
5-
import CompressionPlugin from "compression-webpack-plugin";
65
import CopyPlugin from "copy-webpack-plugin";
7-
import TerserPlugin from "terser-webpack-plugin";
8-
import common from "../webpack.config";
6+
import { configCommon as common } from "../webpack.config";
97

108
const src = path.resolve(__dirname, "../src");
119
const dist = path.resolve(__dirname, "../dist");
@@ -54,12 +52,6 @@ export default merge(common, {
5452
},
5553
],
5654
}),
57-
// firefox商店文件不能大于4M, 所以需要压缩
58-
new CompressionPlugin({
59-
test: /ts.worker.js$/,
60-
filename: () => "ts.worker.js",
61-
deleteOriginalAssets: true,
62-
}),
6355
new NodePolyfillPlugin(),
6456
],
6557
resolve: {

webpack/webpack.i18n.ts

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,10 @@
22
import path from "path";
33
import NodePolyfillPlugin from "node-polyfill-webpack-plugin";
44
import merge from "webpack-merge";
5-
import CompressionPlugin from "compression-webpack-plugin";
65
import CopyPlugin from "copy-webpack-plugin";
76
import HtmlWebpackPlugin from "html-webpack-plugin";
87
import TerserPlugin from "terser-webpack-plugin";
9-
import common from "../webpack.config";
8+
import { configCommon as common } from "../webpack.config";
109

1110
const src = path.resolve(__dirname, "../src");
1211
const dist = path.resolve(__dirname, "../dist");
@@ -73,11 +72,6 @@ export default merge(common, {
7372
},
7473
],
7574
}),
76-
// firefox商店文件不能大于4M, 所以需要压缩
77-
new CompressionPlugin({
78-
test: /ts.worker.js/,
79-
deleteOriginalAssets: true,
80-
}),
8175
new NodePolyfillPlugin(),
8276
],
8377
resolve: {

0 commit comments

Comments
 (0)