catalyst-core 1.1.0 → 1.2.0-canary.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.
Files changed (69) hide show
  1. package/bin/catalyst.js +54 -6
  2. package/changelog.md +27 -1
  3. package/dist/errors/registry.js +306 -18
  4. package/dist/errors-index.json +273 -70
  5. package/dist/native/androidProject/app/build.gradle.kts +53 -79
  6. package/dist/native/androidProject/app/src/main/AndroidManifest.xml +1 -1
  7. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/CatalystConstants.kt +3 -1
  8. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/MainActivity.kt +32 -4
  9. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/utils/KeyboardUtil.kt +5 -1
  10. package/dist/native/androidProject/app/src/test/java/io/yourname/androidproject/ConfigMappingTest.kt +25 -0
  11. package/dist/native/androidSetup.js +76 -170
  12. package/dist/native/androidToolchain.js +642 -0
  13. package/dist/native/appIcon.js +100 -0
  14. package/dist/native/buildAndroid/assets.js +29 -5
  15. package/dist/native/buildAndroid/build.js +110 -223
  16. package/dist/native/buildAndroid/config.js +57 -65
  17. package/dist/native/buildAndroid/devices.js +176 -0
  18. package/dist/native/buildAndroid/index.js +271 -46
  19. package/dist/native/buildAndroid/output.js +43 -0
  20. package/dist/native/buildAndroid/signing.js +456 -0
  21. package/dist/native/buildAppAndroid.js +27 -7
  22. package/dist/native/buildAppIos.js +139 -20
  23. package/dist/native/buildIos/assets.js +76 -4
  24. package/dist/native/buildIos/build.js +31 -181
  25. package/dist/native/buildIos/config.js +126 -11
  26. package/dist/native/buildIos/index.js +354 -34
  27. package/dist/native/buildIos/localhostCert.js +87 -0
  28. package/dist/native/buildIos/output.js +28 -0
  29. package/dist/native/buildIos/signing.js +596 -0
  30. package/dist/native/credentials.js +309 -0
  31. package/dist/native/exportCredentials.js +51 -0
  32. package/dist/native/iosToolchain.js +583 -0
  33. package/dist/native/iosnativeWebView/Configurations/Shared.xcconfig +1 -3
  34. package/dist/native/iosnativeWebView/iosnativeWebView/Info-Release.plist +2 -0
  35. package/dist/native/iosnativeWebView/iosnativeWebView/PrivacyInfo.xcprivacy +40 -0
  36. package/dist/native/iosnativeWebView/iosnativeWebView/iosnativeWebView.entitlements +0 -2
  37. package/dist/native/iosnativeWebView/iosnativeWebView.xcodeproj/project.pbxproj +4 -4
  38. package/dist/native/localState.js +148 -0
  39. package/dist/native/nativeConfig.js +621 -0
  40. package/dist/native/releaseOutput.js +53 -0
  41. package/dist/native/renameAndroidProject.js +154 -369
  42. package/dist/native/setupEmulatorIos.js +58 -275
  43. package/dist/native/setupServer.js +520 -124
  44. package/dist/native/utils.js +418 -115
  45. package/dist/vite/resolveDevServerConfig.js +21 -0
  46. package/mcp_v2/knowledge-base.json +13 -11
  47. package/mcp_v2/test/config.test.ts +40 -0
  48. package/mcp_v2/tools/build.js +80 -104
  49. package/mcp_v2/tools/config.js +181 -174
  50. package/package.json +1 -1
  51. package/test/cli/nativeFlags.test.ts +97 -0
  52. package/test/native/androidDevices.test.ts +230 -0
  53. package/test/native/androidElfAlignment.test.ts +148 -0
  54. package/test/native/androidSigning.test.ts +646 -0
  55. package/test/native/androidToolchain.test.ts +766 -0
  56. package/test/native/appIcon.test.ts +124 -0
  57. package/test/native/credentials.test.ts +241 -0
  58. package/test/native/interaction.test.ts +179 -0
  59. package/test/native/iosConfigPhase.test.ts +611 -0
  60. package/test/native/iosSigning.test.ts +1060 -0
  61. package/test/native/iosToolchain.test.ts +821 -0
  62. package/test/native/localState.test.ts +80 -0
  63. package/test/native/nativeConfig.test.ts +602 -0
  64. package/test/native/setupServer.test.ts +529 -0
  65. package/test/native/utils.test.ts +102 -127
  66. package/test/vite/resolveDevServerConfig.test.js +25 -0
  67. package/dist/native/androidProject/keystore/Untitled +0 -0
  68. package/dist/native/androidProject/keystore/release-key.jks +0 -0
  69. package/dist/native/iosnativeWebView/iosnativeWebView/localhost.p12 +0 -0
@@ -1,13 +1,12 @@
1
1
  "use strict";
2
2
 
3
3
  const fs = require("fs");
4
- const os = require("os");
5
4
  const {
6
- detectAndroidSdk,
7
- listAvds,
8
- expandHome,
9
5
  ANDROID_SDK_HINT
10
6
  } = require("../utils.js");
7
+ const {
8
+ resolveNativeConfig
9
+ } = require("../nativeConfig.js");
11
10
 
12
11
  /** An error carrying the one action that fixes it, shown as the diagnostic's hint. */
13
12
  function withHint(message, hint) {
@@ -15,94 +14,87 @@ function withHint(message, hint) {
15
14
  hint
16
15
  });
17
16
  }
18
- const tilde = dir => dir.startsWith(os.homedir()) ? `~${dir.slice(os.homedir().length)}` : dir;
19
17
  function createConfigPhase(ctx) {
20
18
  const {
21
19
  configPath,
22
20
  progress
23
21
  } = ctx;
24
- async function initializeConfig() {
22
+
23
+ /**
24
+ * Read config.json and resolve what this build uses (nativeConfig.js):
25
+ * release or debug, the start URL and its guard, the app id and the
26
+ * version. The resolved config is written to a temp file for Gradle;
27
+ * call `cleanup()` when done.
28
+ *
29
+ * @param {object} [options]
30
+ * @param {boolean} [options.release] - `--release`
31
+ * @param {string} [options.url] - `--url`
32
+ * @param {string} [options.devHost] - the host the device reaches the dev server on
33
+ */
34
+ async function initializeConfig({
35
+ release: releaseFlag = false,
36
+ url,
37
+ devHost
38
+ } = {}) {
25
39
  const configFile = fs.readFileSync(configPath, "utf8");
26
40
  const config = JSON.parse(configFile);
27
41
  const {
28
- WEBVIEW_CONFIG,
42
+ WEBVIEW_CONFIG: baseConfig,
29
43
  BUILD_OUTPUT_PATH
30
44
  } = config;
31
- if (!WEBVIEW_CONFIG || Object.keys(WEBVIEW_CONFIG).length === 0) {
45
+ if (!baseConfig || Object.keys(baseConfig).length === 0) {
32
46
  throw new Error("WebView Config missing in " + configPath);
33
47
  }
34
- if (!WEBVIEW_CONFIG.android) {
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.");
36
- }
37
- const buildType = WEBVIEW_CONFIG.android.buildType || "debug";
38
- if (buildType === "release") {
39
- progress.log("Release build detected - AAB will be generated", "info");
48
+ const log = (message, type) => progress.log(message, type);
49
+ const resolved = resolveNativeConfig("android", {
50
+ release: releaseFlag,
51
+ url,
52
+ devHost,
53
+ config,
54
+ log
55
+ });
56
+ if (resolved.release) {
57
+ progress.log("Release build - a signed AAB will be generated", "info");
58
+ progress.log(`Release start URL: ${resolved.startUrl}`, "info");
40
59
  }
41
60
  return {
42
- WEBVIEW_CONFIG,
43
- BUILD_OUTPUT_PATH
61
+ config,
62
+ WEBVIEW_CONFIG: resolved.WEBVIEW_CONFIG,
63
+ BUILD_OUTPUT_PATH,
64
+ release: resolved.release,
65
+ version: resolved.version,
66
+ applicationId: resolved.appId,
67
+ usesDevServer: resolved.usesDevServer,
68
+ resolvedConfigPath: resolved.configPath,
69
+ cleanup: resolved.cleanup
44
70
  };
45
71
  }
46
- function validateAndroidTools(androidConfig) {
47
- progress.status("validating tools");
48
72
 
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);
73
+ /**
74
+ * Check the SDK tools a build runs, once ensureAndroidSdk has installed
75
+ * them: adb answers, and the emulator binary is there when one is used.
76
+ *
77
+ * @param {{ sdkPath: string }} androidConfig - sdkPath already resolved (resolveSdkPath)
78
+ * @param {{ needEmulator?: boolean }} [options]
79
+ */
80
+ function validateAndroidTools(androidConfig, {
81
+ needEmulator = true
82
+ } = {}) {
83
+ progress.status("validating tools");
60
84
  const ANDROID_SDK = androidConfig.sdkPath;
61
85
  const ADB_PATH = `${ANDROID_SDK}/platform-tools/adb`;
62
86
  const EMULATOR_PATH = `${ANDROID_SDK}/emulator/emulator`;
63
- if (!fs.existsSync(ANDROID_SDK)) {
64
- throw withHint(`Android SDK path does not exist: ${ANDROID_SDK}`, ANDROID_SDK_HINT);
65
- }
66
- if (!fs.existsSync(ADB_PATH)) {
67
- throw withHint(`adb not found at ${ADB_PATH}`, "Install Android SDK Platform-Tools from Android Studio's SDK Manager.");
68
- }
87
+ if (!fs.existsSync(ADB_PATH)) throw withHint(`adb not found at ${ADB_PATH}`, ANDROID_SDK_HINT);
69
88
  try {
70
- ctx.runCommand(`${ADB_PATH} version`, {
89
+ ctx.runFile(ADB_PATH, ["version"], {
71
90
  timeout: 15000
72
91
  });
73
92
  } catch (error) {
74
93
  if (error.hint) throw error;
75
94
  throw new Error(`ADB is not working properly: ${error.message}`);
76
95
  }
77
- const buildType = androidConfig.buildType || "debug";
78
- if (buildType !== "release") {
79
- if (!fs.existsSync(EMULATOR_PATH)) {
80
- throw new Error(`Emulator not found at: ${EMULATOR_PATH}`);
81
- }
82
- try {
83
- ctx.runCommand(`${EMULATOR_PATH} -version`);
84
- } catch (error) {
85
- throw new Error(`Emulator is not working properly: ${error.message}`);
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
- }
95
- try {
96
- const avdOutput = ctx.runCommand(`${EMULATOR_PATH} -list-avds`);
97
- if (!avdOutput.includes(androidConfig.emulatorName)) {
98
- throw new Error(`Specified emulator "${androidConfig.emulatorName}" not found in available AVDs`);
99
- }
100
- progress.log(`Emulator "${androidConfig.emulatorName}" exists`, "success");
101
- } catch (error) {
102
- throw new Error(`Error checking emulator AVD: ${error.message}`);
103
- }
104
- } else {
105
- progress.log("Skipping emulator validation for release build", "info");
96
+ if (needEmulator && !fs.existsSync(EMULATOR_PATH)) {
97
+ throw withHint(`Emulator not found at ${EMULATOR_PATH}`, ANDROID_SDK_HINT);
106
98
  }
107
99
  return {
108
100
  ANDROID_SDK,
@@ -0,0 +1,176 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.chooseAndroidTarget = chooseAndroidTarget;
7
+ exports.devServerReachPlan = devServerReachPlan;
8
+ exports.launchArgs = launchArgs;
9
+ exports.parseAdbDevices = parseAdbDevices;
10
+ exports.reachDevServer = reachDevServer;
11
+ exports.readyPhones = readyPhones;
12
+ var _utils = require("../utils.js");
13
+ // Where a debug build runs, and how it reaches the dev server.
14
+ //
15
+ // The emulator is the default. `--device` picks a connected phone instead,
16
+ // and in a terminal with both available the person is asked, with the last
17
+ // pick (.catalyst/local.json) as the default. A phone is never picked just
18
+ // because it is plugged in.
19
+ //
20
+ // Debug builds load http://localhost:<port>. On the emulator and on a USB
21
+ // phone alike, localhost is the device itself, so every port the app uses
22
+ // is forwarded to this machine with `adb reverse` after each install.
23
+
24
+ /**
25
+ * Parse `adb devices -l`.
26
+ *
27
+ * @param {string} output
28
+ * @returns {{ serial: string, state: string, emulator: boolean, model: string | null }[]}
29
+ */
30
+ function parseAdbDevices(output) {
31
+ return String(output || "").split("\n").map(line => line.trim()).filter(line => line && !line.startsWith("List of devices") && !line.startsWith("*")).map(line => {
32
+ const [serial, state, ...rest] = line.split(/\s+/);
33
+ const props = Object.fromEntries(rest.filter(token => token.includes(":")).map(token => [token.slice(0, token.indexOf(":")), token.slice(token.indexOf(":") + 1)]));
34
+ return {
35
+ serial,
36
+ state,
37
+ emulator: serial.startsWith("emulator-"),
38
+ model: props.model ? props.model.replace(/_/g, " ") : null
39
+ };
40
+ }).filter(device => device.serial && device.state);
41
+ }
42
+
43
+ /** Phones adb can install on: not an emulator, and authorized ("device"). */
44
+ function readyPhones(devices) {
45
+ return devices.filter(device => !device.emulator && device.state === "device");
46
+ }
47
+ const phoneLabel = device => device.model ? `${device.model} (${device.serial})` : device.serial;
48
+
49
+ /**
50
+ * Choose where a debug build runs.
51
+ *
52
+ * @param {object} options
53
+ * @param {boolean} [options.wantDevice] - `--device`
54
+ * @param {ReturnType<typeof parseAdbDevices>} options.devices
55
+ * @param {{ target: string, serial?: string, avd?: string } | null} [options.last] - getLastTarget("android")
56
+ * @param {Record<string, string | undefined>} [options.env] - ANDROID_SERIAL picks a phone without a terminal
57
+ * @param {boolean} [options.isTTY]
58
+ * @param {typeof selectChoice} [options.select]
59
+ * @param {string} [options.emulatorHint] - shown beside the emulator choice
60
+ * @returns {Promise<{ target: "emulator" } | { target: "device", serial: string, model: string | null }>}
61
+ */
62
+ async function chooseAndroidTarget({
63
+ wantDevice = false,
64
+ devices,
65
+ last = null,
66
+ env = process.env,
67
+ isTTY,
68
+ select = _utils.select,
69
+ emulatorHint
70
+ }) {
71
+ const phones = readyPhones(devices);
72
+ const asDevice = device => ({
73
+ target: "device",
74
+ serial: device.serial,
75
+ model: device.model
76
+ });
77
+ const lastPhone = last?.target === "device" ? phones.find(device => device.serial === last.serial) : undefined;
78
+ if (wantDevice) {
79
+ if (env.ANDROID_SERIAL) {
80
+ const named = phones.find(device => device.serial === env.ANDROID_SERIAL);
81
+ if (!named) {
82
+ throw (0, _utils.nativeError)("ANDROID-011", `ANDROID_SERIAL=${env.ANDROID_SERIAL} is not a connected phone`);
83
+ }
84
+ return asDevice(named);
85
+ }
86
+ if (!phones.length) {
87
+ const unauthorized = devices.filter(device => !device.emulator && device.state === "unauthorized");
88
+ throw (0, _utils.nativeError)("ANDROID-011", unauthorized.length ? `${unauthorized.map(device => device.serial).join(", ")} has not allowed this computer yet; accept the prompt on the phone` : "adb devices lists no phone");
89
+ }
90
+ if (phones.length === 1) return asDevice(phones[0]);
91
+ const question = `Several Android phones are connected (${phones.map(device => device.serial).join(", ")}). Which one`;
92
+ if (!isTTY) {
93
+ throw (0, _utils.needsAnswerError)(question, {
94
+ hint: "Set ANDROID_SERIAL to the serial of the phone to use (adb devices lists them), or run the command in a terminal to pick one."
95
+ });
96
+ }
97
+ const serial = await select(question, phones.map(device => ({
98
+ label: phoneLabel(device),
99
+ value: device.serial
100
+ })), {
101
+ defaultValue: (lastPhone || phones[0]).serial,
102
+ isTTY
103
+ });
104
+ return asDevice(phones.find(device => device.serial === serial));
105
+ }
106
+
107
+ // No flag: the emulator, unless a person here picks a connected phone.
108
+ if (!phones.length || !isTTY) return {
109
+ target: "emulator"
110
+ };
111
+ const pick = await select("Run the app on", [{
112
+ label: "Emulator",
113
+ value: "emulator",
114
+ hint: emulatorHint
115
+ }, ...phones.map(device => ({
116
+ label: phoneLabel(device),
117
+ value: device.serial,
118
+ hint: "connected phone"
119
+ }))], {
120
+ defaultValue: lastPhone ? lastPhone.serial : "emulator",
121
+ isTTY
122
+ });
123
+ if (pick === "emulator") return {
124
+ target: "emulator"
125
+ };
126
+ return asDevice(phones.find(device => device.serial === pick));
127
+ }
128
+
129
+ /**
130
+ * The adb calls that let the app on `serial` reach the dev server: stop the
131
+ * running app first (its in-app server scans device ports 3000 to 3099 and
132
+ * would hold the port a reverse needs), then reverse every port.
133
+ *
134
+ * @param {string} serial
135
+ * @param {string} packageName - the installed (debug) package
136
+ * @param {number[]} ports - devServerPorts()
137
+ * @returns {string[][]} adb argv lists, in order
138
+ */
139
+ function devServerReachPlan(serial, packageName, ports) {
140
+ return [["-s", serial, "shell", "am", "force-stop", packageName], ...ports.map(port => ["-s", serial, "reverse", `tcp:${port}`, `tcp:${port}`])];
141
+ }
142
+
143
+ /**
144
+ * Run devServerReachPlan with `adb(args)`. Stopping the app may fail (it may
145
+ * not be running); a failed reverse is an error, since the app would load a
146
+ * page with nothing behind it.
147
+ *
148
+ * @param {(args: string[]) => unknown} adb
149
+ * @param {string} serial
150
+ * @param {string} packageName
151
+ * @param {number[]} ports
152
+ */
153
+ function reachDevServer(adb, serial, packageName, ports) {
154
+ const [stop, ...reverses] = devServerReachPlan(serial, packageName, ports);
155
+ try {
156
+ adb(stop);
157
+ } catch {
158
+ // Not running is fine.
159
+ }
160
+ for (const args of reverses) {
161
+ try {
162
+ adb(args);
163
+ } catch (error) {
164
+ const detail = String(error.stderr || error.message || "").trim().split("\n")[0];
165
+ throw Object.assign(new Error(`adb ${args.slice(2).join(" ")} failed on ${serial}: ${detail}`), {
166
+ stage: "Connecting the device to the dev server",
167
+ hint: `Run adb -s ${serial} reverse --list to see what holds the port, quit it, and build again.`
168
+ });
169
+ }
170
+ }
171
+ }
172
+
173
+ /** adb argv that opens the app's launcher activity. */
174
+ function launchArgs(serial, packageName) {
175
+ return ["-s", serial, "shell", "monkey", "-p", packageName, "-c", "android.intent.category.LAUNCHER", "1"];
176
+ }