mobile-app-builder 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +191 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +113 -0
- package/dist/commands/buildAndroid.d.ts +15 -0
- package/dist/commands/buildAndroid.js +160 -0
- package/dist/commands/buildIos.d.ts +1 -0
- package/dist/commands/buildIos.js +11 -0
- package/dist/commands/doctor.d.ts +1 -0
- package/dist/commands/doctor.js +28 -0
- package/dist/commands/sign.d.ts +10 -0
- package/dist/commands/sign.js +185 -0
- package/dist/commands/upload.d.ts +10 -0
- package/dist/commands/upload.js +114 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +27 -0
- package/dist/utils/config.d.ts +7 -0
- package/dist/utils/config.js +175 -0
- package/dist/utils/dependencies.d.ts +13 -0
- package/dist/utils/dependencies.js +388 -0
- package/dist/utils/github.d.ts +5 -0
- package/dist/utils/github.js +216 -0
- package/dist/utils/gradle.d.ts +4 -0
- package/dist/utils/gradle.js +76 -0
- package/dist/utils/project.d.ts +22 -0
- package/dist/utils/project.js +143 -0
- package/dist/utils/signing.d.ts +11 -0
- package/dist/utils/signing.js +311 -0
- package/package.json +62 -0
package/README.md
ADDED
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
# Mobile App Builder (`mobile-app-builder`)
|
|
2
|
+
|
|
3
|
+
[](https://badge.fury.io/js/mobile-app-builder)
|
|
4
|
+
[](https://www.npmjs.com/package/mobile-app-builder)
|
|
5
|
+
[](https://opensource.org/licenses/MIT)
|
|
6
|
+
|
|
7
|
+
A cross-platform CLI builder and signer for **Expo** (Managed & Bare), **Native React Native**, and **Android** mobile projects. It automates bootstrapping dependencies, building, signing with keystores, and publishing applications.
|
|
8
|
+
|
|
9
|
+
## Table of Contents
|
|
10
|
+
|
|
11
|
+
- [Introduction](#introduction)
|
|
12
|
+
- [Features](#features)
|
|
13
|
+
- [Prerequisites](#prerequisites)
|
|
14
|
+
- [Installation](#installation)
|
|
15
|
+
- [Usage](#usage)
|
|
16
|
+
- [Command Options](#command-options)
|
|
17
|
+
- [Standalone Artifact Signing](#standalone-artifact-signing)
|
|
18
|
+
- [Build Profiles](#build-profiles)
|
|
19
|
+
- [Workflow Steps](#workflow-steps)
|
|
20
|
+
- [Development](#development)
|
|
21
|
+
- [License](#license)
|
|
22
|
+
|
|
23
|
+
## Introduction
|
|
24
|
+
|
|
25
|
+
`mobile-app-builder` is an all-in-one CLI tool designed to streamline mobile builds for both **Expo** and **Native (Bare) React Native** projects. Instead of wrestling with complex cloud build configurations or manual Gradle signing setups, this tool automatically detects your project type, manages Android configurations safely, compiles APKs or AABs via Gradle, signs them with production or custom keystores, and optionally publishes to GitHub Releases or Google Play Store.
|
|
26
|
+
|
|
27
|
+
## Features
|
|
28
|
+
|
|
29
|
+
- **Dual Project Support**: Works seamlessly on Expo projects (Managed CNG and Bare) as well as Native (Bare) React Native projects.
|
|
30
|
+
- **Permanent Android Folder Safety**: Automatically detects Native/Bare projects and **never** deletes or wipes your source-controlled `android/` folder.
|
|
31
|
+
- **Automated Native Generation**: Automatically runs native prebuild only for Managed Expo projects where the `android/` directory is ephemeral.
|
|
32
|
+
- **Universal Signing Pipeline**: Strips prior signatures and signs `.apk` and `.aab` artifacts using `apksigner` and `jarsigner`. Generates release keystores interactively and saves configurations to `.env.local`.
|
|
33
|
+
- **Dynamic Configuration & Versioning**: Automatically syncs version codes and version names directly into `package.json`, `app.json`, and `android/app/build.gradle`.
|
|
34
|
+
- **Multiple Build Profiles**: Supports `dev` (Debug APK), `preview` (Release APK), and `prod` (Release AAB).
|
|
35
|
+
- **Environment Management**: Inject specific `.env` files for different build flavors.
|
|
36
|
+
- **Publishing & CI Integration**: Generates GitHub Actions workflows (for production AABs), creates GitHub Releases, and triggers Play Store rollouts.
|
|
37
|
+
|
|
38
|
+
## Prerequisites
|
|
39
|
+
|
|
40
|
+
Ensure you have the following installed on your machine (the `doctor` command can verify or install missing dependencies for you):
|
|
41
|
+
|
|
42
|
+
- [Node.js](https://nodejs.org/) (>= 22.0.0)
|
|
43
|
+
- [Java Development Kit (JDK 21)](https://adoptium.net/)
|
|
44
|
+
- [Android SDK](https://developer.android.com/studio) (Command Line Tools, Platform Tools, Build Tools)
|
|
45
|
+
- [Git](https://git-scm.com/)
|
|
46
|
+
- [GitHub CLI (gh)](https://cli.github.com/) (Required if using GitHub Release / Publish flags)
|
|
47
|
+
|
|
48
|
+
## Installation
|
|
49
|
+
|
|
50
|
+
You can install `mobile-app-builder` directly from npm, or use it locally.
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
# Global installation
|
|
54
|
+
npm install -g mobile-app-builder
|
|
55
|
+
|
|
56
|
+
# Local installation in a React Native or Expo project
|
|
57
|
+
npm install --save-dev mobile-app-builder
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## Usage
|
|
61
|
+
|
|
62
|
+
Navigate to your React Native or Expo project directory and run the command using `mobile-builder` (or `mobile-app-builder`):
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
# Check environment and dependencies
|
|
66
|
+
npx mobile-builder doctor
|
|
67
|
+
|
|
68
|
+
# Build Android app
|
|
69
|
+
npx mobile-builder android [options]
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### Examples
|
|
73
|
+
|
|
74
|
+
**Build a development APK:**
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
npx mobile-builder android --dev
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
**Build a release preview APK:**
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
npx mobile-builder android --preview
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
**Build a production AAB and publish:**
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
npx mobile-builder android --prod --publish --track production --notes "Production release"
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
**Test the build pipeline without running Gradle:**
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
npx mobile-builder android --preview --dry-run
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
## Standalone Artifact Signing
|
|
99
|
+
|
|
100
|
+
Already built an APK or AAB? You can sign or re-sign it directly with:
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
# Signs the latest AAB or APK found in ./build or current directory
|
|
104
|
+
npx mobile-builder sign
|
|
105
|
+
|
|
106
|
+
# Sign a specific artifact with a specified keystore
|
|
107
|
+
npx mobile-builder sign ./build/my-app.aab --keystore ./release.jks --alias my-key
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
## Command Options
|
|
111
|
+
|
|
112
|
+
| Option | Description |
|
|
113
|
+
| ----------------- | ------------------------------------------------------------------------- |
|
|
114
|
+
| `--dev` | Build a Development APK (`assembleDebug`, preserves cache) |
|
|
115
|
+
| `--preview` | Build a Preview APK (`assembleRelease`) |
|
|
116
|
+
| `--prod` | Build a Production AAB (`bundleRelease`) |
|
|
117
|
+
| `--clean` | Forces clean regeneration (Managed Expo only) |
|
|
118
|
+
| `--bump` | Explicitly bump patch version in `package.json` |
|
|
119
|
+
| `--no-bump` | Skip bumping patch version in `package.json` |
|
|
120
|
+
| `--publish` | Publish to Google Play Store (via GitHub Actions workflow for `--prod`) |
|
|
121
|
+
| `--track <track>` | Google Play Store track (e.g., `production`, `beta`, `alpha`, `internal`) |
|
|
122
|
+
| `--notes <notes>` | Release notes for GitHub Release |
|
|
123
|
+
| `--dry-run` | Run the full script but skip Gradle build, signing, and publishing |
|
|
124
|
+
| `--env <file>` | Path to a specific environment file to load (e.g., `.env.production`) |
|
|
125
|
+
| `--cleanup` | Force cleanup of ephemeral `android/` directory (Managed Expo only) |
|
|
126
|
+
| `--no-cleanup` | Keep the generated `android/` folder after build |
|
|
127
|
+
|
|
128
|
+
## Build Profiles
|
|
129
|
+
|
|
130
|
+
The builder automatically maps profiles to the correct build types and artifact formats:
|
|
131
|
+
|
|
132
|
+
1. **Dev (`--dev`)**
|
|
133
|
+
- **Profile Name**: `development`
|
|
134
|
+
- **Build Task**: `assembleDebug`
|
|
135
|
+
- **Output**: `app-debug.apk`
|
|
136
|
+
- **Versioning**: Preserves current version string and local `versionCode`
|
|
137
|
+
- **GitHub Release**: Marked as **Pre-release**
|
|
138
|
+
- **Speed**: Preserves `android/` folder and Gradle daemon for fast incremental rebuilds
|
|
139
|
+
|
|
140
|
+
2. **Preview (`--preview`)**
|
|
141
|
+
- **Profile Name**: `preview`
|
|
142
|
+
- **Build Task**: `assembleRelease`
|
|
143
|
+
- **Output**: `app-release.apk`
|
|
144
|
+
- **Signing**: Automatically signed with release keystore
|
|
145
|
+
- **GitHub Release**: Marked as **Pre-release**
|
|
146
|
+
|
|
147
|
+
3. **Prod (`--prod`)**
|
|
148
|
+
- **Profile Name**: `production`
|
|
149
|
+
- **Build Task**: `bundleRelease`
|
|
150
|
+
- **Output**: `app-release.aab`
|
|
151
|
+
- **Versioning**: Bumps patch version and increments `versionCode`
|
|
152
|
+
- **Signing**: Strips prior signatures and signs AAB via `jarsigner`
|
|
153
|
+
- **GitHub Release**: Marked as official **Latest Release**
|
|
154
|
+
- **CI/CD**: Generates `.github/workflows/publish.yml` and triggers Play Store rollout if `--publish` is specified
|
|
155
|
+
|
|
156
|
+
## Workflow Steps
|
|
157
|
+
|
|
158
|
+
When you run a build, the CLI automatically performs the following steps:
|
|
159
|
+
|
|
160
|
+
1. **Project Detection**: Detects whether the project is Expo (Managed or Bare) or Native React Native.
|
|
161
|
+
2. **Bootstrapping**: Validates system dependencies (Node, Java 21, Android SDK, Git, gh).
|
|
162
|
+
3. **Version Management**: Bumps `package.json` (for prod or if `--bump` is set) and syncs `versionCode` into Gradle or EAS.
|
|
163
|
+
4. **Native Generation (Expo Managed only)**: Generates Android project files (`expo prebuild`). Skipped for Native React Native and Bare projects.
|
|
164
|
+
5. **Gradle Patching**: Injects memory optimizations, caching, daemon, and incremental settings into `gradle.properties`.
|
|
165
|
+
6. **Gradle Build**: Runs the Gradle wrapper with `--build-cache`, `--parallel`, and `--configure-on-demand`.
|
|
166
|
+
7. **Signing**: Strips any previous signatures and signs the artifact with keystore credentials (`apksigner` for APK, `jarsigner` for AAB).
|
|
167
|
+
8. **Copying**: Moves the finished `.apk` or `.aab` to the `build/` directory in the root.
|
|
168
|
+
9. **GitHub Release**: Automatically creates/updates a GitHub Release (tagged as **Pre-release** for dev/preview). Skips gracefully if remote is unconfigured.
|
|
169
|
+
10. **Play Store Rollout (Optional)**: If `--publish` is passed with `--prod`, triggers the Google Play Store deployment workflow.
|
|
170
|
+
11. **Cleanup**: Cleans up ephemeral `android/` only on Managed Expo builds, while strictly preserving permanent native source code.
|
|
171
|
+
|
|
172
|
+
## Development
|
|
173
|
+
|
|
174
|
+
```bash
|
|
175
|
+
# Clone the repository
|
|
176
|
+
git clone https://github.com/georgechitechi/mobile-app-builder.git
|
|
177
|
+
cd mobile-app-builder
|
|
178
|
+
|
|
179
|
+
# Install dependencies
|
|
180
|
+
npm install
|
|
181
|
+
|
|
182
|
+
# Watch mode for TypeScript
|
|
183
|
+
npm run dev
|
|
184
|
+
|
|
185
|
+
# Build the final dist
|
|
186
|
+
npm run build
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
## License
|
|
190
|
+
|
|
191
|
+
MIT License. See [LICENSE](LICENSE) for more details.
|
package/dist/cli.d.ts
ADDED
package/dist/cli.js
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
4
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
5
|
+
};
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
const dotenv_1 = require("dotenv");
|
|
8
|
+
const fs_1 = __importDefault(require("fs"));
|
|
9
|
+
const path_1 = __importDefault(require("path"));
|
|
10
|
+
// Load .env and .env.local from the current working directory
|
|
11
|
+
const cwd = process.cwd();
|
|
12
|
+
if (fs_1.default.existsSync(path_1.default.join(cwd, ".env"))) {
|
|
13
|
+
(0, dotenv_1.config)({ path: path_1.default.join(cwd, ".env"), quiet: true });
|
|
14
|
+
}
|
|
15
|
+
if (fs_1.default.existsSync(path_1.default.join(cwd, ".env.local"))) {
|
|
16
|
+
(0, dotenv_1.config)({ path: path_1.default.join(cwd, ".env.local"), quiet: true });
|
|
17
|
+
}
|
|
18
|
+
const commander_1 = require("commander");
|
|
19
|
+
const buildAndroid_js_1 = require("./commands/buildAndroid.js");
|
|
20
|
+
const doctor_js_1 = require("./commands/doctor.js");
|
|
21
|
+
const buildIos_js_1 = require("./commands/buildIos.js");
|
|
22
|
+
const sign_js_1 = require("./commands/sign.js");
|
|
23
|
+
const upload_js_1 = require("./commands/upload.js");
|
|
24
|
+
const pkgJsonPath = path_1.default.resolve(__dirname, "../package.json");
|
|
25
|
+
let version = "0.0.1";
|
|
26
|
+
try {
|
|
27
|
+
const pkg = JSON.parse(fs_1.default.readFileSync(pkgJsonPath, "utf-8"));
|
|
28
|
+
if (pkg.version) {
|
|
29
|
+
version = pkg.version;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
catch { }
|
|
33
|
+
commander_1.program
|
|
34
|
+
.name("mobile-builder")
|
|
35
|
+
.description("Cross-platform CLI builder and signer for mobile applications (Expo, React Native, and Android projects)")
|
|
36
|
+
.version(version);
|
|
37
|
+
commander_1.program
|
|
38
|
+
.command("doctor")
|
|
39
|
+
.aliases(["doc", "health"])
|
|
40
|
+
.description("Check dependencies and bootstrap environment")
|
|
41
|
+
.action(() => {
|
|
42
|
+
(0, doctor_js_1.doctor)().catch((err) => {
|
|
43
|
+
console.error(err);
|
|
44
|
+
process.exit(1);
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
commander_1.program
|
|
48
|
+
.command("android")
|
|
49
|
+
.aliases(["build:android", "ba"])
|
|
50
|
+
.description("Build Android app (APK or AAB)")
|
|
51
|
+
.option("--dev", "Build development profile")
|
|
52
|
+
.option("--preview", "Build preview profile")
|
|
53
|
+
.option("--prod", "Build production profile (AAB)")
|
|
54
|
+
.option("--clean", "Clean before building")
|
|
55
|
+
.option("--bump", "Explicitly bump patch version in package.json")
|
|
56
|
+
.option("--no-bump", "Skip bumping patch version in package.json")
|
|
57
|
+
.option("--publish", "Publish to Play Store")
|
|
58
|
+
.option("--track <track>", "Play Store Track", "alpha")
|
|
59
|
+
.option("--notes <notes>", "Release Notes")
|
|
60
|
+
.option("--dry-run", "Validate environment without running Gradle")
|
|
61
|
+
.option("--env <file>", "Path to .env file to inject")
|
|
62
|
+
.option("--cleanup", "Force cleaning up the android folder after build")
|
|
63
|
+
.option("--no-cleanup", "Skip cleaning up the android folder after build")
|
|
64
|
+
.action((options) => {
|
|
65
|
+
(0, buildAndroid_js_1.buildAndroid)(options).catch((err) => {
|
|
66
|
+
console.error(err);
|
|
67
|
+
process.exit(1);
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
commander_1.program
|
|
71
|
+
.command("sign [file]")
|
|
72
|
+
.aliases(["artifact:sign", "resign"])
|
|
73
|
+
.description("Sign an existing AAB or APK while stripping any prior signatures")
|
|
74
|
+
.option("-f, --file <path>", "Path to the AAB or APK file")
|
|
75
|
+
.option("-k, --keystore <path>", "Keystore file path")
|
|
76
|
+
.option("-a, --alias <alias>", "Keystore key alias")
|
|
77
|
+
.option("-p, --storepass <password>", "Keystore store password")
|
|
78
|
+
.option("--keypass <password>", "Key password")
|
|
79
|
+
.option("--no-verify", "Skip verifying signature after signing")
|
|
80
|
+
.action((file, options) => {
|
|
81
|
+
(0, sign_js_1.signCommand)(file, options).catch((err) => {
|
|
82
|
+
console.error(err);
|
|
83
|
+
process.exit(1);
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
commander_1.program
|
|
87
|
+
.command("upload [file]")
|
|
88
|
+
.aliases(["release:upload", "up"])
|
|
89
|
+
.description("Upload an existing AAB or APK to GitHub Releases")
|
|
90
|
+
.option("-f, --file <path>", "Path to the AAB or APK file")
|
|
91
|
+
.option("-t, --tag <tag>", "GitHub release tag (e.g. v1.0.0 or v1.0.0-prod)")
|
|
92
|
+
.option("--title <title>", "Release title")
|
|
93
|
+
.option("-n, --notes <notes>", "Release notes")
|
|
94
|
+
.option("--publish", "Trigger Play Store deployment workflow after upload (AAB only)")
|
|
95
|
+
.option("--track <track>", "Play Store Track (alpha, beta, production, internal)", "alpha")
|
|
96
|
+
.option("--prerelease", "Mark GitHub release as pre-release")
|
|
97
|
+
.action((file, options) => {
|
|
98
|
+
(0, upload_js_1.uploadCommand)(file, options).catch((err) => {
|
|
99
|
+
console.error(err);
|
|
100
|
+
process.exit(1);
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
commander_1.program
|
|
104
|
+
.command("ios")
|
|
105
|
+
.aliases(["build:ios", "bi"])
|
|
106
|
+
.description("Build iOS app (coming soon)")
|
|
107
|
+
.action(() => {
|
|
108
|
+
(0, buildIos_js_1.buildIos)().catch((err) => {
|
|
109
|
+
console.error(err);
|
|
110
|
+
process.exit(1);
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
commander_1.program.parse(process.argv);
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export interface BuildAndroidOptions {
|
|
2
|
+
dev?: boolean;
|
|
3
|
+
preview?: boolean;
|
|
4
|
+
prod?: boolean;
|
|
5
|
+
clean?: boolean;
|
|
6
|
+
bump?: boolean;
|
|
7
|
+
noBump?: boolean;
|
|
8
|
+
publish?: boolean;
|
|
9
|
+
track: string;
|
|
10
|
+
notes?: string;
|
|
11
|
+
dryRun?: boolean;
|
|
12
|
+
env?: string;
|
|
13
|
+
cleanup?: boolean;
|
|
14
|
+
}
|
|
15
|
+
export declare function buildAndroid(options: BuildAndroidOptions): Promise<void>;
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.buildAndroid = buildAndroid;
|
|
7
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
8
|
+
const path_1 = __importDefault(require("path"));
|
|
9
|
+
const fs_extra_1 = __importDefault(require("fs-extra"));
|
|
10
|
+
const execa_1 = require("execa");
|
|
11
|
+
const dotenv_1 = __importDefault(require("dotenv"));
|
|
12
|
+
const dependencies_js_1 = require("../utils/dependencies.js");
|
|
13
|
+
const config_js_1 = require("../utils/config.js");
|
|
14
|
+
const gradle_js_1 = require("../utils/gradle.js");
|
|
15
|
+
const signing_js_1 = require("../utils/signing.js");
|
|
16
|
+
const github_js_1 = require("../utils/github.js");
|
|
17
|
+
const project_js_1 = require("../utils/project.js");
|
|
18
|
+
async function buildAndroid(options) {
|
|
19
|
+
const projectInfo = await (0, dependencies_js_1.ensureProjectType)();
|
|
20
|
+
console.log(chalk_1.default.cyan("Bootstrapping drop-in tool architecture..."));
|
|
21
|
+
if (options.env) {
|
|
22
|
+
console.log(chalk_1.default.cyan(`Loading environment variables from ${options.env}...`));
|
|
23
|
+
dotenv_1.default.config({
|
|
24
|
+
path: path_1.default.resolve(process.cwd(), options.env),
|
|
25
|
+
quiet: true,
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
const profile = options.dev ? "dev" : options.prod ? "prod" : "preview";
|
|
29
|
+
const fullProfileName = options.dev
|
|
30
|
+
? "development"
|
|
31
|
+
: options.prod
|
|
32
|
+
? "production"
|
|
33
|
+
: "preview";
|
|
34
|
+
const isAab = profile === "prod";
|
|
35
|
+
const buildType = profile === "dev" ? "Debug" : "Release";
|
|
36
|
+
const assembleTask = isAab
|
|
37
|
+
? "bundleRelease"
|
|
38
|
+
: profile === "dev"
|
|
39
|
+
? "assembleDebug"
|
|
40
|
+
: "assembleRelease";
|
|
41
|
+
const apkDir = profile === "dev" ? "debug" : "release";
|
|
42
|
+
const ext = isAab ? "aab" : "apk";
|
|
43
|
+
process.env.NODE_ENV = isAab ? "production" : "development";
|
|
44
|
+
process.env.APP_VARIANT = fullProfileName;
|
|
45
|
+
if (projectInfo.type === "expo") {
|
|
46
|
+
process.env.EAS_BUILD_PROFILE = fullProfileName;
|
|
47
|
+
}
|
|
48
|
+
try {
|
|
49
|
+
await (0, dependencies_js_1.ensureGit)();
|
|
50
|
+
await (0, dependencies_js_1.ensureNode)(22);
|
|
51
|
+
await (0, dependencies_js_1.ensureGitHubCLI)();
|
|
52
|
+
await (0, dependencies_js_1.ensureJava)();
|
|
53
|
+
await (0, dependencies_js_1.ensureAndroidSdk)();
|
|
54
|
+
const shouldBump = options.bump === true || (profile === "prod" && options.noBump !== true);
|
|
55
|
+
await (0, project_js_1.syncProjectVersionConfig)(shouldBump, projectInfo.type);
|
|
56
|
+
const currentVersionCode = await (0, config_js_1.syncVersionCode)(fullProfileName, shouldBump, projectInfo.type);
|
|
57
|
+
const { appName, appVersion, appPackageName } = await (0, config_js_1.getAppConfigInfo)(projectInfo.type);
|
|
58
|
+
if (!appPackageName) {
|
|
59
|
+
console.log(chalk_1.default.red("Error: Could not dynamically determine Android package name / applicationId!"));
|
|
60
|
+
process.exit(1);
|
|
61
|
+
}
|
|
62
|
+
const outputName = `${appName}-v${appVersion}-${profile}.${ext}`;
|
|
63
|
+
if (isAab) {
|
|
64
|
+
await (0, github_js_1.generatePublishWorkflow)(appPackageName);
|
|
65
|
+
}
|
|
66
|
+
if (projectInfo.isManaged) {
|
|
67
|
+
await (0, config_js_1.generateNativeProject)(!!options.clean);
|
|
68
|
+
}
|
|
69
|
+
else {
|
|
70
|
+
console.log(chalk_1.default.gray(`\nStep 04: Using existing native android project (${projectInfo.type === "expo" ? "Bare Expo" : "Native React Native"}). Skipping generation.`));
|
|
71
|
+
if (!(await fs_extra_1.default.pathExists(path_1.default.join(process.cwd(), "android")))) {
|
|
72
|
+
console.log(chalk_1.default.red("Error: 'android' directory not found in project root!"));
|
|
73
|
+
process.exit(1);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
await (0, gradle_js_1.patchGradleProperties)();
|
|
77
|
+
await (0, gradle_js_1.syncBuildGradle)(appVersion, currentVersionCode);
|
|
78
|
+
console.log(chalk_1.default.cyan(`\nStep 06: Building ${buildType} ${ext} (${assembleTask})...`));
|
|
79
|
+
if (options.dryRun) {
|
|
80
|
+
console.log(chalk_1.default.yellow("\nDry run requested. Skipping Gradle build, signing, and publishing."));
|
|
81
|
+
console.log(chalk_1.default.green("\nDry run completed successfully!"));
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
await (0, gradle_js_1.runGradleBuild)(assembleTask);
|
|
85
|
+
console.log(chalk_1.default.cyan(`\nStep 07: Copying ${ext} to build folder...`));
|
|
86
|
+
const outDir = path_1.default.join(process.cwd(), "build");
|
|
87
|
+
await fs_extra_1.default.ensureDir(outDir);
|
|
88
|
+
const artifactSubDir = isAab ? "bundle/release" : `apk/${apkDir}`;
|
|
89
|
+
const artifactDir = path_1.default.join(process.cwd(), "android/app/build/outputs", artifactSubDir);
|
|
90
|
+
const sourceName = isAab
|
|
91
|
+
? "app-release.aab"
|
|
92
|
+
: profile === "dev"
|
|
93
|
+
? "app-debug.apk"
|
|
94
|
+
: (await fs_extra_1.default.pathExists(path_1.default.join(artifactDir, "app-release-unsigned.apk")))
|
|
95
|
+
? "app-release-unsigned.apk"
|
|
96
|
+
: "app-release.apk";
|
|
97
|
+
const artifactPath = path_1.default.join(artifactDir, sourceName);
|
|
98
|
+
const destPath = path_1.default.join(outDir, outputName);
|
|
99
|
+
if (await fs_extra_1.default.pathExists(artifactPath)) {
|
|
100
|
+
await fs_extra_1.default.copy(artifactPath, destPath, { overwrite: true });
|
|
101
|
+
if (!(await fs_extra_1.default.pathExists(destPath))) {
|
|
102
|
+
console.log(chalk_1.default.red(`Error: Failed to confirm ${ext} in build folder at ${destPath}.`));
|
|
103
|
+
process.exit(1);
|
|
104
|
+
}
|
|
105
|
+
console.log(chalk_1.default.green(`Success! ${ext} generated at ${destPath}`));
|
|
106
|
+
}
|
|
107
|
+
else {
|
|
108
|
+
console.log(chalk_1.default.red(`Error: ${ext} not found at expected location (${artifactPath}).`));
|
|
109
|
+
process.exit(1);
|
|
110
|
+
}
|
|
111
|
+
console.log(chalk_1.default.cyan(`\nStep 08: Signing ${ext}...`));
|
|
112
|
+
await (0, signing_js_1.signArtifact)(outputName, outDir, isAab, appName);
|
|
113
|
+
const tag = `v${appVersion}${profile ? "-" + profile : ""}`;
|
|
114
|
+
const isPrerelease = profile !== "prod";
|
|
115
|
+
const published = await (0, github_js_1.publishGitHubRelease)(tag, destPath, tag, options.notes || "", fullProfileName, isPrerelease);
|
|
116
|
+
if (options.publish && isAab && published) {
|
|
117
|
+
await (0, github_js_1.triggerGitHubWorkflow)(tag, options.track);
|
|
118
|
+
}
|
|
119
|
+
else if (options.publish && isAab && !published) {
|
|
120
|
+
console.log(chalk_1.default.yellow(`Note: Skipping Play Store trigger because artifact could not be published to GitHub Releases.`));
|
|
121
|
+
}
|
|
122
|
+
else if (options.publish && !isAab) {
|
|
123
|
+
console.log(chalk_1.default.yellow(`Note: Skipping Play Store trigger (only production AAB builds can be deployed to Play Store).`));
|
|
124
|
+
}
|
|
125
|
+
console.log(chalk_1.default.green("\nBuild completed successfully!"));
|
|
126
|
+
}
|
|
127
|
+
finally {
|
|
128
|
+
const shouldCleanup = projectInfo.isManaged &&
|
|
129
|
+
(options.cleanup !== undefined ? options.cleanup : profile !== "dev");
|
|
130
|
+
if (shouldCleanup) {
|
|
131
|
+
console.log(chalk_1.default.gray("\nCleaning up ephemeral android folder..."));
|
|
132
|
+
if (await fs_extra_1.default.pathExists("android")) {
|
|
133
|
+
if (await fs_extra_1.default.pathExists(path_1.default.join(process.cwd(), "android", "gradlew"))) {
|
|
134
|
+
console.log(chalk_1.default.gray("Stopping Gradle daemon to release file locks..."));
|
|
135
|
+
try {
|
|
136
|
+
await (0, execa_1.execa)(process.platform === "win32" ? ".\\gradlew.bat" : "./gradlew", ["--stop"], { cwd: "android" });
|
|
137
|
+
}
|
|
138
|
+
catch { }
|
|
139
|
+
}
|
|
140
|
+
try {
|
|
141
|
+
await fs_extra_1.default.remove("android");
|
|
142
|
+
console.log(chalk_1.default.green("Cleanup completed."));
|
|
143
|
+
}
|
|
144
|
+
catch (err) {
|
|
145
|
+
console.log(chalk_1.default.yellow(`Cleanup partially failed (file lock). Run 'gradlew --stop' if you need to manually delete it.`));
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
else {
|
|
150
|
+
if (projectInfo.isManaged) {
|
|
151
|
+
console.log(chalk_1.default.gray(profile === "dev"
|
|
152
|
+
? "\nPreserving android/ folder and Gradle daemon for fast rebuilds (use --cleanup to delete)."
|
|
153
|
+
: "\nSkipping cleanup (--no-cleanup specified). The android/ folder has been left intact."));
|
|
154
|
+
}
|
|
155
|
+
else {
|
|
156
|
+
console.log(chalk_1.default.gray("\nPreserving permanent android/ source directory intact."));
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function buildIos(): Promise<void>;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.buildIos = buildIos;
|
|
7
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
8
|
+
async function buildIos() {
|
|
9
|
+
console.log(chalk_1.default.cyan("Starting iOS build pipeline..."));
|
|
10
|
+
console.log(chalk_1.default.yellow("iOS build support is coming soon! This command is currently a placeholder."));
|
|
11
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function doctor(): Promise<void>;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.doctor = doctor;
|
|
7
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
8
|
+
const dependencies_js_1 = require("../utils/dependencies.js");
|
|
9
|
+
async function doctor() {
|
|
10
|
+
try {
|
|
11
|
+
await (0, dependencies_js_1.ensureProjectType)();
|
|
12
|
+
console.log(chalk_1.default.cyan("Running environment diagnostics...\n"));
|
|
13
|
+
await (0, dependencies_js_1.ensureNode)(22);
|
|
14
|
+
await (0, dependencies_js_1.ensureGit)();
|
|
15
|
+
await (0, dependencies_js_1.ensureGitHubCLI)();
|
|
16
|
+
await (0, dependencies_js_1.ensureJava)();
|
|
17
|
+
const sdkPath = await (0, dependencies_js_1.ensureAndroidSdk)();
|
|
18
|
+
console.log(chalk_1.default.green("\nAll dependency checks passed!"));
|
|
19
|
+
if (sdkPath) {
|
|
20
|
+
console.log(chalk_1.default.gray(`Android SDK Path: ${sdkPath}`));
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
catch (err) {
|
|
24
|
+
console.error(chalk_1.default.red("\nDiagnostics failed!"));
|
|
25
|
+
console.error(err);
|
|
26
|
+
process.exit(1);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export interface SignCommandOptions {
|
|
2
|
+
file?: string;
|
|
3
|
+
keystore?: string;
|
|
4
|
+
alias?: string;
|
|
5
|
+
storepass?: string;
|
|
6
|
+
keypass?: string;
|
|
7
|
+
verify?: boolean;
|
|
8
|
+
}
|
|
9
|
+
export declare function findLatestArtifact(extFilter?: string[]): Promise<string | null>;
|
|
10
|
+
export declare function signCommand(fileArg?: string, options?: SignCommandOptions): Promise<void>;
|