-
Notifications
You must be signed in to change notification settings - Fork 59
Expand file tree
/
Copy pathbuild.js
More file actions
205 lines (180 loc) · 6.86 KB
/
build.js
File metadata and controls
205 lines (180 loc) · 6.86 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
const fs = require("fs");
const path = require("path");
const zlib = require("zlib");
const https = require("https");
// CRC-32 lookup table
const crcTable = new Uint32Array(256);
for (let i = 0; i < 256; i++) {
let c = i;
for (let j = 0; j < 8; j++) c = (c & 1) ? (0xEDB88320 ^ (c >>> 1)) : (c >>> 1);
crcTable[i] = c;
}
function crc32(buf) {
let crc = 0xFFFFFFFF;
for (const byte of buf) crc = crcTable[(crc ^ byte) & 0xFF] ^ (crc >>> 8);
return (crc ^ 0xFFFFFFFF) >>> 0;
}
/**
* Zip files/folders into destFile.
* @param {string|string[]} sources - Paths to zip
* @param {string} destFile - Output zip file
* @param {string[]} [exclude=[]] - Optional array of folder/file paths to exclude (relative paths)
*/
function zip(sources, destFile, exclude = []) {
const files = [];
// Ensure parent directory exists
const parentDir = path.dirname(destFile);
if (!fs.existsSync(parentDir)) {
fs.mkdirSync(parentDir, { recursive: true });
}
function collect(full, rel) {
// skip if rel matches any exclude pattern
if (exclude.some(e => rel === e || rel.startsWith(e + "/"))) return;
const stat = fs.statSync(full);
if (stat.isDirectory()) {
for (const name of fs.readdirSync(full)) {
collect(path.join(full, name), rel + "/" + name);
}
} else {
files.push({ full, rel });
}
}
if (typeof sources === "string") {
for (const name of fs.readdirSync(sources)) collect(path.join(sources, name), name);
} else {
for (const src of sources) collect(src, src);
}
const parts = [];
const centralDir = [];
let offset = 0;
for (const { full, rel } of files) {
const data = fs.readFileSync(full);
const compressed = zlib.deflateRawSync(data);
const useDeflate = compressed.length < data.length;
const fileData = useDeflate ? compressed : data;
const method = useDeflate ? 8 : 0;
const crc = crc32(data);
const nameBytes = Buffer.from(rel, "utf8");
const local = Buffer.alloc(30 + nameBytes.length);
local.writeUInt32LE(0x04034b50, 0);
local.writeUInt16LE(20, 4);
local.writeUInt16LE(0, 6);
local.writeUInt16LE(method, 8);
local.writeUInt16LE(0, 10);
local.writeUInt16LE(0, 12);
local.writeUInt32LE(crc, 14);
local.writeUInt32LE(fileData.length, 18);
local.writeUInt32LE(data.length, 22);
local.writeUInt16LE(nameBytes.length, 26);
local.writeUInt16LE(0, 28);
nameBytes.copy(local, 30);
const cd = Buffer.alloc(46 + nameBytes.length);
cd.writeUInt32LE(0x02014b50, 0);
cd.writeUInt16LE(20, 4);
cd.writeUInt16LE(20, 6);
cd.writeUInt16LE(0, 8);
cd.writeUInt16LE(method, 10);
cd.writeUInt16LE(0, 12);
cd.writeUInt16LE(0, 14);
cd.writeUInt32LE(crc, 16);
cd.writeUInt32LE(fileData.length, 20);
cd.writeUInt32LE(data.length, 24);
cd.writeUInt16LE(nameBytes.length, 28);
cd.writeUInt16LE(0, 30);
cd.writeUInt16LE(0, 32);
cd.writeUInt16LE(0, 34);
cd.writeUInt16LE(0, 36);
cd.writeUInt32LE(0, 38);
cd.writeUInt32LE(offset, 42);
nameBytes.copy(cd, 46);
parts.push(local, fileData);
centralDir.push(cd);
offset += local.length + fileData.length;
}
const cdBuf = Buffer.concat(centralDir);
const eocd = Buffer.alloc(22);
eocd.writeUInt32LE(0x06054b50, 0);
eocd.writeUInt16LE(0, 4);
eocd.writeUInt16LE(0, 6);
eocd.writeUInt16LE(files.length, 8);
eocd.writeUInt16LE(files.length, 10);
eocd.writeUInt32LE(cdBuf.length, 12);
eocd.writeUInt32LE(offset, 16);
eocd.writeUInt16LE(0, 20);
fs.writeFileSync(destFile, Buffer.concat([...parts, cdBuf, eocd]));
}
function rm(dir) {
if (!fs.existsSync(dir)) return;
fs.rmSync(dir, { recursive: true, force: true });
}
function get(url) {
return new Promise((resolve, reject) => {
https.get(url, { headers: { "User-Agent": "build-script" } }, res => {
if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
resolve(get(res.headers.location));
return;
}
if (res.statusCode !== 200) {
reject(new Error(`HTTP ${res.statusCode} for ${url}`));
return;
}
const chunks = [];
res.on("data", chunk => chunks.push(chunk));
res.on("end", () => resolve(Buffer.concat(chunks)));
res.on("error", reject);
}).on("error", reject);
});
}
async function fetchJSON(url) {
const buf = await get(url);
return JSON.parse(buf.toString("utf8"));
}
async function main() {
const { version } = JSON.parse(fs.readFileSync("package.json", "utf8"));
const manifest = JSON.parse(fs.readFileSync("src/manifest.json", "utf8"));
manifest.version = version;
fs.writeFileSync("src/manifest.json", JSON.stringify(manifest, null, 2) + "\n");
console.log(`Set manifest version to ${version}`);
console.log("Cleaning output directory ...");
rm("dist");
console.log("Fetching latest vfs-client from GitHub ...");
const repoPath = "modules/vfs-toolkit/vfs-client";
const commits = await fetchJSON(
`https://api.github.com/repos/thunderbird/webext-support/commits?path=${repoPath}&per_page=1`
);
const sha = commits[0].sha;
console.log(` Latest commit: ${sha}`);
// Use the Git tree API to list all files in the folder at this commit
const treeUrl = `https://api.github.com/repos/thunderbird/webext-support/git/trees/${sha}?recursive=1`;
const tree = await fetchJSON(treeUrl);
const prefix = repoPath + "/";
const clientFiles = tree.tree.filter(f => f.type === "blob" && f.path.startsWith(prefix));
console.log(` Found ${clientFiles.length} files, fetching ...`);
rm("src/vendor/vfs-client");
for (const file of clientFiles) {
const relativePath = file.path.slice(prefix.length);
const rawUrl = `https://github.com/thunderbird/webext-support/raw/${sha}/${file.path}`;
console.log(` Fetching ${relativePath} ...`);
const buf = await get(rawUrl);
const dest = path.join("src/vendor/vfs-client", relativePath);
fs.mkdirSync(path.dirname(dest), { recursive: true });
fs.writeFileSync(dest, buf);
}
// Update VENDOR.md — replace the vfs-clientkit upstream URL with the new commit hash
const vendorMdPath = "src/VENDOR.md";
let vendorMd = fs.readFileSync(vendorMdPath, "utf8");
const newTreeUrl = `https://github.com/thunderbird/webext-support/tree/${sha}/${repoPath}`;
vendorMd = vendorMd.replace(
/^(## vfs-client\n[\s\S]*?- \*\*Upstream\*\* : )https:\/\/github\.com\/thunderbird\/webext-support\/tree\/[0-9a-f]+\/modules\/vfs-toolkit\/vfs-client$/m,
`$1${newTreeUrl}`
);
fs.writeFileSync(vendorMdPath, vendorMd);
console.log(` Updated VENDOR.md with commit ${sha}`);
const xpiVersion = version.replace(/\./g, "_");
const xpiName = `quicktext_${xpiVersion}.xpi`;
console.log(`Creating extension file (dist/${xpiName}) ...`);
zip("src", `dist/${xpiName}`);
console.log("Build finished. Output is in the 'dist' folder.");
https.globalAgent.destroy();
}
main().catch(e => { console.error(e); process.exit(1); });