All files / packages/icons/.claude/skills/icons-release/scripts/lib assets.js

0% Statements 0/49
0% Branches 0/26
0% Functions 0/6
0% Lines 0/44

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 }