IReader supports iOS through Kotlin/JS compilation. Sources written in Kotlin are compiled to JavaScript and executed via JavaScriptCore on iOS devices, enabling dynamic source loading while remaining App Store compliant.
Apple prohibits loading dynamic native code at runtime. JavaScript execution via JavaScriptCore is allowed, making Kotlin/JS the ideal solution for cross-platform source support.
| Platform | Loading Method |
|---|---|
| Android | DEX/APK dynamic loading |
| Desktop | JAR class loading |
| iOS | JavaScript via JavaScriptCore |
┌─────────────────────────────────────────────────────────────────┐
│ IReader iOS App │
├─────────────────────────────────────────────────────────────────┤
│ runtime.js (bundled in app, ~800KB-1.2MB) │
│ ├── Kotlin stdlib, coroutines, serialization │
│ ├── Ktor HTTP client (JS engine) │
│ ├── Ksoup HTML parser │
│ ├── source-api: HttpSource, SourceFactory, models │
│ └── SourceRegistry + SourceBridge │
└─────────────────────────────────────────────────────────────────┘
│
│ provides globals
▼
┌─────────────────────────────────────────────────────────────────┐
│ Extension JS Files (downloaded per source) │
├─────────────────────────────────────────────────────────────────┤
│ freewebnovelkmp.js (~24KB) │
│ ├── FreeWebNovelKmp source class │
│ ├── JsExtension implementation │
│ └── initFreeWebNovelKmp() registration function │
└─────────────────────────────────────────────────────────────────┘
// 1. App startup - load runtime once
jsContext.evaluateScript(bundledRuntimeJs)
// 2. User installs source - download JS file
let sourceJs = download("https://repo.example.com/js/freewebnovelkmp.js")
jsContext.evaluateScript(sourceJs)
// 3. Initialize source
jsContext.evaluateScript("initFreeWebNovelKmp()")
// 4. Source is now registered and ready to use
let results = jsContext.evaluateScript("SourceBridge.search('freewebnovelkmp', 'query', 1)")In the source's build.gradle.kts:
Extension(
name = "MySource",
versionCode = 1,
libVersion = "2",
lang = "en",
// Enable JS compilation
)# Build JS bundle for all enabled sources
./gradlew :js-sources:jsBrowserProductionWebpack
# Generate repository with JS files
./gradlew repo
# Or run both
./gradlew :js-sources:createSourceIndex repobuild/repo/
├── index.json # Includes JS source metadata
├── apk/ # Android APKs
├── icon/ # Source icons
└── js/ # JavaScript bundles
├── freewebnovelkmp.js
└── freewebnovelkmp.js.map
The index.json includes a js section for iOS sources:
{
"extensions": [...],
"js": {
"note": "JS sources require runtime.js from the main IReader app",
"sources": [
{
"id": "freewebnovelkmp",
"name": "FreeWebNovelKmp",
"lang": "en",
"file": "freewebnovelkmp.js",
"initFunction": "initFreeWebNovelKmp"
}
]
}
}The JsExtensionProcessor generates code conditionally based on the target platform:
The processor detects the build target by examining the KSP output path:
- Android builds: Paths contain
/ksp/enRelease/,/ksp/arDebug/, etc. - JS builds: Paths contain
/js/,/jsMain/,/kotlin/js/
1. Platform-Agnostic Code (always generated): JsInit.kt
package ireader.freewebnovelkmp.js
// Concrete implementation class
class JsExtension(deps: Dependencies) : FreeWebNovelKmp(deps)
// Source metadata
object FreeWebNovelKmpInfo {
val id = "4808063048038840027"
val name = "FreeWebNovelKmp"
val lang = "en"
}
// Factory function
fun createSource(deps: Dependencies): JsExtension = JsExtension(deps)2. JS-Specific Code (only when building for JS target): JsRegistration.kt
package ireader.freewebnovelkmp.js
@JsExport
@JsName("initFreeWebNovelKmp")
fun initFreeWebNovelKmp(): dynamic {
console.log("FreeWebNovelKmp: Initializing source...")
js("""
if (typeof SourceRegistry !== 'undefined') {
SourceRegistry.register('freewebnovelkmp', function(deps) {
return new ireader.freewebnovelkmp.js.JsExtension(deps);
});
}
""")
return js("""({ id: "4808063048038840027", name: "FreeWebNovelKmp", lang: "en" })""")
}This separation ensures:
- Android builds compile successfully (no JS-only constructs like
dynamic,js(),console) - JS builds get full registration functionality for iOS runtime
- The same source code works across all platforms
| Component | Size | Notes |
|---|---|---|
| runtime.js | ~800KB-1.2MB | Bundled in iOS app, loaded once |
| Per source | ~15-30KB | Downloaded when user installs |
| 50 sources | ~1.5MB total | User downloads as needed |
The webpack bundler produces self-contained JS files that include only the source-specific code, relying on runtime.js for shared dependencies.
Sources using SourceFactory are automatically JS-compatible. The same Kotlin code runs on:
- Android (compiled to DEX)
- Desktop (compiled to JVM bytecode)
- iOS (compiled to JavaScript)
- Extend
SourceFactory(not legacyHttpSource) - Use Ksoup for HTML parsing (not Jsoup)
- Use Ktor for HTTP requests
- Avoid JVM-specific APIs
// After loading the JS file in a browser
initFreeWebNovelKmp()
// Should log: "FreeWebNovelKmp: Registered with SourceRegistry"# Check if init function exists
Get-Content "build/repo/js/freewebnovelkmp.js" | Select-String "initFreeWebNovelKmp"
# Check file size
(Get-Item "build/repo/js/freewebnovelkmp.js").Length / 1KB| Issue | Solution |
|---|---|
| JS file too large | Ensure webpack mode is executable, not library |
| Init function not found | Check KSP generated JsInit.kt in build output |
| Source not registering | Verify SourceRegistry is available from runtime.js |
| Build fails on JS target | Check for JVM-only dependencies |
iOS-Source-Architecture.md- Detailed architecture and implementation planJS_INTEGRATION.md- Technical integration detailscompiler/src/main/kotlin/JsExtensionProcessor.kt- KSP processor sourcejs-sources/build.gradle.kts- JS build configuration
IReader's iOS support works by:
- Compiling Kotlin sources to JavaScript via Kotlin/JS
- Bundling shared runtime in the iOS app (~1MB, loaded once)
- Distributing individual source JS files (~15-30KB each)
- Loading sources dynamically via JavaScriptCore
- Using SourceRegistry/SourceBridge for iOS-JS communication
This approach enables the same source code to work across all platforms while complying with App Store requirements.