Skip to content

Commit 287d0ad

Browse files
authored
Add naming strategy option for XCFrameworks (#12)
* Rename rebuildXcframeworkHashed to vendorXcframework * Add support for multiple naming strategies * Error on duplicate discriminators * Rename example packages * Flip the option on the test-app * Ignore build artefacts * Fix logging xcframework paths
1 parent d43ece0 commit 287d0ad

File tree

12 files changed

+311
-110
lines changed

12 files changed

+311
-110
lines changed

apps/test-app/babel.config.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
module.exports = {
22
presets: ['module:@react-native/babel-preset'],
3-
plugins: ['module:react-native-node-api-modules/babel-plugin'],
3+
// plugins: [['module:react-native-node-api-modules/babel-plugin', { naming: "hash" }]],
4+
plugins: [['module:react-native-node-api-modules/babel-plugin', { naming: "package-name" }]],
45
};

package-lock.json

Lines changed: 2 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/node-addon-examples/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
"devDependencies": {
1111
"node-addon-examples": "github:nodejs/node-addon-examples#4213d4c9d07996ae68629c67926251e117f8e52a",
1212
"gyp-to-cmake": "*",
13-
"react-native-node-api-cmake": "*"
13+
"react-native-node-api-cmake": "*",
14+
"read-pkg": "^9.0.1"
1415
}
1516
}

packages/node-addon-examples/scripts/copy-examples.mts

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { createRequire } from "node:module";
22
import fs from "node:fs";
33
import path from "node:path";
4+
import { readPackageSync } from "read-pkg";
45

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

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

65+
let counter = 0;
66+
6467
for (const src of ALLOW_LIST) {
6568
const srcPath = path.join(SRC_DIR, src);
6669
const destPath = path.join(EXAMPLES_DIR, src);
@@ -72,9 +75,14 @@ for (const src of ALLOW_LIST) {
7275
recursive: true,
7376
})) {
7477
if (entry.name === "package.json") {
75-
const filePath = path.join(entry.parentPath, entry.name);
76-
console.log("Deleting", filePath);
77-
fs.rmSync(filePath);
78+
const packageJson = readPackageSync({ cwd: entry.parentPath });
79+
// Ensure example package names are unique
80+
packageJson.name = `example-${counter++}`;
81+
fs.writeFileSync(
82+
path.join(entry.parentPath, entry.name),
83+
JSON.stringify(packageJson, null, 2),
84+
"utf-8"
85+
);
7886
}
7987
}
8088
}
Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,13 @@
1-
include/
1+
2+
# Vendored hermes
23
hermes/
4+
5+
# Vendored Node-API header files
6+
include/
7+
8+
# Android build artifacts
9+
**/android/.cxx/
10+
**/android/build/
11+
12+
# iOS build artifacts
313
xcframeworks/

packages/react-native-node-api-modules/package.json

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@
1010
},
1111
"exports": {
1212
".": "./dist/react-native/index.js",
13-
"./babel-plugin": "./dist/node/babel-plugin/index.js"
13+
"./babel-plugin": "./dist/node/babel-plugin/index.js",
14+
"./cli": "./dist/node/cli/run.js"
1415
},
1516
"scripts": {
1617
"build": "tsc --build",
@@ -69,6 +70,9 @@
6970
"outputDir": {
7071
"ios": "ios/generated",
7172
"android": "android/generated"
73+
},
74+
"android": {
75+
"javaPackageName": "com.callstack.node_api_modules"
7276
}
7377
}
7478
}

packages/react-native-node-api-modules/src/node/babel-plugin/plugin.test.ts

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -39,16 +39,18 @@ describe("plugin", () => {
3939
});
4040

4141
const ADDON_1_REQUIRE_ARG = getLibraryInstallName(
42-
path.join(tempDirectoryPath, "addon-1")
42+
path.join(tempDirectoryPath, "addon-1"),
43+
"hash"
4344
);
4445
const ADDON_2_REQUIRE_ARG = getLibraryInstallName(
45-
path.join(tempDirectoryPath, "addon-2")
46+
path.join(tempDirectoryPath, "addon-2"),
47+
"hash"
4648
);
4749

4850
{
4951
const result = transformFileSync(
5052
path.join(tempDirectoryPath, "./addon-1.js"),
51-
{ plugins: [plugin] }
53+
{ plugins: [[plugin, { naming: "hash" }]] }
5254
);
5355
assert(result);
5456
const { code } = result;
@@ -61,7 +63,7 @@ describe("plugin", () => {
6163
{
6264
const result = transformFileSync(
6365
path.join(tempDirectoryPath, "./addon-2.js"),
64-
{ plugins: [plugin] }
66+
{ plugins: [[plugin, { naming: "hash" }]] }
6567
);
6668
assert(result);
6769
const { code } = result;
@@ -74,7 +76,7 @@ describe("plugin", () => {
7476
{
7577
const result = transformFileSync(
7678
path.join(tempDirectoryPath, "./sub-directory/addon-1.js"),
77-
{ plugins: [plugin] }
79+
{ plugins: [[plugin, { naming: "hash" }]] }
7880
);
7981
assert(result);
8082
const { code } = result;
@@ -87,7 +89,7 @@ describe("plugin", () => {
8789
{
8890
const result = transformFileSync(
8991
path.join(tempDirectoryPath, "./addon-1-bindings.js"),
90-
{ plugins: [plugin] }
92+
{ plugins: [[plugin, { naming: "hash" }]] }
9193
);
9294
assert(result);
9395
const { code } = result;
@@ -100,7 +102,7 @@ describe("plugin", () => {
100102
{
101103
const result = transformFileSync(
102104
path.join(tempDirectoryPath, "./require-js-file.js"),
103-
{ plugins: [plugin] }
105+
{ plugins: [[plugin, { naming: "hash" }]] }
104106
);
105107
assert(result);
106108
const { code } = result;

packages/react-native-node-api-modules/src/node/babel-plugin/plugin.ts

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import assert from "node:assert/strict";
12
import path from "node:path";
23

34
import type { PluginObj, NodePath } from "@babel/core";
@@ -7,11 +8,33 @@ import {
78
getLibraryInstallName,
89
isNodeApiModule,
910
replaceWithNodeExtension,
11+
NamingStrategy,
12+
NAMING_STATEGIES,
1013
} from "../path-utils";
1114

12-
export function replaceWithRequireNodeAddon(p: NodePath, modulePath: string) {
15+
type PluginOptions = {
16+
naming?: NamingStrategy;
17+
};
18+
19+
function assertOptions(opts: unknown): asserts opts is PluginOptions {
20+
assert(typeof opts === "object" && opts !== null, "Expected an object");
21+
if ("naming" in opts) {
22+
assert(typeof opts.naming === "string", "Expected 'naming' to be a string");
23+
assert(
24+
NAMING_STATEGIES.includes(opts.naming as NamingStrategy),
25+
"Expected 'naming' to be either 'hash' or 'package-name'"
26+
);
27+
}
28+
}
29+
30+
export function replaceWithRequireNodeAddon(
31+
p: NodePath,
32+
modulePath: string,
33+
naming: NamingStrategy
34+
) {
1335
const requireCallArgument = getLibraryInstallName(
14-
replaceWithNodeExtension(modulePath)
36+
replaceWithNodeExtension(modulePath),
37+
naming
1538
);
1639
p.replaceWith(
1740
t.callExpression(
@@ -30,6 +53,8 @@ export function plugin(): PluginObj {
3053
return {
3154
visitor: {
3255
CallExpression(p) {
56+
assertOptions(this.opts);
57+
const { naming = "package-name" } = this.opts;
3358
if (typeof this.filename !== "string") {
3459
// This transformation only works when the filename is known
3560
return;
@@ -52,15 +77,15 @@ export function plugin(): PluginObj {
5277
const relativePath = path.join(from, id);
5378
// TODO: Support traversing the filesystem to find the Node-API module
5479
if (isNodeApiModule(relativePath)) {
55-
replaceWithRequireNodeAddon(p.parentPath, relativePath);
80+
replaceWithRequireNodeAddon(p.parentPath, relativePath, naming);
5681
}
5782
}
5883
} else if (
5984
!path.isAbsolute(id) &&
6085
isNodeApiModule(path.join(from, id))
6186
) {
6287
const relativePath = path.join(from, id);
63-
replaceWithRequireNodeAddon(p, relativePath);
88+
replaceWithRequireNodeAddon(p, relativePath, naming);
6489
}
6590
}
6691
},

packages/react-native-node-api-modules/src/node/cli/helpers.ts

Lines changed: 72 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import { spawn } from "bufout";
88
import { packageDirectorySync } from "pkg-dir";
99
import { readPackageSync } from "read-pkg";
1010

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

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

181-
type HashedXCFramework = {
182+
type VendoredXcframework = {
182183
originalPath: string;
183184
outputPath: string;
185+
} & (
186+
| {
187+
hash: string;
188+
packageName?: never;
189+
}
190+
| {
191+
hash?: never;
192+
packageName: string;
193+
}
194+
);
195+
196+
type VendoredXcframeworkResult = VendoredXcframework & {
184197
skipped: boolean;
185-
hash: string;
186198
};
187199

188-
export async function rebuildXcframeworkHashed({
200+
export function determineVendoredXcframeworkDetails(
201+
modulePath: string,
202+
naming: NamingStrategy
203+
): VendoredXcframework {
204+
if (naming === "hash") {
205+
const hash = hashModulePath(modulePath);
206+
return {
207+
hash,
208+
originalPath: modulePath,
209+
outputPath: path.join(XCFRAMEWORKS_PATH, `node-api-${hash}.xcframework`),
210+
};
211+
} else {
212+
const packageRoot = packageDirectorySync({ cwd: modulePath });
213+
assert(packageRoot, `Could not find package root from ${modulePath}`);
214+
const { name } = readPackageSync({ cwd: packageRoot });
215+
assert(name, `Could not find package name from ${packageRoot}`);
216+
return {
217+
packageName: name,
218+
originalPath: modulePath,
219+
outputPath: path.join(XCFRAMEWORKS_PATH, `${name}.xcframework`),
220+
};
221+
}
222+
}
223+
224+
export function hasDuplicatesWhenVendored(
225+
modulePaths: string[],
226+
naming: NamingStrategy
227+
): boolean {
228+
const outputPaths = modulePaths.map((modulePath) => {
229+
const { outputPath } = determineVendoredXcframeworkDetails(
230+
modulePath,
231+
naming
232+
);
233+
return outputPath;
234+
});
235+
const uniqueNames = new Set(outputPaths);
236+
return uniqueNames.size !== outputPaths.length;
237+
}
238+
239+
export async function vendorXcframework({
189240
modulePath,
190241
incremental,
191-
}: RebuildXcframeworkOptions): Promise<HashedXCFramework> {
242+
naming,
243+
}: RebuildXcframeworkOptions): Promise<VendoredXcframeworkResult> {
192244
// Copy the xcframework to the output directory and rename the framework and binary
193-
const hash = hashModulePath(modulePath);
194-
const tempPath = path.join(XCFRAMEWORKS_PATH, `node-api-${hash}-temp`);
245+
const details = determineVendoredXcframeworkDetails(modulePath, naming);
246+
const { outputPath } = details;
247+
const discriminator =
248+
typeof details.hash === "string" ? details.hash : details.packageName;
249+
const tempPath = path.join(
250+
XCFRAMEWORKS_PATH,
251+
`node-api-${discriminator}-temp`
252+
);
195253
try {
196-
const outputPath = path.join(
197-
XCFRAMEWORKS_PATH,
198-
`node-api-${hash}.xcframework`
199-
);
200254
if (incremental && existsSync(outputPath)) {
201255
const moduleModified = getLatestMtime(modulePath);
202256
const outputModified = getLatestMtime(outputPath);
203257
if (moduleModified < outputModified) {
204-
return {
205-
skipped: true,
206-
outputPath,
207-
originalPath: modulePath,
208-
hash,
209-
};
258+
return { ...details, skipped: true };
210259
}
211260
}
212261
// Delete any existing xcframework (or xcodebuild will try to amend it)
@@ -235,7 +284,10 @@ export async function rebuildXcframeworkHashed({
235284
".framework"
236285
);
237286
const oldLibraryPath = path.join(frameworkPath, oldLibraryName);
238-
const newLibraryName = `node-api-${hash}`;
287+
const newLibraryName = path.basename(
288+
details.outputPath,
289+
".xcframework"
290+
);
239291
const newFrameworkPath = path.join(
240292
tripletPath,
241293
`${newLibraryName}.framework`
@@ -252,7 +304,7 @@ export async function rebuildXcframeworkHashed({
252304
await fs.rename(
253305
oldLibraryPath,
254306
// Cannot use newLibraryPath here, because the framework isn't renamed yet
255-
path.join(frameworkPath, `node-api-${hash}`)
307+
path.join(frameworkPath, newLibraryName)
256308
);
257309
// Rename the framework
258310
await fs.rename(frameworkPath, newFrameworkPath);
@@ -298,12 +350,7 @@ export async function rebuildXcframeworkHashed({
298350
}
299351
);
300352

301-
return {
302-
skipped: false,
303-
outputPath,
304-
originalPath: modulePath,
305-
hash,
306-
};
353+
return { ...details, skipped: false };
307354
} finally {
308355
await fs.rm(tempPath, { recursive: true, force: true });
309356
}

0 commit comments

Comments
 (0)