react-native-node-api
Version:
Node-API for React Native
382 lines (381 loc) • 16 kB
JavaScript
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;
}
;