@jskit-ai/mobile-capacitor 0.1.93 → 0.1.95

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jskit-ai/mobile-capacitor",
3
- "version": "0.1.93",
3
+ "version": "0.1.95",
4
4
  "type": "module",
5
5
  "scripts": {
6
6
  "test": "node --test"
@@ -13,8 +13,8 @@
13
13
  "dependencies": {
14
14
  "@capacitor/browser": "^7.0.1",
15
15
  "@capacitor/core": "^7.4.3",
16
- "@jskit-ai/kernel": "0.1.158",
17
- "@jskit-ai/shell-web": "0.1.162"
16
+ "@jskit-ai/kernel": "0.1.160",
17
+ "@jskit-ai/shell-web": "0.1.164"
18
18
  },
19
19
  "description": "Thin Capacitor client integration for JSKIT mobile-shell launch routing and auth callback completion.",
20
20
  "jskit": {
@@ -34,23 +34,14 @@
34
34
  {
35
35
  "entrypoint": "src/client/providers/MobileCapacitorClientProvider.js",
36
36
  "export": "MobileCapacitorClientProvider"
37
+ },
38
+ {
39
+ "entrypoint": "src/client/providers/MobileCapacitorClientProvider.js",
40
+ "export": "MobileCapacitorRuntimeProvider"
37
41
  }
38
42
  ]
39
43
  }
40
44
  },
41
- "lifecycle": {
42
- "install": {
43
- "prepare": {
44
- "entrypoint": "src/server/buildTemplateContext.js",
45
- "export": "prepareInstallHook"
46
- },
47
- "finalize": {
48
- "entrypoint": "src/server/buildTemplateContext.js",
49
- "export": "finalizeInstallHook",
50
- "managesNpmInstall": true
51
- }
52
- }
53
- },
54
45
  "metadata": {
55
46
  "apiSummary": {
56
47
  "surfaces": [
@@ -59,67 +50,11 @@
59
50
  "summary": "Exports the Capacitor client provider, launch adapter helpers, and mobile-shell runtime."
60
51
  }
61
52
  ],
62
- "containerTokens": {
63
- "server": [],
64
- "client": [
65
- "mobile.capacitor.client.runtime",
66
- "mobile.capacitor.adapter.client"
67
- ]
68
- }
53
+ "clientCapabilities": [
54
+ "client.mobile",
55
+ "client.mobile-runtime"
56
+ ]
69
57
  }
70
- },
71
- "mutations": {
72
- "dependencies": {
73
- "runtime": {
74
- "@capacitor/android": "^7.4.3",
75
- "@capacitor/app": "^7.1.0",
76
- "@capacitor/browser": "^7.0.1",
77
- "@capacitor/core": "^7.4.3",
78
- "@jskit-ai/kernel": "0.1.158",
79
- "@jskit-ai/shell-web": "0.1.162"
80
- },
81
- "dev": {
82
- "@capacitor/cli": "^7.4.3"
83
- }
84
- },
85
- "packageJson": {
86
- "scripts": {
87
- "mobile:dev:android": "jskit mobile android dev",
88
- "mobile:devices:android": "jskit mobile android devices",
89
- "mobile:sync:android": "jskit mobile android sync",
90
- "mobile:tunnel:android": "jskit mobile android tunnel",
91
- "mobile:restart:android": "jskit mobile android restart",
92
- "mobile:run:android": "jskit mobile android run",
93
- "mobile:build:web": "npm run build",
94
- "mobile:build:android": "jskit mobile android build"
95
- }
96
- },
97
- "procfile": {},
98
- "text": [],
99
- "files": [
100
- {
101
- "from": "templates/capacitor.config.json",
102
- "to": "capacitor.config.json",
103
- "reason": "Install Capacitor shell configuration rendered from config.mobile.",
104
- "category": "mobile-capacitor",
105
- "id": "mobile-capacitor-config",
106
- "templateContext": {
107
- "entrypoint": "src/server/buildTemplateContext.js",
108
- "export": "buildTemplateContext"
109
- }
110
- },
111
- {
112
- "from": "templates/mobile-capacitor.md",
113
- "to": ".jskit/mobile-capacitor.md",
114
- "reason": "Install managed mobile-shell notes for the Capacitor Android integration.",
115
- "category": "mobile-capacitor",
116
- "id": "mobile-capacitor-notes",
117
- "templateContext": {
118
- "entrypoint": "src/server/buildTemplateContext.js",
119
- "export": "buildTemplateContext"
120
- }
121
- }
122
- ]
123
58
  }
124
59
  }
125
60
  }
@@ -0,0 +1,71 @@
1
+ ---
2
+ id: mobile/android-application
3
+ title: Capacitor Android application
4
+ summary: Wrap a JSKIT web application in a Capacitor Android shell using native Capacitor and Android commands.
5
+ keywords: android, capacitor, device, mobile, native, shell, webview
6
+ requires: @jskit-ai/mobile-capacitor, @capacitor/android, @capacitor/app, @capacitor/cli
7
+ ---
8
+
9
+ # Capacitor Android application
10
+
11
+ ## Use when
12
+
13
+ Use this pattern when the same JSKIT application should run inside a Capacitor
14
+ Android shell and the product accepts a WebView-based native application.
15
+
16
+ ## Do not use when
17
+
18
+ Do not use this pattern for a browser-only app, for a native UI product, or when
19
+ the target platform is not supported by the selected Capacitor packages.
20
+
21
+ ## Product decisions
22
+
23
+ Choose the stable application id, display name, built web directory, callback
24
+ URLs, development server policy, and native permissions before creating the
25
+ Android project.
26
+
27
+ ## Framework APIs
28
+
29
+ Install the declared npm packages normally. Adapt `example/capacitor.config.json`
30
+ into valid JSON, then use Capacitor directly:
31
+
32
+ ```text
33
+ npx cap add android
34
+ npm run build
35
+ npx cap sync android
36
+ npx cap run android
37
+ ```
38
+
39
+ Use `adb devices`, `adb reverse`, Android Studio, and Gradle directly for device
40
+ selection, tunnelling, diagnostics, and release builds. JSKIT does not wrap
41
+ those tools in another command language.
42
+
43
+ ## Example files
44
+
45
+ `example/capacitor.config.json` shows the small Capacitor configuration shape.
46
+ Replace its obvious values directly; it is not an interpolation template.
47
+
48
+ ## Variation points
49
+
50
+ Change application id/name, build directory, development server, callback URLs,
51
+ plugins, native permissions, and platform-specific release configuration.
52
+
53
+ ## Invariants
54
+
55
+ - `webDir` is the actual output of the application's production build.
56
+ - Development origins are explicit and never leak into release configuration.
57
+ - Browser navigation that leaves the app uses the package's public mobile
58
+ routing capability.
59
+ - Native projects and signing configuration are ordinary app-owned source.
60
+
61
+ ## Verification
62
+
63
+ Build the web application, sync Android, launch on an explicit device, test
64
+ callback routing and external links, then build the native release variant.
65
+
66
+ ## Avoid
67
+
68
+ - hidden device selection
69
+ - checking secrets or signing keys into source
70
+ - a JSKIT-specific wrapper around Capacitor, adb, or Gradle
71
+ - generated-operation receipts
@@ -1,13 +1,10 @@
1
- import { MobileCapacitorClientProvider } from "./providers/MobileCapacitorClientProvider.js";
2
-
3
- export { MobileCapacitorClientProvider } from "./providers/MobileCapacitorClientProvider.js";
1
+ export {
2
+ MobileCapacitorClientProvider,
3
+ MobileCapacitorRuntimeProvider
4
+ } from "./providers/MobileCapacitorClientProvider.js";
4
5
  export { createMobileCapacitorRuntime } from "./runtime/mobileCapacitorRuntime.js";
5
6
  export {
6
7
  createGlobalCapacitorAppAdapter,
7
8
  createNoopCapacitorAppAdapter,
8
9
  resolveCapacitorAppPlugin
9
10
  } from "./runtime/globalCapacitorAppAdapter.js";
10
-
11
- const clientProviders = Object.freeze([MobileCapacitorClientProvider]);
12
-
13
- export { clientProviders };
@@ -1,17 +1,14 @@
1
+ import { defineProvider } from "@jskit-ai/kernel/shared/capabilities";
1
2
  import { getClientAppConfig, resolveMobileConfig } from "@jskit-ai/kernel/client";
2
3
  import { createGlobalCapacitorAppAdapter } from "../runtime/globalCapacitorAppAdapter.js";
3
4
  import { createCapacitorAwareFetch } from "../runtime/apiRequestClient.js";
4
5
  import { createMobileCapacitorRuntime } from "../runtime/mobileCapacitorRuntime.js";
5
6
  import { createCapacitorAwareOAuthLaunchClient } from "../runtime/oauthLaunchClient.js";
6
7
 
7
- const AUTH_OAUTH_LAUNCH_CLIENT_TOKEN = "auth.oauth-launch.client";
8
8
  const GLOBAL_FETCH_RESTORE_KEY = Symbol.for("jskit.mobile.capacitor.restoreFetch");
9
9
 
10
10
  function installCapacitorAwareGlobalFetch({ adapter = null, apiBaseUrl = "", globalObject = globalThis } = {}) {
11
- if (!globalObject || typeof globalObject !== "object") {
12
- return null;
13
- }
14
- if (adapter?.available !== true) {
11
+ if (!globalObject || typeof globalObject !== "object" || adapter?.available !== true) {
15
12
  return null;
16
13
  }
17
14
  if (typeof globalObject.fetch !== "function") {
@@ -41,104 +38,70 @@ function installCapacitorAwareGlobalFetch({ adapter = null, apiBaseUrl = "", glo
41
38
  return restore;
42
39
  }
43
40
 
44
- class MobileCapacitorClientProvider {
45
- static id = "mobile.capacitor.client";
46
-
47
- static startsAfter = ["shell.web.client"];
48
-
49
- register(app) {
50
- if (!app || typeof app.singleton !== "function") {
51
- throw new Error("MobileCapacitorClientProvider requires application singleton().");
52
- }
53
-
54
- let adapterInstance = null;
55
- if (!app.has || app.has("mobile.capacitor.adapter.client") !== true) {
56
- adapterInstance = createGlobalCapacitorAppAdapter();
57
- app.singleton("mobile.capacitor.adapter.client", () => adapterInstance);
58
- } else if (typeof app.make === "function") {
59
- adapterInstance = app.make("mobile.capacitor.adapter.client");
60
- }
61
-
62
- const mobileConfig = resolveMobileConfig(getClientAppConfig());
63
- installCapacitorAwareGlobalFetch({
64
- adapter: adapterInstance,
65
- apiBaseUrl: mobileConfig.apiBaseUrl
66
- });
67
-
68
- if (!app.has || app.has(AUTH_OAUTH_LAUNCH_CLIENT_TOKEN) !== true) {
69
- app.singleton(AUTH_OAUTH_LAUNCH_CLIENT_TOKEN, (scope) => {
70
- return createCapacitorAwareOAuthLaunchClient({
71
- adapter: scope.make("mobile.capacitor.adapter.client"),
72
- apiBaseUrl: mobileConfig.apiBaseUrl
73
- });
74
- });
75
- }
76
- app.singleton("mobile.capacitor.client.runtime", (scope) => {
77
- if (!scope.has("jskit.client.router")) {
78
- throw new Error("MobileCapacitorClientProvider requires jskit.client.router.");
79
- }
80
-
81
- const placementRuntime = scope.has("runtime.web-placement.client")
82
- ? scope.make("runtime.web-placement.client")
83
- : null;
84
- const authCallbackCompleter = scope.has("auth.mobile-callback.client")
85
- ? scope.make("auth.mobile-callback.client")
86
- : null;
87
- const authGuardRuntime = scope.has("runtime.auth-guard.client")
88
- ? scope.make("runtime.auth-guard.client")
89
- : null;
90
-
91
- return createMobileCapacitorRuntime({
92
- router: scope.make("jskit.client.router"),
93
- mobileConfig: getClientAppConfig().mobile || {},
94
- adapter: scope.make("mobile.capacitor.adapter.client"),
95
- placementRuntime,
96
- authCallbackCompleter,
97
- authGuardRuntime
98
- });
41
+ const MobileCapacitorClientProvider = defineProvider({
42
+ id: "mobile.capacitor.client",
43
+ provides: {
44
+ mobile: "client.mobile"
45
+ },
46
+ setup() {
47
+ const adapter = createGlobalCapacitorAppAdapter();
48
+ const config = resolveMobileConfig(getClientAppConfig());
49
+ const restoreFetch = installCapacitorAwareGlobalFetch({
50
+ adapter,
51
+ apiBaseUrl: config.apiBaseUrl
99
52
  });
53
+ return {
54
+ mobile: Object.freeze({
55
+ adapter,
56
+ config,
57
+ oauthLaunch: createCapacitorAwareOAuthLaunchClient({
58
+ adapter,
59
+ apiBaseUrl: config.apiBaseUrl
60
+ }),
61
+ restoreFetch
62
+ })
63
+ };
64
+ },
65
+ shutdown(_dependencies, { outputs }) {
66
+ outputs.mobile.restoreFetch?.();
100
67
  }
101
-
102
- async boot(app) {
103
- if (!app || typeof app.make !== "function" || typeof app.has !== "function") {
104
- throw new Error("MobileCapacitorClientProvider boot requires application make()/has().");
105
- }
106
-
107
- if (!app.has("mobile.capacitor.client.runtime")) {
108
- return;
109
- }
110
-
111
- const mobileConfig = resolveMobileConfig(getClientAppConfig());
112
- installCapacitorAwareGlobalFetch({
113
- adapter: app.make("mobile.capacitor.adapter.client"),
114
- apiBaseUrl: mobileConfig.apiBaseUrl
115
- });
116
-
117
- const runtime = app.make("mobile.capacitor.client.runtime");
118
- if (runtime && typeof runtime.initialize === "function") {
119
- await runtime.initialize();
120
- }
121
- }
122
-
123
- shutdown(app) {
124
- if (!app || typeof app.has !== "function" || typeof app.make !== "function") {
125
- return;
126
- }
127
-
128
- if (!app.has("mobile.capacitor.client.runtime")) {
129
- return;
130
- }
131
-
132
- const restoreFetch = globalThis[GLOBAL_FETCH_RESTORE_KEY];
133
- if (typeof restoreFetch === "function") {
134
- restoreFetch();
135
- }
136
-
137
- const runtime = app.make("mobile.capacitor.client.runtime");
138
- if (runtime && typeof runtime.dispose === "function") {
139
- runtime.dispose();
140
- }
68
+ });
69
+
70
+ const MobileCapacitorRuntimeProvider = defineProvider({
71
+ id: "mobile.capacitor.runtime.client",
72
+ requires: {
73
+ mobile: "client.mobile",
74
+ router: "client.router"
75
+ },
76
+ optional: {
77
+ auth: "client.auth",
78
+ shell: "client.shell"
79
+ },
80
+ provides: {
81
+ mobileRuntime: "client.mobile-runtime"
82
+ },
83
+ setup({ auth, mobile, router, shell }) {
84
+ return {
85
+ mobileRuntime: createMobileCapacitorRuntime({
86
+ router,
87
+ mobileConfig: getClientAppConfig().mobile || {},
88
+ adapter: mobile.adapter,
89
+ placementRuntime: shell?.placement || null,
90
+ authCallbackCompleter: auth?.mobileCallback || null,
91
+ authGuardRuntime: auth?.guard || null
92
+ })
93
+ };
94
+ },
95
+ async boot(_dependencies, { outputs }) {
96
+ await outputs.mobileRuntime.initialize();
97
+ },
98
+ shutdown(_dependencies, { outputs }) {
99
+ outputs.mobileRuntime.dispose?.();
141
100
  }
142
- }
101
+ });
143
102
 
144
- export { MobileCapacitorClientProvider, installCapacitorAwareGlobalFetch };
103
+ export {
104
+ MobileCapacitorClientProvider,
105
+ MobileCapacitorRuntimeProvider,
106
+ installCapacitorAwareGlobalFetch
107
+ };
@@ -1,88 +1,44 @@
1
1
  import assert from "node:assert/strict";
2
2
  import test from "node:test";
3
- import { MobileCapacitorClientProvider } from "../src/client/providers/MobileCapacitorClientProvider.js";
3
+ import { createCapabilityRuntime, defineProvider } from "@jskit-ai/kernel/shared/capabilities";
4
+ import {
5
+ MobileCapacitorClientProvider,
6
+ MobileCapacitorRuntimeProvider
7
+ } from "../src/client/providers/MobileCapacitorClientProvider.js";
4
8
 
5
- function createAppDouble({
6
- adapter = null,
7
- authCallbackCompleter = null,
8
- authGuardRuntime = null,
9
- placementRuntime = null
10
- } = {}) {
11
- const singletons = new Map();
12
- const singletonInstances = new Map();
13
- const runtime = {
14
- initializeCalls: 0,
15
- disposeCalls: 0
16
- };
17
-
18
- const app = {
19
- singleton(token, factory) {
20
- singletons.set(token, factory);
21
- },
22
- has(token) {
23
- if (token === "jskit.client.router") {
24
- return true;
25
- }
26
- if (token === "runtime.web-placement.client") {
27
- return Boolean(placementRuntime);
28
- }
29
- if (token === "auth.mobile-callback.client") {
30
- return Boolean(authCallbackCompleter);
31
- }
32
- if (token === "runtime.auth-guard.client") {
33
- return Boolean(authGuardRuntime);
34
- }
35
- if (token === "auth.oauth-launch.client") {
36
- return singletons.has(token) || singletonInstances.has(token);
9
+ function createRouter() {
10
+ return {
11
+ currentRoute: {
12
+ value: {
13
+ fullPath: "/home"
37
14
  }
38
- return singletons.has(token) || singletonInstances.has(token);
39
15
  },
40
- make(token) {
41
- if (token === "jskit.client.router") {
42
- return {
43
- currentRoute: {
44
- value: {
45
- fullPath: "/home"
46
- }
47
- },
48
- async replace() {}
49
- };
50
- }
51
- if (token === "runtime.web-placement.client") {
52
- return placementRuntime;
53
- }
54
- if (token === "auth.mobile-callback.client") {
55
- return authCallbackCompleter;
56
- }
57
- if (token === "runtime.auth-guard.client") {
58
- return authGuardRuntime;
59
- }
60
- if (singletonInstances.has(token)) {
61
- return singletonInstances.get(token);
62
- }
63
- const factory = singletons.get(token);
64
- if (!factory) {
65
- throw new Error(`Unknown token ${String(token)}`);
66
- }
67
- const instance = factory(this);
68
- singletonInstances.set(token, instance);
69
- return instance;
70
- }
16
+ async replace() {},
17
+ back() {}
71
18
  };
19
+ }
72
20
 
73
- if (adapter) {
74
- app.singleton("mobile.capacitor.adapter.client", () => adapter);
75
- }
76
-
77
- return {
78
- app,
79
- runtime,
80
- singletons,
81
- singletonInstances
82
- };
21
+ function createMobileProbe(onCapabilities) {
22
+ return defineProvider({
23
+ id: "test.mobile.capacitor.probe",
24
+ requires: {
25
+ mobile: "client.mobile",
26
+ mobileRuntime: "client.mobile-runtime"
27
+ },
28
+ setup(capabilities) {
29
+ onCapabilities(capabilities);
30
+ return {};
31
+ }
32
+ });
83
33
  }
84
34
 
85
- test("MobileCapacitorClientProvider registers and boots the mobile runtime", async () => {
35
+ test("mobile providers publish, boot, and dispose their declared capabilities", async () => {
36
+ const originalCapacitor = globalThis.Capacitor;
37
+ const originalConfig = globalThis.__JSKIT_CLIENT_APP_CONFIG__;
38
+ const listenerEvents = [];
39
+ const removedListeners = [];
40
+ let capabilities = null;
41
+
86
42
  globalThis.__JSKIT_CLIENT_APP_CONFIG__ = {
87
43
  mobile: {
88
44
  enabled: true,
@@ -91,52 +47,67 @@ test("MobileCapacitorClientProvider registers and boots the mobile runtime", asy
91
47
  }
92
48
  }
93
49
  };
94
-
95
- try {
96
- const adapter = {
97
- available: true,
98
- async getInitialLaunchUrl() {
99
- return "";
100
- },
101
- subscribeToLaunchUrls() {
102
- return () => {};
50
+ globalThis.Capacitor = {
51
+ Plugins: {
52
+ App: {
53
+ async getLaunchUrl() {
54
+ return { url: "" };
55
+ },
56
+ async addListener(eventName) {
57
+ listenerEvents.push(eventName);
58
+ return {
59
+ remove() {
60
+ removedListeners.push(eventName);
61
+ }
62
+ };
63
+ }
103
64
  }
104
- };
105
- const { app } = createAppDouble({ adapter });
106
- const provider = new MobileCapacitorClientProvider();
107
-
108
- provider.register(app);
109
-
110
- assert.equal(app.has("mobile.capacitor.adapter.client"), true);
111
- assert.equal(app.has("mobile.capacitor.client.runtime"), true);
112
- assert.equal(app.has("auth.oauth-launch.client"), true);
65
+ }
66
+ };
113
67
 
114
- const runtime = app.make("mobile.capacitor.client.runtime");
115
- assert.equal(typeof runtime.initialize, "function");
116
- assert.equal(runtime.getState().available, true);
68
+ const runtime = createCapabilityRuntime({
69
+ inputs: {
70
+ "client.router": createRouter()
71
+ },
72
+ providers: [
73
+ MobileCapacitorClientProvider,
74
+ MobileCapacitorRuntimeProvider,
75
+ createMobileProbe((value) => {
76
+ capabilities = value;
77
+ })
78
+ ]
79
+ });
117
80
 
118
- await provider.boot(app);
119
- assert.equal(runtime.getState().initialized, true);
81
+ try {
82
+ await runtime.start();
83
+ assert.equal(capabilities.mobile.adapter.available, true);
84
+ assert.equal(typeof capabilities.mobile.oauthLaunch.open, "function");
85
+ assert.deepEqual(capabilities.mobileRuntime.getState(), {
86
+ initialized: true,
87
+ available: true,
88
+ enabled: true,
89
+ lastAppliedPath: ""
90
+ });
91
+ assert.deepEqual(listenerEvents.sort(), ["appUrlOpen", "backButton"]);
120
92
 
121
- provider.shutdown(app);
122
- assert.equal(runtime.getState().initialized, false);
93
+ await runtime.shutdown();
94
+ assert.equal(capabilities.mobileRuntime.getState().initialized, false);
95
+ await Promise.resolve();
96
+ assert.deepEqual(removedListeners.sort(), ["appUrlOpen", "backButton"]);
123
97
  } finally {
124
- delete globalThis.__JSKIT_CLIENT_APP_CONFIG__;
98
+ globalThis.Capacitor = originalCapacitor;
99
+ globalThis.__JSKIT_CLIENT_APP_CONFIG__ = originalConfig;
125
100
  }
126
101
  });
127
102
 
128
- test("MobileCapacitorClientProvider installs and restores the Capacitor fetch wrapper", async () => {
103
+ test("mobile provider installs and restores the Capacitor-aware fetch wrapper", async () => {
104
+ const originalCapacitor = globalThis.Capacitor;
129
105
  const originalConfig = globalThis.__JSKIT_CLIENT_APP_CONFIG__;
130
106
  const originalFetch = globalThis.fetch;
131
107
  const fetchCalls = [];
132
108
  const stubFetch = async (url, options) => {
133
- fetchCalls.push({
134
- url,
135
- options
136
- });
137
- return {
138
- ok: true
139
- };
109
+ fetchCalls.push({ url, options });
110
+ return { ok: true };
140
111
  };
141
112
 
142
113
  globalThis.__JSKIT_CLIENT_APP_CONFIG__ = {
@@ -148,39 +119,40 @@ test("MobileCapacitorClientProvider installs and restores the Capacitor fetch wr
148
119
  }
149
120
  }
150
121
  };
151
- globalThis.fetch = stubFetch;
152
-
153
- try {
154
- const { app } = createAppDouble({
155
- adapter: {
156
- available: true,
157
- async getInitialLaunchUrl() {
158
- return "";
122
+ globalThis.Capacitor = {
123
+ Plugins: {
124
+ App: {
125
+ async getLaunchUrl() {
126
+ return { url: "" };
159
127
  },
160
- subscribeToLaunchUrls() {
161
- return () => {};
128
+ async addListener() {
129
+ return { remove() {} };
162
130
  }
163
131
  }
164
- });
165
- const provider = new MobileCapacitorClientProvider();
132
+ }
133
+ };
134
+ globalThis.fetch = stubFetch;
166
135
 
167
- provider.register(app);
168
- await globalThis.fetch("/api/session", {
169
- method: "GET"
170
- });
171
- await provider.boot(app);
172
- provider.shutdown(app);
136
+ const runtime = createCapabilityRuntime({
137
+ inputs: {
138
+ "client.router": createRouter()
139
+ },
140
+ providers: [MobileCapacitorClientProvider, MobileCapacitorRuntimeProvider]
141
+ });
173
142
 
174
- assert.deepEqual(fetchCalls, [
175
- {
176
- url: "http://127.0.0.1:3000/api/session",
177
- options: {
178
- method: "GET"
179
- }
180
- }
181
- ]);
143
+ try {
144
+ await runtime.start();
145
+ assert.notEqual(globalThis.fetch, stubFetch);
146
+ await globalThis.fetch("/api/session", { method: "GET" });
147
+ await runtime.shutdown();
148
+
149
+ assert.deepEqual(fetchCalls, [{
150
+ url: "http://127.0.0.1:3000/api/session",
151
+ options: { method: "GET" }
152
+ }]);
182
153
  assert.equal(globalThis.fetch, stubFetch);
183
154
  } finally {
155
+ globalThis.Capacitor = originalCapacitor;
184
156
  globalThis.__JSKIT_CLIENT_APP_CONFIG__ = originalConfig;
185
157
  globalThis.fetch = originalFetch;
186
158
  }
@@ -1,231 +0,0 @@
1
- import path from "node:path";
2
- import { readdir, rm } from "node:fs/promises";
3
- import { loadAppConfigFromAppRoot, resolveMobileConfig } from "@jskit-ai/kernel/server/support";
4
- import { normalizeText } from "@jskit-ai/kernel/shared/support/normalize";
5
-
6
- function requireNonEmptyText(value, label = "value") {
7
- const normalized = normalizeText(value);
8
- if (!normalized) {
9
- throw new Error(`${label} is required in config.mobile before installing Capacitor support.`);
10
- }
11
- return normalized;
12
- }
13
-
14
- function requireUrl(value, label = "value", { allowHttp = true, allowHttps = true } = {}) {
15
- const normalized = requireNonEmptyText(value, label);
16
- let parsed = null;
17
- try {
18
- parsed = new URL(normalized);
19
- } catch {
20
- throw new Error(`${label} must be a valid absolute URL in config.mobile.`);
21
- }
22
-
23
- const protocol = String(parsed.protocol || "").toLowerCase();
24
- if ((protocol === "http:" && allowHttp !== true) || (protocol === "https:" && allowHttps !== true)) {
25
- throw new Error(`${label} must use an allowed protocol in config.mobile.`);
26
- }
27
- if (protocol !== "http:" && protocol !== "https:") {
28
- throw new Error(`${label} must use http or https in config.mobile.`);
29
- }
30
-
31
- return parsed.toString();
32
- }
33
-
34
- function buildCapacitorServerBlock(mobileConfig = {}) {
35
- if (mobileConfig.assetMode !== "dev_server") {
36
- return "";
37
- }
38
-
39
- const devServerUrl = requireUrl(mobileConfig.devServerUrl, "config.mobile.devServerUrl");
40
- const cleartext = String(new URL(devServerUrl).protocol || "").toLowerCase() === "http:";
41
-
42
- return [
43
- ",",
44
- ' "server": {',
45
- ` "url": ${JSON.stringify(devServerUrl)},`,
46
- ` "cleartext": ${cleartext}`,
47
- " }"
48
- ].join("\n");
49
- }
50
-
51
- function buildAppLinkDomainsValue(appLinkDomains = []) {
52
- const domains = Array.isArray(appLinkDomains) ? appLinkDomains : [];
53
- if (domains.length < 1) {
54
- return "(none)";
55
- }
56
- return domains.join(", ");
57
- }
58
-
59
- async function directoryContainsAnyFiles(directoryPath = "") {
60
- const entries = await readdir(directoryPath, { withFileTypes: true });
61
- for (const entry of entries) {
62
- const absoluteChildPath = path.join(directoryPath, entry.name);
63
- if (entry.isFile() || entry.isSymbolicLink()) {
64
- return true;
65
- }
66
- if (entry.isDirectory() && await directoryContainsAnyFiles(absoluteChildPath)) {
67
- return true;
68
- }
69
- }
70
- return false;
71
- }
72
-
73
- async function buildTemplateContext({ appRoot } = {}) {
74
- const mergedConfig = await loadAppConfigFromAppRoot({
75
- appRoot: path.resolve(String(appRoot || ""))
76
- });
77
- const mobileConfig = resolveMobileConfig({
78
- mobile: mergedConfig.mobile
79
- });
80
-
81
- if (mobileConfig.enabled !== true) {
82
- throw new Error("config.mobile.enabled must be true before installing Capacitor support.");
83
- }
84
- if (mobileConfig.strategy !== "capacitor") {
85
- throw new Error('config.mobile.strategy must be "capacitor" before installing Capacitor support.');
86
- }
87
-
88
- const appId = requireNonEmptyText(mobileConfig.appId, "config.mobile.appId");
89
- const appName = requireNonEmptyText(mobileConfig.appName, "config.mobile.appName");
90
- const apiBaseUrl = requireUrl(mobileConfig.apiBaseUrl, "config.mobile.apiBaseUrl");
91
- const callbackPath = requireNonEmptyText(mobileConfig.auth.callbackPath, "config.mobile.auth.callbackPath");
92
- const customScheme = requireNonEmptyText(mobileConfig.auth.customScheme, "config.mobile.auth.customScheme");
93
- const androidPackageName = requireNonEmptyText(
94
- mobileConfig.android.packageName,
95
- "config.mobile.android.packageName"
96
- );
97
- const androidVersionName = requireNonEmptyText(
98
- mobileConfig.android.versionName,
99
- "config.mobile.android.versionName"
100
- );
101
-
102
- return {
103
- "__JSKIT_MOBILE_CAPACITOR_APP_ID__": appId,
104
- "__JSKIT_MOBILE_CAPACITOR_APP_NAME__": appName,
105
- "__JSKIT_MOBILE_CAPACITOR_WEB_DIR__": "dist",
106
- "__JSKIT_MOBILE_CAPACITOR_SERVER_BLOCK__": buildCapacitorServerBlock(mobileConfig),
107
- "__JSKIT_MOBILE_CAPACITOR_ASSET_MODE__": mobileConfig.assetMode,
108
- "__JSKIT_MOBILE_CAPACITOR_DEV_SERVER_URL__": mobileConfig.devServerUrl || "(unused)",
109
- "__JSKIT_MOBILE_CAPACITOR_API_BASE_URL__": apiBaseUrl,
110
- "__JSKIT_MOBILE_CAPACITOR_CALLBACK_PATH__": callbackPath,
111
- "__JSKIT_MOBILE_CAPACITOR_CUSTOM_SCHEME__": customScheme,
112
- "__JSKIT_MOBILE_CAPACITOR_APP_LINK_DOMAINS__": buildAppLinkDomainsValue(mobileConfig.auth.appLinkDomains),
113
- "__JSKIT_MOBILE_CAPACITOR_ANDROID_PACKAGE_NAME__": androidPackageName,
114
- "__JSKIT_MOBILE_CAPACITOR_ANDROID_MIN_SDK__": String(mobileConfig.android.minSdk),
115
- "__JSKIT_MOBILE_CAPACITOR_ANDROID_TARGET_SDK__": String(mobileConfig.android.targetSdk),
116
- "__JSKIT_MOBILE_CAPACITOR_ANDROID_VERSION_CODE__": String(mobileConfig.android.versionCode),
117
- "__JSKIT_MOBILE_CAPACITOR_ANDROID_VERSION_NAME__": androidVersionName
118
- };
119
- }
120
-
121
- async function prepareInstallHook({
122
- appRoot,
123
- appPackageJson = {},
124
- io,
125
- dryRun = false,
126
- helpers = {}
127
- } = {}) {
128
- const ensureManagedMobileConfig = helpers?.ensureManagedMobileConfig;
129
- if (typeof ensureManagedMobileConfig !== "function") {
130
- throw new Error("install hook helpers.ensureManagedMobileConfig is required.");
131
- }
132
-
133
- const addedConfigStub = await ensureManagedMobileConfig({
134
- dryRun
135
- });
136
- if (dryRun === true && addedConfigStub) {
137
- return {
138
- stopInstall: true,
139
- touchedFiles: ["config/public.js"],
140
- stopMessage:
141
- "[dry-run] mobile package install preview stops after the config.mobile stub because rendered Capacitor files depend on those values."
142
- };
143
- }
144
-
145
- return {};
146
- }
147
-
148
- async function finalizeInstallHook({
149
- appRoot,
150
- io,
151
- dryRun = false,
152
- skipManagedFinalize = false,
153
- helpers = {}
154
- } = {}) {
155
- if (skipManagedFinalize === true) {
156
- return {};
157
- }
158
-
159
- const installAppDependencies = helpers?.installAppDependencies;
160
- const runProjectBinary = helpers?.runProjectBinary;
161
- const helperFileExists = helpers?.fileExists;
162
- const collectShellInstallIssues = helpers?.collectCapacitorShellInstallIssues;
163
- const ensureDeepLinks = helpers?.ensureAndroidManifestDeepLinks;
164
- const ensureNativeShellIdentity = helpers?.ensureAndroidNativeShellIdentity;
165
- if (typeof installAppDependencies !== "function") {
166
- throw new Error("install hook helpers.installAppDependencies is required.");
167
- }
168
- if (typeof runProjectBinary !== "function") {
169
- throw new Error("install hook helpers.runProjectBinary is required.");
170
- }
171
- if (typeof helperFileExists !== "function") {
172
- throw new Error("install hook helpers.fileExists is required.");
173
- }
174
- if (typeof collectShellInstallIssues !== "function") {
175
- throw new Error("install hook helpers.collectCapacitorShellInstallIssues is required.");
176
- }
177
- if (typeof ensureDeepLinks !== "function") {
178
- throw new Error("install hook helpers.ensureAndroidManifestDeepLinks is required.");
179
- }
180
- if (typeof ensureNativeShellIdentity !== "function") {
181
- throw new Error("install hook helpers.ensureAndroidNativeShellIdentity is required.");
182
- }
183
-
184
- await installAppDependencies({
185
- dryRun
186
- });
187
-
188
- const androidDirectoryPath = path.join(appRoot, "android");
189
- const shellInstallIssues = await collectShellInstallIssues();
190
- if (shellInstallIssues.length < 1) {
191
- io?.stdout?.write("[mobile] Android shell is already installed. Skipping cap add android.\n");
192
- } else {
193
- if (await helperFileExists(androidDirectoryPath)) {
194
- io?.stdout?.write("[mobile] Android shell is partial or stale. Reprovisioning it with Capacitor CLI because these artifacts are missing:\n");
195
- for (const issue of shellInstallIssues) {
196
- io?.stdout?.write(`- ${issue}\n`);
197
- }
198
- const androidDirectoryHasFiles = await directoryContainsAnyFiles(androidDirectoryPath);
199
- if (!androidDirectoryHasFiles) {
200
- io?.stdout?.write("[mobile] android/ exists but contains no files. Removing the empty partial shell before reprovisioning.\n");
201
- if (!dryRun) {
202
- await rm(androidDirectoryPath, { recursive: true, force: true });
203
- }
204
- } else {
205
- throw new Error(
206
- "android/ exists but the Capacitor shell is incomplete. JSKIT will not delete a non-empty native tree automatically. Remove android/ and rerun the install."
207
- );
208
- }
209
- } else {
210
- io?.stdout?.write("[mobile] Android shell is not installed yet. Provisioning it with Capacitor CLI.\n");
211
- }
212
- await runProjectBinary("cap", ["add", "android"], {
213
- explanation: "[mobile] Provisioning Android shell with Capacitor CLI:",
214
- dryRun
215
- });
216
- if (!dryRun) {
217
- io?.stdout?.write("[mobile] Added Android shell with Capacitor CLI.\n");
218
- }
219
- }
220
- await ensureDeepLinks({
221
- dryRun
222
- });
223
- await ensureNativeShellIdentity({
224
- dryRun
225
- });
226
- return {
227
- touchedFiles: ["android"]
228
- };
229
- }
230
-
231
- export { buildTemplateContext, prepareInstallHook, finalizeInstallHook };
@@ -1,46 +0,0 @@
1
- # Mobile Capacitor
2
-
3
- This file is managed by `@jskit-ai/mobile-capacitor`.
4
-
5
- Installed contract:
6
-
7
- - strategy: `capacitor`
8
- - asset mode: `__JSKIT_MOBILE_CAPACITOR_ASSET_MODE__`
9
- - dev server url: `__JSKIT_MOBILE_CAPACITOR_DEV_SERVER_URL__`
10
- - API base URL: `__JSKIT_MOBILE_CAPACITOR_API_BASE_URL__`
11
- - auth callback path: `__JSKIT_MOBILE_CAPACITOR_CALLBACK_PATH__`
12
- - custom scheme: `__JSKIT_MOBILE_CAPACITOR_CUSTOM_SCHEME__`
13
- - app link domains: `__JSKIT_MOBILE_CAPACITOR_APP_LINK_DOMAINS__`
14
- - Capacitor app id: `__JSKIT_MOBILE_CAPACITOR_APP_ID__`
15
- - Capacitor app name: `__JSKIT_MOBILE_CAPACITOR_APP_NAME__`
16
- - Android package name: `__JSKIT_MOBILE_CAPACITOR_ANDROID_PACKAGE_NAME__`
17
- - Android min SDK: `__JSKIT_MOBILE_CAPACITOR_ANDROID_MIN_SDK__`
18
- - Android target SDK: `__JSKIT_MOBILE_CAPACITOR_ANDROID_TARGET_SDK__`
19
- - Android version code: `__JSKIT_MOBILE_CAPACITOR_ANDROID_VERSION_CODE__`
20
- - Android version name: `__JSKIT_MOBILE_CAPACITOR_ANDROID_VERSION_NAME__`
21
-
22
- Owned artifacts:
23
-
24
- - `capacitor.config.json`
25
- - `android/` after `jskit add package @jskit-ai/mobile-capacitor` runs `cap add android`
26
- - `android/app/src/main/AndroidManifest.xml` managed deep-link intent filter for the custom scheme
27
-
28
- Managed commands:
29
-
30
- - `jskit add package @jskit-ai/mobile-capacitor`
31
- - `jskit mobile android dev [--target <device-id>]`
32
- - `jskit mobile android devices`
33
- - `jskit mobile android sync`
34
- - `jskit mobile android tunnel --target <device-id>`
35
- - `jskit mobile android restart --target <device-id>`
36
- - `jskit mobile android run [--target <device-id>]`
37
- - `jskit mobile android build`
38
- - `jskit mobile android doctor`
39
-
40
- Current Stage 1 limits:
41
-
42
- - Android only
43
- - web assets stay the JSKIT web client
44
- - OAuth start uses the external browser/custom tab only when the app is running inside the Capacitor shell
45
- - auth/deep-link handling stays routed through normal JSKIT paths
46
- - native app-link verification is still out of scope for Stage 1