From f31d04957e1166fc490a4db440560ac8ca5198e5 Mon Sep 17 00:00:00 2001 From: Gabriel Donadel Date: Mon, 14 Sep 2026 14:59:56 +0200 Subject: [PATCH] fix(codegen): skip node_modules and symlinks when crawling for components --- .../generate-artifacts-executor-test.js | 70 ++++++++++++++++++- .../generateRCTThirdPartyComponents.js | 22 ++++-- 2 files changed, 85 insertions(+), 7 deletions(-) diff --git a/packages/react-native/scripts/codegen/__tests__/generate-artifacts-executor-test.js b/packages/react-native/scripts/codegen/__tests__/generate-artifacts-executor-test.js index 6a75e87fae1a..ea1aca23327f 100644 --- a/packages/react-native/scripts/codegen/__tests__/generate-artifacts-executor-test.js +++ b/packages/react-native/scripts/codegen/__tests__/generate-artifacts-executor-test.js @@ -553,8 +553,9 @@ describe('findFilesWithExtension', () => { return []; }, existsSync: () => true, - statSync: () => ({ + lstatSync: () => ({ isDirectory: () => false, + isSymbolicLink: () => false, }), readFileSync: () => packageJson, })); @@ -586,11 +587,12 @@ describe('findFilesWithExtension', () => { return []; }, existsSync: () => true, - statSync: filePath => ({ + lstatSync: filePath => ({ isDirectory: () => filePath === pnpmFolder || filePath === packageFolder || filePath === path.join(targetFolder, '.hidden'), + isSymbolicLink: () => false, }), readFileSync: () => packageJson, })); @@ -619,8 +621,9 @@ describe('findFilesWithExtension', () => { return []; }, existsSync: () => true, - statSync: filePath => ({ + lstatSync: filePath => ({ isDirectory: () => filePath === path.join(targetFolder, 'Components'), + isSymbolicLink: () => false, }), readFileSync: () => packageJson, })); @@ -635,6 +638,67 @@ describe('findFilesWithExtension', () => { path.join(targetFolder, 'Components', 'MyComponent.mm'), ]); }); + + it('skips nested node_modules folders', () => { + const targetFolder = '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/project/my-library'; + const nodeModules = path.join(targetFolder, 'node_modules'); + + jest.mock('node:fs', () => ({ + readdirSync: dirPath => { + if (dirPath === targetFolder) { + return ['node_modules', 'Component.mm']; + } + if (dirPath === nodeModules) { + return ['Dependency.mm']; + } + return []; + }, + existsSync: () => true, + lstatSync: filePath => ({ + isDirectory: () => filePath === nodeModules, + isSymbolicLink: () => false, + }), + readFileSync: () => packageJson, + })); + + const { + findFilesWithExtension: findFiles, + } = require('../generate-artifacts-executor/generateRCTThirdPartyComponents'); + + const result = findFiles(targetFolder, '.mm'); + expect(result).toEqual([path.join(targetFolder, 'Component.mm')]); + }); + + it('does not follow symlinked folders', () => { + const targetFolder = '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/project/my-library'; + const symlinkedFolder = path.join(targetFolder, 'linked'); + + jest.mock('node:fs', () => ({ + readdirSync: dirPath => { + if (dirPath === targetFolder) { + return ['linked', 'Component.mm']; + } + // A symlink pointing back at its parent: following it never terminates. + if (dirPath === symlinkedFolder) { + return ['linked', 'Component.mm']; + } + return []; + }, + existsSync: () => true, + lstatSync: filePath => ({ + isDirectory: () => filePath.endsWith('linked'), + isSymbolicLink: () => filePath.endsWith('linked'), + }), + readFileSync: () => packageJson, + })); + + const { + findFilesWithExtension: findFiles, + } = require('../generate-artifacts-executor/generateRCTThirdPartyComponents'); + + const result = findFiles(targetFolder, '.mm'); + expect(result).toEqual([path.join(targetFolder, 'Component.mm')]); + }); }); describe('generateSchemaInfos', () => { diff --git a/packages/react-native/scripts/codegen/generate-artifacts-executor/generateRCTThirdPartyComponents.js b/packages/react-native/scripts/codegen/generate-artifacts-executor/generateRCTThirdPartyComponents.js index 03abb392090f..84857672fa0f 100644 --- a/packages/react-native/scripts/codegen/generate-artifacts-executor/generateRCTThirdPartyComponents.js +++ b/packages/react-native/scripts/codegen/generate-artifacts-executor/generateRCTThirdPartyComponents.js @@ -177,10 +177,24 @@ function findFilesWithExtension( return null; } - if ( - fs.existsSync(absolutePath) && - fs.statSync(absolutePath).isDirectory() - ) { + // A library's own sources never live in its dependencies, and crawling them + // is what makes this walk explode on large projects. + if (file === 'node_modules') { + return null; + } + + if (!fs.existsSync(absolutePath)) { + return null; + } + + // `lstatSync` does not resolve symlinks: following them can loop forever, + // e.g. workspace packages that link into each other under pnpm. + const stats = fs.lstatSync(absolutePath); + if (stats.isSymbolicLink()) { + return null; + } + + if (stats.isDirectory()) { files.push(...findFilesWithExtension(absolutePath, extension)); } else if (file.endsWith(extension)) { files.push(absolutePath);