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.
- package/bin/catalyst.js +105 -1
- package/dist/cli/README.md +119 -0
- package/dist/cli/appConfig.js +50 -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/errors/registry.js +8 -0
- package/dist/errors-index.json +7 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.jsx +1 -1
- 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 +61 -94
- 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 +335 -54
- package/dist/otel.js +15 -9
- package/dist/scripts/build.js +314 -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 +115 -11
- package/dist/scripts/scriptUtils.js +34 -0
- package/dist/scripts/serve.js +65 -9
- package/dist/scripts/start.js +58 -21
- package/dist/server/expressServer.js +167 -32
- package/dist/server/renderer/document/Body.d.ts +0 -11
- package/dist/server/renderer/document/Body.jsx +0 -10
- package/dist/server/renderer/document/Head.d.ts +0 -13
- package/dist/server/renderer/document/Head.jsx +0 -12
- 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 +19 -1
- package/dist/vite/vite.config.js +96 -19
- package/dist/vite/vite.config.server.js +21 -0
- package/dist/web-router/components/MetaTag.jsx +20 -14
- package/dist/web-router/components/MetaTag.test.jsx +108 -32
- package/dist/web-router/components/RouterDataProvider.d.ts +25 -1
- package/dist/web-router/components/RouterDataProvider.jsx +35 -1
- package/dist/web-router/components/RouterDataProvider.test.jsx +44 -0
- package/dist/web-router/context.d.ts +15 -1
- package/dist/web-router/context.jsx +11 -0
- package/dist/webmcp/WebMcpProvider.jsx +234 -0
- package/dist/webmcp/declarative.js +207 -0
- package/dist/webmcp/declarative.test.js +122 -0
- package/dist/webmcp/errors.js +59 -0
- package/dist/webmcp/errors.test.js +47 -0
- package/dist/webmcp/index.js +40 -0
- package/dist/webmcp/native.js +37 -0
- package/dist/webmcp/native.test.js +41 -0
- package/dist/webmcp/pageInfo.js +119 -0
- package/dist/webmcp/pageInfo.test.js +85 -0
- package/dist/webmcp/registry.dom.test.js +175 -0
- package/dist/webmcp/registry.js +540 -0
- package/dist/webmcp/registry.test.js +228 -0
- package/dist/webmcp/routeContext.js +11 -0
- package/dist/webmcp/shim-entry.js +8 -0
- package/dist/webmcp/shim.js +149 -0
- package/dist/webmcp/useTool.js +124 -0
- package/package.json +4 -7
- 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/preflight.test.ts +100 -2
- 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/manifestCache.test.ts +13 -0
- package/test/server/otel.test.ts +556 -0
- package/test/server/validator.test.ts +19 -8
- package/vitest.config.ts +4 -0
- package/dist/scripts/checkVersion.js +0 -3
- 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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
150
|
-
|
|
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
|
-
|
|
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
|
|
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.
|
|
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...", "
|
|
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...", "
|
|
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
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
}
|
|
245
|
+
progress.status("installing");
|
|
246
|
+
await runCommand(`xcrun simctl install booted "${APP_PATH}"`);
|
|
284
247
|
}
|
|
285
248
|
async function waitForInstallation() {
|
|
286
|
-
|
|
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}"
|
|
290
|
-
|
|
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
|
-
|
|
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
|
-
|
|
267
|
+
progress.log("App container not found", "warning");
|
|
305
268
|
}
|
|
306
269
|
throw error;
|
|
307
270
|
}
|
|
308
271
|
async function verifyAppLaunch() {
|
|
309
|
-
|
|
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}"
|
|
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)
|
|
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
|
-
|
|
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
|
-
|
|
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}"
|
|
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
|
-
|
|
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.
|
|
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(
|
|
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(
|
|
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.
|
|
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.
|
|
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(
|
|
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(
|
|
427
|
+
progress.log(`Skipping placeholder: ${name.trim()}`, "info");
|
|
460
428
|
}
|
|
461
429
|
}
|
|
462
430
|
}
|
|
463
431
|
} catch (error) {
|
|
464
|
-
progress.log("xcodebuild destinations failed", "
|
|
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(`
|
|
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
|
-
|
|
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
|
-
|
|
498
|
-
|
|
499
|
-
|
|
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...", "
|
|
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}`, "
|
|
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.
|
|
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
|
-
|
|
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})`, "
|
|
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.
|
|
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
|
-
|
|
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
|
}
|