budexp 0.1.0
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/LICENSE +21 -0
- package/README.md +516 -0
- package/bin/budexp.js +162 -0
- package/package.json +63 -0
- package/src/commands/build.js +548 -0
- package/src/commands/check.js +130 -0
- package/src/commands/clean.js +92 -0
- package/src/commands/dev.js +186 -0
- package/src/utils/cleaner.js +377 -0
- package/src/utils/eas.js +198 -0
- package/src/utils/expo-doctor.js +1097 -0
- package/src/utils/logger.js +25 -0
package/package.json
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "budexp",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "A powerful CLI tool to facilitate mobile development with Expo + React Native stack",
|
|
5
|
+
"main": "./bin/budexp.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"budexp": "./bin/budexp.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"bin",
|
|
11
|
+
"src",
|
|
12
|
+
"README.md",
|
|
13
|
+
"LICENSE"
|
|
14
|
+
],
|
|
15
|
+
"scripts": {
|
|
16
|
+
"test": "node bin/budexp.js --help",
|
|
17
|
+
"test:check": "node bin/budexp.js check --help",
|
|
18
|
+
"test:dev": "node bin/budexp.js dev --help",
|
|
19
|
+
"test:build": "node bin/budexp.js build --help",
|
|
20
|
+
"test:clean": "node bin/budexp.js clean --help",
|
|
21
|
+
"lint": "eslint .",
|
|
22
|
+
"lint:fix": "eslint . --fix",
|
|
23
|
+
"format": "prettier . --write",
|
|
24
|
+
"format:check": "prettier . --check"
|
|
25
|
+
},
|
|
26
|
+
"keywords": [
|
|
27
|
+
"expo",
|
|
28
|
+
"react-native",
|
|
29
|
+
"cli",
|
|
30
|
+
"development",
|
|
31
|
+
"build",
|
|
32
|
+
"apk",
|
|
33
|
+
"ipa",
|
|
34
|
+
"eas"
|
|
35
|
+
],
|
|
36
|
+
"author": "Jorge Silva",
|
|
37
|
+
"license": "MIT",
|
|
38
|
+
"dependencies": {
|
|
39
|
+
"chalk": "^4.1.2",
|
|
40
|
+
"commander": "^11.1.0",
|
|
41
|
+
"fs-extra": "^11.2.0",
|
|
42
|
+
"inquirer": "^8.2.7",
|
|
43
|
+
"ora": "5.4.1"
|
|
44
|
+
},
|
|
45
|
+
"engines": {
|
|
46
|
+
"node": ">=14.0.0"
|
|
47
|
+
},
|
|
48
|
+
"repository": {
|
|
49
|
+
"type": "git",
|
|
50
|
+
"url": "git+https://github.com/JorgeSilva1997/budexp.git"
|
|
51
|
+
},
|
|
52
|
+
"bugs": {
|
|
53
|
+
"url": "https://github.com/JorgeSilva1997/budexp/issues"
|
|
54
|
+
},
|
|
55
|
+
"homepage": "https://github.com/JorgeSilva1997/budexp#readme",
|
|
56
|
+
"devDependencies": {
|
|
57
|
+
"@eslint/js": "^9.39.4",
|
|
58
|
+
"eslint": "^9.39.4",
|
|
59
|
+
"eslint-config-prettier": "^10.1.8",
|
|
60
|
+
"globals": "^17.5.0",
|
|
61
|
+
"prettier": "^3.8.3"
|
|
62
|
+
}
|
|
63
|
+
}
|
|
@@ -0,0 +1,548 @@
|
|
|
1
|
+
// ============================================
|
|
2
|
+
// FILE: src/commands/build.js (Enhanced with local/cloud selection)
|
|
3
|
+
// ============================================
|
|
4
|
+
const inquirer = require('inquirer');
|
|
5
|
+
const prompt = inquirer.default?.prompt || inquirer.prompt;
|
|
6
|
+
const logger = require('../utils/logger');
|
|
7
|
+
const cleaner = require('../utils/cleaner');
|
|
8
|
+
const expoDoctor = require('../utils/expo-doctor');
|
|
9
|
+
const eas = require('../utils/eas');
|
|
10
|
+
const { execFileSync } = require('child_process');
|
|
11
|
+
const fs = require('fs-extra');
|
|
12
|
+
const path = require('path');
|
|
13
|
+
|
|
14
|
+
async function buildCommand(options) {
|
|
15
|
+
logger.info('Starting build mode...');
|
|
16
|
+
const isInteractive = process.stdin.isTTY && process.stdout.isTTY;
|
|
17
|
+
|
|
18
|
+
// Determine platform
|
|
19
|
+
let platform = 'android'; // default
|
|
20
|
+
if (options.all) {
|
|
21
|
+
platform = 'all';
|
|
22
|
+
} else if (options.ios) {
|
|
23
|
+
platform = 'ios';
|
|
24
|
+
} else if (options.android) {
|
|
25
|
+
platform = 'android';
|
|
26
|
+
} else if (isInteractive) {
|
|
27
|
+
// Ask user if no platform specified
|
|
28
|
+
const answer = await prompt([
|
|
29
|
+
{
|
|
30
|
+
type: 'list',
|
|
31
|
+
name: 'platform',
|
|
32
|
+
message: 'Select platform:',
|
|
33
|
+
choices: [
|
|
34
|
+
{ name: 'Android', value: 'android' },
|
|
35
|
+
{ name: 'iOS', value: 'ios' },
|
|
36
|
+
{ name: 'Both', value: 'all' },
|
|
37
|
+
],
|
|
38
|
+
},
|
|
39
|
+
]);
|
|
40
|
+
platform = answer.platform;
|
|
41
|
+
} else {
|
|
42
|
+
logger.info('Non-interactive shell detected. Using default platform: android');
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
logger.info(`Platform: ${platform}`);
|
|
46
|
+
console.log('');
|
|
47
|
+
|
|
48
|
+
// Ask about build type (local vs cloud)
|
|
49
|
+
let buildLocally = options.local || false;
|
|
50
|
+
|
|
51
|
+
if (!options.local && isInteractive) {
|
|
52
|
+
const buildTypeAnswer = await prompt([
|
|
53
|
+
{
|
|
54
|
+
type: 'list',
|
|
55
|
+
name: 'buildType',
|
|
56
|
+
message: 'How do you want to build?',
|
|
57
|
+
choices: [
|
|
58
|
+
{
|
|
59
|
+
name: '🏠 Local build (faster, no internet required, free)',
|
|
60
|
+
value: 'local',
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
name: '☁️ Cloud build via EAS (requires EAS account, slower)',
|
|
64
|
+
value: 'cloud',
|
|
65
|
+
},
|
|
66
|
+
],
|
|
67
|
+
},
|
|
68
|
+
]);
|
|
69
|
+
|
|
70
|
+
buildLocally = buildTypeAnswer.buildType === 'local';
|
|
71
|
+
} else if (!options.local) {
|
|
72
|
+
logger.info('Non-interactive shell detected. Using cloud build by default.');
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
logger.info(`Build type: ${buildLocally ? 'Local' : 'Cloud (EAS)'}`);
|
|
76
|
+
console.log('');
|
|
77
|
+
|
|
78
|
+
const buildProfile = await resolveBuildProfile(options);
|
|
79
|
+
logger.info(`Build profile: ${buildProfile}`);
|
|
80
|
+
console.log('');
|
|
81
|
+
|
|
82
|
+
// If cloud build, check EAS login
|
|
83
|
+
if (!buildLocally) {
|
|
84
|
+
const easStatus = await eas.checkEASLogin();
|
|
85
|
+
|
|
86
|
+
if (!easStatus.loggedIn) {
|
|
87
|
+
logger.warning('You need to be logged in to EAS for cloud builds');
|
|
88
|
+
|
|
89
|
+
if (!isInteractive) {
|
|
90
|
+
logger.info('Non-interactive shell detected. Run eas login before starting a cloud build.');
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const loginAnswer = await prompt([
|
|
95
|
+
{
|
|
96
|
+
type: 'confirm',
|
|
97
|
+
name: 'login',
|
|
98
|
+
message: 'Do you want to login to EAS now?',
|
|
99
|
+
default: true,
|
|
100
|
+
},
|
|
101
|
+
]);
|
|
102
|
+
|
|
103
|
+
if (loginAnswer.login) {
|
|
104
|
+
try {
|
|
105
|
+
execFileSync('eas', ['login'], { stdio: 'inherit' });
|
|
106
|
+
} catch (e) {
|
|
107
|
+
logger.error('Failed to login to EAS');
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
} else {
|
|
111
|
+
logger.info('Build cancelled. You need to be logged in for cloud builds.');
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Step 1: Run expo-doctor
|
|
118
|
+
const doctorResult = await expoDoctor.runExpoDoctor({ openReport: options.open });
|
|
119
|
+
const issuesSummary = expoDoctor.getIssuesSummary(doctorResult.output);
|
|
120
|
+
|
|
121
|
+
if (issuesSummary.hasIssues) {
|
|
122
|
+
logger.warning(issuesSummary.summary);
|
|
123
|
+
logger.info(`Detailed report saved to: ${doctorResult.reportPath}`);
|
|
124
|
+
|
|
125
|
+
let shouldContinue = Boolean(options.yes);
|
|
126
|
+
|
|
127
|
+
if (!shouldContinue && isInteractive) {
|
|
128
|
+
const continueAnswer = await prompt([
|
|
129
|
+
{
|
|
130
|
+
type: 'confirm',
|
|
131
|
+
name: 'continue',
|
|
132
|
+
message: 'Issues found. Do you want to continue anyway?',
|
|
133
|
+
default: false,
|
|
134
|
+
},
|
|
135
|
+
]);
|
|
136
|
+
shouldContinue = continueAnswer.continue;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (!shouldContinue) {
|
|
140
|
+
logger.info('Aborted by user');
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
} else {
|
|
144
|
+
logger.success('Health check passed!');
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
console.log('');
|
|
148
|
+
|
|
149
|
+
// Step 2: Clean everything (only for local builds)
|
|
150
|
+
if (buildLocally) {
|
|
151
|
+
logger.info('Step 2: Cleaning caches and dependencies...');
|
|
152
|
+
const bundleId = await cleaner.getBundleId();
|
|
153
|
+
|
|
154
|
+
await cleaner.killRunningApps(bundleId, platform);
|
|
155
|
+
await cleaner.cleanWatchman();
|
|
156
|
+
await cleaner.cleanCaches();
|
|
157
|
+
await cleaner.deleteNativeFolders(platform);
|
|
158
|
+
await cleaner.cleanDependencies();
|
|
159
|
+
|
|
160
|
+
console.log('');
|
|
161
|
+
|
|
162
|
+
// Step 3: Reinstall dependencies
|
|
163
|
+
logger.info('Step 3: Reinstalling dependencies...');
|
|
164
|
+
await cleaner.reinstallDependencies();
|
|
165
|
+
|
|
166
|
+
console.log('');
|
|
167
|
+
|
|
168
|
+
// Step 4: Rebuild native code
|
|
169
|
+
logger.info('Step 4: Rebuilding native code...');
|
|
170
|
+
await cleaner.rebuildNative(platform);
|
|
171
|
+
|
|
172
|
+
console.log('');
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Step 5: Build APK/IPA
|
|
176
|
+
logger.info(`Step ${buildLocally ? '5' : '2'}: Building ${platform.toUpperCase()}...`);
|
|
177
|
+
|
|
178
|
+
try {
|
|
179
|
+
if (buildLocally) {
|
|
180
|
+
// Local build
|
|
181
|
+
if (platform === 'all') {
|
|
182
|
+
logger.info('Building Android first...');
|
|
183
|
+
execFileSync(
|
|
184
|
+
'eas',
|
|
185
|
+
['build', '--platform', 'android', '--local', '--profile', buildProfile],
|
|
186
|
+
{
|
|
187
|
+
stdio: 'inherit',
|
|
188
|
+
}
|
|
189
|
+
);
|
|
190
|
+
await moveBuildArtifactsToFolder('android');
|
|
191
|
+
|
|
192
|
+
logger.info('Now building iOS...');
|
|
193
|
+
execFileSync('eas', ['build', '--platform', 'ios', '--local', '--profile', buildProfile], {
|
|
194
|
+
stdio: 'inherit',
|
|
195
|
+
});
|
|
196
|
+
await moveBuildArtifactsToFolder('ios');
|
|
197
|
+
} else {
|
|
198
|
+
execFileSync(
|
|
199
|
+
'eas',
|
|
200
|
+
['build', '--platform', platform, '--local', '--profile', buildProfile],
|
|
201
|
+
{
|
|
202
|
+
stdio: 'inherit',
|
|
203
|
+
}
|
|
204
|
+
);
|
|
205
|
+
await moveBuildArtifactsToFolder(platform);
|
|
206
|
+
}
|
|
207
|
+
} else {
|
|
208
|
+
// Cloud build via EAS
|
|
209
|
+
if (platform === 'all') {
|
|
210
|
+
logger.info('Starting Android cloud build...');
|
|
211
|
+
execFileSync('eas', ['build', '--platform', 'android', '--profile', buildProfile], {
|
|
212
|
+
stdio: 'inherit',
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
logger.info('Starting iOS cloud build...');
|
|
216
|
+
execFileSync('eas', ['build', '--platform', 'ios', '--profile', buildProfile], {
|
|
217
|
+
stdio: 'inherit',
|
|
218
|
+
});
|
|
219
|
+
} else {
|
|
220
|
+
execFileSync('eas', ['build', '--platform', platform, '--profile', buildProfile], {
|
|
221
|
+
stdio: 'inherit',
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
logger.success('✅ Build submitted to EAS!');
|
|
226
|
+
console.log('');
|
|
227
|
+
logger.info('Track your build progress:');
|
|
228
|
+
logger.info(' - Run: budexp check eas:list');
|
|
229
|
+
logger.info(' - Or visit: https://expo.dev');
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
if (buildLocally) {
|
|
233
|
+
logger.success('✅ Build completed!');
|
|
234
|
+
}
|
|
235
|
+
} catch (e) {
|
|
236
|
+
console.log('');
|
|
237
|
+
logger.error('Build failed');
|
|
238
|
+
|
|
239
|
+
// Provide helpful error messages
|
|
240
|
+
const errorCode = e.status || e.code;
|
|
241
|
+
const errorMessage = (e.message || e.toString()).toLowerCase();
|
|
242
|
+
|
|
243
|
+
if (
|
|
244
|
+
errorCode === 1 ||
|
|
245
|
+
errorMessage.includes('eas project') ||
|
|
246
|
+
errorMessage.includes('eas init') ||
|
|
247
|
+
errorMessage.includes('not configured')
|
|
248
|
+
) {
|
|
249
|
+
console.log('');
|
|
250
|
+
logger.warning('EAS project is not configured.');
|
|
251
|
+
console.log('');
|
|
252
|
+
logger.info('To fix this:');
|
|
253
|
+
logger.info(' 1. Run: eas init');
|
|
254
|
+
logger.info(' 2. Follow the prompts to configure your EAS project');
|
|
255
|
+
console.log('');
|
|
256
|
+
logger.info('Learn more: https://docs.expo.dev/build/setup/');
|
|
257
|
+
} else {
|
|
258
|
+
console.log('');
|
|
259
|
+
logger.warning('Build failed. Check the error messages above for details.');
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
process.exit(1);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
console.log('');
|
|
266
|
+
|
|
267
|
+
// Only ask about submission for local builds
|
|
268
|
+
if (buildLocally && isInteractive) {
|
|
269
|
+
const submitAnswer = await prompt([
|
|
270
|
+
{
|
|
271
|
+
type: 'confirm',
|
|
272
|
+
name: 'submit',
|
|
273
|
+
message: 'Do you want to submit the build to App/Play Store?',
|
|
274
|
+
default: false,
|
|
275
|
+
},
|
|
276
|
+
]);
|
|
277
|
+
|
|
278
|
+
if (submitAnswer.submit) {
|
|
279
|
+
const easStatus = await eas.checkEASLogin();
|
|
280
|
+
|
|
281
|
+
if (!easStatus.loggedIn) {
|
|
282
|
+
logger.warning('Not logged in to EAS. Please run: eas login');
|
|
283
|
+
const loginAnswer = await prompt([
|
|
284
|
+
{
|
|
285
|
+
type: 'confirm',
|
|
286
|
+
name: 'login',
|
|
287
|
+
message: 'Do you want to login to EAS now?',
|
|
288
|
+
default: true,
|
|
289
|
+
},
|
|
290
|
+
]);
|
|
291
|
+
|
|
292
|
+
if (loginAnswer.login) {
|
|
293
|
+
try {
|
|
294
|
+
execFileSync('eas', ['login'], { stdio: 'inherit' });
|
|
295
|
+
} catch (e) {
|
|
296
|
+
logger.error('Failed to login to EAS');
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
} else {
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// Submit builds
|
|
305
|
+
if (platform === 'all') {
|
|
306
|
+
await eas.submitToEAS('android');
|
|
307
|
+
await eas.submitToEAS('ios');
|
|
308
|
+
} else {
|
|
309
|
+
await eas.submitToEAS(platform);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
async function resolveBuildProfile(options) {
|
|
316
|
+
if (typeof options.profile === 'string' && options.profile.trim().length > 0) {
|
|
317
|
+
const profile = options.profile.trim();
|
|
318
|
+
validateBuildProfile(profile);
|
|
319
|
+
return profile;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
const knownProfiles = ['development', 'preview', 'production'];
|
|
323
|
+
const easProfiles = getEASProfiles();
|
|
324
|
+
const profileChoices = [...new Set([...knownProfiles, ...easProfiles])];
|
|
325
|
+
const isInteractive = process.stdin.isTTY && process.stdout.isTTY;
|
|
326
|
+
|
|
327
|
+
if (!isInteractive) {
|
|
328
|
+
return 'preview';
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
const answer = await prompt([
|
|
332
|
+
{
|
|
333
|
+
type: 'list',
|
|
334
|
+
name: 'profile',
|
|
335
|
+
message: 'Select build profile:',
|
|
336
|
+
choices: profileChoices.map((profile) => ({
|
|
337
|
+
name: profile,
|
|
338
|
+
value: profile,
|
|
339
|
+
})),
|
|
340
|
+
default: profileChoices.includes('preview') ? 'preview' : profileChoices[0],
|
|
341
|
+
},
|
|
342
|
+
]);
|
|
343
|
+
|
|
344
|
+
return answer.profile;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
function validateBuildProfile(profile) {
|
|
348
|
+
if (!/^[a-zA-Z0-9_.-]+$/.test(profile)) {
|
|
349
|
+
throw new Error(
|
|
350
|
+
'Invalid build profile. Use only letters, numbers, underscores, dots, and hyphens.'
|
|
351
|
+
);
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
function getEASProfiles() {
|
|
356
|
+
const easConfigPath = path.join(process.cwd(), 'eas.json');
|
|
357
|
+
if (!fs.existsSync(easConfigPath)) {
|
|
358
|
+
return [];
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
try {
|
|
362
|
+
const easConfig = fs.readJsonSync(easConfigPath);
|
|
363
|
+
const profiles = easConfig?.build;
|
|
364
|
+
if (!profiles || typeof profiles !== 'object') {
|
|
365
|
+
return [];
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
return Object.keys(profiles).filter(
|
|
369
|
+
(profile) => typeof profile === 'string' && profile.trim().length > 0
|
|
370
|
+
);
|
|
371
|
+
} catch (error) {
|
|
372
|
+
logger.warning('Could not read eas.json build profiles. Using defaults.');
|
|
373
|
+
return [];
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/**
|
|
378
|
+
* Ensure `builds/` exists (ask permission if missing) and is gitignored.
|
|
379
|
+
*/
|
|
380
|
+
async function ensureBuildsFolderAndGitignore() {
|
|
381
|
+
const buildsDir = path.join(process.cwd(), 'builds');
|
|
382
|
+
const isInteractive = process.stdin.isTTY && process.stdout.isTTY;
|
|
383
|
+
|
|
384
|
+
if (!fs.existsSync(buildsDir)) {
|
|
385
|
+
if (!isInteractive) {
|
|
386
|
+
logger.warning('builds/ folder does not exist and shell is non-interactive.');
|
|
387
|
+
logger.warning('Keeping build artifacts in their current location.');
|
|
388
|
+
return { ok: false, buildsDir };
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
const createAnswer = await prompt([
|
|
392
|
+
{
|
|
393
|
+
type: 'confirm',
|
|
394
|
+
name: 'create',
|
|
395
|
+
message: 'Build output folder "builds/" does not exist. Create it in project root?',
|
|
396
|
+
default: true,
|
|
397
|
+
},
|
|
398
|
+
]);
|
|
399
|
+
|
|
400
|
+
if (!createAnswer.create) {
|
|
401
|
+
logger.info('Keeping build artifacts in their current location.');
|
|
402
|
+
return { ok: false, buildsDir };
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
fs.ensureDirSync(buildsDir);
|
|
406
|
+
logger.success('Created builds/ folder');
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
const gitignorePath = path.join(process.cwd(), '.gitignore');
|
|
410
|
+
if (fs.existsSync(gitignorePath)) {
|
|
411
|
+
const gitignore = fs.readFileSync(gitignorePath, 'utf8');
|
|
412
|
+
const alreadyIgnored =
|
|
413
|
+
gitignore.includes('\nbuilds/\n') ||
|
|
414
|
+
gitignore.includes('\n/builds/\n') ||
|
|
415
|
+
gitignore.includes('\nbuilds\n') ||
|
|
416
|
+
gitignore.includes('\n/builds\n') ||
|
|
417
|
+
gitignore.trim() === 'builds/' ||
|
|
418
|
+
gitignore.trim() === '/builds/' ||
|
|
419
|
+
gitignore.trim() === 'builds' ||
|
|
420
|
+
gitignore.trim() === '/builds';
|
|
421
|
+
|
|
422
|
+
if (!alreadyIgnored) {
|
|
423
|
+
if (!isInteractive) {
|
|
424
|
+
logger.warning('"builds/" is not listed in .gitignore (non-interactive).');
|
|
425
|
+
logger.warning('Consider adding it to prevent committing .aab/.apk/.ipa artifacts.');
|
|
426
|
+
} else {
|
|
427
|
+
logger.warning('"builds/" is not listed in .gitignore.');
|
|
428
|
+
logger.info('Build artifacts can be large and are usually not meant to be committed.');
|
|
429
|
+
const addAnswer = await prompt([
|
|
430
|
+
{
|
|
431
|
+
type: 'confirm',
|
|
432
|
+
name: 'add',
|
|
433
|
+
message: 'Do you want to add "builds/" to .gitignore?',
|
|
434
|
+
default: true,
|
|
435
|
+
},
|
|
436
|
+
]);
|
|
437
|
+
|
|
438
|
+
if (addAnswer.add) {
|
|
439
|
+
const needsNewline = gitignore.length > 0 && !gitignore.endsWith('\n');
|
|
440
|
+
const updated = `${gitignore}${needsNewline ? '\n' : ''}\n# Build artifacts\nbuilds/\n`;
|
|
441
|
+
fs.writeFileSync(gitignorePath, updated, 'utf8');
|
|
442
|
+
logger.success('Added builds/ to .gitignore');
|
|
443
|
+
} else {
|
|
444
|
+
logger.warning(
|
|
445
|
+
'Not adding builds/ to .gitignore. Be careful not to commit build artifacts.'
|
|
446
|
+
);
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
} else {
|
|
451
|
+
logger.warning('.gitignore not found in project root.');
|
|
452
|
+
if (isInteractive) {
|
|
453
|
+
const createGitignoreAnswer = await prompt([
|
|
454
|
+
{
|
|
455
|
+
type: 'confirm',
|
|
456
|
+
name: 'create',
|
|
457
|
+
message: 'Do you want to create a .gitignore and ignore "builds/"?',
|
|
458
|
+
default: true,
|
|
459
|
+
},
|
|
460
|
+
]);
|
|
461
|
+
|
|
462
|
+
if (createGitignoreAnswer.create) {
|
|
463
|
+
fs.writeFileSync(gitignorePath, '# Build artifacts\nbuilds/\n', 'utf8');
|
|
464
|
+
logger.success('Created .gitignore and added builds/');
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
return { ok: true, buildsDir };
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
/**
|
|
473
|
+
* Move build artifacts into builds/ with app name + version.
|
|
474
|
+
*/
|
|
475
|
+
async function moveBuildArtifactsToFolder(platform) {
|
|
476
|
+
try {
|
|
477
|
+
const { ok, buildsDir } = await ensureBuildsFolderAndGitignore();
|
|
478
|
+
|
|
479
|
+
// Try to get config from app.json or app.config.js
|
|
480
|
+
let config = null;
|
|
481
|
+
if (fs.existsSync('app.json')) {
|
|
482
|
+
config = fs.readJsonSync('app.json');
|
|
483
|
+
} else if (fs.existsSync('app.config.js')) {
|
|
484
|
+
try {
|
|
485
|
+
delete require.cache[require.resolve(path.join(process.cwd(), 'app.config.js'))];
|
|
486
|
+
const loaded = require(path.join(process.cwd(), 'app.config.js'));
|
|
487
|
+
config = loaded.default || loaded;
|
|
488
|
+
} catch (e) {
|
|
489
|
+
// Ignore
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
if (!config) return;
|
|
494
|
+
|
|
495
|
+
const appName = (config.expo?.name || 'app').toLowerCase().replace(/[^a-z0-9]/g, '');
|
|
496
|
+
// This is the target Expo app version, not the budexp package version.
|
|
497
|
+
const version = config.expo?.version || '1.0.0';
|
|
498
|
+
const currentDate = new Date().toISOString().split('T')[0];
|
|
499
|
+
const customName = `${appName}-${version}-${currentDate}`;
|
|
500
|
+
|
|
501
|
+
// Search for build files
|
|
502
|
+
const searchDirs = [process.cwd(), path.join(process.cwd(), 'dist')];
|
|
503
|
+
|
|
504
|
+
const wantedExtensions =
|
|
505
|
+
platform === 'android' ? ['.aab', '.apk'] : platform === 'ios' ? ['.ipa'] : [];
|
|
506
|
+
|
|
507
|
+
if (wantedExtensions.length === 0) return;
|
|
508
|
+
|
|
509
|
+
let latestArtifact = null;
|
|
510
|
+
let latestMtime = 0;
|
|
511
|
+
|
|
512
|
+
for (const dir of searchDirs) {
|
|
513
|
+
if (!fs.existsSync(dir)) continue;
|
|
514
|
+
|
|
515
|
+
try {
|
|
516
|
+
const files = fs.readdirSync(dir);
|
|
517
|
+
for (const file of files) {
|
|
518
|
+
const ext = path.extname(file).toLowerCase();
|
|
519
|
+
if (!wantedExtensions.includes(ext)) continue;
|
|
520
|
+
|
|
521
|
+
const filePath = path.join(dir, file);
|
|
522
|
+
const stats = fs.statSync(filePath);
|
|
523
|
+
const fileMtime = stats.mtime.getTime();
|
|
524
|
+
|
|
525
|
+
if (fileMtime > Date.now() - 10 * 60 * 1000 && fileMtime > latestMtime) {
|
|
526
|
+
latestArtifact = filePath;
|
|
527
|
+
latestMtime = fileMtime;
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
} catch (e) {
|
|
531
|
+
// Ignore errors
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
if (!latestArtifact) return;
|
|
536
|
+
|
|
537
|
+
const ext = path.extname(latestArtifact).toLowerCase();
|
|
538
|
+
const targetDir = ok ? buildsDir : process.cwd();
|
|
539
|
+
const newPath = path.join(targetDir, `${customName}${ext}`);
|
|
540
|
+
|
|
541
|
+
fs.moveSync(latestArtifact, newPath, { overwrite: true });
|
|
542
|
+
logger.success(`Build artifact saved to: ${path.relative(process.cwd(), newPath)}`);
|
|
543
|
+
} catch (e) {
|
|
544
|
+
logger.warning('Could not rename build file: ' + e.message);
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
module.exports = buildCommand;
|