// Where a JS project's native plugins (Capacitor, Cordova, React Native) live, also in monorepos: Node's own lookup // (node_modules next to the app's package.json and in every folder above it, up to the repo root), workspace package // folders (package.json "workspaces", npm / yarn / pnpm-style array or { packages }, as Bun reads them) and local // SwiftPM packages (`.package(path:)` in a Package.swift, e.g. Capacitor 8's CapApp-SPM). Used by `privacy-manifest` // and `scan`. Read-only; never throws. import { type Dirent, existsSync, readdirSync, readFileSync, realpathSync, statSync } from 'node:fs'; import { dirname, join, resolve } from 'node:path'; import { skipDir } from './walk-skip'; interface PkgJson { name?: string; dependencies?: Record; workspaces?: string[] | { packages?: string[] } } const readJson = (p: string): PkgJson | null => { try { return JSON.parse(readFileSync(p, 'utf8')) as PkgJson; } catch { return null; } }; const isDir = (p: string): boolean => { try { return statSync(p).isDirectory(); } catch { return false; } }; const real = (p: string): string => { try { return realpathSync(p); } catch { return resolve(p); } }; /** The repo root at or above `dir`: the nearest folder with .git, else the top-most package.json with workspaces, * else `dir` itself. */ export function repoRoot(dir: string): string { let ws: string | null = null; for (let d = resolve(dir); ; d = dirname(d)) { if (existsSync(join(d, '.git'))) return d; if (readJson(join(d, 'package.json'))?.workspaces) ws = d; if (dirname(d) === d) return ws ?? resolve(dir); } } /** Workspace package folders of a repo root ("apps/*", "packages/**" one level deep, or literal folders). */ export function workspaceDirs(root: string): string[] { const w = readJson(join(root, 'package.json'))?.workspaces; const pats = Array.isArray(w) ? w : Array.isArray(w?.packages) ? w.packages : []; const out: string[] = []; for (const p of pats) { if (typeof p !== 'string' || p.startsWith('!')) continue; const m = /^(.*?)\/\*\*?$/.exec(p); if (m) { const base = join(root, m[1]!); let names: string[] = []; try { names = readdirSync(base).sort(); } catch { /* missing */ } for (const n of names) if (existsSync(join(base, n, 'package.json'))) out.push(join(base, n)); } else if (!p.includes('*') && existsSync(join(root, p, 'package.json'))) out.push(join(root, p)); } return out; } /** Folder of package `name` as Node resolves it from `fromDir` (node_modules there and in every parent up to `root`), * else the workspace package of that name. */ export function resolvePackage(name: string, fromDir: string, root: string): string | null { const top = resolve(root); for (let d = resolve(fromDir); ; d = dirname(d)) { const p = join(d, 'node_modules', name); if (existsSync(join(p, 'package.json'))) return p; if (d === top || dirname(d) === d) break; } for (const w of workspaceDirs(top)) if (readJson(join(w, 'package.json'))?.name === name) return w; return null; } /** The package has native iOS code: a podspec or Package.swift at its root, or an ios/ folder. */ export function hasNativeIos(dir: string): boolean { let names: string[] = []; try { names = readdirSync(dir); } catch { return false; } return names.some(n => n.endsWith('.podspec') || n === 'Package.swift') || isDir(join(dir, 'ios')); } /** Local SwiftPM packages (`.package(path: "...")` / `.package(name: "X", path: "...")`) of every Package.swift under * `dir` (a few levels, skipping build output and checkouts). */ export function localSwiftPackages(dir: string): string[] { const out: string[] = []; const skip = new Set(['Pods', 'build', 'DerivedData', 'node_modules', '.build', 'SourcePackages', '.git', 'xcuserdata']); const walk = (d: string, depth: number) => { if (depth > 4) return; let ents: Dirent[] = []; try { ents = readdirSync(d, { withFileTypes: true }); } catch { return; } for (const e of ents) { const n = e.name; if (skip.has(n)) continue; const p = join(d, n); if (n === 'Package.swift') { let t = ''; try { t = readFileSync(p, 'utf8'); } catch { continue; } for (const m of t.matchAll(/\.package\(\s*(?:name:\s*"[^"]*"\s*,\s*)?path:\s*"([^"]+)"/g)) { const target = resolve(d, m[1]!); if (isDir(target)) out.push(target); } } else if ((e.isDirectory() || (e.isSymbolicLink() && isDir(p))) && !/\.(xc)?framework$|\.xcassets$/.test(n) && !skipDir(dir, p, n)) walk(p, depth + 1); } }; walk(dir, 0); return out; } export interface NativePlugin { name: string; dir: string } /** * The native plugin packages an iOS project at `iosDir` builds: the dependencies of the app's package.json (the * nearest one at or above `iosDir`, i.e. next to capacitor.config) resolved through node_modules and workspaces, * plus the local SwiftPM packages its Package.swift files reference. Without a package.json: every package with * native iOS code in the node_modules folders from `iosDir` up to the repo root. Deduplicated by real path. */ export function nativeIosPlugins(iosDir: string): NativePlugin[] { const start = resolve(iosDir); const root = repoRoot(start); const out: NativePlugin[] = []; const seen = new Set(); const add = (dir: string, name?: string) => { const r = real(dir); if (seen.has(r) || !hasNativeIos(dir)) return; seen.add(r); out.push({ name: name ?? readJson(join(dir, 'package.json'))?.name ?? dir.split(/[\\/]/).pop()!, dir }); }; let appDir: string | null = null; for (let d = start; ; d = dirname(d)) { if (existsSync(join(d, 'package.json'))) { appDir = d; break; } if (d === root || dirname(d) === d) break; } if (appDir) { for (const name of Object.keys(readJson(join(appDir, 'package.json'))?.dependencies ?? {})) { const p = resolvePackage(name, appDir, root); if (p) add(p, name); } } else { for (let d = start; ; d = dirname(d)) { const nm = join(d, 'node_modules'); let names: string[] = []; try { names = readdirSync(nm).filter(n => !n.startsWith('.')).sort(); } catch { /* none */ } for (const n of names) { if (n.startsWith('@')) { try { for (const k of readdirSync(join(nm, n)).sort()) add(join(nm, n, k), `${n}/${k}`); } catch { /* unreadable */ } } else add(join(nm, n), n); } if (d === root || dirname(d) === d) break; } } for (const p of localSwiftPackages(start)) add(p); return out; }