catalyst-core 1.2.0-canary.2 → 1.2.0-canary.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/changelog.md +34 -0
- package/dist/cli/hints.js +5 -0
- package/dist/errors/registry.js +2 -2
- package/dist/errors-index.json +2 -2
- package/dist/native/buildAndroid/assets.js +18 -1
- package/dist/native/buildAndroid/build.js +45 -3
- package/dist/native/buildAndroid/index.js +51 -18
- package/dist/native/buildAndroid/output.js +166 -4
- package/dist/native/buildAppAndroid.js +26 -1
- package/dist/native/buildAppIos.js +145 -5
- package/dist/native/buildIos/config.js +9 -2
- package/dist/native/buildIos/index.js +52 -14
- package/dist/native/buildIos/signing.js +624 -13
- package/dist/native/nativeConfig.js +64 -45
- package/dist/native/releaseOutput.js +34 -1
- package/dist/native/renameAndroidProject.js +23 -8
- package/dist/native/setupServer.js +280 -18
- package/dist/native/splashConfig.js +64 -0
- package/dist/native/utils.js +1 -1
- package/dist/scripts/preServerInit.js +14 -1
- package/dist/server/expressServer.js +21 -13
- package/dist/server/utils/validator.d.ts +8 -1
- package/dist/server/utils/validator.js +20 -1
- package/dist/web-router/components/MetaTag.jsx +17 -2
- package/dist/web-router/components/MetaTag.test.jsx +95 -0
- package/mcp_v2/conversion-tasks.json +29 -24
- package/mcp_v2/knowledge-base.json +39 -11
- package/mcp_v2/test/build.test.ts +136 -0
- package/mcp_v2/test/config.test.ts +253 -1
- package/mcp_v2/test/native-scripts.test.ts +129 -0
- package/mcp_v2/tools/build.js +61 -17
- package/mcp_v2/tools/config.d.ts +3 -0
- package/mcp_v2/tools/config.js +186 -78
- package/mcp_v2/tools/conversion.js +123 -21
- package/mcp_v2/tools/tasks.js +7 -0
- package/package.json +2 -2
- package/test/cli/buildErrors.test.ts +12 -0
- package/test/native/androidOutput.test.ts +228 -0
- package/test/native/androidSigning.test.ts +9 -5
- package/test/native/iosConfigPhase.test.ts +17 -4
- package/test/native/iosInstalledSigning.test.ts +746 -0
- package/test/native/iosSigning.test.ts +92 -0
- package/test/native/localState.test.ts +4 -5
- package/test/native/nativeConfig.test.ts +104 -11
- package/test/native/setupServer.test.ts +313 -8
- package/test/native/splashConfig.test.ts +177 -0
- package/test/server/validator.test.ts +37 -0
package/changelog.md
CHANGED
|
@@ -1,5 +1,39 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [1.2.0] - Unreleased
|
|
4
|
+
|
|
5
|
+
This release builds, runs and releases the Android and iOS apps from the CLI, without opening Android Studio or Xcode. The web runtime has no breaking changes. Universal apps should read the breaking changes below and the 1.1.x to 1.2.0 section of the Version Upgrades guide. After upgrading, run `npm audit fix`: the `body-parser` floor is raised to `^1.20.9`.
|
|
6
|
+
|
|
7
|
+
### One command to run, one to release
|
|
8
|
+
|
|
9
|
+
New apps have four scripts. `npm run android` and `npm run ios` check the toolchain and install what is missing after asking, start the dev server when nothing answers on `NODE_SERVER_PORT`, and build, install and open the app on an emulator, simulator or phone. Android also saves the app as a debug APK in `build-output/android/debug/` that can be shared. `npm run android:release` writes a signed App Bundle, a signed APK and `upload_certificate.pem` to `build-output/android/<version>-<build>/`, and `npm run ios:release` writes a signed IPA and a zip of its dSYMs to `build-output/ios/<version>-<build>/`. The scripts run `catalyst buildApp:android` and `catalyst buildApp:ios`, with `--release` for the store builds, so older apps keep `buildApp:<platform>` and build a release with `npm run buildApp:<platform> -- --release`. `setupEmulator:android` and `setupEmulator:ios` still work and are no longer needed.
|
|
10
|
+
|
|
11
|
+
### App config
|
|
12
|
+
|
|
13
|
+
A native app needs `WEBVIEW_CONFIG.appId`, `appName` and `url`. `appId` is the Android application id and the iOS bundle id, and `android.packageName` and `ios.bundleIdentifier` (then `ios.appBundleId`) win over it. `url` is the deployed site a release opens; debug builds load the dev server. New apps get `appId` and `appName` from the project name. A release run in a terminal asks once for an app id or website that is missing or a placeholder, and saves the answer to `config/config.json`.
|
|
14
|
+
|
|
15
|
+
### Signing
|
|
16
|
+
|
|
17
|
+
Signing files live in `credentials/android/` and `credentials/ios/`, which new apps keep out of git. The first Android release in a terminal creates one upload key per app after asking; back it up, because Google Play only accepts updates signed with it. `npx catalyst credentials:export <platform>` prints the folder as one value for the `CATALYST_ANDROID_CREDENTIALS` or `CATALYST_IOS_CREDENTIALS` CI secret. On a Mac signed in to Xcode, an iOS release signs with the Apple account or an installed App Store profile and needs no files; the order the build tries is in the iOS release guide. The version comes from `package.json`, and the build number is `CATALYST_BUILD_NUMBER` or the minutes since 2024-01-01 UTC. Android apps target API 36 and align their native libraries for 16 KB memory pages.
|
|
18
|
+
|
|
19
|
+
### Breaking changes
|
|
20
|
+
|
|
21
|
+
- An iOS bundle id with an underscore fails with `NATIVE-010`. Set `ios.bundleIdentifier` to an id without one.
|
|
22
|
+
- A release with a placeholder app id (any `com.example.` id or a template id) fails with `NATIVE-003`. A release whose start URL is `localhost`, a private or link-local address, a `.local` name or this machine fails with `NATIVE-001`, unless `WEBVIEW_CONFIG.allowPrivateHost` is `true`. Run in a terminal, the build asks for the value instead.
|
|
23
|
+
- A debug build no longer installs on a connected phone by itself. A terminal run asks once where to run the app and remembers the answer in `.catalyst/local.json`; without a terminal the emulator or simulator is used. Pass `--device` for the phone.
|
|
24
|
+
- The first debug build rewrites `config/config.json` once: a value equal to this machine's LAN address, and a private `LOCAL_IP` that `NODE_SERVER_HOSTNAME` also holds, go back to `localhost`. Each change is printed. It never runs for a release, in CI or with `allowPrivateHost: true`.
|
|
25
|
+
- Keystore passwords in `android.keystoreConfig` and `android.keystore` are no longer used. Apps that uploaded a build signed with the keystore shipped in the template, or with a key generated by an earlier release build, need an upload key reset in Play Console.
|
|
26
|
+
|
|
27
|
+
### create-catalyst-app
|
|
28
|
+
|
|
29
|
+
New apps get a new starter page, an animated HTML splash screen that is on by default (`splashScreen.minDuration` `7000`), a restyled offline page, a placeholder `public/icon.png`, and a `server/index.js` with an empty stub for `preServerInit` and each per-request hook. Their `.gitignore` covers `credentials/`, `.catalyst/` and `build-output/`.
|
|
30
|
+
|
|
31
|
+
### Fixes
|
|
32
|
+
|
|
33
|
+
- `MetaTag` waits for a `split()` route's chunk before it replaces the head tags, so the server's tags are no longer removed before the route's `setMetaData` is available.
|
|
34
|
+
- Startup warns when `server/index.js` puts a lifecycle hook inside `export default { ... }`, where Catalyst never calls it. Export each hook by name.
|
|
35
|
+
- `npm run start` prints a hint when `NODE_SERVER_HOSTNAME` is not an address of this machine.
|
|
36
|
+
|
|
3
37
|
## [1.1.0] - 2026-10-22
|
|
4
38
|
|
|
5
39
|
This release contains everything in 1.0.2, namely the HTML splash screen, experimental WebMCP support and the new CLI output, plus one fix. There are no breaking API changes.
|
package/dist/cli/hints.js
CHANGED
|
@@ -27,6 +27,11 @@ const HINTS = [
|
|
|
27
27
|
: `Something is already on ${where}. Stop it, or set NODE_SERVER_PORT in config/config.json.`
|
|
28
28
|
},
|
|
29
29
|
},
|
|
30
|
+
{
|
|
31
|
+
test: /listen EADDRNOTAVAIL\b(?:.*?address not available\s+(\S+):\d+)?/i,
|
|
32
|
+
hint: (match) =>
|
|
33
|
+
`NODE_SERVER_HOSTNAME ${match[1] ? `${match[1]} ` : ""}is not an address of this machine; set it to localhost (or this machine's address) in config/config.json.`,
|
|
34
|
+
},
|
|
30
35
|
{
|
|
31
36
|
test: /SDK location not found|ANDROID_HOME/i,
|
|
32
37
|
hint: () =>
|
package/dist/errors/registry.js
CHANGED
|
@@ -330,9 +330,9 @@ export const ERROR_DEFINITIONS = {
|
|
|
330
330
|
category: "IOS",
|
|
331
331
|
defaultMessage: "iOS code signing is not set up",
|
|
332
332
|
defaultDetails:
|
|
333
|
-
"A device or release build needs a signing identity and provisioning profile.
|
|
333
|
+
"A device or release build needs a signing identity and provisioning profile. A release signs with the first of: a .p12 and its profile in credentials/ios/, an installed ios.provisioningProfile, an App Store Connect API key in credentials/ios/, an App Store profile installed on this Mac for the app id (with its certificate in the keychain), or the Apple account signed in to Xcode for WEBVIEW_CONFIG.ios.developmentTeam. xcodebuild reported a signing failure, or a release build has no team to sign for. When an App Store release finds no App Store profile for its bundle id and has nothing else to sign with, the error says so first.",
|
|
334
334
|
suggestedAction:
|
|
335
|
-
"For
|
|
335
|
+
"For an App Store release, install the App Store provisioning profile for your bundle id (download it from developer.apple.com, or with Xcode > Settings > Accounts > Download Manual Profiles) or set WEBVIEW_CONFIG.ios.provisioningProfile to an installed profile's name. Otherwise, and for a device build, sign in to Xcode (Xcode > Settings > Accounts) with the Apple account that has your company's team. In CI, put the signing files in credentials/ios/ and set CATALYST_IOS_CREDENTIALS to the output of catalyst credentials:export ios.",
|
|
336
336
|
},
|
|
337
337
|
[ERROR_CODES.IOS_ASC_API_KEY_INVALID]: {
|
|
338
338
|
category: "IOS",
|
package/dist/errors-index.json
CHANGED
|
@@ -226,8 +226,8 @@
|
|
|
226
226
|
"IOS-010": {
|
|
227
227
|
"category": "IOS",
|
|
228
228
|
"message": "iOS code signing is not set up",
|
|
229
|
-
"details": "A device or release build needs a signing identity and provisioning profile.
|
|
230
|
-
"suggestedAction": "For
|
|
229
|
+
"details": "A device or release build needs a signing identity and provisioning profile. A release signs with the first of: a .p12 and its profile in credentials/ios/, an installed ios.provisioningProfile, an App Store Connect API key in credentials/ios/, an App Store profile installed on this Mac for the app id (with its certificate in the keychain), or the Apple account signed in to Xcode for WEBVIEW_CONFIG.ios.developmentTeam. xcodebuild reported a signing failure, or a release build has no team to sign for. When an App Store release finds no App Store profile for its bundle id and has nothing else to sign with, the error says so first.",
|
|
230
|
+
"suggestedAction": "For an App Store release, install the App Store provisioning profile for your bundle id (download it from developer.apple.com, or with Xcode > Settings > Accounts > Download Manual Profiles) or set WEBVIEW_CONFIG.ios.provisioningProfile to an installed profile's name. Otherwise, and for a device build, sign in to Xcode (Xcode > Settings > Accounts) with the Apple account that has your company's team. In CI, put the signing files in credentials/ios/ and set CATALYST_IOS_CREDENTIALS to the output of catalyst credentials:export ios.",
|
|
231
231
|
"docUrl": "https://github.com/tata1mg/catalyst-core/blob/main/errors/IOS/IOS-010.md"
|
|
232
232
|
},
|
|
233
233
|
"IOS-011": {
|
|
@@ -5,6 +5,7 @@ const path = require("path");
|
|
|
5
5
|
const {
|
|
6
6
|
checkAppIcon
|
|
7
7
|
} = require("../appIcon.js");
|
|
8
|
+
const splashConfig = require("../splashConfig.js");
|
|
8
9
|
function createAssetsPhase(ctx) {
|
|
9
10
|
const {
|
|
10
11
|
configPath,
|
|
@@ -123,8 +124,24 @@ function createAssetsPhase(ctx) {
|
|
|
123
124
|
async function copyOfflinePage() {
|
|
124
125
|
await copyPublicHtml("offline.html", "offline");
|
|
125
126
|
}
|
|
127
|
+
|
|
128
|
+
// The page paints the native splash colour once it takes over, so it gets the
|
|
129
|
+
// colour the native side resolves (SplashActivity and themes.xml default to white).
|
|
126
130
|
async function copySplashHtml() {
|
|
127
|
-
|
|
131
|
+
let backgroundColor = "#ffffff";
|
|
132
|
+
try {
|
|
133
|
+
const {
|
|
134
|
+
WEBVIEW_CONFIG = {}
|
|
135
|
+
} = JSON.parse(fs.readFileSync(configPath, "utf8"));
|
|
136
|
+
backgroundColor = WEBVIEW_CONFIG.splashScreen?.backgroundColor || backgroundColor;
|
|
137
|
+
} catch {
|
|
138
|
+
// An unreadable config fails the config phase; the copy itself still goes ahead.
|
|
139
|
+
}
|
|
140
|
+
try {
|
|
141
|
+
splashConfig.copySplashHtml(`${process.cwd()}/public/splashscreen.html`, `${pwd}/androidProject/app/src/main/assets/splash/splashscreen.html`, backgroundColor);
|
|
142
|
+
} catch (error) {
|
|
143
|
+
progress.log(`Warning: Error copying splashscreen.html: ${error.message}`, "warning");
|
|
144
|
+
}
|
|
128
145
|
}
|
|
129
146
|
|
|
130
147
|
/**
|
|
@@ -19,6 +19,12 @@ const {
|
|
|
19
19
|
const {
|
|
20
20
|
DEFAULT_VERSION_NAME
|
|
21
21
|
} = require("../releaseOutput.js");
|
|
22
|
+
const {
|
|
23
|
+
saveDebugApk
|
|
24
|
+
} = require("./output.js");
|
|
25
|
+
const {
|
|
26
|
+
displayPath
|
|
27
|
+
} = require("../../cli/theme.js");
|
|
22
28
|
|
|
23
29
|
// adb answers in milliseconds when healthy. A wedged adb server accepts the
|
|
24
30
|
// connection and never replies, so without a bound the build sat on
|
|
@@ -252,13 +258,39 @@ function createBuildPhase(ctx) {
|
|
|
252
258
|
* Build the debug APK and install it on `serial` only. Gradle's
|
|
253
259
|
* installDebug installs on every connected device unless ANDROID_SERIAL
|
|
254
260
|
* names one.
|
|
261
|
+
*
|
|
262
|
+
* The APK is copied to build-output/android/debug/ (see saveDebugApk),
|
|
263
|
+
* also when the install fails after Gradle packaged it: installDebug
|
|
264
|
+
* writes app-debug.apk before it installs, so one Gradle run is enough.
|
|
265
|
+
*
|
|
266
|
+
* @returns {Promise<{ apkPath: string | null }>}
|
|
255
267
|
*/
|
|
256
268
|
async function buildApp(androidConfig, buildOptimisation, serial, {
|
|
257
269
|
configPath = defaultConfigPath,
|
|
258
270
|
applicationId,
|
|
259
|
-
version
|
|
271
|
+
version,
|
|
272
|
+
appRoot = process.cwd(),
|
|
273
|
+
name,
|
|
274
|
+
usesDevServer = false
|
|
260
275
|
} = {}) {
|
|
261
276
|
progress.status("building and installing app");
|
|
277
|
+
const builtAfter = Date.now();
|
|
278
|
+
// Best effort: a copy that fails must not fail a build that
|
|
279
|
+
// installed, or hide the error of one that did not.
|
|
280
|
+
const keepApk = () => {
|
|
281
|
+
try {
|
|
282
|
+
return saveDebugApk({
|
|
283
|
+
projectPath: `${pwd}/androidProject`,
|
|
284
|
+
appRoot,
|
|
285
|
+
name,
|
|
286
|
+
usesDevServer,
|
|
287
|
+
builtAfter
|
|
288
|
+
});
|
|
289
|
+
} catch (error) {
|
|
290
|
+
progress.log(`Could not save the APK to build-output: ${error.message}`, "warning");
|
|
291
|
+
return null;
|
|
292
|
+
}
|
|
293
|
+
};
|
|
262
294
|
try {
|
|
263
295
|
// Gradle needs the SDK location in local.properties. Only
|
|
264
296
|
// setupEmulator:android wrote it, so building without a prior
|
|
@@ -290,11 +322,19 @@ function createBuildPhase(ctx) {
|
|
|
290
322
|
}
|
|
291
323
|
});
|
|
292
324
|
} catch (error) {
|
|
325
|
+
const apkPath = keepApk();
|
|
326
|
+
if (apkPath) {
|
|
327
|
+
error.apkPath = apkPath;
|
|
328
|
+
progress.log(`The APK was built and saved to ${displayPath(apkPath, appRoot)}`, "warning");
|
|
329
|
+
}
|
|
293
330
|
// Re-wrapping with a new Error dropped `output`, and with it the
|
|
294
331
|
// compiler errors. Keep the original and let the reporter mine it.
|
|
295
332
|
error.stage = "Building and installing the app";
|
|
296
333
|
throw error;
|
|
297
334
|
}
|
|
335
|
+
return {
|
|
336
|
+
apkPath: keepApk()
|
|
337
|
+
};
|
|
298
338
|
}
|
|
299
339
|
|
|
300
340
|
/** Open the installed app on `serial`. A failure is a warning: the app is installed. */
|
|
@@ -341,7 +381,7 @@ function createBuildPhase(ctx) {
|
|
|
341
381
|
}
|
|
342
382
|
|
|
343
383
|
/**
|
|
344
|
-
* Build the signed release bundle. The webview config is generated in
|
|
384
|
+
* Build the signed release bundle and APK. The webview config is generated in
|
|
345
385
|
* the temp_build_<pkg> copy that Gradle then bundles, from the resolved
|
|
346
386
|
* release config and with -PcatalystBuildType=release, so the release
|
|
347
387
|
* branch of generateWebViewConfig actually runs. Signing and version
|
|
@@ -353,7 +393,8 @@ function createBuildPhase(ctx) {
|
|
|
353
393
|
version,
|
|
354
394
|
applicationId,
|
|
355
395
|
buildOptimisation = false,
|
|
356
|
-
finalAabPath
|
|
396
|
+
finalAabPath,
|
|
397
|
+
finalApkPath
|
|
357
398
|
} = {}) {
|
|
358
399
|
progress.log("Building signed AAB for release...", "info");
|
|
359
400
|
try {
|
|
@@ -362,6 +403,7 @@ function createBuildPhase(ctx) {
|
|
|
362
403
|
if (androidConfig.sdkPath) ensureLocalProperties(`${pwd}/androidProject`, androidConfig.sdkPath);
|
|
363
404
|
const aabConfig = await createAABConfig(androidConfig);
|
|
364
405
|
if (finalAabPath) aabConfig.finalAabPath = finalAabPath;
|
|
406
|
+
if (finalApkPath) aabConfig.finalApkPath = finalApkPath;
|
|
365
407
|
aabConfig.gradle = {
|
|
366
408
|
configPath,
|
|
367
409
|
buildOptimisation,
|
|
@@ -40,12 +40,16 @@ const {
|
|
|
40
40
|
reachDevServer
|
|
41
41
|
} = require("./devices.js");
|
|
42
42
|
const {
|
|
43
|
+
RELEASE_APK_NOTE,
|
|
44
|
+
artifactName,
|
|
45
|
+
debugApkNotes,
|
|
43
46
|
releaseOutputDir,
|
|
44
47
|
releaseArtifactPaths
|
|
45
48
|
} = require("./output.js");
|
|
46
49
|
const {
|
|
47
50
|
DEFAULT_VERSION_NAME,
|
|
48
|
-
assertReleaseFolderFree
|
|
51
|
+
assertReleaseFolderFree,
|
|
52
|
+
ensureBuildOutputIgnore
|
|
49
53
|
} = require("../releaseOutput.js");
|
|
50
54
|
const {
|
|
51
55
|
applyJavaHome,
|
|
@@ -66,6 +70,10 @@ const {
|
|
|
66
70
|
const catalystCorePath = path.dirname(require.resolve("catalyst-core/package.json"));
|
|
67
71
|
const pwd = path.join(catalystCorePath, "dist/native");
|
|
68
72
|
const ANDROID_PACKAGE = "io.yourname.androidproject";
|
|
73
|
+
|
|
74
|
+
// The summary rows are "<key padded to 11> <value>"; a note under a value
|
|
75
|
+
// starts in the value column.
|
|
76
|
+
const SUMMARY_NOTE_INDENT = " ".repeat(11);
|
|
69
77
|
const steps = {
|
|
70
78
|
server: "Start dev server",
|
|
71
79
|
config: "Initialize configuration",
|
|
@@ -190,6 +198,7 @@ function createAndroidBuild(config, {
|
|
|
190
198
|
let resolved = null;
|
|
191
199
|
let signing = null;
|
|
192
200
|
let artifactPath = null;
|
|
201
|
+
let apkPath = null;
|
|
193
202
|
let certificatePath = null;
|
|
194
203
|
try {
|
|
195
204
|
const log = (message, type) => progress.log(message, type);
|
|
@@ -223,7 +232,7 @@ function createAndroidBuild(config, {
|
|
|
223
232
|
appRoot,
|
|
224
233
|
androidConfig,
|
|
225
234
|
version
|
|
226
|
-
}), [".aab"]);
|
|
235
|
+
}), [".aab", ".apk"]);
|
|
227
236
|
progress.complete("config");
|
|
228
237
|
|
|
229
238
|
// One-command dev: a debug build starts the dev server when
|
|
@@ -362,32 +371,45 @@ function createAndroidBuild(config, {
|
|
|
362
371
|
androidConfig,
|
|
363
372
|
version
|
|
364
373
|
});
|
|
365
|
-
const names = releaseArtifactPaths(outputDir, androidConfig
|
|
374
|
+
const names = releaseArtifactPaths(outputDir, artifactName(androidConfig));
|
|
375
|
+
ensureBuildOutputIgnore(outputDir, appRoot);
|
|
366
376
|
const aab = await buildSignedAAB(androidConfig, {
|
|
367
377
|
...gradleOptions,
|
|
368
378
|
signing,
|
|
369
379
|
buildOptimisation,
|
|
370
|
-
finalAabPath: names.aab
|
|
380
|
+
finalAabPath: names.aab,
|
|
381
|
+
finalApkPath: names.apk
|
|
371
382
|
});
|
|
372
383
|
artifactPath = aab?.finalAabPath || names.aab;
|
|
384
|
+
apkPath = aab?.finalApkPath || names.apk;
|
|
373
385
|
// The upload certificate beside the bundle (#508): Play asks
|
|
374
386
|
// for it when an upload key is registered or reset.
|
|
375
387
|
certificatePath = exportUploadCertificate(signing, names.certificate);
|
|
376
388
|
progress.complete("aab");
|
|
377
389
|
} else {
|
|
378
390
|
progress.start("build");
|
|
379
|
-
await buildApp(androidConfig, buildOptimisation, targetDevice.id,
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
const ports = devServerPorts(resolved.config, {
|
|
385
|
-
hmrPort: await readHmrPort({
|
|
386
|
-
cwd: appRoot
|
|
387
|
-
})
|
|
391
|
+
const debug = await buildApp(androidConfig, buildOptimisation, targetDevice.id, {
|
|
392
|
+
...gradleOptions,
|
|
393
|
+
appRoot,
|
|
394
|
+
name: artifactName(androidConfig),
|
|
395
|
+
usesDevServer: resolved.usesDevServer
|
|
388
396
|
});
|
|
389
|
-
|
|
390
|
-
|
|
397
|
+
apkPath = debug?.apkPath || null;
|
|
398
|
+
const packageName = `${applicationId}.debug`;
|
|
399
|
+
// A dev server build loads http://localhost:<port>; on the
|
|
400
|
+
// device that is the device itself. Forward every port the app
|
|
401
|
+
// uses to this machine, after the install and before the
|
|
402
|
+
// launch, every run. A build that loads a real host from
|
|
403
|
+
// LOCAL_IP reaches it directly and needs none of this.
|
|
404
|
+
if (resolved.usesDevServer) {
|
|
405
|
+
const ports = devServerPorts(resolved.config, {
|
|
406
|
+
hmrPort: await readHmrPort({
|
|
407
|
+
cwd: appRoot
|
|
408
|
+
})
|
|
409
|
+
});
|
|
410
|
+
reachDevServer(adbRunner(ADB_PATH), targetDevice.id, packageName, ports);
|
|
411
|
+
progress.log(`Forwarded ports ${ports.join(", ")} to this machine (adb reverse)`, "info");
|
|
412
|
+
}
|
|
391
413
|
launchApp(ADB_PATH, targetDevice.id, packageName);
|
|
392
414
|
progress.complete("build");
|
|
393
415
|
}
|
|
@@ -401,7 +423,18 @@ function createAndroidBuild(config, {
|
|
|
401
423
|
}];
|
|
402
424
|
if (artifactPath) {
|
|
403
425
|
summaryItems.push({
|
|
404
|
-
text: `
|
|
426
|
+
text: `aab ${displayPath(artifactPath, appRoot)}`,
|
|
427
|
+
color: "gray"
|
|
428
|
+
});
|
|
429
|
+
}
|
|
430
|
+
if (apkPath) {
|
|
431
|
+
summaryItems.push({
|
|
432
|
+
text: `apk ${displayPath(apkPath, appRoot)}`,
|
|
433
|
+
color: "gray"
|
|
434
|
+
});
|
|
435
|
+
const notes = release ? [RELEASE_APK_NOTE] : debugApkNotes(resolved.usesDevServer);
|
|
436
|
+
for (const note of notes) summaryItems.push({
|
|
437
|
+
text: `${SUMMARY_NOTE_INDENT}${note}`,
|
|
405
438
|
color: "gray"
|
|
406
439
|
});
|
|
407
440
|
}
|
|
@@ -421,9 +454,9 @@ function createAndroidBuild(config, {
|
|
|
421
454
|
});
|
|
422
455
|
progress.printTreeContent("Build", summaryItems);
|
|
423
456
|
if (release) {
|
|
424
|
-
progress.summary("Built the signed AAB", "Upload
|
|
457
|
+
progress.summary("Built the signed AAB and APK", "Upload the AAB to Google Play");
|
|
425
458
|
} else {
|
|
426
|
-
progress.summary(`Running on ${deviceName}`, "Edit your app; the dev server reloads it");
|
|
459
|
+
progress.summary(`Running on ${deviceName}`, resolved.usesDevServer ? "Edit your app; the dev server reloads it" : `The app loads ${resolved.startUrl || wvConfig.LOCAL_IP}`);
|
|
427
460
|
}
|
|
428
461
|
} catch (error) {
|
|
429
462
|
// Mark the step, then show what actually broke. The old handler
|
|
@@ -3,11 +3,58 @@
|
|
|
3
3
|
Object.defineProperty(exports, "__esModule", {
|
|
4
4
|
value: true
|
|
5
5
|
});
|
|
6
|
+
exports.RELEASE_GRADLE_TASKS = exports.RELEASE_APK_NOTE = void 0;
|
|
7
|
+
exports.apkBeside = apkBeside;
|
|
8
|
+
exports.artifactName = artifactName;
|
|
9
|
+
exports.builtReleaseApk = builtReleaseApk;
|
|
10
|
+
exports.debugApkNotes = debugApkNotes;
|
|
11
|
+
exports.debugApkPaths = debugApkPaths;
|
|
12
|
+
exports.gradleOutputs = gradleOutputs;
|
|
6
13
|
exports.releaseArtifactPaths = releaseArtifactPaths;
|
|
7
14
|
exports.releaseOutputDir = releaseOutputDir;
|
|
15
|
+
exports.saveDebugApk = saveDebugApk;
|
|
16
|
+
var _fs = _interopRequireDefault(require("fs"));
|
|
8
17
|
var _path = _interopRequireDefault(require("path"));
|
|
9
18
|
var _releaseOutput = require("../releaseOutput.js");
|
|
10
19
|
function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
|
|
20
|
+
/**
|
|
21
|
+
* The Gradle tasks a release runs, in one Gradle run: the bundle for Play
|
|
22
|
+
* and a universal APK for test installs. The release buildType's
|
|
23
|
+
* signingConfig signs both, and the app sets no ABI splits, so the APK is
|
|
24
|
+
* a single app-release.apk.
|
|
25
|
+
*/
|
|
26
|
+
const RELEASE_GRADLE_TASKS = exports.RELEASE_GRADLE_TASKS = ["clean", "bundleRelease", "assembleRelease"];
|
|
27
|
+
|
|
28
|
+
/** The note printed under a release APK. */
|
|
29
|
+
const RELEASE_APK_NOTE = exports.RELEASE_APK_NOTE = "The APK is for test installs; uninstall a Play-installed copy first.";
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* A debug APK copied after a failed run must come from this run: a step
|
|
33
|
+
* that fails before Gradle's clean leaves the previous run's APK in place.
|
|
34
|
+
* The slack covers file systems that store mtimes in whole seconds.
|
|
35
|
+
*/
|
|
36
|
+
const MTIME_SLACK_MS = 2000;
|
|
37
|
+
|
|
38
|
+
/** The name the AAB and APK files are given, before artifactBaseName makes it file-name safe. */
|
|
39
|
+
function artifactName(androidConfig = {}) {
|
|
40
|
+
return androidConfig.newProjectName || androidConfig.appName || "app";
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Where Gradle writes its outputs in an Android project (androidProject or
|
|
45
|
+
* the temp_build copy a release builds in).
|
|
46
|
+
*
|
|
47
|
+
* @param {string} projectPath
|
|
48
|
+
*/
|
|
49
|
+
function gradleOutputs(projectPath) {
|
|
50
|
+
const outputs = _path.default.join(projectPath, "app", "build", "outputs");
|
|
51
|
+
return {
|
|
52
|
+
debugApk: _path.default.join(outputs, "apk", "debug", "app-debug.apk"),
|
|
53
|
+
releaseAab: _path.default.join(outputs, "bundle", "release", "app-release.aab"),
|
|
54
|
+
releaseApk: _path.default.join(outputs, "apk", "release", "app-release.apk")
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
11
58
|
/**
|
|
12
59
|
* Where a release puts its files: build-output/android/<version>-<build>/
|
|
13
60
|
* in the app, or <android.outputPath>/<version>-<build>/ when the config
|
|
@@ -24,20 +71,135 @@ function releaseOutputDir({
|
|
|
24
71
|
androidConfig = {},
|
|
25
72
|
version
|
|
26
73
|
}) {
|
|
27
|
-
const root = androidConfig.outputPath ? _path.default.resolve(appRoot, androidConfig.outputPath) : _path.default.join(appRoot,
|
|
74
|
+
const root = androidConfig.outputPath ? _path.default.resolve(appRoot, androidConfig.outputPath) : _path.default.join(appRoot, _releaseOutput.BUILD_OUTPUT_DIR, "android");
|
|
28
75
|
return _path.default.join(root, (0, _releaseOutput.releaseFolderName)(version));
|
|
29
76
|
}
|
|
30
77
|
|
|
31
78
|
/**
|
|
32
|
-
* The release files in that folder: the bundle for Play
|
|
33
|
-
* certificate (PEM) that identifies the key that
|
|
79
|
+
* The release files in that folder: the bundle for Play, the APK for test
|
|
80
|
+
* installs, and the upload certificate (PEM) that identifies the key that
|
|
81
|
+
* signed both.
|
|
34
82
|
*
|
|
35
83
|
* @param {string} dir - releaseOutputDir()
|
|
36
84
|
* @param {string} name - the app's name; made file-name safe here
|
|
37
85
|
*/
|
|
38
86
|
function releaseArtifactPaths(dir, name) {
|
|
87
|
+
const base = (0, _releaseOutput.artifactBaseName)(name);
|
|
39
88
|
return {
|
|
40
|
-
aab: _path.default.join(dir, `${
|
|
89
|
+
aab: _path.default.join(dir, `${base}.aab`),
|
|
90
|
+
apk: _path.default.join(dir, `${base}.apk`),
|
|
41
91
|
certificate: _path.default.join(dir, "upload_certificate.pem")
|
|
42
92
|
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** The APK beside an AAB: the same name with .apk. */
|
|
96
|
+
function apkBeside(aabPath) {
|
|
97
|
+
return `${aabPath.replace(/\.aab$/i, "")}.apk`;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* The signed release APK Gradle built, or a clear error. Without a signing
|
|
102
|
+
* config Gradle names it app-release-unsigned.apk, which no phone installs.
|
|
103
|
+
*
|
|
104
|
+
* @param {string} projectPath
|
|
105
|
+
* @returns {string}
|
|
106
|
+
*/
|
|
107
|
+
function builtReleaseApk(projectPath) {
|
|
108
|
+
const {
|
|
109
|
+
releaseApk
|
|
110
|
+
} = gradleOutputs(projectPath);
|
|
111
|
+
if (_fs.default.existsSync(releaseApk)) return releaseApk;
|
|
112
|
+
const unsigned = _path.default.join(_path.default.dirname(releaseApk), "app-release-unsigned.apk");
|
|
113
|
+
if (_fs.default.existsSync(unsigned)) {
|
|
114
|
+
throw new Error(`The release APK was built without the upload key (${unsigned}), so it cannot be installed`);
|
|
115
|
+
}
|
|
116
|
+
throw new Error(`Signed APK file was not generated at ${releaseApk}`);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Where a debug build keeps its APK: build-output/android/debug/, under a
|
|
121
|
+
* fixed name that each run overwrites. A build that loads this computer's
|
|
122
|
+
* dev server gets its own name, since it only works here.
|
|
123
|
+
*
|
|
124
|
+
* @param {object} options
|
|
125
|
+
* @param {string} [options.appRoot]
|
|
126
|
+
* @param {string} [options.name] - the app's name; made file-name safe here
|
|
127
|
+
* @param {boolean} [options.usesDevServer] - resolveNativeConfig().usesDevServer
|
|
128
|
+
* @returns {{ dir: string, apk: string, stale: string }} `stale` is the other variant's file
|
|
129
|
+
*/
|
|
130
|
+
function debugApkPaths({
|
|
131
|
+
appRoot = process.cwd(),
|
|
132
|
+
name,
|
|
133
|
+
usesDevServer = false
|
|
134
|
+
} = {}) {
|
|
135
|
+
const dir = _path.default.join(appRoot, _releaseOutput.BUILD_OUTPUT_DIR, "android", "debug");
|
|
136
|
+
const base = (0, _releaseOutput.artifactBaseName)(name);
|
|
137
|
+
const realHost = _path.default.join(dir, `${base}-debug.apk`);
|
|
138
|
+
const devServer = _path.default.join(dir, `${base}-debug-devserver.apk`);
|
|
139
|
+
return usesDevServer ? {
|
|
140
|
+
dir,
|
|
141
|
+
apk: devServer,
|
|
142
|
+
stale: realHost
|
|
143
|
+
} : {
|
|
144
|
+
dir,
|
|
145
|
+
apk: realHost,
|
|
146
|
+
stale: devServer
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Copy the debug APK Gradle built into build-output/android/debug/ and
|
|
152
|
+
* remove the other variant's file there, so only the current one exists.
|
|
153
|
+
* Returns null, and changes nothing, when there is no APK from this run.
|
|
154
|
+
*
|
|
155
|
+
* @param {object} options
|
|
156
|
+
* @param {string} options.projectPath - the androidProject Gradle built in
|
|
157
|
+
* @param {string} [options.appRoot]
|
|
158
|
+
* @param {string} [options.name]
|
|
159
|
+
* @param {boolean} [options.usesDevServer]
|
|
160
|
+
* @param {number} [options.builtAfter] - ms timestamp; an older APK is from an earlier run
|
|
161
|
+
* @returns {string | null} the saved APK's path
|
|
162
|
+
*/
|
|
163
|
+
function saveDebugApk({
|
|
164
|
+
projectPath,
|
|
165
|
+
appRoot = process.cwd(),
|
|
166
|
+
name,
|
|
167
|
+
usesDevServer = false,
|
|
168
|
+
builtAfter = 0
|
|
169
|
+
}) {
|
|
170
|
+
const source = gradleOutputs(projectPath).debugApk;
|
|
171
|
+
let stat;
|
|
172
|
+
try {
|
|
173
|
+
stat = _fs.default.statSync(source);
|
|
174
|
+
} catch (error) {
|
|
175
|
+
return null;
|
|
176
|
+
}
|
|
177
|
+
if (builtAfter && stat.mtimeMs < builtAfter - MTIME_SLACK_MS) return null;
|
|
178
|
+
const {
|
|
179
|
+
dir,
|
|
180
|
+
apk,
|
|
181
|
+
stale
|
|
182
|
+
} = debugApkPaths({
|
|
183
|
+
appRoot,
|
|
184
|
+
name,
|
|
185
|
+
usesDevServer
|
|
186
|
+
});
|
|
187
|
+
(0, _releaseOutput.ensureBuildOutputIgnore)(dir, appRoot);
|
|
188
|
+
_fs.default.mkdirSync(dir, {
|
|
189
|
+
recursive: true
|
|
190
|
+
});
|
|
191
|
+
_fs.default.copyFileSync(source, apk);
|
|
192
|
+
_fs.default.rmSync(stale, {
|
|
193
|
+
force: true
|
|
194
|
+
});
|
|
195
|
+
return apk;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/** The lines printed under a debug APK in the build summary. */
|
|
199
|
+
function debugApkNotes(usesDevServer = false) {
|
|
200
|
+
const notes = ["Test build: debuggable, for testing only."];
|
|
201
|
+
if (usesDevServer) {
|
|
202
|
+
notes.push("Only works with this computer's dev server. To share, set WEBVIEW_CONFIG.LOCAL_IP to your site, or run android:release.");
|
|
203
|
+
}
|
|
204
|
+
return notes;
|
|
43
205
|
}
|
|
@@ -15,6 +15,10 @@ const {
|
|
|
15
15
|
buildFailure
|
|
16
16
|
} = require("../cli/diagnostic.js");
|
|
17
17
|
const {
|
|
18
|
+
askReleaseConfig
|
|
19
|
+
} = require("./setupServer.js");
|
|
20
|
+
const {
|
|
21
|
+
isInteractive,
|
|
18
22
|
parseNativeArgs
|
|
19
23
|
} = require("./utils.js");
|
|
20
24
|
const {
|
|
@@ -34,12 +38,33 @@ async function main() {
|
|
|
34
38
|
}));
|
|
35
39
|
process.exit(1);
|
|
36
40
|
}
|
|
41
|
+
// The build reads config.json from disk when it starts (initializeConfig),
|
|
42
|
+
// so answers saved below reach it; the config passed here is not read again.
|
|
37
43
|
const {
|
|
38
|
-
buildAndroidApp
|
|
44
|
+
buildAndroidApp,
|
|
45
|
+
progress
|
|
39
46
|
} = createAndroidBuild({
|
|
40
47
|
WEBVIEW_CONFIG,
|
|
41
48
|
BUILD_OUTPUT_PATH
|
|
42
49
|
}, flags);
|
|
50
|
+
|
|
51
|
+
// A store build in a terminal asks once for a missing app id or website
|
|
52
|
+
// and saves the answers. Its errors are left for the build, which
|
|
53
|
+
// resolves the config again and reports them the usual way.
|
|
54
|
+
if (isInteractive()) {
|
|
55
|
+
const log = (message, type) => progress.log(message, type);
|
|
56
|
+
try {
|
|
57
|
+
await askReleaseConfig("android", {
|
|
58
|
+
release: flags.release,
|
|
59
|
+
url: flags.url,
|
|
60
|
+
log
|
|
61
|
+
});
|
|
62
|
+
} catch (error) {
|
|
63
|
+
if (!/^NATIVE-\d{3}$/.test(error?.code)) {
|
|
64
|
+
log(`Could not read or update config/config.json: ${error.message}`, "warning");
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
43
68
|
try {
|
|
44
69
|
await buildAndroidApp();
|
|
45
70
|
} catch (error) {
|