catalyst-core 1.0.2-canary.1 → 1.0.2
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/bin/catalyst.js +105 -1
- package/changelog.md +34 -0
- package/dist/cli/README.md +119 -0
- package/dist/cli/appConfig.js +51 -0
- package/dist/cli/buildErrors.js +170 -0
- package/dist/cli/buildWarnings.js +28 -0
- package/dist/cli/devOutput.js +524 -0
- package/dist/cli/diagnostic.js +388 -0
- package/dist/cli/hints.js +69 -0
- package/dist/cli/package.json +3 -0
- package/dist/cli/spinner.js +141 -0
- package/dist/cli/theme.js +155 -0
- package/dist/errors/index.js +7 -3
- package/dist/logger.js +61 -6
- package/dist/native/androidProject/app/build.gradle.kts +2 -1
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/CustomWebview.kt +2 -0
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/MainActivity.kt +9 -0
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/SplashActivity.kt +65 -34
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/SplashOverlay.kt +109 -0
- package/dist/native/androidSetup.js +46 -57
- package/dist/native/buildAndroid/assets.js +22 -13
- package/dist/native/buildAndroid/build.js +98 -26
- package/dist/native/buildAndroid/config.js +43 -12
- package/dist/native/buildAndroid/index.js +49 -191
- package/dist/native/buildAppAndroid.js +19 -8
- package/dist/native/buildAppIos.js +35 -16
- package/dist/native/buildIos/assets.js +1 -6
- package/dist/native/buildIos/build.js +66 -95
- package/dist/native/buildIos/config.js +26 -8
- package/dist/native/buildIos/index.js +143 -64
- package/dist/native/buildIos/plugins.js +12 -15
- package/dist/native/internalPluginUtils.js +0 -1
- package/dist/native/iosnativeWebView/CoreLogic/Sources/CatalystCoreLogic/Constants/ConfigConstants.swift +2 -0
- package/dist/native/iosnativeWebView/Sources/Core/WebView/SplashHTMLView.swift +31 -0
- package/dist/native/iosnativeWebView/Sources/Core/WebView/SplashView.swift +17 -1
- package/dist/native/iosnativeWebView/Sources/Core/WebView/SplashViewModel.swift +41 -1
- package/dist/native/iosnativeWebView/Sources/Core/WebView/WebViewModel.swift +2 -0
- package/dist/native/iosnativeWebView/Sources/Core/WebView/WebViewNavigationDelegate.swift +3 -0
- package/dist/native/pluginComposerAndroid.js +2 -3
- package/dist/native/renameAndroidProject.js +42 -20
- package/dist/native/setupEmulatorIos.js +28 -49
- package/dist/native/setupServer.js +12 -15
- package/dist/native/terminalProgress.js +186 -147
- package/dist/native/utils.js +340 -54
- package/dist/otel.js +15 -9
- package/dist/scripts/build.js +313 -42
- package/dist/scripts/generateOfflineManifest.js +11 -1
- package/dist/scripts/loadEnvironmentVariables.js +24 -2
- package/dist/scripts/preServerInit.js +32 -10
- package/dist/scripts/preflight.js +35 -11
- package/dist/scripts/scriptUtils.js +34 -0
- package/dist/scripts/serve.js +64 -15
- package/dist/scripts/start.js +58 -21
- package/dist/server/expressServer.js +167 -32
- package/dist/server/renderer/handler.jsx +87 -11
- package/dist/server/utils/validator.js +14 -11
- package/dist/vite/manifest-categorization-plugin.js +7 -5
- package/dist/vite/resolveDevServerConfig.js +9 -2
- package/dist/vite/vite.config.client.js +18 -0
- package/dist/vite/vite.config.js +73 -13
- package/dist/vite/vite.config.server.js +21 -0
- package/dist/web-router/components/MetaTag.jsx +15 -2
- package/dist/web-router/components/MetaTag.test.jsx +71 -2
- package/dist/webmcp/WebMcpProvider.jsx +3 -2
- package/dist/webmcp/useTool.js +4 -1
- package/mcp_v2/conversion-tasks.json +6 -5
- package/mcp_v2/knowledge-base.json +124 -7
- package/mcp_v2/test/config.test.ts +59 -0
- package/mcp_v2/tools/build.js +6 -6
- package/mcp_v2/tools/config.js +35 -18
- package/mcp_v2/tools/conversion.js +7 -13
- package/package.json +5 -5
- package/test/cli/buildErrors.test.ts +173 -0
- package/test/cli/buildWarnings.test.cjs +62 -0
- package/test/cli/devOutput.test.ts +549 -0
- package/test/cli/diagnostic.test.ts +434 -0
- package/test/cli/output.test.cjs +90 -0
- package/test/cli/spinner.test.ts +177 -0
- package/test/cli/theme.test.ts +163 -0
- package/test/native/redactSecrets.test.ts +72 -0
- package/test/native/utils.test.ts +532 -0
- package/test/scripts/build.test.ts +275 -0
- package/test/scripts/serve.test.ts +102 -0
- package/test/server/fixtures/template/src/js/routes/index.jsx +8 -0
- package/test/server/handler.test.ts +17 -0
- package/test/server/logger.test.ts +185 -0
- package/test/server/otel.test.ts +556 -0
- package/test/server/validator.test.ts +19 -8
- package/dist/scripts/checkVersion.js +0 -3
- package/dist/scripts/cleanCache.js +0 -30
|
@@ -10,29 +10,25 @@ function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e
|
|
|
10
10
|
const catalystCorePath = _path.default.dirname(require.resolve("catalyst-core/package.json"));
|
|
11
11
|
const pwd = _path.default.join(catalystCorePath, "dist/native");
|
|
12
12
|
const configPath = `${process.env.PWD}/config/config.json`;
|
|
13
|
+
|
|
14
|
+
// Sentence case, verb first: these read as a sequence of actions, not as
|
|
15
|
+
// headings.
|
|
13
16
|
const steps = {
|
|
14
|
-
java: "Check Java
|
|
15
|
-
config: "Initialize
|
|
16
|
-
androidTools: "Validate Android
|
|
17
|
-
emulator: "Configure Android
|
|
18
|
-
properties: "Update
|
|
19
|
-
saveConfig: "Save
|
|
20
|
-
setupServer: "
|
|
21
|
-
};
|
|
22
|
-
const progressPaddingConfig = {
|
|
23
|
-
titlePaddingTop: 2,
|
|
24
|
-
titlePaddingBottom: 1,
|
|
25
|
-
stepPaddingLeft: 4,
|
|
26
|
-
stepSpacing: 1,
|
|
27
|
-
errorPaddingLeft: 6,
|
|
28
|
-
bottomMargin: 2
|
|
17
|
+
java: "Check Java environment",
|
|
18
|
+
config: "Initialize configuration",
|
|
19
|
+
androidTools: "Validate Android tools",
|
|
20
|
+
emulator: "Configure Android emulator",
|
|
21
|
+
properties: "Update local.properties",
|
|
22
|
+
saveConfig: "Save configuration",
|
|
23
|
+
setupServer: "Set up server"
|
|
29
24
|
};
|
|
30
|
-
const progress = new _terminalProgress.default(steps, "
|
|
25
|
+
const progress = new _terminalProgress.default(steps, "catalyst setupEmulator", {
|
|
26
|
+
subject: "android"
|
|
27
|
+
});
|
|
31
28
|
async function checkJavaInstallation() {
|
|
32
29
|
try {
|
|
33
30
|
(0, _utils.runCommand)("java -version");
|
|
34
31
|
(0, _utils.runCommand)("javac -version");
|
|
35
|
-
progress.log("Java is installed and configured", "success");
|
|
36
32
|
return true;
|
|
37
33
|
} catch (error) {
|
|
38
34
|
progress.log("Java installation not found or not properly configured", "error");
|
|
@@ -87,23 +83,23 @@ async function installJava() {
|
|
|
87
83
|
// Verify Java configuration
|
|
88
84
|
try {
|
|
89
85
|
const javaVersion = (0, _utils.runCommand)("java -version");
|
|
90
|
-
progress.log("Java
|
|
86
|
+
progress.log("Installed Java 17 (Zulu)", "info");
|
|
91
87
|
progress.log(`Java Version: ${javaVersion}`, "info");
|
|
92
88
|
progress.log(`IMPORTANT: Please run the following commands to complete setup:`, "warning");
|
|
93
|
-
progress.log(`1. source ${shellProfile}`, "
|
|
94
|
-
progress.log(`2. echo $JAVA_HOME`, "
|
|
89
|
+
progress.log(`1. source ${shellProfile}`, "note");
|
|
90
|
+
progress.log(`2. echo $JAVA_HOME`, "note");
|
|
95
91
|
return true;
|
|
96
92
|
} catch (error) {
|
|
97
93
|
throw new Error(`Java installation verified but environment not configured. Please run:\n1. source ${shellProfile}\n2. echo $JAVA_HOME`);
|
|
98
94
|
}
|
|
99
95
|
} catch (error) {
|
|
100
96
|
progress.log(error.message, "error");
|
|
101
|
-
progress.log("Manual Setup Instructions:", "
|
|
102
|
-
progress.log("1. Run: brew reinstall --cask zulu@17", "
|
|
103
|
-
progress.log("2. Add to ~/.zshrc:", "
|
|
104
|
-
progress.log(" export JAVA_HOME=/Library/Java/JavaVirtualMachines/zulu-17.jdk/Contents/Home", "
|
|
105
|
-
progress.log(" export PATH=$JAVA_HOME/bin:$PATH", "
|
|
106
|
-
progress.log("3. Run: source ~/.zshrc", "
|
|
97
|
+
progress.log("Manual Setup Instructions:", "note");
|
|
98
|
+
progress.log("1. Run: brew reinstall --cask zulu@17", "note");
|
|
99
|
+
progress.log("2. Add to ~/.zshrc:", "note");
|
|
100
|
+
progress.log(" export JAVA_HOME=/Library/Java/JavaVirtualMachines/zulu-17.jdk/Contents/Home", "note");
|
|
101
|
+
progress.log(" export PATH=$JAVA_HOME/bin:$PATH", "note");
|
|
102
|
+
progress.log("3. Run: source ~/.zshrc", "note");
|
|
107
103
|
throw error;
|
|
108
104
|
}
|
|
109
105
|
}
|
|
@@ -149,18 +145,19 @@ async function saveConfig(newConfig) {
|
|
|
149
145
|
}
|
|
150
146
|
};
|
|
151
147
|
_fs.default.writeFileSync(configPath, JSON.stringify(updatedConfig, null, 2));
|
|
152
|
-
progress.log("Configuration saved successfully", "success");
|
|
153
148
|
} catch (error) {
|
|
154
149
|
progress.log("Failed to save configuration: " + error.message, "error");
|
|
155
150
|
process.exit(1);
|
|
156
151
|
}
|
|
157
152
|
}
|
|
158
153
|
function validateAndroidTools(androidConfig) {
|
|
159
|
-
const ANDROID_SDK = androidConfig.sdkPath;
|
|
154
|
+
const ANDROID_SDK = (0, _utils.expandHome)(androidConfig.sdkPath);
|
|
160
155
|
const EMULATOR_PATH = `${ANDROID_SDK}/emulator/emulator`;
|
|
161
156
|
const ADB_PATH = `${ANDROID_SDK}/platform-tools/adb`;
|
|
162
157
|
if (!ANDROID_SDK) {
|
|
163
|
-
throw new Error("Android SDK
|
|
158
|
+
throw Object.assign(new Error("Android SDK not found"), {
|
|
159
|
+
hint: _utils.ANDROID_SDK_HINT
|
|
160
|
+
});
|
|
164
161
|
}
|
|
165
162
|
if (!_fs.default.existsSync(ANDROID_SDK)) {
|
|
166
163
|
throw new Error(`Android SDK path does not exist: ${ANDROID_SDK}`);
|
|
@@ -170,7 +167,6 @@ function validateAndroidTools(androidConfig) {
|
|
|
170
167
|
}
|
|
171
168
|
try {
|
|
172
169
|
(0, _utils.runCommand)(`${ADB_PATH} version`);
|
|
173
|
-
progress.log("ADB is valid", "success");
|
|
174
170
|
} catch (error) {
|
|
175
171
|
throw new Error(`ADB is not working properly: ${error.message}`);
|
|
176
172
|
}
|
|
@@ -179,7 +175,6 @@ function validateAndroidTools(androidConfig) {
|
|
|
179
175
|
}
|
|
180
176
|
try {
|
|
181
177
|
(0, _utils.runCommand)(`${EMULATOR_PATH} -version`);
|
|
182
|
-
progress.log("Emulator is valid", "success");
|
|
183
178
|
} catch (error) {
|
|
184
179
|
throw new Error(`Emulator is not working properly: ${error.message}`);
|
|
185
180
|
}
|
|
@@ -188,11 +183,10 @@ function validateAndroidTools(androidConfig) {
|
|
|
188
183
|
if (!avdOutput.includes(androidConfig.emulatorName)) {
|
|
189
184
|
throw new Error(`Specified emulator "${androidConfig.emulatorName}" not found in available AVDs`);
|
|
190
185
|
}
|
|
191
|
-
progress.log(`
|
|
186
|
+
progress.log(`Using emulator "${androidConfig.emulatorName}"`, "info");
|
|
192
187
|
} catch (error) {
|
|
193
188
|
throw new Error(`Error checking emulator AVD: ${error.message}`);
|
|
194
189
|
}
|
|
195
|
-
progress.log("Android tools validation completed successfully!", "success");
|
|
196
190
|
return {
|
|
197
191
|
EMULATOR_PATH,
|
|
198
192
|
ADB_PATH
|
|
@@ -200,7 +194,9 @@ function validateAndroidTools(androidConfig) {
|
|
|
200
194
|
}
|
|
201
195
|
async function checkEmulator(ADB_PATH) {
|
|
202
196
|
try {
|
|
203
|
-
const devices = (0, _utils.runCommand)(`${ADB_PATH} devices
|
|
197
|
+
const devices = (0, _utils.runCommand)(`${ADB_PATH} devices`, {
|
|
198
|
+
timeout: 15000
|
|
199
|
+
});
|
|
204
200
|
return devices.includes("emulator");
|
|
205
201
|
} catch (error) {
|
|
206
202
|
progress.log(`Error checking emulator: ${error}`, "error");
|
|
@@ -223,7 +219,6 @@ async function updateLocalProperties(sdkPath) {
|
|
|
223
219
|
progress.start("properties");
|
|
224
220
|
try {
|
|
225
221
|
(0, _utils.runCommand)(`cd ${pwd}/androidProject && ./gradlew updateSdkPath -PsdkPath="${sdkPath}"`);
|
|
226
|
-
progress.log("Updated local.properties successfully", "success");
|
|
227
222
|
progress.complete("properties");
|
|
228
223
|
} catch (error) {
|
|
229
224
|
progress.fail("properties", `Failed to update local.properties: ${error.message}`);
|
|
@@ -249,11 +244,11 @@ async function setupAndroidEnvironment() {
|
|
|
249
244
|
progress.log("No emulator running, attempting to start one...");
|
|
250
245
|
await startEmulator(EMULATOR_PATH, config.android);
|
|
251
246
|
} else {
|
|
252
|
-
progress.log("Emulator already running
|
|
247
|
+
progress.log("Emulator already running", "info");
|
|
253
248
|
}
|
|
254
249
|
progress.complete("emulator");
|
|
255
250
|
if (config.android?.sdkPath) {
|
|
256
|
-
await updateLocalProperties(config.android.sdkPath);
|
|
251
|
+
await updateLocalProperties((0, _utils.expandHome)(config.android.sdkPath));
|
|
257
252
|
}
|
|
258
253
|
progress.start("saveConfig");
|
|
259
254
|
await saveConfig({
|
|
@@ -261,40 +256,34 @@ async function setupAndroidEnvironment() {
|
|
|
261
256
|
});
|
|
262
257
|
progress.complete("saveConfig");
|
|
263
258
|
progress.start("setupServer");
|
|
264
|
-
await (0, _setupServer.setupServer)(configPath);
|
|
259
|
+
await (0, _setupServer.setupServer)(configPath, message => progress.log(message, "info"));
|
|
265
260
|
progress.complete("setupServer");
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
prefix: "└─ ",
|
|
261
|
+
|
|
262
|
+
// What was configured, not what the keys mean -- the explanation
|
|
263
|
+
// belongs in the docs, not in every successful run's transcript.
|
|
264
|
+
// validateAndCompleteConfig returns { port, android: {...} } -- flat,
|
|
265
|
+
// not nested under WEBVIEW_CONFIG.
|
|
266
|
+
const androidCfg = config.android || {};
|
|
267
|
+
progress.printTreeContent("Config config/config.json", [{
|
|
268
|
+
text: `port ${config.port ?? ""}`,
|
|
275
269
|
color: "gray"
|
|
276
270
|
}, {
|
|
277
|
-
text:
|
|
278
|
-
indent: 2,
|
|
279
|
-
prefix: "├─ ",
|
|
271
|
+
text: `buildType ${androidCfg.buildType ?? ""}`,
|
|
280
272
|
color: "gray"
|
|
281
273
|
}, {
|
|
282
|
-
text:
|
|
283
|
-
indent: 2,
|
|
284
|
-
prefix: "├─ ",
|
|
274
|
+
text: `sdkPath ${androidCfg.sdkPath ?? ""}`,
|
|
285
275
|
color: "gray"
|
|
286
276
|
}, {
|
|
287
|
-
text:
|
|
288
|
-
indent: 2,
|
|
289
|
-
prefix: "└─ ",
|
|
277
|
+
text: `emulatorName ${androidCfg.emulatorName ?? ""}`,
|
|
290
278
|
color: "gray"
|
|
291
279
|
}]);
|
|
292
|
-
progress.
|
|
280
|
+
progress.summary("Ready", "Run catalyst buildApp:android to install the app");
|
|
293
281
|
process.exit(0);
|
|
294
282
|
} catch (error) {
|
|
295
283
|
if (progress.currentStep) {
|
|
296
284
|
progress.fail(progress.currentStep.id, error.message);
|
|
297
285
|
}
|
|
286
|
+
progress.hint(error.hint);
|
|
298
287
|
process.exit(1);
|
|
299
288
|
}
|
|
300
289
|
}
|
|
@@ -93,22 +93,36 @@ function createAssetsPhase(ctx) {
|
|
|
93
93
|
progress.log(`Error processing splash screen: ${error.message}`, "warning");
|
|
94
94
|
}
|
|
95
95
|
}
|
|
96
|
-
|
|
96
|
+
|
|
97
|
+
// Copies public/<fileName> to assets/<destSubdir>/<fileName>. Optional files are
|
|
98
|
+
// removed from assets when absent so deleting the source turns the feature off.
|
|
99
|
+
async function copyPublicHtml(fileName, destSubdir, optional = false) {
|
|
97
100
|
try {
|
|
98
|
-
const sourcePath = `${process.cwd()}/public
|
|
99
|
-
const destDir = `${pwd}/androidProject/app/src/main/assets
|
|
100
|
-
const destPath = `${destDir}
|
|
101
|
+
const sourcePath = `${process.cwd()}/public/${fileName}`;
|
|
102
|
+
const destDir = `${pwd}/androidProject/app/src/main/assets/${destSubdir}`;
|
|
103
|
+
const destPath = `${destDir}/${fileName}`;
|
|
101
104
|
if (!fs.existsSync(sourcePath)) {
|
|
102
|
-
|
|
105
|
+
if (optional) {
|
|
106
|
+
fs.rmSync(destPath, {
|
|
107
|
+
force: true
|
|
108
|
+
});
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
progress.log(`${fileName} not found in public/; skipping ${destSubdir} asset copy`, "warning");
|
|
103
112
|
return;
|
|
104
113
|
}
|
|
105
114
|
ctx.runCommand(`mkdir -p ${destDir}`);
|
|
106
115
|
fs.copyFileSync(sourcePath, destPath);
|
|
107
|
-
progress.log("offline.html copied to Android assets", "success");
|
|
108
116
|
} catch (error) {
|
|
109
|
-
progress.log(`Warning: Error copying
|
|
117
|
+
progress.log(`Warning: Error copying ${fileName}: ${error.message}`, "warning");
|
|
110
118
|
}
|
|
111
119
|
}
|
|
120
|
+
async function copyOfflinePage() {
|
|
121
|
+
await copyPublicHtml("offline.html", "offline");
|
|
122
|
+
}
|
|
123
|
+
async function copySplashHtml() {
|
|
124
|
+
await copyPublicHtml("splashscreen.html", "splash", true);
|
|
125
|
+
}
|
|
112
126
|
async function copyIconAssets() {
|
|
113
127
|
try {
|
|
114
128
|
const destPath = `${pwd}/androidProject/app/src/main/res`;
|
|
@@ -202,9 +216,6 @@ function createAssetsPhase(ctx) {
|
|
|
202
216
|
if (hasCustomIcons) {
|
|
203
217
|
progress.log("Applied Android launcher icons from public/android/appIcons.", "success");
|
|
204
218
|
}
|
|
205
|
-
if (usedFallback) {
|
|
206
|
-
progress.log("Used bundled Catalyst fallback icon for launcher.", "info");
|
|
207
|
-
}
|
|
208
219
|
} catch (error) {
|
|
209
220
|
progress.log(`Warning: Error copying app icon assets: ${error.message}`, "warning");
|
|
210
221
|
}
|
|
@@ -243,7 +254,6 @@ function createAssetsPhase(ctx) {
|
|
|
243
254
|
});
|
|
244
255
|
}
|
|
245
256
|
fs.copyFileSync(rootGoogleServicesPath, androidGoogleServicesPath);
|
|
246
|
-
progress.log("Copied google-services.json to androidProject/app/", "success");
|
|
247
257
|
return true;
|
|
248
258
|
} else if (fs.existsSync(androidGoogleServicesPath)) {
|
|
249
259
|
progress.log("google-services.json already exists in androidProject/app/", "info");
|
|
@@ -267,7 +277,6 @@ function createAssetsPhase(ctx) {
|
|
|
267
277
|
await cleanupNotificationMetadata();
|
|
268
278
|
await cleanupNotificationAssets();
|
|
269
279
|
if (!hasNotificationConfig) {
|
|
270
|
-
progress.log("Notifications disabled - cleaned up notification configurations", "info");
|
|
271
280
|
return;
|
|
272
281
|
}
|
|
273
282
|
const hasGoogleServices = await handleGoogleServicesJson();
|
|
@@ -288,7 +297,6 @@ function createAssetsPhase(ctx) {
|
|
|
288
297
|
const propertiesPath = `${pwd}/androidProject/app/catalyst-build.properties`;
|
|
289
298
|
const propertiesContent = ["# Generated by Catalyst. Used by Android Studio builds when -PconfigPath is not provided.", `notifications.enabled=${notificationsEnabled ? "true" : "false"}`, ""].join("\n");
|
|
290
299
|
fs.writeFileSync(propertiesPath, propertiesContent);
|
|
291
|
-
progress.log("Updated generated Android build properties", "success");
|
|
292
300
|
} catch (error) {
|
|
293
301
|
throw new Error(`Failed to write generated Android build properties: ${error.message}`);
|
|
294
302
|
}
|
|
@@ -541,6 +549,7 @@ function createAssetsPhase(ctx) {
|
|
|
541
549
|
copyBuildAssets,
|
|
542
550
|
copySplashscreenAssets,
|
|
543
551
|
copyOfflinePage,
|
|
552
|
+
copySplashHtml,
|
|
544
553
|
copyIconAssets,
|
|
545
554
|
configureAppName,
|
|
546
555
|
processNotifications
|
|
@@ -1,14 +1,25 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
|
-
const fs = require("fs");
|
|
4
|
-
const path = require("path");
|
|
5
3
|
const {
|
|
6
4
|
spawn,
|
|
7
5
|
execFileSync
|
|
8
6
|
} = require("child_process");
|
|
7
|
+
const fs = require("fs");
|
|
8
|
+
const path = require("path");
|
|
9
9
|
const {
|
|
10
10
|
buildAndroidAAB
|
|
11
11
|
} = require("../renameAndroidProject.js");
|
|
12
|
+
const {
|
|
13
|
+
ensureLocalProperties
|
|
14
|
+
} = require("../utils.js");
|
|
15
|
+
|
|
16
|
+
// adb answers in milliseconds when healthy. A wedged adb server accepts the
|
|
17
|
+
// connection and never replies, so without a bound the build sat on
|
|
18
|
+
// "detecting physical devices" forever.
|
|
19
|
+
const ADB_TIMEOUT_MS = 15000;
|
|
20
|
+
|
|
21
|
+
/** A timed-out device tool is fatal: every later adb call would hang the same way. */
|
|
22
|
+
const isToolTimeout = error => error?.code === "ETOOLTIMEOUT";
|
|
12
23
|
const DEFAULT_DEPLOYMENT_PATH = "./deployment";
|
|
13
24
|
const DEFAULT_OLD_PROJECT_NAME = "androidProject";
|
|
14
25
|
const DEFAULT_OVERWRITE_EXISTING = true;
|
|
@@ -21,9 +32,11 @@ function createBuildPhase(ctx) {
|
|
|
21
32
|
} = ctx;
|
|
22
33
|
async function detectPhysicalDevice(ADB_PATH) {
|
|
23
34
|
try {
|
|
24
|
-
progress.
|
|
35
|
+
progress.status("detecting physical devices");
|
|
25
36
|
// nosemgrep: javascript.lang.security.audit.dangerous-spawn-shell-command.dangerous-spawn-shell-command - ADB_PATH is derived from androidConfig.sdkPath, a trusted internal config value.
|
|
26
|
-
const devices = ctx.runCommand(`${ADB_PATH} devices -l
|
|
37
|
+
const devices = ctx.runCommand(`${ADB_PATH} devices -l`, {
|
|
38
|
+
timeout: ADB_TIMEOUT_MS
|
|
39
|
+
});
|
|
27
40
|
const lines = devices.split("\n").filter(line => line.trim() && !line.includes("List of devices"));
|
|
28
41
|
for (const line of lines) {
|
|
29
42
|
const parts = line.trim().split(/\s+/);
|
|
@@ -32,13 +45,17 @@ function createBuildPhase(ctx) {
|
|
|
32
45
|
if (!deviceId.startsWith("emulator-")) {
|
|
33
46
|
try {
|
|
34
47
|
// nosemgrep: javascript.lang.security.audit.dangerous-spawn-shell-command.dangerous-spawn-shell-command - deviceId comes from adb devices output, not user input.
|
|
35
|
-
const qemuCheck = ctx.runCommand(`${ADB_PATH} -s ${deviceId} shell getprop ro.kernel.qemu
|
|
48
|
+
const qemuCheck = ctx.runCommand(`${ADB_PATH} -s ${deviceId} shell getprop ro.kernel.qemu`, {
|
|
49
|
+
timeout: ADB_TIMEOUT_MS
|
|
50
|
+
});
|
|
36
51
|
if (!qemuCheck.trim()) {
|
|
37
52
|
progress.log(`Physical device detected: ${deviceId}`, "success");
|
|
38
53
|
let deviceModel = "Unknown Device";
|
|
39
54
|
try {
|
|
40
55
|
// nosemgrep: javascript.lang.security.audit.dangerous-spawn-shell-command.dangerous-spawn-shell-command - deviceId comes from adb devices output, not user input.
|
|
41
|
-
const model = ctx.runCommand(`${ADB_PATH} -s ${deviceId} shell getprop ro.product.model
|
|
56
|
+
const model = ctx.runCommand(`${ADB_PATH} -s ${deviceId} shell getprop ro.product.model`, {
|
|
57
|
+
timeout: ADB_TIMEOUT_MS
|
|
58
|
+
});
|
|
42
59
|
deviceModel = model.trim() || deviceModel;
|
|
43
60
|
} catch (e) {
|
|
44
61
|
// Ignore model detection errors
|
|
@@ -57,6 +74,7 @@ function createBuildPhase(ctx) {
|
|
|
57
74
|
progress.log("No physical devices detected", "info");
|
|
58
75
|
return null;
|
|
59
76
|
} catch (error) {
|
|
77
|
+
if (isToolTimeout(error)) throw error;
|
|
60
78
|
progress.log("Error detecting physical devices: " + error.message, "error");
|
|
61
79
|
return null;
|
|
62
80
|
}
|
|
@@ -114,15 +132,44 @@ function createBuildPhase(ctx) {
|
|
|
114
132
|
async function checkEmulator(ADB_PATH) {
|
|
115
133
|
try {
|
|
116
134
|
// nosemgrep: javascript.lang.security.audit.dangerous-spawn-shell-command.dangerous-spawn-shell-command - ADB_PATH is derived from androidConfig.sdkPath, a trusted internal config value.
|
|
117
|
-
const devices = ctx.runCommand(`${ADB_PATH} devices
|
|
135
|
+
const devices = ctx.runCommand(`${ADB_PATH} devices`, {
|
|
136
|
+
timeout: ADB_TIMEOUT_MS
|
|
137
|
+
});
|
|
118
138
|
const onlineEmulator = devices.split("\n").map(line => line.trim().split(/\s+/)).find(([id, state]) => id?.startsWith("emulator-") && state === "device");
|
|
119
139
|
return onlineEmulator ? onlineEmulator[0] : null;
|
|
120
140
|
} catch (error) {
|
|
141
|
+
if (isToolTimeout(error)) throw error;
|
|
121
142
|
progress.log("Error checking emulator status: " + error.message, "error");
|
|
122
143
|
return null;
|
|
123
144
|
}
|
|
124
145
|
}
|
|
125
146
|
|
|
147
|
+
/**
|
|
148
|
+
* The task name from a gradle line, or null for lines worth ignoring.
|
|
149
|
+
* "> Task :app:compileDebugKotlin" becomes "app:compileDebugKotlin".
|
|
150
|
+
*/
|
|
151
|
+
function gradleTask(line) {
|
|
152
|
+
const match = /^>\s*Task\s+:(\S+)/.exec(line.trim());
|
|
153
|
+
return match ? match[1] : null;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Run something that writes directly to the terminal, with the progress
|
|
158
|
+
* tree stood down for the duration.
|
|
159
|
+
*
|
|
160
|
+
* The resume must happen on the failure path too -- a build that throws
|
|
161
|
+
* while paused would otherwise leave the tree silent for the rest of the
|
|
162
|
+
* run, hiding exactly the output someone needs when a build fails.
|
|
163
|
+
*/
|
|
164
|
+
async function withStreamedOutput(fn) {
|
|
165
|
+
progress.pause();
|
|
166
|
+
try {
|
|
167
|
+
return await fn();
|
|
168
|
+
} finally {
|
|
169
|
+
progress.resume();
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
126
173
|
// The Android emulator binary is a long-running server process — it does not
|
|
127
174
|
// exit after boot, it keeps running until the emulator window is closed. Using
|
|
128
175
|
// runInteractiveCommand (which only resolves on the child's `close` event, see
|
|
@@ -179,9 +226,12 @@ function createBuildPhase(ctx) {
|
|
|
179
226
|
function listEmulatorSerials(ADB_PATH) {
|
|
180
227
|
try {
|
|
181
228
|
// nosemgrep: javascript.lang.security.audit.dangerous-spawn-shell-command.dangerous-spawn-shell-command - ADB_PATH is derived from androidConfig.sdkPath, a trusted internal config value.
|
|
182
|
-
const devices = ctx.runCommand(`${ADB_PATH} devices
|
|
229
|
+
const devices = ctx.runCommand(`${ADB_PATH} devices`, {
|
|
230
|
+
timeout: ADB_TIMEOUT_MS
|
|
231
|
+
});
|
|
183
232
|
return devices.split("\n").map(line => line.trim().split(/\s+/)[0]).filter(id => id && id.startsWith("emulator-"));
|
|
184
|
-
} catch {
|
|
233
|
+
} catch (error) {
|
|
234
|
+
if (isToolTimeout(error)) throw error;
|
|
185
235
|
return [];
|
|
186
236
|
}
|
|
187
237
|
}
|
|
@@ -226,7 +276,10 @@ function createBuildPhase(ctx) {
|
|
|
226
276
|
const remaining = Math.max(deadline - Date.now(), 1000);
|
|
227
277
|
return execFileSync(ADB_PATH, ["-s", serial, "shell", "getprop", prop], {
|
|
228
278
|
encoding: "utf8",
|
|
229
|
-
timeout: remaining
|
|
279
|
+
timeout: remaining,
|
|
280
|
+
// "adb: device offline" is the expected answer while the
|
|
281
|
+
// emulator boots; inherited, it landed on the spinner's row.
|
|
282
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
230
283
|
}).trim();
|
|
231
284
|
};
|
|
232
285
|
while (Date.now() < deadline) {
|
|
@@ -252,7 +305,7 @@ function createBuildPhase(ctx) {
|
|
|
252
305
|
return new Error(`[ANDROID-001] Timed out waiting for the Android emulator to boot: ${detail}`);
|
|
253
306
|
}
|
|
254
307
|
async function handleEmulatorSetup(ADB_PATH, EMULATOR_PATH, androidConfig) {
|
|
255
|
-
progress.
|
|
308
|
+
progress.status("setting up emulator");
|
|
256
309
|
let serial = await checkEmulator(ADB_PATH);
|
|
257
310
|
let pid = null;
|
|
258
311
|
if (!serial) {
|
|
@@ -281,19 +334,35 @@ function createBuildPhase(ctx) {
|
|
|
281
334
|
};
|
|
282
335
|
}
|
|
283
336
|
async function buildApp(ADB_PATH, androidConfig, buildOptimisation, targetDevice = null) {
|
|
284
|
-
progress.
|
|
337
|
+
progress.status("building and installing app");
|
|
285
338
|
try {
|
|
339
|
+
// Gradle needs the SDK location in local.properties. Only
|
|
340
|
+
// setupEmulator:android wrote it, so building without a prior
|
|
341
|
+
// setup died in gradle despite a configured sdkPath.
|
|
342
|
+
if (androidConfig.sdkPath) ensureLocalProperties(`${pwd}/androidProject`, androidConfig.sdkPath);
|
|
343
|
+
|
|
344
|
+
// --quiet was the Android silence: it suppresses gradle's task and
|
|
345
|
+
// error output entirely. --console=rich emits ANSI redraws, which
|
|
346
|
+
// are meaningless on a pipe; plain gives one line per task.
|
|
286
347
|
// nosemgrep: javascript.lang.security.audit.dangerous-spawn-shell-command.dangerous-spawn-shell-command - pwd and configPath are internal resolved paths, not user input.
|
|
287
|
-
let buildCommand = `cd ${pwd}/androidProject && ./gradlew generateWebViewConfig -PconfigPath=${configPath} -PbuildOptimisation=${buildOptimisation} && ./gradlew clean installDebug -PconfigPath=${configPath} --
|
|
348
|
+
let buildCommand = `cd ${pwd}/androidProject && ./gradlew generateWebViewConfig -PconfigPath=${configPath} -PbuildOptimisation=${buildOptimisation} && ./gradlew clean installDebug -PconfigPath=${configPath} --console=plain`;
|
|
288
349
|
if (targetDevice && targetDevice.type === "physical") {
|
|
289
350
|
buildCommand = buildCommand.replace("installDebug", `installDebug -Pandroid.injected.target.device=${targetDevice.id}`);
|
|
290
351
|
}
|
|
352
|
+
|
|
353
|
+
// Gradle emits hundreds of lines that nobody reads on success and
|
|
354
|
+
// that the error path re-derives from error.output anyway. Show the
|
|
355
|
+
// current task on the spinner's own row instead of scrolling them.
|
|
291
356
|
await ctx.runInteractiveCommand("sh", ["-c", buildCommand], {
|
|
292
357
|
"BUILD SUCCESSFUL": ""
|
|
358
|
+
}, {
|
|
359
|
+
onLine: line => progress.status(gradleTask(line))
|
|
293
360
|
});
|
|
294
|
-
progress.log("App build and installation completed successfully!", "success");
|
|
295
361
|
} catch (error) {
|
|
296
|
-
|
|
362
|
+
// Re-wrapping with a new Error dropped `output`, and with it the
|
|
363
|
+
// compiler errors. Keep the original and let the reporter mine it.
|
|
364
|
+
error.stage = "Building and installing the app";
|
|
365
|
+
throw error;
|
|
297
366
|
}
|
|
298
367
|
}
|
|
299
368
|
async function launchApp(ADB_PATH, buildType = "debug", targetDevice = null) {
|
|
@@ -303,16 +372,15 @@ function createBuildPhase(ctx) {
|
|
|
303
372
|
}
|
|
304
373
|
if (targetDevice.type === "physical") {
|
|
305
374
|
progress.log(`App installed on physical device: ${targetDevice.model}`, "success");
|
|
306
|
-
progress.log("
|
|
375
|
+
progress.log("Open the app on your device to launch it", "note");
|
|
307
376
|
return;
|
|
308
377
|
}
|
|
309
378
|
try {
|
|
310
|
-
progress.
|
|
379
|
+
progress.status("launching app on emulator");
|
|
311
380
|
const packageName = `${ANDROID_PACKAGE}${buildType === "debug" ? ".debug" : ""}`;
|
|
312
381
|
// nosemgrep: javascript.lang.security.audit.dangerous-spawn-shell-command.dangerous-spawn-shell-command - ADB_PATH is derived from androidConfig.sdkPath, a trusted internal config value.
|
|
313
382
|
const launchCommand = `${ADB_PATH} shell monkey -p ${packageName} 1`;
|
|
314
383
|
await ctx.runInteractiveCommand("sh", ["-c", launchCommand], {});
|
|
315
|
-
progress.log("App launched successfully on emulator!", "success");
|
|
316
384
|
} catch (error) {
|
|
317
385
|
progress.log(`Warning: Could not auto-launch app: ${error.message}`, "warning");
|
|
318
386
|
progress.log("App was installed successfully, but auto-launch failed", "info");
|
|
@@ -362,12 +430,9 @@ function createBuildPhase(ctx) {
|
|
|
362
430
|
return null;
|
|
363
431
|
}
|
|
364
432
|
const currentDate = new Date().toLocaleDateString("en-GB").replace(/\//g, "-");
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
minute: "2-digit",
|
|
369
|
-
second: "2-digit"
|
|
370
|
-
}).replace(/:/g, "-");
|
|
433
|
+
// 24-hour and locale-free: "02-05-09 PM" put a space in every
|
|
434
|
+
// artifact name, which breaks unquoted shell use of the path.
|
|
435
|
+
const currentTime = new Date().toTimeString().slice(0, 8).replace(/:/g, "-");
|
|
371
436
|
let destinationApkFileName = "";
|
|
372
437
|
if (appName) {
|
|
373
438
|
destinationApkFileName = buildType === "release" ? `${appName}-${currentTime}.release.apk` : `${appName}-${currentTime}.debug.apk`;
|
|
@@ -405,12 +470,16 @@ function createBuildPhase(ctx) {
|
|
|
405
470
|
async function buildSignedAAB(androidConfig) {
|
|
406
471
|
progress.log("Building signed AAB for release...", "info");
|
|
407
472
|
try {
|
|
473
|
+
// Same as buildApp: gradle needs local.properties on a fresh machine.
|
|
474
|
+
if (androidConfig.sdkPath) ensureLocalProperties(`${pwd}/androidProject`, androidConfig.sdkPath);
|
|
408
475
|
progress.log("Generating webview configuration for release...", "info");
|
|
409
476
|
try {
|
|
410
477
|
// nosemgrep: javascript.lang.security.audit.dangerous-spawn-shell-command.dangerous-spawn-shell-command - pwd and configPath are internal resolved paths, not user input.
|
|
411
478
|
const generateConfigCommand = `cd ${pwd}/androidProject && ./gradlew generateWebViewConfig -PconfigPath=${configPath}`;
|
|
412
479
|
await ctx.runInteractiveCommand("sh", ["-c", generateConfigCommand], {
|
|
413
480
|
"BUILD SUCCESSFUL": ""
|
|
481
|
+
}, {
|
|
482
|
+
onLine: line => progress.status(gradleTask(line))
|
|
414
483
|
});
|
|
415
484
|
progress.log("Webview config generated successfully", "success");
|
|
416
485
|
} catch (configError) {
|
|
@@ -418,10 +487,13 @@ function createBuildPhase(ctx) {
|
|
|
418
487
|
throw new Error("Webview config generation is required for release builds");
|
|
419
488
|
}
|
|
420
489
|
const aabConfig = await createAABConfig(androidConfig);
|
|
421
|
-
|
|
490
|
+
// buildAndroidAAB shells out to `./gradlew bundleRelease` and prints
|
|
491
|
+
// its own progress lines, so it owns the screen for its duration.
|
|
492
|
+
await withStreamedOutput(() => buildAndroidAAB(aabConfig));
|
|
422
493
|
progress.log("Signed AAB build completed successfully!", "success");
|
|
423
494
|
} catch (error) {
|
|
424
|
-
|
|
495
|
+
error.stage = "Building the signed AAB";
|
|
496
|
+
throw error;
|
|
425
497
|
}
|
|
426
498
|
}
|
|
427
499
|
return {
|
|
@@ -1,6 +1,21 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
3
|
const fs = require("fs");
|
|
4
|
+
const os = require("os");
|
|
5
|
+
const {
|
|
6
|
+
detectAndroidSdk,
|
|
7
|
+
listAvds,
|
|
8
|
+
expandHome,
|
|
9
|
+
ANDROID_SDK_HINT
|
|
10
|
+
} = require("../utils.js");
|
|
11
|
+
|
|
12
|
+
/** An error carrying the one action that fixes it, shown as the diagnostic's hint. */
|
|
13
|
+
function withHint(message, hint) {
|
|
14
|
+
return Object.assign(new Error(message), {
|
|
15
|
+
hint
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
const tilde = dir => dir.startsWith(os.homedir()) ? `~${dir.slice(os.homedir().length)}` : dir;
|
|
4
19
|
function createConfigPhase(ctx) {
|
|
5
20
|
const {
|
|
6
21
|
configPath,
|
|
@@ -17,10 +32,9 @@ function createConfigPhase(ctx) {
|
|
|
17
32
|
throw new Error("WebView Config missing in " + configPath);
|
|
18
33
|
}
|
|
19
34
|
if (!WEBVIEW_CONFIG.android) {
|
|
20
|
-
throw
|
|
35
|
+
throw withHint("Android config missing in WebView Config", "Run catalyst setupEmulator:android to create it, or add WEBVIEW_CONFIG.android to config/config.json.");
|
|
21
36
|
}
|
|
22
37
|
const buildType = WEBVIEW_CONFIG.android.buildType || "debug";
|
|
23
|
-
progress.log(`Build Type: ${buildType}`, "info");
|
|
24
38
|
if (buildType === "release") {
|
|
25
39
|
progress.log("Release build detected - AAB will be generated", "info");
|
|
26
40
|
}
|
|
@@ -30,23 +44,34 @@ function createConfigPhase(ctx) {
|
|
|
30
44
|
};
|
|
31
45
|
}
|
|
32
46
|
function validateAndroidTools(androidConfig) {
|
|
47
|
+
progress.status("validating tools");
|
|
48
|
+
|
|
49
|
+
// An unset sdkPath is usually just an SDK in the default place. Use it,
|
|
50
|
+
// and say so, rather than failing on a field most people never set.
|
|
51
|
+
if (!androidConfig.sdkPath) {
|
|
52
|
+
const detected = detectAndroidSdk();
|
|
53
|
+
if (!detected) throw withHint("Android SDK not found", ANDROID_SDK_HINT);
|
|
54
|
+
androidConfig.sdkPath = detected;
|
|
55
|
+
progress.log(`Using the Android SDK at ${tilde(detected)}`, "note");
|
|
56
|
+
}
|
|
57
|
+
// "~/Library/Android/sdk", as the hint suggests. Expanded in memory
|
|
58
|
+
// only; the config file keeps what the user wrote.
|
|
59
|
+
androidConfig.sdkPath = expandHome(androidConfig.sdkPath);
|
|
33
60
|
const ANDROID_SDK = androidConfig.sdkPath;
|
|
34
61
|
const ADB_PATH = `${ANDROID_SDK}/platform-tools/adb`;
|
|
35
62
|
const EMULATOR_PATH = `${ANDROID_SDK}/emulator/emulator`;
|
|
36
|
-
progress.log("Validating Android tools...", "info");
|
|
37
|
-
if (!ANDROID_SDK) {
|
|
38
|
-
throw new Error("Android SDK path is not configured");
|
|
39
|
-
}
|
|
40
63
|
if (!fs.existsSync(ANDROID_SDK)) {
|
|
41
|
-
throw
|
|
64
|
+
throw withHint(`Android SDK path does not exist: ${ANDROID_SDK}`, ANDROID_SDK_HINT);
|
|
42
65
|
}
|
|
43
66
|
if (!fs.existsSync(ADB_PATH)) {
|
|
44
|
-
throw
|
|
67
|
+
throw withHint(`adb not found at ${ADB_PATH}`, "Install Android SDK Platform-Tools from Android Studio's SDK Manager.");
|
|
45
68
|
}
|
|
46
69
|
try {
|
|
47
|
-
ctx.runCommand(`${ADB_PATH} version
|
|
48
|
-
|
|
70
|
+
ctx.runCommand(`${ADB_PATH} version`, {
|
|
71
|
+
timeout: 15000
|
|
72
|
+
});
|
|
49
73
|
} catch (error) {
|
|
74
|
+
if (error.hint) throw error;
|
|
50
75
|
throw new Error(`ADB is not working properly: ${error.message}`);
|
|
51
76
|
}
|
|
52
77
|
const buildType = androidConfig.buildType || "debug";
|
|
@@ -56,10 +81,17 @@ function createConfigPhase(ctx) {
|
|
|
56
81
|
}
|
|
57
82
|
try {
|
|
58
83
|
ctx.runCommand(`${EMULATOR_PATH} -version`);
|
|
59
|
-
progress.log("Emulator validation successful", "success");
|
|
60
84
|
} catch (error) {
|
|
61
85
|
throw new Error(`Emulator is not working properly: ${error.message}`);
|
|
62
86
|
}
|
|
87
|
+
if (!androidConfig.emulatorName) {
|
|
88
|
+
const [first] = listAvds(ANDROID_SDK);
|
|
89
|
+
if (!first) {
|
|
90
|
+
throw withHint("No Android emulator found", "Create one in Android Studio's Device Manager, or set WEBVIEW_CONFIG.android.emulatorName.");
|
|
91
|
+
}
|
|
92
|
+
androidConfig.emulatorName = first;
|
|
93
|
+
progress.log(`Using the "${first}" emulator`, "note");
|
|
94
|
+
}
|
|
63
95
|
try {
|
|
64
96
|
const avdOutput = ctx.runCommand(`${EMULATOR_PATH} -list-avds`);
|
|
65
97
|
if (!avdOutput.includes(androidConfig.emulatorName)) {
|
|
@@ -72,7 +104,6 @@ function createConfigPhase(ctx) {
|
|
|
72
104
|
} else {
|
|
73
105
|
progress.log("Skipping emulator validation for release build", "info");
|
|
74
106
|
}
|
|
75
|
-
progress.log("Android tools validation completed successfully!", "success");
|
|
76
107
|
return {
|
|
77
108
|
ANDROID_SDK,
|
|
78
109
|
ADB_PATH,
|