@livx.cc/appwrap 0.19.0 → 0.20.0
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/README.md +10 -3
- package/package.json +4 -2
- package/src/cli.ts +113 -160
- package/src/config.ts +125 -0
- package/templates/ci/fastlane/Fastfile +26 -5
- package/templates/ci/fastlane/Matchfile +1 -0
- package/templates/ci/github/workflows/appwrap-pr.yml +1 -1
- package/templates/ci/github/workflows/appwrap-release-android.yml +2 -2
- package/templates/ci/github/workflows/appwrap-release-ios.yml +18 -6
package/README.md
CHANGED
|
@@ -6,14 +6,21 @@ CLI that wraps any PWA into a native iOS/Android app with real native capabiliti
|
|
|
6
6
|
|
|
7
7
|
```bash
|
|
8
8
|
bun add -d @livx.cc/appwrap
|
|
9
|
-
bunx appwrap init # scaffold native/ from
|
|
9
|
+
bunx appwrap init # scaffold native/ from your config
|
|
10
10
|
bunx appwrap sync # regenerate native/ after web or config changes
|
|
11
11
|
bunx appwrap dev # live-load your dev server in the native shell
|
|
12
12
|
```
|
|
13
13
|
|
|
14
|
-
Configuration lives in `appwrap.
|
|
14
|
+
Configuration lives in a typed `appwrap.config.ts` (app id, name, icon, modules, permissions, signing) — author it with autocomplete + type-checking via `defineConfig`:
|
|
15
15
|
|
|
16
|
-
|
|
16
|
+
```ts
|
|
17
|
+
import { defineConfig } from '@livx.cc/appwrap/config';
|
|
18
|
+
export default defineConfig({ id: 'com.you.app', name: 'My App', version: '1.0.0', pwaDist: 'dist' });
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
A plain `appwrap.json` is still supported as a fallback (the CLI resolves `appwrap.config.ts` → `appwrap.config.js` → `appwrap.json`). The CLI generates and keeps `native/` in sync from a managed runtime template — you edit the template/config, never the generated output.
|
|
22
|
+
|
|
23
|
+
**Full documentation, config reference, and the managed-regeneration model:** see the [appwrap README](https://github.com/Livshitz/appwrap#readme).
|
|
17
24
|
|
|
18
25
|
## License
|
|
19
26
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@livx.cc/appwrap",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.20.0",
|
|
4
4
|
"description": "Wrap any PWA into a native app with native capabilities (appwrap runtime + @livx.cc/native-kit).",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Elya Livshitz",
|
|
@@ -28,7 +28,9 @@
|
|
|
28
28
|
"appwrap": "src/cli.ts"
|
|
29
29
|
},
|
|
30
30
|
"exports": {
|
|
31
|
-
".": "./src/
|
|
31
|
+
".": "./src/config.ts",
|
|
32
|
+
"./config": "./src/config.ts",
|
|
33
|
+
"./cli": "./src/cli.ts"
|
|
32
34
|
},
|
|
33
35
|
"files": [
|
|
34
36
|
"src",
|
package/src/cli.ts
CHANGED
|
@@ -2,115 +2,24 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* appwrap CLI v0 — scaffold a native wrapper around a built PWA.
|
|
4
4
|
*
|
|
5
|
-
* appwrap init [--config
|
|
6
|
-
* appwrap sync [--config
|
|
5
|
+
* appwrap init [--config <path>] [--out native] # from the PWA project dir
|
|
6
|
+
* appwrap sync [--config <path>] [--out native] # re-copy PWA dist into the wrapper
|
|
7
7
|
*
|
|
8
|
-
*
|
|
8
|
+
* Config (TS preferred, JSON fallback) — probed in order: appwrap.config.ts → .js → appwrap.json.
|
|
9
|
+
* Shape: { id, name, version, entry?, backgroundColor?, statusBarStyle?, pwaDist }. See config.ts.
|
|
9
10
|
*/
|
|
10
11
|
import { execFileSync } from 'child_process';
|
|
11
12
|
import { cpSync, existsSync, mkdirSync, readdirSync, readFileSync, rmSync, writeFileSync } from 'fs';
|
|
12
13
|
import { networkInterfaces, tmpdir } from 'os';
|
|
13
14
|
import { dirname, join, resolve } from 'path';
|
|
15
|
+
import { pathToFileURL } from 'url';
|
|
14
16
|
// PURE-DATA capability manifest (no NativeScript globals) — type-only import (erased at runtime);
|
|
15
17
|
// the VALUES are loaded dynamically below from the resolved runtime so the CLI works both in the
|
|
16
18
|
// monorepo and from a published tarball (where runtime/ is bundled at the package root).
|
|
17
19
|
import type * as CapManifest from '../../../runtime/app/shell/capabilities.manifest';
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
name: string;
|
|
22
|
-
version: string;
|
|
23
|
-
entry?: string;
|
|
24
|
-
backgroundColor?: string;
|
|
25
|
-
statusBarStyle?: 'light' | 'dark';
|
|
26
|
-
/** Android only (experimental). When true, the WebView draws genuinely edge-to-edge UNDER the
|
|
27
|
-
* transparent system bars (NS `androidOverflowEdge='dont-apply'`) and the real safe-area insets
|
|
28
|
-
* are injected as `--saie-*` CSS vars + native `env(safe-area-inset-*)`, so a multi-theme PWA
|
|
29
|
-
* paints the bar regions itself. Default false = bars show the page `backgroundColor` (works, but
|
|
30
|
-
* can't match a multi-theme app). iOS is always genuinely edge-to-edge. */
|
|
31
|
-
edgeToEdge?: boolean;
|
|
32
|
-
pwaDist: string;
|
|
33
|
-
/** Custom URL scheme for deep links (e.g. "hellowrap" → hellowrap://...). */
|
|
34
|
-
urlScheme?: string;
|
|
35
|
-
/** App icon source (≥512px square png). Defaults to the largest icon in the PWA manifest. */
|
|
36
|
-
icon?: string;
|
|
37
|
-
/** Loader: 'app' (default — app:// scheme, ES modules OK), 'file' (debug fallback), or 'server'
|
|
38
|
-
* (load `serverUrl` live — dev HMR over LAN or a deployed URL). `appwrap dev` sets this. */
|
|
39
|
-
loader?: 'app' | 'file' | 'server';
|
|
40
|
-
/** Live URL loaded when loader === 'server'. Set via appwrap.json or `appwrap dev --url <url>`. */
|
|
41
|
-
serverUrl?: string;
|
|
42
|
-
/** Absolute backend origin for an offline (loader:'app') PWA whose API/WebSocket calls were
|
|
43
|
-
* originally same-origin (e.g. "https://api.example.com"). Injected to the page as
|
|
44
|
-
* `window.__APPWRAP_BACKEND_ORIGIN__`; a same-origin PWA reads it to make its calls absolute.
|
|
45
|
-
* Empty/unset = same-origin (browser default), so the same web build is unaffected. */
|
|
46
|
-
backendOrigin?: string;
|
|
47
|
-
/** Backend-served STATIC assets the PWA loads via a *relative, hardcoded* URL (e.g. an SDK
|
|
48
|
-
* `<script src="/_vendor/sdk.js">` that can't be made absolute without breaking script order).
|
|
49
|
-
* Fetched from `backendOrigin` at build time and bundled into www/, so they resolve offline at
|
|
50
|
-
* app://. The asset is pinned to the build (correct for a no-OTA native app). Needs `backendOrigin`. */
|
|
51
|
-
vendorPaths?: string[];
|
|
52
|
-
/** Debug/dev mode: keeps the screen awake (no auto-lock while foreground) + makes the WebView
|
|
53
|
-
* inspectable (Safari Web Inspector / chrome://inspect) for continuous troubleshooting. `appwrap
|
|
54
|
-
* deploy` forces this on; `appwrap build` leaves it off. NEVER ship a store build with debug on. */
|
|
55
|
-
debug?: boolean;
|
|
56
|
-
/** In debug mode, the value written to `localStorage.DEBUG` at startup so the PWA's logger goes
|
|
57
|
-
* verbose (common convention — `'*'` = all, or comma-separated module names). Default `'*'`. */
|
|
58
|
-
debugLog?: string;
|
|
59
|
-
/** Apple Development Team ID for device builds (Xcode → Settings → Accounts). */
|
|
60
|
-
teamId?: string;
|
|
61
|
-
/** Path (relative to the PWA project) to a StoreKit configuration file for LOCAL IAP
|
|
62
|
-
* testing — products resolve without App Store Connect. Only applies when launched from
|
|
63
|
-
* Xcode (simulator or device-from-Xcode), not a standalone devicectl sideload. */
|
|
64
|
-
storekitConfig?: string;
|
|
65
|
-
/** Permission usage strings, keyed by domain. Only listed ones are stamped
|
|
66
|
-
* (iOS: Info.plist usage string; Android: <uses-permission>). 'contacts' has no
|
|
67
|
-
* iOS key (CNContactPicker needs none) — it only stamps Android READ_CONTACTS. */
|
|
68
|
-
permissions?: Partial<
|
|
69
|
-
Record<'location' | 'photos' | 'camera' | 'microphone' | 'faceid' | 'calendar' | 'contacts' | 'motion', string>
|
|
70
|
-
>;
|
|
71
|
-
/** Monotonic build identifier. Stores reject a re-upload unless this is HIGHER than the last:
|
|
72
|
-
* iOS `CFBundleVersion`, Android `versionCode` (the marketing `version` stays the user-facing
|
|
73
|
-
* string). Default: an integer derived from `version` (0.2.1 → 201). Set explicitly from a CI
|
|
74
|
-
* run number for fleet builds of the same marketing version. */
|
|
75
|
-
buildNumber?: string | number;
|
|
76
|
-
/** iOS export-compliance. `ITSAppUsesNonExemptEncryption` — stamped `false` by default (skips the
|
|
77
|
-
* per-upload prompt). Set `true` only if the app uses non-exempt encryption. */
|
|
78
|
-
usesNonExemptEncryption?: boolean;
|
|
79
|
-
/** Pure-native escape hatch: a directory (relative to the PWA project) whose contents are copied
|
|
80
|
-
* OVER the generated wrapper after stamping — for legacy/custom native code the declarative config
|
|
81
|
-
* can't express. Default `'appwrap.overrides'`; applied only if it exists. */
|
|
82
|
-
overrides?: string;
|
|
83
|
-
/** Reserved — appwrap plugins (npm packages contributing a kit module + native handlers + config).
|
|
84
|
-
* Parsed today; full native composition lands with the plugin contract (see framework-extensibility). */
|
|
85
|
-
plugins?: string[];
|
|
86
|
-
/** Opt-in capability allow-list (built-in modules — see capabilities.manifest.ts). When PRESENT,
|
|
87
|
-
* only the listed capabilities (plus always-on core) are advertised, permissioned, and — for
|
|
88
|
-
* modules that own their handler file (e.g. health) — compiled into the shell. Their permissions,
|
|
89
|
-
* background modes and native deps are collected from each module's self-contained manifest entry
|
|
90
|
-
* (the per-app `permissions{}` map only OVERRIDES the default usage copy). When ABSENT, every
|
|
91
|
-
* capability is active and permissions come solely from `permissions{}` (pre-modules behavior). */
|
|
92
|
-
modules?: string[];
|
|
93
|
-
/** Remote push (APNs/FCM). Off unless set — gating matters: an `aps-environment` entitlement on a
|
|
94
|
-
* team that can't hold the Push capability (e.g. a personal team) BREAKS code signing, and the
|
|
95
|
-
* handshake should honestly report `push: 'none'` on an un-provisioned build. The kit returns a raw
|
|
96
|
-
* token; SENDING is your backend's job (provider-agnostic). */
|
|
97
|
-
push?: {
|
|
98
|
-
/** Master switch for the push lane. */
|
|
99
|
-
enabled?: boolean;
|
|
100
|
-
/** Per-platform gates (default true when `enabled`). Split because the two platforms have
|
|
101
|
-
* independent prerequisites: iOS needs the `aps-environment` entitlement (a PAID Apple team —
|
|
102
|
-
* a personal team can't hold it, and stamping it would break signing), Android needs FCM +
|
|
103
|
-
* google-services.json. e.g. `{ enabled:true, ios:false, android:true }` ships Android push
|
|
104
|
-
* while keeping a personal-team iOS build signable. */
|
|
105
|
-
ios?: boolean;
|
|
106
|
-
android?: boolean;
|
|
107
|
-
/** iOS APNs environment in the entitlement: 'development' (debug/TestFlight builds) or
|
|
108
|
-
* 'production' (App Store). Default 'development'. */
|
|
109
|
-
apsEnvironment?: 'development' | 'production';
|
|
110
|
-
/** Path (relative to the PWA project) to the Firebase `google-services.json` for Android FCM. */
|
|
111
|
-
googleServicesJson?: string;
|
|
112
|
-
};
|
|
113
|
-
}
|
|
20
|
+
// Config shape lives in its own import-safe module so a `appwrap.config.ts` file can import the
|
|
21
|
+
// type + `defineConfig` helper without pulling in (and running) the CLI dispatch.
|
|
22
|
+
import type { AppwrapConfig } from './config';
|
|
114
23
|
|
|
115
24
|
/** Marketing version → a monotonic integer build (0.2.1 → 201; 1.4.12 → 10412). Stable & increasing
|
|
116
25
|
* across semver bumps so store re-uploads are always accepted without a manual bump. */
|
|
@@ -253,7 +162,7 @@ function generateModuleArtifacts(outDir: string, req: NativeReqs): void {
|
|
|
253
162
|
const shell = join(outDir, 'app/shell');
|
|
254
163
|
writeFileSync(
|
|
255
164
|
join(shell, 'active-modules.generated.ts'),
|
|
256
|
-
`/** Generated by \`appwrap\` from appwrap
|
|
165
|
+
`/** Generated by \`appwrap\` from the appwrap config \`modules\`. Do not edit. */\n` +
|
|
257
166
|
`export const ACTIVE_MODULE_NAMES: string[] = ${JSON.stringify(req.activeOptIn)};\n`
|
|
258
167
|
);
|
|
259
168
|
|
|
@@ -370,15 +279,39 @@ function loadManifest(cwd: string, cfg: AppwrapConfig): Record<string, any> | nu
|
|
|
370
279
|
return null;
|
|
371
280
|
}
|
|
372
281
|
|
|
373
|
-
|
|
374
|
-
|
|
282
|
+
/** Config filenames probed (in order) when `--config` is not passed. TS is preferred (typed,
|
|
283
|
+
* autocomplete via `defineConfig`); `.js` then `.json` are supported as fallbacks. */
|
|
284
|
+
const CONFIG_CANDIDATES = ['appwrap.config.ts', 'appwrap.config.js', 'appwrap.json'] as const;
|
|
285
|
+
|
|
286
|
+
/** Load a `.ts`/`.js`/`.json` config. TS/JS are imported (Bun runs them natively — no transpile
|
|
287
|
+
* step) and may `export default` (or a named `config`); JSON is parsed. Returns the raw object. */
|
|
288
|
+
async function readConfigFile(configPath: string): Promise<AppwrapConfig> {
|
|
289
|
+
if (configPath.endsWith('.json')) {
|
|
290
|
+
return JSON.parse(readFileSync(configPath, 'utf8')) as AppwrapConfig;
|
|
291
|
+
}
|
|
292
|
+
// .ts / .js — dynamic import (Bun runs it natively). Each CLI command is its own process, so the
|
|
293
|
+
// ESM module cache never outlives a single run.
|
|
294
|
+
const mod = await import(pathToFileURL(configPath).href);
|
|
295
|
+
const cfg = mod.default ?? mod.config;
|
|
296
|
+
if (!cfg || typeof cfg !== 'object') {
|
|
297
|
+
console.error(`✖ ${configPath} must \`export default\` (or export \`config\`) an appwrap config object.`);
|
|
298
|
+
process.exit(1);
|
|
299
|
+
}
|
|
300
|
+
return cfg as AppwrapConfig;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
async function loadConfig(cwd: string, flags: Record<string, string>): Promise<AppwrapConfig> {
|
|
304
|
+
// Explicit --config wins; otherwise probe ts → js → json (TS preferred).
|
|
305
|
+
const configPath = flags.config
|
|
306
|
+
? resolve(cwd, flags.config)
|
|
307
|
+
: (CONFIG_CANDIDATES.map((f) => resolve(cwd, f)).find(existsSync) ?? resolve(cwd, CONFIG_CANDIDATES[0]));
|
|
375
308
|
if (!existsSync(configPath)) {
|
|
376
|
-
console.error(`✖ Config not found
|
|
309
|
+
console.error(`✖ Config not found — looked for ${CONFIG_CANDIDATES.join(' / ')} in ${cwd}`);
|
|
377
310
|
process.exit(1);
|
|
378
311
|
}
|
|
379
|
-
const cfg =
|
|
312
|
+
const cfg = await readConfigFile(configPath);
|
|
380
313
|
|
|
381
|
-
// Manifest as source: appwrap
|
|
314
|
+
// Manifest as source: the appwrap config wins, the PWA manifest fills the gaps, template default last.
|
|
382
315
|
// (DRY single-source — devs don't re-type identity already declared in the manifest.)
|
|
383
316
|
if (cfg.pwaDist) {
|
|
384
317
|
const mf = loadManifest(cwd, cfg);
|
|
@@ -390,7 +323,7 @@ function loadConfig(cwd: string, flags: Record<string, string>): AppwrapConfig {
|
|
|
390
323
|
|
|
391
324
|
for (const key of ['id', 'name', 'version', 'pwaDist'] as const) {
|
|
392
325
|
if (!cfg[key]) {
|
|
393
|
-
console.error(`✖
|
|
326
|
+
console.error(`✖ config missing required field: ${key}` + (key === 'name' ? ' (and no name/short_name in the PWA manifest)' : ''));
|
|
394
327
|
process.exit(1);
|
|
395
328
|
}
|
|
396
329
|
}
|
|
@@ -399,7 +332,7 @@ function loadConfig(cwd: string, flags: Record<string, string>): AppwrapConfig {
|
|
|
399
332
|
|
|
400
333
|
function stampShellConfig(outDir: string, cfg: AppwrapConfig): void {
|
|
401
334
|
const content = `/**
|
|
402
|
-
* Shell config — stamped by \`appwrap init\`/\`sync\` from appwrap.
|
|
335
|
+
* Shell config — stamped by \`appwrap init\`/\`sync\` from the appwrap config. Do not edit.
|
|
403
336
|
*/
|
|
404
337
|
export const SHELL_CONFIG = {
|
|
405
338
|
appId: ${JSON.stringify(cfg.id)},
|
|
@@ -499,7 +432,7 @@ function stampStoreKit(cwd: string, outDir: string, cfg: AppwrapConfig): void {
|
|
|
499
432
|
if (!cfg.storekitConfig) return;
|
|
500
433
|
const source = resolve(cwd, cfg.storekitConfig);
|
|
501
434
|
if (!existsSync(source)) {
|
|
502
|
-
console.warn(`⚠
|
|
435
|
+
console.warn(`⚠ config \`storekitConfig\` not found: ${source} — skipping StoreKit wiring`);
|
|
503
436
|
return;
|
|
504
437
|
}
|
|
505
438
|
const base = source.split('/').pop()!;
|
|
@@ -572,7 +505,7 @@ function stampPush(cwd: string, outDir: string, cfg: AppwrapConfig): void {
|
|
|
572
505
|
if (fcmVals) console.log(` push ← Android FCM wired (firebase resources for ${fcmVals.project_id}, no plugin)`);
|
|
573
506
|
else console.warn(`⚠ Could not parse ${src} — skipping Android FCM`);
|
|
574
507
|
} else {
|
|
575
|
-
console.warn(`⚠
|
|
508
|
+
console.warn(`⚠ config \`push.googleServicesJson\` not found: ${src} — skipping Android FCM`);
|
|
576
509
|
}
|
|
577
510
|
}
|
|
578
511
|
stampAndroidFcm(outDir, fcmVals);
|
|
@@ -694,7 +627,7 @@ function findIconSource(cwd: string, cfg: AppwrapConfig): string | null {
|
|
|
694
627
|
if (cfg.icon) {
|
|
695
628
|
const p = resolve(cwd, cfg.icon);
|
|
696
629
|
if (existsSync(p)) return p;
|
|
697
|
-
console.warn(`⚠
|
|
630
|
+
console.warn(`⚠ config \`icon\` not found: ${p}`);
|
|
698
631
|
return null;
|
|
699
632
|
}
|
|
700
633
|
const dist = resolve(cwd, cfg.pwaDist);
|
|
@@ -732,7 +665,7 @@ function findMaskableSource(cwd: string, cfg: AppwrapConfig): string | null {
|
|
|
732
665
|
function generateIcons(cwd: string, outDir: string, cfg: AppwrapConfig): void {
|
|
733
666
|
const source = findIconSource(cwd, cfg);
|
|
734
667
|
if (!source) {
|
|
735
|
-
console.warn('⚠ No app icon source found (manifest icons or
|
|
668
|
+
console.warn('⚠ No app icon source found (manifest icons or config `icon`) — keeping template icons');
|
|
736
669
|
return;
|
|
737
670
|
}
|
|
738
671
|
const probe = (prop: string) =>
|
|
@@ -862,7 +795,7 @@ function copyPwa(cwd: string, outDir: string, cfg: AppwrapConfig): void {
|
|
|
862
795
|
function vendorBackendAssets(www: string, cfg: AppwrapConfig): void {
|
|
863
796
|
if (!cfg.vendorPaths?.length) return;
|
|
864
797
|
if (!cfg.backendOrigin) {
|
|
865
|
-
console.error('✖ vendorPaths requires backendOrigin in appwrap
|
|
798
|
+
console.error('✖ vendorPaths requires backendOrigin in the appwrap config');
|
|
866
799
|
process.exit(1);
|
|
867
800
|
}
|
|
868
801
|
const origin = cfg.backendOrigin.replace(/\/+$/, '');
|
|
@@ -912,7 +845,7 @@ function gitRoot(start: string): string {
|
|
|
912
845
|
}
|
|
913
846
|
|
|
914
847
|
/** Emit CI scaffolding (GH Actions → git repo root, fastlane → native/). Never overwrites. */
|
|
915
|
-
function copyCiTemplates(cwd: string, outDir: string): void {
|
|
848
|
+
function copyCiTemplates(cwd: string, outDir: string, cfg: AppwrapConfig): void {
|
|
916
849
|
if (!existsSync(CI_TEMPLATE_DIR)) return;
|
|
917
850
|
// GitHub only reads `.github/workflows` at the REPO ROOT — in a monorepo, writing it under the
|
|
918
851
|
// package cwd (e.g. packages/app/.github) is dead config and regenerates a stray workflow each init.
|
|
@@ -924,7 +857,18 @@ function copyCiTemplates(cwd: string, outDir: string): void {
|
|
|
924
857
|
mkdirSync(to, { recursive: true });
|
|
925
858
|
cpSync(from, to, { recursive: true, force: false, errorOnExist: false });
|
|
926
859
|
}
|
|
927
|
-
|
|
860
|
+
// Stamp the app id + team into the emitted fastlane (signing needs them; the templates ship
|
|
861
|
+
// `__APP_ID__`/`__TEAM_ID__` placeholders). Idempotent: re-init finds no placeholders → no-op.
|
|
862
|
+
const fastlaneDir = join(outDir, 'fastlane');
|
|
863
|
+
for (const file of ['Fastfile', 'Matchfile']) {
|
|
864
|
+
const p = join(fastlaneDir, file);
|
|
865
|
+
if (!existsSync(p)) continue;
|
|
866
|
+
const stamped = readFileSync(p, 'utf8')
|
|
867
|
+
.replaceAll('__APP_ID__', cfg.id)
|
|
868
|
+
.replaceAll('__TEAM_ID__', cfg.teamId ?? '');
|
|
869
|
+
writeFileSync(p, stamped);
|
|
870
|
+
}
|
|
871
|
+
console.log(' ci ← GH Actions (.github/workflows) + fastlane (native/fastlane, signing stamped) — see secrets contract in workflow headers');
|
|
928
872
|
}
|
|
929
873
|
|
|
930
874
|
/**
|
|
@@ -938,7 +882,7 @@ function copyCiTemplates(cwd: string, outDir: string): void {
|
|
|
938
882
|
function regenerateCore(cwd: string, outDir: string, cfg: AppwrapConfig, opts: { firstRun?: boolean } = {}): void {
|
|
939
883
|
const req = nativeReqs(cfg);
|
|
940
884
|
if (opts.firstRun && !req.explicit) {
|
|
941
|
-
console.log(' ℹ no `modules` in appwrap
|
|
885
|
+
console.log(' ℹ no `modules` in the appwrap config → all capabilities active. Declare `modules` to shrink the store build (strip unused handlers/perms).');
|
|
942
886
|
}
|
|
943
887
|
cpSync(TEMPLATE_DIR, outDir, {
|
|
944
888
|
recursive: true,
|
|
@@ -966,8 +910,8 @@ function regenerateCore(cwd: string, outDir: string, cfg: AppwrapConfig, opts: {
|
|
|
966
910
|
copyPwa(cwd, outDir, cfg);
|
|
967
911
|
}
|
|
968
912
|
|
|
969
|
-
function init(cwd: string, flags: Record<string, string>): void {
|
|
970
|
-
const cfg = loadConfig(cwd, flags);
|
|
913
|
+
async function init(cwd: string, flags: Record<string, string>): Promise<void> {
|
|
914
|
+
const cfg = await loadConfig(cwd, flags);
|
|
971
915
|
const outDir = resolve(cwd, flags.out ?? 'native');
|
|
972
916
|
|
|
973
917
|
if (!existsSync(TEMPLATE_DIR)) {
|
|
@@ -992,7 +936,7 @@ function init(cwd: string, flags: Record<string, string>): void {
|
|
|
992
936
|
console.log(`🎁 appwrap init → ${outDir}`);
|
|
993
937
|
mkdirSync(outDir, { recursive: true });
|
|
994
938
|
regenerateCore(cwd, outDir, cfg, { firstRun: true });
|
|
995
|
-
copyCiTemplates(cwd, outDir); // first-time scaffold (never overwrites)
|
|
939
|
+
copyCiTemplates(cwd, outDir, cfg); // first-time scaffold (never overwrites)
|
|
996
940
|
writeFileSync(join(outDir, '.gitignore'), 'node_modules/\nplatforms/\nhooks/\n');
|
|
997
941
|
applyOverrides(cwd, outDir, cfg); // escape hatch — last, so custom native code wins
|
|
998
942
|
stampVersionManifest(outDir, cfg); // provenance — also marks the dir appwrap-managed
|
|
@@ -1002,8 +946,8 @@ function init(cwd: string, flags: Record<string, string>): void {
|
|
|
1002
946
|
// `sync` = the same regenerate as `init`, minus the first-time guard/scaffold. It is a TRUE refresh from
|
|
1003
947
|
// source (shell + config + PWA), so runtime/config edits never silently lag behind. `native/` is
|
|
1004
948
|
// disposable; re-copying the shell costs ~ms (the real cost is the later `ns build`, which both share).
|
|
1005
|
-
function sync(cwd: string, flags: Record<string, string>): void {
|
|
1006
|
-
const cfg = loadConfig(cwd, flags);
|
|
949
|
+
async function sync(cwd: string, flags: Record<string, string>): Promise<void> {
|
|
950
|
+
const cfg = await loadConfig(cwd, flags);
|
|
1007
951
|
const outDir = resolve(cwd, flags.out ?? 'native');
|
|
1008
952
|
if (!existsSync(outDir)) {
|
|
1009
953
|
console.error(`✖ Wrapper not found at ${outDir} — run \`appwrap init\` first`);
|
|
@@ -1028,8 +972,8 @@ function lanIp(): string | null {
|
|
|
1028
972
|
/** `appwrap dev` — point the existing wrapper at a LIVE url (loader 'server') instead of bundled www.
|
|
1029
973
|
* Dev runs their own web server (vite host:true) or a deployed URL; this just stamps the shell config.
|
|
1030
974
|
* `--url <url>` explicit; else http://<lan-ip>:<port> (default 5173). Re-run `appwrap sync`/`init` to revert. */
|
|
1031
|
-
function dev(cwd: string, flags: Record<string, string>): void {
|
|
1032
|
-
const cfg = loadConfig(cwd, flags);
|
|
975
|
+
async function dev(cwd: string, flags: Record<string, string>): Promise<void> {
|
|
976
|
+
const cfg = await loadConfig(cwd, flags);
|
|
1033
977
|
const outDir = resolve(cwd, flags.out ?? 'native');
|
|
1034
978
|
if (!existsSync(outDir)) {
|
|
1035
979
|
console.error(`✖ Wrapper not found at ${outDir} — run \`appwrap init\` first`);
|
|
@@ -1060,12 +1004,12 @@ function dev(cwd: string, flags: Record<string, string>): void {
|
|
|
1060
1004
|
/** `appwrap build <ios|android> [--release] [--aab]` — store-readiness build path. Re-stamps config,
|
|
1061
1005
|
* re-copies the PWA, then delegates the actual compile to NativeScript with the right flags. Release
|
|
1062
1006
|
* Android signing comes from env (APPWRAP_ANDROID_KEYSTORE[_PASSWORD|_ALIAS|_ALIAS_PASSWORD]) — secrets
|
|
1063
|
-
* never live in appwrap.
|
|
1007
|
+
* never live in the appwrap config. iOS distribution signing/upload is the fastlane release lane's job (the
|
|
1064
1008
|
* cicd templates); `--release` here just builds the Release config for the device. */
|
|
1065
|
-
function build(cwd: string, flags: Record<string, string>, positionals: string[]): void {
|
|
1009
|
+
async function build(cwd: string, flags: Record<string, string>, positionals: string[]): Promise<void> {
|
|
1066
1010
|
const platform = positionals[0];
|
|
1067
1011
|
if (platform !== 'ios' && platform !== 'android') {
|
|
1068
|
-
console.error('Usage: appwrap build <ios|android> [--release] [--aab] [--config
|
|
1012
|
+
console.error('Usage: appwrap build <ios|android> [--release] [--aab] [--config <path>] [--out native]');
|
|
1069
1013
|
process.exit(1);
|
|
1070
1014
|
}
|
|
1071
1015
|
const outDir = resolve(cwd, flags.out ?? 'native');
|
|
@@ -1073,8 +1017,8 @@ function build(cwd: string, flags: Record<string, string>, positionals: string[]
|
|
|
1073
1017
|
console.error(`✖ Wrapper not found at ${outDir} — run \`appwrap init\` first`);
|
|
1074
1018
|
process.exit(1);
|
|
1075
1019
|
}
|
|
1076
|
-
// Make sure the wrapper reflects the latest config + PWA before compiling (also validates
|
|
1077
|
-
sync(cwd, flags);
|
|
1020
|
+
// Make sure the wrapper reflects the latest config + PWA before compiling (also validates the config).
|
|
1021
|
+
await sync(cwd, flags);
|
|
1078
1022
|
|
|
1079
1023
|
const release = 'release' in flags;
|
|
1080
1024
|
const args = ['build', platform];
|
|
@@ -1160,13 +1104,13 @@ function pickDevice(devices: DeviceInfo[], explicitId?: string): DeviceInfo {
|
|
|
1160
1104
|
/** `appwrap deploy ios [--device <id|name>] [--no-launch]` — build for device, auto-pick the
|
|
1161
1105
|
* connected phone (USB or network; prompts if several), install + launch. Debug build (no
|
|
1162
1106
|
* distribution signing) — for testing on your own device. Run the PWA build first (or via the script). */
|
|
1163
|
-
function deploy(cwd: string, flags: Record<string, string>, positionals: string[]): void {
|
|
1107
|
+
async function deploy(cwd: string, flags: Record<string, string>, positionals: string[]): Promise<void> {
|
|
1164
1108
|
const platform = positionals[0];
|
|
1165
1109
|
if (platform !== 'ios') {
|
|
1166
1110
|
console.error('Usage: appwrap deploy ios [--device <id|name>] [--no-launch] (android: use `ns run android`)');
|
|
1167
1111
|
process.exit(1);
|
|
1168
1112
|
}
|
|
1169
|
-
const cfg = loadConfig(cwd, flags);
|
|
1113
|
+
const cfg = await loadConfig(cwd, flags);
|
|
1170
1114
|
const outDir = resolve(cwd, flags.out ?? 'native');
|
|
1171
1115
|
if (!existsSync(outDir)) {
|
|
1172
1116
|
console.error(`✖ Wrapper not found at ${outDir} — run \`appwrap init\` first`);
|
|
@@ -1175,9 +1119,9 @@ function deploy(cwd: string, flags: Record<string, string>, positionals: string[
|
|
|
1175
1119
|
// Pick the device up front so we fail fast before a long build if nothing's connected.
|
|
1176
1120
|
const device = pickDevice(listIosDevices(), flags.device || undefined);
|
|
1177
1121
|
|
|
1178
|
-
sync(cwd, flags); // re-stamp config + copy latest PWA dist (+ vendor backend assets)
|
|
1122
|
+
await sync(cwd, flags); // re-stamp config + copy latest PWA dist (+ vendor backend assets)
|
|
1179
1123
|
// Dev deploy → debug mode: keep-awake + WebView inspector for continuous troubleshooting.
|
|
1180
|
-
stampShellConfig(outDir, { ...
|
|
1124
|
+
stampShellConfig(outDir, { ...cfg, debug: true });
|
|
1181
1125
|
console.log('▶ ns build ios --for-device (debug: keep-awake + inspector on)');
|
|
1182
1126
|
execFileSync('ns', ['build', 'ios', '--for-device'], { cwd: outDir, stdio: 'inherit' });
|
|
1183
1127
|
|
|
@@ -1243,13 +1187,13 @@ function libimobiledeviceUdid(): { udid: string; network: boolean } | null {
|
|
|
1243
1187
|
* `devicectl device copy`. DEFAULT: watch (poll the file ~every 3s, print new lines). `--once`:
|
|
1244
1188
|
* one snapshot. `--native`: the OS-level app syslog firehose via idevicesyslog (native crashes; USB).
|
|
1245
1189
|
* Headless-friendly: redirect to a file and read it. */
|
|
1246
|
-
function logs(cwd: string, flags: Record<string, string>, positionals: string[]): void {
|
|
1190
|
+
async function logs(cwd: string, flags: Record<string, string>, positionals: string[]): Promise<void> {
|
|
1247
1191
|
const platform = positionals[0] ?? 'ios';
|
|
1248
1192
|
if (platform !== 'ios') {
|
|
1249
1193
|
console.error('Usage: appwrap logs ios [--once] [--native] [--device <id|name>]');
|
|
1250
1194
|
process.exit(1);
|
|
1251
1195
|
}
|
|
1252
|
-
const cfg = loadConfig(cwd, flags);
|
|
1196
|
+
const cfg = await loadConfig(cwd, flags);
|
|
1253
1197
|
|
|
1254
1198
|
if ('native' in flags) {
|
|
1255
1199
|
const li = libimobiledeviceUdid();
|
|
@@ -1298,31 +1242,40 @@ function logs(cwd: string, flags: Record<string, string>, positionals: string[])
|
|
|
1298
1242
|
}
|
|
1299
1243
|
}
|
|
1300
1244
|
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1245
|
+
/** CLI dispatch. Guarded by `import.meta.main` so importing this module (e.g. for the `AppwrapConfig`
|
|
1246
|
+
* type via the package entry) doesn't run a command. */
|
|
1247
|
+
async function main(): Promise<void> {
|
|
1248
|
+
const { command, flags, positionals } = parseArgs(process.argv.slice(2));
|
|
1249
|
+
const cwd = process.cwd();
|
|
1250
|
+
|
|
1251
|
+
switch (command) {
|
|
1252
|
+
case 'init':
|
|
1253
|
+
await init(cwd, flags);
|
|
1254
|
+
break;
|
|
1255
|
+
case 'sync':
|
|
1256
|
+
await sync(cwd, flags);
|
|
1257
|
+
break;
|
|
1258
|
+
case 'dev':
|
|
1259
|
+
await dev(cwd, flags);
|
|
1260
|
+
break;
|
|
1261
|
+
case 'build':
|
|
1262
|
+
await build(cwd, flags, positionals);
|
|
1263
|
+
break;
|
|
1264
|
+
case 'deploy':
|
|
1265
|
+
await deploy(cwd, flags, positionals);
|
|
1266
|
+
break;
|
|
1267
|
+
case 'logs':
|
|
1268
|
+
await logs(cwd, flags, positionals);
|
|
1269
|
+
break;
|
|
1270
|
+
default:
|
|
1271
|
+
console.log('Usage: appwrap <init|sync|dev|build|deploy|logs> [--config <path>] [--out native]\n' +
|
|
1272
|
+
' config: appwrap.config.ts (preferred) → .js → appwrap.json\n' +
|
|
1273
|
+
' build <ios|android> [--release] [--aab] deploy ios [--device <id|name>] [--no-launch]\n' +
|
|
1274
|
+
' logs ios [--once] [--native] dev [--url <url> | --port <p>]');
|
|
1275
|
+
process.exit(command ? 1 : 0);
|
|
1276
|
+
}
|
|
1277
|
+
}
|
|
1278
|
+
|
|
1279
|
+
if (import.meta.main) {
|
|
1280
|
+
main();
|
|
1328
1281
|
}
|
package/src/config.ts
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* appwrap config — the typed shape of an app's wrapper config + the `defineConfig` helper.
|
|
3
|
+
*
|
|
4
|
+
* This module is import-safe (no side effects, no CLI dispatch), so it can be imported from a
|
|
5
|
+
* TypeScript config file to get full autocomplete + type-checking:
|
|
6
|
+
*
|
|
7
|
+
* // appwrap.config.ts
|
|
8
|
+
* import { defineConfig } from '@livx.cc/appwrap/config';
|
|
9
|
+
* export default defineConfig({
|
|
10
|
+
* id: 'com.example.app',
|
|
11
|
+
* name: 'Example',
|
|
12
|
+
* version: '1.0.0',
|
|
13
|
+
* pwaDist: 'dist',
|
|
14
|
+
* });
|
|
15
|
+
*
|
|
16
|
+
* The CLI resolves `appwrap.config.ts` → `appwrap.config.js` → `appwrap.json` (TS preferred,
|
|
17
|
+
* JSON still supported as a fallback). See `loadConfig` in cli.ts.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
export interface AppwrapConfig {
|
|
21
|
+
id: string;
|
|
22
|
+
name: string;
|
|
23
|
+
version: string;
|
|
24
|
+
entry?: string;
|
|
25
|
+
backgroundColor?: string;
|
|
26
|
+
statusBarStyle?: 'light' | 'dark';
|
|
27
|
+
/** Android only (experimental). When true, the WebView draws genuinely edge-to-edge UNDER the
|
|
28
|
+
* transparent system bars (NS `androidOverflowEdge='dont-apply'`) and the real safe-area insets
|
|
29
|
+
* are injected as `--saie-*` CSS vars + native `env(safe-area-inset-*)`, so a multi-theme PWA
|
|
30
|
+
* paints the bar regions itself. Default false = bars show the page `backgroundColor` (works, but
|
|
31
|
+
* can't match a multi-theme app). iOS is always genuinely edge-to-edge. */
|
|
32
|
+
edgeToEdge?: boolean;
|
|
33
|
+
pwaDist: string;
|
|
34
|
+
/** Custom URL scheme for deep links (e.g. "hellowrap" → hellowrap://...). */
|
|
35
|
+
urlScheme?: string;
|
|
36
|
+
/** App icon source (≥512px square png). Defaults to the largest icon in the PWA manifest. */
|
|
37
|
+
icon?: string;
|
|
38
|
+
/** Loader: 'app' (default — app:// scheme, ES modules OK), 'file' (debug fallback), or 'server'
|
|
39
|
+
* (load `serverUrl` live — dev HMR over LAN or a deployed URL). `appwrap dev` sets this. */
|
|
40
|
+
loader?: 'app' | 'file' | 'server';
|
|
41
|
+
/** Live URL loaded when loader === 'server'. Set via config or `appwrap dev --url <url>`. */
|
|
42
|
+
serverUrl?: string;
|
|
43
|
+
/** Absolute backend origin for an offline (loader:'app') PWA whose API/WebSocket calls were
|
|
44
|
+
* originally same-origin (e.g. "https://api.example.com"). Injected to the page as
|
|
45
|
+
* `window.__APPWRAP_BACKEND_ORIGIN__`; a same-origin PWA reads it to make its calls absolute.
|
|
46
|
+
* Empty/unset = same-origin (browser default), so the same web build is unaffected. */
|
|
47
|
+
backendOrigin?: string;
|
|
48
|
+
/** Backend-served STATIC assets the PWA loads via a *relative, hardcoded* URL (e.g. an SDK
|
|
49
|
+
* `<script src="/_vendor/sdk.js">` that can't be made absolute without breaking script order).
|
|
50
|
+
* Fetched from `backendOrigin` at build time and bundled into www/, so they resolve offline at
|
|
51
|
+
* app://. The asset is pinned to the build (correct for a no-OTA native app). Needs `backendOrigin`. */
|
|
52
|
+
vendorPaths?: string[];
|
|
53
|
+
/** Debug/dev mode: keeps the screen awake (no auto-lock while foreground) + makes the WebView
|
|
54
|
+
* inspectable (Safari Web Inspector / chrome://inspect) for continuous troubleshooting. `appwrap
|
|
55
|
+
* deploy` forces this on; `appwrap build` leaves it off. NEVER ship a store build with debug on. */
|
|
56
|
+
debug?: boolean;
|
|
57
|
+
/** In debug mode, the value written to `localStorage.DEBUG` at startup so the PWA's logger goes
|
|
58
|
+
* verbose (common convention — `'*'` = all, or comma-separated module names). Default `'*'`. */
|
|
59
|
+
debugLog?: string;
|
|
60
|
+
/** Apple Development Team ID for device builds (Xcode → Settings → Accounts). */
|
|
61
|
+
teamId?: string;
|
|
62
|
+
/** Path (relative to the PWA project) to a StoreKit configuration file for LOCAL IAP
|
|
63
|
+
* testing — products resolve without App Store Connect. Only applies when launched from
|
|
64
|
+
* Xcode (simulator or device-from-Xcode), not a standalone devicectl sideload. */
|
|
65
|
+
storekitConfig?: string;
|
|
66
|
+
/** Permission usage strings, keyed by domain. Only listed ones are stamped
|
|
67
|
+
* (iOS: Info.plist usage string; Android: <uses-permission>). 'contacts' has no
|
|
68
|
+
* iOS key (CNContactPicker needs none) — it only stamps Android READ_CONTACTS. */
|
|
69
|
+
permissions?: Partial<
|
|
70
|
+
Record<'location' | 'photos' | 'camera' | 'microphone' | 'faceid' | 'calendar' | 'contacts' | 'motion', string>
|
|
71
|
+
>;
|
|
72
|
+
/** Monotonic build identifier. Stores reject a re-upload unless this is HIGHER than the last:
|
|
73
|
+
* iOS `CFBundleVersion`, Android `versionCode` (the marketing `version` stays the user-facing
|
|
74
|
+
* string). Default: an integer derived from `version` (0.2.1 → 201). Set explicitly from a CI
|
|
75
|
+
* run number for fleet builds of the same marketing version. */
|
|
76
|
+
buildNumber?: string | number;
|
|
77
|
+
/** iOS export-compliance. `ITSAppUsesNonExemptEncryption` — stamped `false` by default (skips the
|
|
78
|
+
* per-upload prompt). Set `true` only if the app uses non-exempt encryption. */
|
|
79
|
+
usesNonExemptEncryption?: boolean;
|
|
80
|
+
/** Pure-native escape hatch: a directory (relative to the PWA project) whose contents are copied
|
|
81
|
+
* OVER the generated wrapper after stamping — for legacy/custom native code the declarative config
|
|
82
|
+
* can't express. Default `'appwrap.overrides'`; applied only if it exists. */
|
|
83
|
+
overrides?: string;
|
|
84
|
+
/** Reserved — appwrap plugins (npm packages contributing a kit module + native handlers + config).
|
|
85
|
+
* Parsed today; full native composition lands with the plugin contract (see framework-extensibility). */
|
|
86
|
+
plugins?: string[];
|
|
87
|
+
/** Opt-in capability allow-list (built-in modules — see capabilities.manifest.ts). When PRESENT,
|
|
88
|
+
* only the listed capabilities (plus always-on core) are advertised, permissioned, and — for
|
|
89
|
+
* modules that own their handler file (e.g. health) — compiled into the shell. Their permissions,
|
|
90
|
+
* background modes and native deps are collected from each module's self-contained manifest entry
|
|
91
|
+
* (the per-app `permissions{}` map only OVERRIDES the default usage copy). When ABSENT, every
|
|
92
|
+
* capability is active and permissions come solely from `permissions{}` (pre-modules behavior). */
|
|
93
|
+
modules?: string[];
|
|
94
|
+
/** Remote push (APNs/FCM). Off unless set — gating matters: an `aps-environment` entitlement on a
|
|
95
|
+
* team that can't hold the Push capability (e.g. a personal team) BREAKS code signing, and the
|
|
96
|
+
* handshake should honestly report `push: 'none'` on an un-provisioned build. The kit returns a raw
|
|
97
|
+
* token; SENDING is your backend's job (provider-agnostic). */
|
|
98
|
+
push?: {
|
|
99
|
+
/** Master switch for the push lane. */
|
|
100
|
+
enabled?: boolean;
|
|
101
|
+
/** Per-platform gates (default true when `enabled`). Split because the two platforms have
|
|
102
|
+
* independent prerequisites: iOS needs the `aps-environment` entitlement (a PAID Apple team —
|
|
103
|
+
* a personal team can't hold it, and stamping it would break signing), Android needs FCM +
|
|
104
|
+
* google-services.json. e.g. `{ enabled:true, ios:false, android:true }` ships Android push
|
|
105
|
+
* while keeping a personal-team iOS build signable. */
|
|
106
|
+
ios?: boolean;
|
|
107
|
+
android?: boolean;
|
|
108
|
+
/** iOS APNs environment in the entitlement: 'development' (debug/TestFlight builds) or
|
|
109
|
+
* 'production' (App Store). Default 'development'. */
|
|
110
|
+
apsEnvironment?: 'development' | 'production';
|
|
111
|
+
/** Path (relative to the PWA project) to the Firebase `google-services.json` for Android FCM. */
|
|
112
|
+
googleServicesJson?: string;
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Identity helper for a TypeScript config file. Does nothing at runtime — it exists purely so your
|
|
118
|
+
* editor type-checks the object and offers autocomplete against {@link AppwrapConfig}.
|
|
119
|
+
*
|
|
120
|
+
* import { defineConfig } from '@livx.cc/appwrap/config';
|
|
121
|
+
* export default defineConfig({ id: '…', name: '…', version: '1.0.0', pwaDist: 'dist' });
|
|
122
|
+
*/
|
|
123
|
+
export function defineConfig(config: AppwrapConfig): AppwrapConfig {
|
|
124
|
+
return config;
|
|
125
|
+
}
|
|
@@ -1,11 +1,16 @@
|
|
|
1
|
-
# appwrap fastlane — emitted by `appwrap init` into native/fastlane/.
|
|
2
|
-
# Auth is App Store Connect API key only (headless-safe; no interactive Apple ID).
|
|
1
|
+
# appwrap fastlane — emitted by `appwrap init` into native/fastlane/. App id + team are stamped
|
|
2
|
+
# from the appwrap config. Auth is App Store Connect API key only (headless-safe; no interactive Apple ID).
|
|
3
3
|
|
|
4
4
|
default_platform(:ios)
|
|
5
5
|
|
|
6
|
+
APP_ID = '__APP_ID__'.freeze
|
|
7
|
+
TEAM_ID = '__TEAM_ID__'.freeze
|
|
8
|
+
|
|
6
9
|
platform :ios do
|
|
7
10
|
desc 'Build the NativeScript wrapper and upload to TestFlight'
|
|
8
11
|
lane :beta do
|
|
12
|
+
setup_ci # on CI: provisions a temporary unlocked keychain so codesign can use the match cert
|
|
13
|
+
|
|
9
14
|
api_key = app_store_connect_api_key(
|
|
10
15
|
key_id: ENV['ASC_KEY_ID'],
|
|
11
16
|
issuer_id: ENV['ASC_ISSUER_ID'],
|
|
@@ -13,14 +18,30 @@ platform :ios do
|
|
|
13
18
|
is_key_content_base64: true
|
|
14
19
|
)
|
|
15
20
|
|
|
16
|
-
|
|
21
|
+
# readonly: certs/profiles must be seeded once via `fastlane match appstore` (see workflow header).
|
|
22
|
+
match(type: 'appstore', readonly: true, app_identifier: APP_ID,
|
|
23
|
+
git_url: ENV['MATCH_GIT_URL'], api_key: api_key)
|
|
17
24
|
|
|
18
|
-
|
|
25
|
+
# NativeScript prepares the Xcode project inside the wrapper dir (native/ = parent of fastlane/).
|
|
26
|
+
sh("cd '#{File.expand_path('..', __dir__)}' && ns prepare ios --release")
|
|
27
|
+
|
|
28
|
+
# NS defaults the project to automatic signing → archive can't find a profile. Pin manual signing
|
|
29
|
+
# to the match-provided App Store profile (name exported by match into ENV).
|
|
30
|
+
profile = ENV["sigh_#{APP_ID}_appstore_profile-name"] || "match AppStore #{APP_ID}"
|
|
31
|
+
update_code_signing_settings(
|
|
32
|
+
use_automatic_signing: false,
|
|
33
|
+
path: 'platforms/ios/native.xcodeproj',
|
|
34
|
+
team_id: TEAM_ID,
|
|
35
|
+
bundle_identifier: APP_ID,
|
|
36
|
+
code_sign_identity: 'Apple Distribution',
|
|
37
|
+
profile_name: profile
|
|
38
|
+
)
|
|
19
39
|
|
|
20
40
|
build_app(
|
|
21
41
|
workspace: 'platforms/ios/native.xcworkspace',
|
|
22
42
|
scheme: 'native',
|
|
23
|
-
export_method: 'app-store'
|
|
43
|
+
export_method: 'app-store',
|
|
44
|
+
export_options: { provisioningProfiles: { APP_ID => profile } }
|
|
24
45
|
)
|
|
25
46
|
|
|
26
47
|
upload_to_testflight(api_key: api_key, skip_waiting_for_build_processing: true)
|
|
@@ -23,6 +23,6 @@ jobs:
|
|
|
23
23
|
with: { node-version: 22 }
|
|
24
24
|
- run: bun install && bun run build.ts
|
|
25
25
|
- run: npm i -g nativescript
|
|
26
|
-
- run: bunx appwrap init # native/ is generated (gitignored) — regenerate it fresh in CI
|
|
26
|
+
- run: bunx @livx.cc/appwrap init # native/ is generated (gitignored) — regenerate it fresh in CI
|
|
27
27
|
- working-directory: native
|
|
28
28
|
run: npm install && ns build ios --emulator
|
|
@@ -25,7 +25,7 @@ jobs:
|
|
|
25
25
|
- uses: android-actions/setup-android@v3
|
|
26
26
|
- run: bun install && bun run build.ts # → dist/ (the PWA bundle the wrapper ships)
|
|
27
27
|
- run: npm i -g nativescript
|
|
28
|
-
- run: bunx appwrap init # native/ is generated (gitignored) — regenerate fresh in CI
|
|
28
|
+
- run: bunx @livx.cc/appwrap init # native/ is generated (gitignored) — regenerate fresh in CI
|
|
29
29
|
- working-directory: native
|
|
30
30
|
run: npm install
|
|
31
31
|
- name: Decode upload keystore
|
|
@@ -36,7 +36,7 @@ jobs:
|
|
|
36
36
|
APPWRAP_ANDROID_KEYSTORE_PASSWORD: ${{ secrets.ANDROID_KEYSTORE_PASSWORD }}
|
|
37
37
|
APPWRAP_ANDROID_KEYSTORE_ALIAS: ${{ secrets.ANDROID_KEY_ALIAS }}
|
|
38
38
|
APPWRAP_ANDROID_KEYSTORE_ALIAS_PASSWORD: ${{ secrets.ANDROID_KEY_ALIAS_PASSWORD }}
|
|
39
|
-
run: bunx appwrap build android --release --aab
|
|
39
|
+
run: bunx @livx.cc/appwrap build android --release --aab
|
|
40
40
|
- uses: actions/upload-artifact@v4 # keep the AAB even if Play upload is skipped
|
|
41
41
|
with:
|
|
42
42
|
name: app-release-aab
|
|
@@ -1,13 +1,16 @@
|
|
|
1
|
-
# appwrap iOS
|
|
1
|
+
# appwrap iOS → TestFlight via fastlane. On-demand (workflow_dispatch) or tag push (v*).
|
|
2
2
|
# Secrets contract (repo settings → Actions secrets):
|
|
3
|
-
# MATCH_GIT_URL private repo holding signing certs (fastlane match)
|
|
3
|
+
# MATCH_GIT_URL private repo holding signing certs (fastlane match) — SSH form for the deploy key
|
|
4
|
+
# MATCH_DEPLOY_KEY read-only SSH deploy key on that certs repo (so CI can clone it)
|
|
4
5
|
# MATCH_PASSWORD match encryption passphrase
|
|
5
6
|
# ASC_KEY_ID App Store Connect API key id
|
|
6
7
|
# ASC_ISSUER_ID App Store Connect issuer id
|
|
7
8
|
# ASC_KEY_P8 App Store Connect .p8 key contents (base64)
|
|
8
|
-
# No interactive Apple ID — API-key auth only (headless-safe).
|
|
9
|
+
# No interactive Apple ID — API-key auth only (headless-safe). Certs/profiles must be seeded once
|
|
10
|
+
# locally via `fastlane match appstore` before CI runs.
|
|
9
11
|
name: Release iOS
|
|
10
12
|
on:
|
|
13
|
+
workflow_dispatch:
|
|
11
14
|
push:
|
|
12
15
|
tags: ['v*']
|
|
13
16
|
|
|
@@ -16,17 +19,27 @@ jobs:
|
|
|
16
19
|
runs-on: macos-15
|
|
17
20
|
steps:
|
|
18
21
|
- uses: actions/checkout@v4
|
|
22
|
+
# Apple requires the iOS 26 SDK (Xcode 26+) for uploads — runners may default to older Xcode.
|
|
23
|
+
- uses: maxim-lobanov/setup-xcode@v1
|
|
24
|
+
with: { xcode-version: latest-stable }
|
|
19
25
|
- uses: oven-sh/setup-bun@v2
|
|
20
26
|
- uses: actions/setup-node@v4
|
|
21
27
|
with: { node-version: 22 }
|
|
22
28
|
- run: bun install && bun run build.ts
|
|
23
29
|
- run: npm i -g nativescript
|
|
24
|
-
- run: bunx appwrap init # native/ is generated (gitignored) — regenerate it fresh in CI
|
|
30
|
+
- run: bunx @livx.cc/appwrap init # native/ is generated (gitignored) — regenerate it fresh in CI
|
|
25
31
|
- working-directory: native
|
|
26
32
|
run: npm install
|
|
27
33
|
- uses: ruby/setup-ruby@v1
|
|
28
34
|
with: { ruby-version: '3.3', bundler-cache: false }
|
|
29
35
|
- run: gem install fastlane
|
|
36
|
+
- name: Set up match deploy key (read-only SSH on the certs repo)
|
|
37
|
+
run: |
|
|
38
|
+
mkdir -p ~/.ssh && chmod 700 ~/.ssh
|
|
39
|
+
printf '%s\n' "${{ secrets.MATCH_DEPLOY_KEY }}" > ~/.ssh/match_deploy_key
|
|
40
|
+
chmod 600 ~/.ssh/match_deploy_key
|
|
41
|
+
printf 'Host github.com\n IdentityFile ~/.ssh/match_deploy_key\n IdentitiesOnly yes\n' >> ~/.ssh/config
|
|
42
|
+
ssh-keyscan github.com >> ~/.ssh/known_hosts 2>/dev/null
|
|
30
43
|
- name: Build & upload to TestFlight
|
|
31
44
|
working-directory: native
|
|
32
45
|
env:
|
|
@@ -37,5 +50,4 @@ jobs:
|
|
|
37
50
|
ASC_KEY_P8: ${{ secrets.ASC_KEY_P8 }}
|
|
38
51
|
run: fastlane ios beta
|
|
39
52
|
|
|
40
|
-
# Production promotion is a manual gate:
|
|
41
|
-
# environment approved, or promote the TestFlight build in App Store Connect.
|
|
53
|
+
# Production promotion is a manual gate: promote the TestFlight build in App Store Connect.
|