Ctrl K
ComunidadGuías…

scripts/check-cap9-deprecated.mjs

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
#!/usr/bin/env node
/**
 * Capacitor 9 deprecated native API guard.
 *
 * Fails when plugin native sources still use APIs removed in Capacitor 9.
 * Does not flag Cordova SwiftPM product dependencies (still required on Cap 8).
 *
 * Usage:
 *   node scripts/check-cap9-deprecated.mjs packages/capacitor-notifications
 */

import fs from 'node:fs'
import path from 'node:path'
import { fileURLToPath } from 'node:url'

const REPO_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..')

const PLUGIN_REL_PATTERN = /^packages\/[\w-]+(?:\/[\w-]+)*$/

const SKIP_DIRS = new Set([
  'node_modules',
  'dist',
  'build',
  '.build',
  '.gradle',
  'Pods',
  'DerivedData',
  '.swiftpm',
  '.git',
  'example-app',
])

/** @type {{ id: string, pattern: RegExp, exts: string[], ignoreLine?: RegExp }[]} */
const RULES = [
  {
    id: 'hasOption',
    pattern: /\bhasOption\s*\(/,
    exts: ['.java', '.kt', '.swift'],
  },
  {
    id: 'getConfigValue',
    pattern: /\bgetConfigValue\s*\(/,
    exts: ['.java', '.kt', '.swift'],
  },
  {
    id: '@NativePlugin',
    pattern: /@NativePlugin\b/,
    exts: ['.java', '.kt'],
  },
  {
    id: 'saveCall',
    pattern: /\bsaveCall\s*\(/,
    exts: ['.java', '.kt', '.swift'],
  },
  {
    id: 'getSavedCall',
    pattern: /\bgetSavedCall\s*\(/,
    exts: ['.java', '.kt', '.swift'],
  },
  {
    id: 'freeSavedCall',
    pattern: /\bfreeSavedCall\s*\(/,
    exts: ['.java', '.kt', '.swift'],
  },
  {
    id: 'releaseCall',
    pattern: /\breleaseCall\s*\(/,
    exts: ['.java', '.kt', '.swift'],
  },
  {
    id: 'pluginRequestPermission',
    pattern: /\bpluginRequestPermissions?\s*\(/,
    exts: ['.java', '.kt'],
  },
  {
    id: 'pluginRequestAllPermissions',
    pattern: /\bpluginRequestAllPermissions\s*\(/,
    exts: ['.java', '.kt'],
  },
  {
    id: 'hasDefinedPermissions',
    pattern: /\bhasDefinedPermissions\s*\(/,
    exts: ['.java', '.kt'],
  },
  {
    id: 'CAPBridge',
    pattern: /\bCAPBridge\./,
    exts: ['.swift'],
    ignoreLine: /CAPBridgedPlugin/,
  },
  {
    id: 'CAPNotifications',
    pattern: /\bCAPNotifications\b/,
    exts: ['.swift'],
  },
]

const CORDova_SPM_LINE
  = /\.product\s*\(\s*name\s*:\s*"Cordova"\s*,\s*package\s*:\s*"capacitor-swift-pm"\s*\)/

/** @type {string} */
let pluginRoot = ''

/** Returns whether `targetPath` resolves inside `rootDir`. */
function isUnderRoot(targetPath, rootDir) {
  const resolved = path.resolve(targetPath)
  const rel = path.relative(rootDir, resolved)
  return rel === '' || (!rel.startsWith('..') && !path.isAbsolute(rel))
}

/**
 * Resolve plugin directory from a repo-relative path (no cwd / free-form CLI paths).
 * @param {string | undefined} pluginRel
 * @returns {string | null}
 */
function resolvePluginDir(pluginRel) {
  if (!pluginRel) {
    console.error('[cap9-deprecated] ERROR: missing plugin path (expected packages/<name>)')
    return null
  }
  const normalized = pluginRel.replaceAll('\\', '/')
  if (!PLUGIN_REL_PATTERN.test(normalized)) {
    console.error('[cap9-deprecated] ERROR: plugin path must match packages/<name>')
    return null
  }
  const abs = path.join(REPO_ROOT, normalized)
  if (!isUnderRoot(abs, REPO_ROOT)) {
    console.error('[cap9-deprecated] ERROR: plugin dir must stay inside repo')
    return null
  }
  return abs
}

/** @param {string} p */
function readText(p) {
  const resolved = path.resolve(p)
  if (!isUnderRoot(resolved, pluginRoot) || !fs.existsSync(resolved)) {
    return ''
  }
  return fs.readFileSync(resolved, 'utf8')
}

/** @param {string} p */
function exists(p) {
  const resolved = path.resolve(p)
  return isUnderRoot(resolved, pluginRoot) && fs.existsSync(resolved)
}

/** @param {string} rootDir @param {string[]} exts */
function walkFiles(rootDir, exts) {
  const out = []
  const stack = [rootDir]
  while (stack.length) {
    const dir = stack.pop()
    if (!dir || !isUnderRoot(dir, pluginRoot)) {
      continue
    }
    let entries
    try {
      entries = fs.readdirSync(dir, { withFileTypes: true })
    }
    catch (err) {
      const message = err instanceof Error ? err.message : String(err)
      console.warn(`[cap9-deprecated] skip unreadable directory ${dir}: ${message}`)
      continue
    }
    for (const e of entries) {
      if (e.isDirectory()) {
        if (SKIP_DIRS.has(e.name))
          continue
        stack.push(path.join(dir, e.name))
        continue
      }
      if (!e.isFile())
        continue
      for (const ext of exts) {
        if (e.name.endsWith(ext)) {
          out.push(path.join(dir, e.name))
          break
        }
      }
    }
  }
  out.sort()
  return out
}

/** @param {string} pluginDir @param {Record<string, unknown>} pkg */
function collectScanRoots(pluginDir, pkg) {
  const cap = typeof pkg.capacitor === 'object' && pkg.capacitor ? pkg.capacitor : {}
  const roots = []
  if (cap.android) {
    const androidMain = path.join(pluginDir, 'android', 'src', 'main')
    if (exists(androidMain))
      roots.push(androidMain)
  }
  if (cap.ios) {
    const iosSources = path.join(pluginDir, 'ios', 'Sources')
    if (exists(iosSources))
      roots.push(iosSources)
    else {
      const iosDir = path.join(pluginDir, 'ios')
      if (exists(iosDir))
        roots.push(iosDir)
    }
  }
  const packageSwift = path.join(pluginDir, 'Package.swift')
  if (exists(packageSwift))
    roots.push(packageSwift)
  return roots
}

/** @param {string} filePath @param {{ exts: string[], ignoreLine?: RegExp, pattern: RegExp }} rule */
function scanFile(filePath, rule) {
  const ext = path.extname(filePath)
  if (!rule.exts.includes(ext))
    return []

  const txt = readText(filePath)
  const lines = txt.split(/\r?\n/)
  const hits = []
  for (let i = 0; i < lines.length; i++) {
    const line = lines[i]
    if (filePath.endsWith('Package.swift') && CORDova_SPM_LINE.test(line)) {
      continue
    }
    if (rule.ignoreLine?.test(line))
      continue
    if (rule.pattern.test(line)) {
      hits.push({ line: i + 1, text: line.trim() })
    }
  }
  return hits
}

function run() {
  pluginRoot = resolvePluginDir(process.argv[2]) ?? ''
  if (!pluginRoot) {
    console.error('Usage: node scripts/check-cap9-deprecated.mjs packages/<plugin-name>')
    process.exitCode = 2
    return
  }

  const pkgPath = path.join(pluginRoot, 'package.json')
  if (!exists(pkgPath)) {
    console.error(`[cap9-deprecated] ERROR: missing package.json in ${pluginRoot}`)
    process.exitCode = 2
    return
  }

  let pkg
  try {
    pkg = JSON.parse(readText(pkgPath))
  }
  catch (err) {
    const message = err instanceof Error ? err.message : String(err)
    console.error(`[cap9-deprecated] ERROR: invalid package.json (${pkgPath}): ${message}`)
    process.exitCode = 2
    return
  }

  const cap = typeof pkg.capacitor === 'object' && pkg.capacitor ? pkg.capacitor : {}
  if (!cap.android && !cap.ios) {
    process.exitCode = 0
    return
  }

  const scanRoots = collectScanRoots(pluginRoot, pkg)
  const allExts = [...new Set(RULES.flatMap(r => r.exts))]
  const files = []
  for (const root of scanRoots) {
    if (root.endsWith('Package.swift')) {
      files.push(root)
      continue
    }
    files.push(...walkFiles(root, allExts))
  }

  const violations = []
  for (const file of files) {
    for (const rule of RULES) {
      const hits = scanFile(file, rule)
      for (const hit of hits) {
        violations.push({
          rule: rule.id,
          file: path.relative(pluginRoot, file),
          line: hit.line,
          text: hit.text,
        })
      }
    }
  }

  if (violations.length) {
    const relDir = path.relative(REPO_ROOT, pluginRoot) || '.'
    console.error(`[cap9-deprecated] FAIL in ${relDir}`)
    for (const v of violations) {
      console.error(`  - ${v.rule}: ${v.file}:${v.line}: ${v.text}`)
    }
    process.exitCode = 1
    return
  }

  process.exitCode = 0
}

run()