expo-release-it 0.1.2 → 0.1.4
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 +133 -12
- package/build/command/build.js +135 -60
- package/build/command/build.js.map +1 -1
- package/build/command/bump.js +15 -18
- package/build/command/bump.js.map +1 -1
- package/build/command/init.js +13 -9
- package/build/command/init.js.map +1 -1
- package/build/command/pull.js +136 -50
- package/build/command/pull.js.map +1 -1
- package/build/command/submit.js +135 -55
- package/build/command/submit.js.map +1 -1
- package/build/command/upload.js +136 -56
- package/build/command/upload.js.map +1 -1
- package/build/index.js +194 -141
- package/build/index.js.map +1 -1
- package/build/util/FastlaneOption.js +10 -7
- package/build/util/FastlaneOption.js.map +1 -1
- package/build/util/MetadataSyncUtil.d.ts +4 -0
- package/build/util/MetadataSyncUtil.js +169 -0
- package/build/util/MetadataSyncUtil.js.map +1 -0
- package/build/util/OptionHolder.d.ts +2 -1
- package/build/util/OptionHolder.js +4 -2
- package/build/util/OptionHolder.js.map +1 -1
- package/build/util/generateAppStoreConnectApiKeyFile.js +5 -3
- package/build/util/generateAppStoreConnectApiKeyFile.js.map +1 -1
- package/build/util/injectTemplatePlaceHolders.js +14 -21
- package/build/util/injectTemplatePlaceHolders.js.map +1 -1
- package/build/util/input/InqueryInputs.d.ts +1 -0
- package/build/util/input/InqueryInputs.js +19 -2
- package/build/util/input/InqueryInputs.js.map +1 -1
- package/build/util/setup/VersionUtil.js +10 -12
- package/build/util/setup/VersionUtil.js.map +1 -1
- package/build/util/setup/hydrateKeyholderFileValues.js +4 -2
- package/build/util/setup/hydrateKeyholderFileValues.js.map +1 -1
- package/build/util/setup/prepareAndroid.js +56 -28
- package/build/util/setup/prepareAndroid.js.map +1 -1
- package/build/util/setup/prepareIos.js +61 -27
- package/build/util/setup/prepareIos.js.map +1 -1
- package/build/util/setup/setup.js +12 -8
- package/build/util/setup/setup.js.map +1 -1
- package/package.json +3 -1
- package/template/android/fastlane/Fastfile +2 -0
- package/template/android/fastlane/Appfile +0 -2
- package/template/android/fastlane/metadata/android/ko-KR/changelogs/default.txt +0 -1
- package/template/android/fastlane/metadata/android/ko-KR/full_description.txt +0 -0
- package/template/android/fastlane/metadata/android/ko-KR/short_description.txt +0 -0
- package/template/android/fastlane/metadata/android/ko-KR/title.txt +0 -0
- package/template/android/fastlane/metadata/android/ko-KR/video.txt +0 -0
- package/template/ios/fastlane/metadata/copyright.txt +0 -0
- package/template/ios/fastlane/metadata/ko/apple_tv_privacy_policy.txt +0 -0
- package/template/ios/fastlane/metadata/ko/description.txt +0 -0
- package/template/ios/fastlane/metadata/ko/keywords.txt +0 -0
- package/template/ios/fastlane/metadata/ko/marketing_url.txt +0 -0
- package/template/ios/fastlane/metadata/ko/name.txt +0 -0
- package/template/ios/fastlane/metadata/ko/privacy_url.txt +0 -0
- package/template/ios/fastlane/metadata/ko/promotional_text.txt +0 -0
- package/template/ios/fastlane/metadata/ko/release_notes.txt +0 -0
- package/template/ios/fastlane/metadata/ko/subtitle.txt +0 -0
- package/template/ios/fastlane/metadata/ko/support_url.txt +0 -0
- package/template/ios/fastlane/metadata/primary_category.txt +0 -0
- package/template/ios/fastlane/metadata/primary_first_sub_category.txt +0 -1
- package/template/ios/fastlane/metadata/primary_second_sub_category.txt +0 -1
- package/template/ios/fastlane/metadata/review_information/demo_password.txt +0 -0
- package/template/ios/fastlane/metadata/review_information/demo_user.txt +0 -0
- package/template/ios/fastlane/metadata/review_information/email_address.txt +0 -0
- package/template/ios/fastlane/metadata/review_information/first_name.txt +0 -0
- package/template/ios/fastlane/metadata/review_information/last_name.txt +0 -0
- package/template/ios/fastlane/metadata/review_information/notes.txt +0 -1
- package/template/ios/fastlane/metadata/review_information/phone_number.txt +0 -0
- package/template/ios/fastlane/metadata/secondary_category.txt +0 -1
- package/template/ios/fastlane/metadata/secondary_first_sub_category.txt +0 -1
- package/template/ios/fastlane/metadata/secondary_second_sub_category.txt +0 -1
- package/template/ios/fastlane/screenshots/README.txt +0 -30
- package/template/key/android_upload_certificate.pem +0 -2
- /package/template/key/{ios_testflight_upload_api_key.p8 → ios_app_store_connect_api_key.p8} +0 -0
package/README.md
CHANGED
|
@@ -2,22 +2,28 @@
|
|
|
2
2
|
|
|
3
3
|

|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Opinionated Expo CICD workflow CLI for building & uploading & submitting Expo projects on your machine
|
|
6
6
|
|
|
7
7
|
> [!CAUTION]
|
|
8
|
-
> This project is under
|
|
8
|
+
> This project is under development.
|
|
9
9
|
|
|
10
10
|
# Features
|
|
11
11
|
|
|
12
|
-
- No EAS
|
|
13
|
-
-
|
|
14
|
-
-
|
|
15
|
-
-
|
|
16
|
-
-
|
|
17
|
-
-
|
|
12
|
+
- 🎉 No **EAS**
|
|
13
|
+
- 🩵 No headaches when understanding and managing **keys and credentials**
|
|
14
|
+
- 🧩 Manage and sync **store metadata** easily
|
|
15
|
+
- 🌍 Works with [Expo CNG](https://docs.expo.dev/workflow/continuous-native-generation/) mode (also available in non-CNG environments)
|
|
16
|
+
- 📦 Build artifacts **locally** (`apk`, `aab`, `ipa`)
|
|
17
|
+
- 🚀 Upload artifacts to Google Play Console Internal Testing Track and iOS TestFlight
|
|
18
|
+
- ✅ Submit releases for review
|
|
18
19
|
|
|
19
20
|
# Why?
|
|
20
21
|
|
|
22
|
+
**You don't need Expo EAS and to understand how it works in most small projects and our machine is much faster than using the cloud.**
|
|
23
|
+
|
|
24
|
+
<details>
|
|
25
|
+
<summary>Full Details</summary>
|
|
26
|
+
|
|
21
27
|
Expo EAS is convenient, but it requires additional configuration files, console setup, and a conceptual understanding of Over-The-Air (OTA) updates. Also, builds must be performed on EAS Cloud, which is not free and can be slow.
|
|
22
28
|
|
|
23
29
|
While EAS is useful for larger teams, if you are developing alone or with just a few people and question the need for such a process, `expo-release-it` is the answer.
|
|
@@ -26,32 +32,144 @@ Based on years of experience developing with React Native, I encourage a more tr
|
|
|
26
32
|
|
|
27
33
|
This approach is much faster, simpler, and, most importantly, allows you to take full advantage of your computer’s fast CPU.
|
|
28
34
|
|
|
35
|
+
OTA updates are a powerful tool in React Native, but to fully take advantage of them, you need a self-hosted CDN server, a version management console both in the cloud and inside the app, and you have to separately manage binary versions and bundle versions—all of which introduce significant complexity and semantic overhead that often outweigh the benefits of implementing OTA updates. Furthermore, due to a general lack of deep understanding about OTA updates, issues like native library version mismatches can occur frequently, so OTA updates are excluded by default.
|
|
36
|
+
|
|
37
|
+
I’m also a big fan of OTA updates, and I’ve enjoyed a lot of efficiency when working in larger teams, but it always required setting up many supporting systems in advance.
|
|
38
|
+
|
|
29
39
|
`expo-release-it` is a CLI tool that encapsulates the know-how I have gained over years of automating build and review processes.
|
|
30
40
|
Internally, it ports and integrates tools like Fastlane and Match to automate native binary builds and review requests.
|
|
31
41
|
|
|
32
42
|
Therefore, this tool enforces an opinionated versioning, build, and review request workflow.
|
|
33
43
|
However, once you understand this approach, your app deployment process will become significantly faster.
|
|
34
44
|
|
|
45
|
+
</details>
|
|
35
46
|
|
|
36
|
-
# Requirements
|
|
47
|
+
# Structurual Requirements
|
|
37
48
|
|
|
38
|
-
-
|
|
39
|
-
- All Keys, Credentials(except iOS certificates & provisioning profiles) are stored in VCS
|
|
49
|
+
- `Version Code(Android)` and `Builder Number(iOS)` are the same.
|
|
50
|
+
- All Keys, Credentials(except iOS certificates & provisioning profiles served in Github separately) are stored in VCS
|
|
40
51
|
|
|
41
52
|
# Getting Started
|
|
42
53
|
|
|
54
|
+
## 1. Ruby, Bundler
|
|
55
|
+
|
|
56
|
+
`expo-release-it` requires `bundler` of ruby is available in your environment to run `Fastlane` internally.
|
|
57
|
+
|
|
58
|
+
If not, set up your environment so that `ruby` and `bundler` are available by following [Ruby Install Guide](https://www.ruby-lang.org/en/downloads/)
|
|
59
|
+
|
|
60
|
+
## 2. Prepare Keys & Credentials
|
|
61
|
+
|
|
62
|
+
`init` command generates sample key files structure in `expo-release-it/key`. They are **noob files** and you must replace file contents.
|
|
63
|
+
|
|
64
|
+
> [!IMPORTANT]
|
|
65
|
+
**You must prepare all required key files in `expo-release-it/key` exactly the same filenames.**
|
|
66
|
+
|
|
67
|
+
### `android_play_console_service_account.json`
|
|
68
|
+
|
|
69
|
+
Google Play Console actions require authentication using Google Service Account.
|
|
70
|
+
|
|
71
|
+
You can issue new Google Play Service Account by following Guide
|
|
72
|
+
and prepare `json` file.
|
|
73
|
+
|
|
74
|
+
- [How to Get Your Google Play JSON Key](https://help.radio.co/en/articles/6232140-how-to-get-your-google-play-json-key)
|
|
75
|
+
|
|
76
|
+
### `android_release.keystore`
|
|
77
|
+
|
|
78
|
+
Prepare your android release keystore file for **uploading** and **signing** Android build artifacts.
|
|
79
|
+
|
|
80
|
+
Additional passwords, alias will be prompted during the `init` command and be stored in `keyholder.json`.
|
|
81
|
+
|
|
82
|
+
- [Keystore generation with Android Studio](https://developer.android.com/studio/publish/app-signing.html#generate-key)
|
|
83
|
+
- [Keystore generation with CLI](https://gist.github.com/henriquemenezes/70feb8fff20a19a65346e48786bedb8f)
|
|
84
|
+
|
|
85
|
+
### `ios_app_store_connect_api_key`
|
|
86
|
+
|
|
87
|
+
App Store Connect actions require authentication using App Store Connect Api Key(.p8)
|
|
88
|
+
|
|
89
|
+
Additional `issuer_id`, `key_id` values will be prompted during the `init` command and be stored in `keyholder.json`
|
|
90
|
+
|
|
91
|
+
- [Guide](https://www.revenuecat.com/docs/service-credentials/itunesconnect-app-specific-shared-secret/app-store-connect-api-key-configuration)
|
|
92
|
+
- [Apple Docs Guide](https://developer.apple.com/documentation/appstoreconnectapi/creating-api-keys-for-app-store-connect-api)
|
|
93
|
+
|
|
94
|
+
## 3. Create & Configure app on the stores as much as possible
|
|
95
|
+
|
|
96
|
+
Create and configure your application in Google Play Console & App Store Connect as many as possible.
|
|
43
97
|
|
|
98
|
+
You'd have to fill your key & credentials information using `init` command and pull store metadatas using `pull` command later.
|
|
99
|
+
|
|
100
|
+
## 4. Set binary version in expo config file.
|
|
101
|
+
|
|
102
|
+
The following buildNumber, versionCode format `ABBBCCC` is not mandatory but recommended for readability.
|
|
103
|
+
|
|
104
|
+
If your expo config file is `app.json`, then set like the following.
|
|
105
|
+
|
|
106
|
+
```json
|
|
107
|
+
{
|
|
108
|
+
"expo": {
|
|
109
|
+
"version": "1.2.3",
|
|
110
|
+
"ios": {
|
|
111
|
+
"buildNumber": "1002003",
|
|
112
|
+
},
|
|
113
|
+
"android": {
|
|
114
|
+
"versionCode": 1002003,
|
|
115
|
+
},
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
If your expo config file is `app.config.(js|ts|mjs|cjs)`, then set like the following.
|
|
122
|
+
|
|
123
|
+
```typescript
|
|
124
|
+
import type { ExpoConfig, ConfigContext } from 'expo/config';
|
|
125
|
+
const VERSION_NAME = '1.2.3'; // VERSION_NAME variable is required.
|
|
126
|
+
const VERSION_CODE = 1002003; // VERSION_CODE variable is required.
|
|
127
|
+
|
|
128
|
+
export default ({ config }: ConfigContext): ExpoConfig => {
|
|
129
|
+
return {
|
|
130
|
+
...config,
|
|
131
|
+
version: VERSION_NAME,
|
|
132
|
+
ios: {
|
|
133
|
+
buildNumber: VERSION_CODE + '',
|
|
134
|
+
},
|
|
135
|
+
android: {
|
|
136
|
+
versionCode: VERSION_CODE,
|
|
137
|
+
},
|
|
138
|
+
};
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
```
|
|
44
142
|
|
|
45
143
|
# Usage
|
|
46
144
|
|
|
47
145
|
```shell
|
|
48
|
-
npx expo-release-it (init|bump|build|upload|submit) [options]
|
|
146
|
+
npx expo-release-it (init|bump|pull|build|upload|submit) [options]
|
|
49
147
|
```
|
|
50
148
|
|
|
149
|
+
All commands are powered by [Commander.js](https://github.com/tj/commander.js?) means you can query all available options with `--help` or `-h`.
|
|
150
|
+
|
|
51
151
|
## init
|
|
52
152
|
|
|
153
|
+
**Configure resources and environment**
|
|
154
|
+
|
|
155
|
+
```shell
|
|
156
|
+
npx expo-release-it init
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
The `init` command asks you for many form fields, and the information is saved in `expo-release-it/keyholder.json` in your project.
|
|
160
|
+
|
|
161
|
+
Even after running `init`, you can manually edit `keyholder.json`, and all subsequent commands will use the updated values.
|
|
162
|
+
|
|
53
163
|
## bump
|
|
54
164
|
|
|
165
|
+
**Bump binary release patch version with modifying app.json**
|
|
166
|
+
|
|
167
|
+
```shell
|
|
168
|
+
npx expo-release-it bump
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
## pull
|
|
172
|
+
|
|
55
173
|
## build
|
|
56
174
|
|
|
57
175
|
## upload
|
|
@@ -60,6 +178,9 @@ npx expo-release-it (init|bump|build|upload|submit) [options]
|
|
|
60
178
|
|
|
61
179
|
# Limitation
|
|
62
180
|
|
|
181
|
+
# Troubleshooting
|
|
182
|
+
|
|
183
|
+
# Licenses
|
|
63
184
|
|
|
64
185
|
- See [LICENSE](/LICENSE)
|
|
65
186
|
|
package/build/command/build.js
CHANGED
|
@@ -3,26 +3,26 @@ import fs from "fs-extra";
|
|
|
3
3
|
import * as path from "path";
|
|
4
4
|
var join2 = path.join;
|
|
5
5
|
var resolve2 = path.resolve;
|
|
6
|
-
function exist(
|
|
7
|
-
return fs.existsSync(
|
|
6
|
+
function exist(path2) {
|
|
7
|
+
return fs.existsSync(path2);
|
|
8
8
|
}
|
|
9
|
-
function isDir(
|
|
10
|
-
return exist(
|
|
9
|
+
function isDir(path2) {
|
|
10
|
+
return exist(path2) && fs.lstatSync(path2).isDirectory();
|
|
11
11
|
}
|
|
12
12
|
function relativePath(from, to) {
|
|
13
13
|
return path.relative(from, to);
|
|
14
14
|
}
|
|
15
|
-
function isFile(
|
|
16
|
-
return exist(
|
|
15
|
+
function isFile(path2) {
|
|
16
|
+
return exist(path2) && fs.lstatSync(path2).isFile();
|
|
17
17
|
}
|
|
18
|
-
function read(
|
|
19
|
-
return fs.readFileSync(
|
|
18
|
+
function read(path2) {
|
|
19
|
+
return fs.readFileSync(path2, { encoding: "utf8" });
|
|
20
20
|
}
|
|
21
|
-
function readdir(
|
|
22
|
-
return fs.readdirSync(
|
|
21
|
+
function readdir(path2) {
|
|
22
|
+
return fs.readdirSync(path2);
|
|
23
23
|
}
|
|
24
|
-
function readJsonSlow(
|
|
25
|
-
return fs.readJSONSync(
|
|
24
|
+
function readJsonSlow(path2) {
|
|
25
|
+
return fs.readJSONSync(path2);
|
|
26
26
|
}
|
|
27
27
|
function write(p, content) {
|
|
28
28
|
const dir = path.dirname(p);
|
|
@@ -34,21 +34,21 @@ function write(p, content) {
|
|
|
34
34
|
function copy(src, dest, options) {
|
|
35
35
|
fs.copySync(src, dest, options);
|
|
36
36
|
}
|
|
37
|
-
function remove(
|
|
38
|
-
if (!exist(
|
|
37
|
+
function remove(path2) {
|
|
38
|
+
if (!exist(path2)) {
|
|
39
39
|
return;
|
|
40
40
|
}
|
|
41
|
-
if (fs.lstatSync(
|
|
42
|
-
return fs.rmSync(
|
|
41
|
+
if (fs.lstatSync(path2).isDirectory()) {
|
|
42
|
+
return fs.rmSync(path2, { force: true, recursive: true });
|
|
43
43
|
} else {
|
|
44
|
-
return fs.rmSync(
|
|
44
|
+
return fs.rmSync(path2, { force: true });
|
|
45
45
|
}
|
|
46
46
|
}
|
|
47
|
-
async function iterateDir(
|
|
48
|
-
if (!isDir(
|
|
47
|
+
async function iterateDir(path2, fn) {
|
|
48
|
+
if (!isDir(path2)) {
|
|
49
49
|
return;
|
|
50
50
|
}
|
|
51
|
-
for (const file of fs.readdirSync(
|
|
51
|
+
for (const file of fs.readdirSync(path2)) {
|
|
52
52
|
await fn(file);
|
|
53
53
|
}
|
|
54
54
|
}
|
|
@@ -58,8 +58,10 @@ var package_default = {
|
|
|
58
58
|
name: "expo-release-it",
|
|
59
59
|
module: "src/index.ts",
|
|
60
60
|
type: "module",
|
|
61
|
-
version: "0.1.
|
|
61
|
+
version: "0.1.3",
|
|
62
62
|
license: "MIT",
|
|
63
|
+
homepage: "https://github.com/mym0404/expo-release-it",
|
|
64
|
+
repository: "https://github.com/mym0404/expo-release-it",
|
|
63
65
|
scripts: {
|
|
64
66
|
build: "tsup",
|
|
65
67
|
dev: "yarn run build && NODE_ENV=development node bin/expo-release-it.mjs",
|
|
@@ -115,7 +117,7 @@ var OptionHolder = {
|
|
|
115
117
|
keyholderMap: {},
|
|
116
118
|
templateDirNames: ["key"],
|
|
117
119
|
projectDir: "",
|
|
118
|
-
|
|
120
|
+
resourcesDir: "",
|
|
119
121
|
keyDir: "",
|
|
120
122
|
keyholderFilePath: "",
|
|
121
123
|
versionName: "",
|
|
@@ -139,18 +141,12 @@ async function iterateAllFiles(root, callback) {
|
|
|
139
141
|
}
|
|
140
142
|
|
|
141
143
|
// src/util/injectTemplatePlaceHolders.ts
|
|
142
|
-
import * as path2 from "node:path";
|
|
143
144
|
async function injectTemplatePlaceHolders(root) {
|
|
144
145
|
await iterateAllFiles(root, async (filePath) => {
|
|
145
|
-
const dirPath = path2.dirname(filePath);
|
|
146
146
|
let content = read(filePath);
|
|
147
147
|
for (const [key, value] of Object.entries(OptionHolder.keyholderMap)) {
|
|
148
148
|
content = content.replaceAll(`{{${key}}}`, value);
|
|
149
149
|
}
|
|
150
|
-
content = content.replaceAll(
|
|
151
|
-
"{{key_dir_relative_path}}",
|
|
152
|
-
relativePath(dirPath, OptionHolder.keyDir)
|
|
153
|
-
);
|
|
154
150
|
write(filePath, content);
|
|
155
151
|
});
|
|
156
152
|
}
|
|
@@ -194,6 +190,71 @@ ${msg.join(", ")}`)),
|
|
|
194
190
|
dim: (...msg) => console.log(chalk2.dim(msg.join(", ")))
|
|
195
191
|
};
|
|
196
192
|
|
|
193
|
+
// src/util/MetadataSyncUtil.ts
|
|
194
|
+
import chalk3 from "chalk";
|
|
195
|
+
function copyIosMetadata(to) {
|
|
196
|
+
const iosDir = join2(OptionHolder.projectDir, "ios");
|
|
197
|
+
const nativeDir = join2(iosDir, "fastlane");
|
|
198
|
+
const metadataNativeDir = join2(nativeDir, "metadata");
|
|
199
|
+
const screenshotsNativeDir = join2(nativeDir, "screenshots");
|
|
200
|
+
const resourcesDir = join2(OptionHolder.resourcesDir, "metadata/ios");
|
|
201
|
+
const metadataResourcesDir = join2(resourcesDir, "metadata");
|
|
202
|
+
const screenshotsResourcesDir = join2(resourcesDir, "screenshots");
|
|
203
|
+
if (to === "resources") {
|
|
204
|
+
if (exist(metadataNativeDir) && exist(screenshotsNativeDir)) {
|
|
205
|
+
copy(metadataNativeDir, metadataResourcesDir);
|
|
206
|
+
copy(screenshotsNativeDir, screenshotsResourcesDir);
|
|
207
|
+
logger.done(
|
|
208
|
+
`iOS Metadata saved in ${chalk3.inverse(relativePath(OptionHolder.projectDir, resourcesDir))}. You can edit inside contents.`
|
|
209
|
+
);
|
|
210
|
+
} else {
|
|
211
|
+
logger.warn(
|
|
212
|
+
`iOS Metadata not found in ${chalk3.inverse(relativePath(OptionHolder.projectDir, nativeDir))}`
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
} else {
|
|
216
|
+
if (exist(metadataResourcesDir) && exist(screenshotsResourcesDir)) {
|
|
217
|
+
copy(metadataResourcesDir, metadataNativeDir);
|
|
218
|
+
copy(screenshotsResourcesDir, screenshotsNativeDir);
|
|
219
|
+
logger.done(
|
|
220
|
+
`iOS Metadata copyed to ${chalk3.inverse(relativePath(OptionHolder.projectDir, nativeDir))}`
|
|
221
|
+
);
|
|
222
|
+
} else {
|
|
223
|
+
logger.warn(
|
|
224
|
+
`iOS Metadata not found in ${chalk3.inverse(relativePath(OptionHolder.projectDir, resourcesDir))}`
|
|
225
|
+
);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
function copyAndroidMetadata(to) {
|
|
230
|
+
const androidDir = join2(OptionHolder.projectDir, "android");
|
|
231
|
+
const nativeDir = join2(androidDir, "fastlane/metadata/android");
|
|
232
|
+
const resourcesDir = join2(OptionHolder.resourcesDir, "metadata/android");
|
|
233
|
+
if (to === "resouces") {
|
|
234
|
+
if (exist(nativeDir)) {
|
|
235
|
+
copy(nativeDir, resourcesDir);
|
|
236
|
+
logger.done(
|
|
237
|
+
`Android Metadata saved in ${chalk3.inverse(relativePath(OptionHolder.projectDir, resourcesDir))}. You can edit inside contents.`
|
|
238
|
+
);
|
|
239
|
+
} else {
|
|
240
|
+
logger.warn(
|
|
241
|
+
`Android Metadata not found in ${chalk3.inverse(relativePath(OptionHolder.projectDir, nativeDir))}`
|
|
242
|
+
);
|
|
243
|
+
}
|
|
244
|
+
} else {
|
|
245
|
+
if (exist(resourcesDir)) {
|
|
246
|
+
copy(resourcesDir, nativeDir);
|
|
247
|
+
logger.done(
|
|
248
|
+
`Android Metadata copyed to ${chalk3.inverse(relativePath(OptionHolder.projectDir, nativeDir))}`
|
|
249
|
+
);
|
|
250
|
+
} else {
|
|
251
|
+
logger.warn(
|
|
252
|
+
`Android Metadata not found in ${chalk3.inverse(relativePath(OptionHolder.projectDir, resourcesDir))}`
|
|
253
|
+
);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
197
258
|
// src/util/setup/prepareAndroid.ts
|
|
198
259
|
async function prepareAndroid() {
|
|
199
260
|
const srcDir = resolve2(OptionHolder.cli.templateDir, "android");
|
|
@@ -215,8 +276,9 @@ async function prepareAndroid() {
|
|
|
215
276
|
"Hydrate template placeholders",
|
|
216
277
|
injectTemplatePlaceHolders(resolve2(destDir, "fastlane"))
|
|
217
278
|
);
|
|
218
|
-
await exe({ cwd: destDir })
|
|
279
|
+
await exe("bundle", ["install"], { cwd: destDir });
|
|
219
280
|
logger.success("Bunder Install - Done");
|
|
281
|
+
copyAndroidMetadata("native");
|
|
220
282
|
}
|
|
221
283
|
async function replaceAndroidSigningConfig(destDir) {
|
|
222
284
|
const buildGradleDirPath = resolve2(destDir, "app");
|
|
@@ -231,7 +293,7 @@ async function replaceAndroidSigningConfig(destDir) {
|
|
|
231
293
|
`
|
|
232
294
|
signingConfigs {
|
|
233
295
|
debug {
|
|
234
|
-
storeFile file('${relativePath(buildGradleDirPath, resolve2(OptionHolder.
|
|
296
|
+
storeFile file('${relativePath(buildGradleDirPath, resolve2(OptionHolder.resourcesDir, "key", "android_release.keystore"))}')
|
|
235
297
|
storePassword '${android_keystore_store_password}'
|
|
236
298
|
keyAlias '${android_keystore_key_alias}'
|
|
237
299
|
keyPassword '${android_keystore_key_password}'
|
|
@@ -261,6 +323,8 @@ function throwError(message, e) {
|
|
|
261
323
|
|
|
262
324
|
// src/util/setup/VersionUtil.ts
|
|
263
325
|
import semver from "semver";
|
|
326
|
+
var versionNameRegex = () => /const\s+VERSION_NAME\s*?=\s*?['"]([\d\.]*?)['"]\s*?;?/;
|
|
327
|
+
var versionCodeRegex = () => /const\s+VERSION_CODE\s*?=\s*?(\d+)\s*?;?/;
|
|
264
328
|
async function parseBinaryVersions() {
|
|
265
329
|
const files = readdir(OptionHolder.projectDir);
|
|
266
330
|
const jsConfigFiles = ["app.config.ts", "app.config.js", "app.config.cjs", "app.config.mjs"];
|
|
@@ -279,8 +343,8 @@ async function parseBinaryVersions() {
|
|
|
279
343
|
async function parseFromJsConfigFile(filename) {
|
|
280
344
|
const filePath = resolve2(OptionHolder.projectDir, filename);
|
|
281
345
|
const content = read(filePath);
|
|
282
|
-
const VERSION_NAME =
|
|
283
|
-
const VERSION_CODE =
|
|
346
|
+
const VERSION_NAME = versionNameRegex().exec(content)?.[1];
|
|
347
|
+
const VERSION_CODE = versionCodeRegex().exec(content)?.[1];
|
|
284
348
|
if (!VERSION_NAME) {
|
|
285
349
|
throwError("cannot parse version name");
|
|
286
350
|
}
|
|
@@ -328,10 +392,10 @@ async function setup() {
|
|
|
328
392
|
}
|
|
329
393
|
async function setupBasicOptions() {
|
|
330
394
|
OptionHolder.projectDir = isDev ? resolve2("example") : resolve2(".");
|
|
331
|
-
OptionHolder.
|
|
332
|
-
OptionHolder.keyDir = resolve2(OptionHolder.
|
|
333
|
-
OptionHolder.keyholderFilePath = resolve2(OptionHolder.
|
|
334
|
-
OptionHolder.tempDir = resolve2(OptionHolder.
|
|
395
|
+
OptionHolder.resourcesDir = resolve2(OptionHolder.projectDir, "expo-release-it");
|
|
396
|
+
OptionHolder.keyDir = resolve2(OptionHolder.resourcesDir, "key");
|
|
397
|
+
OptionHolder.keyholderFilePath = resolve2(OptionHolder.resourcesDir, "keyholder.json");
|
|
398
|
+
OptionHolder.tempDir = resolve2(OptionHolder.resourcesDir, ".temp");
|
|
335
399
|
}
|
|
336
400
|
|
|
337
401
|
// src/util/setup/prepareIos.ts
|
|
@@ -354,12 +418,13 @@ async function prepareIos() {
|
|
|
354
418
|
"Hydrate template placeholders",
|
|
355
419
|
injectTemplatePlaceHolders(resolve2(destDir, "fastlane"))
|
|
356
420
|
);
|
|
357
|
-
await exe({ cwd: destDir })
|
|
421
|
+
await exe("bundle", ["install"], { cwd: destDir });
|
|
358
422
|
logger.success("Bunder Install - Done");
|
|
423
|
+
copyIosMetadata("native");
|
|
359
424
|
}
|
|
360
425
|
|
|
361
426
|
// src/command/build.ts
|
|
362
|
-
import
|
|
427
|
+
import chalk5 from "chalk";
|
|
363
428
|
|
|
364
429
|
// src/util/calculateElapsed.ts
|
|
365
430
|
import { padZero } from "@mj-studio/js-util";
|
|
@@ -373,7 +438,7 @@ function calculateElapsed(t) {
|
|
|
373
438
|
// src/util/input/InqueryInputs.ts
|
|
374
439
|
import { select } from "@inquirer/prompts";
|
|
375
440
|
import { is } from "@mj-studio/js-util";
|
|
376
|
-
import
|
|
441
|
+
import chalk4 from "chalk";
|
|
377
442
|
var InqueryInputs = {
|
|
378
443
|
platform: async () => {
|
|
379
444
|
if (!OptionHolder.input.platform) {
|
|
@@ -389,6 +454,21 @@ var InqueryInputs = {
|
|
|
389
454
|
});
|
|
390
455
|
}
|
|
391
456
|
},
|
|
457
|
+
platformAll: async () => {
|
|
458
|
+
if (!OptionHolder.input.platformAll) {
|
|
459
|
+
OptionHolder.input.platformAll = await select({
|
|
460
|
+
message: constructInquirerFormattedMessage({
|
|
461
|
+
name: "Platform",
|
|
462
|
+
explanation: `Platforms to run action`
|
|
463
|
+
}),
|
|
464
|
+
choices: [
|
|
465
|
+
{ name: "all", value: "all", description: "Both" },
|
|
466
|
+
{ name: "ios", value: "ios", description: "Release ios" },
|
|
467
|
+
{ name: "android", value: "android", description: "Release android" }
|
|
468
|
+
]
|
|
469
|
+
});
|
|
470
|
+
}
|
|
471
|
+
},
|
|
392
472
|
podinstall: async () => {
|
|
393
473
|
if (OptionHolder.input.platform === "ios") {
|
|
394
474
|
OptionHolder.input.pod = await select({
|
|
@@ -437,19 +517,19 @@ function constructInquirerFormattedMessage({
|
|
|
437
517
|
referUrl,
|
|
438
518
|
name
|
|
439
519
|
}) {
|
|
440
|
-
let ret = `${
|
|
520
|
+
let ret = `${chalk4.bold(name)}`;
|
|
441
521
|
if (explanation) {
|
|
442
522
|
ret += `
|
|
443
523
|
|
|
444
|
-
${
|
|
524
|
+
${chalk4.italic.blue(explanation)}`;
|
|
445
525
|
}
|
|
446
526
|
if (example) {
|
|
447
527
|
ret += `
|
|
448
|
-
${
|
|
528
|
+
${chalk4.italic(`${chalk4.greenBright("example :")} ${example}`)}`;
|
|
449
529
|
}
|
|
450
530
|
if (referUrl) {
|
|
451
531
|
ret += `
|
|
452
|
-
${
|
|
532
|
+
${chalk4.italic(`${chalk4.greenBright("references :")} ${chalk4.underline.blue(referUrl)}`)}`;
|
|
453
533
|
}
|
|
454
534
|
if (explanation) {
|
|
455
535
|
ret += "\n";
|
|
@@ -466,10 +546,10 @@ function getIosFastlaneOptions() {
|
|
|
466
546
|
...Object.entries(OptionHolder.keyholderMap).map(([k, v]) => `${k}:${v}`),
|
|
467
547
|
`version_name:${OptionHolder.versionName}`,
|
|
468
548
|
`version_code:${OptionHolder.versionCode}`,
|
|
469
|
-
`api_key_filepath:${resolve2(OptionHolder.keyDir, "
|
|
549
|
+
`api_key_filepath:${resolve2(OptionHolder.keyDir, "ios_app_store_connect_api_key.p8")}`,
|
|
470
550
|
`upload_metadata:${OptionHolder.input.uploadMetadata}`,
|
|
471
|
-
`ipa_path:${resolve2(OptionHolder.
|
|
472
|
-
`ipa_dir:${resolve2(OptionHolder.
|
|
551
|
+
`ipa_path:${resolve2(OptionHolder.resourcesDir, "output", "ios", "app.ipa")}`,
|
|
552
|
+
`ipa_dir:${resolve2(OptionHolder.resourcesDir, "output", "ios")}`,
|
|
473
553
|
`ipa_name:app.ipa`
|
|
474
554
|
];
|
|
475
555
|
}
|
|
@@ -481,13 +561,12 @@ async function build({ options }) {
|
|
|
481
561
|
await setup();
|
|
482
562
|
await promptInputs();
|
|
483
563
|
logger.info("Build Started");
|
|
484
|
-
logger.info(`Version: ${OptionHolder.versionName}(${OptionHolder.versionCode})`);
|
|
485
564
|
if (OptionHolder.input.platform === "ios") {
|
|
486
565
|
await buildIos();
|
|
487
|
-
logger.success(`iOS Build ${
|
|
566
|
+
logger.success(`iOS Build ${chalk5.bold.inverse(calculateElapsed(startTime))}`);
|
|
488
567
|
} else {
|
|
489
568
|
await buildAndroid();
|
|
490
|
-
logger.success(`Android Build ${
|
|
569
|
+
logger.success(`Android Build ${chalk5.bold.inverse(calculateElapsed(startTime))}`);
|
|
491
570
|
}
|
|
492
571
|
}
|
|
493
572
|
async function promptInputs() {
|
|
@@ -513,7 +592,7 @@ async function buildIos() {
|
|
|
513
592
|
remove(resolve2(iosDir, ".xcode.env.local"));
|
|
514
593
|
await exeEnv("bundle", ["exec", "fastlane", "build", ...getIosFastlaneOptions()]);
|
|
515
594
|
logger.success(
|
|
516
|
-
`artifact generated: ${relativePath(OptionHolder.projectDir, resolve2(OptionHolder.
|
|
595
|
+
`artifact generated: ${relativePath(OptionHolder.projectDir, resolve2(OptionHolder.resourcesDir, "output", "ios", "app.ipa"))}`
|
|
517
596
|
);
|
|
518
597
|
}
|
|
519
598
|
}
|
|
@@ -528,19 +607,15 @@ async function buildAndroid() {
|
|
|
528
607
|
let buildOutputDir;
|
|
529
608
|
let outputDir;
|
|
530
609
|
if (OptionHolder.input.androidOutput === "aab") {
|
|
531
|
-
await
|
|
532
|
-
|
|
533
|
-
exeEnv(isWin ? "gradle.bat" : "./gradlew", ["app:bundleRelease"])
|
|
534
|
-
);
|
|
610
|
+
await exeEnv(isWin ? "gradle.bat" : "./gradlew", ["app:bundleRelease"]);
|
|
611
|
+
logger.done("Bundle AAB");
|
|
535
612
|
buildOutputDir = resolve2(androidDir, "app", "build", "outputs", "bundle", "release");
|
|
536
|
-
outputDir = resolve2(OptionHolder.
|
|
613
|
+
outputDir = resolve2(OptionHolder.resourcesDir, "output", "android", "aab");
|
|
537
614
|
} else {
|
|
538
|
-
await
|
|
539
|
-
|
|
540
|
-
exeEnv(isWin ? "gradle.bat" : "./gradlew", ["app:assembleRelease"])
|
|
541
|
-
);
|
|
615
|
+
await exeEnv(isWin ? "gradle.bat" : "./gradlew", ["app:assembleRelease"]);
|
|
616
|
+
logger.done("Assemble APK");
|
|
542
617
|
buildOutputDir = resolve2(androidDir, "app", "build", "outputs", "apk", "release");
|
|
543
|
-
outputDir = resolve2(OptionHolder.
|
|
618
|
+
outputDir = resolve2(OptionHolder.resourcesDir, "output", "android", "apk");
|
|
544
619
|
}
|
|
545
620
|
remove(outputDir);
|
|
546
621
|
copy(buildOutputDir, outputDir);
|