@jederilson/easl
v0.1.0
Published
Organized local EAS builds — build, store, list, clean and submit App Store / Play Store artifacts from your own machine.
Maintainers
Readme
easl
EAS, but local. Organized local EAS builds for Expo / React Native apps: build on your own machine, keep artifacts tidy, list them, clean them, and submit any of them to the App Store / Play Store — without paying for (or waiting on) EAS build workers.
eas build --local works great, but it drops artifacts wherever you point it and leaves choosing/tracking/submitting them up to you. easl wraps that workflow:
.builds/
ios/
preview/ myapp-ios-preview-20260528-143002.ipa
production/ myapp-ios-production-20260601-101500.ipa
android/
production/ myapp-android-production-20260601-102244.aabEvery build is timestamped and filed under platform/profile (<app> is your package.json name; a scoped @org/app becomes org-app). Submitting picks from that folder — interactively when several builds exist, or automatically with --latest.
Requirements
- Node.js ≥ 20
eas-cliavailable onPATH(global install or project devDependency)- An Expo project with an
eas.json(build profiles; asubmitprofile foreasl submit) — runeaslfrom the project root, whereeas.jsonlives - Whatever
eas build --localitself needs on your machine (Xcode for iOS, Android SDK/JDK for Android). Note that EAS local builds run on macOS/Linux only — on Windows, use WSL (easl submit/list/cleanwork on native Windows too)
Usage
Status: first release in progress — until
@jederilson/[email protected]lands on the registry, run from a checkout withnode bin/easl.mjs <command>.
The package is published as @jederilson/easl (the registry rejects bare easl as too close to existing names), but the binary it installs is plain easl — the scope never shows up in day-to-day usage.
One-off, no install:
npx @jederilson/easl build ios preview
npx @jederilson/easl listInstalled (npm i -D @jederilson/easl), the command is just easl:
npx easl build ios preview
npx easl submit android production --latest
npx easl list
npx easl clean all --keep-latestOr the equivalent colon style, if that reads better to you:
npx easl build:ios:preview
npx easl submit:android:production --latestAdd .builds/ to your .gitignore.
As package scripts
// package.json
{
"devDependencies": { "@jederilson/easl": "^0.1.0" },
"scripts": {
"build:local": "easl build",
"submit:local": "easl submit",
"builds:list": "easl list",
"builds:clean": "easl clean"
}
}yarn build:local ios preview
yarn submit:local ios production --latestCommands
easl build <ios|android> <profile>
Runs eas build --local with the given profile (must exist in eas.json → build) and saves the artifact as
.builds/<platform>/<profile>/<app>-<platform>-<profile>-<YYYYMMDD-HHMMSS>.<ext>.
Extension: iOS → .ipa; Android → the profile's explicit android.buildType if set (apk / app-bundle), else .aab for production and .apk for everything else.
easl submit <ios|android> <profile> [--latest] [--submit-profile <name>]
Submits a stored build from .builds/<platform>/<profile>/:
- one build in the folder → submits it;
- several → interactive picker (newest first), or
--latestto skip the prompt.
In scripts/CI, always pass --latest: with several stored builds and no flag, easl submit waits on the interactive prompt (and exits with an error if stdin is closed).
The eas submit profile is resolved from eas.json → submit: an explicit --submit-profile wins (passed to eas-cli as-is); otherwise a submit profile named like the build profile; otherwise the only one defined; otherwise one named production, if it exists. When none of these resolve, easl exits with an error — pass --submit-profile <name>.
easl list [platform] [profile]
Lists stored builds, newest first (★ = newest per folder).
easl clean [all | <platform> | <platform> <profile>] [flags]
Deletes stored builds. Asks for confirmation unless --yes/-y.
| Flag | Effect |
| --- | --- |
| --keep-latest | keep the newest build in each affected folder |
| --keep-prod-latest | keep ONLY the newest production build(s) in the scope, delete everything else (rejected for a non-production profile scope, where it would just wipe the folder) |
| --yes, -y | skip the confirmation prompt |
easl clean all # wipe everything
easl clean all --keep-latest # keep newest of every profile
easl clean all --keep-prod-latest # keep only the latest production build(s)
easl clean ios preview --keep-latestNon-goals
easl does not wrap cloud builds, manage credentials, or interpret eas.json beyond profile names and each profile's android.buildType (used only to name the artifact) — eas-cli remains the source of truth for all of that. It only organizes the local-build → submit loop.
