Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion apps/test-app/babel.config.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
module.exports = {
presets: ['module:@react-native/babel-preset'],
plugins: ['module:react-native-node-api-modules/babel-plugin'],
// plugins: [['module:react-native-node-api-modules/babel-plugin', { naming: "hash" }]],
plugins: [['module:react-native-node-api-modules/babel-plugin', { naming: "package-name" }]],
};
3 changes: 2 additions & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion packages/node-addon-examples/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
"devDependencies": {
"node-addon-examples": "github:nodejs/node-addon-examples#4213d4c9d07996ae68629c67926251e117f8e52a",
"gyp-to-cmake": "*",
"react-native-node-api-cmake": "*"
"react-native-node-api-cmake": "*",
"read-pkg": "^9.0.1"
}
}
14 changes: 11 additions & 3 deletions packages/node-addon-examples/scripts/copy-examples.mts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { createRequire } from "node:module";
import fs from "node:fs";
import path from "node:path";
import { readPackageSync } from "read-pkg";

import { EXAMPLES_DIR } from "./cmake-projects.mjs";

Expand Down Expand Up @@ -61,6 +62,8 @@ const EXAMPLES_PACKAGE_PATH = require.resolve(
const SRC_DIR = path.join(path.dirname(EXAMPLES_PACKAGE_PATH), "src");
console.log("Copying files from", SRC_DIR);

let counter = 0;

for (const src of ALLOW_LIST) {
const srcPath = path.join(SRC_DIR, src);
const destPath = path.join(EXAMPLES_DIR, src);
Expand All @@ -72,9 +75,14 @@ for (const src of ALLOW_LIST) {
recursive: true,
})) {
if (entry.name === "package.json") {
const filePath = path.join(entry.parentPath, entry.name);
console.log("Deleting", filePath);
fs.rmSync(filePath);
const packageJson = readPackageSync({ cwd: entry.parentPath });
// Ensure example package names are unique
packageJson.name = `example-${counter++}`;
fs.writeFileSync(
path.join(entry.parentPath, entry.name),
JSON.stringify(packageJson, null, 2),
"utf-8"
);
}
}
}
12 changes: 11 additions & 1 deletion packages/react-native-node-api-modules/.gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,13 @@
include/

# Vendored hermes
hermes/

# Vendored Node-API header files
include/

# Android build artifacts
**/android/.cxx/
**/android/build/

# iOS build artifacts
xcframeworks/
6 changes: 5 additions & 1 deletion packages/react-native-node-api-modules/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@
},
"exports": {
".": "./dist/react-native/index.js",
"./babel-plugin": "./dist/node/babel-plugin/index.js"
"./babel-plugin": "./dist/node/babel-plugin/index.js",
"./cli": "./dist/node/cli/run.js"
},
"scripts": {
"build": "tsc --build",
Expand Down Expand Up @@ -69,6 +70,9 @@
"outputDir": {
"ios": "ios/generated",
"android": "android/generated"
},
"android": {
"javaPackageName": "com.callstack.node_api_modules"
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -39,16 +39,18 @@ describe("plugin", () => {
});

const ADDON_1_REQUIRE_ARG = getLibraryInstallName(
path.join(tempDirectoryPath, "addon-1")
path.join(tempDirectoryPath, "addon-1"),
"hash"
);
const ADDON_2_REQUIRE_ARG = getLibraryInstallName(
path.join(tempDirectoryPath, "addon-2")
path.join(tempDirectoryPath, "addon-2"),
"hash"
);

{
const result = transformFileSync(
path.join(tempDirectoryPath, "./addon-1.js"),
{ plugins: [plugin] }
{ plugins: [[plugin, { naming: "hash" }]] }
);
assert(result);
const { code } = result;
Expand All @@ -61,7 +63,7 @@ describe("plugin", () => {
{
const result = transformFileSync(
path.join(tempDirectoryPath, "./addon-2.js"),
{ plugins: [plugin] }
{ plugins: [[plugin, { naming: "hash" }]] }
);
assert(result);
const { code } = result;
Expand All @@ -74,7 +76,7 @@ describe("plugin", () => {
{
const result = transformFileSync(
path.join(tempDirectoryPath, "./sub-directory/addon-1.js"),
{ plugins: [plugin] }
{ plugins: [[plugin, { naming: "hash" }]] }
);
assert(result);
const { code } = result;
Expand All @@ -87,7 +89,7 @@ describe("plugin", () => {
{
const result = transformFileSync(
path.join(tempDirectoryPath, "./addon-1-bindings.js"),
{ plugins: [plugin] }
{ plugins: [[plugin, { naming: "hash" }]] }
);
assert(result);
const { code } = result;
Expand All @@ -100,7 +102,7 @@ describe("plugin", () => {
{
const result = transformFileSync(
path.join(tempDirectoryPath, "./require-js-file.js"),
{ plugins: [plugin] }
{ plugins: [[plugin, { naming: "hash" }]] }
);
assert(result);
const { code } = result;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import assert from "node:assert/strict";
import path from "node:path";

import type { PluginObj, NodePath } from "@babel/core";
Expand All @@ -7,11 +8,33 @@ import {
getLibraryInstallName,
isNodeApiModule,
replaceWithNodeExtension,
NamingStrategy,
NAMING_STATEGIES,
} from "../path-utils";

export function replaceWithRequireNodeAddon(p: NodePath, modulePath: string) {
type PluginOptions = {
naming?: NamingStrategy;
};

function assertOptions(opts: unknown): asserts opts is PluginOptions {
assert(typeof opts === "object" && opts !== null, "Expected an object");
if ("naming" in opts) {
assert(typeof opts.naming === "string", "Expected 'naming' to be a string");
assert(
NAMING_STATEGIES.includes(opts.naming as NamingStrategy),
"Expected 'naming' to be either 'hash' or 'package-name'"
);
}
}

export function replaceWithRequireNodeAddon(
p: NodePath,
modulePath: string,
naming: NamingStrategy
) {
const requireCallArgument = getLibraryInstallName(
replaceWithNodeExtension(modulePath)
replaceWithNodeExtension(modulePath),
naming
);
p.replaceWith(
t.callExpression(
Expand All @@ -30,6 +53,8 @@ export function plugin(): PluginObj {
return {
visitor: {
CallExpression(p) {
assertOptions(this.opts);
const { naming = "package-name" } = this.opts;
if (typeof this.filename !== "string") {
// This transformation only works when the filename is known
return;
Expand All @@ -52,15 +77,15 @@ export function plugin(): PluginObj {
const relativePath = path.join(from, id);
// TODO: Support traversing the filesystem to find the Node-API module
if (isNodeApiModule(relativePath)) {
replaceWithRequireNodeAddon(p.parentPath, relativePath);
replaceWithRequireNodeAddon(p.parentPath, relativePath, naming);
}
}
} else if (
!path.isAbsolute(id) &&
isNodeApiModule(path.join(from, id))
) {
const relativePath = path.join(from, id);
replaceWithRequireNodeAddon(p, relativePath);
replaceWithRequireNodeAddon(p, relativePath, naming);
}
}
},
Expand Down
97 changes: 72 additions & 25 deletions packages/react-native-node-api-modules/src/node/cli/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { spawn } from "bufout";
import { packageDirectorySync } from "pkg-dir";
import { readPackageSync } from "read-pkg";

import { hashModulePath } from "../path-utils.js";
import { NamingStrategy, hashModulePath } from "../path-utils.js";

// Must be in all xcframeworks to be considered as Node-API modules
export const MAGIC_FILENAME = "react-native-node-api-module";
Expand Down Expand Up @@ -176,37 +176,86 @@ export async function updateInfoPlist({
type RebuildXcframeworkOptions = {
modulePath: string;
incremental: boolean;
naming: NamingStrategy;
};

type HashedXCFramework = {
type VendoredXcframework = {
originalPath: string;
outputPath: string;
} & (
| {
hash: string;
packageName?: never;
}
| {
hash?: never;
packageName: string;
}
);

type VendoredXcframeworkResult = VendoredXcframework & {
skipped: boolean;
hash: string;
};

export async function rebuildXcframeworkHashed({
export function determineVendoredXcframeworkDetails(
modulePath: string,
naming: NamingStrategy
): VendoredXcframework {
if (naming === "hash") {
const hash = hashModulePath(modulePath);
return {
hash,
originalPath: modulePath,
outputPath: path.join(XCFRAMEWORKS_PATH, `node-api-${hash}.xcframework`),
};
} else {
const packageRoot = packageDirectorySync({ cwd: modulePath });
assert(packageRoot, `Could not find package root from ${modulePath}`);
const { name } = readPackageSync({ cwd: packageRoot });
assert(name, `Could not find package name from ${packageRoot}`);
return {
packageName: name,
originalPath: modulePath,
outputPath: path.join(XCFRAMEWORKS_PATH, `${name}.xcframework`),
};
}
}

export function hasDuplicatesWhenVendored(
modulePaths: string[],
naming: NamingStrategy
): boolean {
const outputPaths = modulePaths.map((modulePath) => {
const { outputPath } = determineVendoredXcframeworkDetails(
modulePath,
naming
);
return outputPath;
});
const uniqueNames = new Set(outputPaths);
return uniqueNames.size !== outputPaths.length;
}

export async function vendorXcframework({
modulePath,
incremental,
}: RebuildXcframeworkOptions): Promise<HashedXCFramework> {
naming,
}: RebuildXcframeworkOptions): Promise<VendoredXcframeworkResult> {
// Copy the xcframework to the output directory and rename the framework and binary
const hash = hashModulePath(modulePath);
const tempPath = path.join(XCFRAMEWORKS_PATH, `node-api-${hash}-temp`);
const details = determineVendoredXcframeworkDetails(modulePath, naming);
const { outputPath } = details;
const discriminator =
typeof details.hash === "string" ? details.hash : details.packageName;
const tempPath = path.join(
XCFRAMEWORKS_PATH,
`node-api-${discriminator}-temp`
);
try {
const outputPath = path.join(
XCFRAMEWORKS_PATH,
`node-api-${hash}.xcframework`
);
if (incremental && existsSync(outputPath)) {
const moduleModified = getLatestMtime(modulePath);
const outputModified = getLatestMtime(outputPath);
if (moduleModified < outputModified) {
return {
skipped: true,
outputPath,
originalPath: modulePath,
hash,
};
return { ...details, skipped: true };
}
}
// Delete any existing xcframework (or xcodebuild will try to amend it)
Expand Down Expand Up @@ -235,7 +284,10 @@ export async function rebuildXcframeworkHashed({
".framework"
);
const oldLibraryPath = path.join(frameworkPath, oldLibraryName);
const newLibraryName = `node-api-${hash}`;
const newLibraryName = path.basename(
details.outputPath,
".xcframework"
);
const newFrameworkPath = path.join(
tripletPath,
`${newLibraryName}.framework`
Expand All @@ -252,7 +304,7 @@ export async function rebuildXcframeworkHashed({
await fs.rename(
oldLibraryPath,
// Cannot use newLibraryPath here, because the framework isn't renamed yet
path.join(frameworkPath, `node-api-${hash}`)
path.join(frameworkPath, newLibraryName)
);
// Rename the framework
await fs.rename(frameworkPath, newFrameworkPath);
Expand Down Expand Up @@ -298,12 +350,7 @@ export async function rebuildXcframeworkHashed({
}
);

return {
skipped: false,
outputPath,
originalPath: modulePath,
hash,
};
return { ...details, skipped: false };
} finally {
await fs.rm(tempPath, { recursive: true, force: true });
}
Expand Down
Loading