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 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. Se uma funcao nativa for chamada antes do `deviceready`, a bridge espera o Android ficar pronto antes de executar.
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
- { id: "abrir", titulo: "Abrir" },
426
- { id: "cancelar", titulo: "Cancelar" }
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
- acao: "abrir-rota",
441
- rota: "/lembretes"
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": "",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "html2apk",
3
- "version": "0.4.0",
3
+ "version": "0.7.0",
4
4
  "description": "Node CLI and library to turn an HTML/CSS/JS folder into an Android APK through Cordova.",
5
5
  "main": "index.js",
6
6
  "bin": {
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(".apk")) {
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
- async function findApk(buildDir, options) {
28
- const outputRoot = path.join(buildDir, "platforms", "android", "app", "build", "outputs", "apk");
29
- const apks = await walk(outputRoot);
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 (apks.length === 0) {
32
- throw new Error("Cordova build finished, but no APK was found.");
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 = apks
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 || apks.sort((a, b) => b.mtimeMs - a.mtimeMs)[0]).path;
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
  };
@@ -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
@@ -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 injectCordovaRuntimeIntoHtml(htmlPath, scriptPath = "cordova.js") {
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
- if (/<script\b[^>]*\bsrc=["'][^"']*cordova\.js["'][^>]*>/i.test(html)) {
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 ${tag}`);
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 ${tag}`);
121
+ html = html.replace(/<html\b[^>]*>/i, (match) => `${match}\n ${tags}`);
77
122
  } else {
78
- html = `${tag}\n${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 scriptPath = toCordovaPath(path.relative(path.dirname(entryHtmlPath), path.join(wwwDir, "cordova.js"))) || "cordova.js";
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
- return injectCordovaRuntimeIntoHtml(entryHtmlPath, scriptPath);
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 outputApkName(options) {
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
- return `${safeName}-${options.version}-${flavor}.apk`;
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
- if (await installCordovaRuntimeScript(buildDir, options)) {
343
- log("Cordova runtime: injected cordova.js into the entry HTML.");
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 copyBuiltApk(projectRoot, buildDir, options) {
367
- const apkPathInBuild = await findApk(buildDir, options);
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 apkPath = path.join(outputDir, outputApkName(options));
372
- await copyFile(apkPathInBuild, apkPath);
373
- return apkPath;
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 apkPath = await copyBuiltApk(projectRoot, buildDir, options);
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 copyBuiltApk(projectRoot, buildDir, options);
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
  };
@@ -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
  };
@@ -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: [],