UNPKG

react-native-node-api

Version:
382 lines (381 loc) 16 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.DEFAULT_EXCLUDE_PACKAGES = exports.DEFAULT_EXCLUDE_PATTERNS = exports.MAGIC_FILENAME = exports.PATH_SUFFIX_CHOICES = exports.PLATFORM_EXTENSIONS = exports.PLATFORMS = void 0; exports.assertPathSuffix = assertPathSuffix; exports.isNodeApiModule = isNodeApiModule; exports.stripExtension = stripExtension; exports.determineModuleContext = determineModuleContext; exports.normalizeModulePath = normalizeModulePath; exports.escapePath = escapePath; exports.getLibraryName = getLibraryName; exports.prettyPath = prettyPath; exports.resolvePackageRoot = resolvePackageRoot; exports.logModulePaths = logModulePaths; exports.findPackageDependencyPaths = findPackageDependencyPaths; exports.hasPlatformExtension = hasPlatformExtension; exports.findNodeApiModulePaths = findNodeApiModulePaths; exports.findNodeApiModulePathsByDependency = findNodeApiModulePathsByDependency; exports.determineLibraryBasename = determineLibraryBasename; exports.getAutolinkPath = getAutolinkPath; exports.getLatestMtime = getLatestMtime; exports.findNodeAddonForBindings = findNodeAddonForBindings; const strict_1 = __importDefault(require("node:assert/strict")); const node_path_1 = __importDefault(require("node:path")); const node_fs_1 = __importDefault(require("node:fs")); const duplicates_1 = require("./duplicates"); const chalk_1 = __importDefault(require("chalk")); const pkg_dir_1 = require("pkg-dir"); const read_pkg_1 = require("read-pkg"); const node_module_1 = require("node:module"); // TODO: Change to .apple.node exports.PLATFORMS = ["android", "apple"]; exports.PLATFORM_EXTENSIONS = { android: ".android.node", apple: ".apple.node", }; exports.PATH_SUFFIX_CHOICES = ["strip", "keep", "omit"]; function assertPathSuffix(value) { (0, strict_1.default)(typeof value === "string", `Expected a string, got ${typeof value}`); (0, strict_1.default)(exports.PATH_SUFFIX_CHOICES.includes(value), `Expected one of ${exports.PATH_SUFFIX_CHOICES.join(", ")}`); } // Cache mapping package directory to package name across calls const packageNameCache = new Map(); /** * @param modulePath Batch-scans the path to the module to check (must be extensionless or end in .node) * @returns True if a platform specific prebuild exists for the module path, warns on unreadable modules. * @throws If the parent directory cannot be read, or if a detected module is unreadable. * TODO: Consider checking for a specific platform extension. */ function isNodeApiModule(modulePath) { { // HACK: Take a shortcut (if applicable): existing `.node` files are addons try { node_fs_1.default.accessSync(modulePath.endsWith(".node") ? modulePath : `${modulePath}.node`); return true; } catch { // intentionally left empty } } const dir = node_path_1.default.dirname(modulePath); const baseName = node_path_1.default.basename(modulePath, ".node"); let entries; try { entries = node_fs_1.default.readdirSync(dir); } catch { // Cannot read directory: treat as no module return false; } return Object.values(exports.PLATFORM_EXTENSIONS).some((extension) => { const fileName = baseName + extension; if (!entries.includes(fileName)) { return false; } const filePath = node_path_1.default.join(dir, fileName); try { // First, check if file exists (works the same on all platforms) node_fs_1.default.accessSync(filePath, node_fs_1.default.constants.F_OK); // Then check if it's readable (behavior differs by platform) if (!isReadableSync(filePath)) { throw new Error(`Found an unreadable module ${fileName}`); } } catch (err) { throw new Error(`Found an unreadable module ${fileName}`, { cause: err }); } return true; }); } /** * Check if a path is readable according to permission bits. * On Windows, tests store POSIX S_IWUSR bit in stats.mode. * On Unix-like, uses fs.accessSync for R_OK. */ function isReadableSync(p) { try { if (process.platform === "win32") { const stats = node_fs_1.default.statSync(p); return !!(stats.mode & node_fs_1.default.constants.S_IWUSR); } else { node_fs_1.default.accessSync(p, node_fs_1.default.constants.R_OK); return true; } } catch { return false; } } /** * Strip of any platform specific extensions from a module path. */ function stripExtension(modulePath) { return [...Object.values(exports.PLATFORM_EXTENSIONS), ".node"].reduce((modulePath, extension) => { if (modulePath.endsWith(extension)) { return modulePath.slice(0, -extension.length); } else { return modulePath; } }, modulePath); } /** * Traverse the filesystem upward to find a name for the package that which contains a file. */ function determineModuleContext(modulePath, originalPath = modulePath) { // Locate nearest package directory const pkgDir = (0, pkg_dir_1.packageDirectorySync)({ cwd: modulePath }); if (!pkgDir) { throw new Error("Could not find containing package"); } // Read and cache package name let pkgName = packageNameCache.get(pkgDir); if (!pkgName) { const pkg = (0, read_pkg_1.readPackageSync)({ cwd: pkgDir }); (0, strict_1.default)(typeof pkg.name === "string", "Expected package.json to have a name"); pkgName = pkg.name; packageNameCache.set(pkgDir, pkgName); } // Compute module-relative path const relPath = normalizeModulePath(node_path_1.default.relative(pkgDir, originalPath)); return { packageName: pkgName, relativePath: relPath }; } function normalizeModulePath(modulePath) { const dirname = node_path_1.default.normalize(node_path_1.default.dirname(modulePath)); const basename = node_path_1.default.basename(modulePath); const strippedBasename = stripExtension(basename).replace(/^lib/, ""); // Replace backslashes with forward slashes for cross-platform compatibility return node_path_1.default.join(dirname, strippedBasename).replaceAll("\\", "/"); } function escapePath(modulePath) { return modulePath.replace(/[^a-zA-Z0-9]/g, "-"); } /** * Get the name of the library which will be used when the module is linked in. */ function getLibraryName(modulePath, naming) { const { packageName, relativePath } = determineModuleContext(modulePath); const escapedPackageName = escapePath(packageName); return naming.pathSuffix === "omit" ? escapedPackageName : `${escapedPackageName}--${escapePath(naming.pathSuffix === "strip" ? node_path_1.default.basename(relativePath) : relativePath)}`; } function prettyPath(p) { return chalk_1.default.dim(node_path_1.default.relative(process.cwd(), p) || chalk_1.default.italic("current directory")); } function resolvePackageRoot(requireFromPackageRoot, packageName) { try { const resolvedPath = requireFromPackageRoot.resolve(packageName); return (0, pkg_dir_1.packageDirectorySync)({ cwd: resolvedPath }); } catch { // TODO: Add a debug log here return undefined; } } function logModulePaths(modulePaths, // TODO: Default to iterating and printing for all supported naming strategies naming) { const pathsPerName = new Map(); for (const modulePath of modulePaths) { const libraryName = getLibraryName(modulePath, naming); const existingPaths = pathsPerName.get(libraryName) ?? []; existingPaths.push(modulePath); pathsPerName.set(libraryName, existingPaths); } const allModulePaths = modulePaths.map((modulePath) => modulePath); const duplicatePaths = (0, duplicates_1.findDuplicates)(allModulePaths); for (const [libraryName, modulePaths] of pathsPerName) { console.log(chalk_1.default.greenBright(`${libraryName}`), ...modulePaths.flatMap((modulePath) => { const line = duplicatePaths.has(modulePath) ? chalk_1.default.redBright(prettyPath(modulePath)) : prettyPath(modulePath); return `\n ↳ ${line}`; })); } } /** * Search upwards from a directory to find a package.json and * return a record mapping from each dependencies of that package to their path on disk. */ function findPackageDependencyPaths(fromPath) { const packageRoot = (0, pkg_dir_1.packageDirectorySync)({ cwd: fromPath }); (0, strict_1.default)(packageRoot, `Could not find package root from ${fromPath}`); const requireFromPackageRoot = (0, node_module_1.createRequire)(node_path_1.default.join(packageRoot, "noop.js")); const { dependencies = {} } = (0, read_pkg_1.readPackageSync)({ cwd: packageRoot }); return Object.fromEntries(Object.keys(dependencies).flatMap((dependencyName) => { const resolvedDependencyRoot = resolvePackageRoot(requireFromPackageRoot, dependencyName); return resolvedDependencyRoot ? [[dependencyName, resolvedDependencyRoot]] : []; })); } exports.MAGIC_FILENAME = "react-native-node-api-module"; /** * Default patterns to use when excluding paths from the search for Node-API modules. */ exports.DEFAULT_EXCLUDE_PATTERNS = [ /(^|\/)node_modules\//, /(^|\/).git\//, ]; function hasPlatformExtension(platform, fileName) { if (typeof platform === "string") { return fileName.endsWith(exports.PLATFORM_EXTENSIONS[platform]); } else { return platform.some((p) => hasPlatformExtension(p, fileName)); } } /** * Recursively search into a directory for directories containing Node-API modules. */ async function findNodeApiModulePaths(options, suffix = "") { const { fromPath, platform, excludePatterns = exports.DEFAULT_EXCLUDE_PATTERNS, } = options; if (excludePatterns.some((pattern) => pattern.test(suffix))) { return []; } const candidatePath = node_path_1.default.join(fromPath, suffix); // Normalize path separators for consistent pattern matching on all platforms const normalizedSuffix = suffix.split(node_path_1.default.sep).join("/"); if (excludePatterns.some((pattern) => pattern.test(normalizedSuffix))) { return []; } const result = []; const pendingResults = []; try { for await (const dirent of await node_fs_1.default.promises.opendir(candidatePath)) { if (dirent.isFile() && dirent.name === exports.MAGIC_FILENAME && hasPlatformExtension(platform, candidatePath)) { result.push(candidatePath); } else if (dirent.isDirectory()) { // Traverse into the child directory // Pushing result into a list instead of awaiting immediately to parallelize the search pendingResults.push(findNodeApiModulePaths(options, node_path_1.default.join(suffix, dirent.name))); } } } catch (error) { if (error instanceof Error && "code" in error && (error.code === "ENOENT" || error.code === "EACCES")) { // Gracefully handling issues with reading directories return []; } throw error; } const childResults = await Promise.all(pendingResults); result.push(...childResults.flatMap((filePath) => filePath)); return result; } /** * Default package names to use when excluding packages from the search for Node-API modules. */ exports.DEFAULT_EXCLUDE_PACKAGES = [ "react-native-node-api", // The host package itself "react-native", // React Native core ]; /** * Finds all dependencies of the app package and their xcframeworks. */ async function findNodeApiModulePathsByDependency({ fromPath, includeSelf, excludePackages = exports.DEFAULT_EXCLUDE_PACKAGES, ...options }) { // Find the location of each dependency const packagePathsByName = findPackageDependencyPaths(fromPath); if (includeSelf) { const packageRoot = (0, pkg_dir_1.packageDirectorySync)({ cwd: fromPath }); (0, strict_1.default)(packageRoot, `Could not find package root from ${fromPath}`); const { name } = (0, read_pkg_1.readPackageSync)({ cwd: packageRoot }); packagePathsByName[name] = packageRoot; } // Find all their node api module paths const resultEntries = await Promise.all(Object.entries(packagePathsByName) .filter(([name]) => !excludePackages.includes(name)) .map(async ([dependencyName, dependencyPath]) => { // Make all the xcframeworks relative to the dependency path const absoluteModulePaths = await findNodeApiModulePaths({ fromPath: dependencyPath, ...options, }); return [ dependencyName, { path: dependencyPath, modulePaths: absoluteModulePaths.map((p) => node_path_1.default.relative(dependencyPath, p)), }, ]; })); // Return an object by dependency name return Object.fromEntries( // Remove any dependencies without Node-API module paths resultEntries.filter(([, { modulePaths }]) => modulePaths.length > 0)); } /** * Determine the library basename (no file extension nor "lib" prefix) based on the library paths. * Errors if all framework paths doesn't produce the same basename. */ function determineLibraryBasename(libraryPaths) { (0, strict_1.default)(libraryPaths.length > 0, "Expected at least one library path to determine its basename"); const libraryNames = libraryPaths.map((p) => // Strip the "lib" prefix and any file extension node_path_1.default.basename(p, node_path_1.default.extname(p)).replace(/^lib/, "")); const candidates = new Set(libraryNames); (0, strict_1.default)(candidates.size === 1, `Expected all libraries to share name, got: ${[...candidates].join(", ")}`); const [name] = candidates; return name; } function getAutolinkPath(platform) { const result = node_path_1.default.resolve(__dirname, "../../auto-linked", platform); node_fs_1.default.mkdirSync(result, { recursive: true }); return result; } /** * Get the latest modification time of all files in a directory and its subdirectories. */ function getLatestMtime(fromPath) { const entries = node_fs_1.default.readdirSync(fromPath, { withFileTypes: true, recursive: true, }); let latest = 0; for (const entry of entries) { if (entry.isFile()) { const fullPath = node_path_1.default.join(entry.parentPath, entry.name); const stat = node_fs_1.default.statSync(fullPath); if (stat.mtimeMs > latest) { latest = stat.mtimeMs; } } } return latest; } // NOTE: List of paths influenced by `node-bindings` itself // https://github.com/TooTallNate/node-bindings/blob/v1.3.0/bindings.js#L21 const nodeBindingsSubdirs = [ "./", "./build/Release", "./build/Debug", "./build", "./out/Release", "./out/Debug", "./Release", "./Debug", ]; function findNodeAddonForBindings(id, fromDir) { const idWithExt = id.endsWith(".node") ? id : `${id}.node`; // Support traversing the filesystem to find the Node-API module. // Currently, we check the most common directories like `bindings` does. for (const subdir of nodeBindingsSubdirs) { const resolvedPath = node_path_1.default.join(fromDir, subdir, idWithExt); if (isNodeApiModule(resolvedPath)) { return resolvedPath; } } return undefined; }