catalyst-core 1.1.0 → 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 +27 -1
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/CustomWebview.kt +72 -0
- package/dist/native/buildAndroid/assets.js +24 -1
- package/dist/native/buildAndroid/index.js +4 -1
- package/dist/native/buildIos/config.js +6 -0
- package/dist/offline/registerServiceWorker.js +2 -0
- package/dist/otel.js +503 -14
- package/dist/scripts/build.js +30 -22
- package/dist/server/renderer/document/Body.jsx +5 -4
- package/dist/server/renderer/document/Head.jsx +2 -1
- package/dist/server/renderer/extract.d.ts +11 -4
- package/dist/server/renderer/extract.js +21 -7
- package/dist/server/renderer/handler.jsx +32 -14
- package/dist/vite/FastRefresh.jsx +2 -1
- package/dist/vite/vite.config.js +33 -3
- package/dist/web-router/components/RouterDataProvider.jsx +5 -3
- package/dist/web-router/components/RouterDataProvider.server.test.jsx +16 -1
- package/dist/web-router/components/RouterDataProvider.test.jsx +3 -1
- package/package.json +1 -1
- package/test/native/locationPermissions.test.js +56 -0
- package/test/server/handler.assets.test.ts +77 -0
- package/test/server/otel.test.ts +3 -2
package/changelog.md
CHANGED
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
## [1.0
|
|
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`.
|
package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/CustomWebview.kt
CHANGED
|
@@ -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
|
|