catalyst-core 1.0.1 → 1.0.2-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 (114) hide show
  1. package/bin/catalyst.js +105 -1
  2. package/dist/cli/README.md +119 -0
  3. package/dist/cli/appConfig.js +50 -0
  4. package/dist/cli/buildErrors.js +170 -0
  5. package/dist/cli/buildWarnings.js +28 -0
  6. package/dist/cli/devOutput.js +524 -0
  7. package/dist/cli/diagnostic.js +388 -0
  8. package/dist/cli/hints.js +69 -0
  9. package/dist/cli/package.json +3 -0
  10. package/dist/cli/spinner.js +141 -0
  11. package/dist/cli/theme.js +155 -0
  12. package/dist/errors/index.js +7 -3
  13. package/dist/errors/registry.js +8 -0
  14. package/dist/errors-index.json +7 -0
  15. package/dist/index.d.ts +1 -1
  16. package/dist/index.jsx +1 -1
  17. package/dist/logger.js +61 -6
  18. package/dist/native/androidProject/app/build.gradle.kts +2 -1
  19. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/CustomWebview.kt +2 -0
  20. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/MainActivity.kt +9 -0
  21. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/SplashActivity.kt +65 -34
  22. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/SplashOverlay.kt +109 -0
  23. package/dist/native/androidSetup.js +46 -57
  24. package/dist/native/buildAndroid/assets.js +22 -13
  25. package/dist/native/buildAndroid/build.js +98 -26
  26. package/dist/native/buildAndroid/config.js +43 -12
  27. package/dist/native/buildAndroid/index.js +49 -191
  28. package/dist/native/buildAppAndroid.js +19 -8
  29. package/dist/native/buildAppIos.js +35 -16
  30. package/dist/native/buildIos/assets.js +1 -6
  31. package/dist/native/buildIos/build.js +61 -94
  32. package/dist/native/buildIos/config.js +26 -8
  33. package/dist/native/buildIos/index.js +143 -64
  34. package/dist/native/buildIos/plugins.js +12 -15
  35. package/dist/native/internalPluginUtils.js +0 -1
  36. package/dist/native/iosnativeWebView/CoreLogic/Sources/CatalystCoreLogic/Constants/ConfigConstants.swift +2 -0
  37. package/dist/native/iosnativeWebView/Sources/Core/WebView/SplashHTMLView.swift +31 -0
  38. package/dist/native/iosnativeWebView/Sources/Core/WebView/SplashView.swift +17 -1
  39. package/dist/native/iosnativeWebView/Sources/Core/WebView/SplashViewModel.swift +41 -1
  40. package/dist/native/iosnativeWebView/Sources/Core/WebView/WebViewModel.swift +2 -0
  41. package/dist/native/iosnativeWebView/Sources/Core/WebView/WebViewNavigationDelegate.swift +3 -0
  42. package/dist/native/pluginComposerAndroid.js +2 -3
  43. package/dist/native/renameAndroidProject.js +42 -20
  44. package/dist/native/setupEmulatorIos.js +28 -49
  45. package/dist/native/setupServer.js +12 -15
  46. package/dist/native/terminalProgress.js +186 -147
  47. package/dist/native/utils.js +335 -54
  48. package/dist/otel.js +15 -9
  49. package/dist/scripts/build.js +314 -42
  50. package/dist/scripts/generateOfflineManifest.js +11 -1
  51. package/dist/scripts/loadEnvironmentVariables.js +24 -2
  52. package/dist/scripts/preServerInit.js +32 -10
  53. package/dist/scripts/preflight.js +115 -11
  54. package/dist/scripts/scriptUtils.js +34 -0
  55. package/dist/scripts/serve.js +65 -9
  56. package/dist/scripts/start.js +58 -21
  57. package/dist/server/expressServer.js +167 -32
  58. package/dist/server/renderer/document/Body.d.ts +0 -11
  59. package/dist/server/renderer/document/Body.jsx +0 -10
  60. package/dist/server/renderer/document/Head.d.ts +0 -13
  61. package/dist/server/renderer/document/Head.jsx +0 -12
  62. package/dist/server/renderer/handler.jsx +87 -11
  63. package/dist/server/utils/validator.js +14 -11
  64. package/dist/vite/manifest-categorization-plugin.js +7 -5
  65. package/dist/vite/resolveDevServerConfig.js +9 -2
  66. package/dist/vite/vite.config.client.js +19 -1
  67. package/dist/vite/vite.config.js +96 -19
  68. package/dist/vite/vite.config.server.js +21 -0
  69. package/dist/web-router/components/MetaTag.jsx +20 -14
  70. package/dist/web-router/components/MetaTag.test.jsx +108 -32
  71. package/dist/web-router/components/RouterDataProvider.d.ts +25 -1
  72. package/dist/web-router/components/RouterDataProvider.jsx +35 -1
  73. package/dist/web-router/components/RouterDataProvider.test.jsx +44 -0
  74. package/dist/web-router/context.d.ts +15 -1
  75. package/dist/web-router/context.jsx +11 -0
  76. package/dist/webmcp/WebMcpProvider.jsx +234 -0
  77. package/dist/webmcp/declarative.js +207 -0
  78. package/dist/webmcp/declarative.test.js +122 -0
  79. package/dist/webmcp/errors.js +59 -0
  80. package/dist/webmcp/errors.test.js +47 -0
  81. package/dist/webmcp/index.js +40 -0
  82. package/dist/webmcp/native.js +37 -0
  83. package/dist/webmcp/native.test.js +41 -0
  84. package/dist/webmcp/pageInfo.js +119 -0
  85. package/dist/webmcp/pageInfo.test.js +85 -0
  86. package/dist/webmcp/registry.dom.test.js +175 -0
  87. package/dist/webmcp/registry.js +540 -0
  88. package/dist/webmcp/registry.test.js +228 -0
  89. package/dist/webmcp/routeContext.js +11 -0
  90. package/dist/webmcp/shim-entry.js +8 -0
  91. package/dist/webmcp/shim.js +149 -0
  92. package/dist/webmcp/useTool.js +124 -0
  93. package/package.json +4 -7
  94. package/test/cli/buildErrors.test.ts +173 -0
  95. package/test/cli/buildWarnings.test.cjs +62 -0
  96. package/test/cli/devOutput.test.ts +549 -0
  97. package/test/cli/diagnostic.test.ts +434 -0
  98. package/test/cli/output.test.cjs +90 -0
  99. package/test/cli/spinner.test.ts +177 -0
  100. package/test/cli/theme.test.ts +163 -0
  101. package/test/native/redactSecrets.test.ts +72 -0
  102. package/test/native/utils.test.ts +532 -0
  103. package/test/scripts/build.test.ts +275 -0
  104. package/test/scripts/preflight.test.ts +100 -2
  105. package/test/scripts/serve.test.ts +102 -0
  106. package/test/server/fixtures/template/src/js/routes/index.jsx +8 -0
  107. package/test/server/handler.test.ts +17 -0
  108. package/test/server/logger.test.ts +185 -0
  109. package/test/server/manifestCache.test.ts +13 -0
  110. package/test/server/otel.test.ts +556 -0
  111. package/test/server/validator.test.ts +19 -8
  112. package/vitest.config.ts +4 -0
  113. package/dist/scripts/checkVersion.js +0 -3
  114. package/dist/scripts/cleanCache.js +0 -30
@@ -16,9 +16,9 @@ module.exports = function createBuildPhase(ctx) {
16
16
  SCHEME_NAME,
17
17
  APP_BUNDLE_ID,
18
18
  IPHONE_MODEL,
19
- url,
20
19
  progress,
21
- runCommand
20
+ runCommand,
21
+ runCommandStreaming
22
22
  } = ctx;
23
23
 
24
24
  // ─── Simulator helpers ────────────────────────────────────────────────────
@@ -32,24 +32,21 @@ module.exports = function createBuildPhase(ctx) {
32
32
  if (line.includes(modelName) && line.includes("Booted")) {
33
33
  const m = line.match(UUID_RE);
34
34
  if (m) {
35
- console.log(`Found booted simulator of model ${modelName}`);
36
35
  return m[0];
37
36
  }
38
37
  }
39
38
  }
40
- console.log(`No booted simulator of model ${modelName} found, checking for any booted simulator...`);
39
+ progress.status("looking for any booted simulator");
41
40
  for (const line of lines) {
42
41
  if (line.includes("Booted")) {
43
42
  const m = line.match(UUID_RE);
44
43
  if (m) {
45
- console.log("Found another booted simulator, will use it instead");
46
44
  return m[0];
47
45
  }
48
46
  }
49
47
  }
50
48
  return null;
51
49
  } catch (error) {
52
- console.log("No booted simulators found");
53
50
  return null;
54
51
  }
55
52
  }
@@ -67,7 +64,6 @@ module.exports = function createBuildPhase(ctx) {
67
64
  }
68
65
  return null;
69
66
  } catch (error) {
70
- console.log("Failed to get booted simulator info:", error.message);
71
67
  return null;
72
68
  }
73
69
  }
@@ -143,32 +139,30 @@ module.exports = function createBuildPhase(ctx) {
143
139
  async function buildProject(scheme, sdk, destination, bundleId, derivedDataPath, projectName) {
144
140
  const bootedInfo = await getBootedSimulatorInfo();
145
141
  if (!bootedInfo) throw new Error("No booted simulator found");
146
- console.log(`Building with destination: platform=iOS Simulator,id=${bootedInfo.udid}`);
142
+ progress.status(`simulator ${bootedInfo.udid.slice(0, 8)}`);
147
143
  const isNotificationsEnabled = WEBVIEW_CONFIG.notifications?.enabled ?? false;
148
144
  progress.log(isNotificationsEnabled ? "Building with notifications enabled (CatalystNotifications module included)" : "Building without notifications (Firebase excluded)", "info");
149
- return runCommand(makeBuildCommand(scheme, sdk, bundleId, derivedDataPath, projectName, bootedInfo.udid, "build"), {
150
- maxBuffer: 1024 * 1024 * 10
151
- });
145
+ // Streamed: xcodebuild runs for minutes, and buffered output made that
146
+ // look like a hang with nothing on screen until it finished.
147
+ return runCommandStreaming(makeBuildCommand(scheme, sdk, bundleId, derivedDataPath, projectName, bootedInfo.udid, "build"));
152
148
  }
153
149
  async function buildProjectForTesting(scheme, sdk, destination, bundleId, derivedDataPath, projectName) {
154
150
  const bootedInfo = await getBootedSimulatorInfo();
155
151
  if (!bootedInfo) throw new Error("No booted simulator found");
156
- console.log(`Building for testing with destination: platform=iOS Simulator,id=${bootedInfo.udid}`);
152
+ progress.status(`simulator ${bootedInfo.udid.slice(0, 8)}`);
157
153
  const isNotificationsEnabled = WEBVIEW_CONFIG.notifications?.enabled ?? false;
158
154
  progress.log(isNotificationsEnabled ? "Building for testing with notifications enabled" : "Building for testing without notifications", "info");
159
- return runCommand(makeBuildCommand(scheme, sdk, bundleId, derivedDataPath, projectName, bootedInfo.udid, "build-for-testing"), {
160
- maxBuffer: 1024 * 1024 * 10
161
- });
155
+ return runCommandStreaming(makeBuildCommand(scheme, sdk, bundleId, derivedDataPath, projectName, bootedInfo.udid, "build-for-testing"));
162
156
  }
163
157
  async function buildXcodeProject() {
164
158
  progress.start("build");
165
159
  try {
166
160
  const derivedDataPath = path.join(os.homedir(), "Library/Developer/Xcode/DerivedData");
167
161
  try {
168
- progress.log("Building for specified simulator...", "info");
162
+ progress.status("building for specified simulator");
169
163
  await buildProject(SCHEME_NAME, "iphonesimulator", `platform=iOS Simulator,name=${IPHONE_MODEL}`, APP_BUNDLE_ID, derivedDataPath, PROJECT_NAME);
170
164
  } catch (buildError) {
171
- progress.log("Initial build failed, attempting fallback...", "warning");
165
+ progress.log("Initial build failed, attempting fallback...", "info");
172
166
  const bootedUUID = await getBootedSimulatorUUID(IPHONE_MODEL);
173
167
  if (bootedUUID) {
174
168
  progress.log(`Found booted simulator, retrying build...`, "info");
@@ -180,32 +174,6 @@ module.exports = function createBuildPhase(ctx) {
180
174
  progress.complete("build");
181
175
  } catch (error) {
182
176
  progress.fail("build", error.message);
183
- progress.printTreeContent("Troubleshooting Guide", ["Build failed. Please try the following steps:", {
184
- text: 'Run "npm run setupEmulator:ios" to reconfigure iOS settings',
185
- indent: 1,
186
- prefix: "├─ ",
187
- color: "yellow"
188
- }, {
189
- text: "Check if Xcode is properly installed and updated",
190
- indent: 1,
191
- prefix: "├─ ",
192
- color: "yellow"
193
- }, {
194
- text: "Verify selected simulator exists",
195
- indent: 1,
196
- prefix: "└─ ",
197
- color: "yellow"
198
- }, "\nVerify Configuration:", {
199
- text: `Selected Simulator: ${IPHONE_MODEL}`,
200
- indent: 1,
201
- prefix: "├─ ",
202
- color: "gray"
203
- }, {
204
- text: `Server URL: ${url}`,
205
- indent: 1,
206
- prefix: "└─ ",
207
- color: "gray"
208
- }]);
209
177
  throw error;
210
178
  }
211
179
  }
@@ -221,7 +189,7 @@ module.exports = function createBuildPhase(ctx) {
221
189
  APP_PATH = execSync(`find "${DERIVED_DATA_DIR}" -path "*${PROJECT_NAME}-*" -prune -not -path "*/Index.noindex*" -path "*/Build/Products/Debug-iphonesimulator/${PROJECT_NAME}.app" -type d | head -n 1` // nosemgrep: javascript.lang.security.detect-child-process.detect-child-process - DERIVED_DATA_DIR and PROJECT_NAME are internal iOS build paths.
222
190
  ).toString().trim();
223
191
  } catch (error) {
224
- progress.log("Primary app path search failed, trying fallback...", "warning");
192
+ progress.log("Primary app path search failed, trying fallback...", "info");
225
193
  }
226
194
  if (!APP_PATH) {
227
195
  try {
@@ -248,13 +216,13 @@ module.exports = function createBuildPhase(ctx) {
248
216
  const buildType = iosConfig.buildType || "debug";
249
217
  const appName = iosConfig.appName || "app";
250
218
  const currentDate = new Date().toLocaleDateString("en-GB").replace(/\//g, "-");
251
- const currentTime = new Date().toLocaleTimeString("en-US", {
252
- hour12: true,
253
- hour: "2-digit",
254
- minute: "2-digit",
255
- second: "2-digit"
256
- }).replace(/:/g, "-");
257
- const destinationDir = path.join(process.cwd(), BUILD_OUTPUT_PATH,
219
+ // 24-hour and locale-free: "02-05-09 PM" put a space in every
220
+ // artifact name, which breaks unquoted shell use of the path.
221
+ const currentTime = new Date().toTimeString().slice(0, 8).replace(/:/g, "-");
222
+ // The app root, not process.cwd(): the iOS build chdirs into the
223
+ // Xcode project inside node_modules, so the artifact landed there
224
+ // and was wiped by the next npm install.
225
+ const destinationDir = path.join(ctx.appRoot || process.cwd(), BUILD_OUTPUT_PATH,
258
226
  // nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal - BUILD_OUTPUT_PATH is trusted application config.
259
227
  "native", "ios", currentDate, buildType // nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal - buildType is "debug" or "release" from config.
260
228
  );
@@ -270,24 +238,20 @@ module.exports = function createBuildPhase(ctx) {
270
238
  }
271
239
  }
272
240
  async function uninstallExistingApp() {
273
- console.log("Uninstalling the app if it exists...");
241
+ progress.status("uninstalling previous build");
274
242
  await runCommand(`xcrun simctl uninstall booted "${APP_BUNDLE_ID}"`);
275
243
  }
276
244
  async function installApp(APP_PATH) {
277
- console.log("Installing the app...");
278
- try {
279
- await runCommand(`xcrun simctl install booted "${APP_PATH}"`);
280
- } catch (error) {
281
- console.error("Error installing the app:", error);
282
- throw error;
283
- }
245
+ progress.status("installing");
246
+ await runCommand(`xcrun simctl install booted "${APP_PATH}"`);
284
247
  }
285
248
  async function waitForInstallation() {
286
- console.log("Waiting for the app to be fully installed...");
249
+ progress.status("waiting for install");
287
250
  for (let i = 0; i < 30; i++) {
288
251
  try {
289
- execSync(`xcrun simctl get_app_container booted "${APP_BUNDLE_ID}"`); // nosemgrep: javascript.lang.security.detect-child-process.detect-child-process - APP_BUNDLE_ID is generated from trusted native config.
290
- console.log("App installed successfully.");
252
+ execSync(`xcrun simctl get_app_container booted "${APP_BUNDLE_ID}"`, {
253
+ stdio: ["ignore", "pipe", "ignore"]
254
+ }); // nosemgrep: javascript.lang.security.detect-child-process.detect-child-process - APP_BUNDLE_ID is generated from trusted native config.
291
255
  break;
292
256
  } catch (error) {
293
257
  if (i === 29) throw new Error("Timeout: App installation took too long.");
@@ -296,32 +260,32 @@ module.exports = function createBuildPhase(ctx) {
296
260
  }
297
261
  }
298
262
  async function handleLaunchError(error) {
299
- console.error("Error launching the app:", error);
300
- console.log("Checking app container...");
263
+ progress.status("checking app container");
301
264
  try {
302
265
  await runCommand(`xcrun simctl get_app_container booted "${APP_BUNDLE_ID}"`);
303
266
  } catch {
304
- console.log("App container not found");
267
+ progress.log("App container not found", "warning");
305
268
  }
306
269
  throw error;
307
270
  }
308
271
  async function verifyAppLaunch() {
309
- console.log("Waiting for the app to launch...");
272
+ progress.status("waiting for launch");
310
273
  for (let i = 0; i < 10; i++) {
311
274
  try {
312
- const launchResult = execSync(`xcrun simctl launch booted "${APP_BUNDLE_ID}"`).toString(); // nosemgrep: javascript.lang.security.detect-child-process.detect-child-process
275
+ const launchResult = execSync(`xcrun simctl launch booted "${APP_BUNDLE_ID}"`, {
276
+ stdio: ["ignore", "pipe", "ignore"]
277
+ }).toString(); // nosemgrep: javascript.lang.security.detect-child-process.detect-child-process
313
278
  if (launchResult.includes("already launched")) {
314
- console.log("App launched successfully.");
315
279
  break;
316
280
  }
317
281
  } catch (error) {
318
- if (i === 9) console.log("Warning: App launch might have failed or taken too long.");
282
+ if (i === 9) progress.log("App launch may have failed or timed out", "warning");
319
283
  }
320
284
  await new Promise(resolve => setTimeout(resolve, 1000));
321
285
  }
322
286
  }
323
287
  async function launchAndVerifyApp() {
324
- console.log("Launching the app...");
288
+ progress.status("launching");
325
289
  try {
326
290
  await runCommand(`xcrun simctl launch booted "${APP_BUNDLE_ID}"`);
327
291
  await verifyAppLaunch();
@@ -330,7 +294,7 @@ module.exports = function createBuildPhase(ctx) {
330
294
  }
331
295
  }
332
296
  async function focusSimulator() {
333
- console.log("Focusing on Simulator...");
297
+ progress.status("focusing simulator");
334
298
  const simulatorAppBundleId = resolveSimulatorAppBundleId();
335
299
  if (!simulatorAppBundleId) {
336
300
  progress.log(SIMULATOR_APP_NOT_FOUND_WARNING, "warning");
@@ -384,7 +348,10 @@ module.exports = function createBuildPhase(ctx) {
384
348
  function tryDevicectl() {
385
349
  const tmpPath = path.join(os.tmpdir(), `catalyst-devicectl-${process.pid}-${Date.now()}.json`);
386
350
  try {
387
- execSync(`xcrun devicectl list devices --json-output "${tmpPath}"`); // nosemgrep: javascript.lang.security.audit.dangerous-spawn-shell-command.dangerous-spawn-shell-command - tmpPath is built from os.tmpdir()/process.pid/Date.now(), not user input.
351
+ execSync(`xcrun devicectl list devices --json-output "${tmpPath}"`, {
352
+ stdio: ["ignore", "ignore", "ignore"],
353
+ timeout: 30000
354
+ }); // nosemgrep: javascript.lang.security.audit.dangerous-spawn-shell-command.dangerous-spawn-shell-command - tmpPath is built from os.tmpdir()/process.pid/Date.now(), not user input.
388
355
  const raw = fs.readFileSync(tmpPath, "utf8");
389
356
  const parsed = JSON.parse(raw);
390
357
  const devices = parsed?.result?.devices ?? [];
@@ -402,7 +369,8 @@ module.exports = function createBuildPhase(ctx) {
402
369
  type: "physical"
403
370
  })).filter(d => typeof d.udid === "string" && VALID_UDID_RE.test(d.udid));
404
371
  } catch (error) {
405
- progress.log(`devicectl detection failed (${error.message}); falling back to xcodebuild destinations`, "warning");
372
+ // A fallback is not a failure the user can act on; keep it for --verbose.
373
+ progress.log(`devicectl detection failed (${error.message}); falling back to xcodebuild destinations`, "info");
406
374
  return [];
407
375
  } finally {
408
376
  try {
@@ -418,21 +386,21 @@ module.exports = function createBuildPhase(ctx) {
418
386
  iosConfig
419
387
  } = ctx;
420
388
  try {
421
- progress.log("Scanning for connected physical devices...", "info");
389
+ progress.status("scanning for connected physical devices");
422
390
  let physicalDevices = [];
423
391
  const configuredUDID = iosConfig.deviceUDID;
424
392
  if (configuredUDID) {
425
393
  progress.log(`Using configured device UDID: ${configuredUDID}`, "info");
426
394
  const configuredDevice = tryDevicectl().find(d => d.udid === configuredUDID);
427
395
  if (configuredDevice) {
428
- progress.log(`✅ Found configured physical device: ${configuredDevice.name} (${configuredDevice.version})`, "success");
396
+ progress.log(`Found configured physical device: ${configuredDevice.name} (${configuredDevice.version})`, "success");
429
397
  progress.complete("deviceDetection");
430
398
  return configuredDevice;
431
399
  }
432
- progress.log(`⚠️ Configured device UDID not found in connected devices`, "warning");
400
+ progress.log(`Configured device UDID not found in connected devices`, "warning");
433
401
  progress.log("Falling back to auto-detection...", "info");
434
402
  } else {
435
- progress.log("No device UDID configured, using auto-detection", "info");
403
+ progress.status("no device udid configured, using auto-detection");
436
404
  }
437
405
  physicalDevices = tryDevicectl();
438
406
  if (physicalDevices.length > 0) progress.log(`Found ${physicalDevices.length} physical device(s) via devicectl`, "success");
@@ -440,14 +408,14 @@ module.exports = function createBuildPhase(ctx) {
440
408
  try {
441
409
  const xcodebuildOutput = execSync(`xcodebuild -scheme "${SCHEME_NAME}" -showdestinations` // nosemgrep: javascript.lang.security.detect-child-process.detect-child-process - SCHEME_NAME is generated from trusted iOS project config.
442
410
  ).toString();
443
- progress.log("Scanning xcodebuild destinations for physical devices...", "info");
411
+ progress.status("scanning xcodebuild destinations for physical devices");
444
412
  for (const line of xcodebuildOutput.split("\n")) {
445
413
  const m = line.match(/\{\s*platform:iOS,\s*arch:(\w+),\s*id:([A-F0-9-]+),\s*name:(.+?)\s*\}/);
446
414
  if (m) {
447
415
  const [, arch, udid, name] = m;
448
416
  progress.log(`Found device candidate: ${name.trim()} - ${udid}`, "info");
449
417
  if (!udid.includes("placeholder") && udid.length > 20) {
450
- progress.log(`✅ Valid physical device: ${name.trim()}`, "success");
418
+ progress.log(`Valid physical device: ${name.trim()}`, "success");
451
419
  physicalDevices.push({
452
420
  name: name.trim(),
453
421
  version: "Unknown",
@@ -456,22 +424,23 @@ module.exports = function createBuildPhase(ctx) {
456
424
  type: "physical"
457
425
  });
458
426
  } else {
459
- progress.log(`❌ Skipping placeholder: ${name.trim()}`, "warning");
427
+ progress.log(`Skipping placeholder: ${name.trim()}`, "info");
460
428
  }
461
429
  }
462
430
  }
463
431
  } catch (error) {
464
- progress.log("xcodebuild destinations failed", "warning");
432
+ progress.log("xcodebuild destinations failed", "info");
465
433
  progress.log(`Error: ${error.message}`, "error");
466
434
  }
467
435
  }
468
436
  if (physicalDevices.length > 0) {
469
437
  progress.log(`Found ${physicalDevices.length} physical device(s):`, "success");
470
- physicalDevices.forEach(d => progress.log(` 📱 ${d.name} (${d.version || "Unknown iOS"}) - ${d.udid}`, "info"));
438
+ physicalDevices.forEach(d => progress.log(` ${d.name} (${d.version || "Unknown iOS"}) - ${d.udid}`, "info"));
471
439
  progress.complete("deviceDetection");
472
440
  return physicalDevices[0];
473
441
  } else {
474
- progress.log("No physical devices detected", "warning");
442
+ // The normal case when building for the simulator, not a warning.
443
+ progress.log("No physical devices detected", "info");
475
444
  progress.complete("deviceDetection");
476
445
  return null;
477
446
  }
@@ -494,10 +463,9 @@ module.exports = function createBuildPhase(ctx) {
494
463
  PRODUCT_BUNDLE_IDENTIFIER=${bundleId} \
495
464
  ONLY_ACTIVE_ARCH=YES \
496
465
  build`;
497
- progress.log(`Executing command: ${buildCommand}`, "info");
498
- return runCommand(buildCommand, {
499
- maxBuffer: 1024 * 1024 * 10
500
- });
466
+ // The full command is a dozen wrapped lines of build settings; the
467
+ // streamed output below is what actually tells you how it is going.
468
+ return runCommandStreaming(buildCommand);
501
469
  }
502
470
  async function findPhysicalDeviceAppPath() {
503
471
  const DERIVED_DATA_DIR = path.join(os.homedir(), "Library/Developer/Xcode/DerivedData");
@@ -506,7 +474,7 @@ module.exports = function createBuildPhase(ctx) {
506
474
  APP_PATH = execSync(`find "${DERIVED_DATA_DIR}" -name "${PROJECT_NAME}.app" -path "*/Build/Products/Debug-iphoneos/*" -not -path "*/Index.noindex/*" -type d | head -n 1` // nosemgrep: javascript.lang.security.detect-child-process.detect-child-process - DERIVED_DATA_DIR and PROJECT_NAME are internal iOS build paths.
507
475
  ).toString().trim();
508
476
  } catch {
509
- progress.log("Primary app path search failed for physical device, trying fallback...", "warning");
477
+ progress.log("Primary app path search failed for physical device, trying fallback...", "info");
510
478
  }
511
479
  if (!APP_PATH) {
512
480
  try {
@@ -537,7 +505,7 @@ module.exports = function createBuildPhase(ctx) {
537
505
  progress.log("App launched successfully using devicectl", "success");
538
506
  } catch (launchError) {
539
507
  progress.log("App installation completed, but launch failed. You can manually launch the app on your device.", "warning");
540
- progress.log(`To launch manually, look for the app with bundle ID: ${APP_BUNDLE_ID}`, "info");
508
+ progress.log(`To launch manually, look for the app with bundle ID: ${APP_BUNDLE_ID}`, "note");
541
509
  }
542
510
  progress.complete("launch");
543
511
  } catch (error) {
@@ -588,7 +556,7 @@ module.exports = function createBuildPhase(ctx) {
588
556
  async function launchIOSSimulator(simulatorName) {
589
557
  progress.start("launchSimulator");
590
558
  try {
591
- progress.log("Launching iOS Simulator...");
559
+ progress.status("launching simulator");
592
560
  const simulatorsJson = JSON.parse(execSync("xcrun simctl list devices -j").toString());
593
561
  // Multiple devices can share a name across runtimes (e.g. leftover devices
594
562
  // from an old Xcode install after an upgrade) — simctl marks the ones whose
@@ -614,11 +582,11 @@ module.exports = function createBuildPhase(ctx) {
614
582
  });
615
583
  });
616
584
  if (!foundSimulator) {
617
- console.log(`Configured simulator "${simulatorName}" not found.`);
585
+ progress.log(`Configured simulator "${simulatorName}" not found`, "warning");
618
586
  return;
619
587
  }
620
588
  if (!isBooted) {
621
- progress.log(`Attempting to boot simulator: ${simulatorName} (${foundSimulatorId})`, "warning");
589
+ progress.log(`Attempting to boot simulator: ${simulatorName} (${foundSimulatorId})`, "info");
622
590
  if (foundSimulator.state === "Shutdown") {
623
591
  progress.log(`Simulator is shutdown, attempting to boot...`, "info");
624
592
  try {
@@ -642,14 +610,13 @@ module.exports = function createBuildPhase(ctx) {
642
610
  }
643
611
  const simulatorAppBundleId = resolveSimulatorAppBundleId();
644
612
  if (simulatorAppBundleId) {
645
- progress.log("Opening simulator UI...");
613
+ progress.status("opening simulator");
646
614
  await runCommand(`open -b ${simulatorAppBundleId}`);
647
615
  await new Promise(resolve => setTimeout(resolve, 1000));
648
616
  await runCommand(`osascript -e 'tell application id "${simulatorAppBundleId}" to activate'`);
649
617
  } else {
650
618
  progress.log(SIMULATOR_APP_NOT_FOUND_WARNING, "warning");
651
619
  }
652
- progress.log("iOS Simulator launched successfully.", "success");
653
620
  progress.complete("launchSimulator");
654
621
  } catch (error) {
655
622
  progress.fail("launchSimulator", error.message);
@@ -82,6 +82,7 @@ module.exports = function createConfigPhase(ctx) {
82
82
  WEBVIEW_CONFIG,
83
83
  iosConfig,
84
84
  PROJECT_DIR,
85
+ PUBLIC_PATH,
85
86
  url,
86
87
  progress,
87
88
  restoreManagedFileFromBaseline,
@@ -185,18 +186,35 @@ public enum ConfigConstants {
185
186
  configContent += `\n public static let splashScreenImageWidth: CGFloat = ${splashConfig.imageWidth || 120}`;
186
187
  configContent += `\n public static let splashScreenImageHeight: CGFloat = ${splashConfig.imageHeight || 120}`;
187
188
  configContent += `\n public static let splashScreenCornerRadius: CGFloat = ${splashConfig.cornerRadius || 20}`;
189
+
190
+ // Optional HTML splash: public/splashscreen.html is embedded as base64 so no
191
+ // bundle resource or Xcode project registration is needed.
192
+ const splashHtmlPath = path.join(PUBLIC_PATH, "splashscreen.html"); // nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal - PUBLIC_PATH is the app's fixed public directory.
193
+ if (fs.existsSync(splashHtmlPath)) {
194
+ const splashHtml = fs.readFileSync(splashHtmlPath);
195
+ if (splashHtml.length > 1024 * 1024) {
196
+ progress.log(`splashscreen.html is ${(splashHtml.length / (1024 * 1024)).toFixed(1)} MB; it is embedded in the app binary, keep it small`, "warning");
197
+ }
198
+ configContent += `\n public static let splashScreenHtml: String? = "${splashHtml.toString("base64")}"`;
199
+ } else {
200
+ configContent += `\n public static let splashScreenHtml: String? = nil`;
201
+ }
202
+ const minDuration = splashConfig.minDuration;
203
+ const validMinDuration = typeof minDuration === "number" && Number.isFinite(minDuration) && minDuration >= 0;
204
+ if (minDuration !== undefined && !validMinDuration) {
205
+ progress.log(`Ignoring invalid splashScreen.minDuration: ${JSON.stringify(minDuration)}`, "warning");
206
+ }
207
+ configContent += `\n public static let splashScreenMinDuration: TimeInterval = ${validMinDuration ? minDuration / 1000.0 : 0}`;
188
208
  } else {
189
- configContent += `\n\n // Splash Screen Configuration\n public static let splashScreenEnabled = false\n public static let splashScreenDuration: TimeInterval? = nil\n public static let splashScreenBackgroundColor = "#ffffff"\n public static let splashScreenImageWidth: CGFloat = 120\n public static let splashScreenImageHeight: CGFloat = 120\n public static let splashScreenCornerRadius: CGFloat = 20`;
209
+ configContent += `\n\n // Splash Screen Configuration\n public static let splashScreenEnabled = false\n public static let splashScreenDuration: TimeInterval? = nil\n public static let splashScreenBackgroundColor = "#ffffff"\n public static let splashScreenImageWidth: CGFloat = 120\n public static let splashScreenImageHeight: CGFloat = 120\n public static let splashScreenCornerRadius: CGFloat = 20\n public static let splashScreenHtml: String? = nil\n public static let splashScreenMinDuration: TimeInterval = 0`;
190
210
  }
191
211
  if (!addedKeys.has("notifications")) {
192
- progress.log("Notifications not found in config, adding default (false)", "info");
193
212
  configContent += "\n public enum Notifications {\n public static let enabled = false\n }";
194
213
  addedKeys.add("notifications");
195
214
  } else {
196
215
  progress.log("Notifications config was processed from WEBVIEW_CONFIG", "info");
197
216
  }
198
217
  if (!addedKeys.has("googleSignIn")) {
199
- progress.log("Google Sign-In not found in config, adding defaults (disabled)", "info");
200
218
  configContent += '\n public enum GoogleSignIn {\n public static let enabled = false\n public static let clientId = ""\n public static let iosClientId = ""\n }';
201
219
  addedKeys.add("googleSignIn");
202
220
  }
@@ -208,8 +226,6 @@ public enum ConfigConstants {
208
226
  progress.log("EdgeToEdge not found in config, adding default (false)", "info");
209
227
  configContent += "\n public enum EdgeToEdge {\n public static let enabled = false\n }";
210
228
  addedKeys.add("edgeToEdge");
211
- } else {
212
- progress.log("EdgeToEdge config was processed from WEBVIEW_CONFIG", "info");
213
229
  }
214
230
  if (!addedKeys.has("initial_url")) {
215
231
  configContent += '\n public static let initial_url = ""';
@@ -233,7 +249,6 @@ public enum ConfigConstants {
233
249
  }
234
250
  configContent += `\n}`;
235
251
  fs.writeFileSync(appConfigPath, configContent, "utf8");
236
- progress.log("Configuration constants generated successfully (SPM Package)", "success");
237
252
  progress.complete("config");
238
253
  } catch (error) {
239
254
  progress.fail("config", error.message);
@@ -314,9 +329,12 @@ INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = UIInterfaceOrientationPort
314
329
  INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight
315
330
  `;
316
331
  fs.writeFileSync(xconfigPath, xconfigContent, "utf8");
317
- console.log(`✅ Generated Shared.xcconfig with Swift ${swiftVersion}, iOS ${deploymentTarget}, Bundle ID: ${bundleId}`);
332
+ // Detail the step's own check line already covers, so it rides the
333
+ // spinner row rather than escaping the gutter at column 0.
334
+ progress.status(`xcconfig · Swift ${swiftVersion} · iOS ${deploymentTarget}`);
318
335
  } catch (error) {
319
- console.error(`❌ Failed to generate Shared.xcconfig: ${error.message}`);
336
+ // Keep the original error (its code and stack); only say which file.
337
+ error.message = `Failed to generate Shared.xcconfig: ${error.message}`;
320
338
  throw error;
321
339
  }
322
340
  }