You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Pro 1.7 + public uniwind in a pnpm monorepo: Metro cannot resolve uniwind/index, then uniwind/components recurses #682
In a pnpm monorepo, one Expo app depends on Uniwind Pro ("uniwind": "npm:uniwind-pro@^1.7.0") while another app depends on public Uniwind ("uniwind": "^1.12.0"). Both packages publish as "name": "uniwind". With publicHoistPattern: "*", the public copy is hoisted to the workspace root.
That produces two Metro failures:
1. Bundling: missing main / index
Unable to resolve "uniwind" from "src/app/_layout.tsx"
The package `…/node_modules/uniwind/package.json` was successfully found. However, this package itself specifies a
`main` module field that could not be resolved (`…/node_modules/uniwind/index`).
Pro installs as a CLI stub with no exports / main until postinstall. Even after the payload is installed, package.json has an exports map (react-native → ./src/index.ts) but no top-level main or react-native field. If Metro skips exports (common when the file map cannot see the pnpm store path), it defaults main to index, which does not exist.
Related: #679 (closed as invalid — not filed with the template). Same class of failure as react/metro#1424.
2. Runtime: uniwind/components rewrite hits the public copy
withUniwindConfig rewrites react-native → uniwind/components using the importing file as originModulePath. Modules outside the Pro app (expo-router, reanimated, …) then resolve uniwind from the hoisted public copy. That copy’s own react-native imports are rewritten again → infinite recursion:
Maximum call stack size exceeded (native stack depth)
"main" has not been registered
This is the same class of bug as #353, fixed for OSS in #570 (pin originModulePath when resolving uniwind / uniwind/*). Pro 1.7 still ships the pre-#570 plugin, so the pin is missing.
require("uniwind/metro") from the Pro app can also walk past the installer stub and load the public plugin.
Steps to Reproduce
pnpm workspace with two Expo apps.
App A: "uniwind": "npm:uniwind-pro@^1.7.0".
App B: "uniwind": "^1.12.0" (public).
publicHoistPattern: "*" (or otherwise hoist public uniwind to the workspace root).
From App A, expo start / expo export --platform ios.
Bundling fails with the uniwind/index error above. If the Pro payload is present but the plugin is the public 1.12 copy (or Pro 1.7 without the origin pin), the device then crashes on launch with a native stack overflow.
Workaround that unblocked us: resolve Pro by filesystem path (<app>/node_modules/uniwind), load withUniwindConfig from that copy’s exports["./metro"], map uniwind / uniwind/* to that copy’s exports targets on disk, and pin originModulePath to App A’s package.json.
Snack or Repository Link (Optional)
Private monorepo — cannot share a public repro. Happy to extract a minimal pnpm workspace if useful.
Uniwind version
uniwind-pro 1.7.0 (app A) + public uniwind 1.12.0 (app B, hoisted)
We pinned Metro to the app’s ownnode_modules/uniwind (the Pro copy) instead of letting Node/require("uniwind/metro") walk up to a hoisted public uniwind. Drop this next to metro.config.js and wrap your Expo config with it.
metro.uniwind-pro.js:
const{ execFileSync }=require("node:child_process");constfs=require("node:fs");constpath=require("node:path");functionreadPackage(uniwindRoot){returnJSON.parse(fs.readFileSync(path.join(uniwindRoot,"package.json"),"utf8"),);}functionensureUniwindPro(projectRoot){constuniwindDir=path.resolve(projectRoot,"node_modules/uniwind");if(!fs.existsSync(uniwindDir)){thrownewError(`Uniwind Pro is not linked at ${uniwindDir}. Run your package manager from the workspace root.`,);}constuniwindRoot=fs.realpathSync(uniwindDir);// Installer stub has no `exports` until postinstall (`npx uniwind-pro` / UNIWIND_AUTH_TOKEN).if(!readPackage(uniwindRoot).exports){execFileSync(process.execPath,[path.join(uniwindRoot,"pre/postinstall/index.js")],{cwd: uniwindRoot,stdio: "inherit"},);}constuniwindPackage=readPackage(uniwindRoot);if(!uniwindPackage.exports){thrownewError(`Uniwind Pro is not installed in ${uniwindRoot}. Run \`npx uniwind-pro\` or set UNIWIND_AUTH_TOKEN.`,);}return{ uniwindRoot, uniwindPackage };}functionpickExportTarget(target,condition){while(target&&typeoftarget==="object"){target=target[condition]??target["react-native"]??target.default??target.import;}returntypeoftarget==="string" ? target : null;}functionresolveExportPath(uniwindRoot,uniwindPackage,subpath,condition){consttarget=pickExportTarget(uniwindPackage.exports[subpath],condition);if(target==null){thrownewError(`Uniwind Pro has no "${condition}" export for ${subpath} in ${uniwindRoot}.`,);}returnpath.join(uniwindRoot,target);}functionexistingFile(filePath){constwithoutExt=filePath.replace(/\.(tsx|ts|jsx|js|cjs|mjs)$/,"");for(constcandidateof[filePath,`${withoutExt}.tsx`,`${withoutExt}.ts`,`${withoutExt}.js`,path.join(filePath,"index.ts"),path.join(filePath,"index.tsx"),path.join(filePath,"index.js"),]){try{if(fs.statSync(candidate).isFile()){returnfs.realpathSync(candidate);}}catch{// try next}}returnnull;}functionresolveFromDisk(uniwindRoot,uniwindPackage,moduleName,platform){if(moduleName!=="uniwind"&&!moduleName.startsWith("uniwind/")){returnnull;}constcondition=platform==="web" ? "browser" : "react-native";constsubpath=moduleName==="uniwind" ? "." : `./${moduleName.slice("uniwind/".length)}`;constexportsField=uniwindPackage.exports;lettarget=pickExportTarget(exportsField[subpath],condition);if(target==null){for(const[key,value]ofObject.entries(exportsField)){if(!key.includes("*"))continue;const[prefix,suffix=""]=key.split("*");if(!subpath.startsWith(prefix)||!subpath.endsWith(suffix))continue;constmatched=subpath.slice(prefix.length,suffix ? subpath.length-suffix.length : undefined,);constpicked=pickExportTarget(value,condition);if(picked){target=picked.replaceAll("*",matched);break;}}}if(target==null)returnnull;constfilePath=existingFile(path.join(uniwindRoot,target));if(filePath==null)returnnull;return{type: "sourceFile", filePath };}functionwithUniwindResolve(resolveRequest,{ uniwindRoot, uniwindPackage, uniwindOrigin },){return(context,moduleName,platform)=>{constfromDisk=resolveFromDisk(uniwindRoot,uniwindPackage,moduleName,platform,);if(fromDisk)returnfromDisk;constresolve=resolveRequest??context.resolveRequest;if(moduleName==="uniwind"||moduleName.startsWith("uniwind/")){returnresolve({ ...context,originModulePath: uniwindOrigin},moduleName,platform,);}returnresolve(context,moduleName,platform);};}/** * Pin Metro to this app's uniwind-pro copy so a hoisted public `uniwind` * (same package name) cannot be loaded instead. * * - Loads `withUniwindConfig` from this copy's `exports["./metro"]` * - Maps `uniwind` / `uniwind/*` to that copy's `exports` targets on disk * (avoids Metro's missing `main` → `index` fallback) * - Pins `originModulePath` as a fallback so `uniwind/components` redirects * from files outside this app never hit the public copy (#353 / #570) */functionwithPinnedUniwind(config,uniwindOptions={}){constprojectRoot=uniwindOptions.projectRoot??config.projectRoot??path.dirname(__filename);const{ uniwindRoot, uniwindPackage }=ensureUniwindPro(projectRoot);constuniwindOrigin=path.join(projectRoot,"package.json");constpin={ uniwindRoot, uniwindPackage, uniwindOrigin };const{ withUniwindConfig }=require(resolveExportPath(uniwindRoot,uniwindPackage,"./metro","require"),);config.watchFolders=[
...newSet([...(config.watchFolders??[]),uniwindRoot]),];config.resolver.resolveRequest=withUniwindResolve(config.resolver.resolveRequest,pin,);const{projectRoot: _projectRoot, ...pluginOptions}=uniwindOptions;constuniwindConfig=withUniwindConfig(config,pluginOptions);uniwindConfig.resolver.resolveRequest=withUniwindResolve(uniwindConfig.resolver.resolveRequest,pin,);returnuniwindConfig;}module.exports={ withPinnedUniwind };
metro.config.js:
const{ getDefaultConfig }=require("expo/metro-config");const{ withPinnedUniwind }=require("./metro.uniwind-pro");constconfig=getDefaultConfig(__dirname);module.exports=withPinnedUniwind(config,{cssEntryFile: "./global.css",// relative to the app rootprojectRoot: __dirname,// required if this helper is not next to metro.config.js});
Restart Metro with a cache reset after adding it (expo start -c).
Opened an OSS PR for the missing main / react-native fallbacks: #683
That covers the Metro …/uniwind/index error when exports is skipped. The origin pin for uniwind/components is already on OSS main via #570 — Pro 1.7 still needs that backport on the Pro payload (not in this repo).
The origin pin for uniwind/components is already on OSS main via #570 — Pro 1.7 still needs that backport on the Pro payload (not in this repo).
The origin pin is applied for both oss and pro versions. I wasn't able to reproduce the issue from your instructions alone, please extract minimal reproduction repo.
Expo enables autolinking module resolution by default in monorepos, and it resolves native packages by name from inside the package's own folder. pnpm stores the aliased package in a folder named uniwind-pro, so looking up uniwind from there walks up to the hoisted public copy. The Pro app then bundles the public uniwind instead of Pro, and its react-native imports get rewritten back to itself. That's the Maximum call stack size exceeded and "main" has not been registered crash on launch.
I have a fix that will be published with next pro version, until then you can:
Set "experiments": { "autolinkingModuleResolution": false } in app.json
Keep the public uniwind from being hoisted to the workspace root
Activity
antoinerousseau commentedon Sep 21, 2026
Workaround
We pinned Metro to the app’s own
node_modules/uniwind(the Pro copy) instead of letting Node/require("uniwind/metro")walk up to a hoisted publicuniwind. Drop this next tometro.config.jsand wrap your Expo config with it.metro.uniwind-pro.js:metro.config.js:Restart Metro with a cache reset after adding it (
expo start -c).antoinerousseau commentedon Sep 21, 2026
Opened an OSS PR for the missing
main/react-nativefallbacks: #683That covers the Metro
…/uniwind/indexerror whenexportsis skipped. The origin pin foruniwind/componentsis already on OSSmainvia #570 — Pro 1.7 still needs that backport on the Pro payload (not in this repo).Brentlok commentedon Sep 24, 2026
The origin pin is applied for both oss and pro versions. I wasn't able to reproduce the issue from your instructions alone, please extract minimal reproduction repo.
jpudysz commentedon Sep 28, 2026
I was able to reproduce this.
Expo enables autolinking module resolution by default in monorepos, and it resolves native packages by name from inside the package's own folder. pnpm stores the aliased package in a folder named
uniwind-pro, so looking upuniwindfrom there walks up to the hoisted public copy. The Pro app then bundles the publicuniwindinstead of Pro, and itsreact-nativeimports get rewritten back to itself. That's theMaximum call stack size exceededand"main" has not been registeredcrash on launch.I have a fix that will be published with next pro version, until then you can:
"experiments": { "autolinkingModuleResolution": false }inapp.jsonuniwindfrom being hoisted to the workspace rootgithub-actions commentedon Oct 1, 2026
New release is published: https://docs.uniwind.dev/pro/changelog