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
@@ -6,6 +6,7 @@ const {
6
6
  spawn
7
7
  } = require("child_process");
8
8
  const fs = require("fs");
9
+ const os = require("os");
9
10
  const path = require("path");
10
11
  const {
11
12
  displayPath
@@ -14,6 +15,38 @@ const TerminalProgress = require("../terminalProgress.js").default;
14
15
  const {
15
16
  buildFailure
16
17
  } = require("../../cli/diagnostic.js");
18
+ const {
19
+ nativeError
20
+ } = require("../utils.js");
21
+ const {
22
+ resolveNativeConfig
23
+ } = require("../nativeConfig.js");
24
+ const {
25
+ resolveCredentials
26
+ } = require("../credentials.js");
27
+ const {
28
+ isReleaseBuildType,
29
+ resolveExportMethod,
30
+ readIosCredentials,
31
+ planIosSigning,
32
+ xcodeAuthArgs,
33
+ setupP12Signing,
34
+ buildExportOptions,
35
+ writeExportOptionsPlist,
36
+ isSigningFailure,
37
+ signingSetupError
38
+ } = require("./signing.js");
39
+ const {
40
+ generateLocalhostP12
41
+ } = require("./localhostCert.js");
42
+ const {
43
+ artifactBaseName,
44
+ releaseOutputDir
45
+ } = require("./output.js");
46
+ const {
47
+ DEFAULT_VERSION_NAME,
48
+ assertReleaseFolderFree
49
+ } = require("../releaseOutput.js");
17
50
  const createConfigPhase = require("./config.js");
18
51
  const createPluginsPhase = require("./plugins.js");
19
52
  const createAssetsPhase = require("./assets.js");
@@ -26,11 +59,124 @@ const pwd = path.join(catalystCorePath, "dist/native");
26
59
  const MANAGED_BASELINE_SUFFIX = ".catalyst-base";
27
60
  const shellCommand = process.platform === "win32" ? "cmd.exe" : "sh";
28
61
  const shellArgs = command => process.platform === "win32" ? ["/d", "/s", "/c", command] : ["-c", command];
29
- function createIosBuild(config) {
62
+
63
+ // The steps of `catalyst buildApp:ios`. buildAppIos.js creates the progress
64
+ // with these, so the toolchain and target steps show before the build exists.
65
+ const IOS_BUILD_STEPS = {
66
+ toolchain: "Check Xcode",
67
+ target: "Choose simulator or iPhone",
68
+ server: "Start dev server",
69
+ signing: "Resolve signing",
70
+ config: "Generate build configuration",
71
+ launchSimulator: "Launch iOS simulator",
72
+ clean: "Clean build artifacts",
73
+ assets: "Process notification assets",
74
+ build: "Build iOS project",
75
+ findApp: "Locate built application",
76
+ install: "Install application",
77
+ launch: "Launch application",
78
+ archive: "Archive release build",
79
+ export: "Export IPA"
80
+ };
81
+
82
+ /**
83
+ * @param {object} config - { WEBVIEW_CONFIG, BUILD_OUTPUT_PATH } from config.json
84
+ * @param {object} [options]
85
+ * @param {object} [options.progress] - an existing TerminalProgress to continue (buildAppIos.js passes one made with IOS_BUILD_STEPS)
86
+ * @param {boolean} [options.release] - `--release`: archive and export the store IPA whatever ios.buildType says
87
+ * @param {string} [options.url] - `--url`: the release's production URL, over WEBVIEW_CONFIG.url
88
+ * @param {string} [options.devHost] - debug: the host the app reaches the dev server on (localhost for the simulator, the Mac's `.local` name or LAN IP for an iPhone)
89
+ * @param {{ target: "simulator", name?: string, udid?: string } | { target: "device", name: string, udid: string }} [options.target] - debug: from buildAppIos.js; without it, the configured simulator
90
+ * @param {string} [options.appRoot] - the app root, for credentials/ and build-output/
91
+ * @param {Record<string, string | undefined>} [options.env]
92
+ */
93
+ function createIosBuild(config, {
94
+ progress: existingProgress,
95
+ release: releaseFlag = false,
96
+ url: urlFlag,
97
+ devHost,
98
+ target,
99
+ appRoot = process.cwd(),
100
+ env = process.env
101
+ } = {}) {
30
102
  const {
31
- WEBVIEW_CONFIG,
32
103
  BUILD_OUTPUT_PATH
33
104
  } = config;
105
+ const release = Boolean(releaseFlag || isReleaseBuildType(config.WEBVIEW_CONFIG?.ios?.buildType));
106
+ const progress = existingProgress || new TerminalProgress(IOS_BUILD_STEPS, "catalyst buildApp", {
107
+ subject: "ios"
108
+ });
109
+ const log = (message, type) => progress.log(message, type);
110
+
111
+ // ─── Config resolution, before anything reads the config ──────────────────
112
+ // nativeConfig.js turns url/appId/appName into the legacy keys this shell
113
+ // reads, points a debug build at the dev server, and for a release guards
114
+ // the start URL and the app id. All of it fails here, before Xcode runs.
115
+
116
+ const resolved = resolveNativeConfig("ios", {
117
+ release,
118
+ url: urlFlag,
119
+ devHost,
120
+ config,
121
+ cwd: appRoot,
122
+ env,
123
+ log
124
+ });
125
+ // iOS reads the resolved config in memory; Gradle is the one that needs the file.
126
+ resolved.cleanup();
127
+ const WEBVIEW_CONFIG = resolved.WEBVIEW_CONFIG;
128
+ const {
129
+ version,
130
+ usesDevServer
131
+ } = resolved;
132
+ // Before signing files are read or Xcode runs: a second release in the
133
+ // same minute would overwrite the first one's IPA.
134
+ if (release) assertReleaseFolderFree(releaseOutputDir(appRoot, version), [".ipa"]);
135
+ const exportMethod = release ? resolveExportMethod(WEBVIEW_CONFIG.ios || {}, {
136
+ log
137
+ }) : undefined;
138
+
139
+ // Signing files come from credentials/ios/, or in CI from
140
+ // CATALYST_IOS_CREDENTIALS unpacked to a temp folder that cleanup()
141
+ // removes. A release reads them up front; a debug build only when it
142
+ // signs for an iPhone (a simulator build never signs).
143
+ let credentials = null;
144
+ function loadCredentials() {
145
+ if (credentials) return credentials;
146
+ const found = resolveCredentials("ios", {
147
+ cwd: appRoot,
148
+ env,
149
+ log
150
+ });
151
+ try {
152
+ credentials = {
153
+ ...readIosCredentials(found.dir, {
154
+ env
155
+ }),
156
+ source: found.source,
157
+ cleanup: found.cleanup
158
+ };
159
+ } catch (error) {
160
+ found.cleanup();
161
+ throw error;
162
+ }
163
+ return credentials;
164
+ }
165
+ if (release) loadCredentials();
166
+ const signingPlan = planIosSigning({
167
+ iosConfig: WEBVIEW_CONFIG.ios || {},
168
+ credentials,
169
+ release,
170
+ exportMethod
171
+ });
172
+ // Automatic signing uses the API key when there is one: cloud signing
173
+ // for a release, development signing for an iPhone.
174
+ const resolveApiKey = () => signingPlan.style === "automatic" ? loadCredentials().apiKey : null;
175
+ const apiKey = release ? resolveApiKey() : null;
176
+ if (release && signingPlan.style === "automatic" && !signingPlan.teamId) {
177
+ credentials?.cleanup();
178
+ throw nativeError("IOS-010", "a signed release with automatic signing needs WEBVIEW_CONFIG.ios.developmentTeam");
179
+ }
34
180
  const iosConfig = WEBVIEW_CONFIG.ios;
35
181
  const isGoogleSignInEnabled = WEBVIEW_CONFIG.googleSignIn?.enabled ?? false;
36
182
  const protocol = WEBVIEW_CONFIG.useHttps ? "https" : "http";
@@ -41,22 +187,10 @@ function createIosBuild(config) {
41
187
  const PROJECT_DIR = `${pwd}/iosnativeWebView`;
42
188
  const SCHEME_NAME = iosConfig.scheme || "iosnativeWebView";
43
189
  const APP_BUNDLE_ID = iosConfig.appBundleId || "com.debug.webview";
190
+ // What Shared.xcconfig sets as PRODUCT_BUNDLE_IDENTIFIER (generateXCConfig).
191
+ const PRODUCT_BUNDLE_ID = iosConfig.bundleIdentifier || iosConfig.appBundleId || "com.catalyst.framework.app";
44
192
  const PROJECT_NAME = path.basename(PROJECT_DIR);
45
- const IPHONE_MODEL = iosConfig.simulatorName;
46
- const steps = {
47
- config: "Generate build configuration",
48
- deviceDetection: "Detect physical device",
49
- launchSimulator: "Launch iOS simulator",
50
- clean: "Clean build artifacts",
51
- assets: "Process notification assets",
52
- build: "Build iOS project",
53
- findApp: "Locate built application",
54
- install: "Install application",
55
- launch: "Launch application"
56
- };
57
- const progress = new TerminalProgress(steps, "catalyst buildApp", {
58
- subject: "ios"
59
- });
193
+ const IPHONE_MODEL = target?.target === "simulator" && target.name || iosConfig.simulatorName || "Catalyst iPhone";
60
194
 
61
195
  // ─── Low-level helpers shared across modules ──────────────────────────────
62
196
 
@@ -107,12 +241,16 @@ function createIosBuild(config) {
107
241
  * on the spinner's own row. The full output is still resolved for callers
108
242
  * that parse it.
109
243
  */
110
- function runCommandStreaming(command) {
244
+ function runCommandStreaming(command, args) {
111
245
  return new Promise((resolve, reject) => {
112
246
  progress.pause();
113
247
 
248
+ // With `args`, the program runs directly with that argv (no shell
249
+ // quoting to get wrong); without, `command` is a shell string.
114
250
  // nosemgrep: javascript.lang.security.detect-child-process.detect-child-process - Internal build command strings, same source as runCommand.
115
- const child = spawn(shellCommand, shellArgs(command), {
251
+ const child = Array.isArray(args) ? spawn(command, args, {
252
+ stdio: ["ignore", "pipe", "pipe"]
253
+ }) : spawn(shellCommand, shellArgs(command), {
116
254
  stdio: ["ignore", "pipe", "pipe"]
117
255
  });
118
256
  let output = "";
@@ -204,7 +342,7 @@ function createIosBuild(config) {
204
342
  WEBVIEW_CONFIG,
205
343
  BUILD_OUTPUT_PATH,
206
344
  // Captured before the build chdirs into the Xcode project.
207
- appRoot: process.cwd(),
345
+ appRoot,
208
346
  iosConfig,
209
347
  isGoogleSignInEnabled,
210
348
  PROJECT_DIR,
@@ -215,6 +353,15 @@ function createIosBuild(config) {
215
353
  url,
216
354
  PUBLIC_PATH,
217
355
  progress,
356
+ release,
357
+ version,
358
+ target,
359
+ // Debug Info.plist gets NSAllowsLocalNetworking when the app loads the dev server.
360
+ localNetworking: !release && usesDevServer,
361
+ signingPlan,
362
+ apiKey,
363
+ resolveApiKey,
364
+ apsEnvironment: signingPlan.apsEnvironment,
218
365
  runCommand,
219
366
  runCommandStreaming,
220
367
  getXcodeProjectFilePath,
@@ -248,9 +395,7 @@ function createIosBuild(config) {
248
395
  buildXcodeProject,
249
396
  buildProjectForTesting,
250
397
  findAppPath,
251
- moveAppToBuildOutput,
252
398
  installAndLaunchApp,
253
- detectPhysicalDevices,
254
399
  buildProjectForPhysicalDevice,
255
400
  findPhysicalDeviceAppPath,
256
401
  installAndLaunchOnPhysicalDevice,
@@ -258,6 +403,19 @@ function createIosBuild(config) {
258
403
  getBootedSimulatorUUID
259
404
  } = createBuildPhase(ctx);
260
405
 
406
+ /** A fresh self-signed localhost identity for the in-app HTTPS server, every build. */
407
+ function generateLocalhostCertificate() {
408
+ generateLocalhostP12(path.join(PROJECT_DIR, PROJECT_NAME, "localhost.p12"));
409
+ }
410
+
411
+ /** Remove a leftover file at this path (e.g. dSYMs from a failed export) before writing it. */
412
+ function clearArtifact(file) {
413
+ fs.rmSync(file, {
414
+ recursive: true,
415
+ force: true
416
+ });
417
+ }
418
+
261
419
  // ─── Orchestrated build flows ─────────────────────────────────────────────
262
420
 
263
421
  async function buildForIOS(pluginComposition = {}) {
@@ -272,11 +430,16 @@ function createIosBuild(config) {
272
430
  await generateXCConfig();
273
431
  await copySplashscreenAssets();
274
432
  await copyAppIcon();
433
+ generateLocalhostCertificate();
275
434
  process.chdir(PROJECT_DIR);
276
- const physicalDevice = await detectPhysicalDevices();
435
+ // buildAppIos.js chose the target before the config was resolved,
436
+ // so the dev server host fits it. No target means the simulator.
437
+ const physicalDevice = target?.target === "device" ? {
438
+ name: target.name,
439
+ udid: target.udid
440
+ } : null;
277
441
  let APP_PATH, targetInfo;
278
442
  if (physicalDevice) {
279
- progress.log("Building for physical device workflow", "success");
280
443
  targetInfo = {
281
444
  type: "physical",
282
445
  name: physicalDevice.name,
@@ -285,14 +448,17 @@ function createIosBuild(config) {
285
448
  await cleanBuildArtifacts();
286
449
  progress.start("build");
287
450
  try {
451
+ if (!ctx.apiKey) ctx.apiKey = ctx.resolveApiKey();
288
452
  await buildProjectForPhysicalDevice(SCHEME_NAME, APP_BUNDLE_ID, path.join(process.env.HOME, "Library/Developer/Xcode/DerivedData"), PROJECT_NAME, physicalDevice);
289
453
  progress.complete("build");
290
454
  } catch (error) {
291
455
  // The diagnostic in the catch below shows what xcodebuild
292
- // said, with a signing hint when that is the cause; a fixed
293
- // checklist here only guessed.
294
- error.stage = error.stage || "Building for the device";
295
- throw error;
456
+ // said. A signing failure gets IOS-010, which names both
457
+ // ways to set signing up. Not checked up front: an Apple
458
+ // account signed in to Xcode signs fine without either.
459
+ const failure = isSigningFailure(error.output) ? signingSetupError("Xcode could not sign the device build", error) : error;
460
+ failure.stage = failure.stage || "Building for the device";
461
+ throw failure;
296
462
  }
297
463
  progress.start("findApp");
298
464
  try {
@@ -315,23 +481,22 @@ function createIosBuild(config) {
315
481
  APP_PATH = await findAppPath();
316
482
  progress.log("Found app at: " + APP_PATH, "success");
317
483
  await installAndLaunchApp(APP_PATH);
318
- const MOVED_APP_PATH = await moveAppToBuildOutput(APP_PATH);
319
- APP_PATH = MOVED_APP_PATH;
320
484
  }
321
485
  // Same aligned key/value shape as the Android summary and the
322
- // Config tree the setup commands print.
486
+ // Config tree the setup commands print. The .app stays in
487
+ // DerivedData: a debug build copies nothing into the app.
323
488
  const deviceName = `${targetInfo.name}${targetInfo.type === "physical" ? "" : " simulator"}`;
324
489
  progress.printTreeContent("Build", [{
325
490
  text: `device ${deviceName}`,
326
491
  color: "gray"
327
492
  }, {
328
- text: `artifact ${displayPath(APP_PATH, originalDir)}`,
493
+ text: `app ${displayPath(APP_PATH, originalDir)}`,
329
494
  color: "gray"
330
495
  }, {
331
496
  text: `loads ${url}`,
332
497
  color: "gray"
333
498
  }]);
334
- progress.summary(`Installed on ${deviceName}`, "Run catalyst start to serve the app");
499
+ progress.summary(`Installed on ${deviceName}`, usesDevServer ? "Edit your code; the app reloads from the dev server" : "Open the app");
335
500
  return {
336
501
  success: true,
337
502
  targetInfo,
@@ -352,6 +517,152 @@ function createIosBuild(config) {
352
517
  process.chdir(originalDir);
353
518
  }
354
519
  }
520
+
521
+ /**
522
+ * The store build: `xcodebuild archive` in Release for a generic iOS
523
+ * device, then `-exportArchive` to an IPA, with the IPA and zipped dSYMs
524
+ * written to build-output/ios/<version>-<build>/.
525
+ */
526
+ async function buildReleaseForIOS(pluginComposition = {}) {
527
+ const originalDir = process.cwd();
528
+ // nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal - fixed prefix under the OS temp dir.
529
+ const workDir = fs.mkdtempSync(path.join(os.tmpdir(), "catalyst-ios-release-"));
530
+ let p12Signing = null;
531
+ try {
532
+ await generatePackageSwift(pluginComposition.iosDependencies);
533
+ await updateXcodeProjectPackageDependencies();
534
+ progress.start("assets");
535
+ await processNotificationAssets(WEBVIEW_CONFIG);
536
+ await copyOfflinePage();
537
+ progress.complete("assets");
538
+ progress.start("signing");
539
+ const signing = {
540
+ ...signingPlan
541
+ };
542
+ if (signingPlan.p12) {
543
+ p12Signing = setupP12Signing({
544
+ p12: credentials.p12,
545
+ bundleId: PRODUCT_BUNDLE_ID,
546
+ env,
547
+ log
548
+ });
549
+ signing.provisioningProfile = p12Signing.profile.name;
550
+ signing.identity = p12Signing.identity.sha1;
551
+ signing.teamId = p12Signing.profile.teamId || signing.teamId;
552
+ } else if (signing.style === "manual" && exportMethod !== "debugging") {
553
+ signing.identity = "iPhone Distribution";
554
+ }
555
+ const from = credentials?.source === "env" ? "CATALYST_IOS_CREDENTIALS" : "credentials/ios";
556
+ if (signingPlan.p12) {
557
+ progress.log(`Local signing with the .p12 and profile from ${from}`, "info");
558
+ } else if (signing.style === "manual") {
559
+ progress.log(`Manual signing with profile "${signing.provisioningProfile}"`, "info");
560
+ } else {
561
+ progress.log(ctx.apiKey ? `Cloud signing for team ${signing.teamId} with App Store Connect key ${ctx.apiKey.keyId} from ${from}` : `Automatic signing for team ${signing.teamId} with the Apple account signed in to Xcode`, "info");
562
+ }
563
+ progress.complete("signing");
564
+ await generateXCConfig({
565
+ signing,
566
+ version
567
+ });
568
+ await copySplashscreenAssets();
569
+ await copyAppIcon({
570
+ release: true
571
+ });
572
+ generateLocalhostCertificate();
573
+ process.chdir(PROJECT_DIR);
574
+ const outDir = releaseOutputDir(appRoot, version);
575
+ fs.mkdirSync(outDir, {
576
+ recursive: true
577
+ });
578
+ const name = artifactBaseName(iosConfig.appName);
579
+ const archivePath = path.join(workDir, `${name}.xcarchive`);
580
+ const signingArgs = signing.style === "automatic" ? xcodeAuthArgs(ctx.apiKey) : [];
581
+ progress.start("archive");
582
+ try {
583
+ await runCommandStreaming("xcodebuild", ["archive", "-project", `${PROJECT_NAME}.xcodeproj`, "-scheme", SCHEME_NAME, "-configuration", "Release", "-destination", "generic/platform=iOS", "-archivePath", archivePath, ...signingArgs]);
584
+ } catch (error) {
585
+ const failure = isSigningFailure(error.output) ? signingSetupError("Xcode could not sign the release archive", error) : error;
586
+ failure.stage = "Archiving the release build";
587
+ throw failure;
588
+ }
589
+ progress.complete("archive");
590
+ progress.start("export");
591
+ const exportPath = path.join(workDir, "export");
592
+ const optionsPlist = writeExportOptionsPlist(buildExportOptions({
593
+ method: exportMethod,
594
+ teamId: signing.teamId,
595
+ signingStyle: signing.style,
596
+ bundleId: PRODUCT_BUNDLE_ID,
597
+ profileName: signing.provisioningProfile,
598
+ signingCertificate: signing.p12 ? signing.identity : undefined
599
+ }), path.join(workDir, "ExportOptions.plist"));
600
+ try {
601
+ await runCommandStreaming("xcodebuild", ["-exportArchive", "-archivePath", archivePath, "-exportPath", exportPath, "-exportOptionsPlist", optionsPlist, ...(signing.style === "automatic" ? xcodeAuthArgs(ctx.apiKey) : [])]);
602
+ } catch (error) {
603
+ const failure = isSigningFailure(error.output) ? signingSetupError("Xcode could not sign the IPA for export", error) : error;
604
+ failure.stage = "Exporting the IPA";
605
+ throw failure;
606
+ }
607
+ const exported = fs.readdirSync(exportPath).find(file => file.endsWith(".ipa"));
608
+ if (!exported) throw new Error(`xcodebuild -exportArchive wrote no .ipa to ${exportPath}`);
609
+ const ipaPath = path.join(outDir, `${name}.ipa`);
610
+ clearArtifact(ipaPath);
611
+ fs.copyFileSync(path.join(exportPath, exported), ipaPath);
612
+ const dsymsDir = path.join(archivePath, "dSYMs");
613
+ const dsymsZip = path.join(outDir, `${name}-dSYMs.zip`);
614
+ clearArtifact(dsymsZip);
615
+ if (fs.existsSync(dsymsDir) && fs.readdirSync(dsymsDir).length > 0) {
616
+ execFileSync("ditto", ["-c", "-k", "--keepParent", dsymsDir, dsymsZip]);
617
+ }
618
+ progress.complete("export");
619
+ const rows = [{
620
+ text: `artifact ${displayPath(ipaPath, originalDir)}`,
621
+ color: "gray"
622
+ }];
623
+ if (fs.existsSync(dsymsZip)) {
624
+ rows.push({
625
+ text: `dSYMs ${displayPath(dsymsZip, originalDir)}`,
626
+ color: "gray"
627
+ });
628
+ }
629
+ rows.push({
630
+ text: `version ${version.versionName || DEFAULT_VERSION_NAME} (${version.buildNumber})`,
631
+ color: "gray"
632
+ });
633
+ rows.push({
634
+ text: `signing ${signing.style}, ${exportMethod}`,
635
+ color: "gray"
636
+ });
637
+ rows.push({
638
+ text: `loads ${url}`,
639
+ color: "gray"
640
+ });
641
+ progress.printTreeContent("Build", rows);
642
+ progress.summary("Built the signed IPA", "Upload it to App Store Connect with Transporter");
643
+ return {
644
+ success: true,
645
+ ipaPath,
646
+ dsymsZip: fs.existsSync(dsymsZip) ? dsymsZip : null
647
+ };
648
+ } catch (error) {
649
+ if (progress.currentStep) progress.fail(progress.currentStep.id);
650
+ process.stderr.write(buildFailure({
651
+ error,
652
+ stage: error.stage || "iOS release build",
653
+ code: "IOS-000",
654
+ cwd: originalDir
655
+ }));
656
+ throw error;
657
+ } finally {
658
+ p12Signing?.cleanup();
659
+ fs.rmSync(workDir, {
660
+ recursive: true,
661
+ force: true
662
+ });
663
+ process.chdir(originalDir);
664
+ }
665
+ }
355
666
  async function buildIosForTesting(pluginComposition = {}) {
356
667
  const originalDir = process.cwd();
357
668
  try {
@@ -366,6 +677,7 @@ function createIosBuild(config) {
366
677
  await generateXCConfig();
367
678
  await copySplashscreenAssets();
368
679
  await copyAppIcon();
680
+ generateLocalhostCertificate();
369
681
  progress.log("Changing directory to: " + PROJECT_DIR, "info");
370
682
  process.chdir(PROJECT_DIR);
371
683
  await cleanBuildArtifacts();
@@ -409,14 +721,22 @@ function createIosBuild(config) {
409
721
  copySplashscreenAssets,
410
722
  copyAppIcon,
411
723
  buildForIOS,
724
+ buildReleaseForIOS,
412
725
  buildIosForTesting,
413
726
  progress,
727
+ release,
728
+ version,
414
729
  WEBVIEW_CONFIG,
730
+ /** Debug: the app loads the dev server, which buildAppIos.js makes sure answers. */
731
+ usesDevServer,
415
732
  PROJECT_DIR,
416
- SCHEME_NAME
733
+ SCHEME_NAME,
734
+ /** Removes the credentials unpacked from CATALYST_IOS_CREDENTIALS. */
735
+ cleanup: () => credentials?.cleanup()
417
736
  };
418
737
  }
419
738
  module.exports = {
420
739
  createIosBuild,
740
+ IOS_BUILD_STEPS,
421
741
  pwd
422
742
  };
@@ -0,0 +1,87 @@
1
+ "use strict";
2
+
3
+ // The shell's in-app HTTPS server (FrameworkServerUtils.swift) serves on
4
+ // localhost with the identity in localhost.p12, imported with an empty
5
+ // passphrase. WKWebView accepts it because WebViewNavigationDelegate trusts
6
+ // any server certificate for localhost/127.0.0.1 (no pinning), so a fresh
7
+ // self-signed pair per build works and no private key has to live in git.
8
+ //
9
+ // The PKCS#12 uses SHA1-3DES and a SHA-1 MAC: the format SecPKCS12Import has
10
+ // always read, and one OpenSSL 3 writes without its legacy provider and
11
+ // LibreSSL (/usr/bin/openssl on macOS) writes by default.
12
+ //
13
+ // Also runnable for CI or a manual Xcode build:
14
+ // node src/native/buildIos/localhostCert.js [destination.p12]
15
+ const {
16
+ execFileSync
17
+ } = require("child_process");
18
+ const fs = require("fs");
19
+ const os = require("os");
20
+ const path = require("path");
21
+ const DEFAULT_DESTINATION = path.join(__dirname, "..", "iosnativeWebView", "iosnativeWebView", "localhost.p12");
22
+ const VALIDITY_DAYS = 3650;
23
+ function defaultRun(command, args) {
24
+ return execFileSync(command, args, {
25
+ encoding: "utf8",
26
+ stdio: ["ignore", "pipe", "pipe"]
27
+ });
28
+ }
29
+
30
+ /** openssl from PATH, else the one macOS ships. */
31
+ function opensslCandidates() {
32
+ return ["openssl", "/usr/bin/openssl"];
33
+ }
34
+
35
+ /**
36
+ * Write a new self-signed localhost identity to `destination`.
37
+ *
38
+ * @param {string} [destination]
39
+ * @param {object} [options]
40
+ * @param {(command: string, args: string[]) => string} [options.run]
41
+ * @returns {string} the destination
42
+ */
43
+ function generateLocalhostP12(destination = DEFAULT_DESTINATION, {
44
+ run = defaultRun
45
+ } = {}) {
46
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), "catalyst-localhost-cert-"));
47
+ fs.chmodSync(dir, 0o700);
48
+ const keyFile = path.join(dir, "key.pem");
49
+ const certFile = path.join(dir, "cert.pem");
50
+ const p12File = path.join(dir, "localhost.p12");
51
+ try {
52
+ let lastError = null;
53
+ for (const openssl of opensslCandidates()) {
54
+ try {
55
+ run(openssl, ["req", "-x509", "-newkey", "rsa:2048", "-nodes", "-keyout", keyFile, "-out", certFile, "-days", String(VALIDITY_DAYS), "-subj", "/O=Catalyst/CN=localhost", "-addext", "subjectAltName=DNS:localhost,IP:127.0.0.1", "-addext", "extendedKeyUsage=serverAuth"]);
56
+ run(openssl, ["pkcs12", "-export", "-inkey", keyFile, "-in", certFile, "-name", "localhost", "-out", p12File, "-passout", "pass:", "-certpbe", "PBE-SHA1-3DES", "-keypbe", "PBE-SHA1-3DES", "-macalg", "sha1"]);
57
+ lastError = null;
58
+ break;
59
+ } catch (error) {
60
+ lastError = error;
61
+ }
62
+ }
63
+ if (lastError) {
64
+ const detail = String(lastError.stderr || lastError.message || "").trim().split("\n").pop();
65
+ throw new Error(`Could not create the localhost certificate with openssl: ${detail}`);
66
+ }
67
+ fs.mkdirSync(path.dirname(destination), {
68
+ recursive: true
69
+ });
70
+ fs.copyFileSync(p12File, destination);
71
+ return destination;
72
+ } finally {
73
+ fs.rmSync(dir, {
74
+ recursive: true,
75
+ force: true
76
+ });
77
+ }
78
+ }
79
+ module.exports = {
80
+ generateLocalhostP12,
81
+ DEFAULT_DESTINATION
82
+ };
83
+ if (require.main === module) {
84
+ const destination = path.resolve(process.argv[2] || DEFAULT_DESTINATION);
85
+ generateLocalhostP12(destination);
86
+ console.log(`Wrote ${destination}`);
87
+ }
@@ -0,0 +1,28 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ Object.defineProperty(exports, "artifactBaseName", {
7
+ enumerable: true,
8
+ get: function () {
9
+ return _releaseOutput.artifactBaseName;
10
+ }
11
+ });
12
+ exports.releaseOutputDir = releaseOutputDir;
13
+ var _path = _interopRequireDefault(require("path"));
14
+ var _releaseOutput = require("../releaseOutput.js");
15
+ function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
16
+ /**
17
+ * Where a release writes its IPA and dSYMs: <app>/build-output/ios/<version>-<build>,
18
+ * with the version from package.json and the build number from
19
+ * nativeConfig.js resolveAppVersion (the same pair as CFBundleShortVersionString
20
+ * and CFBundleVersion). Outside build/, which `catalyst build` deletes.
21
+ *
22
+ * @param {string} appRoot
23
+ * @param {{ versionName?: string, buildNumber: number }} version
24
+ */
25
+ function releaseOutputDir(appRoot, version) {
26
+ // nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal - appRoot is the local app; the folder name is sanitised.
27
+ return _path.default.resolve(appRoot, "build-output", "ios", (0, _releaseOutput.releaseFolderName)(version));
28
+ }