Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 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 | 'use strict' const fs = require('fs') const path = require('path') const { spawnSync } = require('child_process') const { typeFromFolderName } = require('./types') /** * Extract a .zip into destDir. Deterministic and fail-loud. * * Prefers macOS `ditto` because Info-ZIP `unzip` cannot create entry names * containing emoji/unicode (fails with "Illegal byte sequence"). Falls back to * `unzip` under a UTF-8 locale. Args are passed without a shell so spaces in * the path are safe. */ function extractZip(zipPath, destDir) { fs.mkdirSync(destDir, { recursive: true }) let result = spawnSync('ditto', ['-x', '-k', zipPath, destDir], { encoding: 'utf8' }) if (result.error) { // ditto unavailable (non-macOS) — try unzip with a UTF-8 locale result = spawnSync('unzip', ['-o', '-q', zipPath, '-d', destDir], { encoding: 'utf8', env: { ...process.env, LANG: 'en_US.UTF-8', LC_ALL: 'en_US.UTF-8' } }) } if (result.error) { const err = new Error(`Failed to extract zip: ${result.error.message}`) err.code = 'UNZIP_FAILED' throw err } if (result.status !== 0) { const err = new Error(`zip extraction exited with code ${result.status}: ${(result.stderr || '').trim()}`) err.code = 'UNZIP_FAILED' throw err } return destDir } /** * Resolve --assets (folder or .zip) to a directory to scan. A zip is * extracted into downloadDir; a folder is returned as-is. */ function resolveAssetsDir(assetsInput, downloadDir) { const stat = fs.statSync(assetsInput) if (stat.isDirectory()) return assetsInput if (assetsInput.toLowerCase().endsWith('.zip')) { return extractZip(assetsInput, downloadDir) } const err = new Error(`--assets must be a folder or a .zip (got: ${assetsInput})`) err.code = 'UNSUPPORTED_ASSETS' throw err } /** * Recursively scan the assets root and map each repo type to its source * folder. Folders whose names don't match a known type keyword are ignored. * Recursion handles nested exports like * "<top>/Exported Icons/Exported Standard Icons/". * * Returns e.g. { standard: '/abs/Exported Standard Icons', utility: '...' }. */ function indexAssetFolders(assetsRoot) { if (!fs.existsSync(assetsRoot)) { const err = new Error(`Assets path does not exist: ${assetsRoot}`) err.code = 'ASSETS_NOT_FOUND' throw err } const map = {} const walk = dir => { for (const entry of fs.readdirSync(dir, { withFileTypes: true })) { if (!entry.isDirectory()) continue const full = path.join(dir, entry.name) const type = typeFromFolderName(entry.name) if (type && !map[type]) map[type] = full walk(full) } } walk(assetsRoot) return map } /** * Case-insensitive lookup of `<iconName>.svg` inside a folder. * Returns the actual source filename (preserving its real casing) or null. */ function findSvgFile(folderPath, iconName) { if (!folderPath || !fs.existsSync(folderPath)) return null const wanted = `${iconName}.svg`.toLowerCase() const files = fs.readdirSync(folderPath) const match = files.find(file => file.toLowerCase() === wanted) return match || null } module.exports = { indexAssetFolders, findSvgFile, extractZip, resolveAssetsDir } |