Skip to content

Commit acf1a7c

Browse files
authored
Treat opendir failures as empty directories (#186)
* Tread a failing opendir as an empty dir * Exclude specific packages * Add a changeset * Refactor setupTempDirectory to accept objects for dirs * Add simple test for findNodeApiModulePathsByDependency
1 parent d367b6e commit acf1a7c

File tree

4 files changed

+152
-44
lines changed

4 files changed

+152
-44
lines changed

.changeset/stale-coins-drop.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"react-native-node-api": patch
3+
---
4+
5+
Treating failures when scanning filesystems for Node-API prebuilds more gracefully

packages/host/src/node/path-utils.test.ts

Lines changed: 91 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
getLibraryName,
1313
isNodeApiModule,
1414
stripExtension,
15+
findNodeApiModulePathsByDependency,
1516
} from "./path-utils.js";
1617
import { setupTempDirectory } from "./test-utils.js";
1718

@@ -62,9 +63,9 @@ describe("isNodeApiModule", () => {
6263
assert(isNodeApiModule(path.join(tempDirectoryPath, "addon.node")));
6364
});
6465

65-
// there is no way to set ACLs on directories in Node.js on Windows with brittle powershell commands
6666
it(
6767
"returns false when directory cannot be read due to permissions",
68+
// Skipping on Windows because there is no way to set ACLs on directories in Node.js on Windows without brittle powershell commands
6869
{ skip: process.platform === "win32" },
6970
(context) => {
7071
const tempDirectoryPath = setupTempDirectory(context, {
@@ -268,24 +269,30 @@ describe("getLibraryName", () => {
268269
describe("findPackageDependencyPaths", () => {
269270
it("should find package dependency paths", (context) => {
270271
const tempDir = setupTempDirectory(context, {
271-
"node_modules/lib-a/package.json": JSON.stringify({
272-
name: "lib-a",
273-
main: "index.js",
274-
}),
275-
"node_modules/lib-a/index.js": "",
276-
"test-package/node_modules/lib-b/package.json": JSON.stringify({
277-
name: "lib-b",
278-
main: "index.js",
279-
}),
280-
"test-package/node_modules/lib-b/index.js": "",
281-
"test-package/package.json": JSON.stringify({
282-
name: "test-package",
283-
dependencies: {
284-
"lib-a": "^1.0.0",
285-
"lib-b": "^1.0.0",
272+
"node_modules/lib-a": {
273+
"package.json": JSON.stringify({
274+
name: "lib-a",
275+
main: "index.js",
276+
}),
277+
"index.js": "",
278+
},
279+
"test-package": {
280+
"package.json": JSON.stringify({
281+
name: "test-package",
282+
dependencies: {
283+
"lib-a": "^1.0.0",
284+
"lib-b": "^1.0.0",
285+
},
286+
}),
287+
"src/index.js": "console.log('Hello, world!')",
288+
"node_modules/lib-b": {
289+
"package.json": JSON.stringify({
290+
name: "lib-b",
291+
main: "index.js",
292+
}),
293+
"index.js": "",
286294
},
287-
}),
288-
"test-package/src/index.js": "console.log('Hello, world!')",
295+
},
289296
});
290297

291298
const result = findPackageDependencyPaths(
@@ -367,6 +374,72 @@ describe("findNodeApiModulePaths", () => {
367374
path.join(tempDir, "node_modules/root.apple.node"),
368375
]);
369376
});
377+
378+
it(
379+
"returns empty when directory cannot be read due to permissions",
380+
// Skipping on Windows because there is no way to set ACLs on directories in Node.js on Windows without brittle powershell commands
381+
{ skip: process.platform === "win32" },
382+
async (context) => {
383+
const tempDir = setupTempDirectory(context, {
384+
"addon.apple.node/react-native-node-api-module": "",
385+
});
386+
387+
removeReadPermissions(tempDir);
388+
try {
389+
const result = findNodeApiModulePaths({
390+
fromPath: tempDir,
391+
platform: "apple",
392+
});
393+
assert.deepEqual(await result, []);
394+
} finally {
395+
restoreReadPermissions(tempDir);
396+
}
397+
}
398+
);
399+
});
400+
401+
describe("findNodeApiModulePathsByDependency", () => {
402+
it.only("should find Node-API paths by dependency (excluding certain packages)", async (context) => {
403+
const packagesNames = ["lib-a", "lib-b", "lib-c"];
404+
const tempDir = setupTempDirectory(context, {
405+
"app/package.json": JSON.stringify({
406+
name: "app",
407+
dependencies: Object.fromEntries(
408+
packagesNames.map((packageName) => [packageName, "^1.0.0"])
409+
),
410+
}),
411+
...Object.fromEntries(
412+
packagesNames.map((packageName) => [
413+
`app/node_modules/${packageName}`,
414+
{
415+
"package.json": JSON.stringify({
416+
name: packageName,
417+
main: "index.js",
418+
}),
419+
"index.js": "",
420+
"addon.apple.node/react-native-node-api-module": "",
421+
},
422+
])
423+
),
424+
});
425+
426+
const result = await findNodeApiModulePathsByDependency({
427+
fromPath: path.join(tempDir, "app"),
428+
platform: "apple",
429+
includeSelf: false,
430+
excludePackages: ["lib-a"],
431+
});
432+
assert.deepEqual(result, {
433+
"lib-b": {
434+
path: path.join(tempDir, "app/node_modules/lib-b"),
435+
modulePaths: ["addon.apple.node"],
436+
},
437+
"lib-c": {
438+
path: path.join(tempDir, "app/node_modules/lib-c"),
439+
modulePaths: ["addon.apple.node"],
440+
},
441+
});
442+
});
370443
});
371444

372445
describe("determineModuleContext", () => {

packages/host/src/node/path-utils.ts

Lines changed: 37 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -279,7 +279,6 @@ export const MAGIC_FILENAME = "react-native-node-api-module";
279279
* Default patterns to use when excluding paths from the search for Node-API modules.
280280
*/
281281
export const DEFAULT_EXCLUDE_PATTERNS = [
282-
/(^|\/)react-native-node-api\//,
283282
/(^|\/)node_modules\//,
284283
/(^|\/).git\//,
285284
];
@@ -327,35 +326,57 @@ export async function findNodeApiModulePaths(
327326
const result: string[] = [];
328327
const pendingResults: Promise<string[]>[] = [];
329328

330-
for await (const dirent of await fs.promises.opendir(candidatePath)) {
329+
try {
330+
for await (const dirent of await fs.promises.opendir(candidatePath)) {
331+
if (
332+
dirent.isFile() &&
333+
dirent.name === MAGIC_FILENAME &&
334+
hasPlatformExtension(platform, candidatePath)
335+
) {
336+
result.push(candidatePath);
337+
} else if (dirent.isDirectory()) {
338+
// Traverse into the child directory
339+
// Pushing result into a list instead of awaiting immediately to parallelize the search
340+
pendingResults.push(
341+
findNodeApiModulePaths(options, path.join(suffix, dirent.name))
342+
);
343+
}
344+
}
345+
} catch (error) {
331346
if (
332-
dirent.isFile() &&
333-
dirent.name === MAGIC_FILENAME &&
334-
hasPlatformExtension(platform, candidatePath)
347+
error instanceof Error &&
348+
"code" in error &&
349+
(error.code === "ENOENT" || error.code === "EACCES")
335350
) {
336-
result.push(candidatePath);
337-
} else if (dirent.isDirectory()) {
338-
// Traverse into the child directory
339-
// Pushing result into a list instead of awaiting immediately to parallelize the search
340-
pendingResults.push(
341-
findNodeApiModulePaths(options, path.join(suffix, dirent.name))
342-
);
351+
// Gracefully handling issues with reading directories
352+
return [];
343353
}
354+
throw error;
344355
}
345356
const childResults = await Promise.all(pendingResults);
346357
result.push(...childResults.flatMap((filePath) => filePath));
347358
return result;
348359
}
349360

361+
/**
362+
* Default package names to use when excluding packages from the search for Node-API modules.
363+
*/
364+
export const DEFAULT_EXCLUDE_PACKAGES = [
365+
"react-native-node-api", // The host package itself
366+
"react-native", // React Native core
367+
];
368+
350369
/**
351370
* Finds all dependencies of the app package and their xcframeworks.
352371
*/
353372
export async function findNodeApiModulePathsByDependency({
354373
fromPath,
355374
includeSelf,
375+
excludePackages = DEFAULT_EXCLUDE_PACKAGES,
356376
...options
357377
}: FindNodeApiModuleOptions & {
358378
includeSelf: boolean;
379+
excludePackages?: string[];
359380
}) {
360381
// Find the location of each dependency
361382
const packagePathsByName = findPackageDependencyPaths(fromPath);
@@ -368,8 +389,9 @@ export async function findNodeApiModulePathsByDependency({
368389

369390
// Find all their node api module paths
370391
const resultEntries = await Promise.all(
371-
Object.entries(packagePathsByName).map(
372-
async ([dependencyName, dependencyPath]) => {
392+
Object.entries(packagePathsByName)
393+
.filter(([name]) => !excludePackages.includes(name))
394+
.map(async ([dependencyName, dependencyPath]) => {
373395
// Make all the xcframeworks relative to the dependency path
374396
const absoluteModulePaths = await findNodeApiModulePaths({
375397
fromPath: dependencyPath,
@@ -384,8 +406,7 @@ export async function findNodeApiModulePathsByDependency({
384406
),
385407
},
386408
] as const;
387-
}
388-
)
409+
})
389410
);
390411
// Return an object by dependency name
391412
return Object.fromEntries(

packages/host/src/node/test-utils.ts

Lines changed: 19 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,23 +3,32 @@ import os from "node:os";
33
import fs from "node:fs";
44
import path from "node:path";
55

6-
export function setupTempDirectory(
7-
context: TestContext,
8-
files: Record<string, string>
9-
) {
6+
export interface FileMap {
7+
[key: string]: string | FileMap;
8+
}
9+
10+
function writeFiles(fromPath: string, files: FileMap) {
11+
for (const [filePath, content] of Object.entries(files)) {
12+
const fullPath = path.join(fromPath, filePath);
13+
fs.mkdirSync(path.dirname(fullPath), { recursive: true });
14+
if (typeof content === "string") {
15+
fs.writeFileSync(fullPath, content, "utf8");
16+
} else {
17+
writeFiles(fullPath, content);
18+
}
19+
}
20+
}
21+
22+
export function setupTempDirectory(context: TestContext, files: FileMap) {
1023
const tempDirectoryPath = fs.realpathSync(
11-
fs.mkdtempSync(path.join(os.tmpdir(), "babel-transform-test-"))
24+
fs.mkdtempSync(path.join(os.tmpdir(), "react-native-node-api-test-"))
1225
);
1326

1427
context.after(() => {
1528
fs.rmSync(tempDirectoryPath, { recursive: true, force: true });
1629
});
1730

18-
for (const [filePath, content] of Object.entries(files)) {
19-
const fullPath = path.join(tempDirectoryPath, filePath);
20-
fs.mkdirSync(path.dirname(fullPath), { recursive: true });
21-
fs.writeFileSync(fullPath, content, "utf8");
22-
}
31+
writeFiles(tempDirectoryPath, files);
2332

2433
return tempDirectoryPath;
2534
}

0 commit comments

Comments
 (0)