@livx.cc/appwrap 0.54.0 → 0.56.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/package.json +5 -4
- package/runtime/app/main-page.ts +0 -2
- package/runtime/app/shell/capabilities.manifest.ts +27 -92
- package/runtime/app/shell/config.ts +4 -0
- package/runtime/app/shell/env.ts +8 -1
- package/runtime/app/shell/handlers.ts +2 -2
- package/runtime/tests/bridge-response-delivery.test.ts +10 -1
- package/runtime/tests/env-scheme.test.ts +50 -0
- package/scripts/stage-assets.mjs +1 -1
- package/src/cli.ts +424 -621
- package/src/config.ts +8 -1
- package/src/handlers.ts +1 -1
- package/src/packs.ts +174 -0
- package/src/testing.ts +173 -0
- package/templates/module-pack/README.md +44 -0
- package/templates/module-pack/handler.ts +9 -0
- package/templates/module-pack/manifest.ts +27 -0
- package/templates/module-pack/native-src/__MODULE_NAME__/.gitkeep +0 -0
- package/runtime/app/shell/billing-offer.ts +0 -54
- package/runtime/app/shell/handlers-billing.ts +0 -662
- package/runtime/app/shell/handlers-health.ts +0 -178
- package/runtime/app/shell/handlers-widget.ts +0 -97
- package/runtime/modules-native/billing/App_Resources/iOS/src/AppwrapManageSubscriptions.swift +0 -44
- package/runtime/modules-native/health/App_Resources/Android/src/main/java/cc/livx/appwrap/HealthConnectBridge.kt +0 -74
- package/runtime/modules-native/widget/App_Resources/Android/src/main/java/cc/livx/appwrap/AppwrapWidgetProvider.kt +0 -118
- package/runtime/modules-native/widget/App_Resources/Android/src/main/res/drawable/appwrap_badge_bg.xml +0 -9
- package/runtime/modules-native/widget/App_Resources/Android/src/main/res/layout/appwrap_widget.xml +0 -123
- package/runtime/modules-native/widget/App_Resources/Android/src/main/res/values/appwrap_widget_colors.xml +0 -10
- package/runtime/modules-native/widget/App_Resources/Android/src/main/res/values/appwrap_widget_styles.xml +0 -67
- package/runtime/modules-native/widget/App_Resources/Android/src/main/res/values-night/appwrap_widget_colors.xml +0 -9
- package/runtime/modules-native/widget/App_Resources/Android/src/main/res/xml/appwrap_widget_info.xml +0 -17
- package/runtime/modules-native/widget/App_Resources/iOS/extensions/AppwrapWidget/AppwrapWidget.entitlements +0 -10
- package/runtime/modules-native/widget/App_Resources/iOS/extensions/AppwrapWidget/AppwrapWidget.swift +0 -275
- package/runtime/modules-native/widget/App_Resources/iOS/extensions/AppwrapWidget/Info.plist +0 -25
- package/runtime/modules-native/widget/App_Resources/iOS/extensions/AppwrapWidget/extension.json +0 -9
- package/runtime/modules-native/widget/App_Resources/iOS/src/AppwrapWidgetReload.swift +0 -18
- package/runtime/tests/billing-offer.test.ts +0 -61
- package/runtime-desktop/autotest/index.html +0 -77
- package/runtime-desktop/bridge-shim/build.sh +0 -9
- package/runtime-desktop/bridge-shim/content-entry.ts +0 -87
- package/runtime-desktop/bridge-shim/shim.ts +0 -132
- package/runtime-desktop/chrome/appwrap-browser-chrome.html +0 -185
- package/runtime-desktop/crates/webview-control/Cargo.lock +0 -330
- package/runtime-desktop/crates/webview-control/Cargo.toml +0 -28
- package/runtime-desktop/crates/webview-control/examples/host_harness.rs +0 -362
- package/runtime-desktop/crates/webview-control/src/embedded.rs +0 -138
- package/runtime-desktop/crates/webview-control/src/lib.rs +0 -100
- package/runtime-desktop/crates/webview-control/src/popup.rs +0 -522
- package/runtime-desktop/crates/webview-control/src/profiles.rs +0 -57
- package/runtime-desktop/crates/webview-control/src/wkwebview_backend.rs +0 -558
- package/runtime-desktop/src-tauri/Cargo.lock +0 -5474
- package/runtime-desktop/src-tauri/Cargo.toml +0 -45
- package/runtime-desktop/src-tauri/build.rs +0 -3
- package/runtime-desktop/src-tauri/capabilities/default.json +0 -6
- package/runtime-desktop/src-tauri/icons/icon.png +0 -0
- package/runtime-desktop/src-tauri/shell_config.json +0 -10
- package/runtime-desktop/src-tauri/src/biometrics_mac.rs +0 -138
- package/runtime-desktop/src-tauri/src/bridge_mac.rs +0 -424
- package/runtime-desktop/src-tauri/src/browser_tab.rs +0 -804
- package/runtime-desktop/src-tauri/src/main.rs +0 -1228
- package/runtime-desktop/src-tauri/src/notifications_mac.rs +0 -202
- package/runtime-desktop/src-tauri/src/oauth_mac.rs +0 -139
- package/runtime-desktop/src-tauri/src/plugin_host.rs +0 -522
- package/runtime-desktop/src-tauri/src/popup_mac.rs +0 -5
- package/runtime-desktop/src-tauri/src/push_mac.rs +0 -193
- package/runtime-desktop/src-tauri/src/server.rs +0 -156
- package/runtime-desktop/src-tauri/src/sidecar.rs +0 -398
- package/runtime-desktop/src-tauri/tauri.conf.json +0 -14
- package/src/desktop.ts +0 -300
- package/src/plugin/host.ts +0 -242
package/src/config.ts
CHANGED
|
@@ -308,6 +308,13 @@ export interface AppwrapConfig {
|
|
|
308
308
|
* (the per-app `permissions{}` map only OVERRIDES the default usage copy). When ABSENT, every
|
|
309
309
|
* capability is active and permissions come solely from `permissions{}` (pre-modules behavior). */
|
|
310
310
|
modules?: string[];
|
|
311
|
+
/** Extra module packs layered on top of the built-in capabilities (see packs.ts). Each entry is a
|
|
312
|
+
* local directory OR an npm package name; a pack contributes `ModuleManifest[]` (+ handler files,
|
|
313
|
+
* native source, an optional kit client). Packs apply in order, LAST-WINS by module name, so a pack
|
|
314
|
+
* can add a NEW capability or wholesale-shadow a built-in one (e.g. swap the billing implementation).
|
|
315
|
+
* This is the single public extension seam — the same mechanism a host, consumer apps that vendor
|
|
316
|
+
* their own module, and community plugins all use. Absent → built-ins only (unchanged behavior). */
|
|
317
|
+
modulePacks?: string[];
|
|
311
318
|
/** Permitted headless background-task identifiers (for the `backgroundTask` module). iOS REQUIRES
|
|
312
319
|
* these declared at build time — they stamp into Info.plist `BGTaskSchedulerPermittedIdentifiers`
|
|
313
320
|
* (without them `BGTaskScheduler.register` throws) plus `fetch`+`processing` into UIBackgroundModes.
|
|
@@ -366,7 +373,7 @@ export function defineConfig(config: AppwrapConfig): AppwrapConfig {
|
|
|
366
373
|
*/
|
|
367
374
|
export const KNOWN_CONFIG_KEYS: ReadonlySet<string> = new Set([
|
|
368
375
|
'androidAppLinks', 'appBoundDomains', 'backendOrigin', 'backgroundAudio', 'backgroundColor', 'backgroundTasks', 'buildNumber', 'debug',
|
|
369
|
-
'debugLog', 'desktop', 'devMenu', 'edgeToEdge', 'entry', 'icon', 'id', 'iosKeyboardExtraLift', 'loader', 'modules', 'name',
|
|
376
|
+
'debugLog', 'desktop', 'devMenu', 'edgeToEdge', 'entry', 'icon', 'id', 'iosKeyboardExtraLift', 'loader', 'modules', 'modulePacks', 'name',
|
|
370
377
|
'envSwitcher', 'iosEntitlements', 'neutralizeServiceWorker', 'oauthRedirectSchemes', 'openNewWindowsInBrowser', 'orientation', 'overrides', 'permissions',
|
|
371
378
|
'plugins', 'push', 'pwaDist', 'queryPackages', 'queryUrlSchemes', 'serverUrl', 'signing', 'signingProfiles', 'statusBarStyle',
|
|
372
379
|
'splashIcon', 'storekitConfig', 'targetedDevices', 'teamId', 'themeColor', 'trackingDomains', 'urlScheme',
|
package/src/handlers.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* `defineHandlers` — author custom native handlers for the appwrap DESKTOP shell, run as a Bun sidecar.
|
|
3
3
|
*
|
|
4
|
-
* The desktop shell
|
|
4
|
+
* The desktop shell spawns `bun <yourFile>` when `desktop.handlers` is set, and routes
|
|
5
5
|
* any method your map advertises to this process over NDJSON on stdin/stdout (appwrap sidecar protocol v1).
|
|
6
6
|
* The PWA calls them through the kit with a raw invoke: `kit.invoke('myapp.foo', { … })`.
|
|
7
7
|
*
|
package/src/packs.ts
ADDED
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Module-pack resolver — the single public extension seam of appwrap (CE/EE seam A2).
|
|
3
|
+
*
|
|
4
|
+
* A "pack" is a directory (local, or an npm package) that contributes capabilities to a wrapper build
|
|
5
|
+
* exactly like the built-in modules do: it exports `ModuleManifest[]` (the same contribution bags the
|
|
6
|
+
* built-ins use — permissions/entitlements/gradleDeps/nativeSrc/handler), plus the handler source and
|
|
7
|
+
* optional native source those manifests reference. The built-in modules are treated as *pack 0*; the
|
|
8
|
+
* config's `modulePacks` apply on top, in order.
|
|
9
|
+
*
|
|
10
|
+
* Merge policy (name-keyed):
|
|
11
|
+
* - a pack module whose `name` is NEW → added;
|
|
12
|
+
* - a pack module whose `name` already exists (from a different pack) → LAST-WINS, wholesale-shadowing
|
|
13
|
+
* the earlier module (its whole manifest + handler + native source), with a provenance log line;
|
|
14
|
+
* - a duplicate name WITHIN one pack → idempotent (later entry wins silently — a pack re-exporting its
|
|
15
|
+
* own module is not an error).
|
|
16
|
+
*
|
|
17
|
+
* This one mechanism serves three audiences identically: a host layers its private packs, a consumer
|
|
18
|
+
* app vendors its own copy of a module, and the community ships plugins — none of them patch CE.
|
|
19
|
+
*
|
|
20
|
+
* PURE of CLI I/O: filesystem/npm resolution and pack import are injected ports (`resolvePack`/
|
|
21
|
+
* `importPack`), so the merge logic unit-tests against in-memory fakes. The CLI wires the real ports.
|
|
22
|
+
*/
|
|
23
|
+
import { existsSync } from 'node:fs';
|
|
24
|
+
import { resolve } from 'node:path';
|
|
25
|
+
import type { ModuleManifest } from '../../../runtime/app/shell/capabilities.manifest';
|
|
26
|
+
|
|
27
|
+
/** Build-time context a pack's manifest may depend on (platform/env/CI). Manifest entries may be
|
|
28
|
+
* `(ctx) => ModuleManifest` so a pack can contribute different bags per platform/env without the
|
|
29
|
+
* downstream derivation (nativeReqs) knowing packs exist — thunks are resolved to plain values here. */
|
|
30
|
+
export interface SyncContext {
|
|
31
|
+
/** The lane being generated. 'both' when a single sync stamps a cross-platform wrapper. */
|
|
32
|
+
platform: 'ios' | 'android' | 'both';
|
|
33
|
+
/** Resolved environment name (e.g. 'prod', 'lab', 'default'). */
|
|
34
|
+
env: string;
|
|
35
|
+
/** True in CI. */
|
|
36
|
+
ci: boolean;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** A manifest entry as authored in a pack: a plain manifest, or a fn of the sync context. */
|
|
40
|
+
export type ModuleEntry = ModuleManifest | ((ctx: SyncContext) => ModuleManifest);
|
|
41
|
+
|
|
42
|
+
/** The shape a pack's manifest module must export (as `default` or named `pack`). `modules` may be a
|
|
43
|
+
* plain array, or a fn of the context (for a pack whose whole set is context-dependent). */
|
|
44
|
+
export interface PackModule {
|
|
45
|
+
/** The manifest schema version this pack targets — checked against MANIFEST_SCHEMA_VERSION. */
|
|
46
|
+
manifestSchemaVersion: number;
|
|
47
|
+
/** The capabilities this pack contributes. */
|
|
48
|
+
modules: ModuleEntry[] | ((ctx: SyncContext) => ModuleEntry[]);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** A merged module + where it came from (for provenance + to locate its handler/native source). */
|
|
52
|
+
export interface ResolvedModule {
|
|
53
|
+
/** The module's contribution bags, with any context thunks resolved to plain values. */
|
|
54
|
+
manifest: ModuleManifest;
|
|
55
|
+
/** Provenance label: BUILTIN_SOURCE for a built-in, else the pack ref as written in `modulePacks`. */
|
|
56
|
+
source: string;
|
|
57
|
+
/** Absolute pack directory — the base for resolving this module's `handler.file` / `nativeSrc` /
|
|
58
|
+
* kit client. `null` for a built-in (those live under the runtime template, wired by the CLI). */
|
|
59
|
+
packDir: string | null;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Provenance label used for the built-in modules (pack 0). */
|
|
63
|
+
export const BUILTIN_SOURCE = '(built-in)';
|
|
64
|
+
|
|
65
|
+
export interface ResolvePacksOptions {
|
|
66
|
+
/** The built-in modules — pack 0. */
|
|
67
|
+
builtins: ModuleManifest[];
|
|
68
|
+
/** The manifest schema version the built-ins (and this CLI) speak. */
|
|
69
|
+
builtinSchemaVersion: number;
|
|
70
|
+
/** The config's `modulePacks` (local dirs or npm names), applied in order after the built-ins. */
|
|
71
|
+
packRefs: string[];
|
|
72
|
+
/** Build-time context passed to any manifest thunks. */
|
|
73
|
+
ctx: SyncContext;
|
|
74
|
+
/** Directory `modulePacks` refs resolve relative to (the app root). */
|
|
75
|
+
cwd: string;
|
|
76
|
+
/** Resolve a pack ref → absolute directory. Injected for tests; defaults to dir-or-npm resolution. */
|
|
77
|
+
resolvePack?: (ref: string, cwd: string) => string;
|
|
78
|
+
/** Import a pack's manifest module from its directory. Injected for tests; defaults to importing
|
|
79
|
+
* `<dir>/manifest.ts` and reading its `default` or named `pack` export. */
|
|
80
|
+
importPack?: (dir: string) => Promise<PackModule>;
|
|
81
|
+
/** Provenance sink. Defaults to console.log. */
|
|
82
|
+
log?: (msg: string) => void;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Default pack-ref resolution: an existing directory (relative to the app root) wins; otherwise the
|
|
86
|
+
* ref is treated as an npm package name and resolved via Bun's resolver (same path the plugin loader
|
|
87
|
+
* uses). Throws a clear error when a ref is neither. */
|
|
88
|
+
export function defaultResolvePack(ref: string, cwd: string): string {
|
|
89
|
+
const asDir = resolve(cwd, ref);
|
|
90
|
+
if (existsSync(asDir)) return asDir;
|
|
91
|
+
try {
|
|
92
|
+
// Resolve the package's own manifest, then take its directory — the pack root.
|
|
93
|
+
const pkgJson = (Bun as unknown as { resolveSync(id: string, parent: string): string }).resolveSync(
|
|
94
|
+
`${ref}/package.json`,
|
|
95
|
+
cwd
|
|
96
|
+
);
|
|
97
|
+
return pkgJson.slice(0, pkgJson.length - '/package.json'.length);
|
|
98
|
+
} catch {
|
|
99
|
+
throw new Error(
|
|
100
|
+
`module pack "${ref}" not found — no such directory (relative to ${cwd}) and not resolvable as an npm package.`
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** Default pack import: load `<dir>/manifest.ts` and read its `default` or named `pack` export. */
|
|
106
|
+
export async function defaultImportPack(dir: string): Promise<PackModule> {
|
|
107
|
+
const manifestPath = resolve(dir, 'manifest.ts');
|
|
108
|
+
if (!existsSync(manifestPath)) {
|
|
109
|
+
throw new Error(`module pack at "${dir}" is missing manifest.ts (must export the pack manifest).`);
|
|
110
|
+
}
|
|
111
|
+
const mod = (await import(manifestPath)) as { default?: PackModule; pack?: PackModule };
|
|
112
|
+
const pack = mod.default ?? mod.pack;
|
|
113
|
+
if (!pack || typeof pack.manifestSchemaVersion !== 'number' || !pack.modules) {
|
|
114
|
+
throw new Error(
|
|
115
|
+
`module pack at "${dir}" must export (default or \`pack\`) an object with { manifestSchemaVersion, modules }.`
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
return pack;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** Resolve a manifest entry (plain or thunk) against the context to a plain manifest. */
|
|
122
|
+
function resolveEntry(entry: ModuleEntry, ctx: SyncContext): ModuleManifest {
|
|
123
|
+
return typeof entry === 'function' ? entry(ctx) : entry;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Compose the built-in modules with the config's module packs into an ordered, name-keyed map. The
|
|
128
|
+
* insertion order is: built-ins first, then packs in `packRefs` order — and a shadow REPLACES in place
|
|
129
|
+
* (Map preserves the original key position), so a shadowing pack module keeps the built-in's slot. The
|
|
130
|
+
* returned map is what the generator derives the native requirements + handler barrel from.
|
|
131
|
+
*/
|
|
132
|
+
export async function resolveModulePacks(opts: ResolvePacksOptions): Promise<Map<string, ResolvedModule>> {
|
|
133
|
+
const {
|
|
134
|
+
builtins,
|
|
135
|
+
builtinSchemaVersion,
|
|
136
|
+
packRefs,
|
|
137
|
+
ctx,
|
|
138
|
+
cwd,
|
|
139
|
+
resolvePack = defaultResolvePack,
|
|
140
|
+
importPack = defaultImportPack,
|
|
141
|
+
log = console.log,
|
|
142
|
+
} = opts;
|
|
143
|
+
|
|
144
|
+
const map = new Map<string, ResolvedModule>();
|
|
145
|
+
for (const m of builtins) map.set(m.name, { manifest: m, source: BUILTIN_SOURCE, packDir: null });
|
|
146
|
+
|
|
147
|
+
for (const ref of packRefs) {
|
|
148
|
+
const dir = resolvePack(ref, cwd);
|
|
149
|
+
const pack = await importPack(dir);
|
|
150
|
+
if (pack.manifestSchemaVersion !== builtinSchemaVersion) {
|
|
151
|
+
throw new Error(
|
|
152
|
+
`module pack "${ref}" targets manifest schema v${pack.manifestSchemaVersion} but this appwrap ` +
|
|
153
|
+
`speaks v${builtinSchemaVersion}. Upgrade the pack or appwrap so the versions match.`
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
const entries = typeof pack.modules === 'function' ? pack.modules(ctx) : pack.modules;
|
|
157
|
+
const seenInThisPack = new Set<string>();
|
|
158
|
+
for (const entry of entries) {
|
|
159
|
+
const manifest = resolveEntry(entry, ctx);
|
|
160
|
+
const name = manifest.name;
|
|
161
|
+
const prior = map.get(name);
|
|
162
|
+
// A dup WITHIN this same pack is idempotent (later wins silently) — not a shadow event.
|
|
163
|
+
if (prior && prior.source !== ref && !seenInThisPack.has(name)) {
|
|
164
|
+
log(` mod ← ${ref} (${name} shadows ${prior.source})`);
|
|
165
|
+
} else if (!prior) {
|
|
166
|
+
log(` mod ← ${ref} (${name})`);
|
|
167
|
+
}
|
|
168
|
+
seenInThisPack.add(name);
|
|
169
|
+
map.set(name, { manifest, source: ref, packDir: dir });
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return map;
|
|
174
|
+
}
|
package/src/testing.ts
ADDED
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pack conformance validator — the public `@livx.cc/appwrap/testing` entry (CE/EE seam A8).
|
|
3
|
+
*
|
|
4
|
+
* A module pack (see packs.ts) is authored out-of-tree and staged into the shell at sync. Because a
|
|
5
|
+
* malformed pack fails LATE (mid-generate, on a device build), this gives pack authors a FAST, offline
|
|
6
|
+
* conformance check they can run in their own `bun test` — the same contract the CLI enforces, minus
|
|
7
|
+
* any NativeScript/CLI side effects, so it's import-safe as a library entry.
|
|
8
|
+
*
|
|
9
|
+
* It checks a pack DIRECTORY:
|
|
10
|
+
* - manifest.ts exists and (default or named `pack`) exports { manifestSchemaVersion, modules }
|
|
11
|
+
* (REUSES `defaultImportPack` from packs.ts — same loader the CLI uses);
|
|
12
|
+
* - manifestSchemaVersion === MANIFEST_SCHEMA_VERSION (the version this appwrap speaks);
|
|
13
|
+
* - every resolved module has a non-empty `name`, `group`, and a `capabilities` object;
|
|
14
|
+
* - each module `handler.file` resolves to an existing file under the pack dir + `handler.fn` is set;
|
|
15
|
+
* - each handler file imports ONLY sanctioned specifiers (no `../` escaping the pack, no bare deep
|
|
16
|
+
* import into appwrap internals other than `@livx.cc/appwrap/runtime/app/shell/*`).
|
|
17
|
+
*
|
|
18
|
+
* ALL problems are collected (not fail-fast) so `errors[]` is a full report.
|
|
19
|
+
*/
|
|
20
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
21
|
+
import { extname, relative, resolve } from 'node:path';
|
|
22
|
+
import { builtinModules } from 'node:module';
|
|
23
|
+
import { defaultImportPack, type ModuleEntry, type SyncContext } from './packs';
|
|
24
|
+
import { MANIFEST_SCHEMA_VERSION } from '../../../runtime/app/shell/capabilities.manifest';
|
|
25
|
+
|
|
26
|
+
export interface ValidatePackResult {
|
|
27
|
+
ok: boolean;
|
|
28
|
+
errors: string[];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Node.js core module names (bare + `node:` forms). Allowed in a handler (they're externalized). */
|
|
32
|
+
const NODE_BUILTIN_SET = new Set(builtinModules.flatMap((m) => [m, `node:${m}`]));
|
|
33
|
+
|
|
34
|
+
/** The sanctioned shell-API specifier prefix a pack handler may deep-import (rewritten to relative on
|
|
35
|
+
* staging). Any OTHER `@livx.cc/appwrap/...` deep import reaches into CE internals → rejected. */
|
|
36
|
+
const SANCTIONED_SHELL_PREFIX = '@livx.cc/appwrap/runtime/app/shell/';
|
|
37
|
+
|
|
38
|
+
/** Map a handler file extension to the Bun.Transpiler loader for its dialect (mirrors cli.ts). */
|
|
39
|
+
function loaderFor(file: string): 'ts' | 'tsx' | 'js' | 'jsx' {
|
|
40
|
+
const ext = extname(file).toLowerCase();
|
|
41
|
+
if (ext === '.tsx') return 'tsx';
|
|
42
|
+
if (ext === '.jsx') return 'jsx';
|
|
43
|
+
if (ext === '.js' || ext === '.mjs' || ext === '.cjs') return 'js';
|
|
44
|
+
return 'ts';
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** True if a bare specifier is an allowed non-relative import (shell API, @nativescript/core, node
|
|
48
|
+
* builtin). Anything else bare is treated as a normal npm dep the pack declares — NOT our concern here,
|
|
49
|
+
* so we only FLAG the one illegitimate class: a deep import into appwrap internals off the sanctioned
|
|
50
|
+
* path. */
|
|
51
|
+
function bareImportError(spec: string): string | null {
|
|
52
|
+
// Sanctioned shell API — always fine.
|
|
53
|
+
if (spec === SANCTIONED_SHELL_PREFIX.slice(0, -1) || spec.startsWith(SANCTIONED_SHELL_PREFIX)) return null;
|
|
54
|
+
// Any OTHER appwrap deep import escapes the sanctioned seam into CE internals.
|
|
55
|
+
if (spec === '@livx.cc/appwrap' || spec.startsWith('@livx.cc/appwrap/')) {
|
|
56
|
+
return `imports appwrap internals via "${spec}" — a pack may only deep-import the sanctioned ` +
|
|
57
|
+
`"${SANCTIONED_SHELL_PREFIX}*" shell APIs.`;
|
|
58
|
+
}
|
|
59
|
+
// @nativescript/core (+ subpaths), node builtins, and any other npm dep are all acceptable.
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Scan a handler file's imports; return an error string per illegal specifier (empty when clean).
|
|
64
|
+
* Uses Bun.Transpiler.scanImports (real import/require STATEMENTS only — never a string-literal match),
|
|
65
|
+
* the same primitive the CLI uses. */
|
|
66
|
+
function scanHandlerImports(packDir: string, handlerFile: string): string[] {
|
|
67
|
+
const errors: string[] = [];
|
|
68
|
+
const src = readFileSync(handlerFile, 'utf8');
|
|
69
|
+
const rel = relative(packDir, handlerFile);
|
|
70
|
+
for (const { path: spec } of new Bun.Transpiler({ loader: loaderFor(handlerFile) }).scanImports(src)) {
|
|
71
|
+
if (spec.startsWith('.')) {
|
|
72
|
+
// Relative import — must resolve to somewhere INSIDE the pack dir.
|
|
73
|
+
const target = resolve(handlerFile, '..', spec);
|
|
74
|
+
const back = relative(packDir, target);
|
|
75
|
+
if (back.startsWith('..') || resolve(packDir, back) !== target) {
|
|
76
|
+
errors.push(`handler ${rel} imports "${spec}" which escapes the pack directory (${target}).`);
|
|
77
|
+
}
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
if (NODE_BUILTIN_SET.has(spec) || NODE_BUILTIN_SET.has(spec.replace(/^node:/, '').split('/')[0])) continue;
|
|
81
|
+
const bareErr = bareImportError(spec);
|
|
82
|
+
if (bareErr) errors.push(`handler ${rel} ${bareErr}`);
|
|
83
|
+
}
|
|
84
|
+
return errors;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** A representative sync context for resolving context-dependent manifest thunks during validation. */
|
|
88
|
+
const REPRESENTATIVE_CTX: SyncContext = { platform: 'both', env: 'default', ci: false };
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Validate that `dir` is a conformant module pack. Collects ALL errors; `ok` is `errors.length === 0`.
|
|
92
|
+
* Import-safe: no NativeScript globals, no CLI dispatch.
|
|
93
|
+
*/
|
|
94
|
+
export async function validatePack(dir: string): Promise<ValidatePackResult> {
|
|
95
|
+
const errors: string[] = [];
|
|
96
|
+
const packDir = resolve(dir);
|
|
97
|
+
|
|
98
|
+
// 1. Load the manifest via the SAME loader the CLI uses (throws a clear message if absent/malformed).
|
|
99
|
+
let pack: Awaited<ReturnType<typeof defaultImportPack>>;
|
|
100
|
+
try {
|
|
101
|
+
pack = await defaultImportPack(packDir);
|
|
102
|
+
} catch (e) {
|
|
103
|
+
errors.push((e as Error).message);
|
|
104
|
+
return { ok: false, errors };
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// 2. Schema-version match.
|
|
108
|
+
if (pack.manifestSchemaVersion !== MANIFEST_SCHEMA_VERSION) {
|
|
109
|
+
errors.push(
|
|
110
|
+
`manifestSchemaVersion ${pack.manifestSchemaVersion} does not match this appwrap's ` +
|
|
111
|
+
`MANIFEST_SCHEMA_VERSION ${MANIFEST_SCHEMA_VERSION}.`
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// 3. Resolve modules (may be a fn of the context) → plain manifests.
|
|
116
|
+
let entries: ModuleEntry[];
|
|
117
|
+
try {
|
|
118
|
+
entries = typeof pack.modules === 'function' ? pack.modules(REPRESENTATIVE_CTX) : pack.modules;
|
|
119
|
+
} catch (e) {
|
|
120
|
+
errors.push(`resolving \`modules\` threw: ${(e as Error).message}`);
|
|
121
|
+
return { ok: false, errors };
|
|
122
|
+
}
|
|
123
|
+
if (!Array.isArray(entries)) {
|
|
124
|
+
errors.push('`modules` must resolve to an array of module entries.');
|
|
125
|
+
return { ok: false, errors };
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
for (const [i, entry] of entries.entries()) {
|
|
129
|
+
let m;
|
|
130
|
+
try {
|
|
131
|
+
m = typeof entry === 'function' ? entry(REPRESENTATIVE_CTX) : entry;
|
|
132
|
+
} catch (e) {
|
|
133
|
+
errors.push(`module[${i}] entry thunk threw: ${(e as Error).message}`);
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
const label = m && m.name ? `module "${m.name}"` : `module[${i}]`;
|
|
137
|
+
if (!m || typeof m !== 'object') {
|
|
138
|
+
errors.push(`module[${i}] is not an object.`);
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
if (!m.name || typeof m.name !== 'string') errors.push(`${label} is missing a non-empty \`name\`.`);
|
|
142
|
+
if (!m.group || typeof m.group !== 'string') errors.push(`${label} is missing a non-empty \`group\`.`);
|
|
143
|
+
if (!m.capabilities || typeof m.capabilities !== 'object') {
|
|
144
|
+
errors.push(`${label} is missing a \`capabilities\` object.`);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// 4. Handler wiring (optional field, but if present must be complete + resolvable).
|
|
148
|
+
if (m.handler) {
|
|
149
|
+
if (!m.handler.fn || typeof m.handler.fn !== 'string') {
|
|
150
|
+
errors.push(`${label} handler is missing a non-empty \`fn\`.`);
|
|
151
|
+
}
|
|
152
|
+
if (!m.handler.file || typeof m.handler.file !== 'string') {
|
|
153
|
+
errors.push(`${label} handler is missing a \`file\`.`);
|
|
154
|
+
} else {
|
|
155
|
+
const handlerFile = resolve(packDir, m.handler.file);
|
|
156
|
+
if (!existsSync(handlerFile)) {
|
|
157
|
+
errors.push(`${label} handler.file "${m.handler.file}" does not exist (looked at ${handlerFile}).`);
|
|
158
|
+
} else {
|
|
159
|
+
// 5. Import legality scan.
|
|
160
|
+
errors.push(...scanHandlerImports(packDir, handlerFile));
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return { ok: errors.length === 0, errors };
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/** Convenience: throw with the joined errors if the pack is not conformant. */
|
|
170
|
+
export async function assertValidPack(dir: string): Promise<void> {
|
|
171
|
+
const { ok, errors } = await validatePack(dir);
|
|
172
|
+
if (!ok) throw new Error(`invalid module pack at "${dir}":\n - ${errors.join('\n - ')}`);
|
|
173
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# __MODULE_NAME__ — appwrap module pack
|
|
2
|
+
|
|
3
|
+
A **module pack** is a self-contained directory that contributes native capabilities to an appwrap
|
|
4
|
+
wrapper build, exactly like the built-in modules do — without patching appwrap itself.
|
|
5
|
+
|
|
6
|
+
## Layout
|
|
7
|
+
|
|
8
|
+
```
|
|
9
|
+
__MODULE_NAME__/
|
|
10
|
+
├── manifest.ts # default-exports { manifestSchemaVersion, modules } (a PackModule)
|
|
11
|
+
├── handler.ts # exports register__MODULE_PASCAL__Handlers() — wires the bridge method(s)
|
|
12
|
+
└── native-src/ # optional module-owned native source (copied in ONLY when active)
|
|
13
|
+
└── __MODULE_NAME__/ # mirrors the App_Resources layout (App_Resources/iOS, /Android, …)
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
- **manifest.ts** declares the module: its `name`, `group`, `capabilities`, and the `handler`
|
|
17
|
+
({ file, fn }) the shell calls at page load. Optionally `nativeSrc` (a `native-src/<name>/` dir),
|
|
18
|
+
`ios`/`android` blocks (permissions / entitlements / gradleDeps).
|
|
19
|
+
- **handler.ts** imports shell APIs via the sanctioned `@livx.cc/appwrap/runtime/app/shell/<name>`
|
|
20
|
+
specifier (e.g. `bridge`). The generator rewrites these to relative `./` on staging. A handler may
|
|
21
|
+
also import `@nativescript/core`, node builtins, or pack-relative `./…` paths — but it must NOT reach
|
|
22
|
+
into appwrap internals via `../` escapes or other `@livx.cc/appwrap/…` deep imports.
|
|
23
|
+
|
|
24
|
+
## Use it in an app
|
|
25
|
+
|
|
26
|
+
In your app's `appwrap.config.ts`:
|
|
27
|
+
|
|
28
|
+
```ts
|
|
29
|
+
export default {
|
|
30
|
+
// …
|
|
31
|
+
modulePacks: ['./path/to/__MODULE_NAME__'], // local dir, or an npm package name
|
|
32
|
+
modules: ['__MODULE_NAME__' /* , …other active modules */],
|
|
33
|
+
};
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Then `appwrap sync` stages the pack's handler + native source into the generated `native/` shell, and
|
|
37
|
+
the capability is advertised to the web side in the handshake.
|
|
38
|
+
|
|
39
|
+
## Validate
|
|
40
|
+
|
|
41
|
+
```ts
|
|
42
|
+
import { assertValidPack } from '@livx.cc/appwrap/testing';
|
|
43
|
+
await assertValidPack(new URL('.', import.meta.url).pathname);
|
|
44
|
+
```
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
// Handler for the `__MODULE_NAME__` module. Imports the shell bridge via the sanctioned package
|
|
2
|
+
// specifier (resolvable standalone — the npm package ships runtime/); the generator rewrites it to a
|
|
3
|
+
// relative `./bridge` when staging this file beside the shell sources.
|
|
4
|
+
import { bridge } from '@livx.cc/appwrap/runtime/app/shell/bridge';
|
|
5
|
+
|
|
6
|
+
export function register__MODULE_PASCAL__Handlers(): void {
|
|
7
|
+
// One demo bridge method — call it from the web side via the kit's raw bridge.
|
|
8
|
+
bridge.register('__MODULE_NAME__.ping', () => ({ pong: true }));
|
|
9
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// Module pack manifest for `__MODULE_NAME__` — scaffolded by `appwrap create-module`.
|
|
2
|
+
//
|
|
3
|
+
// A pack contributes one or more capabilities to a wrapper build exactly like the built-in modules do.
|
|
4
|
+
// This file default-exports { manifestSchemaVersion, modules } (a `PackModule`). The `handler` points
|
|
5
|
+
// at ./handler.ts and names the register fn the shell barrel calls at page load. Reference this pack
|
|
6
|
+
// from your app's appwrap.config.ts:
|
|
7
|
+
//
|
|
8
|
+
// modulePacks: ['./path/to/__MODULE_NAME__'], // this pack directory
|
|
9
|
+
// modules: ['__MODULE_NAME__', ...], // activate the module by name
|
|
10
|
+
//
|
|
11
|
+
// No type import here on purpose, so the scaffold has zero unresolved imports out of the box. The shape
|
|
12
|
+
// is validated by `@livx.cc/appwrap/testing`'s `validatePack` (and by the CLI at sync).
|
|
13
|
+
const pack = {
|
|
14
|
+
manifestSchemaVersion: 1,
|
|
15
|
+
modules: [
|
|
16
|
+
{
|
|
17
|
+
name: '__MODULE_NAME__',
|
|
18
|
+
group: '__MODULE_NAME__',
|
|
19
|
+
capabilities: { __MODULE_NAME__: { ios: true, android: true } },
|
|
20
|
+
handler: { file: './handler.ts', fn: 'register__MODULE_PASCAL__Handlers' },
|
|
21
|
+
// Optional: module-owned native source mirroring the App_Resources layout, copied into native/
|
|
22
|
+
// ONLY when this module is active. Drop platform sources under ./native-src/__MODULE_NAME__/.
|
|
23
|
+
nativeSrc: '__MODULE_NAME__',
|
|
24
|
+
},
|
|
25
|
+
],
|
|
26
|
+
};
|
|
27
|
+
export default pack;
|
|
File without changes
|
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
// Pure Play-Billing offer selection — NO NativeScript/native deps, so it is unit-testable in
|
|
2
|
-
// isolation from com.android.billingclient.* objects. handlers-billing.ts adapts native
|
|
3
|
-
// SubscriptionOfferDetails into these plain shapes and delegates the CHOICE here.
|
|
4
|
-
//
|
|
5
|
-
// WHY this exists (money/product-critical): getSubscriptionOfferDetails() returns EVERY offer the
|
|
6
|
-
// user is eligible for, in NO guaranteed order. A trial-eligible (first-time) user's list carries
|
|
7
|
-
// BOTH the base-plan offer (offerId==null, a single paid phase) AND a separate free-trial offer
|
|
8
|
-
// (tag "freetrial", two phases: first priceAmountMicros==0, then the base price). A lapsed /
|
|
9
|
-
// ineligible user gets ONLY the base-plan offer. Blindly taking offers.get(0) can drop the
|
|
10
|
-
// MANDATORY free trial. We must pick the trial offer WHEN PRESENT, else the base plan.
|
|
11
|
-
|
|
12
|
-
/** One pricing phase of an offer (only the fields the selection/pricing logic needs). */
|
|
13
|
-
export interface OfferPhase {
|
|
14
|
-
/** Micros of `currency`; 0 marks a free-trial phase. */
|
|
15
|
-
priceAmountMicros: number;
|
|
16
|
-
/** ISO-8601 billing period, e.g. "P1W", "P1M". */
|
|
17
|
-
billingPeriod?: string;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
/** A single subscription offer, flattened from Play's SubscriptionOfferDetails. */
|
|
21
|
-
export interface OfferCandidate {
|
|
22
|
-
/** null/"" for the base-plan offer; a non-empty id for a promotional offer (trial). */
|
|
23
|
-
offerId: string | null;
|
|
24
|
-
phases: OfferPhase[];
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
/** True when the offer contains a zero-price (free-trial) phase. */
|
|
28
|
-
export function hasFreeTrialPhase(offer: OfferCandidate): boolean {
|
|
29
|
-
return offer.phases.some((p) => p.priceAmountMicros === 0);
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
/**
|
|
33
|
-
* Select the offer to purchase / price from. Prefers the free-trial offer (a zero-price phase) when
|
|
34
|
-
* present, so a trial-eligible user actually gets the trial; otherwise falls back to the base-plan
|
|
35
|
-
* offer (offerId==null), else the first offer. Returns null for an empty list.
|
|
36
|
-
*/
|
|
37
|
-
export function selectSubscriptionOffer<T extends OfferCandidate>(offers: readonly T[]): T | null {
|
|
38
|
-
if (!offers || offers.length === 0) return null;
|
|
39
|
-
const trial = offers.find(hasFreeTrialPhase);
|
|
40
|
-
if (trial) return trial;
|
|
41
|
-
const base = offers.find((o) => o.offerId == null || o.offerId === '');
|
|
42
|
-
return base ?? offers[0];
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
/** The recurring paid phase to display (first phase with a price > 0), or the last phase as fallback. */
|
|
46
|
-
export function recurringPhase(offer: OfferCandidate): OfferPhase | undefined {
|
|
47
|
-
const paid = offer.phases.find((p) => p.priceAmountMicros > 0);
|
|
48
|
-
return paid ?? offer.phases[offer.phases.length - 1];
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
/** The intro/free-trial phase (first zero-price phase), if any. */
|
|
52
|
-
export function introPhase(offer: OfferCandidate): OfferPhase | undefined {
|
|
53
|
-
return offer.phases.find((p) => p.priceAmountMicros === 0);
|
|
54
|
-
}
|