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.
@@ -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
- const { webRoot } = await validateEntryFile(projectRoot, options);
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 createCordovaProject(buildDir, options, runner);
133
- const cordovaOptions = { ...options };
134
- cordovaOptions.icon = await copyCordovaAsset(projectRoot, buildDir, options.icon, "icon");
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 bridgePluginPath = await installBridgePlugin(buildDir);
141
- await addCordovaPlugin(buildDir, bridgePluginPath, runner);
451
+ const apkPath = await copyBuiltApk(projectRoot, buildDir, options);
142
452
 
143
- for (const plugin of options.plugins) {
144
- await addCordovaPlugin(buildDir, plugin, runner);
453
+ if (!options.debug) {
454
+ await removePath(tempRoot);
455
+ tempCleaned = true;
145
456
  }
146
457
 
147
- await addAndroidPlatform(buildDir, options, runner);
148
- const buildJsonPath = await createBuildJson(buildDir, options, projectRoot);
149
- await buildAndroid(buildDir, options, buildJsonPath, runner);
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
- const apkPathInBuild = await findApk(buildDir, options);
152
- const outputDir = path.resolve(projectRoot, options.outputDir || "dist");
153
- await ensureDir(outputDir);
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 = path.join(outputDir, outputApkName(options));
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
  };
@@ -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
  };
@@ -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: ".",