html2apk 0.2.0 → 0.4.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 +323 -12
- package/examples/minimal/app.json +2 -0
- package/examples/minimal/dist/MeuApp-1.0.0-debug.apk +0 -0
- package/package.json +3 -2
- package/src/cli/index.js +10 -1
- package/src/cordova/config-xml.js +59 -1
- package/src/cordova/project.js +19 -1
- package/src/core/build-apk.js +399 -23
- package/src/core/config.js +47 -1
- package/src/core/defaults.js +6 -0
- package/src/desktop/main.js +152 -2
- package/src/desktop/preload.js +14 -0
- package/src/desktop/renderer/index.html +20 -2
- package/src/desktop/renderer/renderer.js +1254 -41
- package/src/desktop/renderer/styles.css +98 -2
- package/src/templates/cordova-plugin-html2apk-bridge/plugin.xml +4 -0
- package/src/templates/cordova-plugin-html2apk-bridge/src/android/Html2ApkBridge.java +1770 -38
- package/src/templates/cordova-plugin-html2apk-bridge/src/android/NotificationReceiver.java +19 -5
- package/src/templates/cordova-plugin-html2apk-bridge/src/android/NotificationStore.java +13 -1
- package/src/templates/cordova-plugin-html2apk-bridge/www/html2apk-bridge.js +516 -3
- package/src/templates/html2apk-auto-theme.js +144 -0
- package/src/templates/html2apk-onesignal.js +155 -0
- package/src/utils/command-runner.js +3 -0
package/src/core/build-apk.js
CHANGED
|
@@ -5,14 +5,27 @@ const os = require("os");
|
|
|
5
5
|
const path = require("path");
|
|
6
6
|
const { resolveBuildOptions } = require("./config");
|
|
7
7
|
const { validateEntryFile, validateRequiredOptions } = require("./validation");
|
|
8
|
-
const { createCordovaProject, addAndroidPlatform, buildAndroid, addCordovaPlugin } = require("../cordova/project");
|
|
8
|
+
const { createCordovaProject, addAndroidPlatform, buildAndroid, runAndroidDevice, addCordovaPlugin } = require("../cordova/project");
|
|
9
9
|
const { writeConfigXml } = require("../cordova/config-xml");
|
|
10
10
|
const { findApk } = require("../cordova/apk-finder");
|
|
11
|
-
const { copyWebAssets, ensureDir, removePath, copyFile } = require("../utils/fs-extra");
|
|
11
|
+
const { copyWebAssets, ensureDir, removePath, copyFile, copyDirectory } = require("../utils/fs-extra");
|
|
12
12
|
const { createCommandRunner } = require("../utils/command-runner");
|
|
13
13
|
const { installBridgePlugin } = require("../bridge/install-bridge");
|
|
14
14
|
const { getRuntimeEnvironment } = require("../runtime-manager");
|
|
15
15
|
|
|
16
|
+
const AUTO_THEME_SCRIPT_NAME = "html2apk-auto-theme.js";
|
|
17
|
+
const ONESIGNAL_SCRIPT_NAME = "html2apk-onesignal.js";
|
|
18
|
+
const ONESIGNAL_PLUGIN_PACKAGE = "onesignal-cordova-plugin";
|
|
19
|
+
const DEFAULT_APP_ICON_NAME = "html2apk.png";
|
|
20
|
+
|
|
21
|
+
function defaultAppIconPath() {
|
|
22
|
+
return path.resolve(__dirname, "..", "..", DEFAULT_APP_ICON_NAME);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function withDefaultAppIcon(assetPath) {
|
|
26
|
+
return String(assetPath || "").trim() || defaultAppIconPath();
|
|
27
|
+
}
|
|
28
|
+
|
|
16
29
|
function isRemoteAsset(assetPath) {
|
|
17
30
|
return /^https?:\/\//i.test(String(assetPath || ""));
|
|
18
31
|
}
|
|
@@ -34,6 +47,172 @@ function toCordovaPath(value) {
|
|
|
34
47
|
return String(value).replace(/\\/g, "/");
|
|
35
48
|
}
|
|
36
49
|
|
|
50
|
+
function isAutoTheme(options) {
|
|
51
|
+
return String(options.themeMode || options.theme || "").toLowerCase() === "auto";
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function oneSignalAppId(options) {
|
|
55
|
+
return String(options.oneSignalAppId || options.onesignalAppId || options.oneSignal?.appId || options.onesignal?.appId || "").trim();
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function hasOneSignal(options) {
|
|
59
|
+
return oneSignalAppId(options).length > 0;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function scriptTag(scriptPath) {
|
|
63
|
+
return `<script src="${scriptPath}"></script>`;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async function injectCordovaRuntimeIntoHtml(htmlPath, scriptPath = "cordova.js") {
|
|
67
|
+
let html = await fs.readFile(htmlPath, "utf8");
|
|
68
|
+
if (/<script\b[^>]*\bsrc=["'][^"']*cordova\.js["'][^>]*>/i.test(html)) {
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const tag = scriptTag(scriptPath);
|
|
73
|
+
if (/<head\b[^>]*>/i.test(html)) {
|
|
74
|
+
html = html.replace(/<head\b[^>]*>/i, (match) => `${match}\n ${tag}`);
|
|
75
|
+
} else if (/<html\b[^>]*>/i.test(html)) {
|
|
76
|
+
html = html.replace(/<html\b[^>]*>/i, (match) => `${match}\n ${tag}`);
|
|
77
|
+
} else {
|
|
78
|
+
html = `${tag}\n${html}`;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
await fs.writeFile(htmlPath, html, "utf8");
|
|
82
|
+
return true;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async function installCordovaRuntimeScript(buildDir, options) {
|
|
86
|
+
const wwwDir = path.join(buildDir, "www");
|
|
87
|
+
const entryHtmlPath = path.resolve(wwwDir, options.entryFile || "index.html");
|
|
88
|
+
const scriptPath = toCordovaPath(path.relative(path.dirname(entryHtmlPath), path.join(wwwDir, "cordova.js"))) || "cordova.js";
|
|
89
|
+
|
|
90
|
+
if (!isInside(wwwDir, entryHtmlPath)) {
|
|
91
|
+
throw new Error(`Entry file must stay inside the Cordova www folder: ${options.entryFile}`);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return injectCordovaRuntimeIntoHtml(entryHtmlPath, scriptPath);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async function injectScriptIntoHtml(htmlPath, scriptPath) {
|
|
98
|
+
let html = await fs.readFile(htmlPath, "utf8");
|
|
99
|
+
if (html.includes(scriptPath)) {
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const tag = scriptTag(scriptPath);
|
|
104
|
+
if (/<\/body>/i.test(html)) {
|
|
105
|
+
html = html.replace(/<\/body>/i, ` ${tag}\n</body>`);
|
|
106
|
+
} else {
|
|
107
|
+
html = `${html}\n${tag}\n`;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
await fs.writeFile(htmlPath, html, "utf8");
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
async function installAutoThemeScript(buildDir, options) {
|
|
114
|
+
if (!isAutoTheme(options)) {
|
|
115
|
+
return false;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const wwwDir = path.join(buildDir, "www");
|
|
119
|
+
const source = path.resolve(__dirname, "..", "templates", AUTO_THEME_SCRIPT_NAME);
|
|
120
|
+
const target = path.join(wwwDir, AUTO_THEME_SCRIPT_NAME);
|
|
121
|
+
const entryHtmlPath = path.resolve(wwwDir, options.entryFile || "index.html");
|
|
122
|
+
const scriptPath = toCordovaPath(path.relative(path.dirname(entryHtmlPath), target));
|
|
123
|
+
|
|
124
|
+
if (!isInside(wwwDir, entryHtmlPath)) {
|
|
125
|
+
throw new Error(`Entry file must stay inside the Cordova www folder: ${options.entryFile}`);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
await copyFile(source, target);
|
|
129
|
+
await injectScriptIntoHtml(entryHtmlPath, scriptPath || AUTO_THEME_SCRIPT_NAME);
|
|
130
|
+
return true;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function jsString(value) {
|
|
134
|
+
return JSON.stringify(String(value || ""));
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
async function installOneSignalScript(buildDir, options) {
|
|
138
|
+
const appId = oneSignalAppId(options);
|
|
139
|
+
if (!appId) {
|
|
140
|
+
return false;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const wwwDir = path.join(buildDir, "www");
|
|
144
|
+
const source = path.resolve(__dirname, "..", "templates", ONESIGNAL_SCRIPT_NAME);
|
|
145
|
+
const target = path.join(wwwDir, ONESIGNAL_SCRIPT_NAME);
|
|
146
|
+
const entryHtmlPath = path.resolve(wwwDir, options.entryFile || "index.html");
|
|
147
|
+
const scriptPath = toCordovaPath(path.relative(path.dirname(entryHtmlPath), target));
|
|
148
|
+
const template = await fs.readFile(source, "utf8");
|
|
149
|
+
|
|
150
|
+
if (!isInside(wwwDir, entryHtmlPath)) {
|
|
151
|
+
throw new Error(`Entry file must stay inside the Cordova www folder: ${options.entryFile}`);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
await fs.writeFile(
|
|
155
|
+
target,
|
|
156
|
+
template.replace("__HTML2APK_ONESIGNAL_APP_ID__", jsString(appId)),
|
|
157
|
+
"utf8"
|
|
158
|
+
);
|
|
159
|
+
await injectScriptIntoHtml(entryHtmlPath, scriptPath || ONESIGNAL_SCRIPT_NAME);
|
|
160
|
+
return true;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function removePluginPlatform(pluginXml, platformName) {
|
|
164
|
+
const pattern = new RegExp(`\\n?\\s*<platform name="${platformName}">[\\s\\S]*?\\n\\s*</platform>`, "i");
|
|
165
|
+
return pluginXml.replace(pattern, "");
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
async function prepareBundledPlugin(buildDir, packageName) {
|
|
169
|
+
const localPath = path.resolve(__dirname, "..", "..", "node_modules", packageName);
|
|
170
|
+
try {
|
|
171
|
+
await fs.access(path.join(localPath, "plugin.xml"));
|
|
172
|
+
} catch {
|
|
173
|
+
return packageName;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const destination = path.join(buildDir, `${packageName}-android`);
|
|
177
|
+
await removePath(destination);
|
|
178
|
+
await copyDirectory(localPath, destination, () => false);
|
|
179
|
+
|
|
180
|
+
const packageJsonPath = path.join(destination, "package.json");
|
|
181
|
+
const sourcePackage = JSON.parse(await fs.readFile(packageJsonPath, "utf8"));
|
|
182
|
+
const pluginPackage = {
|
|
183
|
+
name: sourcePackage.name,
|
|
184
|
+
version: sourcePackage.version,
|
|
185
|
+
description: sourcePackage.description,
|
|
186
|
+
license: sourcePackage.license,
|
|
187
|
+
main: sourcePackage.main || "dist/index.cjs",
|
|
188
|
+
types: sourcePackage.types,
|
|
189
|
+
cordova: {
|
|
190
|
+
id: sourcePackage.cordova?.id || packageName,
|
|
191
|
+
platforms: ["android"]
|
|
192
|
+
}
|
|
193
|
+
};
|
|
194
|
+
Object.keys(pluginPackage).forEach((key) => {
|
|
195
|
+
if (pluginPackage[key] === undefined) {
|
|
196
|
+
delete pluginPackage[key];
|
|
197
|
+
}
|
|
198
|
+
});
|
|
199
|
+
await fs.writeFile(packageJsonPath, `${JSON.stringify(pluginPackage, null, 2)}\n`, "utf8");
|
|
200
|
+
|
|
201
|
+
const pluginXmlPath = path.join(destination, "plugin.xml");
|
|
202
|
+
let pluginXml = await fs.readFile(pluginXmlPath, "utf8");
|
|
203
|
+
pluginXml = removePluginPlatform(pluginXml, "ios");
|
|
204
|
+
await fs.writeFile(pluginXmlPath, pluginXml, "utf8");
|
|
205
|
+
|
|
206
|
+
return destination;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function hasPlugin(plugins, packageName) {
|
|
210
|
+
return (plugins || []).some((plugin) => {
|
|
211
|
+
const text = String(plugin || "").replace(/\\/g, "/").toLowerCase();
|
|
212
|
+
return text === packageName || text.endsWith(`/node_modules/${packageName}`) || text.endsWith(`/${packageName}`);
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
|
|
37
216
|
async function copyCordovaAsset(projectRoot, buildDir, assetPath, assetName) {
|
|
38
217
|
if (!assetPath || isRemoteAsset(assetPath)) {
|
|
39
218
|
return assetPath;
|
|
@@ -99,11 +278,147 @@ function outputApkName(options) {
|
|
|
99
278
|
return `${safeName}-${options.version}-${flavor}.apk`;
|
|
100
279
|
}
|
|
101
280
|
|
|
281
|
+
function parseAdbDevices(output) {
|
|
282
|
+
return String(output || "")
|
|
283
|
+
.split(/\r?\n/)
|
|
284
|
+
.map((line) => line.trim())
|
|
285
|
+
.filter((line) => line
|
|
286
|
+
&& !/^list of devices/i.test(line)
|
|
287
|
+
&& !/^\*/.test(line)
|
|
288
|
+
&& !/^adb server/i.test(line))
|
|
289
|
+
.map((line) => {
|
|
290
|
+
const parts = line.split(/\s+/);
|
|
291
|
+
return {
|
|
292
|
+
id: parts[0],
|
|
293
|
+
status: parts[1] || "unknown"
|
|
294
|
+
};
|
|
295
|
+
})
|
|
296
|
+
.filter((device) => device.id);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function deviceTargetId(device) {
|
|
300
|
+
return device && device.id ? String(device.id) : "";
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
async function ensureUsbDebugDevice(runner) {
|
|
304
|
+
let result;
|
|
305
|
+
try {
|
|
306
|
+
result = await runner.run("adb", ["devices"]);
|
|
307
|
+
} catch (error) {
|
|
308
|
+
throw new Error("ADB nao foi encontrado. Instale Android platform-tools pelo ambiente do html2apk e tente novamente.");
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
const devices = parseAdbDevices(result.stdout);
|
|
312
|
+
const physicalDevices = devices.filter((device) => !/^emulator-/i.test(device.id));
|
|
313
|
+
const ready = physicalDevices.find((device) => device.status === "device");
|
|
314
|
+
if (ready) {
|
|
315
|
+
return ready;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
if (physicalDevices.some((device) => device.status === "unauthorized")) {
|
|
319
|
+
throw new Error("Celular encontrado, mas a depuracao USB ainda nao foi autorizada. Desbloqueie o celular e aceite a chave RSA de depuracao USB.");
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
if (physicalDevices.some((device) => device.status === "offline")) {
|
|
323
|
+
throw new Error("Celular USB encontrado, mas esta offline. Reconecte o cabo USB, desbloqueie o celular e confirme a depuracao USB.");
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
throw new Error("Nenhum celular USB autorizado foi encontrado. Ative Opcoes do desenvolvedor > Depuracao USB, conecte o celular e aceite a permissao RSA.");
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
async function prepareCordovaProject(projectRoot, buildDir, options, runner, log) {
|
|
330
|
+
await createCordovaProject(buildDir, options, runner);
|
|
331
|
+
const cordovaOptions = { ...options };
|
|
332
|
+
const effectiveIcon = withDefaultAppIcon(options.icon);
|
|
333
|
+
const effectiveSplash = options.splash || effectiveIcon;
|
|
334
|
+
if (!String(options.icon || "").trim()) {
|
|
335
|
+
log("Icon: using the default html2apk icon.");
|
|
336
|
+
}
|
|
337
|
+
cordovaOptions.icon = await copyCordovaAsset(projectRoot, buildDir, effectiveIcon, "icon");
|
|
338
|
+
cordovaOptions.splash = await copyCordovaAsset(projectRoot, buildDir, effectiveSplash, "splash");
|
|
339
|
+
cordovaOptions.androidSplashScreenAnimatedIcon = toBuildAssetPath(buildDir, cordovaOptions.splash);
|
|
340
|
+
await writeConfigXml(path.join(buildDir, "config.xml"), cordovaOptions);
|
|
341
|
+
await copyWebAssets(path.resolve(projectRoot, options.webRoot || "."), path.join(buildDir, "www"), options, projectRoot);
|
|
342
|
+
if (await installCordovaRuntimeScript(buildDir, options)) {
|
|
343
|
+
log("Cordova runtime: injected cordova.js into the entry HTML.");
|
|
344
|
+
}
|
|
345
|
+
if (await installAutoThemeScript(buildDir, options)) {
|
|
346
|
+
log("Theme mode: auto (system bars follow the visible screen color).");
|
|
347
|
+
}
|
|
348
|
+
if (await installOneSignalScript(buildDir, options)) {
|
|
349
|
+
log("OneSignal: enabled for remote push notifications.");
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
const bridgePluginPath = await installBridgePlugin(buildDir);
|
|
353
|
+
await addCordovaPlugin(buildDir, bridgePluginPath, runner);
|
|
354
|
+
|
|
355
|
+
if (hasOneSignal(options) && !hasPlugin(options.plugins, ONESIGNAL_PLUGIN_PACKAGE)) {
|
|
356
|
+
await addCordovaPlugin(buildDir, await prepareBundledPlugin(buildDir, ONESIGNAL_PLUGIN_PACKAGE), runner);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
for (const plugin of options.plugins) {
|
|
360
|
+
await addCordovaPlugin(buildDir, plugin, runner);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
await addAndroidPlatform(buildDir, options, runner);
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
async function copyBuiltApk(projectRoot, buildDir, options) {
|
|
367
|
+
const apkPathInBuild = await findApk(buildDir, options);
|
|
368
|
+
const outputDir = path.resolve(projectRoot, options.outputDir || "dist");
|
|
369
|
+
await ensureDir(outputDir);
|
|
370
|
+
|
|
371
|
+
const apkPath = path.join(outputDir, outputApkName(options));
|
|
372
|
+
await copyFile(apkPathInBuild, apkPath);
|
|
373
|
+
return apkPath;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
async function ensureBuiltDebugApk(buildDir, options, runner, log) {
|
|
377
|
+
try {
|
|
378
|
+
return await findApk(buildDir, options);
|
|
379
|
+
} catch {
|
|
380
|
+
log("USB debug: building debug APK for direct ADB install.");
|
|
381
|
+
await buildAndroid(buildDir, options, null, runner);
|
|
382
|
+
return findApk(buildDir, options);
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
async function installDebugApkWithAdb(buildDir, options, runner, device, log) {
|
|
387
|
+
const deviceId = deviceTargetId(device);
|
|
388
|
+
const apkPath = await ensureBuiltDebugApk(buildDir, options, runner, log);
|
|
389
|
+
log(`USB debug fallback: installing APK with ADB on ${deviceId}.`);
|
|
390
|
+
|
|
391
|
+
try {
|
|
392
|
+
await runner.run("adb", ["-s", deviceId, "install", "-r", "-d", apkPath]);
|
|
393
|
+
} catch (error) {
|
|
394
|
+
const output = `${error.stdout || ""}\n${error.stderr || ""}\n${error.message || ""}`;
|
|
395
|
+
if (/INSTALL_FAILED_UPDATE_INCOMPATIBLE/i.test(output)) {
|
|
396
|
+
throw new Error("O app ja esta instalado nesse celular com outra assinatura. Desinstale a versao antiga no Android e clique em Testar no USB novamente.");
|
|
397
|
+
}
|
|
398
|
+
throw error;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
log(`USB debug fallback: opening ${options.packageId}.`);
|
|
402
|
+
await runner.run("adb", [
|
|
403
|
+
"-s",
|
|
404
|
+
deviceId,
|
|
405
|
+
"shell",
|
|
406
|
+
"monkey",
|
|
407
|
+
"-p",
|
|
408
|
+
options.packageId,
|
|
409
|
+
"-c",
|
|
410
|
+
"android.intent.category.LAUNCHER",
|
|
411
|
+
"1"
|
|
412
|
+
]);
|
|
413
|
+
|
|
414
|
+
return apkPath;
|
|
415
|
+
}
|
|
416
|
+
|
|
102
417
|
async function buildApk(overrides = {}) {
|
|
103
418
|
const onLog = typeof overrides.onLog === "function" ? overrides.onLog : null;
|
|
104
419
|
const { projectRoot, configPath, options } = await resolveBuildOptions(overrides);
|
|
105
420
|
validateRequiredOptions(options);
|
|
106
|
-
|
|
421
|
+
await validateEntryFile(projectRoot, options);
|
|
107
422
|
|
|
108
423
|
const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "html2apk-"));
|
|
109
424
|
const buildDir = path.join(tempRoot, "cordova-project");
|
|
@@ -129,31 +444,86 @@ async function buildApk(overrides = {}) {
|
|
|
129
444
|
log(`JAVA_HOME: ${runtime.javaHome}`);
|
|
130
445
|
}
|
|
131
446
|
|
|
132
|
-
await
|
|
133
|
-
const
|
|
134
|
-
|
|
135
|
-
cordovaOptions.splash = await copyCordovaAsset(projectRoot, buildDir, options.splash || options.icon, "splash");
|
|
136
|
-
cordovaOptions.androidSplashScreenAnimatedIcon = toBuildAssetPath(buildDir, cordovaOptions.splash);
|
|
137
|
-
await writeConfigXml(path.join(buildDir, "config.xml"), cordovaOptions);
|
|
138
|
-
await copyWebAssets(webRoot, path.join(buildDir, "www"), options, projectRoot);
|
|
447
|
+
await prepareCordovaProject(projectRoot, buildDir, options, runner, log);
|
|
448
|
+
const buildJsonPath = await createBuildJson(buildDir, options, projectRoot);
|
|
449
|
+
await buildAndroid(buildDir, options, buildJsonPath, runner);
|
|
139
450
|
|
|
140
|
-
const
|
|
141
|
-
await addCordovaPlugin(buildDir, bridgePluginPath, runner);
|
|
451
|
+
const apkPath = await copyBuiltApk(projectRoot, buildDir, options);
|
|
142
452
|
|
|
143
|
-
|
|
144
|
-
await
|
|
453
|
+
if (!options.debug) {
|
|
454
|
+
await removePath(tempRoot);
|
|
455
|
+
tempCleaned = true;
|
|
145
456
|
}
|
|
146
457
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
458
|
+
return {
|
|
459
|
+
apkPath,
|
|
460
|
+
buildDir: options.debug ? buildDir : null,
|
|
461
|
+
logs,
|
|
462
|
+
status: "success",
|
|
463
|
+
tempCleaned
|
|
464
|
+
};
|
|
465
|
+
} catch (error) {
|
|
466
|
+
log(`Error: ${error.message}`);
|
|
467
|
+
if (!options.debug) {
|
|
468
|
+
await removePath(tempRoot).catch(() => {});
|
|
469
|
+
tempCleaned = true;
|
|
470
|
+
} else {
|
|
471
|
+
log(`Debug mode enabled. Temporary build kept at: ${buildDir}`);
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
error.logs = logs;
|
|
475
|
+
error.buildDir = options.debug ? buildDir : null;
|
|
476
|
+
error.tempCleaned = tempCleaned;
|
|
477
|
+
error.status = "error";
|
|
478
|
+
throw error;
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
async function runDebugUsb(overrides = {}) {
|
|
483
|
+
const onLog = typeof overrides.onLog === "function" ? overrides.onLog : null;
|
|
484
|
+
const resolved = await resolveBuildOptions(overrides);
|
|
485
|
+
const projectRoot = resolved.projectRoot;
|
|
486
|
+
const configPath = resolved.configPath;
|
|
487
|
+
const options = {
|
|
488
|
+
...resolved.options,
|
|
489
|
+
release: false
|
|
490
|
+
};
|
|
491
|
+
validateRequiredOptions(options);
|
|
492
|
+
await validateEntryFile(projectRoot, options);
|
|
493
|
+
|
|
494
|
+
const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "html2apk-"));
|
|
495
|
+
const buildDir = path.join(tempRoot, "cordova-project");
|
|
496
|
+
const logs = [];
|
|
497
|
+
const runtime = getRuntimeEnvironment();
|
|
498
|
+
const runner = createCommandRunner({ logs, env: runtime.env, onLog });
|
|
499
|
+
let tempCleaned = false;
|
|
500
|
+
|
|
501
|
+
function log(line) {
|
|
502
|
+
logs.push(line);
|
|
503
|
+
if (onLog) {
|
|
504
|
+
onLog(line);
|
|
505
|
+
}
|
|
506
|
+
}
|
|
150
507
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
508
|
+
try {
|
|
509
|
+
log(`Project root: ${projectRoot}`);
|
|
510
|
+
log(configPath ? `Config: ${configPath}` : "Config: defaults only");
|
|
511
|
+
log("USB debug: checking connected Android device.");
|
|
512
|
+
const device = await ensureUsbDebugDevice(runner);
|
|
513
|
+
log(`USB debug device: ${device.id}`);
|
|
514
|
+
|
|
515
|
+
await prepareCordovaProject(projectRoot, buildDir, options, runner, log);
|
|
516
|
+
try {
|
|
517
|
+
await runAndroidDevice(buildDir, options, null, runner, deviceTargetId(device));
|
|
518
|
+
} catch (error) {
|
|
519
|
+
log("USB debug: Cordova run failed. Trying direct ADB install fallback.");
|
|
520
|
+
if (error.stdout || error.stderr) {
|
|
521
|
+
log([error.stdout, error.stderr].filter(Boolean).join("\n").trim().slice(-4000));
|
|
522
|
+
}
|
|
523
|
+
await installDebugApkWithAdb(buildDir, options, runner, device, log);
|
|
524
|
+
}
|
|
154
525
|
|
|
155
|
-
const apkPath =
|
|
156
|
-
await copyFile(apkPathInBuild, apkPath);
|
|
526
|
+
const apkPath = await copyBuiltApk(projectRoot, buildDir, options);
|
|
157
527
|
|
|
158
528
|
if (!options.debug) {
|
|
159
529
|
await removePath(tempRoot);
|
|
@@ -163,8 +533,10 @@ async function buildApk(overrides = {}) {
|
|
|
163
533
|
return {
|
|
164
534
|
apkPath,
|
|
165
535
|
buildDir: options.debug ? buildDir : null,
|
|
536
|
+
device,
|
|
166
537
|
logs,
|
|
167
538
|
status: "success",
|
|
539
|
+
usbDebug: true,
|
|
168
540
|
tempCleaned
|
|
169
541
|
};
|
|
170
542
|
} catch (error) {
|
|
@@ -185,5 +557,9 @@ async function buildApk(overrides = {}) {
|
|
|
185
557
|
}
|
|
186
558
|
|
|
187
559
|
module.exports = {
|
|
188
|
-
buildApk
|
|
560
|
+
buildApk,
|
|
561
|
+
defaultAppIconPath,
|
|
562
|
+
injectCordovaRuntimeIntoHtml,
|
|
563
|
+
parseAdbDevices,
|
|
564
|
+
runDebugUsb
|
|
189
565
|
};
|
package/src/core/config.js
CHANGED
|
@@ -71,6 +71,15 @@ function normalizeMinSdkVersion(value) {
|
|
|
71
71
|
: DEFAULT_ANDROID_MIN_SDK_VERSION;
|
|
72
72
|
}
|
|
73
73
|
|
|
74
|
+
function normalizeThemeMode(value) {
|
|
75
|
+
return String(value || "").trim().toLowerCase() === "auto" ? "auto" : "fixed";
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function normalizeThemeColor(value) {
|
|
79
|
+
const color = String(value || "").trim();
|
|
80
|
+
return /^#[0-9a-fA-F]{6}$/.test(color) ? color : "#126fff";
|
|
81
|
+
}
|
|
82
|
+
|
|
74
83
|
function normalizeOptions(options) {
|
|
75
84
|
const normalized = { ...options };
|
|
76
85
|
|
|
@@ -85,6 +94,13 @@ function normalizeOptions(options) {
|
|
|
85
94
|
: "default";
|
|
86
95
|
normalized.debug = Boolean(normalized.debug);
|
|
87
96
|
normalized.release = Boolean(normalized.release);
|
|
97
|
+
const themeColorText = String(normalized.themeColor || "").trim().toLowerCase();
|
|
98
|
+
const themeModeText = String(normalized.themeMode || "").trim().toLowerCase();
|
|
99
|
+
const themeText = String(normalized.theme || "").trim().toLowerCase();
|
|
100
|
+
normalized.themeMode = themeModeText === "auto" || themeText === "auto" || themeColorText === "auto" ? "auto" : "fixed";
|
|
101
|
+
normalized.theme = normalized.themeMode;
|
|
102
|
+
normalized.themeColor = normalizeThemeColor(themeColorText === "auto" ? normalized.backgroundColor : normalized.themeColor);
|
|
103
|
+
normalized.oneSignalAppId = normalizeOneSignalAppId(normalized.oneSignalAppId || normalized.onesignalAppId || normalized.oneSignal?.appId || normalized.onesignal?.appId);
|
|
88
104
|
normalized.minSdkVersion = normalizeMinSdkVersion(normalized.minSdkVersion || normalized.androidMinSdkVersion);
|
|
89
105
|
normalized.permissions = Array.isArray(normalized.permissions)
|
|
90
106
|
? normalized.permissions.map((permission) => String(permission).trim()).filter(Boolean)
|
|
@@ -92,12 +108,39 @@ function normalizeOptions(options) {
|
|
|
92
108
|
normalized.plugins = Array.isArray(normalized.plugins)
|
|
93
109
|
? normalized.plugins.map((plugin) => String(plugin).trim()).filter(Boolean)
|
|
94
110
|
: [];
|
|
111
|
+
normalized.deepLinks = normalizeDeepLinks(normalized.deepLinks);
|
|
95
112
|
normalized.entryFile = normalized.entryFile || "index.html";
|
|
96
113
|
normalized.webRoot = normalized.webRoot || ".";
|
|
97
114
|
|
|
98
115
|
return normalized;
|
|
99
116
|
}
|
|
100
117
|
|
|
118
|
+
function normalizeOneSignalAppId(value) {
|
|
119
|
+
return String(value || "").trim();
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function normalizeDeepLinks(value) {
|
|
123
|
+
const input = value && typeof value === "object" ? value : {};
|
|
124
|
+
const schemes = Array.isArray(input.schemes)
|
|
125
|
+
? input.schemes.map((scheme) => String(scheme).trim()).filter(Boolean)
|
|
126
|
+
: [];
|
|
127
|
+
const appLinks = Array.isArray(input.appLinks)
|
|
128
|
+
? input.appLinks
|
|
129
|
+
.filter((item) => item && typeof item === "object" && item.host)
|
|
130
|
+
.map((item) => ({
|
|
131
|
+
scheme: item.scheme || "https",
|
|
132
|
+
host: String(item.host).trim(),
|
|
133
|
+
paths: Array.isArray(item.paths) ? item.paths.map((pathItem) => String(pathItem).trim()).filter(Boolean) : [],
|
|
134
|
+
autoVerify: Boolean(item.autoVerify)
|
|
135
|
+
}))
|
|
136
|
+
: [];
|
|
137
|
+
|
|
138
|
+
return {
|
|
139
|
+
schemes,
|
|
140
|
+
appLinks
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
101
144
|
async function resolveBuildOptions(overrides = {}) {
|
|
102
145
|
const projectRoot = path.resolve(overrides.projectRoot || process.cwd());
|
|
103
146
|
const { config, configPath } = await loadProjectConfig(projectRoot);
|
|
@@ -118,5 +161,8 @@ module.exports = {
|
|
|
118
161
|
resolveBuildOptions,
|
|
119
162
|
mergeDeep,
|
|
120
163
|
normalizeOptions,
|
|
121
|
-
normalizeMinSdkVersion
|
|
164
|
+
normalizeMinSdkVersion,
|
|
165
|
+
normalizeThemeMode,
|
|
166
|
+
normalizeOneSignalAppId,
|
|
167
|
+
normalizeDeepLinks
|
|
122
168
|
};
|
package/src/core/defaults.js
CHANGED
|
@@ -31,6 +31,12 @@ function createDefaultOptions(projectRoot) {
|
|
|
31
31
|
androidPlatform: "android@15.0.0",
|
|
32
32
|
minSdkVersion: DEFAULT_ANDROID_MIN_SDK_VERSION,
|
|
33
33
|
themeColor: "#126fff",
|
|
34
|
+
themeMode: "fixed",
|
|
35
|
+
oneSignalAppId: "",
|
|
36
|
+
deepLinks: {
|
|
37
|
+
schemes: [],
|
|
38
|
+
appLinks: []
|
|
39
|
+
},
|
|
34
40
|
files: null,
|
|
35
41
|
entryFile: "index.html",
|
|
36
42
|
webRoot: ".",
|