Skip to content

Commit 0a106e5

Browse files
author
Mariusz Pasinski
committed
feat(babel): add 2 params to requireNodeAddon(); respect package entrypoints
This big squashed change makes our Babel plugin to replace `require()` calls with `requireNodeAddons()` with 3 parameters, as initially described in #91 While squashing, I also removed my attempt to reimplement Node's resolution algorithm (https://nodejs.org/api/modules.html#all-together) and replaced it with a call to `require.resolve()`, which basically seems to do the same, just better with less code. This code has been tested with package that uses "main" entry point and has no JavaScript files. Finally, it commit will resolve the TODO regarding scanning the filesystem for addons when using `bindings`. Instead of globbing over the directories, I'm scanning the most popular directories where addons will be placed. Such list of directories was heavily influenced by `node-bindings` itself (https://github.com/TooTallNate/node-bindings/blob/v1.3.0/bindings.js#L21).
1 parent 521fe0d commit 0a106e5

File tree

1 file changed

+76
-17
lines changed
  • packages/react-native-node-api-modules/src/node/babel-plugin

1 file changed

+76
-17
lines changed

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

Lines changed: 76 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ import path from "node:path";
33

44
import type { PluginObj, NodePath } from "@babel/core";
55
import * as t from "@babel/types";
6+
import { packageDirectorySync } from "pkg-dir";
7+
import { readPackageSync } from "read-pkg";
68

79
import { getLibraryName, isNodeApiModule, NamingStrategy } from "../path-utils";
810

@@ -20,12 +22,65 @@ function assertOptions(opts: unknown): asserts opts is PluginOptions {
2022
}
2123
}
2224

23-
export function replaceWithRequireNodeAddon(
25+
// This function should work with both CommonJS and ECMAScript modules,
26+
// (pretending that addons are supported with ES module imports), hence it
27+
// must accept following import specifiers:
28+
// - "Relative specifiers" (e.g. `./build/Release/addon.node`)
29+
// - "Bare specifiers", in particular
30+
// - to an entry point (e.g. `@callstack/example-addon`)
31+
// - any specific exported feature within
32+
// - "Absolute specifiers" like `node:fs/promise` and URLs.
33+
//
34+
// This function should also respect the Package entry points defined in the
35+
// respective "package.json" file using "main" or "exports" and "imports"
36+
// fields (including conditional exports and subpath imports).
37+
// - https://nodejs.org/api/packages.html#package-entry-points
38+
// - https://nodejs.org/api/packages.html#subpath-imports
39+
function tryResolveModulePath(id: string, from: string): string | undefined {
40+
if (id.includes(":")) {
41+
// This must be a prefixed "Absolute specifier". We assume its a built-in
42+
// module and pass it through without any changes. For security reasons,
43+
// we don't support URLs to dynamic libraries (like Node-API addons).
44+
return undefined;
45+
} else {
46+
// TODO: Stay compatible with https://nodejs.org/api/modules.html#all-together
47+
try {
48+
return require.resolve(id, { paths: [from] });
49+
} catch {
50+
return undefined;
51+
}
52+
}
53+
}
54+
55+
const nodeBindingsSubdirs = [
56+
"./",
57+
"./build/Release",
58+
"./build/Debug",
59+
"./build",
60+
"./out/Release",
61+
"./out/Debug",
62+
"./Release",
63+
"./Debug",
64+
];
65+
function findNodeAddonForBindings(id: string, fromDir: string) {
66+
67+
}
68+
69+
export function replaceWithRequireNodeAddon3(
2470
p: NodePath,
25-
modulePath: string,
26-
naming: NamingStrategy
71+
resolvedPath: string,
72+
requiredFrom: string
2773
) {
28-
const requireCallArgument = getLibraryName(modulePath, naming);
74+
const pkgRoot = packageDirectorySync({ cwd: resolvedPath });
75+
if (pkgRoot === undefined) {
76+
throw new Error("Unable to locate package directory!");
77+
}
78+
79+
const subpath = path.relative(pkgRoot, resolvedPath);
80+
const dottedSubpath = subpath.startsWith("./") ? subpath : `./${subpath}`;
81+
const fromRelative = path.relative(pkgRoot, requiredFrom); // TODO: might escape package
82+
const { name } = readPackageSync({ cwd: pkgRoot });
83+
2984
p.replaceWith(
3085
t.callExpression(
3186
t.memberExpression(
@@ -34,7 +89,8 @@ export function replaceWithRequireNodeAddon(
3489
]),
3590
t.identifier("requireNodeAddon")
3691
),
37-
[t.stringLiteral(requireCallArgument)]
92+
[dottedSubpath, name, fromRelative]
93+
.map(t.stringLiteral),
3894
)
3995
);
4096
}
@@ -64,20 +120,23 @@ export function plugin(): PluginObj {
64120
const [argument] = p.parent.arguments;
65121
if (argument.type === "StringLiteral") {
66122
const id = argument.value;
67-
const relativePath = path.join(from, id);
68-
// TODO: Support traversing the filesystem to find the Node-API module
69-
if (isNodeApiModule(relativePath)) {
70-
replaceWithRequireNodeAddon(p.parentPath, relativePath, {
71-
stripPathSuffix,
72-
});
123+
const idWithExt = id.endsWith(".node") ? id : `${id}.node`;
124+
// Support traversing the filesystem to find the Node-API module.
125+
// Currently, we check the most common directories from `bindings`.
126+
for (const subdir of nodeBindingsSubdirs) {
127+
const resolvedPath = path.join(from, subdir, idWithExt);
128+
if (isNodeApiModule(resolvedPath)) {
129+
replaceWithRequireNodeAddon3(p.parentPath, resolvedPath, this.filename);
130+
break;
131+
}
73132
}
74133
}
75-
} else if (
76-
!path.isAbsolute(id) &&
77-
isNodeApiModule(path.join(from, id))
78-
) {
79-
const relativePath = path.join(from, id);
80-
replaceWithRequireNodeAddon(p, relativePath, { stripPathSuffix });
134+
} else {
135+
// This should handle "bare specifiers" and "private imports" that start with `#`
136+
const resolvedPath = tryResolveModulePath(id, from);
137+
if (!!resolvedPath && isNodeApiModule(resolvedPath)) {
138+
replaceWithRequireNodeAddon3(p, resolvedPath, this.filename);
139+
}
81140
}
82141
}
83142
},

0 commit comments

Comments
 (0)