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.
- package/bin/catalyst.js +54 -6
- package/changelog.md +27 -1
- package/dist/errors/registry.js +306 -18
- package/dist/errors-index.json +273 -70
- package/dist/native/androidProject/app/build.gradle.kts +53 -79
- package/dist/native/androidProject/app/src/main/AndroidManifest.xml +1 -1
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/CatalystConstants.kt +3 -1
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/MainActivity.kt +32 -4
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/utils/KeyboardUtil.kt +5 -1
- package/dist/native/androidProject/app/src/test/java/io/yourname/androidproject/ConfigMappingTest.kt +25 -0
- package/dist/native/androidSetup.js +76 -170
- package/dist/native/androidToolchain.js +642 -0
- package/dist/native/appIcon.js +100 -0
- package/dist/native/buildAndroid/assets.js +29 -5
- package/dist/native/buildAndroid/build.js +110 -223
- package/dist/native/buildAndroid/config.js +57 -65
- package/dist/native/buildAndroid/devices.js +176 -0
- package/dist/native/buildAndroid/index.js +271 -46
- package/dist/native/buildAndroid/output.js +43 -0
- package/dist/native/buildAndroid/signing.js +456 -0
- package/dist/native/buildAppAndroid.js +27 -7
- package/dist/native/buildAppIos.js +139 -20
- package/dist/native/buildIos/assets.js +76 -4
- package/dist/native/buildIos/build.js +31 -181
- package/dist/native/buildIos/config.js +126 -11
- package/dist/native/buildIos/index.js +354 -34
- package/dist/native/buildIos/localhostCert.js +87 -0
- package/dist/native/buildIos/output.js +28 -0
- package/dist/native/buildIos/signing.js +596 -0
- package/dist/native/credentials.js +309 -0
- package/dist/native/exportCredentials.js +51 -0
- package/dist/native/iosToolchain.js +583 -0
- package/dist/native/iosnativeWebView/Configurations/Shared.xcconfig +1 -3
- package/dist/native/iosnativeWebView/iosnativeWebView/Info-Release.plist +2 -0
- package/dist/native/iosnativeWebView/iosnativeWebView/PrivacyInfo.xcprivacy +40 -0
- package/dist/native/iosnativeWebView/iosnativeWebView/iosnativeWebView.entitlements +0 -2
- package/dist/native/iosnativeWebView/iosnativeWebView.xcodeproj/project.pbxproj +4 -4
- package/dist/native/localState.js +148 -0
- package/dist/native/nativeConfig.js +621 -0
- package/dist/native/releaseOutput.js +53 -0
- package/dist/native/renameAndroidProject.js +154 -369
- package/dist/native/setupEmulatorIos.js +58 -275
- package/dist/native/setupServer.js +520 -124
- package/dist/native/utils.js +418 -115
- package/dist/vite/resolveDevServerConfig.js +21 -0
- package/mcp_v2/knowledge-base.json +13 -11
- package/mcp_v2/test/config.test.ts +40 -0
- package/mcp_v2/tools/build.js +80 -104
- package/mcp_v2/tools/config.js +181 -174
- package/package.json +1 -1
- package/test/cli/nativeFlags.test.ts +97 -0
- package/test/native/androidDevices.test.ts +230 -0
- package/test/native/androidElfAlignment.test.ts +148 -0
- package/test/native/androidSigning.test.ts +646 -0
- package/test/native/androidToolchain.test.ts +766 -0
- package/test/native/appIcon.test.ts +124 -0
- package/test/native/credentials.test.ts +241 -0
- package/test/native/interaction.test.ts +179 -0
- package/test/native/iosConfigPhase.test.ts +611 -0
- package/test/native/iosSigning.test.ts +1060 -0
- package/test/native/iosToolchain.test.ts +821 -0
- package/test/native/localState.test.ts +80 -0
- package/test/native/nativeConfig.test.ts +602 -0
- package/test/native/setupServer.test.ts +529 -0
- package/test/native/utils.test.ts +102 -127
- package/test/vite/resolveDevServerConfig.test.js +25 -0
- package/dist/native/androidProject/keystore/Untitled +0 -0
- package/dist/native/androidProject/keystore/release-key.jks +0 -0
- package/dist/native/iosnativeWebView/iosnativeWebView/localhost.p12 +0 -0
package/dist/native/utils.js
CHANGED
|
@@ -3,19 +3,26 @@
|
|
|
3
3
|
Object.defineProperty(exports, "__esModule", {
|
|
4
4
|
value: true
|
|
5
5
|
});
|
|
6
|
-
exports.ANDROID_SDK_HINT = void 0;
|
|
6
|
+
exports.NATIVE_ERRORS = exports.ANDROID_SDK_HINT = void 0;
|
|
7
|
+
exports.askConsent = askConsent;
|
|
7
8
|
exports.commandExists = commandExists;
|
|
9
|
+
exports.confirm = confirm;
|
|
8
10
|
exports.detectAndroidSdk = detectAndroidSdk;
|
|
9
11
|
exports.ensureLocalProperties = ensureLocalProperties;
|
|
10
12
|
exports.expandHome = expandHome;
|
|
13
|
+
exports.isInteractive = isInteractive;
|
|
11
14
|
exports.listAvds = listAvds;
|
|
15
|
+
exports.nativeError = nativeError;
|
|
16
|
+
exports.needsAnswerError = needsAnswerError;
|
|
17
|
+
exports.parseNativeArgs = parseNativeArgs;
|
|
12
18
|
exports.promptUser = promptUser;
|
|
13
19
|
exports.redactError = redactError;
|
|
14
20
|
exports.redactSecrets = redactSecrets;
|
|
15
21
|
exports.runCommand = runCommand;
|
|
22
|
+
exports.runFile = runFile;
|
|
23
|
+
exports.runInheritedCommand = runInheritedCommand;
|
|
16
24
|
exports.runInteractiveCommand = runInteractiveCommand;
|
|
17
|
-
exports.
|
|
18
|
-
exports.validateAndCompleteConfig = validateAndCompleteConfig;
|
|
25
|
+
exports.select = select;
|
|
19
26
|
var _child_process = require("child_process");
|
|
20
27
|
var _readline = _interopRequireDefault(require("readline"));
|
|
21
28
|
var _fs = _interopRequireDefault(require("fs"));
|
|
@@ -105,6 +112,44 @@ function runCommand(command, {
|
|
|
105
112
|
throw error;
|
|
106
113
|
}
|
|
107
114
|
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* runCommand for a program and its arguments, with no shell in between. For
|
|
118
|
+
* tools whose path comes from the environment or config (an SDK's adb or
|
|
119
|
+
* emulator): a path holding quotes or `$(...)` is just a file name here.
|
|
120
|
+
*
|
|
121
|
+
* @param {string} file - the program; must be a non-empty path with no NUL or newline
|
|
122
|
+
* @param {string[]} args
|
|
123
|
+
* @param {object} [options]
|
|
124
|
+
* @param {number} [options.timeout] - ms before the program is killed
|
|
125
|
+
*/
|
|
126
|
+
function runFile(file, args = [], {
|
|
127
|
+
timeout
|
|
128
|
+
} = {}) {
|
|
129
|
+
assertToolPath(file);
|
|
130
|
+
const display = [file, ...args].join(" ");
|
|
131
|
+
try {
|
|
132
|
+
// nosemgrep: javascript.lang.security.detect-child-process.detect-child-process - argv array, no shell; the path is validated above.
|
|
133
|
+
return (0, _child_process.execFileSync)(file, args, {
|
|
134
|
+
encoding: "utf8",
|
|
135
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
136
|
+
timeout
|
|
137
|
+
});
|
|
138
|
+
} catch (error) {
|
|
139
|
+
if (timeout && isTimeout(error)) throw toolTimeoutError(_path.default.basename(file), timeout);
|
|
140
|
+
redactError(error, display);
|
|
141
|
+
console.error(`Error executing command: ${secretScrubber(display)(display)}`);
|
|
142
|
+
console.error(`Error message: ${error.message}`);
|
|
143
|
+
throw error;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** A tool path from config or the environment: a non-empty string with no NUL or line break. */
|
|
148
|
+
function assertToolPath(file) {
|
|
149
|
+
if (typeof file !== "string" || !file.trim() || /[\0\r\n]/.test(file)) {
|
|
150
|
+
throw new Error(`Not a valid tool path: ${JSON.stringify(file)}`);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
108
153
|
function commandExists(command) {
|
|
109
154
|
try {
|
|
110
155
|
// nosemgrep: javascript.lang.security.detect-child-process.detect-child-process - Utility checks known local tool names during native setup.
|
|
@@ -199,8 +244,11 @@ function detectAndroidSdk(env = process.env) {
|
|
|
199
244
|
function listAvds(sdkPath) {
|
|
200
245
|
if (!sdkPath) return [];
|
|
201
246
|
try {
|
|
202
|
-
// nosemgrep: javascript.lang.security.
|
|
203
|
-
const
|
|
247
|
+
// nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal - sdkPath is a local SDK directory from config or auto-detection, never remote input.
|
|
248
|
+
const emulator = _path.default.join(expandHome(sdkPath), "emulator", "emulator");
|
|
249
|
+
assertToolPath(emulator);
|
|
250
|
+
// nosemgrep: javascript.lang.security.detect-child-process.detect-child-process - argv array, no shell.
|
|
251
|
+
const out = (0, _child_process.execFileSync)(emulator, ["-list-avds"], {
|
|
204
252
|
encoding: "utf8",
|
|
205
253
|
stdio: ["ignore", "pipe", "ignore"],
|
|
206
254
|
timeout: 15000
|
|
@@ -220,7 +268,7 @@ function expandHome(dir) {
|
|
|
220
268
|
}
|
|
221
269
|
|
|
222
270
|
/** The hint for a missing or wrong SDK path, naming the one field to set. */
|
|
223
|
-
const ANDROID_SDK_HINT = exports.ANDROID_SDK_HINT = '
|
|
271
|
+
const ANDROID_SDK_HINT = exports.ANDROID_SDK_HINT = 'Run catalyst setupEmulator:android to install it, or set WEBVIEW_CONFIG.android.sdkPath in config/config.json (e.g. "~/Library/Android/sdk").';
|
|
224
272
|
|
|
225
273
|
/**
|
|
226
274
|
* Write the SDK location gradle reads. `setupEmulator:android` used to be the
|
|
@@ -250,6 +298,8 @@ function ensureLocalProperties(projectDir, sdkPath) {
|
|
|
250
298
|
* @param {string[]} args
|
|
251
299
|
* @param {object} [promptResponses] - text to answer prompts with
|
|
252
300
|
* @param {object} [options]
|
|
301
|
+
* @param {NodeJS.ProcessEnv} [options.env] - the child's environment (defaults to this process's)
|
|
302
|
+
* @param {string} [options.cwd] - the child's working directory
|
|
253
303
|
* @param {(line: string) => void} [options.onLine] - called per output line.
|
|
254
304
|
* Without it the output is captured silently. Gradle and xcodebuild emit
|
|
255
305
|
* hundreds of lines that nobody reads on success and that are redundant on
|
|
@@ -258,14 +308,24 @@ function ensureLocalProperties(projectDir, sdkPath) {
|
|
|
258
308
|
* than letting the raw stream scroll.
|
|
259
309
|
*/
|
|
260
310
|
async function runInteractiveCommand(command, args, promptResponses = {}, {
|
|
261
|
-
onLine
|
|
311
|
+
onLine,
|
|
312
|
+
env,
|
|
313
|
+
cwd
|
|
262
314
|
} = {}) {
|
|
263
315
|
return new Promise((resolve, reject) => {
|
|
264
316
|
// Named `child`, not `process`: the previous name shadowed the global
|
|
265
317
|
// `process` for the whole function body.
|
|
318
|
+
// `env` is how secrets reach a child (ORG_GRADLE_PROJECT_*): never argv,
|
|
319
|
+
// where ps and the failure echo would show them.
|
|
266
320
|
// nosemgrep: javascript.lang.security.detect-child-process.detect-child-process - Native build commands are fixed internal commands passed with argv arrays.
|
|
267
321
|
const child = (0, _child_process.spawn)(command, args, {
|
|
268
|
-
stdio: ["pipe", "pipe", "pipe"]
|
|
322
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
323
|
+
...(env ? {
|
|
324
|
+
env
|
|
325
|
+
} : {}),
|
|
326
|
+
...(cwd ? {
|
|
327
|
+
cwd
|
|
328
|
+
} : {})
|
|
269
329
|
});
|
|
270
330
|
let promptBuffer = "";
|
|
271
331
|
let outputBuffer = "";
|
|
@@ -352,128 +412,371 @@ function handlePrompts(child, buffer, promptResponses) {
|
|
|
352
412
|
}
|
|
353
413
|
return buffer;
|
|
354
414
|
}
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
415
|
+
|
|
416
|
+
/**
|
|
417
|
+
* Run a command on the user's own terminal: inherited stdio, so they see its
|
|
418
|
+
* progress and can answer it (a sudo password, a licence). The spinner is
|
|
419
|
+
* stood down for the duration. Only for steps a person has agreed to.
|
|
420
|
+
*
|
|
421
|
+
* @param {string} command
|
|
422
|
+
* @param {string[]} args
|
|
423
|
+
* @param {object} [options]
|
|
424
|
+
* @param {NodeJS.ProcessEnv} [options.env]
|
|
425
|
+
* @param {string} [options.cwd]
|
|
426
|
+
*/
|
|
427
|
+
async function runInheritedCommand(command, args, {
|
|
428
|
+
env,
|
|
429
|
+
cwd
|
|
430
|
+
} = {}) {
|
|
431
|
+
return withSpinnersPaused(() => new Promise((resolve, reject) => {
|
|
432
|
+
// nosemgrep: javascript.lang.security.detect-child-process.detect-child-process - Fixed setup commands with argv arrays, run after the user agreed.
|
|
433
|
+
const child = (0, _child_process.spawn)(command, args, {
|
|
434
|
+
stdio: "inherit",
|
|
435
|
+
...(env ? {
|
|
436
|
+
env
|
|
437
|
+
} : {}),
|
|
438
|
+
...(cwd ? {
|
|
439
|
+
cwd
|
|
440
|
+
} : {})
|
|
441
|
+
});
|
|
442
|
+
child.on("error", error => reject(error.code === "ENOENT" ? new Error(`Command not found: ${command}`, {
|
|
443
|
+
cause: error
|
|
444
|
+
}) : error));
|
|
445
|
+
child.on("close", code => {
|
|
446
|
+
if (code === 0) resolve();else reject(Object.assign(new Error(`${command} exited with code ${code}`), {
|
|
447
|
+
code
|
|
448
|
+
}));
|
|
449
|
+
});
|
|
450
|
+
}));
|
|
361
451
|
}
|
|
362
|
-
async function validateAndCompleteConfig(platform, configPath) {
|
|
363
|
-
// Read existing config
|
|
364
|
-
let config;
|
|
365
|
-
try {
|
|
366
|
-
const configContent = _fs.default.readFileSync(configPath, "utf8");
|
|
367
|
-
config = JSON.parse(configContent);
|
|
368
|
-
} catch (error) {
|
|
369
|
-
console.error(`Error reading config file: ${error.message}`);
|
|
370
|
-
config = {};
|
|
371
|
-
}
|
|
372
452
|
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
453
|
+
// ---------------------------------------------------------------------------
|
|
454
|
+
// Coded errors shared by the Android and iOS builds.
|
|
455
|
+
// ---------------------------------------------------------------------------
|
|
456
|
+
|
|
457
|
+
// Messages mirror errors/registry.js ERROR_DEFINITIONS (see errors/<CATEGORY>/*.md).
|
|
458
|
+
// This CJS subtree can't import the ESM error registry (see
|
|
459
|
+
// buildErrorFormat.js), so the strings are copied here; a test keeps them equal.
|
|
460
|
+
const NATIVE_ERRORS = exports.NATIVE_ERRORS = {
|
|
461
|
+
"ANDROID-005": {
|
|
462
|
+
message: "No upload key for the release build",
|
|
463
|
+
hint: "Run catalyst buildApp:android --release once in a terminal to create credentials/android/upload.jks, then back it up with catalyst credentials:export android. In CI, set the CATALYST_ANDROID_CREDENTIALS secret to the output of catalyst credentials:export android."
|
|
464
|
+
},
|
|
465
|
+
"ANDROID-006": {
|
|
466
|
+
message: "Upload key settings are incomplete or wrong",
|
|
467
|
+
hint: "Check credentials/android/keystore.properties against credentials/android/upload.jks. In CI, run catalyst credentials:export android again and update the CATALYST_ANDROID_CREDENTIALS secret."
|
|
468
|
+
},
|
|
469
|
+
"ANDROID-007": {
|
|
470
|
+
message: "No JDK 17 to 23 found",
|
|
471
|
+
hint: "Install JDK 17 or 21 (macOS: brew install --cask zulu@17; Linux: your distribution's openjdk-17-jdk package) and set JAVA_HOME, or run catalyst buildApp:android in a terminal, or with --yes, to install it."
|
|
472
|
+
},
|
|
473
|
+
"ANDROID-008": {
|
|
474
|
+
message: "Android SDK setup was declined",
|
|
475
|
+
hint: "Run the command again and answer yes, or pass --yes to allow the downloads and --accept-licenses to accept the Android SDK licences."
|
|
476
|
+
},
|
|
477
|
+
"ANDROID-009": {
|
|
478
|
+
message: "Could not install the Android command-line tools",
|
|
479
|
+
hint: "Check your network or proxy and retry, or download the command-line tools from https://developer.android.com/studio#command-tools and unzip them so that <sdk>/cmdline-tools/latest/bin/sdkmanager exists."
|
|
480
|
+
},
|
|
481
|
+
"ANDROID-010": {
|
|
482
|
+
message: "App icon is not usable",
|
|
483
|
+
hint: "Replace public/icon.png with a square PNG of at least 1024x1024 pixels, or add per-density icons in public/android/appIcons."
|
|
484
|
+
},
|
|
485
|
+
"ANDROID-011": {
|
|
486
|
+
message: "No Android phone connected",
|
|
487
|
+
hint: "Connect the phone, turn on USB debugging in Developer options, accept the Allow USB debugging prompt on the phone and check that adb devices lists it. Or drop --device to use the emulator."
|
|
488
|
+
},
|
|
489
|
+
"IOS-002": {
|
|
490
|
+
message: "Full Xcode is not installed or selected",
|
|
491
|
+
hint: "Install Xcode from the Mac App Store, then run sudo xcode-select -s /Applications/Xcode.app/Contents/Developer."
|
|
492
|
+
},
|
|
493
|
+
"IOS-003": {
|
|
494
|
+
message: "Xcode setup is not finished",
|
|
495
|
+
hint: "Run sudo xcodebuild -license accept and sudo xcodebuild -runFirstLaunch, then retry."
|
|
496
|
+
},
|
|
497
|
+
"IOS-004": {
|
|
498
|
+
message: "No iOS simulator runtime installed",
|
|
499
|
+
hint: "Run catalyst setupEmulator:ios in a terminal or with --yes, or run xcodebuild -downloadPlatform iOS yourself."
|
|
500
|
+
},
|
|
501
|
+
"IOS-005": {
|
|
502
|
+
message: "App icon is not usable",
|
|
503
|
+
hint: "Replace public/icon.png with a square PNG of at least 1024x1024 pixels, or add the sized icons in public/ios/appIcons."
|
|
504
|
+
},
|
|
505
|
+
"IOS-006": {
|
|
506
|
+
message: "App icon has transparency",
|
|
507
|
+
hint: "Export public/icon.png without an alpha channel (flatten it onto a solid background), then rebuild."
|
|
508
|
+
},
|
|
509
|
+
"IOS-010": {
|
|
510
|
+
message: "iOS code signing is not set up",
|
|
511
|
+
hint: "For cloud signing, put an App Store Connect API key (its AuthKey_*.p8 file, with an ascIssuerId line in signing.properties) in credentials/ios/ and set WEBVIEW_CONFIG.ios.developmentTeam; for local signing, put a distribution .p12 and its .mobileprovision there instead. In CI, set CATALYST_IOS_CREDENTIALS to the output of catalyst credentials:export ios."
|
|
512
|
+
},
|
|
513
|
+
"IOS-011": {
|
|
514
|
+
message: "App Store Connect API key settings are incomplete or wrong",
|
|
515
|
+
hint: "Put the AuthKey_*.p8 file in credentials/ios/, and an ascIssuerId line (plus ascKeyId when the file is renamed) in credentials/ios/signing.properties. In CI, run catalyst credentials:export ios again and paste the value into CATALYST_IOS_CREDENTIALS."
|
|
516
|
+
},
|
|
517
|
+
"IOS-012": {
|
|
518
|
+
message: "Manual signing inputs are incomplete or wrong",
|
|
519
|
+
hint: "Keep one distribution .p12 and the .mobileprovision for WEBVIEW_CONFIG.appId in credentials/ios/, with a p12Password line in credentials/ios/signing.properties, and check that they belong together. In CI, run catalyst credentials:export ios again and paste the value into CATALYST_IOS_CREDENTIALS."
|
|
520
|
+
},
|
|
521
|
+
"IOS-013": {
|
|
522
|
+
message: "Unknown iOS export method",
|
|
523
|
+
hint: "Set WEBVIEW_CONFIG.ios.exportMethod to app-store-connect (the default), release-testing, enterprise or debugging."
|
|
524
|
+
},
|
|
525
|
+
"IOS-014": {
|
|
526
|
+
message: "No connected iPhone found",
|
|
527
|
+
hint: "Connect the iPhone with a cable, unlock it, accept Trust This Computer and turn on Developer Mode (Settings > Privacy & Security), then retry; or leave out --device to use the simulator."
|
|
528
|
+
},
|
|
529
|
+
"IOS-015": {
|
|
530
|
+
message: "No address the iPhone can reach this Mac on",
|
|
531
|
+
hint: "Join the Mac and the iPhone to the same Wi-Fi network and check the Local Hostname in System Settings > General > Sharing, then retry."
|
|
532
|
+
},
|
|
533
|
+
"NATIVE-001": {
|
|
534
|
+
message: "Release build points at a local or private host",
|
|
535
|
+
hint: "Set WEBVIEW_CONFIG.url to your production origin (e.g. https://shop.acme.com) or pass --url, or set WEBVIEW_CONFIG.allowPrivateHost to true for an internal app on a private network."
|
|
536
|
+
},
|
|
537
|
+
"NATIVE-002": {
|
|
538
|
+
message: "Invalid build number",
|
|
539
|
+
hint: "Unset CATALYST_BUILD_NUMBER to use the automatic build number, or set it to a whole number from 1 to 2100000000 that is higher than the last one you uploaded."
|
|
540
|
+
},
|
|
541
|
+
"NATIVE-003": {
|
|
542
|
+
message: "Release build uses a placeholder app id",
|
|
543
|
+
hint: "Set WEBVIEW_CONFIG.appId in config/config.json to your own reverse-DNS id, e.g. com.acme.shop."
|
|
544
|
+
},
|
|
545
|
+
"NATIVE-004": {
|
|
546
|
+
message: "Invalid production URL",
|
|
547
|
+
hint: "Use an origin such as https://shop.acme.com, without a path or a port other than 443."
|
|
548
|
+
},
|
|
549
|
+
"NATIVE-005": {
|
|
550
|
+
message: "A step needs your answer and there is no terminal to ask on",
|
|
551
|
+
hint: "Pass the flag named in the message, or run the command in an interactive terminal."
|
|
552
|
+
},
|
|
553
|
+
"NATIVE-006": {
|
|
554
|
+
message: "The dev server did not answer",
|
|
555
|
+
hint: "Run npm start on its own to see why the server does not start, then run the build again."
|
|
556
|
+
},
|
|
557
|
+
"NATIVE-007": {
|
|
558
|
+
message: "The credentials secret could not be unpacked",
|
|
559
|
+
hint: "Run catalyst credentials:export <android|ios> where credentials/<platform>/ exists and paste the whole value into the secret again."
|
|
560
|
+
},
|
|
561
|
+
"NATIVE-008": {
|
|
562
|
+
message: "No credentials to export",
|
|
563
|
+
hint: "Run the command from the app root after a release build has created credentials/<platform>/."
|
|
564
|
+
},
|
|
565
|
+
"NATIVE-009": {
|
|
566
|
+
message: "Credentials are too large for one secret",
|
|
567
|
+
hint: "Remove files the build does not need from credentials/<platform>/ (only signing keys, profiles and their settings belong there), then export again."
|
|
568
|
+
},
|
|
569
|
+
"NATIVE-010": {
|
|
570
|
+
message: "Invalid app id",
|
|
571
|
+
hint: "Set WEBVIEW_CONFIG.appId in config/config.json to a reverse-DNS id such as com.acme.shop: letters and digits only, each part starting with a letter."
|
|
572
|
+
},
|
|
573
|
+
"NATIVE-011": {
|
|
574
|
+
message: "A release with this version and build number already exists",
|
|
575
|
+
hint: "Wait a minute and run the release again, or set CATALYST_BUILD_NUMBER to a higher number. To rebuild the same build on purpose, delete that folder first."
|
|
376
576
|
}
|
|
577
|
+
};
|
|
578
|
+
const ERROR_DOC_BASE = "https://github.com/tata1mg/catalyst-core/blob/main/errors";
|
|
377
579
|
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
580
|
+
/**
|
|
581
|
+
* A coded error the build diagnostic prints with its code, hint and doc link.
|
|
582
|
+
*
|
|
583
|
+
* @param {string} code
|
|
584
|
+
* @param {string} [detail]
|
|
585
|
+
* @param {{ hint?: string }} [options]
|
|
586
|
+
* @returns {Error & { code: string, hint?: string, docUrl: string }}
|
|
587
|
+
*/
|
|
588
|
+
function nativeError(code, detail, {
|
|
589
|
+
hint
|
|
590
|
+
} = {}) {
|
|
591
|
+
const def = NATIVE_ERRORS[code] || {
|
|
592
|
+
message: "Native build failed"
|
|
382
593
|
};
|
|
594
|
+
/** @type {any} */
|
|
595
|
+
const error = new Error(detail ? `${def.message}: ${detail}` : def.message);
|
|
596
|
+
error.code = code;
|
|
597
|
+
error.hint = hint || def.hint;
|
|
598
|
+
error.docUrl = `${ERROR_DOC_BASE}/${code.replace(/-\d{3}$/, "")}/${code}.md`;
|
|
599
|
+
return error;
|
|
600
|
+
}
|
|
383
601
|
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
buildType: "Android build type (debug/release)",
|
|
390
|
-
sdkPath: "Android SDK path",
|
|
391
|
-
emulatorName: "Android emulator (AVD) name"
|
|
392
|
-
},
|
|
393
|
-
ios: {
|
|
394
|
-
buildType: "iOS build type (debug/release)",
|
|
395
|
-
appBundleId: "iOS bundle ID (e.g. com.example.app)",
|
|
396
|
-
simulatorName: "iOS simulator name (e.g. iPhone 16 Pro)"
|
|
397
|
-
}
|
|
398
|
-
};
|
|
602
|
+
// ---------------------------------------------------------------------------
|
|
603
|
+
// Flags and questions. Agents and CI never get a prompt: without a terminal,
|
|
604
|
+
// a question that needs an answer fails at once with NATIVE-005 naming the
|
|
605
|
+
// flag that answers it.
|
|
606
|
+
// ---------------------------------------------------------------------------
|
|
399
607
|
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
608
|
+
/**
|
|
609
|
+
* The native flags the CLI forwards (bin/catalyst.js keeps them out of
|
|
610
|
+
* command dispatch; this reads them back in the platform script):
|
|
611
|
+
* `--release` (signed store build), `--device` (the connected phone instead
|
|
612
|
+
* of the emulator/simulator), `--url <u>` / `--url=<u>` (the release's
|
|
613
|
+
* production URL, over WEBVIEW_CONFIG.url), `--yes` (confirms installs and
|
|
614
|
+
* downloads, never licences or key creation) and `--accept-licenses`.
|
|
615
|
+
*
|
|
616
|
+
* @param {string[]} [argv]
|
|
617
|
+
* @returns {{ release: boolean, device: boolean, url: string | undefined, yes: boolean, acceptLicenses: boolean }}
|
|
618
|
+
*/
|
|
619
|
+
function parseNativeArgs(argv = process.argv.slice(2)) {
|
|
620
|
+
const result = {
|
|
621
|
+
release: false,
|
|
622
|
+
device: false,
|
|
623
|
+
url: undefined,
|
|
624
|
+
yes: false,
|
|
625
|
+
acceptLicenses: false
|
|
408
626
|
};
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
627
|
+
for (let i = 0; i < argv.length; i++) {
|
|
628
|
+
const arg = argv[i];
|
|
629
|
+
if (arg === "--release") result.release = true;else if (arg === "--device") result.device = true;else if (arg === "--yes") result.yes = true;else if (arg === "--accept-licenses") result.acceptLicenses = true;else if (arg === "--url" || arg.startsWith("--url=")) {
|
|
630
|
+
const value = arg === "--url" ? argv[++i] : arg.slice("--url=".length);
|
|
631
|
+
if (!value || value.startsWith("--")) {
|
|
632
|
+
throw nativeError("NATIVE-004", "--url needs a value, e.g. --url https://shop.acme.com");
|
|
633
|
+
}
|
|
634
|
+
result.url = value;
|
|
635
|
+
}
|
|
412
636
|
}
|
|
637
|
+
return result;
|
|
638
|
+
}
|
|
413
639
|
|
|
414
|
-
|
|
415
|
-
|
|
640
|
+
/**
|
|
641
|
+
* True when a person can answer a question here: stdin and stdout are both
|
|
642
|
+
* terminals and `CI` is not set.
|
|
643
|
+
*
|
|
644
|
+
* @param {object} [options]
|
|
645
|
+
* @param {{ isTTY?: boolean }} [options.stdin]
|
|
646
|
+
* @param {{ isTTY?: boolean }} [options.stdout]
|
|
647
|
+
* @param {Record<string, string | undefined>} [options.env]
|
|
648
|
+
*/
|
|
649
|
+
function isInteractive({
|
|
650
|
+
stdin = process.stdin,
|
|
651
|
+
stdout = process.stdout,
|
|
652
|
+
env = process.env
|
|
653
|
+
} = {}) {
|
|
654
|
+
const ci = env.CI !== undefined && env.CI !== "" && env.CI !== "0" && env.CI.toLowerCase() !== "false";
|
|
655
|
+
return Boolean(stdin?.isTTY && stdout?.isTTY && !ci);
|
|
656
|
+
}
|
|
416
657
|
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
658
|
+
/**
|
|
659
|
+
* The error for a question that needs an answer when there is no terminal.
|
|
660
|
+
*
|
|
661
|
+
* @param {string} question - what would have been asked
|
|
662
|
+
* @param {object} [options]
|
|
663
|
+
* @param {string | null} [options.flag] - the flag that answers it; null when only a person can (key creation)
|
|
664
|
+
* @param {string} [options.hint] - replaces the generated hint
|
|
665
|
+
*/
|
|
666
|
+
function needsAnswerError(question, {
|
|
667
|
+
flag = null,
|
|
668
|
+
hint
|
|
669
|
+
} = {}) {
|
|
670
|
+
const label = String(question).replace(/[?:\s]+$/, "");
|
|
671
|
+
return nativeError("NATIVE-005", flag ? `${label} (pass ${flag})` : label, {
|
|
672
|
+
hint: hint || (flag ? `Pass ${flag} to answer yes without a terminal, or run the command in an interactive terminal.` : "Run the command once in an interactive terminal. No flag answers this, not even --yes.")
|
|
673
|
+
});
|
|
674
|
+
}
|
|
424
675
|
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
676
|
+
/**
|
|
677
|
+
* A yes/no question. Enter takes the default. Without a terminal it returns
|
|
678
|
+
* false, so callers decide (usually needsAnswerError naming the flag).
|
|
679
|
+
*/
|
|
680
|
+
async function confirm(question, {
|
|
681
|
+
defaultYes = true,
|
|
682
|
+
prompt = promptUser,
|
|
683
|
+
isTTY
|
|
684
|
+
} = {}) {
|
|
685
|
+
if (!(isTTY ?? isInteractive())) return false;
|
|
686
|
+
const answer = await prompt(`${question.replace(/[?\s]+$/, "")}? ${defaultYes ? "[Y/n]" : "[y/N]"}`, {
|
|
687
|
+
defaultValue: defaultYes ? "y" : "n"
|
|
688
|
+
});
|
|
689
|
+
return /^y(es)?$/i.test(String(answer || "").trim());
|
|
690
|
+
}
|
|
437
691
|
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
}
|
|
692
|
+
// What each kind of consent accepts in place of a typed yes.
|
|
693
|
+
const CONSENT_FLAGS = {
|
|
694
|
+
install: "--yes",
|
|
695
|
+
licence: "--accept-licenses",
|
|
696
|
+
key: null
|
|
697
|
+
};
|
|
445
698
|
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
699
|
+
/**
|
|
700
|
+
* Ask for consent to a step, following the rules for every native command:
|
|
701
|
+
* - "install": an install or download. `--yes` answers it.
|
|
702
|
+
* - "licence": accepting a licence. Only `--accept-licenses` answers it.
|
|
703
|
+
* - "key": creating a signing key. Only a person at a terminal answers it.
|
|
704
|
+
*
|
|
705
|
+
* Returns true when allowed, false when the person said no. Without a
|
|
706
|
+
* terminal and without the answering flag it throws NATIVE-005.
|
|
707
|
+
*
|
|
708
|
+
* @param {"install" | "licence" | "key"} kind
|
|
709
|
+
* @param {string} question
|
|
710
|
+
* @param {object} [options]
|
|
711
|
+
* @param {{ yes?: boolean, acceptLicenses?: boolean }} [options.flags] - from parseNativeArgs
|
|
712
|
+
* @param {boolean} [options.defaultYes] - Enter's answer; defaults to true, false for "key"
|
|
713
|
+
* @param {boolean} [options.isTTY] - defaults to isInteractive()
|
|
714
|
+
* @param {typeof promptUser} [options.prompt]
|
|
715
|
+
* @returns {Promise<boolean>}
|
|
716
|
+
*/
|
|
717
|
+
async function askConsent(kind, question, {
|
|
718
|
+
flags = {},
|
|
719
|
+
defaultYes,
|
|
720
|
+
isTTY,
|
|
721
|
+
prompt = promptUser
|
|
722
|
+
} = {}) {
|
|
723
|
+
if (!(kind in CONSENT_FLAGS)) throw new Error(`Unknown consent kind "${kind}"`);
|
|
724
|
+
if (kind === "install" && flags.yes) return true;
|
|
725
|
+
if (kind === "licence" && flags.acceptLicenses) return true;
|
|
726
|
+
const interactive = isTTY ?? isInteractive();
|
|
727
|
+
if (!interactive) throw needsAnswerError(question, {
|
|
728
|
+
flag: CONSENT_FLAGS[kind]
|
|
729
|
+
});
|
|
730
|
+
return confirm(question, {
|
|
731
|
+
defaultYes: defaultYes ?? kind !== "key",
|
|
732
|
+
prompt,
|
|
733
|
+
isTTY: true
|
|
734
|
+
});
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
/**
|
|
738
|
+
* Pick one of several choices on the terminal (a numbered list; Enter takes
|
|
739
|
+
* the default). Without a terminal it returns the default, or throws
|
|
740
|
+
* NATIVE-005 naming `flag` when there is none.
|
|
741
|
+
*
|
|
742
|
+
* @template T
|
|
743
|
+
* @param {string} question
|
|
744
|
+
* @param {{ label: string, value: T, hint?: string }[]} choices
|
|
745
|
+
* @param {object} [options]
|
|
746
|
+
* @param {T} [options.defaultValue]
|
|
747
|
+
* @param {string} [options.flag] - the flag that makes this choice without a terminal
|
|
748
|
+
* @param {boolean} [options.isTTY]
|
|
749
|
+
* @param {typeof promptUser} [options.prompt]
|
|
750
|
+
* @param {(text: string) => void} [options.write]
|
|
751
|
+
* @returns {Promise<T>}
|
|
752
|
+
*/
|
|
753
|
+
async function select(question, choices, {
|
|
754
|
+
defaultValue,
|
|
755
|
+
flag,
|
|
756
|
+
isTTY,
|
|
757
|
+
prompt = promptUser,
|
|
758
|
+
write = text => process.stdout.write(text)
|
|
759
|
+
} = {}) {
|
|
760
|
+
if (!choices.length) throw new Error("select() needs at least one choice");
|
|
761
|
+
const defaultIndex = Math.max(0, choices.findIndex(choice => choice.value === defaultValue));
|
|
762
|
+
if (!(isTTY ?? isInteractive())) {
|
|
763
|
+
if (defaultValue !== undefined) return defaultValue;
|
|
764
|
+
throw needsAnswerError(question, {
|
|
765
|
+
flag: flag || null
|
|
454
766
|
});
|
|
455
|
-
filled.push(key);
|
|
456
|
-
};
|
|
457
|
-
for (const [key, prompt] of Object.entries(commonFields)) await fill(webviewConfig, key, prompt);
|
|
458
|
-
for (const [key, prompt] of Object.entries(platformConfigs[platform])) {
|
|
459
|
-
await fill(webviewConfig[platform], key, prompt);
|
|
460
767
|
}
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
768
|
+
return withSpinnersPaused(async () => {
|
|
769
|
+
write(`${GUTTER}${t.warn("?")} ${String(question).replace(/[?:\s]+$/, "")}\n`);
|
|
770
|
+
choices.forEach((choice, index) => {
|
|
771
|
+
const hint = choice.hint ? t.dim(` ${choice.hint}`) : "";
|
|
772
|
+
write(`${GUTTER} ${index + 1}. ${choice.label}${hint}\n`);
|
|
773
|
+
});
|
|
774
|
+
for (;;) {
|
|
775
|
+
const answer = await prompt(`Choose 1-${choices.length}`, {
|
|
776
|
+
defaultValue: String(defaultIndex + 1)
|
|
777
|
+
});
|
|
778
|
+
const index = Number(String(answer).trim()) - 1;
|
|
779
|
+
if (Number.isInteger(index) && index >= 0 && index < choices.length) return choices[index].value;
|
|
468
780
|
}
|
|
469
|
-
}
|
|
470
|
-
console.error(`Error saving config file: ${error.message}`);
|
|
471
|
-
throw error;
|
|
472
|
-
}
|
|
473
|
-
|
|
474
|
-
// Return only the platform-specific config
|
|
475
|
-
return {
|
|
476
|
-
port: webviewConfig.port,
|
|
477
|
-
[platform]: webviewConfig[platform]
|
|
478
|
-
};
|
|
781
|
+
});
|
|
479
782
|
}
|
|
@@ -33,6 +33,22 @@ export function resolveDevFsAllow(devFs, frameworkPaths) {
|
|
|
33
33
|
return [...frameworkPaths, ...extra]
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
+
/**
|
|
37
|
+
* `server.allowedHosts` plus the comma-separated hosts in
|
|
38
|
+
* CATALYST_DEV_ALLOWED_HOSTS. `catalyst buildApp:ios` sets it to the Mac's
|
|
39
|
+
* `<name>.local` when a physical iPhone loads the dev server by that name:
|
|
40
|
+
* Vite answers 403 to any Host header that is not localhost or an IP.
|
|
41
|
+
*/
|
|
42
|
+
export function withExtraAllowedHosts(allowedHosts, extra) {
|
|
43
|
+
const hosts = String(extra || "")
|
|
44
|
+
.split(",")
|
|
45
|
+
.map((host) => host.trim())
|
|
46
|
+
.filter(Boolean)
|
|
47
|
+
if (allowedHosts === true || hosts.length === 0) return allowedHosts
|
|
48
|
+
const base = Array.isArray(allowedHosts) ? allowedHosts : []
|
|
49
|
+
return [...base, ...hosts.filter((host) => !base.includes(host))]
|
|
50
|
+
}
|
|
51
|
+
|
|
36
52
|
/**
|
|
37
53
|
* Vite `server` block from `buildConfig.devServer`. Passthrough keys are spread
|
|
38
54
|
* first so the framework-owned `hmr` and `fs` always win.
|
|
@@ -47,8 +63,13 @@ export function buildDevServer(devServer = {}, { frameworkPaths, isProduction =
|
|
|
47
63
|
// module) opt out. isProduction is captured at module load, so it cannot
|
|
48
64
|
// be flipped by setting NODE_ENV just before createServer().
|
|
49
65
|
const hmrDisabled = isProduction || process.env.CATALYST_NO_HMR === "true"
|
|
66
|
+
const allowedHosts = withExtraAllowedHosts(
|
|
67
|
+
serverOverrides.allowedHosts,
|
|
68
|
+
process.env.CATALYST_DEV_ALLOWED_HOSTS
|
|
69
|
+
)
|
|
50
70
|
return {
|
|
51
71
|
...serverOverrides,
|
|
72
|
+
...(allowedHosts === undefined ? {} : { allowedHosts }),
|
|
52
73
|
hmr: hmrDisabled ? false : (hmr ?? true),
|
|
53
74
|
fs: { allow: resolveDevFsAllow(fs, frameworkPaths) },
|
|
54
75
|
}
|