Pop-up dictionary browser extension for language learning. Successor to Yomichan. (PERSONAL FORK)
at lambda-fork/main 293 lines 9.3 kB view raw
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();