html2apk 0.4.0 → 0.7.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 +116 -12
- package/examples/minimal/app.json +2 -0
- package/examples/minimal/dist/MeuApp-1.0.0-debug.apk +0 -0
- package/examples/minimal/dist/MeuApp-1.0.0-release.aab +0 -0
- package/package.json +1 -1
- package/src/cli/index.js +12 -1
- package/src/cordova/apk-finder.js +22 -10
- package/src/cordova/project.js +5 -0
- package/src/core/build-apk.js +102 -22
- package/src/core/config.js +16 -0
- package/src/core/defaults.js +2 -0
- package/src/desktop/main.js +191 -2
- package/src/desktop/preload.js +5 -0
- package/src/desktop/renderer/index.html +71 -0
- package/src/desktop/renderer/renderer.js +473 -12
- package/src/desktop/renderer/styles.css +189 -0
- package/src/templates/cordova-plugin-html2apk-bridge/plugin.xml +2 -0
- package/src/templates/cordova-plugin-html2apk-bridge/src/android/Html2ApkBridge.java +219 -20
- package/src/templates/cordova-plugin-html2apk-bridge/src/android/NotificationClickReceiver.java +28 -0
- package/src/templates/cordova-plugin-html2apk-bridge/src/android/NotificationReceiver.java +1 -1
- package/src/templates/cordova-plugin-html2apk-bridge/www/html2apk-bridge.js +278 -11
- package/src/templates/html2apk-early-bridge.js +860 -0
- package/src/templates/html2apk-runtime-console.js +805 -0
package/README.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# html2apk
|
|
2
2
|
|
|
3
|
-
`html2apk` transforma uma pasta com HTML, CSS, JS e assets em um APK Android usando Cordova.
|
|
3
|
+
`html2apk` transforma uma pasta com HTML, CSS, JS e assets em um APK ou AAB Android usando Cordova.
|
|
4
4
|
|
|
5
|
-
Use ele quando voce ja tem um app web, por exemplo uma pasta com `index.html`, `style.css`, `app.js` e imagens, e quer gerar um `.apk` instalavel no Android.
|
|
5
|
+
Use ele quando voce ja tem um app web, por exemplo uma pasta com `index.html`, `style.css`, `app.js` e imagens, e quer gerar um `.apk` instalavel no Android ou um `.aab` para loja.
|
|
6
6
|
|
|
7
7
|
## O Mais Importante
|
|
8
8
|
|
|
@@ -83,6 +83,21 @@ No Windows, o `html2apk doctor` tenta encontrar automaticamente o SDK em:
|
|
|
83
83
|
C:\Users\SEU_USUARIO\AppData\Local\Android\Sdk
|
|
84
84
|
```
|
|
85
85
|
|
|
86
|
+
## Interface Grafica
|
|
87
|
+
|
|
88
|
+
A interface desktop permite arrastar a pasta do projeto, preencher as configuracoes obrigatorias em etapas e gerar o arquivo Android sem mexer no terminal.
|
|
89
|
+
|
|
90
|
+
Recursos principais:
|
|
91
|
+
|
|
92
|
+
- Escolher idioma, tema claro/escuro e ver logs do processo.
|
|
93
|
+
- Gerar APK normal, APK release ou AAB.
|
|
94
|
+
- Escolher icone PNG, cor de tema, OneSignal App ID e permissoes.
|
|
95
|
+
- Preencher keystore pela interface: arquivo, alias, senha da store e senha da key.
|
|
96
|
+
- Marcar `Console no APK` para o app gerado mostrar um modal de debug no celular.
|
|
97
|
+
- Usar a aba `Arquivos` para abrir arquivos HTML/CSS/JS/JSON do projeto, salvar edicoes, criar novos arquivos e ver uma previa com sintaxe.
|
|
98
|
+
|
|
99
|
+
Para AAB, a interface pede keystore completo antes de liberar o build.
|
|
100
|
+
|
|
86
101
|
## Passo A Passo
|
|
87
102
|
|
|
88
103
|
Entre na pasta do seu app web:
|
|
@@ -177,6 +192,9 @@ Opcoes comuns:
|
|
|
177
192
|
```bash
|
|
178
193
|
html2apk build --debug
|
|
179
194
|
html2apk build --release
|
|
195
|
+
html2apk build --apk
|
|
196
|
+
html2apk build --aab
|
|
197
|
+
html2apk build --show-runtime-logs
|
|
180
198
|
html2apk build --mode fullscreen
|
|
181
199
|
html2apk build --mode standalone
|
|
182
200
|
html2apk build --mode floating
|
|
@@ -194,6 +212,10 @@ Com `--debug`, a pasta Cordova temporaria fica salva para inspecao caso voce que
|
|
|
194
212
|
|
|
195
213
|
Sem `--debug`, a pasta temporaria e limpa no fim do build.
|
|
196
214
|
|
|
215
|
+
Com `--aab`, o build usa formato AAB e release automaticamente. Para publicar, preencha `keystore`.
|
|
216
|
+
|
|
217
|
+
Com `--show-runtime-logs`, o APK/AAB gerado recebe um botao `Console` dentro do app. Esse console intercepta erros JavaScript, promises rejeitadas, logs, chamadas das funcoes interpretadas e requisicoes de rede (`fetch`/`XMLHttpRequest`), ajudando o dev a debugar no proprio celular. Se essa opcao ficar desligada, esse console nao aparece no app gerado.
|
|
218
|
+
|
|
197
219
|
## Configuracao Com `app.json`
|
|
198
220
|
|
|
199
221
|
O `app.json` fica na raiz do seu app. Se ele nao existir, `config.json` e usado como fallback.
|
|
@@ -206,6 +228,7 @@ Exemplo completo:
|
|
|
206
228
|
"appName": "MeuApp",
|
|
207
229
|
"packageId": "com.seuapp.meuapp",
|
|
208
230
|
"version": "1.0.0",
|
|
231
|
+
"buildFormat": "apk",
|
|
209
232
|
"mode": "fullscreen",
|
|
210
233
|
"orientation": "default",
|
|
211
234
|
"minSdkVersion": 24,
|
|
@@ -227,6 +250,7 @@ Exemplo completo:
|
|
|
227
250
|
"permissions": ["INTERNET", "CAMERA", "RECORD_AUDIO", "POST_NOTIFICATIONS", "VIBRATE"],
|
|
228
251
|
"plugins": ["cordova-plugin-camera"],
|
|
229
252
|
"release": false,
|
|
253
|
+
"showRuntimeLogs": false,
|
|
230
254
|
"androidPlatform": "android@15.0.0",
|
|
231
255
|
"keystore": {
|
|
232
256
|
"path": "",
|
|
@@ -248,6 +272,7 @@ Campos principais:
|
|
|
248
272
|
| `appName` | Nome visivel do app. |
|
|
249
273
|
| `packageId` | Identificador Android. Precisa ter formato como `com.empresa.app`. |
|
|
250
274
|
| `version` | Versao do app. |
|
|
275
|
+
| `buildFormat` | `apk` para instalar direto ou `aab` para loja. |
|
|
251
276
|
| `mode` | `fullscreen` para tela cheia, `standalone` para modo normal ou `floating` para icone flutuante. |
|
|
252
277
|
| `orientation` | `default`, `vertical`, `horizontal`, `portrait` ou `landscape`. |
|
|
253
278
|
| `minSdkVersion` | Versao minima do Android em API level. Padrao: `24`. |
|
|
@@ -263,6 +288,7 @@ Campos principais:
|
|
|
263
288
|
| `androidPlatform` | Versao da plataforma Cordova Android. Padrao: `android@15.0.0`. |
|
|
264
289
|
| `debug` | Se `true`, preserva a pasta temporaria de build. |
|
|
265
290
|
| `release` | Se `true`, gera build release. |
|
|
291
|
+
| `showRuntimeLogs` | Se `true`, mostra um modal `Console` no app gerado para depurar erros e funcoes interpretadas. |
|
|
266
292
|
| `keystore` | Dados de assinatura para build release. |
|
|
267
293
|
|
|
268
294
|
Prioridade de configuracao:
|
|
@@ -356,7 +382,7 @@ Retorno:
|
|
|
356
382
|
|
|
357
383
|
A v0.1 instala um plugin Cordova local com uma API global simples para recursos Android. Todas as funcoes retornam `Promise`, exceto os ouvintes `aoEvento`/atalhos, que retornam uma funcao para cancelar a escuta.
|
|
358
384
|
|
|
359
|
-
O html2apk injeta `cordova.js` automaticamente no HTML inicial do APK.
|
|
385
|
+
O html2apk injeta `html2apk-early-bridge.js` e `cordova.js` automaticamente no HTML inicial do APK. A bridge inicial cria as funcoes interpretadas antes dos scripts do seu projeto; se uma funcao nativa for chamada antes do `deviceready`, ela espera o Android ficar pronto antes de executar.
|
|
360
386
|
|
|
361
387
|
Cada funcao em portugues tambem tem alias em ingles. A interface grafica mostra a sintaxe PT-BR quando o idioma esta em portugues e mostra a sintaxe em ingles quando o usuario troca o idioma para English.
|
|
362
388
|
|
|
@@ -418,18 +444,29 @@ No seu JavaScript do app:
|
|
|
418
444
|
toast("Mensagem");
|
|
419
445
|
vibrar(250);
|
|
420
446
|
|
|
447
|
+
await notificar({
|
|
448
|
+
titulo: "Pedido aprovado",
|
|
449
|
+
texto: "Toque para abrir o app"
|
|
450
|
+
});
|
|
451
|
+
|
|
421
452
|
notificar({
|
|
422
453
|
titulo: "Pedido aprovado",
|
|
423
454
|
texto: "Toque para abrir os detalhes",
|
|
424
455
|
acoes: [
|
|
425
|
-
{
|
|
426
|
-
|
|
456
|
+
{
|
|
457
|
+
id: "abrir",
|
|
458
|
+
titulo: "Abrir",
|
|
459
|
+
open: true,
|
|
460
|
+
aoClicar: { funcao: "abrirNoApp", argumentos: ["#/pedido/123"] }
|
|
461
|
+
},
|
|
462
|
+
{
|
|
463
|
+
id: "site",
|
|
464
|
+
titulo: "Ver site",
|
|
465
|
+
open: false,
|
|
466
|
+
aoClicar: { funcao: "abrirForaDoApp", argumentos: ["https://exemplo.com/pedido/123"], open: false }
|
|
467
|
+
}
|
|
427
468
|
],
|
|
428
|
-
aoClicar:
|
|
429
|
-
acao: "abrir-rota",
|
|
430
|
-
rota: "/pedido/123",
|
|
431
|
-
dados: { id: 123 }
|
|
432
|
-
}
|
|
469
|
+
aoClicar: () => abrirForaDoApp("https://exemplo.com/pedido/123")
|
|
433
470
|
});
|
|
434
471
|
|
|
435
472
|
agendarNotificacao({
|
|
@@ -437,8 +474,8 @@ agendarNotificacao({
|
|
|
437
474
|
texto: "Hora de abrir o app",
|
|
438
475
|
quando: Date.now() + 60000,
|
|
439
476
|
aoClicar: {
|
|
440
|
-
|
|
441
|
-
|
|
477
|
+
funcao: "abrirNoApp",
|
|
478
|
+
argumentos: ["#/lembretes"]
|
|
442
479
|
}
|
|
443
480
|
});
|
|
444
481
|
|
|
@@ -466,8 +503,75 @@ const loop = await agendarLoopNotificacoes({
|
|
|
466
503
|
fullscreen(true);
|
|
467
504
|
```
|
|
468
505
|
|
|
506
|
+
`notificar()` nao obriga clique, botao nem funcao. So `titulo` e `texto` ja geram uma notificacao normal. `aoClicar`, `acoes`/`actions` e `open` sao opcionais.
|
|
507
|
+
|
|
469
508
|
`agendarNotificacao()` agenda uma notificacao. Se voce passar um array para ela, ou usar `agendarNotificacoes()`, o html2apk agenda varias em sequencia. Cada item recebe `id` automatico se voce nao informar um.
|
|
470
509
|
|
|
510
|
+
Em `aoClicar`, voce pode passar uma funcao diretamente:
|
|
511
|
+
|
|
512
|
+
```js
|
|
513
|
+
await notificar({
|
|
514
|
+
titulo: "Pedido aprovado",
|
|
515
|
+
texto: "Toque para abrir os detalhes",
|
|
516
|
+
aoClicar: () => abrirForaDoApp("https://exemplo.com/pedido/123")
|
|
517
|
+
});
|
|
518
|
+
```
|
|
519
|
+
|
|
520
|
+
Nao use `aoClicar: { acao: abrirForaDoApp("https://...") }`, porque os parenteses executam a funcao na hora em que a notificacao e criada. Para notificacao agendada, loop ou app fechado, prefira o formato serializavel:
|
|
521
|
+
|
|
522
|
+
```js
|
|
523
|
+
await agendarNotificacao({
|
|
524
|
+
titulo: "Pedido aprovado",
|
|
525
|
+
texto: "Toque para abrir os detalhes",
|
|
526
|
+
quando: Date.now() + 60000,
|
|
527
|
+
aoClicar: {
|
|
528
|
+
funcao: "abrirForaDoApp",
|
|
529
|
+
argumentos: ["https://exemplo.com/pedido/123"]
|
|
530
|
+
}
|
|
531
|
+
});
|
|
532
|
+
```
|
|
533
|
+
|
|
534
|
+
Esse formato tambem aceita funcoes suas, desde que elas existam em `window` quando o app abrir:
|
|
535
|
+
|
|
536
|
+
```js
|
|
537
|
+
window.abrirPedido = (id) => abrirNoApp("#/pedido/" + id);
|
|
538
|
+
|
|
539
|
+
await notificar({
|
|
540
|
+
titulo: "Pedido aprovado",
|
|
541
|
+
texto: "Toque para abrir os detalhes",
|
|
542
|
+
aoClicar: { funcao: "abrirPedido", argumentos: [123] }
|
|
543
|
+
});
|
|
544
|
+
```
|
|
545
|
+
|
|
546
|
+
Para botoes na notificacao, use `acoes` ou `actions`. Cada botao tambem aceita `aoClicar`/`onClick`:
|
|
547
|
+
|
|
548
|
+
```js
|
|
549
|
+
window.marcarPedidoLido = (id) => {
|
|
550
|
+
localStorage.setItem("pedido:" + id, "lido");
|
|
551
|
+
};
|
|
552
|
+
|
|
553
|
+
await notificar({
|
|
554
|
+
titulo: "Pedido aprovado",
|
|
555
|
+
texto: "Escolha uma acao",
|
|
556
|
+
acoes: [
|
|
557
|
+
{
|
|
558
|
+
id: "abrir",
|
|
559
|
+
titulo: "Abrir",
|
|
560
|
+
open: true,
|
|
561
|
+
aoClicar: { funcao: "abrirNoApp", argumentos: ["#/pedido/123"] }
|
|
562
|
+
},
|
|
563
|
+
{
|
|
564
|
+
id: "lido",
|
|
565
|
+
titulo: "Marcar lido",
|
|
566
|
+
open: false,
|
|
567
|
+
aoClicar: { funcao: "marcarPedidoLido", argumentos: [123], open: false }
|
|
568
|
+
}
|
|
569
|
+
]
|
|
570
|
+
});
|
|
571
|
+
```
|
|
572
|
+
|
|
573
|
+
`open: false` evita trazer a tela do app para frente. Se o app ainda estiver vivo em segundo plano, o html2apk dispara o evento e executa a funcao JavaScript. Se o Android tiver matado o app, JavaScript de WebView nao consegue rodar sem abrir o app; nesse caso, acoes externas como `{ funcao: "abrirForaDoApp", argumentos: ["https://..."], open: false }` ainda funcionam por fallback nativo.
|
|
574
|
+
|
|
471
575
|
`agendarLoopNotificacoes()` cria um loop recorrente que funciona com o app fechado. Use `aCada`, `intervalo`, `every` ou `interval` em milissegundos ou como texto (`"30min"`, `"12h"`, `"1d"`). A cada disparo, o Android mostra a proxima notificacao da lista. Para parar:
|
|
472
576
|
|
|
473
577
|
```js
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
"appName": "MeuApp",
|
|
4
4
|
"packageId": "com.seuapp.meuapp",
|
|
5
5
|
"version": "1.0.0",
|
|
6
|
+
"buildFormat": "apk",
|
|
6
7
|
"mode": "fullscreen",
|
|
7
8
|
"orientation": "default",
|
|
8
9
|
"minSdkVersion": 24,
|
|
@@ -18,6 +19,7 @@
|
|
|
18
19
|
],
|
|
19
20
|
"plugins": [],
|
|
20
21
|
"release": false,
|
|
22
|
+
"showRuntimeLogs": false,
|
|
21
23
|
"androidPlatform": "android@15.0.0",
|
|
22
24
|
"keystore": {
|
|
23
25
|
"path": "",
|
|
Binary file
|
|
Binary file
|
package/package.json
CHANGED
package/src/cli/index.js
CHANGED
|
@@ -10,7 +10,7 @@ function printHelp() {
|
|
|
10
10
|
|
|
11
11
|
Usage:
|
|
12
12
|
html2apk init
|
|
13
|
-
html2apk build [--release] [--debug] [--mode fullscreen|standalone|floating] [--theme fixed|auto] [--orientation vertical|horizontal] [--min-sdk 24] [--android-platform android@15.0.0]
|
|
13
|
+
html2apk build [--release] [--debug] [--apk|--aab] [--show-runtime-logs] [--mode fullscreen|standalone|floating] [--theme fixed|auto] [--orientation vertical|horizontal] [--min-sdk 24] [--android-platform android@15.0.0]
|
|
14
14
|
html2apk doctor
|
|
15
15
|
|
|
16
16
|
The current working directory is always treated as the user app root.`);
|
|
@@ -25,6 +25,15 @@ function parseBuildArgs(args) {
|
|
|
25
25
|
options.release = true;
|
|
26
26
|
} else if (arg === "--debug") {
|
|
27
27
|
options.debug = true;
|
|
28
|
+
} else if (arg === "--aab") {
|
|
29
|
+
options.buildFormat = "aab";
|
|
30
|
+
} else if (arg === "--apk") {
|
|
31
|
+
options.buildFormat = "apk";
|
|
32
|
+
} else if (arg === "--show-runtime-logs" || arg === "--runtime-logs" || arg === "--mostrar-logs") {
|
|
33
|
+
options.showRuntimeLogs = true;
|
|
34
|
+
} else if (arg === "--build-format" || arg === "--output-format" || arg === "--format") {
|
|
35
|
+
options.buildFormat = args[index + 1];
|
|
36
|
+
index += 1;
|
|
28
37
|
} else if (arg === "--mode") {
|
|
29
38
|
options.mode = args[index + 1];
|
|
30
39
|
index += 1;
|
|
@@ -94,6 +103,8 @@ function createPlaceholderConfig(projectName = "MeuApp") {
|
|
|
94
103
|
],
|
|
95
104
|
plugins: [],
|
|
96
105
|
release: false,
|
|
106
|
+
buildFormat: "apk",
|
|
107
|
+
showRuntimeLogs: false,
|
|
97
108
|
androidPlatform: "android@15.0.0",
|
|
98
109
|
keystore: {
|
|
99
110
|
path: "",
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
const fs = require("fs/promises");
|
|
4
4
|
const path = require("path");
|
|
5
5
|
|
|
6
|
-
async function walk(dirPath, results = []) {
|
|
6
|
+
async function walk(dirPath, extension, results = []) {
|
|
7
7
|
let entries = [];
|
|
8
8
|
try {
|
|
9
9
|
entries = await fs.readdir(dirPath, { withFileTypes: true });
|
|
@@ -14,8 +14,8 @@ async function walk(dirPath, results = []) {
|
|
|
14
14
|
for (const entry of entries) {
|
|
15
15
|
const fullPath = path.join(dirPath, entry.name);
|
|
16
16
|
if (entry.isDirectory()) {
|
|
17
|
-
await walk(fullPath, results);
|
|
18
|
-
} else if (entry.isFile() && entry.name.endsWith(
|
|
17
|
+
await walk(fullPath, extension, results);
|
|
18
|
+
} else if (entry.isFile() && entry.name.toLowerCase().endsWith(extension)) {
|
|
19
19
|
const stat = await fs.stat(fullPath);
|
|
20
20
|
results.push({ path: fullPath, mtimeMs: stat.mtimeMs });
|
|
21
21
|
}
|
|
@@ -24,22 +24,34 @@ async function walk(dirPath, results = []) {
|
|
|
24
24
|
return results;
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
27
|
+
function artifactFormat(options = {}) {
|
|
28
|
+
return String(options.buildFormat || options.outputFormat || options.packageType || "").toLowerCase() === "aab" ? "aab" : "apk";
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async function findAndroidArtifact(buildDir, options) {
|
|
32
|
+
const format = artifactFormat(options);
|
|
33
|
+
const outputRoot = path.join(buildDir, "platforms", "android", "app", "build", "outputs", format === "aab" ? "bundle" : "apk");
|
|
34
|
+
const extension = `.${format}`;
|
|
35
|
+
const artifacts = await walk(outputRoot, extension);
|
|
30
36
|
|
|
31
|
-
if (
|
|
32
|
-
throw new Error(
|
|
37
|
+
if (artifacts.length === 0) {
|
|
38
|
+
throw new Error(`Cordova build finished, but no ${format.toUpperCase()} was found.`);
|
|
33
39
|
}
|
|
34
40
|
|
|
35
41
|
const expectedFlavor = options.release ? "release" : "debug";
|
|
36
|
-
const preferred =
|
|
42
|
+
const preferred = artifacts
|
|
37
43
|
.filter((item) => item.path.toLowerCase().includes(expectedFlavor))
|
|
38
44
|
.sort((a, b) => b.mtimeMs - a.mtimeMs)[0];
|
|
39
45
|
|
|
40
|
-
return (preferred ||
|
|
46
|
+
return (preferred || artifacts.sort((a, b) => b.mtimeMs - a.mtimeMs)[0]).path;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async function findApk(buildDir, options) {
|
|
50
|
+
return findAndroidArtifact(buildDir, { ...options, buildFormat: "apk" });
|
|
41
51
|
}
|
|
42
52
|
|
|
43
53
|
module.exports = {
|
|
54
|
+
artifactFormat,
|
|
55
|
+
findAndroidArtifact,
|
|
44
56
|
findApk
|
|
45
57
|
};
|
package/src/cordova/project.js
CHANGED
|
@@ -30,6 +30,7 @@ async function addAndroidPlatform(buildDir, options, runner) {
|
|
|
30
30
|
|
|
31
31
|
async function buildAndroid(buildDir, options, buildJsonPath, runner) {
|
|
32
32
|
const args = ["build", "android", "--no-telemetry"];
|
|
33
|
+
const wantsBundle = String(options.buildFormat || options.outputFormat || options.packageType || "").toLowerCase() === "aab";
|
|
33
34
|
|
|
34
35
|
if (options.release) {
|
|
35
36
|
args.push("--release");
|
|
@@ -41,6 +42,10 @@ async function buildAndroid(buildDir, options, buildJsonPath, runner) {
|
|
|
41
42
|
args.push("--buildConfig", buildJsonPath);
|
|
42
43
|
}
|
|
43
44
|
|
|
45
|
+
if (wantsBundle) {
|
|
46
|
+
args.push("--", "--packageType=bundle");
|
|
47
|
+
}
|
|
48
|
+
|
|
44
49
|
await runner.run("cordova", args, {
|
|
45
50
|
cwd: buildDir,
|
|
46
51
|
pipeOutput: options.debug
|
package/src/core/build-apk.js
CHANGED
|
@@ -7,16 +7,20 @@ const { resolveBuildOptions } = require("./config");
|
|
|
7
7
|
const { validateEntryFile, validateRequiredOptions } = require("./validation");
|
|
8
8
|
const { createCordovaProject, addAndroidPlatform, buildAndroid, runAndroidDevice, addCordovaPlugin } = require("../cordova/project");
|
|
9
9
|
const { writeConfigXml } = require("../cordova/config-xml");
|
|
10
|
-
const { findApk } = require("../cordova/apk-finder");
|
|
10
|
+
const { artifactFormat, findAndroidArtifact, findApk } = require("../cordova/apk-finder");
|
|
11
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
16
|
const AUTO_THEME_SCRIPT_NAME = "html2apk-auto-theme.js";
|
|
17
|
+
const EARLY_BRIDGE_SCRIPT_NAME = "html2apk-early-bridge.js";
|
|
18
|
+
const RUNTIME_CONSOLE_SCRIPT_NAME = "html2apk-runtime-console.js";
|
|
19
|
+
const RUNTIME_CONSOLE_ICON_NAME = "html2apk-console.png";
|
|
17
20
|
const ONESIGNAL_SCRIPT_NAME = "html2apk-onesignal.js";
|
|
18
21
|
const ONESIGNAL_PLUGIN_PACKAGE = "onesignal-cordova-plugin";
|
|
19
22
|
const DEFAULT_APP_ICON_NAME = "html2apk.png";
|
|
23
|
+
const DEFAULT_CONSOLE_ICON_NAME = "console.png";
|
|
20
24
|
|
|
21
25
|
function defaultAppIconPath() {
|
|
22
26
|
return path.resolve(__dirname, "..", "..", DEFAULT_APP_ICON_NAME);
|
|
@@ -59,23 +63,64 @@ function hasOneSignal(options) {
|
|
|
59
63
|
return oneSignalAppId(options).length > 0;
|
|
60
64
|
}
|
|
61
65
|
|
|
66
|
+
async function pathExists(filePath) {
|
|
67
|
+
try {
|
|
68
|
+
await fs.access(filePath);
|
|
69
|
+
return true;
|
|
70
|
+
} catch {
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
62
75
|
function scriptTag(scriptPath) {
|
|
63
76
|
return `<script src="${scriptPath}"></script>`;
|
|
64
77
|
}
|
|
65
78
|
|
|
66
|
-
async function
|
|
79
|
+
async function findHtmlFiles(dirPath, results = []) {
|
|
80
|
+
let entries = [];
|
|
81
|
+
try {
|
|
82
|
+
entries = await fs.readdir(dirPath, { withFileTypes: true });
|
|
83
|
+
} catch {
|
|
84
|
+
return results;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
for (const entry of entries) {
|
|
88
|
+
const fullPath = path.join(dirPath, entry.name);
|
|
89
|
+
if (entry.isDirectory()) {
|
|
90
|
+
await findHtmlFiles(fullPath, results);
|
|
91
|
+
} else if (entry.isFile() && /\.html?$/i.test(entry.name)) {
|
|
92
|
+
results.push(fullPath);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return results;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async function injectCordovaRuntimeIntoHtml(htmlPath, scriptPath = "cordova.js", earlyBridgePath = EARLY_BRIDGE_SCRIPT_NAME, runtimeConsolePath = null) {
|
|
67
100
|
let html = await fs.readFile(htmlPath, "utf8");
|
|
68
|
-
|
|
101
|
+
const hasCordova = /<script\b[^>]*\bsrc=["'][^"']*cordova\.js["'][^>]*>/i.test(html);
|
|
102
|
+
const hasEarlyBridge = /<script\b[^>]*\bsrc=["'][^"']*html2apk-early-bridge\.js["'][^>]*>/i.test(html);
|
|
103
|
+
const hasRuntimeConsole = /<script\b[^>]*\bsrc=["'][^"']*html2apk-runtime-console\.js["'][^>]*>/i.test(html);
|
|
104
|
+
if (hasCordova && hasEarlyBridge && (!runtimeConsolePath || hasRuntimeConsole)) {
|
|
105
|
+
return false;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const tags = [
|
|
109
|
+
hasEarlyBridge ? null : scriptTag(earlyBridgePath),
|
|
110
|
+
runtimeConsolePath && !hasRuntimeConsole ? scriptTag(runtimeConsolePath) : null,
|
|
111
|
+
hasCordova ? null : scriptTag(scriptPath)
|
|
112
|
+
].filter(Boolean).join("\n ");
|
|
113
|
+
|
|
114
|
+
if (!tags) {
|
|
69
115
|
return false;
|
|
70
116
|
}
|
|
71
117
|
|
|
72
|
-
const tag = scriptTag(scriptPath);
|
|
73
118
|
if (/<head\b[^>]*>/i.test(html)) {
|
|
74
|
-
html = html.replace(/<head\b[^>]*>/i, (match) => `${match}\n ${
|
|
119
|
+
html = html.replace(/<head\b[^>]*>/i, (match) => `${match}\n ${tags}`);
|
|
75
120
|
} else if (/<html\b[^>]*>/i.test(html)) {
|
|
76
|
-
html = html.replace(/<html\b[^>]*>/i, (match) => `${match}\n ${
|
|
121
|
+
html = html.replace(/<html\b[^>]*>/i, (match) => `${match}\n ${tags}`);
|
|
77
122
|
} else {
|
|
78
|
-
html = `${
|
|
123
|
+
html = `${tags}\n${html}`;
|
|
79
124
|
}
|
|
80
125
|
|
|
81
126
|
await fs.writeFile(htmlPath, html, "utf8");
|
|
@@ -85,13 +130,39 @@ async function injectCordovaRuntimeIntoHtml(htmlPath, scriptPath = "cordova.js")
|
|
|
85
130
|
async function installCordovaRuntimeScript(buildDir, options) {
|
|
86
131
|
const wwwDir = path.join(buildDir, "www");
|
|
87
132
|
const entryHtmlPath = path.resolve(wwwDir, options.entryFile || "index.html");
|
|
88
|
-
const
|
|
133
|
+
const earlyBridgeSource = path.resolve(__dirname, "..", "templates", EARLY_BRIDGE_SCRIPT_NAME);
|
|
134
|
+
const earlyBridgeTarget = path.join(wwwDir, EARLY_BRIDGE_SCRIPT_NAME);
|
|
135
|
+
const runtimeConsoleSource = path.resolve(__dirname, "..", "templates", RUNTIME_CONSOLE_SCRIPT_NAME);
|
|
136
|
+
const runtimeConsoleTarget = path.join(wwwDir, RUNTIME_CONSOLE_SCRIPT_NAME);
|
|
137
|
+
const runtimeConsoleIconSource = path.resolve(__dirname, "..", "..", DEFAULT_CONSOLE_ICON_NAME);
|
|
138
|
+
const runtimeConsoleIconTarget = path.join(wwwDir, RUNTIME_CONSOLE_ICON_NAME);
|
|
139
|
+
const htmlFiles = await findHtmlFiles(wwwDir);
|
|
140
|
+
let injectedCount = 0;
|
|
89
141
|
|
|
90
142
|
if (!isInside(wwwDir, entryHtmlPath)) {
|
|
91
143
|
throw new Error(`Entry file must stay inside the Cordova www folder: ${options.entryFile}`);
|
|
92
144
|
}
|
|
93
145
|
|
|
94
|
-
|
|
146
|
+
await copyFile(earlyBridgeSource, earlyBridgeTarget);
|
|
147
|
+
if (options.showRuntimeLogs) {
|
|
148
|
+
await copyFile(runtimeConsoleSource, runtimeConsoleTarget);
|
|
149
|
+
if (await pathExists(runtimeConsoleIconSource)) {
|
|
150
|
+
await copyFile(runtimeConsoleIconSource, runtimeConsoleIconTarget);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
for (const htmlPath of htmlFiles) {
|
|
155
|
+
const scriptPath = toCordovaPath(path.relative(path.dirname(htmlPath), path.join(wwwDir, "cordova.js"))) || "cordova.js";
|
|
156
|
+
const earlyBridgePath = toCordovaPath(path.relative(path.dirname(htmlPath), earlyBridgeTarget)) || EARLY_BRIDGE_SCRIPT_NAME;
|
|
157
|
+
const runtimeConsolePath = options.showRuntimeLogs
|
|
158
|
+
? (toCordovaPath(path.relative(path.dirname(htmlPath), runtimeConsoleTarget)) || RUNTIME_CONSOLE_SCRIPT_NAME)
|
|
159
|
+
: null;
|
|
160
|
+
if (await injectCordovaRuntimeIntoHtml(htmlPath, scriptPath, earlyBridgePath, runtimeConsolePath)) {
|
|
161
|
+
injectedCount += 1;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return injectedCount;
|
|
95
166
|
}
|
|
96
167
|
|
|
97
168
|
async function injectScriptIntoHtml(htmlPath, scriptPath) {
|
|
@@ -254,7 +325,7 @@ async function createBuildJson(buildDir, options, projectRoot) {
|
|
|
254
325
|
const storeFile = path.resolve(projectRoot, options.keystore.path);
|
|
255
326
|
const buildJsonPath = path.join(buildDir, "build.json");
|
|
256
327
|
const release = {
|
|
257
|
-
packageType: "apk",
|
|
328
|
+
packageType: artifactFormat(options) === "aab" ? "bundle" : "apk",
|
|
258
329
|
keystore: storeFile,
|
|
259
330
|
storePassword: options.keystore.storePassword || options.keystore.password,
|
|
260
331
|
alias: options.keystore.alias,
|
|
@@ -272,10 +343,11 @@ async function createBuildJson(buildDir, options, projectRoot) {
|
|
|
272
343
|
return buildJsonPath;
|
|
273
344
|
}
|
|
274
345
|
|
|
275
|
-
function
|
|
346
|
+
function outputAndroidName(options) {
|
|
276
347
|
const safeName = String(options.appName || "app").replace(/[^a-zA-Z0-9._-]+/g, "-");
|
|
277
348
|
const flavor = options.release ? "release" : "debug";
|
|
278
|
-
|
|
349
|
+
const extension = artifactFormat(options);
|
|
350
|
+
return `${safeName}-${options.version}-${flavor}.${extension}`;
|
|
279
351
|
}
|
|
280
352
|
|
|
281
353
|
function parseAdbDevices(output) {
|
|
@@ -339,8 +411,12 @@ async function prepareCordovaProject(projectRoot, buildDir, options, runner, log
|
|
|
339
411
|
cordovaOptions.androidSplashScreenAnimatedIcon = toBuildAssetPath(buildDir, cordovaOptions.splash);
|
|
340
412
|
await writeConfigXml(path.join(buildDir, "config.xml"), cordovaOptions);
|
|
341
413
|
await copyWebAssets(path.resolve(projectRoot, options.webRoot || "."), path.join(buildDir, "www"), options, projectRoot);
|
|
342
|
-
|
|
343
|
-
|
|
414
|
+
const injectedRuntimePages = await installCordovaRuntimeScript(buildDir, options);
|
|
415
|
+
if (injectedRuntimePages) {
|
|
416
|
+
log(`Cordova runtime: injected scripts into ${injectedRuntimePages} HTML page(s).`);
|
|
417
|
+
}
|
|
418
|
+
if (options.showRuntimeLogs) {
|
|
419
|
+
log("Runtime console: enabled inside the generated APK.");
|
|
344
420
|
}
|
|
345
421
|
if (await installAutoThemeScript(buildDir, options)) {
|
|
346
422
|
log("Theme mode: auto (system bars follow the visible screen color).");
|
|
@@ -363,14 +439,14 @@ async function prepareCordovaProject(projectRoot, buildDir, options, runner, log
|
|
|
363
439
|
await addAndroidPlatform(buildDir, options, runner);
|
|
364
440
|
}
|
|
365
441
|
|
|
366
|
-
async function
|
|
367
|
-
const
|
|
442
|
+
async function copyBuiltArtifact(projectRoot, buildDir, options) {
|
|
443
|
+
const artifactPathInBuild = await findAndroidArtifact(buildDir, options);
|
|
368
444
|
const outputDir = path.resolve(projectRoot, options.outputDir || "dist");
|
|
369
445
|
await ensureDir(outputDir);
|
|
370
446
|
|
|
371
|
-
const
|
|
372
|
-
await copyFile(
|
|
373
|
-
return
|
|
447
|
+
const artifactPath = path.join(outputDir, outputAndroidName(options));
|
|
448
|
+
await copyFile(artifactPathInBuild, artifactPath);
|
|
449
|
+
return artifactPath;
|
|
374
450
|
}
|
|
375
451
|
|
|
376
452
|
async function ensureBuiltDebugApk(buildDir, options, runner, log) {
|
|
@@ -448,7 +524,8 @@ async function buildApk(overrides = {}) {
|
|
|
448
524
|
const buildJsonPath = await createBuildJson(buildDir, options, projectRoot);
|
|
449
525
|
await buildAndroid(buildDir, options, buildJsonPath, runner);
|
|
450
526
|
|
|
451
|
-
const
|
|
527
|
+
const artifactPath = await copyBuiltArtifact(projectRoot, buildDir, options);
|
|
528
|
+
const artifactType = artifactFormat(options);
|
|
452
529
|
|
|
453
530
|
if (!options.debug) {
|
|
454
531
|
await removePath(tempRoot);
|
|
@@ -456,7 +533,9 @@ async function buildApk(overrides = {}) {
|
|
|
456
533
|
}
|
|
457
534
|
|
|
458
535
|
return {
|
|
459
|
-
apkPath,
|
|
536
|
+
apkPath: artifactPath,
|
|
537
|
+
artifactPath,
|
|
538
|
+
artifactType,
|
|
460
539
|
buildDir: options.debug ? buildDir : null,
|
|
461
540
|
logs,
|
|
462
541
|
status: "success",
|
|
@@ -523,7 +602,7 @@ async function runDebugUsb(overrides = {}) {
|
|
|
523
602
|
await installDebugApkWithAdb(buildDir, options, runner, device, log);
|
|
524
603
|
}
|
|
525
604
|
|
|
526
|
-
const apkPath = await
|
|
605
|
+
const apkPath = await copyBuiltArtifact(projectRoot, buildDir, { ...options, buildFormat: "apk" });
|
|
527
606
|
|
|
528
607
|
if (!options.debug) {
|
|
529
608
|
await removePath(tempRoot);
|
|
@@ -560,6 +639,7 @@ module.exports = {
|
|
|
560
639
|
buildApk,
|
|
561
640
|
defaultAppIconPath,
|
|
562
641
|
injectCordovaRuntimeIntoHtml,
|
|
642
|
+
installCordovaRuntimeScript,
|
|
563
643
|
parseAdbDevices,
|
|
564
644
|
runDebugUsb
|
|
565
645
|
};
|
package/src/core/config.js
CHANGED
|
@@ -94,12 +94,23 @@ function normalizeOptions(options) {
|
|
|
94
94
|
: "default";
|
|
95
95
|
normalized.debug = Boolean(normalized.debug);
|
|
96
96
|
normalized.release = Boolean(normalized.release);
|
|
97
|
+
normalized.buildFormat = normalizeBuildFormat(normalized.buildFormat || normalized.outputFormat || normalized.artifactType || normalized.packageType);
|
|
98
|
+
if (normalized.buildFormat === "aab") {
|
|
99
|
+
normalized.release = true;
|
|
100
|
+
}
|
|
97
101
|
const themeColorText = String(normalized.themeColor || "").trim().toLowerCase();
|
|
98
102
|
const themeModeText = String(normalized.themeMode || "").trim().toLowerCase();
|
|
99
103
|
const themeText = String(normalized.theme || "").trim().toLowerCase();
|
|
100
104
|
normalized.themeMode = themeModeText === "auto" || themeText === "auto" || themeColorText === "auto" ? "auto" : "fixed";
|
|
101
105
|
normalized.theme = normalized.themeMode;
|
|
102
106
|
normalized.themeColor = normalizeThemeColor(themeColorText === "auto" ? normalized.backgroundColor : normalized.themeColor);
|
|
107
|
+
normalized.showRuntimeLogs = Boolean(
|
|
108
|
+
normalized.showRuntimeLogs ||
|
|
109
|
+
normalized.mostrarLogs ||
|
|
110
|
+
normalized.runtimeLogs ||
|
|
111
|
+
normalized.debugConsole ||
|
|
112
|
+
normalized.console
|
|
113
|
+
);
|
|
103
114
|
normalized.oneSignalAppId = normalizeOneSignalAppId(normalized.oneSignalAppId || normalized.onesignalAppId || normalized.oneSignal?.appId || normalized.onesignal?.appId);
|
|
104
115
|
normalized.minSdkVersion = normalizeMinSdkVersion(normalized.minSdkVersion || normalized.androidMinSdkVersion);
|
|
105
116
|
normalized.permissions = Array.isArray(normalized.permissions)
|
|
@@ -119,6 +130,10 @@ function normalizeOneSignalAppId(value) {
|
|
|
119
130
|
return String(value || "").trim();
|
|
120
131
|
}
|
|
121
132
|
|
|
133
|
+
function normalizeBuildFormat(value) {
|
|
134
|
+
return String(value || "").trim().toLowerCase() === "aab" ? "aab" : "apk";
|
|
135
|
+
}
|
|
136
|
+
|
|
122
137
|
function normalizeDeepLinks(value) {
|
|
123
138
|
const input = value && typeof value === "object" ? value : {};
|
|
124
139
|
const schemes = Array.isArray(input.schemes)
|
|
@@ -164,5 +179,6 @@ module.exports = {
|
|
|
164
179
|
normalizeMinSdkVersion,
|
|
165
180
|
normalizeThemeMode,
|
|
166
181
|
normalizeOneSignalAppId,
|
|
182
|
+
normalizeBuildFormat,
|
|
167
183
|
normalizeDeepLinks
|
|
168
184
|
};
|
package/src/core/defaults.js
CHANGED
|
@@ -27,11 +27,13 @@ function createDefaultOptions(projectRoot) {
|
|
|
27
27
|
permissions: ["INTERNET", "POST_NOTIFICATIONS", "VIBRATE"],
|
|
28
28
|
plugins: [],
|
|
29
29
|
release: false,
|
|
30
|
+
buildFormat: "apk",
|
|
30
31
|
keystore: null,
|
|
31
32
|
androidPlatform: "android@15.0.0",
|
|
32
33
|
minSdkVersion: DEFAULT_ANDROID_MIN_SDK_VERSION,
|
|
33
34
|
themeColor: "#126fff",
|
|
34
35
|
themeMode: "fixed",
|
|
36
|
+
showRuntimeLogs: false,
|
|
35
37
|
oneSignalAppId: "",
|
|
36
38
|
deepLinks: {
|
|
37
39
|
schemes: [],
|