Cordova, Ionic & Capacitor
JOPS runs unchanged inside a mobile WebView, so the same app can ship to the App Store and Google Play through a hybrid shell. Nothing on this page changes how JOPS behaves in a regular browser: every mobile adjustment is either feature-detected (it only runs when the native shell is present) or a CSS value that resolves to 0 outside it.
| Shell | Status |
|---|---|
| Apache Cordova (iOS + Android) | Supported and tested (cordova-ios 8, cordova-android 15) |
| Capacitor (iOS + Android) | Supported and tested (Capacitor 8). Install @capacitor/app for the Android back button |
| Ionic | Use Ionic’s framework-free UI components (@ionic/core) on top of Cordova or Capacitor |
Quick start
Section titled “Quick start”-
Create the hybrid project and its platforms.
Terminal window cordova create my-app com.example.myapp MyAppcd my-appcordova platform add ios androidTerminal window mkdir my-app && cd my-appnpm init -ynpm i @capacitor/core @capacitor/ios @capacitor/android @capacitor/appnpm i -D @capacitor/clinpx cap init MyApp com.example.myapp --web-dir wwwnpx cap add iosnpx cap add android -
Install JOPS in the project root, the folder with
package.json, not insidewww/:Terminal window npm i jops-coreCordova and Capacitor copy the whole web folder into the app, so a
node_modules/folder there would be shipped too. -
Run
initfrom the project root (running it from insidewww/or yourwebDirworks too):Terminal window npx jops-core initinitdetects the project type and writes the app into the web folder the shell actually serves:www/for Cordova, thewebDirfromcapacitor.config.*for Capacitor.[jops-core] Detected cordova project → web folder: www/ -
Build and run with the shell’s own tools.
Terminal window cordova run ioscordova run androidTerminal window npx cap syncnpx cap run iosnpx cap run androidYou can also open the native project in Xcode or Android Studio.
What init configures
Section titled “What init configures”| Web | Cordova | Capacitor | |
|---|---|---|---|
index.html, lib/, template |
current folder | www/ |
webDir |
.vscode/ and package.json scripts |
current folder | project root | project root |
viewport-fit=cover |
— | ✓ | ✓ |
| Content-Security-Policy | — | adds 'unsafe-inline' (inline import map) and blob: (Thread workers) to script-src |
— (Capacitor has none by default) |
<script src="cordova.js"> |
— | ✓ | — |
iOS custom scheme in config.xml |
— | ✓ (app://localhost) |
— (Capacitor uses capacitor://localhost) |
"build" script in package.json |
✓ | — (use cordova build) |
— (use npx cap sync) |
"postinstall": "jops-core update" |
✓ | ✓ | ✓ |
- Safe to run again. Every step checks first, so running
initagain never duplicates anything. postinstall."jops-core update"copies the library fromnode_modulesafter everynpm install. Projects created before 1.1.8 have"postinstall": "npx jops-core init", which shows the template question on everynpm install(since 1.1.8 it uses 5, None, when nobody answers; before,npm installfailed in CI). Change it to"postinstall": "jops-core update".- Cordova’s sample page. When you pick the Empty or Hello World template and
www/index.htmlis still the page created bycordova create,initrenames it towww/index.cordova.htmland writes a JOPSindex.html. No files are deleted; the sample’scss/,img/andjs/folders can be removed by hand. - Updates and builds.
npx jops-core updatewrites to the same web folder.npx jops-core buildis skipped in hybrid projects, because the web folder is shipped as it is. - Override the detection with
--cordova,--capacitoror--web. For example,npx jops-core init --webtreats the current folder as a plain web app.
Serving origin
Section titled “Serving origin”JOPS loads App.js, views and layouts with native import() and fetch(), which WebViews block on file:// pages. The app must be served from a real origin:
-
Cordova Android and Capacitor already serve from
https://localhost(Capacitor iOS usescapacitor://localhost). -
Cordova iOS:
initadds a custom scheme toconfig.xml. It is applied on the nextcordova prepare,buildorrun:config.xml <platform name="ios"><preference name="scheme" value="app" /><preference name="hostname" value="localhost" /></platform>
Values you already set are never changed, and any custom scheme works. init warns if the iOS scheme is file, http or https, or if AndroidInsecureFileModeEnabled is true, since both keep the app on file://.
Content-Security-Policy
Section titled “Content-Security-Policy”Cordova projects come with a Content-Security-Policy. JOPS needs two additions to script-src: 'unsafe-inline' for the inline import map and blob: for Thread workers. This is the index.html that init produces for a new Cordova project:
<head> <meta charset="UTF-8" /> <meta http-equiv="Content-Security-Policy" content="default-src 'self' data: blob: https://ssl.gstatic.com; script-src 'self' 'unsafe-inline' blob:; style-src 'self' 'unsafe-inline'; connect-src 'self' data: blob:; media-src * blob:; img-src 'self' data: blob: content:;" /> <meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" /> <script type="importmap"> { "imports": { "jops-core": "/lib/jops-core.min.js" } } </script></head><body> <script src="cordova.js"></script> <script type="module" src="/lib/jops-core.min.js" data-app="./src/App.js"></script></body>Capacitor has no policy by default. If you add one, include the same two script-src sources. If your app calls a remote API, add its host to connect-src.
Android back button
Section titled “Android back button”In Cordova and Capacitor, JOPS connects the Android back button (key and gesture) to the Router automatically:
- If
Router.singleton().canGoBack()istrue, it callsRouter.back(). The current view receivesonBackPressed()exactly as with the browser’s Back. - On the first screen it closes the app (
navigator.app.exitApp()), the standard Android behaviour.
This matters on Android 16 (targetSdk 36, predictive back): without a backbutton listener, Cordova lets the system handle back and the app closes on the first press, whatever its history. The handler also smooths over a cordova-android 15 issue where one key press fires backbutton twice.
The hook only activates on the deviceready event, which Cordova and Capacitor fire and a regular browser never does.
Handling back yourself
Section titled “Handling back yourself”If your app handles backbutton itself, turn the built-in handler off with data-native-back="off" on the jops-core script:
<script type="module" src="/lib/jops-core.min.js" data-app="./src/App.js" data-native-back="off"></script>Capacitor: install @capacitor/app
Section titled “Capacitor: install @capacitor/app”Capacitor needs the official App plugin. No code is needed:
npm i @capacitor/appnpx cap syncCapacitor’s Cordova compatibility layer fires deviceready and forwards JOPS’s backbutton listener to the plugin, so the behaviour above applies unchanged. Without @capacitor/app, Capacitor does not intercept back at all and the app closes on the first press; JOPS logs a console warning in that case.
To handle back yourself in Capacitor, opt out with data-native-back="off" and use the plugin’s global (no bundler needed):
import { Router } from "jops-core";
const { App } = window.Capacitor.Plugins;
App.addListener("backButton", () => { const router = Router.singleton(); if (router?.canGoBack()) router.back(); else App.exitApp();});Notches, Dynamic Island and the home indicator
Section titled “Notches, Dynamic Island and the home indicator”With viewport-fit=cover (added by init in Cordova and Capacitor projects), the page extends under the status bar and the home indicator. Pad fixed or sticky bars with the safe-area insets. They are 0 in a regular browser, so the layout does not change there:
.header { padding-top: calc(1rem + env(safe-area-inset-top, 0px));}.footer { padding-bottom: calc(2rem + env(safe-area-inset-bottom, 0px));}The Hello World template already does this.
Debugging
Section titled “Debugging”- Android: open
chrome://inspectin desktop Chrome while the emulator or a device runs the debug build. - iOS: in Safari, choose Develop → Simulator (or your device) → your app.
A cordova_plugins.js “not found” error in the console is expected when the project has no Cordova plugins installed. It is harmless.