catalyst-core 1.0.3-canary.1 → 1.2.0-canary.1

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/changelog.md CHANGED
@@ -1,6 +1,14 @@
1
1
  # Changelog
2
2
 
3
- ## [1.0.2] - unreleased
3
+ ## [1.1.0] - 2026-10-22
4
+
5
+ This release contains everything in 1.0.2, namely the HTML splash screen, experimental WebMCP support and the new CLI output, plus one fix. There are no breaking API changes.
6
+
7
+ ### Fixes
8
+
9
+ - `WebMcpProvider` no longer throws `useRouterState must be used within a RouterDataProvider` on every page under `catalyst serve`. A production build can load Catalyst's router context twice, once inlined in the app's server bundle and once from `catalyst-core/webmcp`, and the two copies did not share state. The context is now shared across copies.
10
+
11
+ ## [1.0.2] - 2026-10-22
4
12
 
5
13
  This release adds two opt-in features, an animated HTML splash screen for universal apps and experimental WebMCP support, and gives every CLI command the same terminal output. There are no breaking API changes.
6
14
 
@@ -34,6 +42,24 @@ The root entry of `catalyst-core` adds `useRouterState`, which returns `{ locati
34
42
 
35
43
  New apps declare `"type": "module"`, and the Tailwind template's config is `postcss.config.cjs`. `catalyst-core` loads either `postcss.config.cjs` or `postcss.config.js`, so existing apps need no change. `create-catalyst-app` declares `node >= 20.12.0` and runs across that range.
36
44
 
45
+ ## [1.0.1] - 2026-10-08
46
+
47
+ This release fixes Android edge-to-edge and brings on-device AI to iOS through `catalyst-ai` 1.0.1. There are no breaking API changes, but apps that converted safe-area insets from device pixels should read the edge-to-edge section.
48
+
49
+ ### On-device AI on iOS
50
+
51
+ `useNativeAI` and `useAI({ provider: "native" })` now run on-device on iOS. Set `ai.enabled: true` in `WEBVIEW_CONFIG`, install `catalyst-ai`, and build the iOS app as usual. Two engines sit behind the same hook. LiteRT-LM runs Gemma 4 E2B by default and downloads the model, about 2.6 GB, once. Apple Foundation Models needs no download and runs on iOS 26 or later on Apple Intelligence devices. The default `engine: "auto"` uses LiteRT-LM if its model is already downloaded, then Foundation Models if it is available, and otherwise LiteRT-LM with a download. The providers `litert` and `foundation-models` pin one engine. Installing `catalyst-ai` without `ai.enabled` adds nothing to the iOS build. Device requirements are in the `catalyst-ai` README.
52
+
53
+ ### Android edge-to-edge
54
+
55
+ With `WEBVIEW_CONFIG.edgeToEdge.enabled`, the WebView now draws under transparent system bars instead of opaque ones, and the bottom inset stays stable while the keyboard is open. `useSafeArea()` and the `X-Safe-Area-*` headers report insets in CSS pixels, and 0 when edge-to-edge is disabled. Apps that divided the insets by `devicePixelRatio` should remove that. The new `edgeToEdge.statusBarStyle` option (`auto`, `dark-content`, `light-content`) sets the system bar icon colour.
56
+
57
+ ### Fixes
58
+
59
+ - Dependency updates clear security advisories in `compression`, `proxy-addr` (through `express`) and `source-map-js`.
60
+ - `catalyst-core` can be installed from a git branch. Its `prepare` script no longer reads files outside the package.
61
+ - The 0.3 to 1.0 upgrade guide and the MCP server's upgrade context are corrected.
62
+
37
63
  ## [1.0.0] - 2026-09-29
38
64
 
39
65
  This release freezes the public API. The root entry of `catalyst-core` now exports exactly eight names and nothing else: `RouterDataProvider`, `useCurrentRouteData`, `useRouterData`, `MetaTag`, `split`, `hydrationReady`, `Head`, and `Body`. Anything previously reachable through the root entry but not in that list is no longer public. Applications that import a removed name will fail at build time rather than silently resolving to `undefined`.
@@ -1,8 +1,10 @@
1
1
  package io.yourname.androidproject
2
2
 
3
+ import android.Manifest
3
4
  import android.annotation.SuppressLint
4
5
  import android.content.Context
5
6
  import android.content.Intent
7
+ import android.content.pm.PackageManager
6
8
  import android.graphics.Bitmap
7
9
  import android.net.Uri
8
10
  import android.os.Bundle
@@ -11,6 +13,10 @@ import android.view.GestureDetector
11
13
  import android.view.MotionEvent
12
14
  import android.view.View
13
15
  import android.webkit.*
16
+ import androidx.activity.ComponentActivity
17
+ import androidx.activity.result.ActivityResultLauncher
18
+ import androidx.activity.result.contract.ActivityResultContracts
19
+ import androidx.core.content.ContextCompat
14
20
  import androidx.webkit.ServiceWorkerClientCompat
15
21
  import androidx.webkit.ServiceWorkerControllerCompat
16
22
  import androidx.webkit.WebViewCompat
@@ -43,6 +49,11 @@ class CustomWebView(
43
49
  private val job = SupervisorJob()
44
50
  override val coroutineContext = Dispatchers.Main + job
45
51
 
52
+ // navigator.geolocation prompt waiting on the runtime permission result
53
+ private var pendingGeolocationCallback: GeolocationPermissions.Callback? = null
54
+ private var pendingGeolocationOrigin: String? = null
55
+ private var geolocationPermissionLauncher: ActivityResultLauncher<Array<String>>? = null
56
+
46
57
  private var cacheManager: WebCacheManager
47
58
  private var offlineCacheService: OfflineCacheService
48
59
  private val metricsMonitor = MetricsMonitor.getInstance(context)
@@ -84,6 +95,10 @@ class CustomWebView(
84
95
  }
85
96
  setupFromProperties()
86
97
  cacheManager = WebCacheManager(context, properties)
98
+ // Must register before the activity is STARTED; CustomWebView is built in onCreate.
99
+ if (context is ComponentActivity) {
100
+ initializeGeolocationPermissionLauncher(context)
101
+ }
87
102
  if (profilerEnabled) metricsMonitor.attachWebView(webView)
88
103
  offlineCacheService = OfflineCacheService(context)
89
104
  setupWebView()
@@ -338,6 +353,30 @@ class CustomWebView(
338
353
  }
339
354
  }
340
355
 
356
+ private fun initializeGeolocationPermissionLauncher(activity: ComponentActivity) {
357
+ geolocationPermissionLauncher = activity.registerForActivityResult(
358
+ ActivityResultContracts.RequestMultiplePermissions()
359
+ ) { permissions ->
360
+ val granted = permissions.values.any { it }
361
+ val callback = pendingGeolocationCallback
362
+ val origin = pendingGeolocationOrigin
363
+ pendingGeolocationCallback = null
364
+ pendingGeolocationOrigin = null
365
+ if (callback != null && origin != null) {
366
+ if (BuildConfig.DEBUG) {
367
+ Log.d(TAG, "Geolocation permission result: granted=$granted")
368
+ }
369
+ callback.invoke(origin, granted, false)
370
+ }
371
+ }
372
+ }
373
+
374
+ private fun hasLocationPermission(): Boolean =
375
+ ContextCompat.checkSelfPermission(context, Manifest.permission.ACCESS_FINE_LOCATION) ==
376
+ PackageManager.PERMISSION_GRANTED ||
377
+ ContextCompat.checkSelfPermission(context, Manifest.permission.ACCESS_COARSE_LOCATION) ==
378
+ PackageManager.PERMISSION_GRANTED
379
+
341
380
  fun destroy() {
342
381
  job.cancel()
343
382
  metricsMonitor.detachWebView()
@@ -794,6 +833,9 @@ class CustomWebView(
794
833
  domStorageEnabled = true
795
834
  allowFileAccess = true
796
835
  allowContentAccess = true
836
+ // navigator.geolocation; the OS permission is only declared when
837
+ // WEBVIEW_CONFIG.location.enabled is set (see buildAndroid/assets.js).
838
+ setGeolocationEnabled(true)
797
839
  setSupportZoom(false)
798
840
  builtInZoomControls = false
799
841
  displayZoomControls = false
@@ -1279,6 +1321,36 @@ class CustomWebView(
1279
1321
  return true
1280
1322
  }
1281
1323
 
1324
+ override fun onGeolocationPermissionsShowPrompt(
1325
+ origin: String?,
1326
+ callback: GeolocationPermissions.Callback?
1327
+ ) {
1328
+ if (BuildConfig.DEBUG) {
1329
+ Log.d(TAG, "Geolocation permission requested for origin: $origin")
1330
+ }
1331
+ if (hasLocationPermission()) {
1332
+ callback?.invoke(origin, true, false)
1333
+ return
1334
+ }
1335
+ val launcher = geolocationPermissionLauncher
1336
+ if (launcher == null || callback == null || origin == null) {
1337
+ callback?.invoke(origin, false, false)
1338
+ return
1339
+ }
1340
+ // Deny any prompt still waiting so its page doesn't hang
1341
+ pendingGeolocationCallback?.invoke(pendingGeolocationOrigin, false, false)
1342
+ pendingGeolocationCallback = callback
1343
+ pendingGeolocationOrigin = origin
1344
+ // Without the permission in the manifest (location disabled), Android
1345
+ // returns denied immediately and no dialog is shown.
1346
+ launcher.launch(
1347
+ arrayOf(
1348
+ Manifest.permission.ACCESS_FINE_LOCATION,
1349
+ Manifest.permission.ACCESS_COARSE_LOCATION
1350
+ )
1351
+ )
1352
+ }
1353
+
1282
1354
  override fun onPermissionRequest(request: PermissionRequest) {
1283
1355
  if (BuildConfig.DEBUG) {
1284
1356
  Log.d(TAG, "Permission request from web content: ${request.resources?.joinToString()}")
@@ -545,6 +545,28 @@ function createAssetsPhase(ctx) {
545
545
  progress.log(`Warning: Error cleaning notification assets: ${error.message}`, "warning");
546
546
  }
547
547
  }
548
+
549
+ // ── Location helpers ──────────────────────────────────────────────────────
550
+
551
+ const LOCATION_PERMISSIONS = ["android.permission.ACCESS_FINE_LOCATION", "android.permission.ACCESS_COARSE_LOCATION"];
552
+
553
+ // Location permissions are only declared when WEBVIEW_CONFIG.location.enabled is
554
+ // true, so apps that never use navigator.geolocation don't ship them.
555
+ async function processLocation(WEBVIEW_CONFIG) {
556
+ const manifestPath = `${pwd}/androidProject/app/src/main/AndroidManifest.xml`;
557
+ try {
558
+ let manifestContent = fs.readFileSync(manifestPath, "utf8");
559
+ manifestContent = manifestContent.split("\n").filter(line => !LOCATION_PERMISSIONS.some(permission => line.includes(`<uses-permission android:name="${permission}"`))).join("\n");
560
+ if (WEBVIEW_CONFIG.location?.enabled) {
561
+ const permissionsXml = LOCATION_PERMISSIONS.map(permission => ` <uses-permission android:name="${permission}" />`).join("\n");
562
+ manifestContent = manifestContent.replace(/(<uses-permission[^>]*>[\s\S]*?)(\s*<uses-feature)/, `$1\n${permissionsXml}$2`);
563
+ progress.log("Added location permissions to AndroidManifest.xml", "success");
564
+ }
565
+ fs.writeFileSync(manifestPath, manifestContent);
566
+ } catch (error) {
567
+ progress.log(`Warning: Error processing location permissions: ${error.message}`, "warning");
568
+ }
569
+ }
548
570
  return {
549
571
  copyBuildAssets,
550
572
  copySplashscreenAssets,
@@ -552,7 +574,8 @@ function createAssetsPhase(ctx) {
552
574
  copySplashHtml,
553
575
  copyIconAssets,
554
576
  configureAppName,
555
- processNotifications
577
+ processNotifications,
578
+ processLocation
556
579
  };
557
580
  }
558
581
  module.exports = createAssetsPhase;
@@ -66,7 +66,8 @@ function createAndroidBuild(config) {
66
66
  copySplashHtml,
67
67
  copyIconAssets,
68
68
  configureAppName,
69
- processNotifications
69
+ processNotifications,
70
+ processLocation
70
71
  } = createAssetsPhase(ctx);
71
72
  const {
72
73
  detectPhysicalDevice,
@@ -164,6 +165,7 @@ function createAndroidBuild(config) {
164
165
  log: (message, status = "info") => progress.log(message, status)
165
166
  });
166
167
  await processNotifications(wvConfig);
168
+ await processLocation(wvConfig);
167
169
  progress.log(`Build optimization: ${buildOptimisation ? "Enabled" : "Disabled"}`, "info");
168
170
  syncAIPackageIfEnabled(wvConfig, progress);
169
171
  progress.complete("copyAssets");
@@ -241,6 +243,7 @@ function createAndroidBuild(config) {
241
243
  log: (message, status = "info") => progress.log(message, status)
242
244
  });
243
245
  await processNotifications(wvConfig);
246
+ await processLocation(wvConfig);
244
247
  syncAIPackageIfEnabled(wvConfig, progress);
245
248
  progress.log("buildAndroidForTesting complete — project ready for gradlew test", "success");
246
249
  return {
@@ -77,6 +77,7 @@ function generateSwiftProperty(key, value, indent = " ") {
77
77
 
78
78
  // ─── Module factory ───────────────────────────────────────────────────────────
79
79
 
80
+ const DEFAULT_LOCATION_USAGE_DESCRIPTION = "This app needs your location to provide location-based services";
80
81
  module.exports = function createConfigPhase(ctx) {
81
82
  const {
82
83
  WEBVIEW_CONFIG,
@@ -365,6 +366,11 @@ INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = UIInterfaceOrientationPo
365
366
  restoreManagedFileFromBaseline(plistPath);
366
367
  const plistObject = readPlistObject(plistPath);
367
368
  plistObject.CFBundleDisplayName = iosConfig.appName || "Catalyst Application";
369
+ // WKWebView's navigator.geolocation needs this key; without it iOS never prompts.
370
+ // Restored from baseline above, so it disappears when location is turned off.
371
+ if (WEBVIEW_CONFIG.location?.enabled) {
372
+ plistObject.NSLocationWhenInUseUsageDescription = WEBVIEW_CONFIG.location.usageDescription || DEFAULT_LOCATION_USAGE_DESCRIPTION;
373
+ }
368
374
  mergeIntoTopLevelObject(plistObject, pluginInfoPlist, "ios.infoPlist");
369
375
  if (isGoogleSignInEnabled && resolvedReversedClientId) {
370
376
  mergeUrlSchemes(plistObject, [{
@@ -9,6 +9,8 @@ export function registerCatalystServiceWorker() {
9
9
  if (registered) return
10
10
  if (typeof window === "undefined" || typeof navigator === "undefined") return
11
11
  if (process.env.NODE_ENV !== "production") return
12
+ // Replaced at build time from config.json (see getClientEnvVariables).
13
+ if (process.env.OFFLINE_SNAPSHOTS_ENABLE === false) return
12
14
  if (isNativeWebView()) return
13
15
  if (!("serviceWorker" in navigator)) return
14
16