local-expo-build 0.2.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.
Files changed (58) hide show
  1. package/CHANGELOG.md +99 -0
  2. package/LICENSE +21 -0
  3. package/README.md +372 -0
  4. package/bin/local-expo-build.js +2 -0
  5. package/dist/cli.js +33 -0
  6. package/dist/cli.js.map +1 -0
  7. package/dist/commands/build.js +121 -0
  8. package/dist/commands/build.js.map +1 -0
  9. package/dist/commands/doctor.js +606 -0
  10. package/dist/commands/doctor.js.map +1 -0
  11. package/dist/commands/init.js +125 -0
  12. package/dist/commands/init.js.map +1 -0
  13. package/dist/commands/keystore.js +47 -0
  14. package/dist/commands/keystore.js.map +1 -0
  15. package/dist/core/bumpVersion.js +70 -0
  16. package/dist/core/bumpVersion.js.map +1 -0
  17. package/dist/core/easLink.js +64 -0
  18. package/dist/core/easLink.js.map +1 -0
  19. package/dist/core/expoConfig.js +71 -0
  20. package/dist/core/expoConfig.js.map +1 -0
  21. package/dist/core/gradleRun.js +31 -0
  22. package/dist/core/gradleRun.js.map +1 -0
  23. package/dist/core/keystore/easFetch.js +109 -0
  24. package/dist/core/keystore/easFetch.js.map +1 -0
  25. package/dist/core/keystore/existing.js +135 -0
  26. package/dist/core/keystore/existing.js.map +1 -0
  27. package/dist/core/keystore/generate.js +72 -0
  28. package/dist/core/keystore/generate.js.map +1 -0
  29. package/dist/core/keystore/index.js +62 -0
  30. package/dist/core/keystore/index.js.map +1 -0
  31. package/dist/core/keystore/rehydrate.js +88 -0
  32. package/dist/core/keystore/rehydrate.js.map +1 -0
  33. package/dist/core/pinGradle.js +50 -0
  34. package/dist/core/pinGradle.js.map +1 -0
  35. package/dist/core/prebuild.js +16 -0
  36. package/dist/core/prebuild.js.map +1 -0
  37. package/dist/core/sdkDetect.js +26 -0
  38. package/dist/core/sdkDetect.js.map +1 -0
  39. package/dist/core/setupSigning.js +168 -0
  40. package/dist/core/setupSigning.js.map +1 -0
  41. package/dist/core/syncEasVersion.js +97 -0
  42. package/dist/core/syncEasVersion.js.map +1 -0
  43. package/dist/core/writeCredentialsJson.js +51 -0
  44. package/dist/core/writeCredentialsJson.js.map +1 -0
  45. package/dist/util/ctx.js +17 -0
  46. package/dist/util/ctx.js.map +1 -0
  47. package/dist/util/gitignore.js +23 -0
  48. package/dist/util/gitignore.js.map +1 -0
  49. package/dist/util/log.js +16 -0
  50. package/dist/util/log.js.map +1 -0
  51. package/package.json +64 -0
  52. package/templates/keystore.properties.example +4 -0
  53. package/templates/scripts/build.js +79 -0
  54. package/templates/scripts/bump-version.js +65 -0
  55. package/templates/scripts/pin-gradle.js +45 -0
  56. package/templates/scripts/print-artifact.js +36 -0
  57. package/templates/scripts/setup-signing.js +137 -0
  58. package/templates/scripts/sync-eas-version.js +59 -0
@@ -0,0 +1,45 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Pins the Android Gradle wrapper to a known-good version after `expo prebuild`.
4
+ * Generated by local-expo-build. Safe to re-run. Edit GRADLE_PIN below if you
5
+ * upgrade Expo SDK.
6
+ */
7
+ const fs = require('fs');
8
+ const path = require('path');
9
+
10
+ const GRADLE_PIN = {
11
+ 50: null, 51: null, 52: null, 53: null, 54: null,
12
+ 55: '8.13',
13
+ 56: null,
14
+ };
15
+
16
+ function detectSdkMajor() {
17
+ const pkg = JSON.parse(fs.readFileSync(path.resolve(__dirname, '../package.json'), 'utf8'));
18
+ const raw = (pkg.dependencies && pkg.dependencies.expo) || (pkg.devDependencies && pkg.devDependencies.expo) || '';
19
+ const m = String(raw).match(/(\d+)/);
20
+ return m ? parseInt(m[1], 10) : 0;
21
+ }
22
+
23
+ const sdk = detectSdkMajor();
24
+ const PINNED = GRADLE_PIN[sdk];
25
+ if (!PINNED) {
26
+ console.log(`[pin-gradle] SDK ${sdk}: no pin required.`);
27
+ process.exit(0);
28
+ }
29
+
30
+ const WRAPPER = path.resolve(__dirname, '..', 'android', 'gradle', 'wrapper', 'gradle-wrapper.properties');
31
+ if (!fs.existsSync(WRAPPER)) {
32
+ console.log('[pin-gradle] android/ not yet generated; nothing to do.');
33
+ process.exit(0);
34
+ }
35
+
36
+ const raw = fs.readFileSync(WRAPPER, 'utf8');
37
+ const reUrl = /^distributionUrl=.*/m;
38
+ const desired = `distributionUrl=https\\://services.gradle.org/distributions/gradle-${PINNED}-bin.zip`;
39
+ const current = raw.match(reUrl)?.[0] ?? '';
40
+ if (current === desired) {
41
+ console.log(`[pin-gradle] already pinned to Gradle ${PINNED}`);
42
+ process.exit(0);
43
+ }
44
+ fs.writeFileSync(WRAPPER, raw.replace(reUrl, desired), 'utf8');
45
+ console.log(`[pin-gradle] pinned Gradle wrapper to ${PINNED}`);
@@ -0,0 +1,36 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Prints the absolute path (and size) of the freshly built APK / AAB.
4
+ * Runs at the very end of the build chain so you always know what was produced.
5
+ * Generated by local-expo-build.
6
+ *
7
+ * Usage: node scripts/print-artifact.js (apk|aab)
8
+ */
9
+ const fs = require('fs');
10
+ const path = require('path');
11
+
12
+ const kind = (process.argv[2] || '').toLowerCase();
13
+ if (kind !== 'apk' && kind !== 'aab') {
14
+ console.error('Usage: node scripts/print-artifact.js (apk|aab)');
15
+ process.exit(1);
16
+ }
17
+
18
+ const PROJECT_ROOT = path.resolve(__dirname, '..');
19
+ const artifact =
20
+ kind === 'aab'
21
+ ? path.join(PROJECT_ROOT, 'android', 'app', 'build', 'outputs', 'bundle', 'release', 'app-release.aab')
22
+ : path.join(PROJECT_ROOT, 'android', 'app', 'build', 'outputs', 'apk', 'release', 'app-release.apk');
23
+
24
+ if (!fs.existsSync(artifact)) {
25
+ console.error('Expected ' + kind.toUpperCase() + ' not found at:');
26
+ console.error(' ' + artifact);
27
+ console.error('Check the Gradle output above for errors.');
28
+ process.exit(1);
29
+ }
30
+
31
+ const stat = fs.statSync(artifact);
32
+ const sizeMb = (stat.size / 1024 / 1024).toFixed(2);
33
+ console.log('');
34
+ console.log('Build artifact (' + kind.toUpperCase() + ', ' + sizeMb + ' MB):');
35
+ console.log(' ' + artifact);
36
+ console.log('');
@@ -0,0 +1,137 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Injects the release signing config into android/app/build.gradle.
4
+ * Reads credentials from keystore.properties at project root (gitignored).
5
+ * Also restores the .jks into android/app/ if `expo prebuild --clean` wiped it.
6
+ * Generated by local-expo-build.
7
+ */
8
+ const fs = require('fs');
9
+ const path = require('path');
10
+
11
+ const PROJECT_ROOT = path.resolve(__dirname, '..');
12
+ const GRADLE_PATH = path.join(PROJECT_ROOT, 'android', 'app', 'build.gradle');
13
+ const PROPS_PATH = path.join(PROJECT_ROOT, 'keystore.properties');
14
+
15
+ function isNonEmptyFile(p) {
16
+ try { return fs.existsSync(p) && fs.statSync(p).size > 0; } catch { return false; }
17
+ }
18
+
19
+ function ensureKeystoreInAndroidApp(cwd, storeFile) {
20
+ const destDir = path.join(cwd, 'android', 'app');
21
+ const dest = path.join(destDir, storeFile);
22
+ if (isNonEmptyFile(dest)) return;
23
+
24
+ const candidates = [];
25
+
26
+ const credPath = path.join(cwd, 'credentials.json');
27
+ if (fs.existsSync(credPath)) {
28
+ try {
29
+ const cred = JSON.parse(fs.readFileSync(credPath, 'utf8'));
30
+ const ksPath = cred && cred.android && cred.android.keystore && cred.android.keystore.keystorePath;
31
+ if (typeof ksPath === 'string' && ksPath.trim()) {
32
+ candidates.push(path.resolve(cwd, ksPath));
33
+ }
34
+ } catch (_) {}
35
+ }
36
+ candidates.push(path.join(cwd, 'credentials', 'android', storeFile));
37
+ candidates.push(path.join(cwd, 'credentials', 'android', 'keystore.jks'));
38
+ candidates.push(path.join(cwd, storeFile));
39
+
40
+ const found = candidates.find((p) => {
41
+ const abs = path.resolve(p);
42
+ return abs !== path.resolve(dest) && isNonEmptyFile(abs);
43
+ });
44
+
45
+ if (!found) {
46
+ const tried = candidates.map((p) => ' - ' + path.relative(cwd, p).replace(/\\/g, '/')).join('\n');
47
+ console.error(
48
+ 'Keystore file ' + path.relative(cwd, dest).replace(/\\/g, '/') + ' not found, ' +
49
+ 'and no recovery source available.\nTried:\n' + tried + '\n\n' +
50
+ 'If your android/ directory was just wiped by `expo prebuild --clean`, ' +
51
+ 're-run `npx local-expo-build keystore rehydrate` or `keystore import <path>` to restore it.'
52
+ );
53
+ process.exit(1);
54
+ }
55
+
56
+ fs.mkdirSync(destDir, { recursive: true });
57
+ fs.copyFileSync(found, dest);
58
+ console.log(
59
+ 'Restored keystore: ' + path.relative(cwd, found).replace(/\\/g, '/') +
60
+ ' → ' + path.relative(cwd, dest).replace(/\\/g, '/')
61
+ );
62
+ }
63
+
64
+ if (!fs.existsSync(PROPS_PATH)) {
65
+ console.error('keystore.properties not found at project root.');
66
+ console.error('Run: npx local-expo-build keystore setup');
67
+ process.exit(1);
68
+ }
69
+
70
+ const props = {};
71
+ fs.readFileSync(PROPS_PATH, 'utf8').split('\n').forEach((line) => {
72
+ const t = line.trim();
73
+ if (!t || t.startsWith('#')) return;
74
+ const i = t.indexOf('=');
75
+ if (i === -1) return;
76
+ props[t.slice(0, i).trim()] = t.slice(i + 1).trim();
77
+ });
78
+
79
+ const missing = ['storeFile', 'storePassword', 'keyAlias', 'keyPassword'].filter(
80
+ (k) => !props[k] || props[k] === 'FILL_IN'
81
+ );
82
+ if (missing.length) {
83
+ console.error(`keystore.properties missing: ${missing.join(', ')}`);
84
+ process.exit(1);
85
+ }
86
+
87
+ if (!fs.existsSync(GRADLE_PATH)) {
88
+ console.error('android/app/build.gradle not found — run `expo prebuild` first.');
89
+ process.exit(1);
90
+ }
91
+
92
+ // Survive `expo prebuild --clean` wiping android/: restore .jks from a stable source.
93
+ ensureKeystoreInAndroidApp(PROJECT_ROOT, props.storeFile);
94
+
95
+ let gradle = fs.readFileSync(GRADLE_PATH, 'utf8');
96
+ if (gradle.includes('signingConfigs.release')) {
97
+ console.log('Release signing config already present — skipping.');
98
+ process.exit(0);
99
+ }
100
+
101
+ const DEFAULT = ` signingConfigs {
102
+ debug {
103
+ storeFile file('debug.keystore')
104
+ storePassword 'android'
105
+ keyAlias 'androiddebugkey'
106
+ keyPassword 'android'
107
+ }
108
+ }`;
109
+
110
+ const REPLACEMENT = ` signingConfigs {
111
+ debug {
112
+ storeFile file('debug.keystore')
113
+ storePassword 'android'
114
+ keyAlias 'androiddebugkey'
115
+ keyPassword 'android'
116
+ }
117
+ release {
118
+ storeFile file('${props.storeFile}')
119
+ storePassword '${props.storePassword}'
120
+ keyAlias '${props.keyAlias}'
121
+ keyPassword '${props.keyPassword}'
122
+ }
123
+ }`;
124
+
125
+ if (!gradle.includes(DEFAULT)) {
126
+ console.error('Could not find default signingConfigs block in build.gradle.');
127
+ process.exit(1);
128
+ }
129
+ gradle = gradle.replace(DEFAULT, REPLACEMENT);
130
+
131
+ const TOKEN = 'signingConfig signingConfigs.debug';
132
+ const parts = gradle.split(TOKEN);
133
+ if (parts.length >= 3) {
134
+ gradle = parts[0] + TOKEN + parts[1] + 'signingConfig signingConfigs.release' + parts.slice(2).join(TOKEN);
135
+ }
136
+ fs.writeFileSync(GRADLE_PATH, gradle, 'utf8');
137
+ console.log(`Release signing config injected (alias=${props.keyAlias})`);
@@ -0,0 +1,59 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Reads versionCode from android/app/build.gradle and pushes it to EAS via
4
+ * api.expo.dev GraphQL. Generated by local-expo-build.
5
+ */
6
+ const https = require('https');
7
+ const fs = require('fs');
8
+ const path = require('path');
9
+ const os = require('os');
10
+
11
+ const GRADLE = path.resolve(__dirname, '../android/app/build.gradle');
12
+ const APP_JSON = path.resolve(__dirname, '../app.json');
13
+
14
+ const gradle = fs.readFileSync(GRADLE, 'utf8');
15
+ const m = gradle.match(/\bversionCode\s+(\d+)/);
16
+ if (!m) { console.error('versionCode not found'); process.exit(1); }
17
+ const versionCode = m[1];
18
+
19
+ const app = JSON.parse(fs.readFileSync(APP_JSON, 'utf8'));
20
+ const projectId = app.expo?.extra?.eas?.projectId;
21
+ const applicationId = app.expo?.android?.package;
22
+ const storeVersion = app.expo?.version ?? '1.0.0';
23
+ if (!projectId) { console.log('No projectId — skip EAS sync'); process.exit(0); }
24
+
25
+ function auth() {
26
+ if (process.env.EXPO_TOKEN) return { token: process.env.EXPO_TOKEN };
27
+ const sp = path.join(os.homedir(), '.expo', 'state.json');
28
+ if (!fs.existsSync(sp)) { console.error('Run `eas login` first.'); process.exit(1); }
29
+ const s = JSON.parse(fs.readFileSync(sp, 'utf8'));
30
+ if (!s?.auth?.sessionSecret) { console.error('No session in ~/.expo/state.json'); process.exit(1); }
31
+ return { sessionSecret: s.auth.sessionSecret };
32
+ }
33
+ const a = auth();
34
+
35
+ const body = JSON.stringify({
36
+ query: `mutation($i: AppVersionInput!) { appVersion { createAppVersion(appVersionInput: $i) { id } } }`,
37
+ variables: { i: { appId: projectId, platform: 'ANDROID', applicationIdentifier: applicationId, storeVersion, buildVersion: String(versionCode) } },
38
+ });
39
+ const headers = {
40
+ 'Content-Type': 'application/json',
41
+ 'Content-Length': Buffer.byteLength(body),
42
+ 'expo-client-info': JSON.stringify({ appVersion: '0.0.0', sdkVersion: '0.0.0' }),
43
+ };
44
+ if (a.sessionSecret) headers['expo-session'] = a.sessionSecret;
45
+ if (a.token) headers['authorization'] = `Bearer ${a.token}`;
46
+
47
+ const req = https.request({ hostname: 'api.expo.dev', path: '/graphql', method: 'POST', headers }, (res) => {
48
+ let d = '';
49
+ res.on('data', (c) => (d += c));
50
+ res.on('end', () => {
51
+ try {
52
+ const j = JSON.parse(d);
53
+ if (j.errors) { console.error('EAS API:', JSON.stringify(j.errors)); process.exit(1); }
54
+ console.log(`EAS remote versionCode set to ${versionCode}`);
55
+ } catch { console.error('Bad response:', d); process.exit(1); }
56
+ });
57
+ });
58
+ req.on('error', (e) => { console.error(e.message); process.exit(1); });
59
+ req.write(body); req.end();