catalyst-core 1.0.2-canary.1 → 1.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/catalyst.js +105 -1
- package/changelog.md +34 -0
- package/dist/cli/README.md +119 -0
- package/dist/cli/appConfig.js +51 -0
- package/dist/cli/buildErrors.js +170 -0
- package/dist/cli/buildWarnings.js +28 -0
- package/dist/cli/devOutput.js +524 -0
- package/dist/cli/diagnostic.js +388 -0
- package/dist/cli/hints.js +69 -0
- package/dist/cli/package.json +3 -0
- package/dist/cli/spinner.js +141 -0
- package/dist/cli/theme.js +155 -0
- package/dist/errors/index.js +7 -3
- package/dist/logger.js +61 -6
- package/dist/native/androidProject/app/build.gradle.kts +2 -1
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/CustomWebview.kt +2 -0
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/MainActivity.kt +9 -0
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/SplashActivity.kt +65 -34
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/SplashOverlay.kt +109 -0
- package/dist/native/androidSetup.js +46 -57
- package/dist/native/buildAndroid/assets.js +22 -13
- package/dist/native/buildAndroid/build.js +98 -26
- package/dist/native/buildAndroid/config.js +43 -12
- package/dist/native/buildAndroid/index.js +49 -191
- package/dist/native/buildAppAndroid.js +19 -8
- package/dist/native/buildAppIos.js +35 -16
- package/dist/native/buildIos/assets.js +1 -6
- package/dist/native/buildIos/build.js +66 -95
- package/dist/native/buildIos/config.js +26 -8
- package/dist/native/buildIos/index.js +143 -64
- package/dist/native/buildIos/plugins.js +12 -15
- package/dist/native/internalPluginUtils.js +0 -1
- package/dist/native/iosnativeWebView/CoreLogic/Sources/CatalystCoreLogic/Constants/ConfigConstants.swift +2 -0
- package/dist/native/iosnativeWebView/Sources/Core/WebView/SplashHTMLView.swift +31 -0
- package/dist/native/iosnativeWebView/Sources/Core/WebView/SplashView.swift +17 -1
- package/dist/native/iosnativeWebView/Sources/Core/WebView/SplashViewModel.swift +41 -1
- package/dist/native/iosnativeWebView/Sources/Core/WebView/WebViewModel.swift +2 -0
- package/dist/native/iosnativeWebView/Sources/Core/WebView/WebViewNavigationDelegate.swift +3 -0
- package/dist/native/pluginComposerAndroid.js +2 -3
- package/dist/native/renameAndroidProject.js +42 -20
- package/dist/native/setupEmulatorIos.js +28 -49
- package/dist/native/setupServer.js +12 -15
- package/dist/native/terminalProgress.js +186 -147
- package/dist/native/utils.js +340 -54
- package/dist/otel.js +15 -9
- package/dist/scripts/build.js +313 -42
- package/dist/scripts/generateOfflineManifest.js +11 -1
- package/dist/scripts/loadEnvironmentVariables.js +24 -2
- package/dist/scripts/preServerInit.js +32 -10
- package/dist/scripts/preflight.js +35 -11
- package/dist/scripts/scriptUtils.js +34 -0
- package/dist/scripts/serve.js +64 -15
- package/dist/scripts/start.js +58 -21
- package/dist/server/expressServer.js +167 -32
- package/dist/server/renderer/handler.jsx +87 -11
- package/dist/server/utils/validator.js +14 -11
- package/dist/vite/manifest-categorization-plugin.js +7 -5
- package/dist/vite/resolveDevServerConfig.js +9 -2
- package/dist/vite/vite.config.client.js +18 -0
- package/dist/vite/vite.config.js +73 -13
- package/dist/vite/vite.config.server.js +21 -0
- package/dist/web-router/components/MetaTag.jsx +15 -2
- package/dist/web-router/components/MetaTag.test.jsx +71 -2
- package/dist/webmcp/WebMcpProvider.jsx +3 -2
- package/dist/webmcp/useTool.js +4 -1
- package/mcp_v2/conversion-tasks.json +6 -5
- package/mcp_v2/knowledge-base.json +124 -7
- package/mcp_v2/test/config.test.ts +59 -0
- package/mcp_v2/tools/build.js +6 -6
- package/mcp_v2/tools/config.js +35 -18
- package/mcp_v2/tools/conversion.js +7 -13
- package/package.json +5 -5
- package/test/cli/buildErrors.test.ts +173 -0
- package/test/cli/buildWarnings.test.cjs +62 -0
- package/test/cli/devOutput.test.ts +549 -0
- package/test/cli/diagnostic.test.ts +434 -0
- package/test/cli/output.test.cjs +90 -0
- package/test/cli/spinner.test.ts +177 -0
- package/test/cli/theme.test.ts +163 -0
- package/test/native/redactSecrets.test.ts +72 -0
- package/test/native/utils.test.ts +532 -0
- package/test/scripts/build.test.ts +275 -0
- package/test/scripts/serve.test.ts +102 -0
- package/test/server/fixtures/template/src/js/routes/index.jsx +8 -0
- package/test/server/handler.test.ts +17 -0
- package/test/server/logger.test.ts +185 -0
- package/test/server/otel.test.ts +556 -0
- package/test/server/validator.test.ts +19 -8
- package/dist/scripts/checkVersion.js +0 -3
- package/dist/scripts/cleanCache.js +0 -30
|
@@ -4,7 +4,8 @@ const fs = require("fs");
|
|
|
4
4
|
const os = require("os");
|
|
5
5
|
const path = require("path");
|
|
6
6
|
const {
|
|
7
|
-
execSync
|
|
7
|
+
execSync,
|
|
8
|
+
execFileSync
|
|
8
9
|
} = require("child_process");
|
|
9
10
|
const {
|
|
10
11
|
runInteractiveCommand
|
|
@@ -16,9 +17,9 @@ module.exports = function createBuildPhase(ctx) {
|
|
|
16
17
|
SCHEME_NAME,
|
|
17
18
|
APP_BUNDLE_ID,
|
|
18
19
|
IPHONE_MODEL,
|
|
19
|
-
url,
|
|
20
20
|
progress,
|
|
21
|
-
runCommand
|
|
21
|
+
runCommand,
|
|
22
|
+
runCommandStreaming
|
|
22
23
|
} = ctx;
|
|
23
24
|
|
|
24
25
|
// ─── Simulator helpers ────────────────────────────────────────────────────
|
|
@@ -32,24 +33,21 @@ module.exports = function createBuildPhase(ctx) {
|
|
|
32
33
|
if (line.includes(modelName) && line.includes("Booted")) {
|
|
33
34
|
const m = line.match(UUID_RE);
|
|
34
35
|
if (m) {
|
|
35
|
-
console.log(`Found booted simulator of model ${modelName}`);
|
|
36
36
|
return m[0];
|
|
37
37
|
}
|
|
38
38
|
}
|
|
39
39
|
}
|
|
40
|
-
|
|
40
|
+
progress.status("looking for any booted simulator");
|
|
41
41
|
for (const line of lines) {
|
|
42
42
|
if (line.includes("Booted")) {
|
|
43
43
|
const m = line.match(UUID_RE);
|
|
44
44
|
if (m) {
|
|
45
|
-
console.log("Found another booted simulator, will use it instead");
|
|
46
45
|
return m[0];
|
|
47
46
|
}
|
|
48
47
|
}
|
|
49
48
|
}
|
|
50
49
|
return null;
|
|
51
50
|
} catch (error) {
|
|
52
|
-
console.log("No booted simulators found");
|
|
53
51
|
return null;
|
|
54
52
|
}
|
|
55
53
|
}
|
|
@@ -67,7 +65,6 @@ module.exports = function createBuildPhase(ctx) {
|
|
|
67
65
|
}
|
|
68
66
|
return null;
|
|
69
67
|
} catch (error) {
|
|
70
|
-
console.log("Failed to get booted simulator info:", error.message);
|
|
71
68
|
return null;
|
|
72
69
|
}
|
|
73
70
|
}
|
|
@@ -143,32 +140,30 @@ module.exports = function createBuildPhase(ctx) {
|
|
|
143
140
|
async function buildProject(scheme, sdk, destination, bundleId, derivedDataPath, projectName) {
|
|
144
141
|
const bootedInfo = await getBootedSimulatorInfo();
|
|
145
142
|
if (!bootedInfo) throw new Error("No booted simulator found");
|
|
146
|
-
|
|
143
|
+
progress.status(`simulator ${bootedInfo.udid.slice(0, 8)}`);
|
|
147
144
|
const isNotificationsEnabled = WEBVIEW_CONFIG.notifications?.enabled ?? false;
|
|
148
145
|
progress.log(isNotificationsEnabled ? "Building with notifications enabled (CatalystNotifications module included)" : "Building without notifications (Firebase excluded)", "info");
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
146
|
+
// Streamed: xcodebuild runs for minutes, and buffered output made that
|
|
147
|
+
// look like a hang with nothing on screen until it finished.
|
|
148
|
+
return runCommandStreaming(makeBuildCommand(scheme, sdk, bundleId, derivedDataPath, projectName, bootedInfo.udid, "build"));
|
|
152
149
|
}
|
|
153
150
|
async function buildProjectForTesting(scheme, sdk, destination, bundleId, derivedDataPath, projectName) {
|
|
154
151
|
const bootedInfo = await getBootedSimulatorInfo();
|
|
155
152
|
if (!bootedInfo) throw new Error("No booted simulator found");
|
|
156
|
-
|
|
153
|
+
progress.status(`simulator ${bootedInfo.udid.slice(0, 8)}`);
|
|
157
154
|
const isNotificationsEnabled = WEBVIEW_CONFIG.notifications?.enabled ?? false;
|
|
158
155
|
progress.log(isNotificationsEnabled ? "Building for testing with notifications enabled" : "Building for testing without notifications", "info");
|
|
159
|
-
return
|
|
160
|
-
maxBuffer: 1024 * 1024 * 10
|
|
161
|
-
});
|
|
156
|
+
return runCommandStreaming(makeBuildCommand(scheme, sdk, bundleId, derivedDataPath, projectName, bootedInfo.udid, "build-for-testing"));
|
|
162
157
|
}
|
|
163
158
|
async function buildXcodeProject() {
|
|
164
159
|
progress.start("build");
|
|
165
160
|
try {
|
|
166
161
|
const derivedDataPath = path.join(os.homedir(), "Library/Developer/Xcode/DerivedData");
|
|
167
162
|
try {
|
|
168
|
-
progress.
|
|
163
|
+
progress.status("building for specified simulator");
|
|
169
164
|
await buildProject(SCHEME_NAME, "iphonesimulator", `platform=iOS Simulator,name=${IPHONE_MODEL}`, APP_BUNDLE_ID, derivedDataPath, PROJECT_NAME);
|
|
170
165
|
} catch (buildError) {
|
|
171
|
-
progress.log("Initial build failed, attempting fallback...", "
|
|
166
|
+
progress.log("Initial build failed, attempting fallback...", "info");
|
|
172
167
|
const bootedUUID = await getBootedSimulatorUUID(IPHONE_MODEL);
|
|
173
168
|
if (bootedUUID) {
|
|
174
169
|
progress.log(`Found booted simulator, retrying build...`, "info");
|
|
@@ -180,32 +175,6 @@ module.exports = function createBuildPhase(ctx) {
|
|
|
180
175
|
progress.complete("build");
|
|
181
176
|
} catch (error) {
|
|
182
177
|
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
178
|
throw error;
|
|
210
179
|
}
|
|
211
180
|
}
|
|
@@ -221,7 +190,7 @@ module.exports = function createBuildPhase(ctx) {
|
|
|
221
190
|
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
191
|
).toString().trim();
|
|
223
192
|
} catch (error) {
|
|
224
|
-
progress.log("Primary app path search failed, trying fallback...", "
|
|
193
|
+
progress.log("Primary app path search failed, trying fallback...", "info");
|
|
225
194
|
}
|
|
226
195
|
if (!APP_PATH) {
|
|
227
196
|
try {
|
|
@@ -248,13 +217,15 @@ module.exports = function createBuildPhase(ctx) {
|
|
|
248
217
|
const buildType = iosConfig.buildType || "debug";
|
|
249
218
|
const appName = iosConfig.appName || "app";
|
|
250
219
|
const currentDate = new Date().toLocaleDateString("en-GB").replace(/\//g, "-");
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
const destinationDir = path.join(
|
|
220
|
+
// 24-hour and locale-free: "02-05-09 PM" put a space in every
|
|
221
|
+
// artifact name, which breaks unquoted shell use of the path.
|
|
222
|
+
const currentTime = new Date().toTimeString().slice(0, 8).replace(/:/g, "-");
|
|
223
|
+
// The app root, not process.cwd(): the iOS build chdirs into the
|
|
224
|
+
// Xcode project inside node_modules, so the artifact landed there
|
|
225
|
+
// and was wiped by the next npm install.
|
|
226
|
+
const destinationDir = path.join(
|
|
227
|
+
// nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal - appRoot is the local project root from the CLI context.
|
|
228
|
+
ctx.appRoot || process.cwd(), BUILD_OUTPUT_PATH,
|
|
258
229
|
// nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal - BUILD_OUTPUT_PATH is trusted application config.
|
|
259
230
|
"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
231
|
);
|
|
@@ -270,24 +241,21 @@ module.exports = function createBuildPhase(ctx) {
|
|
|
270
241
|
}
|
|
271
242
|
}
|
|
272
243
|
async function uninstallExistingApp() {
|
|
273
|
-
|
|
244
|
+
progress.status("uninstalling previous build");
|
|
274
245
|
await runCommand(`xcrun simctl uninstall booted "${APP_BUNDLE_ID}"`);
|
|
275
246
|
}
|
|
276
247
|
async function installApp(APP_PATH) {
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
await runCommand(`xcrun simctl install booted "${APP_PATH}"`);
|
|
280
|
-
} catch (error) {
|
|
281
|
-
console.error("Error installing the app:", error);
|
|
282
|
-
throw error;
|
|
283
|
-
}
|
|
248
|
+
progress.status("installing");
|
|
249
|
+
await runCommand(`xcrun simctl install booted "${APP_PATH}"`);
|
|
284
250
|
}
|
|
285
251
|
async function waitForInstallation() {
|
|
286
|
-
|
|
252
|
+
progress.status("waiting for install");
|
|
287
253
|
for (let i = 0; i < 30; i++) {
|
|
288
254
|
try {
|
|
289
|
-
|
|
290
|
-
|
|
255
|
+
// No shell: the bundle id is passed as an argv entry, never interpolated.
|
|
256
|
+
execFileSync("xcrun", ["simctl", "get_app_container", "booted", APP_BUNDLE_ID], {
|
|
257
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
258
|
+
});
|
|
291
259
|
break;
|
|
292
260
|
} catch (error) {
|
|
293
261
|
if (i === 29) throw new Error("Timeout: App installation took too long.");
|
|
@@ -296,32 +264,32 @@ module.exports = function createBuildPhase(ctx) {
|
|
|
296
264
|
}
|
|
297
265
|
}
|
|
298
266
|
async function handleLaunchError(error) {
|
|
299
|
-
|
|
300
|
-
console.log("Checking app container...");
|
|
267
|
+
progress.status("checking app container");
|
|
301
268
|
try {
|
|
302
269
|
await runCommand(`xcrun simctl get_app_container booted "${APP_BUNDLE_ID}"`);
|
|
303
270
|
} catch {
|
|
304
|
-
|
|
271
|
+
progress.log("App container not found", "warning");
|
|
305
272
|
}
|
|
306
273
|
throw error;
|
|
307
274
|
}
|
|
308
275
|
async function verifyAppLaunch() {
|
|
309
|
-
|
|
276
|
+
progress.status("waiting for launch");
|
|
310
277
|
for (let i = 0; i < 10; i++) {
|
|
311
278
|
try {
|
|
312
|
-
const launchResult =
|
|
279
|
+
const launchResult = execFileSync("xcrun", ["simctl", "launch", "booted", APP_BUNDLE_ID], {
|
|
280
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
281
|
+
}).toString();
|
|
313
282
|
if (launchResult.includes("already launched")) {
|
|
314
|
-
console.log("App launched successfully.");
|
|
315
283
|
break;
|
|
316
284
|
}
|
|
317
285
|
} catch (error) {
|
|
318
|
-
if (i === 9)
|
|
286
|
+
if (i === 9) progress.log("App launch may have failed or timed out", "warning");
|
|
319
287
|
}
|
|
320
288
|
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
321
289
|
}
|
|
322
290
|
}
|
|
323
291
|
async function launchAndVerifyApp() {
|
|
324
|
-
|
|
292
|
+
progress.status("launching");
|
|
325
293
|
try {
|
|
326
294
|
await runCommand(`xcrun simctl launch booted "${APP_BUNDLE_ID}"`);
|
|
327
295
|
await verifyAppLaunch();
|
|
@@ -330,7 +298,7 @@ module.exports = function createBuildPhase(ctx) {
|
|
|
330
298
|
}
|
|
331
299
|
}
|
|
332
300
|
async function focusSimulator() {
|
|
333
|
-
|
|
301
|
+
progress.status("focusing simulator");
|
|
334
302
|
const simulatorAppBundleId = resolveSimulatorAppBundleId();
|
|
335
303
|
if (!simulatorAppBundleId) {
|
|
336
304
|
progress.log(SIMULATOR_APP_NOT_FOUND_WARNING, "warning");
|
|
@@ -384,7 +352,10 @@ module.exports = function createBuildPhase(ctx) {
|
|
|
384
352
|
function tryDevicectl() {
|
|
385
353
|
const tmpPath = path.join(os.tmpdir(), `catalyst-devicectl-${process.pid}-${Date.now()}.json`);
|
|
386
354
|
try {
|
|
387
|
-
execSync(`xcrun devicectl list devices --json-output "${tmpPath}"
|
|
355
|
+
execSync(`xcrun devicectl list devices --json-output "${tmpPath}"`, {
|
|
356
|
+
stdio: ["ignore", "ignore", "ignore"],
|
|
357
|
+
timeout: 30000
|
|
358
|
+
}); // 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
359
|
const raw = fs.readFileSync(tmpPath, "utf8");
|
|
389
360
|
const parsed = JSON.parse(raw);
|
|
390
361
|
const devices = parsed?.result?.devices ?? [];
|
|
@@ -402,7 +373,8 @@ module.exports = function createBuildPhase(ctx) {
|
|
|
402
373
|
type: "physical"
|
|
403
374
|
})).filter(d => typeof d.udid === "string" && VALID_UDID_RE.test(d.udid));
|
|
404
375
|
} catch (error) {
|
|
405
|
-
|
|
376
|
+
// A fallback is not a failure the user can act on; keep it for --verbose.
|
|
377
|
+
progress.log(`devicectl detection failed (${error.message}); falling back to xcodebuild destinations`, "info");
|
|
406
378
|
return [];
|
|
407
379
|
} finally {
|
|
408
380
|
try {
|
|
@@ -418,21 +390,21 @@ module.exports = function createBuildPhase(ctx) {
|
|
|
418
390
|
iosConfig
|
|
419
391
|
} = ctx;
|
|
420
392
|
try {
|
|
421
|
-
progress.
|
|
393
|
+
progress.status("scanning for connected physical devices");
|
|
422
394
|
let physicalDevices = [];
|
|
423
395
|
const configuredUDID = iosConfig.deviceUDID;
|
|
424
396
|
if (configuredUDID) {
|
|
425
397
|
progress.log(`Using configured device UDID: ${configuredUDID}`, "info");
|
|
426
398
|
const configuredDevice = tryDevicectl().find(d => d.udid === configuredUDID);
|
|
427
399
|
if (configuredDevice) {
|
|
428
|
-
progress.log(
|
|
400
|
+
progress.log(`Found configured physical device: ${configuredDevice.name} (${configuredDevice.version})`, "success");
|
|
429
401
|
progress.complete("deviceDetection");
|
|
430
402
|
return configuredDevice;
|
|
431
403
|
}
|
|
432
|
-
progress.log(
|
|
404
|
+
progress.log(`Configured device UDID not found in connected devices`, "warning");
|
|
433
405
|
progress.log("Falling back to auto-detection...", "info");
|
|
434
406
|
} else {
|
|
435
|
-
progress.
|
|
407
|
+
progress.status("no device udid configured, using auto-detection");
|
|
436
408
|
}
|
|
437
409
|
physicalDevices = tryDevicectl();
|
|
438
410
|
if (physicalDevices.length > 0) progress.log(`Found ${physicalDevices.length} physical device(s) via devicectl`, "success");
|
|
@@ -440,14 +412,14 @@ module.exports = function createBuildPhase(ctx) {
|
|
|
440
412
|
try {
|
|
441
413
|
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
414
|
).toString();
|
|
443
|
-
progress.
|
|
415
|
+
progress.status("scanning xcodebuild destinations for physical devices");
|
|
444
416
|
for (const line of xcodebuildOutput.split("\n")) {
|
|
445
417
|
const m = line.match(/\{\s*platform:iOS,\s*arch:(\w+),\s*id:([A-F0-9-]+),\s*name:(.+?)\s*\}/);
|
|
446
418
|
if (m) {
|
|
447
419
|
const [, arch, udid, name] = m;
|
|
448
420
|
progress.log(`Found device candidate: ${name.trim()} - ${udid}`, "info");
|
|
449
421
|
if (!udid.includes("placeholder") && udid.length > 20) {
|
|
450
|
-
progress.log(
|
|
422
|
+
progress.log(`Valid physical device: ${name.trim()}`, "success");
|
|
451
423
|
physicalDevices.push({
|
|
452
424
|
name: name.trim(),
|
|
453
425
|
version: "Unknown",
|
|
@@ -456,22 +428,23 @@ module.exports = function createBuildPhase(ctx) {
|
|
|
456
428
|
type: "physical"
|
|
457
429
|
});
|
|
458
430
|
} else {
|
|
459
|
-
progress.log(
|
|
431
|
+
progress.log(`Skipping placeholder: ${name.trim()}`, "info");
|
|
460
432
|
}
|
|
461
433
|
}
|
|
462
434
|
}
|
|
463
435
|
} catch (error) {
|
|
464
|
-
progress.log("xcodebuild destinations failed", "
|
|
436
|
+
progress.log("xcodebuild destinations failed", "info");
|
|
465
437
|
progress.log(`Error: ${error.message}`, "error");
|
|
466
438
|
}
|
|
467
439
|
}
|
|
468
440
|
if (physicalDevices.length > 0) {
|
|
469
441
|
progress.log(`Found ${physicalDevices.length} physical device(s):`, "success");
|
|
470
|
-
physicalDevices.forEach(d => progress.log(`
|
|
442
|
+
physicalDevices.forEach(d => progress.log(` ${d.name} (${d.version || "Unknown iOS"}) - ${d.udid}`, "info"));
|
|
471
443
|
progress.complete("deviceDetection");
|
|
472
444
|
return physicalDevices[0];
|
|
473
445
|
} else {
|
|
474
|
-
|
|
446
|
+
// The normal case when building for the simulator, not a warning.
|
|
447
|
+
progress.log("No physical devices detected", "info");
|
|
475
448
|
progress.complete("deviceDetection");
|
|
476
449
|
return null;
|
|
477
450
|
}
|
|
@@ -494,10 +467,9 @@ module.exports = function createBuildPhase(ctx) {
|
|
|
494
467
|
PRODUCT_BUNDLE_IDENTIFIER=${bundleId} \
|
|
495
468
|
ONLY_ACTIVE_ARCH=YES \
|
|
496
469
|
build`;
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
});
|
|
470
|
+
// The full command is a dozen wrapped lines of build settings; the
|
|
471
|
+
// streamed output below is what actually tells you how it is going.
|
|
472
|
+
return runCommandStreaming(buildCommand);
|
|
501
473
|
}
|
|
502
474
|
async function findPhysicalDeviceAppPath() {
|
|
503
475
|
const DERIVED_DATA_DIR = path.join(os.homedir(), "Library/Developer/Xcode/DerivedData");
|
|
@@ -506,7 +478,7 @@ module.exports = function createBuildPhase(ctx) {
|
|
|
506
478
|
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
479
|
).toString().trim();
|
|
508
480
|
} catch {
|
|
509
|
-
progress.log("Primary app path search failed for physical device, trying fallback...", "
|
|
481
|
+
progress.log("Primary app path search failed for physical device, trying fallback...", "info");
|
|
510
482
|
}
|
|
511
483
|
if (!APP_PATH) {
|
|
512
484
|
try {
|
|
@@ -537,7 +509,7 @@ module.exports = function createBuildPhase(ctx) {
|
|
|
537
509
|
progress.log("App launched successfully using devicectl", "success");
|
|
538
510
|
} catch (launchError) {
|
|
539
511
|
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}`, "
|
|
512
|
+
progress.log(`To launch manually, look for the app with bundle ID: ${APP_BUNDLE_ID}`, "note");
|
|
541
513
|
}
|
|
542
514
|
progress.complete("launch");
|
|
543
515
|
} catch (error) {
|
|
@@ -588,7 +560,7 @@ module.exports = function createBuildPhase(ctx) {
|
|
|
588
560
|
async function launchIOSSimulator(simulatorName) {
|
|
589
561
|
progress.start("launchSimulator");
|
|
590
562
|
try {
|
|
591
|
-
progress.
|
|
563
|
+
progress.status("launching simulator");
|
|
592
564
|
const simulatorsJson = JSON.parse(execSync("xcrun simctl list devices -j").toString());
|
|
593
565
|
// Multiple devices can share a name across runtimes (e.g. leftover devices
|
|
594
566
|
// from an old Xcode install after an upgrade) — simctl marks the ones whose
|
|
@@ -614,11 +586,11 @@ module.exports = function createBuildPhase(ctx) {
|
|
|
614
586
|
});
|
|
615
587
|
});
|
|
616
588
|
if (!foundSimulator) {
|
|
617
|
-
|
|
589
|
+
progress.log(`Configured simulator "${simulatorName}" not found`, "warning");
|
|
618
590
|
return;
|
|
619
591
|
}
|
|
620
592
|
if (!isBooted) {
|
|
621
|
-
progress.log(`Attempting to boot simulator: ${simulatorName} (${foundSimulatorId})`, "
|
|
593
|
+
progress.log(`Attempting to boot simulator: ${simulatorName} (${foundSimulatorId})`, "info");
|
|
622
594
|
if (foundSimulator.state === "Shutdown") {
|
|
623
595
|
progress.log(`Simulator is shutdown, attempting to boot...`, "info");
|
|
624
596
|
try {
|
|
@@ -642,14 +614,13 @@ module.exports = function createBuildPhase(ctx) {
|
|
|
642
614
|
}
|
|
643
615
|
const simulatorAppBundleId = resolveSimulatorAppBundleId();
|
|
644
616
|
if (simulatorAppBundleId) {
|
|
645
|
-
progress.
|
|
617
|
+
progress.status("opening simulator");
|
|
646
618
|
await runCommand(`open -b ${simulatorAppBundleId}`);
|
|
647
619
|
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
648
620
|
await runCommand(`osascript -e 'tell application id "${simulatorAppBundleId}" to activate'`);
|
|
649
621
|
} else {
|
|
650
622
|
progress.log(SIMULATOR_APP_NOT_FOUND_WARNING, "warning");
|
|
651
623
|
}
|
|
652
|
-
progress.log("iOS Simulator launched successfully.", "success");
|
|
653
624
|
progress.complete("launchSimulator");
|
|
654
625
|
} catch (error) {
|
|
655
626
|
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
|
-
|
|
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
|
-
|
|
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
|
}
|