-
Notifications
You must be signed in to change notification settings - Fork 7
Expand file tree
/
Copy pathplugin.ts
More file actions
139 lines (129 loc) · 4.62 KB
/
plugin.ts
File metadata and controls
139 lines (129 loc) · 4.62 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
import assert from "node:assert/strict";
import path from "node:path";
import type { PluginObj, NodePath } from "@babel/core";
import * as t from "@babel/types";
import { determineModuleContext, isNodeApiModule } from "../path-utils";
export type PluginOptions = {
stripPathSuffix?: boolean;
};
function assertOptions(opts: unknown): asserts opts is PluginOptions {
assert(typeof opts === "object" && opts !== null, "Expected an object");
if ("stripPathSuffix" in opts) {
assert(
typeof opts.stripPathSuffix === "boolean",
"Expected 'stripPathSuffix' to be a boolean"
);
}
}
// This function should work with both CommonJS and ECMAScript modules,
// (pretending that addons are supported with ES module imports), hence it
// must accept following import specifiers:
// - "Relative specifiers" (e.g. `./build/Release/addon.node`)
// - "Bare specifiers", in particular
// - to an entry point (e.g. `@callstack/example-addon`)
// - any specific exported feature within
// - "Absolute specifiers" like `node:fs/promise` and URLs.
//
// This function should also respect the Package entry points defined in the
// respective "package.json" file using "main" or "exports" and "imports"
// fields (including conditional exports and subpath imports).
// - https://nodejs.org/api/packages.html#package-entry-points
// - https://nodejs.org/api/packages.html#subpath-imports
function tryResolveModulePath(id: string, from: string): string | undefined {
if (id.includes(":")) {
// This must be a prefixed "Absolute specifier". We assume its a built-in
// module and pass it through without any changes. For security reasons,
// we don't support URLs to dynamic libraries (like Node-API addons).
return undefined;
} else {
// TODO: Stay compatible with https://nodejs.org/api/modules.html#all-together
try {
return require.resolve(id, { paths: [from] });
} catch {
return undefined;
}
}
}
const nodeBindingsSubdirs = [
"./",
"./build/Release",
"./build/Debug",
"./build",
"./out/Release",
"./out/Debug",
"./Release",
"./Debug",
];
export function findNodeAddonForBindings(id: string, fromDir: string) {
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 = path.join(fromDir, subdir, idWithExt);
if (isNodeApiModule(resolvedPath)) {
return resolvedPath;
}
}
return undefined;
}
export function replaceWithRequireNodeAddon3(
p: NodePath,
resolvedPath: string,
originalPath: string
) {
const { packageName, relativePath } = determineModuleContext(resolvedPath);
const relPath = relativePath.replaceAll("\\", "/");
const finalRelPath = relPath.startsWith("./") ? relPath : `./${relPath}`;
p.replaceWith(
t.callExpression(
t.memberExpression(
t.callExpression(t.identifier("require"), [
t.stringLiteral("react-native-node-api-modules"),
]),
t.identifier("requireNodeAddon")
),
[finalRelPath, packageName, originalPath]
.map(t.stringLiteral),
)
);
}
export function plugin(): PluginObj {
return {
visitor: {
CallExpression(p) {
assertOptions(this.opts);
if (typeof this.filename !== "string") {
// This transformation only works when the filename is known
return;
}
const from = path.dirname(this.filename);
const { node } = p;
const [argument] = node.arguments;
if (
node.callee.type === "Identifier" &&
node.callee.name === "require" &&
argument.type === "StringLiteral"
) {
// Require call with a string literal argument
const id = argument.value;
if (id === "bindings" && p.parent.type === "CallExpression") {
const [argument] = p.parent.arguments;
if (argument.type === "StringLiteral") {
const id = argument.value;
const resolvedPath = findNodeAddonForBindings(id, from);
if (resolvedPath !== undefined) {
replaceWithRequireNodeAddon3(p.parentPath, resolvedPath, id);
}
}
} else {
// This should handle "bare specifiers" and "private imports" that start with `#`
const resolvedPath = tryResolveModulePath(id, from);
if (!!resolvedPath && isNodeApiModule(resolvedPath)) {
replaceWithRequireNodeAddon3(p, resolvedPath, id);
}
}
}
},
},
};
}