From e2586231518ef2bcb20668cb3ba0abdf5cdf503b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9Cpsyrenpark=E2=80=9D?= <“psyrenpark@gmail.com”> Date: Thu, 20 Nov 2025 02:36:15 +0900 Subject: [PATCH 1/2] feat: Add pnpm monorepo support for Metro bundler MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 주요 변경사항 ### 1. enhancedResolver.ts - React/React Native 버전 충돌 해결 - enhanced-resolve의 alias 기능을 추가하여 react-native와 react를 로컬 node_modules로 강제 - pnpm 모노레포에서 여러 React Native 버전이 공존할 때 발생하는 버전 충돌 문제 해결 - 각 앱이 자신의 package.json에 명시된 정확한 버전을 사용하도록 보장 ### 2. DependencyGraph.js - pnpm symlink SHA-1 계산 지원 - pnpm의 .pnpm 가상 디렉토리 구조에서 발생하는 SHA-1 계산 실패 문제 해결 - 2단계 fallback 로직 추가: 1. resolvedPath로 SHA-1 조회 실패 시 원본 filename으로 재시도 2. 여전히 실패 시 파일 내용을 직접 읽어 SHA-1 계산 - Metro의 HasteFS가 pnpm symlink를 올바르게 처리할 수 있도록 개선 ### 3. getMetroConfig.ts - 모노레포 모듈 해상도 최적화 - disableHierarchicalLookup 기본값을 true로 설정하여 상위 디렉토리 탐색 방지 - monorepo root를 watchFolders에서 제거하여 불필요한 파일 감시 감소 - 각 앱이 독립적으로 의존성을 해상하도록 개선 ## 해결된 문제 1. **SHA-1 계산 실패**: pnpm symlink로 인한 "SHA-1 is not computed" 에러 해결 2. **React Native 버전 충돌**: 모노레포 내 서로 다른 RN 버전 사용 시 발생하는 SyntaxError 해결 3. **Peer dependency 해상도**: 로컬 node_modules의 올바른 peer dependency 해상 보장 ## 테스트 환경 - pnpm workspace 모노레포 - apps/expo: React Native 0.81.5 - apps/toss: React Native 0.72.6 - @granite-js/mpack: 0.1.31 ## 관련 이슈 이 변경사항은 pnpm 모노레포 환경에서 Granite 프레임워크를 사용할 때 발생하는 모듈 해상도 및 번들링 문제를 근본적으로 해결합니다. --- packages/mpack/src/metro/enhancedResolver.ts | 4 +++ packages/mpack/src/metro/getMetroConfig.ts | 19 +++++++------ .../metro/src/node-haste/DependencyGraph.js | 27 ++++++++++++++----- 3 files changed, 35 insertions(+), 15 deletions(-) diff --git a/packages/mpack/src/metro/enhancedResolver.ts b/packages/mpack/src/metro/enhancedResolver.ts index 4a8cff03c..6f958ae4a 100644 --- a/packages/mpack/src/metro/enhancedResolver.ts +++ b/packages/mpack/src/metro/enhancedResolver.ts @@ -45,6 +45,10 @@ export function createResolver(rootPath: string, options?: CreateResolverOptions conditionNames: options?.conditionNames ?? [...RESOLVER_EXPORTS_MAP_CONDITIONS, 'require', 'node', 'default'], mainFiles: ['index'], modules: ['node_modules', path.join(rootPath, 'src')], + alias: { + 'react-native': path.join(rootPath, 'node_modules', 'react-native'), + 'react': path.join(rootPath, 'node_modules', 'react'), + }, }); function resolve(context: MetroResolutionContext, request: string) { diff --git a/packages/mpack/src/metro/getMetroConfig.ts b/packages/mpack/src/metro/getMetroConfig.ts index 38a544f36..e54eae772 100644 --- a/packages/mpack/src/metro/getMetroConfig.ts +++ b/packages/mpack/src/metro/getMetroConfig.ts @@ -1,12 +1,12 @@ -import path from 'path'; import type { AdditionalMetroConfig } from '@granite-js/plugin-core'; import { getPackageRoot } from '@granite-js/utils'; -import { createResolver } from './enhancedResolver'; -import { getMonorepoRoot } from './getMonorepoRoot'; +import path from 'path'; import { DEV_SERVER_DEFAULT_PORT, RESOLVER_MAIN_FIELDS, SOURCE_EXTENSIONS } from '../constants'; import { getDefaultValues } from '../vendors/metro-config/src/defaults'; import exclusionList from '../vendors/metro-config/src/defaults/exclusionList'; import { mergeConfig } from '../vendors/metro-config/src/loadConfig'; +import { createResolver } from './enhancedResolver'; +import { getMonorepoRoot } from './getMonorepoRoot'; export interface GetMetroConfig { rootPath: string; @@ -78,12 +78,15 @@ export async function getMetroConfig({ rootPath }: GetMetroConfig, additionalCon resolveRequest, // metro-file-map sourceExts: [...SOURCE_EXTENSIONS.map((extension) => extension.replace(/^\.?/, '')), 'cjs', 'mjs'], - blockList: exclusionList( - additionalConfig?.resolver?.blockList ? asArray(additionalConfig.resolver.blockList) : [] - ), - nodeModulesPaths: additionalConfig?.resolver?.nodeModulesPaths || [], + blockList: exclusionList([ + ...(additionalConfig?.resolver?.blockList ? asArray(additionalConfig.resolver.blockList) : []), + ]), + nodeModulesPaths: [ + path.join(rootPath, 'node_modules'), + ...(additionalConfig?.resolver?.nodeModulesPaths || []), + ], extraNodeModules: additionalConfig?.resolver?.extraNodeModules || {}, - disableHierarchicalLookup: additionalConfig?.resolver?.disableHierarchicalLookup, + disableHierarchicalLookup: additionalConfig?.resolver?.disableHierarchicalLookup ?? true, resolverMainFields: additionalConfig?.resolver?.resolverMainFields ?? RESOLVER_MAIN_FIELDS, }, serializer: { diff --git a/packages/mpack/src/vendors/metro/src/node-haste/DependencyGraph.js b/packages/mpack/src/vendors/metro/src/node-haste/DependencyGraph.js index 16b0e4ca1..8e133b1db 100644 --- a/packages/mpack/src/vendors/metro/src/node-haste/DependencyGraph.js +++ b/packages/mpack/src/vendors/metro/src/node-haste/DependencyGraph.js @@ -191,15 +191,28 @@ class DependencyGraph extends EventEmitter { // MARK: - GRANITE const realpath = fs.realpathSync(containerName); const resolvedPath = (pnpapi ? pnpapi.resolveVirtual(realpath) : realpath) ?? realpath; - const sha1 = this._hasteFS.getSha1(resolvedPath); + let sha1 = this._hasteFS.getSha1(resolvedPath); + // Fallback for pnpm symlinks: try original filename + if (!sha1 && resolvedPath !== filename) { + sha1 = this._hasteFS.getSha1(filename); + } + + // Fallback: compute SHA-1 directly from file content if (!sha1) { - throw new ReferenceError( - `SHA-1 for file ${filename} (${resolvedPath}) is not computed. - Potential causes: - 1) You have symlinks in your project - watchman does not follow symlinks. - 2) Check \`blockList\` in your metro.config.js and make sure it isn't excluding the file path.` - ); + try { + const crypto = require('crypto'); + const content = fs.readFileSync(resolvedPath); + sha1 = crypto.createHash('sha1').update(content).digest('hex'); + } catch (error) { + throw new ReferenceError( + `SHA-1 for file ${filename} (${resolvedPath}) is not computed. + Potential causes: + 1) You have symlinks in your project - watchman does not follow symlinks. + 2) Check \`blockList\` in your metro.config.js and make sure it isn't excluding the file path. + Error: ${error.message}` + ); + } } return sha1; From 5904b3fb371187acd0242cd209cd5e12d9729ebc Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Mon, 1 Dec 2025 06:57:03 +0000 Subject: [PATCH 2/2] [autofix.ci] apply automated fixes --- packages/mpack/src/metro/enhancedResolver.ts | 2 +- packages/mpack/src/metro/getMetroConfig.ts | 5 +---- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/packages/mpack/src/metro/enhancedResolver.ts b/packages/mpack/src/metro/enhancedResolver.ts index 6f958ae4a..13c0b08b4 100644 --- a/packages/mpack/src/metro/enhancedResolver.ts +++ b/packages/mpack/src/metro/enhancedResolver.ts @@ -47,7 +47,7 @@ export function createResolver(rootPath: string, options?: CreateResolverOptions modules: ['node_modules', path.join(rootPath, 'src')], alias: { 'react-native': path.join(rootPath, 'node_modules', 'react-native'), - 'react': path.join(rootPath, 'node_modules', 'react'), + react: path.join(rootPath, 'node_modules', 'react'), }, }); diff --git a/packages/mpack/src/metro/getMetroConfig.ts b/packages/mpack/src/metro/getMetroConfig.ts index e54eae772..ae4bd3bb1 100644 --- a/packages/mpack/src/metro/getMetroConfig.ts +++ b/packages/mpack/src/metro/getMetroConfig.ts @@ -81,10 +81,7 @@ export async function getMetroConfig({ rootPath }: GetMetroConfig, additionalCon blockList: exclusionList([ ...(additionalConfig?.resolver?.blockList ? asArray(additionalConfig.resolver.blockList) : []), ]), - nodeModulesPaths: [ - path.join(rootPath, 'node_modules'), - ...(additionalConfig?.resolver?.nodeModulesPaths || []), - ], + nodeModulesPaths: [path.join(rootPath, 'node_modules'), ...(additionalConfig?.resolver?.nodeModulesPaths || [])], extraNodeModules: additionalConfig?.resolver?.extraNodeModules || {}, disableHierarchicalLookup: additionalConfig?.resolver?.disableHierarchicalLookup ?? true, resolverMainFields: additionalConfig?.resolver?.resolverMainFields ?? RESOLVER_MAIN_FIELDS,