Mobile apps from plugins: a how-to, with Deep Research as the worked example
documentation/how-to-build-mobile-apps-from-plugins/README.md explains why Deep Research (doxa-maps-spa, its app preset) is the plugin to wrap first: the seven production maps in one phone-ready shell, token-free on MapLibre. It then walks the whole path: tools, the wiring check, one command, what each step does, another plugin as the app, making it your own, and fixes. The one command needed two small changes; the default app is unchanged: - internal/set-app-start-page.mjs takes APP_START=<plugin>/<screen>[/<preset>] and opens that page instead of the UPG-100 list. Unset, it behaves and marks the page exactly as before. A malformed value, a page missing from the build, or links that would not resolve are refused, as before. - package.json: phone:from-tree is sync, start page, verify, assemble, install from the committed tree, with no full rebuild (bun run build refuses without BUILD_ALLOW=1). phone:deep-research is phone:from-tree with Deep Research as the start page. how-to.md links the guide, and two of its lines are corrected: webDir is public/dx-maps/, and apk:debug's first step is the full build, which asks for BUILD_ALLOW=1. Checked, with no Android tools: the start page written for doxa-maps-spa/doxa-maps-spa/app, then the app root served and loaded in the shared browser at 390 and 1280. It showed the gallery element, 15 tabs (the seven maps and the experimental ones) and a drawn map, with 0 console errors and 0 failed requests. --check passed for APP_START and failed for the default until it was restored; the default run still writes the UPG-100 page with its original marker. The Gradle and adb steps are those bun run phone already uses; they were not run here.
This commit is contained in:
parent
59fd232a1f
commit
f3fdf4bf37
4 changed files with 165 additions and 15 deletions
131
documentation/how-to-build-mobile-apps-from-plugins/README.md
Normal file
131
documentation/how-to-build-mobile-apps-from-plugins/README.md
Normal file
|
|
@ -0,0 +1,131 @@
|
|||
# How to build a mobile app from a plugin
|
||||
|
||||
The plugins in this repo are web pages, and an Android app can wrap them with no new code. This guide
|
||||
turns one into an app you can install, using **Deep Research** as the worked example. It takes one
|
||||
command once the tools are installed.
|
||||
|
||||
## Why start with Deep Research
|
||||
|
||||
Deep Research is the plugin that is already a whole app. It is the app shell in
|
||||
`plugin-designs/doxa-maps-spa/` (its `app` preset). One screen holds all seven production maps:
|
||||
- Big Picture;
|
||||
- Simple Research;
|
||||
- Regions;
|
||||
- Prayer Progress;
|
||||
- UPG-100 List;
|
||||
- Cities with Internet;
|
||||
- Gospel Resources.
|
||||
|
||||
The experimental maps are there too. It is made for phones:
|
||||
- a round **☰** at the bottom left switches maps;
|
||||
- each legend starts folded;
|
||||
- the ⓘ opens as a sheet over the map;
|
||||
- posters, packets and cards print from the phone.
|
||||
|
||||
It draws with the free MapLibre engine, so it needs no Mapbox key. And it needs no new code: the app
|
||||
is the same built folder the web uses, opened on Deep Research instead of on the Plugin Studio.
|
||||
|
||||
## The short version
|
||||
|
||||
```bash
|
||||
bun install
|
||||
bun run check:mobile # checks the app is wired up; needs no Android tools
|
||||
bun run phone:deep-research # makes the app and installs it on your phone or emulator
|
||||
```
|
||||
|
||||
Then open **Doxa Maps** on the phone.
|
||||
|
||||
## What you need
|
||||
|
||||
- This repo, cloned, and [Bun](https://bun.sh). The README's quick start covers both.
|
||||
- **Android Studio**, which brings the Android SDK and `adb`, and **JDK 17**.
|
||||
- An Android phone plugged in over USB with USB debugging turned on, or an emulator started from
|
||||
Android Studio. `adb devices` should list it.
|
||||
|
||||
iOS is not set up yet. It needs a Mac with Xcode, and its project would live beside the Android one,
|
||||
at `deployment/capacitor/ios`.
|
||||
|
||||
## Step by step
|
||||
|
||||
### 1. Check the wiring (no Android tools needed)
|
||||
|
||||
```bash
|
||||
bun run check:mobile
|
||||
```
|
||||
|
||||
This copies the built folder, `public/dx-maps/`, into the Android project (Capacitor's `cap sync`).
|
||||
It then checks that the copy landed. It should end with `OK webDir copied`.
|
||||
|
||||
### 2. Make the app and install it
|
||||
|
||||
```bash
|
||||
bun run phone:deep-research
|
||||
```
|
||||
|
||||
This runs, in order:
|
||||
|
||||
1. `cap sync android`: copies the built folder into the Android project.
|
||||
2. The start-page step, `APP_START=doxa-maps-spa/doxa-maps-spa/app`: the app opens Deep Research
|
||||
instead of the Plugin Studio.
|
||||
3. `cap:verify`: stops if the copy is empty, which would show as a blank white screen.
|
||||
4. `./gradlew assembleDebug`: builds `app-debug.apk`.
|
||||
5. `adb install -r`: installs it on the phone.
|
||||
|
||||
It uses the built folder as it is committed, so nothing is rebuilt. On a fresh clone that folder is
|
||||
the current build.
|
||||
|
||||
### 3. Open it
|
||||
|
||||
The app is called **Doxa Maps**. On a phone it opens on Simple Research, because Big Picture needs a
|
||||
larger screen. The round **☰** at the bottom left lists every map.
|
||||
|
||||
## Changed a plugin first?
|
||||
|
||||
Rebuild the one you changed, then run the same command:
|
||||
|
||||
```bash
|
||||
BUNDLE=<plugin> bun x vite build && bun run tree && bun run fix:sri
|
||||
bun run phone:deep-research
|
||||
```
|
||||
|
||||
## Any other plugin as an app
|
||||
|
||||
It is the same command with another start page. Name the page's folder inside `public/dx-maps/`, as
|
||||
`<plugin>/<screen>` or `<plugin>/<screen>/<preset>`:
|
||||
|
||||
```bash
|
||||
APP_START=doxa-simple-map/doxa-simple-map bun run phone:from-tree # Prayer Progress
|
||||
bun run phone:from-tree # unset: the UPG-100 list
|
||||
```
|
||||
|
||||
Pick a page that works on a phone; Big Picture, for one, is desktop only. The start-page step
|
||||
refuses a page that is not in the build, and one whose links would not resolve at the app's root.
|
||||
|
||||
## Make it your own
|
||||
|
||||
- **The name under the icon:** `app_name` in
|
||||
`deployment/capacitor/android/app/src/main/res/values/strings.xml`.
|
||||
- **Icon and splash screen:** the images in `deployment/capacitor/android/app/src/main/res/`.
|
||||
- **Version:** `versionCode` and `versionName` in `deployment/capacitor/android/app/build.gradle`.
|
||||
- **A store release:** it needs its own app id and a signing key. The key never goes in the repo.
|
||||
|
||||
## If something goes wrong
|
||||
|
||||
- **A blank white screen:** the copy is empty. Run `bun run check:mobile`; it must say
|
||||
`OK webDir copied`.
|
||||
- **The app opens the Plugin Studio:** the start-page step did not run. Run
|
||||
`bun run phone:deep-research` again.
|
||||
- **`adb` finds no device:** turn on USB debugging and accept the prompt on the phone, or start an
|
||||
emulator.
|
||||
- **Maps without their data:** several maps load live data, so the phone needs internet.
|
||||
- **Using a Mapbox token:** the app's address is `https://localhost`. A token service must allow that
|
||||
address (CORS). Deep Research's `app` preset uses MapLibre and needs no token.
|
||||
|
||||
## How it works
|
||||
|
||||
The app is a WebView around the same built folder the web serves; `capacitor.config.json` points at
|
||||
it (`webDir`). The start-page step, `internal/set-app-start-page.mjs`, copies the chosen page to the
|
||||
folder's root in the app's copy and fixes its links. So the app opens that page, and the web folder
|
||||
itself is untouched. The Plugin Studio stays inside the app at `studio.html`.
|
||||
|
||||
One build, three outputs: the web folder, this Android app, and the desktop app (see the README).
|
||||
|
|
@ -303,13 +303,16 @@ Contracts and gotchas (PROFILES shape, tag-vs-bundle names, `--no-token`):
|
|||
**Mobile is separate from `bun run build`.** The default build produces **only** the `dx-maps`
|
||||
web folder — the embeddable maps (and the SPA, if configured). It never touches Capacitor. The
|
||||
Android/iOS app is an **opt-in** step that *reuses* that web build: Capacitor's `webDir` points at
|
||||
`public/` (`appId: life.doxa.maps`, `appName: "Doxa Maps"`), so `cap sync` wraps the already-built
|
||||
`public/dx-maps/` (`appId: life.doxa.maps`, `appName: "Doxa Maps"`), so `cap sync` wraps the already-built
|
||||
folder into a native app. You never need to build mobile to ship the web maps.
|
||||
|
||||
**The quickest app is one plugin, with no rebuild:** `bun run phone:deep-research` installs Deep Research
|
||||
as an app. The whole tutorial: [how to build mobile apps from plugins](how-to-build-mobile-apps-from-plugins/README.md).
|
||||
|
||||
**Android** (project is committed; needs Android Studio + a JDK):
|
||||
|
||||
```bash
|
||||
bun run apk:debug # build web → cap sync → verify → assemble a debug APK
|
||||
bun run apk:debug # build web → cap sync → verify → assemble a debug APK (the full build asks for BUILD_ALLOW=1)
|
||||
bun run apk:install # adb install onto a connected device/emulator
|
||||
```
|
||||
|
||||
|
|
|
|||
|
|
@ -26,7 +26,11 @@
|
|||
* verified means there is no second copy of that hash to drift. Only the relative depth changes.
|
||||
*
|
||||
* Usage: node internal/set-app-start-page.mjs [--check]
|
||||
* --check assert the app root is already the UPG-100 page; do not write. Exit 1 if not.
|
||||
* --check assert the app root is already the start page; do not write. Exit 1 if not.
|
||||
* APP_START=<bundle>/<profile>[/<preset>] start on another plugin page instead of the UPG-100 list,
|
||||
* as the tree lays it out: APP_START=doxa-maps-spa/doxa-maps-spa/app is Deep Research
|
||||
* (`bun run phone:deep-research`). Unset, the app opens the UPG-100 list, as before.
|
||||
* How-to: documentation/how-to-build-mobile-apps-from-plugins/README.md
|
||||
*/
|
||||
import { readFileSync, writeFileSync, existsSync, copyFileSync } from 'node:fs'
|
||||
import { resolve, dirname, join } from 'node:path'
|
||||
|
|
@ -39,13 +43,21 @@ const CHECK = process.argv.includes('--check')
|
|||
// The assets tree `cap sync` writes. NOT public/ — see the header.
|
||||
const APP_PUBLIC = join(ROOT, 'deployment/capacitor/android/app/src/main/assets/public')
|
||||
const APP_ROOT = join(APP_PUBLIC, 'index.html')
|
||||
// The showcase leaf, inside the served tree.
|
||||
// The showcase leaf, inside the served tree: the UPG-100 list unless APP_START names another page.
|
||||
const TREE = TREE_NAME
|
||||
const LEAF_REL = `my-upg-100-list/upg-100-mfe/index.html` // the app's web root IS the tree (webDir public/<tree>)
|
||||
const DEFAULT_START = 'my-upg-100-list/upg-100-mfe'
|
||||
const START = (process.env.APP_START || DEFAULT_START).trim().replace(/^\/+|\/+$/g, '')
|
||||
if (!/^[a-z0-9_-]+(\/[a-z0-9_-]+){1,2}$/i.test(START)) {
|
||||
console.error(`APP-START-PAGE: FAIL — APP_START must be <bundle>/<profile>[/<preset>], got "${START}".`)
|
||||
process.exit(1)
|
||||
}
|
||||
const NAME = START === DEFAULT_START ? 'the UPG-100 list' : START
|
||||
const LEAF_REL = `${START}/index.html` // the app's web root IS the tree (webDir public/<tree>)
|
||||
const LEAF = join(APP_PUBLIC, LEAF_REL)
|
||||
|
||||
// A marker so --check can tell the app page apart from the studio page without parsing HTML.
|
||||
const MARKER = `<!-- ${TREE_NAME}: mobile app start page (UPG-100 list) -->`
|
||||
// The default keeps its original text, so an app built before APP_START existed still checks.
|
||||
const MARKER = `<!-- ${TREE_NAME}: mobile app start page (${START === DEFAULT_START ? 'UPG-100 list' : START}) -->`
|
||||
|
||||
if (!existsSync(APP_PUBLIC)) {
|
||||
console.log('APP-START-PAGE: SKIP — no synced assets tree.')
|
||||
|
|
@ -62,23 +74,25 @@ if (!existsSync(LEAF)) {
|
|||
if (CHECK) {
|
||||
const cur = existsSync(APP_ROOT) ? readFileSync(APP_ROOT, 'utf8') : ''
|
||||
if (cur.includes(MARKER)) {
|
||||
console.log('APP-START-PAGE: PASS — the app opens the UPG-100 list.')
|
||||
console.log(`APP-START-PAGE: PASS — the app opens ${NAME}.`)
|
||||
process.exit(0)
|
||||
}
|
||||
console.error('APP-START-PAGE: FAIL — the app root is NOT the UPG-100 page.')
|
||||
console.error(`APP-START-PAGE: FAIL — the app root is NOT ${NAME}.`)
|
||||
console.error(cur.includes('<plugin-studio')
|
||||
? ' It mounts <plugin-studio>: a recipient opening the app meets the dev studio.'
|
||||
: ' No UPG-100 marker found.')
|
||||
: ` No start-page marker for ${START} found.`)
|
||||
console.error(' Fix: node internal/set-app-start-page.mjs (after cap:sync).')
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
// ── REWRITE THE LEAF FOR ONE LEVEL UP ────────────────────────────────────────
|
||||
// The leaf lives at <tree>/my-upg-100-list/upg-100-mfe/index.html and reaches its bundle with
|
||||
// `../my-upg-100-list.js`. At the app root that same relative path resolves ABOVE the web root.
|
||||
// Every leaf-relative reference gains the leaf's own directory prefix; absolute and protocol URLs
|
||||
// (the Mapbox CDN tags) are left alone.
|
||||
const LEAF_DIR = `my-upg-100-list/upg-100-mfe/`
|
||||
// The leaf lives at <tree>/<START>/index.html (the UPG-100 list: my-upg-100-list/upg-100-mfe/) and
|
||||
// reaches its bundle with `../<bundle>.js`. At the app root that same relative path resolves ABOVE
|
||||
// the web root. Every leaf-relative reference gains the leaf's own directory prefix; absolute and
|
||||
// protocol URLs (the Mapbox CDN tags) are left alone. An inline "assetsBase":"../../assets" is not
|
||||
// rewritten and needs no rewrite: a path that climbs past the web root stops AT it, and the tree's
|
||||
// assets/ is there.
|
||||
const LEAF_DIR = `${START}/`
|
||||
const html = readFileSync(LEAF, 'utf8')
|
||||
const rebased = html.replace(
|
||||
/\b(src|href)=(["'])(?!https?:|data:|#|\/)([^"']+)\2/g,
|
||||
|
|
@ -110,7 +124,7 @@ const STUDIO_KEEP = join(APP_PUBLIC, 'studio.html')
|
|||
if (existsSync(APP_ROOT) && !existsSync(STUDIO_KEEP)) copyFileSync(APP_ROOT, STUDIO_KEEP)
|
||||
|
||||
writeFileSync(APP_ROOT, rebased.replace(/<head>/i, `<head>\n ${MARKER}`))
|
||||
console.log('APP-START-PAGE: the app now opens the UPG-100 list.')
|
||||
console.log(`APP-START-PAGE: the app now opens ${NAME}.`)
|
||||
console.log(` from ${LEAF_REL}`)
|
||||
console.log(` wrote assets/public/index.html (${referenced.length} reference(s) rebased, all present)`)
|
||||
console.log(' the studio page is kept at assets/public/studio.html')
|
||||
|
|
|
|||
|
|
@ -351,6 +351,8 @@
|
|||
"packet:ci": "node internal/build-packet-fixtures.mjs",
|
||||
"packets": "cd library/scripts/posters && VITE_API_BASE_URL=https://pray.doxa.life node packet.mjs",
|
||||
"phone": "bun run apk:debug && bun run apk:install",
|
||||
"phone:deep-research": "APP_START=doxa-maps-spa/doxa-maps-spa/app bun run phone:from-tree",
|
||||
"phone:from-tree": "bun run cap:sync && bun run app:start-page && bun run cap:verify && cd deployment/capacitor/android && ./gradlew assembleDebug && adb install -r app/build/outputs/apk/debug/app-debug.apk",
|
||||
"posters": "cd library/scripts/posters && SPARSE=1 OFFLINE=1 VITE_API_BASE_URL=https://pray.doxa.life FORMAT=3x3 node generate.mjs",
|
||||
"posters:export": "cd library/scripts/posters && node export-public.mjs",
|
||||
"posters:gallery": "cd library/scripts/posters && node build-gallery.mjs && node build-hd-gallery.mjs",
|
||||
|
|
|
|||
Loading…
Reference in a new issue