Skip to content

Commit 12e7d05

Browse files
author
Ajit Kumar
committed
fix: dev server and plugin compatibility
1 parent 60b235a commit 12e7d05

9 files changed

Lines changed: 472 additions & 56 deletions

File tree

‎.vscode/settings.json‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,7 @@
122122
"endregion",
123123
"ensime",
124124
"ERUDA",
125+
"FDROID",
125126
"filebrowser",
126127
"filesize",
127128
"Fira",

‎CONTRIBUTING.md‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,7 @@ The APK will be at: `platforms/android/app/build/outputs/apk/<edition>/debug/app
138138
- `platforms/android/app/src/main/assets/services.json`: native service registration.
139139
- `package.json`: app ID (`name`), version and Android version code.
140140

141-
The native APIs are available through `Bridge.exec`, `Bridge.file`, `Bridge.http`, `Bridge.clipboard` and `Bridge.websocket`. Native globals and the `deviceready` event remain available. Extensions that directly use the previous native bridge must update to these names; source modules use ordinary imports.
141+
The native APIs are available through `Bridge.exec`, `Bridge.file`, `Bridge.http`, `Bridge.clipboard` and `Bridge.websocket`. App source uses these APIs or ordinary imports. For existing third-party plugins, `src/native/pluginCompatibility.js` exposes the legacy `cordova` namespace and module names for the public native APIs, forwarding to the same implementations. Existing direct globals and `deviceready`, pause/resume and hardware-button events remain available. Keep compatibility aliases in that file; do not use them inside Acode or add Cordova dependencies. Advertising and billing APIs retain their build-edition restrictions.
142142

143143
Set `package.json.name` before building or starting development:
144144

@@ -151,6 +151,11 @@ The scripts and Android Studio read this name; there is no free/paid command arg
151151
After changing the name, restart `npm run dev`. Only the selected Gradle flavor is enabled.
152152
Use `npm run build` to refresh web assets before building directly in Android Studio, which uses the last compiled web bundle.
153153
`npm run dev` hot-reloads JavaScript through Rspack and rebuilds the app when tracked Android source changes.
154+
Startup probes the dev server with Proteus's three-second timeout and loads its
155+
scripts when reachable, or uses the APK's bundled assets when unavailable. The
156+
page stays at `https://localhost` so API CORS permissions, cookies and local
157+
storage keep the same origin. Lazy-loaded assets use the loaded bundle's URL.
158+
Stop and reopen the app after disconnecting the server to use the bundled build.
154159
Gradle only compiles native source and packages the compiled web assets; no Java/Kotlin source is copied or generated by project scripts.
155160
Paid builds exclude the AdMob native sources, Google ads/consent SDKs, manifest entries,
156161
and JavaScript bridge. The editor uses small inactive ads APIs in paid builds, so

‎dev/scripts/dev.js‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,8 @@
99
* 3. Acode Android build/install (after first successful compilation)
1010
* 4. File watcher on native sources and JavaScript APIs for rebuilds
1111
*
12-
* The app loads boot.js from the APK assets; boot.js detects DEV_MODE and
13-
* fetches the latest main.js / main.css from the dev server over HTTP.
12+
* The app stays at https://localhost and loads dev scripts when reachable,
13+
* otherwise using the assets bundled in the APK.
1414
* A WebSocket connection from the app receives "reload" messages on recompile.
1515
*/
1616

‎rspack.config.js‎

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,6 @@ module.exports = (env, options) => {
1010
const devHost = process.env.DEV_HOST || '';
1111
const devPort = process.env.DEV_PORT || '';
1212
const devProto = isDev ? (process.env.DEV_PROTO || '') : '';
13-
const devOrigin = isDev && devHost && devPort && devProto
14-
? ''.concat(devProto, '://', devHost, ':', devPort)
15-
: '';
1613

1714
const typescriptLoader = {
1815
loader: 'builtin:swc-loader',
@@ -156,7 +153,7 @@ module.exports = (env, options) => {
156153
filename: '[name].js',
157154
chunkFilename: '[name].chunk.js',
158155
assetModuleFilename: '[name][ext]',
159-
publicPath: devOrigin ? ''.concat(devOrigin, '/build/') : '/build/',
156+
publicPath: 'auto',
160157
clean: !isDev,
161158
},
162159
// TypeScript's Node-only plugin loader uses a dynamic require. It is never

‎src/boot.js‎

Lines changed: 44 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,3 @@
1-
// boot.js — Entry point loaded by index.html
2-
// Routes between development (HTTP dev server) and production (local assets).
3-
// main.js is never imported directly; it's loaded dynamically so the dev server
4-
// can serve a freshly compiled version on every reload.
5-
61
const DEV_MODE = typeof __DEV_MODE__ !== "undefined" && __DEV_MODE__;
72
const DEV_HOST = typeof __DEV_HOST__ !== "undefined" ? __DEV_HOST__ : "";
83
const DEV_PORT = typeof __DEV_PORT__ !== "undefined" ? __DEV_PORT__ : "";
@@ -12,63 +7,63 @@ const DEV_ORIGIN =
127
? `${DEV_PROTO}://${DEV_HOST}:${DEV_PORT}`
138
: "";
149

15-
const loadScript = (src) =>
16-
new Promise((resolve, reject) => {
10+
(async () => {
11+
let assetOrigin = ".";
12+
if (DEV_MODE && DEV_ORIGIN) {
13+
const controller = new AbortController();
14+
const timeout = setTimeout(() => controller.abort(), 3000);
15+
try {
16+
const response = await fetch(`${DEV_ORIGIN}/build/main.js`, {
17+
method: "HEAD",
18+
cache: "no-store",
19+
signal: controller.signal,
20+
});
21+
if (response.ok) assetOrigin = DEV_ORIGIN;
22+
} catch (error) {
23+
console.error("Error setting dev mode", error);
24+
} finally {
25+
clearTimeout(timeout);
26+
}
27+
}
28+
await bootApp(assetOrigin);
29+
if (assetOrigin === DEV_ORIGIN) connectWS();
30+
})();
31+
32+
function loadScript(src) {
33+
return new Promise((resolve, reject) => {
1734
const el = document.createElement("script");
1835
el.src = src;
1936
el.onload = () => Promise.resolve(window.nativeReady).then(resolve, reject);
2037
el.onerror = reject;
2138
document.head.appendChild(el);
2239
});
40+
}
2341

24-
const loadCSS = (href) => {
42+
function loadCSS(href) {
2543
const el = document.createElement("link");
2644
el.rel = "stylesheet";
2745
el.href = href;
2846
document.head.appendChild(el);
29-
};
30-
31-
const bootDev = async () => {
32-
await loadScript(`${DEV_ORIGIN}/build/native.js`);
33-
loadCSS(`${DEV_ORIGIN}/build/main.css`);
34-
loadScript(`${DEV_ORIGIN}/build/main.js`);
47+
}
3548

49+
function connectWS() {
3650
const wsProto = DEV_PROTO === "https" ? "wss" : "ws";
37-
const connectWS = () => {
38-
let ws;
39-
try {
40-
ws = new WebSocket(`${wsProto}://${DEV_HOST}:${DEV_PORT}`);
41-
} catch {
42-
setTimeout(connectWS, 1000);
43-
return;
44-
}
45-
ws.onmessage = ({ data }) => {
46-
if (data === "reload") location.reload();
47-
};
48-
ws.onclose = () => setTimeout(connectWS, 1000);
49-
ws.onerror = () => {};
51+
let ws;
52+
try {
53+
ws = new WebSocket(`${wsProto}://${DEV_HOST}:${DEV_PORT}`);
54+
} catch {
55+
setTimeout(connectWS, 1000);
56+
return;
57+
}
58+
ws.onmessage = ({ data }) => {
59+
if (data === "reload") location.reload();
5060
};
51-
connectWS();
52-
};
53-
54-
const bootProd = async () => {
55-
await loadScript("./build/native.js");
56-
loadCSS("./build/main.css");
57-
loadScript("./build/main.js");
58-
};
61+
ws.onclose = () => setTimeout(connectWS, 1000);
62+
ws.onerror = () => {};
63+
}
5964

60-
if (DEV_MODE && DEV_ORIGIN) {
61-
fetch(`${DEV_ORIGIN}/build/main.js`, { method: "HEAD", cache: "no-store" })
62-
.then((res) => {
63-
if (res.ok) {
64-
return bootDev();
65-
} else {
66-
return bootProd();
67-
}
68-
})
69-
.catch(() => {
70-
bootProd();
71-
});
72-
} else {
73-
bootProd();
65+
async function bootApp(origin) {
66+
await loadScript(`${origin}/build/native.js`);
67+
loadCSS(`${origin}/build/main.css`);
68+
loadScript(`${origin}/build/main.js`);
7469
}

‎src/native/index.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import device, { initializeDevice } from "./device";
66
import installFileAPI, { file } from "./file";
77
import ftp from "./ftp";
88
import http from "./http/advanced-http";
9+
import installPluginCompatibility from "./pluginCompatibility";
910
import runtime, { expose, initialize, start } from "./runtime";
1011
import sdcard from "./sdcard";
1112
import createServer from "./server";
@@ -45,6 +46,7 @@ async function initializeServices() {
4546
);
4647
if (!__FDROID__)
4748
expose("iap", (await import(/* webpackMode: "eager" */ "./iap")).default);
49+
installPluginCompatibility();
4850
void start(readiness).catch((error) =>
4951
console.error("Native initialization failed", error),
5052
);

‎src/native/pluginCompatibility.js‎

Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
import base64 from "./base64";
2+
import channel from "./channel";
3+
4+
// Only third-party plugins consume this facade. App code uses Bridge/imports.
5+
export default function installPluginCompatibility(host = window) {
6+
if (host.cordova) return;
7+
const bridge = host.Bridge;
8+
const modules = new Map();
9+
const legacyChannel = {
10+
...channel,
11+
onCordovaReady: channel.onBridgeReady,
12+
onPluginsReady: channel.onServicesReady,
13+
waitForInitialization: (name) =>
14+
channel.waitForInitialization(readinessName(name)),
15+
initializationComplete: (name) =>
16+
channel.initializationComplete(readinessName(name)),
17+
};
18+
const cordova = {
19+
plugin: { http: bridge.http },
20+
plugins: { clipboard: bridge.clipboard },
21+
file: bridge.file,
22+
websocket: bridge.websocket,
23+
platformId: bridge.platformId,
24+
version: bridge.version,
25+
platformVersion: bridge.version,
26+
require: requireModule,
27+
define: defineModule,
28+
};
29+
Object.defineProperty(cordova, "exec", { enumerable: true, value: exec });
30+
for (const name of [
31+
"addConstructor",
32+
"addDocumentEventHandler",
33+
"addStickyDocumentEventHandler",
34+
"removeDocumentEventHandler",
35+
"fireDocumentEvent",
36+
"fireWindowEvent",
37+
])
38+
cordova[name] = (...args) => bridge[name](...args);
39+
40+
for (const name of ["pause", "resume"])
41+
host.document.addEventListener(name, (event) =>
42+
channel[name === "pause" ? "onPause" : "onResume"].fire(event),
43+
);
44+
Object.defineProperty(host.device, "cordova", {
45+
enumerable: true,
46+
get: () => bridge.version,
47+
});
48+
const app = host.navigator.app;
49+
let pendingNavigation;
50+
Object.assign(app, {
51+
loadUrl(url, options = {}) {
52+
options ??= {};
53+
pendingNavigation = host.setTimeout(() => {
54+
if (options.clearHistory) app.clearHistory();
55+
if (options.openExternal) host.system.openInBrowser(url);
56+
else host.location.href = url;
57+
}, options.wait || 0);
58+
},
59+
cancelLoadUrl: () => host.clearTimeout(pendingNavigation),
60+
});
61+
// Proteus dismisses the Android splash before any plugin can load.
62+
host.navigator.splashscreen = { show() {}, hide() {} };
63+
register("cordova", cordova);
64+
register("cordova/exec", exec);
65+
register("cordova/channel", legacyChannel);
66+
register("cordova/base64", base64);
67+
register("cordova/plugin/android/app", app);
68+
register("cordova/plugin/android/splashscreen", host.navigator.splashscreen);
69+
register("cordova/plugin/android/statusbar", host.statusbar);
70+
71+
for (const [id, value] of Object.entries({
72+
"cordova-clipboard.Clipboard": bridge.clipboard,
73+
"cordova-plugin-device.device": host.device,
74+
"cordova-plugin-server.CreateServer": host.CreateServer,
75+
"cordova-plugin-ftp.ftp": host.ftp,
76+
"cordova-plugin-sdcard.sdcard": host.sdcard,
77+
"cordova-plugin-websocket.WebSocket": bridge.websocket,
78+
"cordova-plugin-buildinfo.BuildInfo": host.BuildInfo,
79+
"cordova-plugin-sftp.sftp": host.sftp,
80+
"com.foxdebug.acode.rk.exec.terminal.Terminal": host.Terminal,
81+
"com.foxdebug.acode.rk.exec.terminal.Executor": host.Executor,
82+
"cordova-plugin-iap.iap": host.iap,
83+
"com.foxdebug.acode.rk.customtabs.CustomTabs": host.CustomTabs,
84+
"cordova-plugin-advanced-http.http": bridge.http,
85+
"cordova-plugin-system.system": host.system,
86+
"admob-plus-cordova.AdMob": host.admob,
87+
}))
88+
if (value !== undefined) register(id, value);
89+
90+
for (const name of [
91+
"DirectoryEntry",
92+
"DirectoryReader",
93+
"Entry",
94+
"File",
95+
"FileEntry",
96+
"FileError",
97+
"FileReader",
98+
"FileSystem",
99+
"FileUploadOptions",
100+
"FileUploadResult",
101+
"FileWriter",
102+
"Flags",
103+
"LocalFileSystem",
104+
"Metadata",
105+
"ProgressEvent",
106+
"requestFileSystem",
107+
])
108+
register(`cordova-plugin-file.${name}`, host[name]);
109+
register("cordova-plugin-file.fileSystemPaths", { file: bridge.file });
110+
register("cordova-plugin-file.resolveLocalFileSystemURI", {
111+
resolveLocalFileSystemURL: host.resolveLocalFileSystemURL,
112+
resolveLocalFileSystemURI: host.resolveLocalFileSystemURI,
113+
});
114+
host.cordova = cordova;
115+
116+
function exec(success, failure, service, action, args = []) {
117+
args ||= [];
118+
if (service === "CordovaHttpPlugin") service = "NativeHttpPlugin";
119+
if (service === "CoreAndroid" || service === "App") {
120+
service = "App";
121+
if (action === "overrideBackbutton") {
122+
action = "overrideButton";
123+
args = ["backbutton", args[0]];
124+
} else if (action === "overrideButton") {
125+
args = [args[0].replace(/button$/, "") + "button", args[1]];
126+
} else if (action === "loadUrl" || action === "cancelLoadUrl") {
127+
app[action](...args);
128+
success?.();
129+
return;
130+
}
131+
}
132+
return bridge.exec(success, failure, service, action, args);
133+
}
134+
function register(id, exports) {
135+
modules.set(id, { exports });
136+
}
137+
function defineModule(id, factory) {
138+
if (modules.has(id)) throw new Error(`Module ${id} already defined`);
139+
modules.set(id, { factory, exports: {} });
140+
}
141+
function requireModule(id) {
142+
const module = modules.get(id);
143+
if (!module) throw new Error(`Module ${id} not found`);
144+
if (module.factory) {
145+
const factory = module.factory;
146+
delete module.factory;
147+
factory(
148+
(name) =>
149+
requireModule(
150+
name.startsWith("./")
151+
? `${id.slice(0, id.lastIndexOf("."))}.${name.slice(2)}`
152+
: name,
153+
),
154+
module.exports,
155+
module,
156+
);
157+
}
158+
return module.exports;
159+
}
160+
}
161+
162+
function readinessName(name) {
163+
if (name === "onCordovaReady") return "onBridgeReady";
164+
if (name === "onPluginsReady") return "onServicesReady";
165+
return name;
166+
}

0 commit comments

Comments
 (0)