Three ways a scoped monorepo build copy came up short
A build that gets only the folders it needs is safer, until it misses one. Three real failures in our scoped monorepo copy: a vendored local package, a relative import that leaves the app folder, and code wired in by Metro or tsconfig paths, and the checks to run on your own repository.
A monorepo iOS build should not need your whole repository. Ours does not: the Ubriot worker checks out the code, then hands the build a scoped copy holding the app, the workspaces it depends on, and little else. Other apps' environment files and credentials stay out. That scoping is deliberate, and in the past week it failed in three separate ways, each time because the copy was missing something the app really did need. Here is what broke, in the order we found it.
1. The root install wanted a package nobody had copied
The first copy took only the package.json of each other workspace. That is enough for most installs, until a workspace depends on a vendored package through a file:, link: or portal: path. Yarn resolves those from disk, so the root install failed when it could not find the folder, and every monorepo iOS build for repositories that vendor a shared package failed with it.
The fix was to find local package paths from the root manifest, including resolutions and overrides, and from every workspace, following them recursively. A path that points outside the repository is ignored. Everything that is copied still goes through the same filter that drops credentials and other products' environment files.
2. The app imported a sibling folder by relative path
One app imported an analytics SDK as ../../some-sibling/sdk, a plain relative path that crosses out of the app folder into a neighbouring product. It is not a workspace dependency and it is not a local package, so nothing in a manifest mentions it. The scoped copy left it out and the iOS archive could not bundle the JavaScript.
Manifests cannot answer this question, so the worker now reads the source. It scans the app, its workspace dependencies and its local packages for import, export-from, require and dynamic import specifiers that start with a dot, and copies the targets that exist inside the repository. Test files and anything listed in an optional .ubriotignore file are skipped. When a target is a wide folder, we copy only the files that are actually imported. Links that resolve outside the repository are skipped.
import { track } from "../../some-sibling/sdk";3. A config file wired the code in, not an import
The same app resolved one of its own packages through its Metro, Babel and TypeScript path settings. No import statement spells out the real location, so the source scan found nothing. We extended the scan to read the app's metro, babel, app and tsconfig files. A string in those files adds only the file or folder it names, and a tsconfig path alias counts only when the app imports it. An environment file is never added by any of these routes, whatever a config string says.
What the scan has to promise
A scanner that reads untrusted source before a build is a small attack surface of its own. A security review of the first version found that a line of unclosed, regex-looking slashes could make the scan take quadratic time. It is linear now, with a cap on the total bytes read, a cache so repeated or missing targets cost one lookup, and a log line that says so when the file budget runs out. A scan that quietly stops is how a build ends up missing a file again.
We also keep the scan conservative on purpose. It reads comments, template strings and regex literals as text, not as imports. A path built at runtime, such as a string assembled in a loop, cannot be found by static analysis, and we do not pretend it can. For that case, check that the folder is inside the repository, and put it where a manifest or an import will name it.
Checking your own monorepo
If a build only fails on a clean checkout, ask three questions. Does any workspace depend on a package through file:, link: or portal:? Does any app import code by a relative path that leaves its own folder? Does a Metro, Babel or tsconfig setting point at a directory that no import mentions? Each one is a way for a scoped copy to be one folder short, and each one failed a real build for us this week.