catalyst-core 1.1.0 → 1.2.0-canary.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. package/bin/catalyst.js +54 -6
  2. package/changelog.md +27 -1
  3. package/dist/errors/registry.js +306 -18
  4. package/dist/errors-index.json +273 -70
  5. package/dist/native/androidProject/app/build.gradle.kts +53 -79
  6. package/dist/native/androidProject/app/src/main/AndroidManifest.xml +1 -1
  7. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/CatalystConstants.kt +3 -1
  8. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/MainActivity.kt +32 -4
  9. package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/utils/KeyboardUtil.kt +5 -1
  10. package/dist/native/androidProject/app/src/test/java/io/yourname/androidproject/ConfigMappingTest.kt +25 -0
  11. package/dist/native/androidSetup.js +76 -170
  12. package/dist/native/androidToolchain.js +642 -0
  13. package/dist/native/appIcon.js +100 -0
  14. package/dist/native/buildAndroid/assets.js +29 -5
  15. package/dist/native/buildAndroid/build.js +110 -223
  16. package/dist/native/buildAndroid/config.js +57 -65
  17. package/dist/native/buildAndroid/devices.js +176 -0
  18. package/dist/native/buildAndroid/index.js +271 -46
  19. package/dist/native/buildAndroid/output.js +43 -0
  20. package/dist/native/buildAndroid/signing.js +456 -0
  21. package/dist/native/buildAppAndroid.js +27 -7
  22. package/dist/native/buildAppIos.js +139 -20
  23. package/dist/native/buildIos/assets.js +76 -4
  24. package/dist/native/buildIos/build.js +31 -181
  25. package/dist/native/buildIos/config.js +126 -11
  26. package/dist/native/buildIos/index.js +354 -34
  27. package/dist/native/buildIos/localhostCert.js +87 -0
  28. package/dist/native/buildIos/output.js +28 -0
  29. package/dist/native/buildIos/signing.js +596 -0
  30. package/dist/native/credentials.js +309 -0
  31. package/dist/native/exportCredentials.js +51 -0
  32. package/dist/native/iosToolchain.js +583 -0
  33. package/dist/native/iosnativeWebView/Configurations/Shared.xcconfig +1 -3
  34. package/dist/native/iosnativeWebView/iosnativeWebView/Info-Release.plist +2 -0
  35. package/dist/native/iosnativeWebView/iosnativeWebView/PrivacyInfo.xcprivacy +40 -0
  36. package/dist/native/iosnativeWebView/iosnativeWebView/iosnativeWebView.entitlements +0 -2
  37. package/dist/native/iosnativeWebView/iosnativeWebView.xcodeproj/project.pbxproj +4 -4
  38. package/dist/native/localState.js +148 -0
  39. package/dist/native/nativeConfig.js +621 -0
  40. package/dist/native/releaseOutput.js +53 -0
  41. package/dist/native/renameAndroidProject.js +154 -369
  42. package/dist/native/setupEmulatorIos.js +58 -275
  43. package/dist/native/setupServer.js +520 -124
  44. package/dist/native/utils.js +418 -115
  45. package/dist/vite/resolveDevServerConfig.js +21 -0
  46. package/mcp_v2/knowledge-base.json +13 -11
  47. package/mcp_v2/test/config.test.ts +40 -0
  48. package/mcp_v2/tools/build.js +80 -104
  49. package/mcp_v2/tools/config.js +181 -174
  50. package/package.json +1 -1
  51. package/test/cli/nativeFlags.test.ts +97 -0
  52. package/test/native/androidDevices.test.ts +230 -0
  53. package/test/native/androidElfAlignment.test.ts +148 -0
  54. package/test/native/androidSigning.test.ts +646 -0
  55. package/test/native/androidToolchain.test.ts +766 -0
  56. package/test/native/appIcon.test.ts +124 -0
  57. package/test/native/credentials.test.ts +241 -0
  58. package/test/native/interaction.test.ts +179 -0
  59. package/test/native/iosConfigPhase.test.ts +611 -0
  60. package/test/native/iosSigning.test.ts +1060 -0
  61. package/test/native/iosToolchain.test.ts +821 -0
  62. package/test/native/localState.test.ts +80 -0
  63. package/test/native/nativeConfig.test.ts +602 -0
  64. package/test/native/setupServer.test.ts +529 -0
  65. package/test/native/utils.test.ts +102 -127
  66. package/test/vite/resolveDevServerConfig.test.js +25 -0
  67. package/dist/native/androidProject/keystore/Untitled +0 -0
  68. package/dist/native/androidProject/keystore/release-key.jks +0 -0
  69. package/dist/native/iosnativeWebView/iosnativeWebView/localhost.p12 +0 -0
@@ -111,22 +111,92 @@ function handle_check_config({ project_path, platform = "both" } = {}) {
111
111
 
112
112
  const wc = config.WEBVIEW_CONFIG
113
113
 
114
- // ── WEBVIEW_CONFIG.LOCAL_IP ───────────────────────────────────────────────
115
- if (!wc.LOCAL_IP) {
116
- issues.push({
117
- field: "WEBVIEW_CONFIG.LOCAL_IP",
118
- severity: "error",
114
+ // ── App identity and production URL (catalyst-core 1.3.0+) ─────────────────
115
+ // appId and appName apply to both platforms and win over the per-platform
116
+ // keys. url is the origin a release build loads; debug builds ignore it.
117
+ const PLACEHOLDER_APP_IDS = [
118
+ "io.yourname.androidproject",
119
+ "com.catalyst.framework.app",
120
+ "com.debug.webview",
121
+ ]
122
+ const legacyAppId = wc.android?.packageName || wc.ios?.appBundleId || wc.ios?.bundleIdentifier
123
+ const appId = typeof wc.appId === "string" && wc.appId.trim() ? wc.appId.trim() : null
124
+ if (appId) {
125
+ if (
126
+ appId.toLowerCase().startsWith("com.example.") ||
127
+ PLACEHOLDER_APP_IDS.includes(appId.toLowerCase())
128
+ ) {
129
+ warnings.push({
130
+ field: "WEBVIEW_CONFIG.appId",
131
+ message: `appId "${appId}" is a placeholder. Debug builds work, but a release build refuses it (NATIVE-003). Set your own reverse-DNS id, e.g. "com.yourcompany.app"; it cannot change after the first store upload.`,
132
+ })
133
+ } else {
134
+ passed.push({ field: "WEBVIEW_CONFIG.appId", value: appId })
135
+ }
136
+ } else if (!legacyAppId) {
137
+ warnings.push({
138
+ field: "WEBVIEW_CONFIG.appId",
119
139
  message:
120
- 'WEBVIEW_CONFIG.LOCAL_IP is required. Set to your machine LAN IP (e.g. "192.168.0.11"). Do NOT use "localhost" — the native emulator/simulator cannot resolve localhost and will fail to connect to the dev server.',
140
+ 'WEBVIEW_CONFIG.appId not set. It is the Android application id and the iOS bundle id (e.g. "com.yourcompany.app"); a release build refuses to ship without one (NATIVE-003).',
121
141
  })
122
- } else if (wc.LOCAL_IP === "localhost" || wc.LOCAL_IP === "127.0.0.1") {
123
- issues.push({
142
+ }
143
+ if (typeof wc.appName === "string" && wc.appName.trim()) {
144
+ passed.push({ field: "WEBVIEW_CONFIG.appName", value: wc.appName })
145
+ }
146
+ if (wc.url) {
147
+ let parsed = null
148
+ try {
149
+ parsed = new URL(String(wc.url))
150
+ } catch {
151
+ parsed = null
152
+ }
153
+ if (
154
+ !parsed ||
155
+ !["http:", "https:"].includes(parsed.protocol) ||
156
+ (parsed.pathname && parsed.pathname !== "/")
157
+ ) {
158
+ issues.push({
159
+ field: "WEBVIEW_CONFIG.url",
160
+ severity: "error",
161
+ message: `WEBVIEW_CONFIG.url "${wc.url}" must be an http(s) origin without a path, e.g. "https://shop.acme.com". A release build fails with NATIVE-004.`,
162
+ })
163
+ } else if (
164
+ /^(localhost|127\.|10\.|192\.168\.|169\.254\.|172\.(1[6-9]|2\d|3[01])\.)|\.local$/i.test(
165
+ parsed.hostname
166
+ ) &&
167
+ wc.allowPrivateHost !== true
168
+ ) {
169
+ warnings.push({
170
+ field: "WEBVIEW_CONFIG.url",
171
+ message: `WEBVIEW_CONFIG.url "${wc.url}" is a local or private host. A release build refuses it (NATIVE-001) unless WEBVIEW_CONFIG.allowPrivateHost is true.`,
172
+ })
173
+ } else {
174
+ passed.push({ field: "WEBVIEW_CONFIG.url", value: wc.url })
175
+ }
176
+ } else {
177
+ warnings.push({
178
+ field: "WEBVIEW_CONFIG.url",
179
+ message:
180
+ 'WEBVIEW_CONFIG.url not set. Release builds load this production origin (e.g. "https://shop.acme.com"); without it they fall back to LOCAL_IP/port/useHttps. Debug builds do not need it.',
181
+ })
182
+ }
183
+
184
+ // ── WEBVIEW_CONFIG.LOCAL_IP ───────────────────────────────────────────────
185
+ // Debug builds reach the dev server on their own (adb reverse on Android,
186
+ // localhost on the simulator, <mac>.local on an iPhone), so LOCAL_IP stays
187
+ // localhost. A LAN address saved by an older setup is put back to
188
+ // localhost by the first 1.3.0 build.
189
+ if (
190
+ wc.LOCAL_IP &&
191
+ !["localhost", "127.0.0.1"].includes(wc.LOCAL_IP) &&
192
+ /^\d{1,3}(\.\d{1,3}){3}$/.test(wc.LOCAL_IP)
193
+ ) {
194
+ warnings.push({
124
195
  field: "WEBVIEW_CONFIG.LOCAL_IP",
125
- severity: "error",
126
- message: `WEBVIEW_CONFIG.LOCAL_IP is set to "${wc.LOCAL_IP}" — this will not work. The native emulator runs in a separate network namespace and cannot resolve localhost. Use your machine LAN IP (run "ifconfig | grep inet" to find it).`,
196
+ message: `WEBVIEW_CONFIG.LOCAL_IP is "${wc.LOCAL_IP}". Keep it "localhost": debug builds work out the dev server address themselves, and the first build on catalyst-core 1.3.0 resets a private address to localhost. Put the production address in WEBVIEW_CONFIG.url.`,
127
197
  })
128
198
  } else {
129
- passed.push({ field: "WEBVIEW_CONFIG.LOCAL_IP", value: wc.LOCAL_IP })
199
+ passed.push({ field: "WEBVIEW_CONFIG.LOCAL_IP", value: wc.LOCAL_IP || "localhost (default)" })
130
200
  }
131
201
 
132
202
  // ── WEBVIEW_CONFIG.appInfo ────────────────────────────────────────────────
@@ -149,14 +219,18 @@ function handle_check_config({ project_path, platform = "both" } = {}) {
149
219
  message:
150
220
  "WEBVIEW_CONFIG.port is required. Must match NODE_SERVER_PORT (e.g. 3000). The native WebView connects to this port.",
151
221
  })
152
- } else if (typeof wc.port !== "number") {
222
+ } else if (
223
+ !(typeof wc.port === "number" || (typeof wc.port === "string" && /^\d+$/.test(wc.port.trim()))) ||
224
+ !(Number(wc.port) >= 1 && Number(wc.port) <= 65535)
225
+ ) {
226
+ // The template ships "3005": both shells read it as text.
153
227
  issues.push({
154
228
  field: "WEBVIEW_CONFIG.port",
155
229
  severity: "error",
156
- message: `WEBVIEW_CONFIG.port must be a number, got ${typeof wc.port}.`,
230
+ message: `WEBVIEW_CONFIG.port must be a port number from 1 to 65535 (a number or a string such as "3005"), got ${JSON.stringify(wc.port)}.`,
157
231
  })
158
232
  } else {
159
- if (config.NODE_SERVER_PORT !== undefined && wc.port !== config.NODE_SERVER_PORT) {
233
+ if (config.NODE_SERVER_PORT !== undefined && Number(wc.port) !== Number(config.NODE_SERVER_PORT)) {
160
234
  warnings.push({
161
235
  field: "WEBVIEW_CONFIG.port",
162
236
  message: `WEBVIEW_CONFIG.port (${wc.port}) does not match NODE_SERVER_PORT (${config.NODE_SERVER_PORT}). WebView may connect to wrong port.`,
@@ -215,178 +289,111 @@ function handle_check_config({ project_path, platform = "both" } = {}) {
215
289
  }
216
290
 
217
291
  // ── Android fields ─────────────────────────────────────────────────────────
218
- if (platform === "android" || platform === "both") {
219
- if (!wc.android) {
220
- issues.push({
221
- field: "WEBVIEW_CONFIG.android",
222
- severity: "error",
223
- message: "WEBVIEW_CONFIG.android block missing. Android build cannot start without it.",
292
+ // Everything here is optional: buildApp:android finds or installs the SDK,
293
+ // JDK and emulator itself, after asking (or with --yes / --accept-licenses).
294
+ if ((platform === "android" || platform === "both") && wc.android) {
295
+ const a = wc.android
296
+ if (a.sdkPath && !fs.existsSync(a.sdkPath.replace(/^~(?=$|\/)/, process.env.HOME || "~"))) {
297
+ warnings.push({
298
+ field: "WEBVIEW_CONFIG.android.sdkPath",
299
+ message: `android.sdkPath "${a.sdkPath}" does not exist on this machine. Remove it to let the build use ANDROID_HOME, ANDROID_SDK_ROOT or the default SDK location (and install the SDK there).`,
300
+ })
301
+ } else if (a.sdkPath) {
302
+ passed.push({ field: "WEBVIEW_CONFIG.android.sdkPath", value: a.sdkPath })
303
+ }
304
+ if (a.emulatorName)
305
+ passed.push({ field: "WEBVIEW_CONFIG.android.emulatorName", value: a.emulatorName })
306
+ if (!wc.appName && a.appName)
307
+ passed.push({ field: "WEBVIEW_CONFIG.android.appName", value: a.appName })
308
+ if (!wc.appId && a.packageName) {
309
+ passed.push({ field: "WEBVIEW_CONFIG.android.packageName", value: a.packageName })
310
+ }
311
+ if (String(a.buildType || "").toLowerCase() === "release") {
312
+ warnings.push({
313
+ field: "WEBVIEW_CONFIG.android.buildType",
314
+ message:
315
+ 'android.buildType "release" is deprecated. Use `npm run android:release` (catalyst buildApp:android --release) and set buildType back to "debug" or remove it.',
316
+ })
317
+ }
318
+ const keystoreBlocks = [a.keystore, a.keystoreConfig].filter(Boolean)
319
+ if (keystoreBlocks.some((block) => block.storePassword || block.keyPassword)) {
320
+ warnings.push({
321
+ field: "WEBVIEW_CONFIG.android.keystoreConfig",
322
+ message:
323
+ "Keystore passwords in config.json are not used for signing. The upload key lives in credentials/android/ (created by the first release build in a terminal; CI uses the CATALYST_ANDROID_CREDENTIALS secret from `catalyst credentials:export android`). Delete the passwords.",
324
+ })
325
+ }
326
+ if (a.cachePattern !== undefined && typeof a.cachePattern !== "string") {
327
+ warnings.push({
328
+ field: "WEBVIEW_CONFIG.android.cachePattern",
329
+ message: `android.cachePattern should be a comma-separated glob string (e.g. "*.css,*.js,*.png"), got ${typeof a.cachePattern}.`,
330
+ })
331
+ } else if (a.cachePattern) {
332
+ passed.push({ field: "WEBVIEW_CONFIG.android.cachePattern", value: a.cachePattern })
333
+ }
334
+ if (a.security && typeof a.security.allowBackup !== "boolean") {
335
+ warnings.push({
336
+ field: "WEBVIEW_CONFIG.android.security.allowBackup",
337
+ message:
338
+ "android.security.allowBackup should be a boolean. Controls Google cloud backup of app data.",
224
339
  })
225
- } else {
226
- const a = wc.android
227
- // sdkPath and emulatorName are hard required — build exits immediately without them
228
- if (!a.sdkPath) {
229
- issues.push({
230
- field: "WEBVIEW_CONFIG.android.sdkPath",
231
- severity: "error",
232
- message:
233
- "android.sdkPath is required. Set to absolute path of Android SDK (e.g. /Users/you/Android/). Build exits immediately without it.",
234
- })
235
- } else if (!fs.existsSync(a.sdkPath)) {
236
- issues.push({
237
- field: "WEBVIEW_CONFIG.android.sdkPath",
238
- severity: "error",
239
- message: `android.sdkPath "${a.sdkPath}" does not exist on this machine. Android build will fail immediately.`,
240
- })
241
- } else {
242
- passed.push({ field: "WEBVIEW_CONFIG.android.sdkPath", value: a.sdkPath })
243
- }
244
- if (!a.emulatorName) {
245
- issues.push({
246
- field: "WEBVIEW_CONFIG.android.emulatorName",
247
- severity: "error",
248
- message:
249
- "android.emulatorName is required for debug builds. Set to AVD name (run: emulator -list-avds). Build fails if emulator not found.",
250
- })
251
- } else {
252
- passed.push({ field: "WEBVIEW_CONFIG.android.emulatorName", value: a.emulatorName })
253
- }
254
- // appName and packageName — optional but strongly recommended
255
- if (!a.appName) {
256
- warnings.push({
257
- field: "WEBVIEW_CONFIG.android.appName",
258
- message:
259
- 'android.appName not set. App will show "Catalyst Application" as display name on device.',
260
- })
261
- } else {
262
- passed.push({ field: "WEBVIEW_CONFIG.android.appName", value: a.appName })
263
- }
264
- if (!a.packageName) {
265
- warnings.push({
266
- field: "WEBVIEW_CONFIG.android.packageName",
267
- message:
268
- "android.packageName not set (e.g. com.company.app). Will be derived from appName — may produce unexpected package ID.",
269
- })
270
- } else {
271
- passed.push({ field: "WEBVIEW_CONFIG.android.packageName", value: a.packageName })
272
- }
273
- // buildType
274
- if (a.buildType && !["debug", "release"].includes(a.buildType)) {
275
- issues.push({
276
- field: "WEBVIEW_CONFIG.android.buildType",
277
- severity: "error",
278
- message: `android.buildType must be "debug" or "release", got "${a.buildType}".`,
279
- })
280
- } else {
281
- passed.push({
282
- field: "WEBVIEW_CONFIG.android.buildType",
283
- value: a.buildType || "debug (default)",
284
- })
285
- }
286
- // release keystore check
287
- if (a.buildType === "release" && !a.keystore && !a.keystoreConfig) {
288
- issues.push({
289
- field: "WEBVIEW_CONFIG.android.keystore",
290
- severity: "error",
291
- message:
292
- 'android.buildType is "release" but no keystore or keystoreConfig provided. Release build cannot sign the APK.',
293
- })
294
- }
295
- if (a.cachePattern !== undefined && typeof a.cachePattern !== "string") {
296
- warnings.push({
297
- field: "WEBVIEW_CONFIG.android.cachePattern",
298
- message: `android.cachePattern should be a comma-separated glob string (e.g. "*.css,*.js,*.png"), got ${typeof a.cachePattern}.`,
299
- })
300
- } else if (a.cachePattern) {
301
- passed.push({ field: "WEBVIEW_CONFIG.android.cachePattern", value: a.cachePattern })
302
- }
303
- if (a.security && typeof a.security.allowBackup !== "boolean") {
304
- warnings.push({
305
- field: "WEBVIEW_CONFIG.android.security.allowBackup",
306
- message:
307
- "android.security.allowBackup should be a boolean. Controls Google cloud backup of app data.",
308
- })
309
- }
310
340
  }
311
341
  }
312
342
 
313
343
  // ── iOS fields ─────────────────────────────────────────────────────────────
344
+ // buildApp:ios picks an existing iPhone simulator (ios.simulatorName
345
+ // first), creates "Catalyst iPhone" only when there is none, and
346
+ // finishes Xcode setup itself. Signing files live
347
+ // in credentials/ios/ (CI: CATALYST_IOS_CREDENTIALS), never in config.json.
314
348
  if (platform === "ios" || platform === "both") {
315
- if (!wc.ios) {
316
- issues.push({
317
- field: "WEBVIEW_CONFIG.ios",
318
- severity: "error",
319
- message: "WEBVIEW_CONFIG.ios block missing. iOS build cannot start without it.",
320
- })
321
- } else {
322
- const ios = wc.ios
323
- // appBundleId — optional but strongly recommended (defaults to com.debug.webview)
324
- if (!ios.appBundleId) {
349
+ const ios = wc.ios || {}
350
+ if (!wc.appId && ios.appBundleId) {
351
+ if (!/^[a-z0-9][a-z0-9-]*(\.[a-z0-9][a-z0-9-]*){1,}$/i.test(ios.appBundleId)) {
325
352
  warnings.push({
326
353
  field: "WEBVIEW_CONFIG.ios.appBundleId",
327
- message:
328
- 'ios.appBundleId not set. App will use "com.debug.webview" as bundle ID — fine for testing, must be set for distribution.',
354
+ message: `appBundleId "${ios.appBundleId}" may not be valid; expected reverse-DNS format like "com.company.app". Prefer WEBVIEW_CONFIG.appId.`,
329
355
  })
330
356
  } else {
331
- if (!/^[a-z0-9][a-z0-9]*(\.[a-z0-9][a-z0-9]*){1,}$/i.test(ios.appBundleId)) {
332
- warnings.push({
333
- field: "WEBVIEW_CONFIG.ios.appBundleId",
334
- message: `appBundleId "${ios.appBundleId}" may not be valid — expected reverse-DNS format like "com.company.app".`,
335
- })
336
- } else {
337
- passed.push({ field: "WEBVIEW_CONFIG.ios.appBundleId", value: ios.appBundleId })
338
- }
339
- }
340
- // simulatorName — optional, auto-detected if missing
341
- if (!ios.simulatorName) {
342
- warnings.push({
343
- field: "WEBVIEW_CONFIG.ios.simulatorName",
344
- message:
345
- 'ios.simulatorName not set. Will use auto-detected booted simulator. Recommend setting explicitly (e.g. "iPhone 17 Pro"). Run: xcrun simctl list devices.',
346
- })
347
- } else {
348
- passed.push({ field: "WEBVIEW_CONFIG.ios.simulatorName", value: ios.simulatorName })
349
- }
350
- // appName — optional, defaults to "Catalyst Application"
351
- if (!ios.appName) {
352
- warnings.push({
353
- field: "WEBVIEW_CONFIG.ios.appName",
354
- message:
355
- 'ios.appName not set. App will show "Catalyst Application" as display name on device.',
356
- })
357
- } else {
358
- passed.push({ field: "WEBVIEW_CONFIG.ios.appName", value: ios.appName })
359
- }
360
- // buildType — case-sensitive (Debug/Release not debug/release)
361
- if (ios.buildType && !["Debug", "Release"].includes(ios.buildType)) {
362
- issues.push({
363
- field: "WEBVIEW_CONFIG.ios.buildType",
364
- severity: "error",
365
- message: `ios.buildType must be "Debug" or "Release" (case-sensitive). Got: "${ios.buildType}". Common mistake: using lowercase "debug" breaks Xcode build.`,
366
- })
367
- } else {
368
- passed.push({
369
- field: "WEBVIEW_CONFIG.ios.buildType",
370
- value: ios.buildType || "Debug (default)",
371
- })
372
- }
373
- // physical device fields
374
- if (ios.deviceUDID && !ios.developmentTeam) {
375
- warnings.push({
376
- field: "WEBVIEW_CONFIG.ios.developmentTeam",
377
- message:
378
- "ios.deviceUDID is set (physical device build) but ios.developmentTeam is missing. Physical device builds require Apple Developer Team ID for code signing.",
379
- })
380
- }
381
- if (ios.cachePattern !== undefined && typeof ios.cachePattern !== "string") {
382
- warnings.push({
383
- field: "WEBVIEW_CONFIG.ios.cachePattern",
384
- message: `ios.cachePattern should be a comma-separated glob string, got ${typeof ios.cachePattern}.`,
385
- })
386
- } else if (ios.cachePattern) {
387
- passed.push({ field: "WEBVIEW_CONFIG.ios.cachePattern", value: ios.cachePattern })
357
+ passed.push({ field: "WEBVIEW_CONFIG.ios.appBundleId", value: ios.appBundleId })
388
358
  }
389
359
  }
360
+ if (ios.simulatorName)
361
+ passed.push({ field: "WEBVIEW_CONFIG.ios.simulatorName", value: ios.simulatorName })
362
+ if (String(ios.buildType || "").toLowerCase() === "release") {
363
+ warnings.push({
364
+ field: "WEBVIEW_CONFIG.ios.buildType",
365
+ message:
366
+ 'ios.buildType "release" is deprecated. Use `npm run ios:release` (catalyst buildApp:ios --release) and set buildType back to "debug" or remove it.',
367
+ })
368
+ }
369
+ if (ios.developmentTeam) {
370
+ passed.push({ field: "WEBVIEW_CONFIG.ios.developmentTeam", value: ios.developmentTeam })
371
+ } else {
372
+ warnings.push({
373
+ field: "WEBVIEW_CONFIG.ios.developmentTeam",
374
+ message:
375
+ "ios.developmentTeam not set. The simulator does not need it, but a physical iPhone (--device) and a release with cloud or Xcode-account signing do (IOS-010). Set your 10-character Apple team ID.",
376
+ })
377
+ }
378
+ // iOS reads an array of patterns; a string becomes one literal
379
+ // pattern (not split on commas, unlike android.cachePattern).
380
+ const iosPattern = ios.cachePattern
381
+ const iosPatternOk = Array.isArray(iosPattern)
382
+ ? iosPattern.every((item) => typeof item === "string")
383
+ : typeof iosPattern === "string"
384
+ if (iosPattern !== undefined && !iosPatternOk) {
385
+ warnings.push({
386
+ field: "WEBVIEW_CONFIG.ios.cachePattern",
387
+ message: `ios.cachePattern should be an array of glob strings (e.g. ["*.css", "*.js"]), got ${JSON.stringify(iosPattern)}.`,
388
+ })
389
+ } else if (typeof iosPattern === "string" && iosPattern.includes(",")) {
390
+ warnings.push({
391
+ field: "WEBVIEW_CONFIG.ios.cachePattern",
392
+ message: `ios.cachePattern "${iosPattern}" is one literal pattern on iOS, not a comma-separated list. Use an array, e.g. ["*.css", "*.js"].`,
393
+ })
394
+ } else if (iosPattern && iosPattern.length) {
395
+ passed.push({ field: "WEBVIEW_CONFIG.ios.cachePattern", value: iosPattern })
396
+ }
390
397
  }
391
398
 
392
399
  // ── splashScreen (inside WEBVIEW_CONFIG; the native builds read only that) ──
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "catalyst-core",
3
- "version": "1.1.0",
3
+ "version": "1.2.0-canary.2",
4
4
  "type": "module",
5
5
  "main": "./dist/index.jsx",
6
6
  "description": "Web framework that provides great performance out of the box",
@@ -0,0 +1,97 @@
1
+ import { execFileSync } from "child_process"
2
+ import fs from "fs"
3
+ import os from "os"
4
+ import path from "path"
5
+ import { fileURLToPath } from "url"
6
+ import { afterAll, beforeAll, describe, expect, it } from "vitest"
7
+
8
+ // bin/catalyst.js runs on import, so it is exercised as a process: a copy sits
9
+ // in a temp package whose dist/native scripts just print the argv they get.
10
+ const here = path.dirname(fileURLToPath(import.meta.url))
11
+ const binSource = path.resolve(here, "../../bin/catalyst.js")
12
+
13
+ let root = ""
14
+
15
+ beforeAll(() => {
16
+ root = fs.mkdtempSync(path.join(os.tmpdir(), "catalyst-bin-flags-"))
17
+ fs.mkdirSync(path.join(root, "bin"))
18
+ fs.mkdirSync(path.join(root, "dist", "native"), { recursive: true })
19
+ fs.copyFileSync(binSource, path.join(root, "bin", "catalyst.js"))
20
+ fs.writeFileSync(path.join(root, "package.json"), JSON.stringify({ type: "module", version: "0.0.0" }))
21
+ for (const name of [
22
+ "buildAppAndroid",
23
+ "buildAppIos",
24
+ "androidSetup",
25
+ "setupEmulatorIos",
26
+ "exportCredentials",
27
+ ]) {
28
+ fs.writeFileSync(
29
+ path.join(root, "dist", "native", `${name}.js`),
30
+ `console.log(JSON.stringify({ script: ${JSON.stringify(name)}, args: process.argv.slice(2) }))\n`
31
+ )
32
+ }
33
+ })
34
+
35
+ afterAll(() => {
36
+ fs.rmSync(root, { recursive: true, force: true })
37
+ })
38
+
39
+ function run(args: string[]) {
40
+ const out = execFileSync(process.execPath, [path.join(root, "bin", "catalyst.js"), ...args], {
41
+ encoding: "utf8",
42
+ })
43
+ return out
44
+ .trim()
45
+ .split("\n")
46
+ .map((line) => JSON.parse(line))
47
+ }
48
+
49
+ describe("native build flags", () => {
50
+ it("forwards --release and --url to the platform script", () => {
51
+ expect(run(["buildApp:android", "--release", "--url", "https://shop.acme.com"])).toEqual([
52
+ { script: "buildAppAndroid", args: ["--release", "--url", "https://shop.acme.com"] },
53
+ ])
54
+ })
55
+
56
+ it("does not dispatch on a --url value that names a command", () => {
57
+ expect(run(["--url", "build", "buildApp:android"])).toEqual([
58
+ { script: "buildAppAndroid", args: ["--url", "build"] },
59
+ ])
60
+ expect(run(["buildApp:android", "--url=serve"])).toEqual([
61
+ { script: "buildAppAndroid", args: ["--url=serve"] },
62
+ ])
63
+ })
64
+
65
+ it("keeps flags given before the command away from node", () => {
66
+ expect(run(["--release", "buildApp:android", "--verbose"])).toEqual([
67
+ { script: "buildAppAndroid", args: ["--release", "--verbose"] },
68
+ ])
69
+ })
70
+
71
+ it("forwards --device wherever it appears", () => {
72
+ expect(run(["--device", "buildApp:ios"])).toEqual([{ script: "buildAppIos", args: ["--device"] }])
73
+ })
74
+
75
+ it("forwards --yes and --accept-licenses wherever they appear", () => {
76
+ expect(run(["--yes", "setupEmulator:android", "--accept-licenses"])).toEqual([
77
+ { script: "androidSetup", args: ["--yes", "--accept-licenses"] },
78
+ ])
79
+ expect(run(["setupEmulator", "--yes"])).toEqual([
80
+ { script: "setupEmulatorIos", args: ["--yes"] },
81
+ { script: "androidSetup", args: ["--yes"] },
82
+ ])
83
+ })
84
+
85
+ it("passes the flags to both platforms for buildApp", () => {
86
+ expect(run(["buildApp", "--release", "--url=https://shop.acme.com"])).toEqual([
87
+ { script: "buildAppIos", args: ["--release", "--url=https://shop.acme.com"] },
88
+ { script: "buildAppAndroid", args: ["--release", "--url=https://shop.acme.com"] },
89
+ ])
90
+ })
91
+
92
+ it("dispatches credentials:export with its platform", () => {
93
+ expect(run(["credentials:export", "android"])).toEqual([
94
+ { script: "exportCredentials", args: ["android"] },
95
+ ])
96
+ })
97
+ })