Pop-up dictionary browser extension for language learning. Successor to Yomichan. (PERSONAL FORK)
1/*
2 * Copyright (C) 2023-2025 Yomitan Authors
3 * Copyright (C) 2020-2022 Yomichan Authors
4 *
5 * This program is free software: you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation, either version 3 of the License, or
8 * (at your option) any later version.
9 *
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
14 *
15 * You should have received a copy of the GNU General Public License
16 * along with this program. If not, see <https://www.gnu.org/licenses/>.
17 */
18
19import assert from 'assert';
20import childProcess from 'child_process';
21import fs from 'fs';
22import JSZip from 'jszip';
23import {fileURLToPath} from 'node:url';
24import path from 'path';
25import readline from 'readline';
26import {parseArgs} from 'util';
27import {buildLibs} from '../build-libs.js';
28import {ManifestUtil} from '../manifest-util.js';
29import {getAllFiles} from '../util.js';
30
31const dirname = path.dirname(fileURLToPath(import.meta.url));
32
33/**
34 * @param {string} directory
35 * @param {string[]} excludeFiles
36 * @param {string} outputFileName
37 * @param {string[]} sevenZipExes
38 * @param {?import('jszip').OnUpdateCallback} onUpdate
39 * @param {boolean} dryRun
40 */
41async function createZip(directory, excludeFiles, outputFileName, sevenZipExes, onUpdate, dryRun) {
42 try {
43 fs.unlinkSync(outputFileName);
44 } catch (e) {
45 // NOP
46 }
47
48 if (!dryRun) {
49 for (const exe of sevenZipExes) {
50 try {
51 const excludeArguments = excludeFiles.map((excludeFilePath) => `-x!${excludeFilePath}`);
52 childProcess.execFileSync(
53 exe,
54 [
55 'a',
56 outputFileName,
57 '.',
58 ...excludeArguments,
59 ],
60 {
61 cwd: directory,
62 },
63 );
64 return;
65 } catch (e) {
66 // NOP
67 }
68 }
69 }
70 await createJSZip(directory, excludeFiles, outputFileName, onUpdate, dryRun);
71}
72
73/**
74 * @param {string} directory
75 * @param {string[]} excludeFiles
76 * @param {string} outputFileName
77 * @param {?import('jszip').OnUpdateCallback} onUpdate
78 * @param {boolean} dryRun
79 */
80async function createJSZip(directory, excludeFiles, outputFileName, onUpdate, dryRun) {
81 const files = getAllFiles(directory);
82 removeItemsFromArray(files, excludeFiles);
83 const zip = new JSZip();
84 for (const fileName of files) {
85 zip.file(
86 fileName.replace(/\\/g, '/'),
87 fs.readFileSync(path.join(directory, fileName), {encoding: null, flag: 'r'}),
88 {},
89 );
90 }
91
92 if (typeof onUpdate !== 'function') {
93 onUpdate = () => {}; // NOP
94 }
95
96 const data = await zip.generateAsync({
97 type: 'nodebuffer',
98 compression: 'DEFLATE',
99 compressionOptions: {level: 9},
100 }, onUpdate);
101 process.stdout.write('\n');
102
103 if (!dryRun) {
104 fs.writeFileSync(outputFileName, data, {encoding: null, flag: 'w'});
105 }
106}
107
108/**
109 * @param {string[]} array
110 * @param {string[]} removeItems
111 */
112function removeItemsFromArray(array, removeItems) {
113 for (const item of removeItems) {
114 const index = getIndexOfFilePath(array, item);
115 if (index >= 0) {
116 array.splice(index, 1);
117 }
118 }
119}
120
121/**
122 * @param {string[]} array
123 * @param {string} item
124 * @returns {number}
125 */
126function getIndexOfFilePath(array, item) {
127 const pattern = /\\/g;
128 const separator = '/';
129 item = item.replace(pattern, separator);
130 for (let i = 0, ii = array.length; i < ii; ++i) {
131 if (array[i].replace(pattern, separator) === item) {
132 return i;
133 }
134 }
135 return -1;
136}
137
138/**
139 * @param {string} buildDir
140 * @param {string} extDir
141 * @param {ManifestUtil} manifestUtil
142 * @param {string[]} variantNames
143 * @param {string} manifestPath
144 * @param {boolean} dryRun
145 * @param {boolean} dryRunBuildZip
146 * @param {string} yomitanVersion
147 */
148async function build(buildDir, extDir, manifestUtil, variantNames, manifestPath, dryRun, dryRunBuildZip, yomitanVersion) {
149 const sevenZipExes = ['7za', '7z'];
150
151 // Create build directory
152 if (!fs.existsSync(buildDir) && !dryRun) {
153 fs.mkdirSync(buildDir, {recursive: true});
154 }
155
156 const dontLogOnUpdate = !process.stdout.isTTY;
157 /** @type {import('jszip').OnUpdateCallback} */
158 const onUpdate = (metadata) => {
159 if (dontLogOnUpdate) { return; }
160
161 let message = `Progress: ${metadata.percent.toFixed(2)}%`;
162 if (metadata.currentFile) {
163 message += ` (${metadata.currentFile})`;
164 }
165
166 readline.clearLine(process.stdout, 0);
167 readline.cursorTo(process.stdout, 0);
168 process.stdout.write(message);
169 };
170
171 process.stdout.write(`Version: ${yomitanVersion}...\n`);
172
173 for (const variantName of variantNames) {
174 const variant = manifestUtil.getVariant(variantName);
175 if (typeof variant === 'undefined' || variant.buildable === false) { continue; }
176
177 const {name, fileName, fileCopies} = variant;
178 let {excludeFiles} = variant;
179 if (!Array.isArray(excludeFiles)) { excludeFiles = []; }
180
181 process.stdout.write(`Building ${name}...\n`);
182
183 const modifiedManifest = manifestUtil.getManifest(variant.name);
184
185 ensureFilesExist(extDir, excludeFiles);
186
187 if (typeof fileName === 'string') {
188 const fileNameSafe = path.basename(fileName);
189 const fullFileName = path.join(buildDir, fileNameSafe);
190 if (!dryRun) {
191 fs.writeFileSync(manifestPath, ManifestUtil.createManifestString(modifiedManifest).replace('$YOMITAN_VERSION', yomitanVersion));
192 }
193
194 if (fileName.endsWith('.zip')) {
195 if (!dryRun || dryRunBuildZip) {
196 await createZip(extDir, excludeFiles, fullFileName, sevenZipExes, onUpdate, dryRun);
197 }
198
199 if (!dryRun && Array.isArray(fileCopies)) {
200 for (const fileName2 of fileCopies) {
201 const fileName2Safe = path.basename(fileName2);
202 fs.copyFileSync(fullFileName, path.join(buildDir, fileName2Safe));
203 }
204 }
205 } else {
206 if (!dryRun) {
207 fs.cpSync(extDir, fullFileName, {recursive: true});
208 }
209 }
210 }
211
212 process.stdout.write('\n');
213 }
214}
215
216/**
217 * @param {string} directory
218 * @param {string[]} files
219 */
220function ensureFilesExist(directory, files) {
221 for (const file of files) {
222 assert.ok(fs.existsSync(path.join(directory, file)));
223 }
224}
225
226/** */
227export async function main() {
228 /** @type {import('util').ParseArgsConfig['options']} */
229 const parseArgsConfigOptions = {
230 all: {
231 type: 'boolean',
232 default: false,
233 },
234 default: {
235 type: 'boolean',
236 default: false,
237 },
238 manifest: {
239 type: 'string',
240 },
241 dryRun: {
242 type: 'boolean',
243 default: false,
244 },
245 dryRunBuildZip: {
246 type: 'boolean',
247 default: false,
248 },
249 version: {
250 type: 'string',
251 default: '0.0.0.0',
252 },
253 target: {
254 type: 'string',
255 },
256 };
257
258 const argv = process.argv.slice(2);
259 const {values: args, positionals: targets} = parseArgs({args: argv, options: parseArgsConfigOptions, allowPositionals: true});
260
261 const dryRun = /** @type {boolean} */ (args.dryRun);
262 const dryRunBuildZip = /** @type {boolean} */ (args.dryRunBuildZip);
263 const yomitanVersion = /** @type {string} */ (args.version);
264
265 const manifestUtil = new ManifestUtil();
266
267 const rootDir = path.join(dirname, '..', '..');
268 const extDir = path.join(rootDir, 'ext');
269 const buildDir = path.join(rootDir, 'builds');
270 const manifestPath = path.join(extDir, 'manifest.json');
271
272 try {
273 await buildLibs();
274 const variantNames = /** @type {string[]} */ ((
275 args.target ?
276 [args.target] :
277 (argv.length === 0 || args.all ?
278 manifestUtil.getVariants().filter(({buildable}) => buildable !== false).map(({name}) => name) :
279 targets)
280 ));
281 await build(buildDir, extDir, manifestUtil, variantNames, manifestPath, dryRun, dryRunBuildZip, yomitanVersion);
282 } finally {
283 // Restore manifest
284 const manifestName = /** @type {?string} */ ((!args.default && typeof args.manifest !== 'undefined') ? args.manifest : null);
285 const restoreManifest = manifestUtil.getManifest(manifestName);
286 process.stdout.write('Restoring manifest...\n');
287 if (!dryRun) {
288 fs.writeFileSync(manifestPath, ManifestUtil.createManifestString(restoreManifest).replace('$YOMITAN_VERSION', yomitanVersion));
289 }
290 }
291}
292
293await main();