expo: build managed Expo projects by prebuilding on the runner - #18
Merged
Merged
Conversation
A managed Expo project keeps no ios/ directory in git, so detection returned no iOS path and the runners walked straight into a missing directory. detectIOSPath now reports "Expo (managed)" with path "ios" when package.json depends on Expo and no Xcode project exists anywhere; init says the native project is generated on the runner. Nothing is generated locally, and managed projects gitignore ios/, so the working-tree snapshot stays free of it. All three runners gained an Expo prebuild step, after the node install (which provides the CLI) and before the Pods step (which reads the Podfile prebuild writes). It skips a path that already holds an Xcode project, so ejected projects are untouched, and refuses a project whose app config has no ios.bundleIdentifier: expo prebuild would otherwise prompt and hang the job until the timeout. The steps that walk the iOS path earlier - XcodeGen and the base-configuration check - now skip a directory that is not there yet. The Pods cache key hashes a Podfile.lock that a managed project does not have before the prebuild; hashFiles then returns an empty string and the key falls back to its prefix, which still restores and saves, and pod install syncs it.
isExpoProject searched package.json for the raw string "expo", so a project named expo, an "expo" script or an "expo" keyword made an unrelated Node repo look like an Expo app. Now that the string also decides whether a repo with no Xcode project anywhere is an iOS project, parse the dependency maps instead and fall back to the substring only when package.json does not parse. Check pubspec.yaml before the Expo fallback too, for the same reason the runners do: a Flutter repo that does not commit ios/ is not a managed Expo project, and the runner would detect it as Flutter and never prebuild it.
`npx expo config` printing anything but JSON made jq spill a parse error into the log before the app.json fallback silently succeeded, which reads like a failed build. Send that jq's stderr to /dev/null; the fallback and the named error still report a genuinely missing bundle identifier. Assert prebuild precedes the build step in both workflows, not just the Pods cache in ios-build.yml: the scheme detection and `pod install` both live there and need the project prebuild generates.
A Debug IPA loads its JavaScript from Metro, so a standalone Expo IPA needs ios.configuration set to Release.
…and Node version A managed Expo app that pins pnpm through `packageManager` (cherry-studio-app, for one) died in the node step: `npm install` walks a workspace protocol it does not implement and exits with EUNSUPPORTEDPROTOCOL Unsupported URL Type "workspace:". The hard-coded Node 20 was the other half of the problem — that project asks for 24.x in engines.node. All three runners now share one shell block, delimited so a test can compare the copies: the manager comes from `packageManager`, then the lockfile, then npm; pnpm and yarn arrive through corepack and bun through its installer; `expo prebuild` runs through whichever one won. The GitHub workflows resolve the Node version in a step before setup-node, because `with:` cannot choose between node-version and node-version-file on its own, and the node_modules key now hashes every lockfile and carries the manager name.
…acks it js_node_version stripped the range prefix from engines.node but took .nvmrc verbatim, so "v20.11.1" matched neither [0-9]* nor lts/* and runner.sh asked nvm for Node 22. GitHub was unaffected: there the file goes to setup-node as node-version-file. The prefix is now dropped from either source. Node 25 no longer bundles corepack and provider images may lack it, so `corepack enable || true` fell through to an unpinned `npm install -g pnpm`. Install corepack with npm first, so the packageManager pin is what runs. The stubbed install test now runs on a PATH of its own, so a manager on the developer's machine cannot stand in for a missing stub.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Builder only supported ejected Expo projects with a committed
ios/directory. This adds managed projects, the most obvious gap for anyone comparing Builder with EAS Build.builder initdetects "Expo (managed)" whenpackage.jsonlistsexpoin dependencies and no Xcode project exists anywhere. It writesios.path: iosandreactNative.expo: trueand tells the user the native project is generated on the runner. Detection parses dependencies rather than matching the word, and Flutter/KMP/native/ejected detection is unchanged and takes precedence.Expo prebuildstep inios-build.yml,ios-share.ymlandrunner.sh(Codemagic/Bitrise): after node dependencies, before Pods and scheme detection. It skips when an Xcode project already exists, resolves the bundle identifier vianpx expo configwith anapp.jsonfallback, fails with a named error when it is missing instead of hanging on a prompt, runsnpx expo prebuild --platform ios --no-installunderCI=1, and errors if no project appeared orios.pathis notios.runner.shpreviously ran an unconditional prebuild that could clobber ejected projects; it is now guarded the same way.ios/, so the working-tree snapshot naturally excludes it and the runner generates it.cmd/builder/expo_test.go;internal/workflow/expo_test.goparses the YAML, checks the step gate and ordering, and executes the extracted step scripts and the runner function against a stubbednpx.ios.configuration: Releaseproduces a standalone IPA.Test plan
go build ./... && go vet ./... && go test ./...builder initin a freshnpx create-expo-appproject detects Expo (managed)builder ios buildon that project produces an IPA; the log shows the prebuild stepios.bundleIdentifierfails with the named error