@tamer4lynx/cli 0.0.8 → 0.0.11

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.
Files changed (3) hide show
  1. package/README.md +45 -30
  2. package/dist/index.js +1907 -1358
  3. package/package.json +2 -4
package/dist/index.js CHANGED
@@ -7,16 +7,16 @@ process.on("warning", (w) => {
7
7
  });
8
8
 
9
9
  // index.ts
10
- import fs23 from "fs";
11
- import path24 from "path";
10
+ import fs24 from "fs";
11
+ import path25 from "path";
12
12
  import { program } from "commander";
13
13
 
14
14
  // package.json
15
- var version = "0.0.8";
15
+ var version = "0.0.11";
16
16
 
17
17
  // src/android/create.ts
18
- import fs3 from "fs";
19
- import path3 from "path";
18
+ import fs4 from "fs";
19
+ import path4 from "path";
20
20
  import os2 from "os";
21
21
 
22
22
  // src/android/getGradle.ts
@@ -297,6 +297,55 @@ function loadHostConfig(cwd = process.cwd()) {
297
297
  if (!cfg) throw new Error("tamer.config.json not found in the project root.");
298
298
  return cfg;
299
299
  }
300
+ function formatAdaptiveInsetValue(v, fallback) {
301
+ if (v === void 0) return fallback;
302
+ if (typeof v === "number") {
303
+ if (!Number.isFinite(v) || v < 0 || v > 50) return fallback;
304
+ return `${v}%`;
305
+ }
306
+ const s = String(v).trim();
307
+ if (s.endsWith("%") || s.endsWith("dp")) return s;
308
+ if (/^\d+(\.\d+)?$/.test(s)) return `${s}%`;
309
+ return fallback;
310
+ }
311
+ function resolveAdaptiveForegroundLayoutFromConfig(ad) {
312
+ const hasLayoutOpt = ad.foregroundScale != null || ad.foregroundPadding != null;
313
+ if (!hasLayoutOpt) return void 0;
314
+ let scale = ad.foregroundScale;
315
+ if (scale != null && typeof scale === "number") {
316
+ if (!Number.isFinite(scale)) scale = void 0;
317
+ else scale = Math.min(1, Math.max(0.05, scale));
318
+ }
319
+ let padding;
320
+ if (ad.foregroundPadding != null) {
321
+ const pad = ad.foregroundPadding;
322
+ if (typeof pad === "number" || typeof pad === "string") {
323
+ const u = formatAdaptiveInsetValue(pad, "0%");
324
+ padding = { left: u, top: u, right: u, bottom: u };
325
+ } else {
326
+ const d = "0%";
327
+ padding = {
328
+ left: formatAdaptiveInsetValue(pad.left, d),
329
+ top: formatAdaptiveInsetValue(pad.top, d),
330
+ right: formatAdaptiveInsetValue(pad.right, d),
331
+ bottom: formatAdaptiveInsetValue(pad.bottom, d)
332
+ };
333
+ }
334
+ }
335
+ return { scale, padding };
336
+ }
337
+ function normalizeAndroidAdaptiveColor(input) {
338
+ const raw = input.trim().replace(/^#/, "");
339
+ if (!/^[0-9a-fA-F]{3}$|^[0-9a-fA-F]{6}$|^[0-9a-fA-F]{8}$/.test(raw)) return null;
340
+ let h = raw;
341
+ if (h.length === 3) {
342
+ h = h[0] + h[0] + h[1] + h[1] + h[2] + h[2];
343
+ }
344
+ if (h.length === 6) {
345
+ h = "FF" + h;
346
+ }
347
+ return `#${h.toUpperCase()}`;
348
+ }
300
349
  function resolveIconPaths(projectRoot, config) {
301
350
  const raw = config.icon;
302
351
  if (!raw) return null;
@@ -318,9 +367,225 @@ function resolveIconPaths(projectRoot, config) {
318
367
  const p = join(raw.ios);
319
368
  if (fs2.existsSync(p)) out.ios = p;
320
369
  }
370
+ const ad = raw.androidAdaptive;
371
+ if (ad?.foreground) {
372
+ const fg = join(ad.foreground);
373
+ if (fs2.existsSync(fg)) {
374
+ let usedAdaptive = false;
375
+ if (ad.background) {
376
+ const bg = join(ad.background);
377
+ if (fs2.existsSync(bg)) {
378
+ out.androidAdaptiveForeground = fg;
379
+ out.androidAdaptiveBackground = bg;
380
+ usedAdaptive = true;
381
+ }
382
+ }
383
+ if (!usedAdaptive && ad.backgroundColor) {
384
+ const norm = normalizeAndroidAdaptiveColor(ad.backgroundColor);
385
+ if (norm) {
386
+ out.androidAdaptiveForeground = fg;
387
+ out.androidAdaptiveBackgroundColor = norm;
388
+ }
389
+ }
390
+ if (out.androidAdaptiveForeground) {
391
+ const lay = resolveAdaptiveForegroundLayoutFromConfig(ad);
392
+ if (lay) out.androidAdaptiveForegroundLayout = lay;
393
+ }
394
+ }
395
+ }
321
396
  return Object.keys(out).length ? out : null;
322
397
  }
323
398
 
399
+ // src/common/syncAppIcons.ts
400
+ import fs3 from "fs";
401
+ import path3 from "path";
402
+ function purgeAdaptiveForegroundArtifacts(drawableDir) {
403
+ for (const base of ["ic_launcher_fg_src", "ic_launcher_fg_bm", "ic_launcher_fg_sc"]) {
404
+ for (const ext of [".png", ".webp", ".jpg", ".jpeg", ".xml"]) {
405
+ try {
406
+ fs3.unlinkSync(path3.join(drawableDir, base + ext));
407
+ } catch {
408
+ }
409
+ }
410
+ }
411
+ try {
412
+ fs3.unlinkSync(path3.join(drawableDir, "ic_launcher_foreground.xml"));
413
+ } catch {
414
+ }
415
+ }
416
+ function parsePadDp(v) {
417
+ if (v.endsWith("dp")) return Math.max(0, Math.min(54, parseFloat(v)));
418
+ if (v.endsWith("%")) return Math.max(0, Math.min(54, parseFloat(v) / 100 * 108));
419
+ return 0;
420
+ }
421
+ function writeAdaptiveForegroundLayer(drawableDir, fgSourcePath, fgExt, layout) {
422
+ purgeAdaptiveForegroundArtifacts(drawableDir);
423
+ for (const ext of [".png", ".webp", ".jpg", ".jpeg"]) {
424
+ try {
425
+ fs3.unlinkSync(path3.join(drawableDir, `ic_launcher_foreground${ext}`));
426
+ } catch {
427
+ }
428
+ }
429
+ if (!layout) {
430
+ fs3.copyFileSync(fgSourcePath, path3.join(drawableDir, `ic_launcher_foreground${fgExt}`));
431
+ return;
432
+ }
433
+ fs3.copyFileSync(fgSourcePath, path3.join(drawableDir, `ic_launcher_fg_src${fgExt}`));
434
+ const CANVAS_DP = 108;
435
+ const scale = layout.scale ?? 1;
436
+ const shrinkDp = (1 - scale) / 2 * CANVAS_DP;
437
+ const pad = layout.padding;
438
+ const parsedPad = {
439
+ left: pad ? parsePadDp(pad.left) : 0,
440
+ top: pad ? parsePadDp(pad.top) : 0,
441
+ right: pad ? parsePadDp(pad.right) : 0,
442
+ bottom: pad ? parsePadDp(pad.bottom) : 0
443
+ };
444
+ const insetLeft = Math.round(shrinkDp + parsedPad.left);
445
+ const insetTop = Math.round(shrinkDp + parsedPad.top);
446
+ const insetRight = Math.round(shrinkDp + parsedPad.right);
447
+ const insetBottom = Math.round(shrinkDp + parsedPad.bottom);
448
+ const layerXml = `<?xml version="1.0" encoding="utf-8"?>
449
+ <layer-list xmlns:android="http://schemas.android.com/apk/res/android">
450
+ <item
451
+ android:left="${insetLeft}dp"
452
+ android:top="${insetTop}dp"
453
+ android:right="${insetRight}dp"
454
+ android:bottom="${insetBottom}dp"
455
+ android:drawable="@drawable/ic_launcher_fg_src" />
456
+ </layer-list>`;
457
+ fs3.writeFileSync(path3.join(drawableDir, "ic_launcher_foreground.xml"), layerXml);
458
+ }
459
+ function ensureAndroidManifestLauncherIcon(manifestPath) {
460
+ if (!fs3.existsSync(manifestPath)) return;
461
+ let content = fs3.readFileSync(manifestPath, "utf8");
462
+ if (content.includes("android:icon=")) return;
463
+ const next = content.replace(/<application(\s[^>]*)>/, (full, attrs) => {
464
+ if (String(attrs).includes("android:icon")) return full;
465
+ return `<application${attrs}
466
+ android:icon="@mipmap/ic_launcher"
467
+ android:roundIcon="@mipmap/ic_launcher">`;
468
+ });
469
+ if (next !== content) {
470
+ fs3.writeFileSync(manifestPath, next, "utf8");
471
+ console.log("\u2705 Added android:icon / roundIcon to AndroidManifest.xml");
472
+ }
473
+ }
474
+ function applyAndroidLauncherIcons(resDir, iconPaths) {
475
+ if (!iconPaths) return false;
476
+ fs3.mkdirSync(resDir, { recursive: true });
477
+ const fgAd = iconPaths.androidAdaptiveForeground;
478
+ const bgAd = iconPaths.androidAdaptiveBackground;
479
+ const bgColor = iconPaths.androidAdaptiveBackgroundColor;
480
+ if (fgAd && (bgAd || bgColor)) {
481
+ const drawableDir = path3.join(resDir, "drawable");
482
+ fs3.mkdirSync(drawableDir, { recursive: true });
483
+ const fgExt = path3.extname(fgAd) || ".png";
484
+ writeAdaptiveForegroundLayer(drawableDir, fgAd, fgExt, iconPaths.androidAdaptiveForegroundLayout);
485
+ if (bgColor) {
486
+ for (const ext of [".png", ".webp", ".jpg", ".jpeg"]) {
487
+ try {
488
+ fs3.unlinkSync(path3.join(drawableDir, `ic_launcher_background${ext}`));
489
+ } catch {
490
+ }
491
+ }
492
+ try {
493
+ fs3.unlinkSync(path3.join(drawableDir, "ic_launcher_background.xml"));
494
+ } catch {
495
+ }
496
+ const shapeXml = `<?xml version="1.0" encoding="utf-8"?>
497
+ <shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle">
498
+ <solid android:color="${bgColor}" />
499
+ </shape>
500
+ `;
501
+ fs3.writeFileSync(path3.join(drawableDir, "ic_launcher_background.xml"), shapeXml);
502
+ } else {
503
+ try {
504
+ fs3.unlinkSync(path3.join(drawableDir, "ic_launcher_background.xml"));
505
+ } catch {
506
+ }
507
+ const bgExt = path3.extname(bgAd) || ".png";
508
+ fs3.copyFileSync(bgAd, path3.join(drawableDir, `ic_launcher_background${bgExt}`));
509
+ }
510
+ const anyDpi = path3.join(resDir, "mipmap-anydpi-v26");
511
+ fs3.mkdirSync(anyDpi, { recursive: true });
512
+ const adaptiveXml = `<?xml version="1.0" encoding="utf-8"?>
513
+ <adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
514
+ <background android:drawable="@drawable/ic_launcher_background" />
515
+ <foreground android:drawable="@drawable/ic_launcher_foreground" />
516
+ </adaptive-icon>
517
+ `;
518
+ fs3.writeFileSync(path3.join(anyDpi, "ic_launcher.xml"), adaptiveXml);
519
+ fs3.writeFileSync(path3.join(anyDpi, "ic_launcher_round.xml"), adaptiveXml);
520
+ const legacySrc = iconPaths.source ?? fgAd;
521
+ const legacyExt = path3.extname(legacySrc) || ".png";
522
+ const mipmapDensities = ["mdpi", "hdpi", "xhdpi", "xxhdpi", "xxxhdpi"];
523
+ for (const d of mipmapDensities) {
524
+ const dir = path3.join(resDir, `mipmap-${d}`);
525
+ fs3.mkdirSync(dir, { recursive: true });
526
+ fs3.copyFileSync(legacySrc, path3.join(dir, `ic_launcher${legacyExt}`));
527
+ }
528
+ return true;
529
+ }
530
+ if (iconPaths.android) {
531
+ const src = iconPaths.android;
532
+ const entries = fs3.readdirSync(src, { withFileTypes: true });
533
+ for (const e of entries) {
534
+ const dest = path3.join(resDir, e.name);
535
+ if (e.isDirectory()) {
536
+ fs3.cpSync(path3.join(src, e.name), dest, { recursive: true });
537
+ } else {
538
+ fs3.copyFileSync(path3.join(src, e.name), dest);
539
+ }
540
+ }
541
+ return true;
542
+ }
543
+ if (iconPaths.source) {
544
+ const mipmapDensities = ["mdpi", "hdpi", "xhdpi", "xxhdpi", "xxxhdpi"];
545
+ for (const d of mipmapDensities) {
546
+ const dir = path3.join(resDir, `mipmap-${d}`);
547
+ fs3.mkdirSync(dir, { recursive: true });
548
+ fs3.copyFileSync(iconPaths.source, path3.join(dir, "ic_launcher.png"));
549
+ }
550
+ return true;
551
+ }
552
+ return false;
553
+ }
554
+ function applyIosAppIconAssets(appIconDir, iconPaths) {
555
+ if (!iconPaths) return false;
556
+ fs3.mkdirSync(appIconDir, { recursive: true });
557
+ if (iconPaths.ios) {
558
+ const entries = fs3.readdirSync(iconPaths.ios, { withFileTypes: true });
559
+ for (const e of entries) {
560
+ const dest = path3.join(appIconDir, e.name);
561
+ if (e.isDirectory()) {
562
+ fs3.cpSync(path3.join(iconPaths.ios, e.name), dest, { recursive: true });
563
+ } else {
564
+ fs3.copyFileSync(path3.join(iconPaths.ios, e.name), dest);
565
+ }
566
+ }
567
+ return true;
568
+ }
569
+ if (iconPaths.source) {
570
+ const ext = path3.extname(iconPaths.source) || ".png";
571
+ const icon1024 = `Icon-1024${ext}`;
572
+ fs3.copyFileSync(iconPaths.source, path3.join(appIconDir, icon1024));
573
+ fs3.writeFileSync(
574
+ path3.join(appIconDir, "Contents.json"),
575
+ JSON.stringify(
576
+ {
577
+ images: [{ filename: icon1024, idiom: "universal", platform: "ios", size: "1024x1024" }],
578
+ info: { author: "xcode", version: 1 }
579
+ },
580
+ null,
581
+ 2
582
+ )
583
+ );
584
+ return true;
585
+ }
586
+ return false;
587
+ }
588
+
324
589
  // src/explorer/ref.ts
325
590
  var LYNX_RAW_BASE = "https://raw.githubusercontent.com/lynx-family/lynx/develop/explorer";
326
591
  async function fetchExplorerFile(relativePath) {
@@ -573,6 +838,7 @@ function getProjectActivity(vars) {
573
838
  ` : "";
574
839
  const devClientImports = hasDevClient ? `
575
840
  import ${vars.packageName}.DevClientManager
841
+ import com.nanofuxion.tamerdevclient.DevClientModule
576
842
  import com.nanofuxion.tamerdevclient.TamerRelogLogService` : "";
577
843
  const reloadMethod = hasDevClient ? `
578
844
  private fun reloadProjectView() {
@@ -585,7 +851,7 @@ import com.nanofuxion.tamerdevclient.TamerRelogLogService` : "";
585
851
  setContentView(nextView)
586
852
  GeneratedActivityLifecycle.onViewAttached(nextView)
587
853
  GeneratedLynxExtensions.onHostViewChanged(nextView)
588
- nextView.renderTemplateUrl("main.lynx.bundle", "")
854
+ nextView.renderTemplateUrl("main.lynx.bundle", DevClientModule.getProjectInitDataJson(this))
589
855
  GeneratedActivityLifecycle.onCreateDelayed(handler)
590
856
  }
591
857
  ` : "";
@@ -595,9 +861,6 @@ import android.content.Intent
595
861
  import android.os.Bundle
596
862
  import android.os.Handler
597
863
  import android.os.Looper
598
- import android.view.MotionEvent
599
- import android.view.inputmethod.InputMethodManager
600
- import android.widget.EditText
601
864
  import androidx.appcompat.app.AppCompatActivity
602
865
  import androidx.core.view.WindowCompat
603
866
  import androidx.core.view.WindowInsetsControllerCompat
@@ -619,7 +882,7 @@ ${devClientField} private val handler = Handler(Looper.getMainLooper())
619
882
  setContentView(lynxView)
620
883
  GeneratedActivityLifecycle.onViewAttached(lynxView)
621
884
  GeneratedLynxExtensions.onHostViewChanged(lynxView)
622
- lynxView?.renderTemplateUrl("main.lynx.bundle", "")${devClientInit}
885
+ lynxView?.renderTemplateUrl("main.lynx.bundle", ${hasDevClient ? "DevClientModule.getProjectInitDataJson(this)" : '""'})${devClientInit}
623
886
  GeneratedActivityLifecycle.onCreateDelayed(handler)
624
887
  }
625
888
 
@@ -633,26 +896,6 @@ ${reloadMethod}
633
896
  GeneratedActivityLifecycle.onResume()
634
897
  }
635
898
 
636
- override fun dispatchTouchEvent(ev: MotionEvent): Boolean {
637
- if (ev.action == MotionEvent.ACTION_DOWN) maybeClearFocusedInput(ev)
638
- return super.dispatchTouchEvent(ev)
639
- }
640
-
641
- private fun maybeClearFocusedInput(ev: MotionEvent) {
642
- val focused = currentFocus
643
- if (focused is EditText) {
644
- val loc = IntArray(2)
645
- focused.getLocationOnScreen(loc)
646
- val x = ev.rawX.toInt()
647
- val y = ev.rawY.toInt()
648
- if (x < loc[0] || x > loc[0] + focused.width || y < loc[1] || y > loc[1] + focused.height) {
649
- focused.clearFocus()
650
- (getSystemService(INPUT_METHOD_SERVICE) as? InputMethodManager)
651
- ?.hideSoftInputFromWindow(focused.windowToken, 0)
652
- }
653
- }
654
- }
655
-
656
899
  override fun onNewIntent(intent: Intent) {
657
900
  super.onNewIntent(intent)
658
901
  setIntent(intent)
@@ -778,9 +1021,6 @@ import com.nanofuxion.tamerdevclient.DevClientModule
778
1021
 
779
1022
  import android.os.Build
780
1023
  import android.os.Bundle
781
- import android.view.MotionEvent
782
- import android.view.inputmethod.InputMethodManager
783
- import android.widget.EditText
784
1024
  import androidx.appcompat.app.AppCompatActivity
785
1025
  import androidx.core.view.WindowCompat
786
1026
  import androidx.core.view.WindowInsetsControllerCompat
@@ -813,26 +1053,6 @@ ${devClientField} private var lynxView: LynxView? = null${!hasDevClient ? "\n
813
1053
  GeneratedActivityLifecycle.onResume()
814
1054
  }
815
1055
 
816
- override fun dispatchTouchEvent(ev: MotionEvent): Boolean {
817
- if (ev.action == MotionEvent.ACTION_DOWN) maybeClearFocusedInput(ev)
818
- return super.dispatchTouchEvent(ev)
819
- }
820
-
821
- private fun maybeClearFocusedInput(ev: MotionEvent) {
822
- val focused = currentFocus
823
- if (focused is EditText) {
824
- val loc = IntArray(2)
825
- focused.getLocationOnScreen(loc)
826
- val x = ev.rawX.toInt()
827
- val y = ev.rawY.toInt()
828
- if (x < loc[0] || x > loc[0] + focused.width || y < loc[1] || y > loc[1] + focused.height) {
829
- focused.clearFocus()
830
- (getSystemService(INPUT_METHOD_SERVICE) as? InputMethodManager)
831
- ?.hideSoftInputFromWindow(focused.windowToken, 0)
832
- }
833
- }
834
- }
835
-
836
1056
  @Deprecated("Deprecated in Java")
837
1057
  override fun onBackPressed() {
838
1058
  GeneratedActivityLifecycle.onBackPressed { consumed ->
@@ -903,7 +1123,7 @@ object DevServerPrefs {
903
1123
 
904
1124
  // src/android/create.ts
905
1125
  function readAndSubstituteTemplate(templatePath, vars) {
906
- const raw = fs3.readFileSync(templatePath, "utf-8");
1126
+ const raw = fs4.readFileSync(templatePath, "utf-8");
907
1127
  return Object.entries(vars).reduce(
908
1128
  (s, [k, v]) => s.replace(new RegExp(`\\{\\{${k}\\}\\}`, "g"), v),
909
1129
  raw
@@ -914,7 +1134,7 @@ var create = async (opts = {}) => {
914
1134
  const origCwd = process.cwd();
915
1135
  if (target === "dev-app") {
916
1136
  const devAppDir = findDevAppPackage(origCwd) ?? findDevAppPackage(findRepoRoot(origCwd));
917
- if (!devAppDir || !fs3.existsSync(path3.join(devAppDir, "tamer.config.json"))) {
1137
+ if (!devAppDir || !fs4.existsSync(path4.join(devAppDir, "tamer.config.json"))) {
918
1138
  console.error("\u274C tamer-dev-app not found. Add @tamer4lynx/tamer-dev-app to dependencies.");
919
1139
  process.exit(1);
920
1140
  }
@@ -944,30 +1164,30 @@ var create = async (opts = {}) => {
944
1164
  const packagePath = packageName.replace(/\./g, "/");
945
1165
  const gradleVersion = "8.14.2";
946
1166
  const androidDir = config.paths?.androidDir ?? "android";
947
- const rootDir = path3.join(process.cwd(), androidDir);
948
- const appDir = path3.join(rootDir, "app");
949
- const mainDir = path3.join(appDir, "src", "main");
950
- const javaDir = path3.join(mainDir, "java", packagePath);
951
- const kotlinDir = path3.join(mainDir, "kotlin", packagePath);
952
- const kotlinGeneratedDir = path3.join(kotlinDir, "generated");
953
- const assetsDir = path3.join(mainDir, "assets");
954
- const themesDir = path3.join(mainDir, "res", "values");
955
- const gradleDir = path3.join(rootDir, "gradle");
1167
+ const rootDir = path4.join(process.cwd(), androidDir);
1168
+ const appDir = path4.join(rootDir, "app");
1169
+ const mainDir = path4.join(appDir, "src", "main");
1170
+ const javaDir = path4.join(mainDir, "java", packagePath);
1171
+ const kotlinDir = path4.join(mainDir, "kotlin", packagePath);
1172
+ const kotlinGeneratedDir = path4.join(kotlinDir, "generated");
1173
+ const assetsDir = path4.join(mainDir, "assets");
1174
+ const themesDir = path4.join(mainDir, "res", "values");
1175
+ const gradleDir = path4.join(rootDir, "gradle");
956
1176
  function writeFile2(filePath, content, options) {
957
- fs3.mkdirSync(path3.dirname(filePath), { recursive: true });
958
- fs3.writeFileSync(
1177
+ fs4.mkdirSync(path4.dirname(filePath), { recursive: true });
1178
+ fs4.writeFileSync(
959
1179
  filePath,
960
1180
  content.trimStart(),
961
1181
  options?.encoding ?? "utf8"
962
1182
  );
963
1183
  }
964
- if (fs3.existsSync(rootDir)) {
1184
+ if (fs4.existsSync(rootDir)) {
965
1185
  console.log(`\u{1F9F9} Removing existing directory: ${rootDir}`);
966
- fs3.rmSync(rootDir, { recursive: true, force: true });
1186
+ fs4.rmSync(rootDir, { recursive: true, force: true });
967
1187
  }
968
1188
  console.log(`\u{1F680} Creating a new Tamer4Lynx project in: ${rootDir}`);
969
1189
  writeFile2(
970
- path3.join(gradleDir, "libs.versions.toml"),
1190
+ path4.join(gradleDir, "libs.versions.toml"),
971
1191
  `
972
1192
  [versions]
973
1193
  agp = "8.9.1"
@@ -981,12 +1201,12 @@ junit = "4.13.2"
981
1201
  junitVersion = "1.1.5"
982
1202
  espressoCore = "3.5.1"
983
1203
  appcompat = "1.6.1"
984
- lynx = "3.3.1"
1204
+ lynx = "3.6.0"
985
1205
  material = "1.10.0"
986
1206
  activity = "1.8.0"
987
1207
  constraintlayout = "2.1.4"
988
1208
  okhttp = "4.9.0"
989
- primjs = "2.12.0"
1209
+ primjs = "3.6.1"
990
1210
  zxing = "4.3.0"
991
1211
 
992
1212
  [libraries]
@@ -1028,7 +1248,7 @@ kotlin-kapt = { id = "org.jetbrains.kotlin.kapt", version.ref = "kotlin" }
1028
1248
  `
1029
1249
  );
1030
1250
  writeFile2(
1031
- path3.join(rootDir, "settings.gradle.kts"),
1251
+ path4.join(rootDir, "settings.gradle.kts"),
1032
1252
  `
1033
1253
  pluginManagement {
1034
1254
  repositories {
@@ -1062,7 +1282,7 @@ println("If you have native modules please run tamer android link")
1062
1282
  `
1063
1283
  );
1064
1284
  writeFile2(
1065
- path3.join(rootDir, "build.gradle.kts"),
1285
+ path4.join(rootDir, "build.gradle.kts"),
1066
1286
  `
1067
1287
  // Top-level build file where you can add configuration options common to all sub-projects/modules.
1068
1288
  plugins {
@@ -1074,7 +1294,7 @@ plugins {
1074
1294
  `
1075
1295
  );
1076
1296
  writeFile2(
1077
- path3.join(rootDir, "gradle.properties"),
1297
+ path4.join(rootDir, "gradle.properties"),
1078
1298
  `
1079
1299
  org.gradle.jvmargs=-Xmx2048m
1080
1300
  android.useAndroidX=true
@@ -1083,7 +1303,7 @@ android.enableJetifier=true
1083
1303
  `
1084
1304
  );
1085
1305
  writeFile2(
1086
- path3.join(appDir, "build.gradle.kts"),
1306
+ path4.join(appDir, "build.gradle.kts"),
1087
1307
  `
1088
1308
  plugins {
1089
1309
  alias(libs.plugins.android.application)
@@ -1175,7 +1395,7 @@ dependencies {
1175
1395
  `
1176
1396
  );
1177
1397
  writeFile2(
1178
- path3.join(themesDir, "themes.xml"),
1398
+ path4.join(themesDir, "themes.xml"),
1179
1399
  `
1180
1400
  <resources>
1181
1401
  <style name="Theme.MyApp" parent="Theme.AppCompat.Light.NoActionBar">
@@ -1209,7 +1429,7 @@ dependencies {
1209
1429
  const iconPaths = resolveIconPaths(process.cwd(), config);
1210
1430
  const manifestIconAttrs = iconPaths ? ' android:icon="@mipmap/ic_launcher"\n android:roundIcon="@mipmap/ic_launcher"\n' : "";
1211
1431
  writeFile2(
1212
- path3.join(mainDir, "AndroidManifest.xml"),
1432
+ path4.join(mainDir, "AndroidManifest.xml"),
1213
1433
  `
1214
1434
  <manifest xmlns:android="http://schemas.android.com/apk/res/android">
1215
1435
  ${manifestPermissions}
@@ -1223,7 +1443,7 @@ ${manifestIconAttrs} android:usesCleartextTraffic="true"
1223
1443
  `
1224
1444
  );
1225
1445
  writeFile2(
1226
- path3.join(kotlinGeneratedDir, "GeneratedLynxExtensions.kt"),
1446
+ path4.join(kotlinGeneratedDir, "GeneratedLynxExtensions.kt"),
1227
1447
  `
1228
1448
  package ${packageName}.generated
1229
1449
 
@@ -1251,14 +1471,14 @@ object GeneratedLynxExtensions {
1251
1471
  const hostPkg = findTamerHostPackage(process.cwd());
1252
1472
  const devClientPkg = findDevClientPackage(process.cwd());
1253
1473
  if (!hasDevLauncher && hostPkg) {
1254
- const templateDir = path3.join(hostPkg, "android", "templates");
1474
+ const templateDir = path4.join(hostPkg, "android", "templates");
1255
1475
  for (const [src, dst] of [
1256
- ["App.java", path3.join(javaDir, "App.java")],
1257
- ["TemplateProvider.java", path3.join(javaDir, "TemplateProvider.java")],
1258
- ["MainActivity.kt", path3.join(kotlinDir, "MainActivity.kt")]
1476
+ ["App.java", path4.join(javaDir, "App.java")],
1477
+ ["TemplateProvider.java", path4.join(javaDir, "TemplateProvider.java")],
1478
+ ["MainActivity.kt", path4.join(kotlinDir, "MainActivity.kt")]
1259
1479
  ]) {
1260
- const srcPath = path3.join(templateDir, src);
1261
- if (fs3.existsSync(srcPath)) {
1480
+ const srcPath = path4.join(templateDir, src);
1481
+ if (fs4.existsSync(srcPath)) {
1262
1482
  writeFile2(dst, readAndSubstituteTemplate(srcPath, templateVars));
1263
1483
  }
1264
1484
  }
@@ -1267,74 +1487,61 @@ object GeneratedLynxExtensions {
1267
1487
  fetchAndPatchApplication(vars),
1268
1488
  fetchAndPatchTemplateProvider(vars)
1269
1489
  ]);
1270
- writeFile2(path3.join(javaDir, "App.java"), applicationSource);
1271
- writeFile2(path3.join(javaDir, "TemplateProvider.java"), templateProviderSource);
1272
- writeFile2(path3.join(kotlinDir, "MainActivity.kt"), getStandaloneMainActivity(vars));
1490
+ writeFile2(path4.join(javaDir, "App.java"), applicationSource);
1491
+ writeFile2(path4.join(javaDir, "TemplateProvider.java"), templateProviderSource);
1492
+ writeFile2(path4.join(kotlinDir, "MainActivity.kt"), getStandaloneMainActivity(vars));
1273
1493
  if (hasDevLauncher) {
1274
1494
  if (devClientPkg) {
1275
- const templateDir = path3.join(devClientPkg, "android", "templates");
1495
+ const templateDir = path4.join(devClientPkg, "android", "templates");
1276
1496
  for (const [src, dst] of [
1277
- ["ProjectActivity.kt", path3.join(kotlinDir, "ProjectActivity.kt")],
1278
- ["DevClientManager.kt", path3.join(kotlinDir, "DevClientManager.kt")],
1279
- ["DevServerPrefs.kt", path3.join(kotlinDir, "DevServerPrefs.kt")]
1497
+ ["ProjectActivity.kt", path4.join(kotlinDir, "ProjectActivity.kt")],
1498
+ ["DevClientManager.kt", path4.join(kotlinDir, "DevClientManager.kt")],
1499
+ ["DevServerPrefs.kt", path4.join(kotlinDir, "DevServerPrefs.kt")]
1280
1500
  ]) {
1281
- const srcPath = path3.join(templateDir, src);
1282
- if (fs3.existsSync(srcPath)) {
1501
+ const srcPath = path4.join(templateDir, src);
1502
+ if (fs4.existsSync(srcPath)) {
1283
1503
  writeFile2(dst, readAndSubstituteTemplate(srcPath, templateVars));
1284
1504
  }
1285
1505
  }
1286
1506
  } else {
1287
- writeFile2(path3.join(kotlinDir, "ProjectActivity.kt"), getProjectActivity(vars));
1507
+ writeFile2(path4.join(kotlinDir, "ProjectActivity.kt"), getProjectActivity(vars));
1288
1508
  const devClientManagerSource = getDevClientManager(vars);
1289
1509
  if (devClientManagerSource) {
1290
- writeFile2(path3.join(kotlinDir, "DevClientManager.kt"), devClientManagerSource);
1291
- writeFile2(path3.join(kotlinDir, "DevServerPrefs.kt"), getDevServerPrefs(vars));
1510
+ writeFile2(path4.join(kotlinDir, "DevClientManager.kt"), devClientManagerSource);
1511
+ writeFile2(path4.join(kotlinDir, "DevServerPrefs.kt"), getDevServerPrefs(vars));
1292
1512
  }
1293
1513
  }
1294
1514
  }
1295
1515
  }
1296
1516
  if (iconPaths) {
1297
- const resDir = path3.join(mainDir, "res");
1298
- if (iconPaths.android) {
1299
- const src = iconPaths.android;
1300
- const entries = fs3.readdirSync(src, { withFileTypes: true });
1301
- for (const e of entries) {
1302
- const dest = path3.join(resDir, e.name);
1303
- if (e.isDirectory()) {
1304
- fs3.cpSync(path3.join(src, e.name), dest, { recursive: true });
1305
- } else {
1306
- fs3.mkdirSync(resDir, { recursive: true });
1307
- fs3.copyFileSync(path3.join(src, e.name), dest);
1308
- }
1309
- }
1310
- console.log("\u2705 Copied Android icon from tamer.config.json icon.android");
1311
- } else if (iconPaths.source) {
1312
- const mipmapDensities = ["mdpi", "hdpi", "xhdpi", "xxhdpi", "xxxhdpi"];
1313
- for (const d of mipmapDensities) {
1314
- const dir = path3.join(resDir, `mipmap-${d}`);
1315
- fs3.mkdirSync(dir, { recursive: true });
1316
- fs3.copyFileSync(iconPaths.source, path3.join(dir, "ic_launcher.png"));
1517
+ const resDir = path4.join(mainDir, "res");
1518
+ if (applyAndroidLauncherIcons(resDir, iconPaths)) {
1519
+ if (iconPaths.androidAdaptiveForeground && (iconPaths.androidAdaptiveBackground || iconPaths.androidAdaptiveBackgroundColor)) {
1520
+ console.log("\u2705 Android adaptive launcher from tamer.config.json icon.androidAdaptive");
1521
+ } else if (iconPaths.android) {
1522
+ console.log("\u2705 Copied Android icon from tamer.config.json icon.android");
1523
+ } else if (iconPaths.source) {
1524
+ console.log("\u2705 Copied app icon from tamer.config.json icon.source");
1317
1525
  }
1318
- console.log("\u2705 Copied app icon from tamer.config.json icon.source");
1319
1526
  }
1320
1527
  }
1321
- fs3.mkdirSync(assetsDir, { recursive: true });
1322
- fs3.writeFileSync(path3.join(assetsDir, ".gitkeep"), "");
1528
+ fs4.mkdirSync(assetsDir, { recursive: true });
1529
+ fs4.writeFileSync(path4.join(assetsDir, ".gitkeep"), "");
1323
1530
  console.log(`\u2705 Android Kotlin project created at ${rootDir}`);
1324
1531
  async function finalizeProjectSetup() {
1325
1532
  if (androidSdk) {
1326
1533
  try {
1327
1534
  const sdkDirContent = `sdk.dir=${androidSdk.replace(/\\/g, "/")}`;
1328
- writeFile2(path3.join(rootDir, "local.properties"), sdkDirContent);
1535
+ writeFile2(path4.join(rootDir, "local.properties"), sdkDirContent);
1329
1536
  console.log("\u{1F4E6} Created local.properties from tamer.config.json.");
1330
1537
  } catch (err) {
1331
1538
  console.error(`\u274C Failed to create local.properties: ${err.message}`);
1332
1539
  }
1333
1540
  } else {
1334
- const localPropsPath = path3.join(process.cwd(), "local.properties");
1335
- if (fs3.existsSync(localPropsPath)) {
1541
+ const localPropsPath = path4.join(process.cwd(), "local.properties");
1542
+ if (fs4.existsSync(localPropsPath)) {
1336
1543
  try {
1337
- fs3.copyFileSync(localPropsPath, path3.join(rootDir, "local.properties"));
1544
+ fs4.copyFileSync(localPropsPath, path4.join(rootDir, "local.properties"));
1338
1545
  console.log("\u{1F4E6} Copied existing local.properties to the android project.");
1339
1546
  } catch (err) {
1340
1547
  console.error("\u274C Failed to copy local.properties:", err);
@@ -1353,32 +1560,33 @@ object GeneratedLynxExtensions {
1353
1560
  var create_default = create;
1354
1561
 
1355
1562
  // src/android/autolink.ts
1563
+ import fs7 from "fs";
1564
+ import path7 from "path";
1565
+ import { execSync as execSync2 } from "child_process";
1566
+
1567
+ // src/common/discoverModules.ts
1356
1568
  import fs6 from "fs";
1357
1569
  import path6 from "path";
1358
1570
 
1359
- // src/common/discoverModules.ts
1571
+ // src/common/config.ts
1360
1572
  import fs5 from "fs";
1361
1573
  import path5 from "path";
1362
-
1363
- // src/common/config.ts
1364
- import fs4 from "fs";
1365
- import path4 from "path";
1366
1574
  var LYNX_EXT_JSON = "lynx.ext.json";
1367
1575
  var TAMER_JSON = "tamer.json";
1368
1576
  function loadLynxExtJson(packagePath) {
1369
- const p = path4.join(packagePath, LYNX_EXT_JSON);
1370
- if (!fs4.existsSync(p)) return null;
1577
+ const p = path5.join(packagePath, LYNX_EXT_JSON);
1578
+ if (!fs5.existsSync(p)) return null;
1371
1579
  try {
1372
- return JSON.parse(fs4.readFileSync(p, "utf8"));
1580
+ return JSON.parse(fs5.readFileSync(p, "utf8"));
1373
1581
  } catch {
1374
1582
  return null;
1375
1583
  }
1376
1584
  }
1377
1585
  function loadTamerJson(packagePath) {
1378
- const p = path4.join(packagePath, TAMER_JSON);
1379
- if (!fs4.existsSync(p)) return null;
1586
+ const p = path5.join(packagePath, TAMER_JSON);
1587
+ if (!fs5.existsSync(p)) return null;
1380
1588
  try {
1381
- return JSON.parse(fs4.readFileSync(p, "utf8"));
1589
+ return JSON.parse(fs5.readFileSync(p, "utf8"));
1382
1590
  } catch {
1383
1591
  return null;
1384
1592
  }
@@ -1431,7 +1639,7 @@ function loadExtensionConfig(packagePath) {
1431
1639
  return normalized;
1432
1640
  }
1433
1641
  function hasExtensionConfig(packagePath) {
1434
- return fs4.existsSync(path4.join(packagePath, LYNX_EXT_JSON)) || fs4.existsSync(path4.join(packagePath, TAMER_JSON));
1642
+ return fs5.existsSync(path5.join(packagePath, LYNX_EXT_JSON)) || fs5.existsSync(path5.join(packagePath, TAMER_JSON));
1435
1643
  }
1436
1644
  function getAndroidModuleClassNames(config) {
1437
1645
  if (!config) return [];
@@ -1449,15 +1657,15 @@ function getIosElements(config) {
1449
1657
  return config?.elements ?? {};
1450
1658
  }
1451
1659
  function getNodeModulesPath(projectRoot) {
1452
- let nodeModulesPath = path4.join(projectRoot, "node_modules");
1453
- const workspaceRoot = path4.join(projectRoot, "..", "..");
1454
- const rootNodeModules = path4.join(workspaceRoot, "node_modules");
1455
- if (fs4.existsSync(path4.join(workspaceRoot, "package.json")) && fs4.existsSync(rootNodeModules) && path4.basename(path4.dirname(projectRoot)) === "packages") {
1660
+ let nodeModulesPath = path5.join(projectRoot, "node_modules");
1661
+ const workspaceRoot = path5.join(projectRoot, "..", "..");
1662
+ const rootNodeModules = path5.join(workspaceRoot, "node_modules");
1663
+ if (fs5.existsSync(path5.join(workspaceRoot, "package.json")) && fs5.existsSync(rootNodeModules) && path5.basename(path5.dirname(projectRoot)) === "packages") {
1456
1664
  nodeModulesPath = rootNodeModules;
1457
- } else if (!fs4.existsSync(nodeModulesPath)) {
1458
- const altRoot = path4.join(projectRoot, "..", "..");
1459
- const altNodeModules = path4.join(altRoot, "node_modules");
1460
- if (fs4.existsSync(path4.join(altRoot, "package.json")) && fs4.existsSync(altNodeModules)) {
1665
+ } else if (!fs5.existsSync(nodeModulesPath)) {
1666
+ const altRoot = path5.join(projectRoot, "..", "..");
1667
+ const altNodeModules = path5.join(altRoot, "node_modules");
1668
+ if (fs5.existsSync(path5.join(altRoot, "package.json")) && fs5.existsSync(altNodeModules)) {
1461
1669
  nodeModulesPath = altNodeModules;
1462
1670
  }
1463
1671
  }
@@ -1466,8 +1674,8 @@ function getNodeModulesPath(projectRoot) {
1466
1674
  function discoverNativeExtensions(projectRoot) {
1467
1675
  const nodeModulesPath = getNodeModulesPath(projectRoot);
1468
1676
  const result = [];
1469
- if (!fs4.existsSync(nodeModulesPath)) return result;
1470
- const packageDirs = fs4.readdirSync(nodeModulesPath);
1677
+ if (!fs5.existsSync(nodeModulesPath)) return result;
1678
+ const packageDirs = fs5.readdirSync(nodeModulesPath);
1471
1679
  const check = (name, packagePath) => {
1472
1680
  if (!hasExtensionConfig(packagePath)) return;
1473
1681
  const config = loadExtensionConfig(packagePath);
@@ -1477,11 +1685,11 @@ function discoverNativeExtensions(projectRoot) {
1477
1685
  }
1478
1686
  };
1479
1687
  for (const dirName of packageDirs) {
1480
- const fullPath = path4.join(nodeModulesPath, dirName);
1688
+ const fullPath = path5.join(nodeModulesPath, dirName);
1481
1689
  if (dirName.startsWith("@")) {
1482
1690
  try {
1483
- for (const scopedDirName of fs4.readdirSync(fullPath)) {
1484
- check(`${dirName}/${scopedDirName}`, path4.join(fullPath, scopedDirName));
1691
+ for (const scopedDirName of fs5.readdirSync(fullPath)) {
1692
+ check(`${dirName}/${scopedDirName}`, path5.join(fullPath, scopedDirName));
1485
1693
  }
1486
1694
  } catch {
1487
1695
  }
@@ -1494,15 +1702,15 @@ function discoverNativeExtensions(projectRoot) {
1494
1702
 
1495
1703
  // src/common/discoverModules.ts
1496
1704
  function resolveNodeModulesPath(projectRoot) {
1497
- let nodeModulesPath = path5.join(projectRoot, "node_modules");
1498
- const workspaceRoot = path5.join(projectRoot, "..", "..");
1499
- const rootNodeModules = path5.join(workspaceRoot, "node_modules");
1500
- if (fs5.existsSync(path5.join(workspaceRoot, "package.json")) && fs5.existsSync(rootNodeModules) && path5.basename(path5.dirname(projectRoot)) === "packages") {
1705
+ let nodeModulesPath = path6.join(projectRoot, "node_modules");
1706
+ const workspaceRoot = path6.join(projectRoot, "..", "..");
1707
+ const rootNodeModules = path6.join(workspaceRoot, "node_modules");
1708
+ if (fs6.existsSync(path6.join(workspaceRoot, "package.json")) && fs6.existsSync(rootNodeModules) && path6.basename(path6.dirname(projectRoot)) === "packages") {
1501
1709
  nodeModulesPath = rootNodeModules;
1502
- } else if (!fs5.existsSync(nodeModulesPath)) {
1503
- const altRoot = path5.join(projectRoot, "..", "..");
1504
- const altNodeModules = path5.join(altRoot, "node_modules");
1505
- if (fs5.existsSync(path5.join(altRoot, "package.json")) && fs5.existsSync(altNodeModules)) {
1710
+ } else if (!fs6.existsSync(nodeModulesPath)) {
1711
+ const altRoot = path6.join(projectRoot, "..", "..");
1712
+ const altNodeModules = path6.join(altRoot, "node_modules");
1713
+ if (fs6.existsSync(path6.join(altRoot, "package.json")) && fs6.existsSync(altNodeModules)) {
1506
1714
  nodeModulesPath = altNodeModules;
1507
1715
  }
1508
1716
  }
@@ -1511,12 +1719,12 @@ function resolveNodeModulesPath(projectRoot) {
1511
1719
  function discoverModules(projectRoot) {
1512
1720
  const nodeModulesPath = resolveNodeModulesPath(projectRoot);
1513
1721
  const packages = [];
1514
- if (!fs5.existsSync(nodeModulesPath)) {
1722
+ if (!fs6.existsSync(nodeModulesPath)) {
1515
1723
  return [];
1516
1724
  }
1517
- const packageDirs = fs5.readdirSync(nodeModulesPath);
1725
+ const packageDirs = fs6.readdirSync(nodeModulesPath);
1518
1726
  for (const dirName of packageDirs) {
1519
- const fullPath = path5.join(nodeModulesPath, dirName);
1727
+ const fullPath = path6.join(nodeModulesPath, dirName);
1520
1728
  const checkPackage = (name, packagePath) => {
1521
1729
  if (!hasExtensionConfig(packagePath)) return;
1522
1730
  const config = loadExtensionConfig(packagePath);
@@ -1525,9 +1733,9 @@ function discoverModules(projectRoot) {
1525
1733
  };
1526
1734
  if (dirName.startsWith("@")) {
1527
1735
  try {
1528
- const scopedDirs = fs5.readdirSync(fullPath);
1736
+ const scopedDirs = fs6.readdirSync(fullPath);
1529
1737
  for (const scopedDirName of scopedDirs) {
1530
- const scopedPackagePath = path5.join(fullPath, scopedDirName);
1738
+ const scopedPackagePath = path6.join(fullPath, scopedDirName);
1531
1739
  checkPackage(`${dirName}/${scopedDirName}`, scopedPackagePath);
1532
1740
  }
1533
1741
  } catch (e) {
@@ -1542,16 +1750,25 @@ function discoverModules(projectRoot) {
1542
1750
  }
1543
1751
 
1544
1752
  // src/common/generateExtCode.ts
1545
- function generateLynxExtensionsKotlin(packages, projectPackage) {
1546
- const modulePackages = packages.filter((p) => getAndroidModuleClassNames(p.config.android).length > 0);
1547
- const elementPackages = packages.filter((p) => p.config.android?.elements && Object.keys(p.config.android.elements).length > 0);
1753
+ function getDedupedAndroidModuleClassNames(packages) {
1548
1754
  const seenNames = /* @__PURE__ */ new Set();
1549
- const allModuleClasses = modulePackages.flatMap((p) => getAndroidModuleClassNames(p.config.android)).filter((fullClassName) => {
1755
+ return packages.flatMap((p) => getAndroidModuleClassNames(p.config.android)).filter((fullClassName) => {
1550
1756
  const simple = fullClassName.split(".").pop();
1551
1757
  if (seenNames.has(simple)) return false;
1552
1758
  seenNames.add(simple);
1553
1759
  return true;
1554
1760
  });
1761
+ }
1762
+ function generateLynxExtensionsKotlin(packages, projectPackage) {
1763
+ const modulePackages = packages.filter((p) => getAndroidModuleClassNames(p.config.android).length > 0);
1764
+ const elementPackages = packages.filter((p) => p.config.android?.elements && Object.keys(p.config.android.elements).length > 0);
1765
+ const allModuleClasses = getDedupedAndroidModuleClassNames(packages);
1766
+ const hasDevClient = packages.some((p) => p.name === "@tamer4lynx/tamer-dev-client");
1767
+ const devClientSupportedBlock = hasDevClient && allModuleClasses.length > 0 ? `
1768
+ com.nanofuxion.tamerdevclient.DevClientModule.attachSupportedModuleClassNames(listOf(
1769
+ ${allModuleClasses.map((c) => ` "${c.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`).join(",\n")}
1770
+ ))
1771
+ ` : hasDevClient ? "\n com.nanofuxion.tamerdevclient.DevClientModule.attachSupportedModuleClassNames(emptyList())\n" : "";
1555
1772
  const moduleImports = allModuleClasses.map((c) => `import ${c}`).join("\n");
1556
1773
  const elementImports = elementPackages.flatMap((p) => Object.values(p.config.android.elements).map((cls) => `import ${cls}`)).filter((v, i, a) => a.indexOf(v) === i).join("\n");
1557
1774
  const moduleRegistrations = allModuleClasses.map((fullClassName) => {
@@ -1591,7 +1808,7 @@ ${elementImports}
1591
1808
 
1592
1809
  object GeneratedLynxExtensions {
1593
1810
  fun register(context: Context) {
1594
- ${allRegistrations}
1811
+ ${allRegistrations}${devClientSupportedBlock}
1595
1812
  }
1596
1813
 
1597
1814
  fun configureViewBuilder(viewBuilder: LynxViewBuilder) {
@@ -1724,11 +1941,11 @@ var autolink = (opts) => {
1724
1941
  const packageName = config.android.packageName;
1725
1942
  const projectRoot = resolved.projectRoot;
1726
1943
  function updateGeneratedSection(filePath, newContent, startMarker, endMarker) {
1727
- if (!fs6.existsSync(filePath)) {
1944
+ if (!fs7.existsSync(filePath)) {
1728
1945
  console.warn(`\u26A0\uFE0F File not found, skipping update: ${filePath}`);
1729
1946
  return;
1730
1947
  }
1731
- let fileContent = fs6.readFileSync(filePath, "utf8");
1948
+ let fileContent = fs7.readFileSync(filePath, "utf8");
1732
1949
  const escapedStartMarker = startMarker.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1733
1950
  const escapedEndMarker = endMarker.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1734
1951
  const regex = new RegExp(`${escapedStartMarker}[\\s\\S]*?${escapedEndMarker}`, "g");
@@ -1738,16 +1955,16 @@ ${endMarker}`;
1738
1955
  if (regex.test(fileContent)) {
1739
1956
  fileContent = fileContent.replace(regex, replacementBlock);
1740
1957
  } else {
1741
- console.warn(`\u26A0\uFE0F Could not find autolink markers in ${path6.basename(filePath)}. Appending to the end of the file.`);
1958
+ console.warn(`\u26A0\uFE0F Could not find autolink markers in ${path7.basename(filePath)}. Appending to the end of the file.`);
1742
1959
  fileContent += `
1743
1960
  ${replacementBlock}
1744
1961
  `;
1745
1962
  }
1746
- fs6.writeFileSync(filePath, fileContent);
1747
- console.log(`\u2705 Updated autolinked section in ${path6.basename(filePath)}`);
1963
+ fs7.writeFileSync(filePath, fileContent);
1964
+ console.log(`\u2705 Updated autolinked section in ${path7.basename(filePath)}`);
1748
1965
  }
1749
1966
  function updateSettingsGradle(packages) {
1750
- const settingsFilePath = path6.join(appAndroidPath, "settings.gradle.kts");
1967
+ const settingsFilePath = path7.join(appAndroidPath, "settings.gradle.kts");
1751
1968
  let scriptContent = `// This section is automatically generated by Tamer4Lynx.
1752
1969
  // Manual edits will be overwritten.`;
1753
1970
  const androidPackages = packages.filter((p) => p.config.android);
@@ -1755,7 +1972,7 @@ ${replacementBlock}
1755
1972
  androidPackages.forEach((pkg) => {
1756
1973
  const gradleProjectName = pkg.name.replace(/^@/, "").replace(/\//g, "_");
1757
1974
  const sourceDir = pkg.config.android?.sourceDir || "android";
1758
- const projectPath = path6.resolve(pkg.packagePath, sourceDir).replace(/\\/g, "/");
1975
+ const projectPath = path7.resolve(pkg.packagePath, sourceDir).replace(/\\/g, "/");
1759
1976
  scriptContent += `
1760
1977
  include(":${gradleProjectName}")`;
1761
1978
  scriptContent += `
@@ -1768,7 +1985,7 @@ println("No native modules found by Tamer4Lynx autolinker.")`;
1768
1985
  updateGeneratedSection(settingsFilePath, scriptContent.trim(), "// GENERATED AUTOLINK START", "// GENERATED AUTOLINK END");
1769
1986
  }
1770
1987
  function updateAppBuildGradle(packages) {
1771
- const appBuildGradlePath = path6.join(appAndroidPath, "app", "build.gradle.kts");
1988
+ const appBuildGradlePath = path7.join(appAndroidPath, "app", "build.gradle.kts");
1772
1989
  const androidPackages = packages.filter((p) => p.config.android);
1773
1990
  const implementationLines = androidPackages.map((p) => {
1774
1991
  const gradleProjectName = p.name.replace(/^@/, "").replace(/\//g, "_");
@@ -1786,35 +2003,35 @@ ${implementationLines || " // No native dependencies found to link."}`;
1786
2003
  }
1787
2004
  function generateKotlinExtensionsFile(packages, projectPackage) {
1788
2005
  const packagePath = projectPackage.replace(/\./g, "/");
1789
- const generatedDir = path6.join(appAndroidPath, "app", "src", "main", "kotlin", packagePath, "generated");
1790
- const kotlinExtensionsPath = path6.join(generatedDir, "GeneratedLynxExtensions.kt");
2006
+ const generatedDir = path7.join(appAndroidPath, "app", "src", "main", "kotlin", packagePath, "generated");
2007
+ const kotlinExtensionsPath = path7.join(generatedDir, "GeneratedLynxExtensions.kt");
1791
2008
  const content = `/**
1792
2009
  * This file is generated by the Tamer4Lynx autolinker.
1793
2010
  * Do not edit this file manually.
1794
2011
  */
1795
2012
  ${generateLynxExtensionsKotlin(packages, projectPackage)}`;
1796
- fs6.mkdirSync(generatedDir, { recursive: true });
1797
- fs6.writeFileSync(kotlinExtensionsPath, content.trimStart());
2013
+ fs7.mkdirSync(generatedDir, { recursive: true });
2014
+ fs7.writeFileSync(kotlinExtensionsPath, content.trimStart());
1798
2015
  console.log(`\u2705 Generated Kotlin extensions at ${kotlinExtensionsPath}`);
1799
2016
  }
1800
2017
  function generateActivityLifecycleFile(packages, projectPackage) {
1801
2018
  const packageKotlinPath = projectPackage.replace(/\./g, "/");
1802
- const generatedDir = path6.join(appAndroidPath, "app", "src", "main", "kotlin", packageKotlinPath, "generated");
1803
- const outputPath = path6.join(generatedDir, "GeneratedActivityLifecycle.kt");
2019
+ const generatedDir = path7.join(appAndroidPath, "app", "src", "main", "kotlin", packageKotlinPath, "generated");
2020
+ const outputPath = path7.join(generatedDir, "GeneratedActivityLifecycle.kt");
1804
2021
  const content = `/**
1805
2022
  * This file is generated by the Tamer4Lynx autolinker.
1806
2023
  * Do not edit this file manually.
1807
2024
  */
1808
2025
  ${generateActivityLifecycleKotlin(packages, projectPackage)}`;
1809
- fs6.mkdirSync(generatedDir, { recursive: true });
1810
- fs6.writeFileSync(outputPath, content);
2026
+ fs7.mkdirSync(generatedDir, { recursive: true });
2027
+ fs7.writeFileSync(outputPath, content);
1811
2028
  console.log(`\u2705 Generated activity lifecycle patches at ${outputPath}`);
1812
2029
  }
1813
2030
  function syncDeepLinkIntentFilters() {
1814
2031
  const deepLinks = config.android?.deepLinks;
1815
2032
  if (!deepLinks || deepLinks.length === 0) return;
1816
- const manifestPath = path6.join(appAndroidPath, "app", "src", "main", "AndroidManifest.xml");
1817
- if (!fs6.existsSync(manifestPath)) return;
2033
+ const manifestPath = path7.join(appAndroidPath, "app", "src", "main", "AndroidManifest.xml");
2034
+ if (!fs7.existsSync(manifestPath)) return;
1818
2035
  const intentFilters = deepLinks.map((link) => {
1819
2036
  const dataAttrs = [
1820
2037
  `android:scheme="${link.scheme}"`,
@@ -1839,9 +2056,9 @@ ${generateActivityLifecycleKotlin(packages, projectPackage)}`;
1839
2056
  console.log("\u{1F50E} Finding Lynx extension packages (lynx.ext.json / tamer.json)...");
1840
2057
  let packages = discoverModules(projectRoot).filter((p) => p.config.android);
1841
2058
  const includeDevClient = opts?.includeDevClient === true;
1842
- const devClientScoped = path6.join(projectRoot, "node_modules", "@tamer4lynx", "tamer-dev-client");
1843
- const devClientFlat = path6.join(projectRoot, "node_modules", "tamer-dev-client");
1844
- const devClientPath = fs6.existsSync(path6.join(devClientScoped, "android")) ? devClientScoped : fs6.existsSync(path6.join(devClientFlat, "android")) ? devClientFlat : null;
2059
+ const devClientScoped = path7.join(projectRoot, "node_modules", "@tamer4lynx", "tamer-dev-client");
2060
+ const devClientFlat = path7.join(projectRoot, "node_modules", "tamer-dev-client");
2061
+ const devClientPath = fs7.existsSync(path7.join(devClientScoped, "android")) ? devClientScoped : fs7.existsSync(path7.join(devClientFlat, "android")) ? devClientFlat : null;
1845
2062
  const hasDevClient = packages.some((p) => p.name === "@tamer4lynx/tamer-dev-client" || p.name === "tamer-dev-client");
1846
2063
  if (includeDevClient && devClientPath && !hasDevClient) {
1847
2064
  packages = [{
@@ -1864,17 +2081,33 @@ ${generateActivityLifecycleKotlin(packages, projectPackage)}`;
1864
2081
  syncVersionCatalog(packages);
1865
2082
  ensureXElementDeps();
1866
2083
  ensureReleaseSigning();
2084
+ runGradleSync();
1867
2085
  console.log("\u2728 Autolinking complete.");
1868
2086
  }
2087
+ function runGradleSync() {
2088
+ const gradlew = path7.join(appAndroidPath, process.platform === "win32" ? "gradlew.bat" : "gradlew");
2089
+ if (!fs7.existsSync(gradlew)) return;
2090
+ try {
2091
+ console.log("\u2139\uFE0F Running Gradle sync in android directory...");
2092
+ execSync2(process.platform === "win32" ? "gradlew.bat projects" : "./gradlew projects", {
2093
+ cwd: appAndroidPath,
2094
+ stdio: "inherit"
2095
+ });
2096
+ console.log("\u2705 Gradle sync completed.");
2097
+ } catch (e) {
2098
+ console.warn("\u26A0\uFE0F Gradle sync failed:", e.message);
2099
+ console.log("\u26A0\uFE0F You can run `./gradlew tasks` in the android directory to sync.");
2100
+ }
2101
+ }
1869
2102
  function syncVersionCatalog(packages) {
1870
- const libsTomlPath = path6.join(appAndroidPath, "gradle", "libs.versions.toml");
1871
- if (!fs6.existsSync(libsTomlPath)) return;
2103
+ const libsTomlPath = path7.join(appAndroidPath, "gradle", "libs.versions.toml");
2104
+ if (!fs7.existsSync(libsTomlPath)) return;
1872
2105
  const requiredAliases = /* @__PURE__ */ new Set();
1873
2106
  const requiredPluginAliases = /* @__PURE__ */ new Set();
1874
2107
  for (const pkg of packages) {
1875
- const buildPath = path6.join(pkg.packagePath, pkg.config.android?.sourceDir || "android", "build.gradle.kts");
1876
- if (!fs6.existsSync(buildPath)) continue;
1877
- const content = fs6.readFileSync(buildPath, "utf8");
2108
+ const buildPath = path7.join(pkg.packagePath, pkg.config.android?.sourceDir || "android", "build.gradle.kts");
2109
+ if (!fs7.existsSync(buildPath)) continue;
2110
+ const content = fs7.readFileSync(buildPath, "utf8");
1878
2111
  for (const m of content.matchAll(/libs\.([\w.]+)/g)) {
1879
2112
  const alias = m[1];
1880
2113
  if (alias && alias in REQUIRED_CATALOG_ENTRIES) requiredAliases.add(alias);
@@ -1885,7 +2118,7 @@ ${generateActivityLifecycleKotlin(packages, projectPackage)}`;
1885
2118
  }
1886
2119
  }
1887
2120
  if (requiredAliases.size === 0 && requiredPluginAliases.size === 0) return;
1888
- let toml = fs6.readFileSync(libsTomlPath, "utf8");
2121
+ let toml = fs7.readFileSync(libsTomlPath, "utf8");
1889
2122
  let updated = false;
1890
2123
  for (const alias of requiredAliases) {
1891
2124
  const entry = REQUIRED_CATALOG_ENTRIES[alias];
@@ -1909,14 +2142,14 @@ ${generateActivityLifecycleKotlin(packages, projectPackage)}`;
1909
2142
  updated = true;
1910
2143
  }
1911
2144
  if (updated) {
1912
- fs6.writeFileSync(libsTomlPath, toml);
2145
+ fs7.writeFileSync(libsTomlPath, toml);
1913
2146
  console.log("\u2705 Synced version catalog (libs.versions.toml) for linked modules.");
1914
2147
  }
1915
2148
  }
1916
2149
  function ensureXElementDeps() {
1917
- const libsTomlPath = path6.join(appAndroidPath, "gradle", "libs.versions.toml");
1918
- if (fs6.existsSync(libsTomlPath)) {
1919
- let toml = fs6.readFileSync(libsTomlPath, "utf8");
2150
+ const libsTomlPath = path7.join(appAndroidPath, "gradle", "libs.versions.toml");
2151
+ if (fs7.existsSync(libsTomlPath)) {
2152
+ let toml = fs7.readFileSync(libsTomlPath, "utf8");
1920
2153
  let updated = false;
1921
2154
  if (!toml.includes("lynx-xelement =")) {
1922
2155
  toml = toml.replace(
@@ -1928,13 +2161,13 @@ lynx-xelement-input = { module = "org.lynxsdk.lynx:xelement-input", version.ref
1928
2161
  updated = true;
1929
2162
  }
1930
2163
  if (updated) {
1931
- fs6.writeFileSync(libsTomlPath, toml);
2164
+ fs7.writeFileSync(libsTomlPath, toml);
1932
2165
  console.log("\u2705 Added XElement entries to version catalog.");
1933
2166
  }
1934
2167
  }
1935
- const appBuildPath = path6.join(appAndroidPath, "app", "build.gradle.kts");
1936
- if (fs6.existsSync(appBuildPath)) {
1937
- let content = fs6.readFileSync(appBuildPath, "utf8");
2168
+ const appBuildPath = path7.join(appAndroidPath, "app", "build.gradle.kts");
2169
+ if (fs7.existsSync(appBuildPath)) {
2170
+ let content = fs7.readFileSync(appBuildPath, "utf8");
1938
2171
  if (!content.includes("lynx.xelement")) {
1939
2172
  content = content.replace(
1940
2173
  /(implementation\(libs\.lynx\.service\.http\))/,
@@ -1942,15 +2175,15 @@ lynx-xelement-input = { module = "org.lynxsdk.lynx:xelement-input", version.ref
1942
2175
  implementation(libs.lynx.xelement)
1943
2176
  implementation(libs.lynx.xelement.input)`
1944
2177
  );
1945
- fs6.writeFileSync(appBuildPath, content);
2178
+ fs7.writeFileSync(appBuildPath, content);
1946
2179
  console.log("\u2705 Added XElement dependencies to app build.gradle.kts.");
1947
2180
  }
1948
2181
  }
1949
2182
  }
1950
2183
  function ensureReleaseSigning() {
1951
- const appBuildPath = path6.join(appAndroidPath, "app", "build.gradle.kts");
1952
- if (!fs6.existsSync(appBuildPath)) return;
1953
- let content = fs6.readFileSync(appBuildPath, "utf8");
2184
+ const appBuildPath = path7.join(appAndroidPath, "app", "build.gradle.kts");
2185
+ if (!fs7.existsSync(appBuildPath)) return;
2186
+ let content = fs7.readFileSync(appBuildPath, "utf8");
1954
2187
  if (content.includes('signingConfig = signingConfigs.getByName("debug")')) return;
1955
2188
  const releaseBlock = /(release\s*\{)([\s\S]*?)(\n \}\s*\n(\s*\}|\s*compileOptions))/;
1956
2189
  const match = content.match(releaseBlock);
@@ -1961,13 +2194,13 @@ lynx-xelement-input = { module = "org.lynxsdk.lynx:xelement-input", version.ref
1961
2194
  signingConfig = signingConfigs.getByName("debug")
1962
2195
  $3`
1963
2196
  );
1964
- fs6.writeFileSync(appBuildPath, content);
2197
+ fs7.writeFileSync(appBuildPath, content);
1965
2198
  console.log("\u2705 Set release signing to debug so installRelease works without a keystore.");
1966
2199
  }
1967
2200
  }
1968
2201
  function syncManifestPermissions(packages) {
1969
- const manifestPath = path6.join(appAndroidPath, "app", "src", "main", "AndroidManifest.xml");
1970
- if (!fs6.existsSync(manifestPath)) return;
2202
+ const manifestPath = path7.join(appAndroidPath, "app", "src", "main", "AndroidManifest.xml");
2203
+ if (!fs7.existsSync(manifestPath)) return;
1971
2204
  const allPermissions = /* @__PURE__ */ new Set();
1972
2205
  for (const pkg of packages) {
1973
2206
  const perms = pkg.config.android?.permissions;
@@ -1979,7 +2212,7 @@ lynx-xelement-input = { module = "org.lynxsdk.lynx:xelement-input", version.ref
1979
2212
  }
1980
2213
  }
1981
2214
  if (allPermissions.size === 0) return;
1982
- let manifest = fs6.readFileSync(manifestPath, "utf8");
2215
+ let manifest = fs7.readFileSync(manifestPath, "utf8");
1983
2216
  const existingMatch = [...manifest.matchAll(/<uses-permission android:name="(android\.permission\.\w+)"\s*\/>/g)];
1984
2217
  const existing = new Set(existingMatch.map((m) => m[1]));
1985
2218
  const toAdd = [...allPermissions].filter((p) => !existing.has(p));
@@ -1990,7 +2223,7 @@ lynx-xelement-input = { module = "org.lynxsdk.lynx:xelement-input", version.ref
1990
2223
  `${newLines}
1991
2224
  $1$2`
1992
2225
  );
1993
- fs6.writeFileSync(manifestPath, manifest);
2226
+ fs7.writeFileSync(manifestPath, manifest);
1994
2227
  console.log(`\u2705 Synced manifest permissions: ${toAdd.map((p) => p.split(".").pop()).join(", ")}`);
1995
2228
  }
1996
2229
  run();
@@ -1998,26 +2231,26 @@ $1$2`
1998
2231
  var autolink_default = autolink;
1999
2232
 
2000
2233
  // src/android/bundle.ts
2001
- import fs9 from "fs";
2002
- import path9 from "path";
2003
- import { execSync as execSync2 } from "child_process";
2234
+ import fs10 from "fs";
2235
+ import path10 from "path";
2236
+ import { execSync as execSync3 } from "child_process";
2004
2237
 
2005
2238
  // src/common/copyDistAssets.ts
2006
- import fs7 from "fs";
2007
- import path7 from "path";
2239
+ import fs8 from "fs";
2240
+ import path8 from "path";
2008
2241
  var SKIP = /* @__PURE__ */ new Set([".rspeedy", "stats.json"]);
2009
2242
  function copyDistAssets(distDir, destDir, bundleFile) {
2010
- if (!fs7.existsSync(distDir)) return;
2011
- for (const entry of fs7.readdirSync(distDir)) {
2243
+ if (!fs8.existsSync(distDir)) return;
2244
+ for (const entry of fs8.readdirSync(distDir)) {
2012
2245
  if (SKIP.has(entry)) continue;
2013
- const src = path7.join(distDir, entry);
2014
- const dest = path7.join(destDir, entry);
2015
- const stat = fs7.statSync(src);
2246
+ const src = path8.join(distDir, entry);
2247
+ const dest = path8.join(destDir, entry);
2248
+ const stat = fs8.statSync(src);
2016
2249
  if (stat.isDirectory()) {
2017
- fs7.mkdirSync(dest, { recursive: true });
2250
+ fs8.mkdirSync(dest, { recursive: true });
2018
2251
  copyDistAssets(src, dest, bundleFile);
2019
2252
  } else {
2020
- fs7.copyFileSync(src, dest);
2253
+ fs8.copyFileSync(src, dest);
2021
2254
  if (entry !== bundleFile) {
2022
2255
  console.log(`\u2728 Copied asset: ${entry}`);
2023
2256
  }
@@ -2026,18 +2259,18 @@ function copyDistAssets(distDir, destDir, bundleFile) {
2026
2259
  }
2027
2260
 
2028
2261
  // src/android/syncDevClient.ts
2029
- import fs8 from "fs";
2030
- import path8 from "path";
2262
+ import fs9 from "fs";
2263
+ import path9 from "path";
2031
2264
  function readAndSubstituteTemplate2(templatePath, vars) {
2032
- const raw = fs8.readFileSync(templatePath, "utf-8");
2265
+ const raw = fs9.readFileSync(templatePath, "utf-8");
2033
2266
  return Object.entries(vars).reduce(
2034
2267
  (s, [k, v]) => s.replace(new RegExp(`\\{\\{${k}\\}\\}`, "g"), v),
2035
2268
  raw
2036
2269
  );
2037
2270
  }
2038
2271
  function patchAppLogService(appPath) {
2039
- if (!fs8.existsSync(appPath)) return;
2040
- const raw = fs8.readFileSync(appPath, "utf-8");
2272
+ if (!fs9.existsSync(appPath)) return;
2273
+ const raw = fs9.readFileSync(appPath, "utf-8");
2041
2274
  const patched = raw.replace(
2042
2275
  /private void initLynxService\(\)\s*\{[\s\S]*?\n\s*}\s*\n\s*private void initFresco\(\)/,
2043
2276
  `private void initLynxService() {
@@ -2056,7 +2289,7 @@ function patchAppLogService(appPath) {
2056
2289
  private void initFresco()`
2057
2290
  );
2058
2291
  if (patched !== raw) {
2059
- fs8.writeFileSync(appPath, patched);
2292
+ fs9.writeFileSync(appPath, patched);
2060
2293
  }
2061
2294
  }
2062
2295
  async function syncDevClient(opts) {
@@ -2071,9 +2304,9 @@ async function syncDevClient(opts) {
2071
2304
  const packageName = config.android?.packageName;
2072
2305
  const appName = config.android?.appName;
2073
2306
  const packagePath = packageName.replace(/\./g, "/");
2074
- const javaDir = path8.join(rootDir, "app", "src", "main", "java", packagePath);
2075
- const kotlinDir = path8.join(rootDir, "app", "src", "main", "kotlin", packagePath);
2076
- if (!fs8.existsSync(javaDir) || !fs8.existsSync(kotlinDir)) {
2307
+ const javaDir = path9.join(rootDir, "app", "src", "main", "java", packagePath);
2308
+ const kotlinDir = path9.join(rootDir, "app", "src", "main", "kotlin", packagePath);
2309
+ if (!fs9.existsSync(javaDir) || !fs9.existsSync(kotlinDir)) {
2077
2310
  console.error("\u274C Android project not found. Run `tamer android create` first.");
2078
2311
  process.exit(1);
2079
2312
  }
@@ -2089,14 +2322,14 @@ async function syncDevClient(opts) {
2089
2322
  const [templateProviderSource] = await Promise.all([
2090
2323
  fetchAndPatchTemplateProvider(vars)
2091
2324
  ]);
2092
- fs8.writeFileSync(path8.join(javaDir, "TemplateProvider.java"), templateProviderSource);
2093
- fs8.writeFileSync(path8.join(kotlinDir, "MainActivity.kt"), getStandaloneMainActivity(vars));
2094
- patchAppLogService(path8.join(javaDir, "App.java"));
2095
- const appDir = path8.join(rootDir, "app");
2096
- const mainDir = path8.join(appDir, "src", "main");
2097
- const manifestPath = path8.join(mainDir, "AndroidManifest.xml");
2325
+ fs9.writeFileSync(path9.join(javaDir, "TemplateProvider.java"), templateProviderSource);
2326
+ fs9.writeFileSync(path9.join(kotlinDir, "MainActivity.kt"), getStandaloneMainActivity(vars));
2327
+ patchAppLogService(path9.join(javaDir, "App.java"));
2328
+ const appDir = path9.join(rootDir, "app");
2329
+ const mainDir = path9.join(appDir, "src", "main");
2330
+ const manifestPath = path9.join(mainDir, "AndroidManifest.xml");
2098
2331
  if (hasDevClient) {
2099
- const templateDir = path8.join(devClientPkg, "android", "templates");
2332
+ const templateDir = path9.join(devClientPkg, "android", "templates");
2100
2333
  const templateVars = { PACKAGE_NAME: packageName, APP_NAME: appName };
2101
2334
  const devClientFiles = [
2102
2335
  "DevClientManager.kt",
@@ -2105,13 +2338,13 @@ async function syncDevClient(opts) {
2105
2338
  "PortraitCaptureActivity.kt"
2106
2339
  ];
2107
2340
  for (const f of devClientFiles) {
2108
- const src = path8.join(templateDir, f);
2109
- if (fs8.existsSync(src)) {
2341
+ const src = path9.join(templateDir, f);
2342
+ if (fs9.existsSync(src)) {
2110
2343
  const content = readAndSubstituteTemplate2(src, templateVars);
2111
- fs8.writeFileSync(path8.join(kotlinDir, f), content);
2344
+ fs9.writeFileSync(path9.join(kotlinDir, f), content);
2112
2345
  }
2113
2346
  }
2114
- let manifest = fs8.readFileSync(manifestPath, "utf-8");
2347
+ let manifest = fs9.readFileSync(manifestPath, "utf-8");
2115
2348
  const projectActivityEntry = ' <activity android:name=".ProjectActivity" android:exported="false" android:taskAffinity="" android:launchMode="singleTask" android:documentLaunchMode="always" android:windowSoftInputMode="adjustResize" />';
2116
2349
  const portraitCaptureEntry = ' <activity android:name=".PortraitCaptureActivity" android:screenOrientation="portrait" android:stateNotNeeded="true" android:theme="@style/zxing_CaptureTheme" android:windowSoftInputMode="stateAlwaysHidden" />';
2117
2350
  if (!manifest.includes("ProjectActivity")) {
@@ -2133,16 +2366,16 @@ $1$2`);
2133
2366
  '$1 android:windowSoftInputMode="adjustResize"$2'
2134
2367
  );
2135
2368
  }
2136
- fs8.writeFileSync(manifestPath, manifest);
2369
+ fs9.writeFileSync(manifestPath, manifest);
2137
2370
  console.log("\u2705 Synced dev client (TemplateProvider, MainActivity, ProjectActivity, DevClientManager)");
2138
2371
  } else {
2139
2372
  for (const f of ["DevClientManager.kt", "DevServerPrefs.kt", "ProjectActivity.kt", "PortraitCaptureActivity.kt", "DevLauncherActivity.kt"]) {
2140
2373
  try {
2141
- fs8.rmSync(path8.join(kotlinDir, f));
2374
+ fs9.rmSync(path9.join(kotlinDir, f));
2142
2375
  } catch {
2143
2376
  }
2144
2377
  }
2145
- let manifest = fs8.readFileSync(manifestPath, "utf-8");
2378
+ let manifest = fs9.readFileSync(manifestPath, "utf-8");
2146
2379
  manifest = manifest.replace(/\s*<activity android:name="\.ProjectActivity"[^\/]*\/>\n?/g, "");
2147
2380
  manifest = manifest.replace(/\s*<activity android:name="\.PortraitCaptureActivity"[^\/]*\/>\n?/g, "");
2148
2381
  const mainActivityTag = manifest.match(/<activity[^>]*android:name="\.MainActivity"[^>]*>/);
@@ -2152,7 +2385,7 @@ $1$2`);
2152
2385
  '$1 android:windowSoftInputMode="adjustResize"$2'
2153
2386
  );
2154
2387
  }
2155
- fs8.writeFileSync(manifestPath, manifest);
2388
+ fs9.writeFileSync(manifestPath, manifest);
2156
2389
  console.log("\u2705 Synced (dev client disabled - use -d for debug build with dev client)");
2157
2390
  }
2158
2391
  }
@@ -2174,24 +2407,27 @@ async function bundleAndDeploy(opts = {}) {
2174
2407
  const destinationDir = androidAssetsDir;
2175
2408
  autolink_default({ includeDevClient });
2176
2409
  await syncDevClient_default({ includeDevClient });
2177
- const bundleExists = fs9.existsSync(lynxBundlePath);
2178
- if (!bundleExists) {
2179
- try {
2180
- console.log("\u{1F4E6} Building Lynx bundle...");
2181
- execSync2("npm run build", { stdio: "inherit", cwd: lynxProjectDir });
2182
- console.log("\u2705 Build completed successfully.");
2183
- } catch (error) {
2184
- console.error("\u274C Build process failed.");
2185
- process.exit(1);
2410
+ const iconPaths = resolveIconPaths(projectRoot, resolved.config);
2411
+ if (iconPaths) {
2412
+ const resDir = path10.join(resolved.androidAppDir, "src", "main", "res");
2413
+ if (applyAndroidLauncherIcons(resDir, iconPaths)) {
2414
+ console.log("\u2705 Synced Android launcher icon(s) from tamer.config.json");
2415
+ ensureAndroidManifestLauncherIcon(path10.join(resolved.androidAppDir, "src", "main", "AndroidManifest.xml"));
2186
2416
  }
2187
- } else {
2188
- console.log("\u{1F4E6} Using pre-built Lynx bundle.");
2189
2417
  }
2190
- if (includeDevClient && devClientBundlePath && !fs9.existsSync(devClientBundlePath)) {
2191
- const devClientDir = path9.dirname(path9.dirname(devClientBundlePath));
2418
+ try {
2419
+ console.log("\u{1F4E6} Building Lynx bundle...");
2420
+ execSync3("npm run build", { stdio: "inherit", cwd: lynxProjectDir });
2421
+ console.log("\u2705 Build completed successfully.");
2422
+ } catch (error) {
2423
+ console.error("\u274C Build process failed.");
2424
+ process.exit(1);
2425
+ }
2426
+ if (includeDevClient && devClientBundlePath && !fs10.existsSync(devClientBundlePath)) {
2427
+ const devClientDir = path10.dirname(path10.dirname(devClientBundlePath));
2192
2428
  try {
2193
2429
  console.log("\u{1F4E6} Building dev launcher (tamer-dev-client)...");
2194
- execSync2("npm run build", { stdio: "inherit", cwd: devClientDir });
2430
+ execSync3("npm run build", { stdio: "inherit", cwd: devClientDir });
2195
2431
  console.log("\u2705 Dev launcher build completed.");
2196
2432
  } catch (error) {
2197
2433
  console.error("\u274C Dev launcher build failed.");
@@ -2199,22 +2435,22 @@ async function bundleAndDeploy(opts = {}) {
2199
2435
  }
2200
2436
  }
2201
2437
  try {
2202
- fs9.mkdirSync(destinationDir, { recursive: true });
2438
+ fs10.mkdirSync(destinationDir, { recursive: true });
2203
2439
  if (release) {
2204
- const devClientAsset = path9.join(destinationDir, "dev-client.lynx.bundle");
2205
- if (fs9.existsSync(devClientAsset)) {
2206
- fs9.rmSync(devClientAsset);
2440
+ const devClientAsset = path10.join(destinationDir, "dev-client.lynx.bundle");
2441
+ if (fs10.existsSync(devClientAsset)) {
2442
+ fs10.rmSync(devClientAsset);
2207
2443
  console.log(`\u2728 Removed dev-client.lynx.bundle from assets (production build)`);
2208
2444
  }
2209
- } else if (includeDevClient && devClientBundlePath && fs9.existsSync(devClientBundlePath)) {
2210
- fs9.copyFileSync(devClientBundlePath, path9.join(destinationDir, "dev-client.lynx.bundle"));
2445
+ } else if (includeDevClient && devClientBundlePath && fs10.existsSync(devClientBundlePath)) {
2446
+ fs10.copyFileSync(devClientBundlePath, path10.join(destinationDir, "dev-client.lynx.bundle"));
2211
2447
  console.log(`\u2728 Copied dev-client.lynx.bundle to assets`);
2212
2448
  }
2213
- if (!fs9.existsSync(lynxBundlePath)) {
2449
+ if (!fs10.existsSync(lynxBundlePath)) {
2214
2450
  console.error(`\u274C Build output not found at: ${lynxBundlePath}`);
2215
2451
  process.exit(1);
2216
2452
  }
2217
- const distDir = path9.dirname(lynxBundlePath);
2453
+ const distDir = path10.dirname(lynxBundlePath);
2218
2454
  copyDistAssets(distDir, destinationDir, resolved.lynxBundleFile);
2219
2455
  console.log(`\u2728 Copied ${resolved.lynxBundleFile} to assets`);
2220
2456
  } catch (error) {
@@ -2225,8 +2461,8 @@ async function bundleAndDeploy(opts = {}) {
2225
2461
  var bundle_default = bundleAndDeploy;
2226
2462
 
2227
2463
  // src/android/build.ts
2228
- import path10 from "path";
2229
- import { execSync as execSync3 } from "child_process";
2464
+ import path11 from "path";
2465
+ import { execSync as execSync4 } from "child_process";
2230
2466
  async function buildApk(opts = {}) {
2231
2467
  let resolved;
2232
2468
  try {
@@ -2236,19 +2472,19 @@ async function buildApk(opts = {}) {
2236
2472
  }
2237
2473
  await bundle_default({ release: opts.release });
2238
2474
  const androidDir = resolved.androidDir;
2239
- const gradlew = path10.join(androidDir, process.platform === "win32" ? "gradlew.bat" : "gradlew");
2475
+ const gradlew = path11.join(androidDir, process.platform === "win32" ? "gradlew.bat" : "gradlew");
2240
2476
  const variant = opts.release ? "Release" : "Debug";
2241
2477
  const task = opts.install ? `install${variant}` : `assemble${variant}`;
2242
2478
  console.log(`
2243
2479
  \u{1F528} Building ${variant.toLowerCase()} APK${opts.install ? " and installing" : ""}...`);
2244
- execSync3(`"${gradlew}" ${task}`, { stdio: "inherit", cwd: androidDir });
2480
+ execSync4(`"${gradlew}" ${task}`, { stdio: "inherit", cwd: androidDir });
2245
2481
  console.log(`\u2705 APK ${opts.install ? "installed" : "built"} successfully.`);
2246
2482
  if (opts.install) {
2247
2483
  const packageName = resolved.config.android?.packageName;
2248
2484
  if (packageName) {
2249
2485
  try {
2250
2486
  console.log(`\u{1F680} Launching ${packageName}...`);
2251
- execSync3(`adb shell am start -n ${packageName}/.MainActivity`, { stdio: "inherit" });
2487
+ execSync4(`adb shell am start -n ${packageName}/.MainActivity`, { stdio: "inherit" });
2252
2488
  console.log("\u2705 App launched.");
2253
2489
  } catch (e) {
2254
2490
  console.warn("\u26A0\uFE0F Could not launch app. Is a device/emulator connected?");
@@ -2261,16 +2497,16 @@ async function buildApk(opts = {}) {
2261
2497
  var build_default = buildApk;
2262
2498
 
2263
2499
  // src/ios/create.ts
2264
- import fs11 from "fs";
2265
- import path12 from "path";
2500
+ import fs12 from "fs";
2501
+ import path13 from "path";
2266
2502
 
2267
2503
  // src/ios/getPod.ts
2268
- import { execSync as execSync4 } from "child_process";
2269
- import fs10 from "fs";
2270
- import path11 from "path";
2504
+ import { execSync as execSync5 } from "child_process";
2505
+ import fs11 from "fs";
2506
+ import path12 from "path";
2271
2507
  function isCocoaPodsInstalled() {
2272
2508
  try {
2273
- execSync4("command -v pod >/dev/null 2>&1");
2509
+ execSync5("command -v pod >/dev/null 2>&1");
2274
2510
  return true;
2275
2511
  } catch (error) {
2276
2512
  return false;
@@ -2289,19 +2525,19 @@ async function setupCocoaPods(rootDir) {
2289
2525
  }
2290
2526
  try {
2291
2527
  console.log("\u{1F4E6} CocoaPods is installed. Proceeding with dependency installation...");
2292
- const podfilePath = path11.join(rootDir, "Podfile");
2293
- if (!fs10.existsSync(podfilePath)) {
2528
+ const podfilePath = path12.join(rootDir, "Podfile");
2529
+ if (!fs11.existsSync(podfilePath)) {
2294
2530
  throw new Error(`Podfile not found at ${podfilePath}`);
2295
2531
  }
2296
2532
  console.log(`\u{1F680} Executing pod install in: ${rootDir}`);
2297
2533
  try {
2298
- execSync4("pod install", {
2534
+ execSync5("pod install", {
2299
2535
  cwd: rootDir,
2300
2536
  stdio: "inherit"
2301
2537
  });
2302
2538
  } catch {
2303
2539
  console.log("\u2139\uFE0F Retrying CocoaPods install with repo update...");
2304
- execSync4("pod install --repo-update", {
2540
+ execSync5("pod install --repo-update", {
2305
2541
  cwd: rootDir,
2306
2542
  stdio: "inherit"
2307
2543
  });
@@ -2316,7 +2552,7 @@ async function setupCocoaPods(rootDir) {
2316
2552
  // src/ios/create.ts
2317
2553
  import { randomBytes } from "crypto";
2318
2554
  function readAndSubstituteTemplate3(templatePath, vars) {
2319
- const raw = fs11.readFileSync(templatePath, "utf-8");
2555
+ const raw = fs12.readFileSync(templatePath, "utf-8");
2320
2556
  return Object.entries(vars).reduce(
2321
2557
  (s, [k, v]) => s.replace(new RegExp(`\\{\\{${k}\\}\\}`, "g"), v),
2322
2558
  raw
@@ -2339,17 +2575,17 @@ var create2 = () => {
2339
2575
  process.exit(1);
2340
2576
  }
2341
2577
  const iosDir = config.paths?.iosDir ?? "ios";
2342
- const rootDir = path12.join(process.cwd(), iosDir);
2343
- const projectDir = path12.join(rootDir, appName);
2344
- const xcodeprojDir = path12.join(rootDir, `${appName}.xcodeproj`);
2578
+ const rootDir = path13.join(process.cwd(), iosDir);
2579
+ const projectDir = path13.join(rootDir, appName);
2580
+ const xcodeprojDir = path13.join(rootDir, `${appName}.xcodeproj`);
2345
2581
  const bridgingHeader = `${appName}-Bridging-Header.h`;
2346
2582
  function writeFile2(filePath, content) {
2347
- fs11.mkdirSync(path12.dirname(filePath), { recursive: true });
2348
- fs11.writeFileSync(filePath, content.trimStart(), "utf8");
2583
+ fs12.mkdirSync(path13.dirname(filePath), { recursive: true });
2584
+ fs12.writeFileSync(filePath, content.trimStart(), "utf8");
2349
2585
  }
2350
- if (fs11.existsSync(rootDir)) {
2586
+ if (fs12.existsSync(rootDir)) {
2351
2587
  console.log(`\u{1F9F9} Removing existing directory: ${rootDir}`);
2352
- fs11.rmSync(rootDir, { recursive: true, force: true });
2588
+ fs12.rmSync(rootDir, { recursive: true, force: true });
2353
2589
  }
2354
2590
  console.log(`\u{1F680} Creating a new Tamer4Lynx project in: ${rootDir}`);
2355
2591
  const ids = {
@@ -2385,7 +2621,7 @@ var create2 = () => {
2385
2621
  targetDebugConfig: generateId(),
2386
2622
  targetReleaseConfig: generateId()
2387
2623
  };
2388
- writeFile2(path12.join(rootDir, "Podfile"), `
2624
+ writeFile2(path13.join(rootDir, "Podfile"), `
2389
2625
  source 'https://cdn.cocoapods.org/'
2390
2626
 
2391
2627
  platform :ios, '13.0'
@@ -2445,6 +2681,12 @@ post_install do |installer|
2445
2681
  config.build_settings['CLANG_WARN_ENUM_CONVERSION'] = 'NO'
2446
2682
  end
2447
2683
  end
2684
+ if target.name == 'PrimJS'
2685
+ target.build_configurations.each do |config|
2686
+ config.build_settings['OTHER_CFLAGS'] = "$(inherited) -Wno-macro-redefined"
2687
+ config.build_settings['OTHER_CPLUSPLUSFLAGS'] = "$(inherited) -Wno-macro-redefined"
2688
+ end
2689
+ end
2448
2690
  end
2449
2691
  Dir.glob(File.join(installer.sandbox.root, 'Target Support Files', 'Lynx', '*.xcconfig')).each do |xcconfig_path|
2450
2692
  next unless File.file?(xcconfig_path)
@@ -2464,15 +2706,15 @@ end
2464
2706
  const hostPkg = findTamerHostPackage(process.cwd());
2465
2707
  const templateVars = { PACKAGE_NAME: bundleId, APP_NAME: appName, BUNDLE_ID: bundleId };
2466
2708
  if (hostPkg) {
2467
- const templateDir = path12.join(hostPkg, "ios", "templates");
2709
+ const templateDir = path13.join(hostPkg, "ios", "templates");
2468
2710
  for (const f of ["AppDelegate.swift", "SceneDelegate.swift", "ViewController.swift", "LynxProvider.swift", "LynxInitProcessor.swift"]) {
2469
- const srcPath = path12.join(templateDir, f);
2470
- if (fs11.existsSync(srcPath)) {
2471
- writeFile2(path12.join(projectDir, f), readAndSubstituteTemplate3(srcPath, templateVars));
2711
+ const srcPath = path13.join(templateDir, f);
2712
+ if (fs12.existsSync(srcPath)) {
2713
+ writeFile2(path13.join(projectDir, f), readAndSubstituteTemplate3(srcPath, templateVars));
2472
2714
  }
2473
2715
  }
2474
2716
  } else {
2475
- writeFile2(path12.join(projectDir, "AppDelegate.swift"), `
2717
+ writeFile2(path13.join(projectDir, "AppDelegate.swift"), `
2476
2718
  import UIKit
2477
2719
 
2478
2720
  @UIApplicationMain
@@ -2487,7 +2729,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
2487
2729
  }
2488
2730
  }
2489
2731
  `);
2490
- writeFile2(path12.join(projectDir, "SceneDelegate.swift"), `
2732
+ writeFile2(path13.join(projectDir, "SceneDelegate.swift"), `
2491
2733
  import UIKit
2492
2734
 
2493
2735
  class SceneDelegate: UIResponder, UIWindowSceneDelegate {
@@ -2501,7 +2743,7 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
2501
2743
  }
2502
2744
  }
2503
2745
  `);
2504
- writeFile2(path12.join(projectDir, "ViewController.swift"), `
2746
+ writeFile2(path13.join(projectDir, "ViewController.swift"), `
2505
2747
  import UIKit
2506
2748
  import Lynx
2507
2749
  import tamerinsets
@@ -2554,6 +2796,7 @@ class ViewController: UIViewController {
2554
2796
  private func setupLynxView() {
2555
2797
  let lv = buildLynxView()
2556
2798
  view.addSubview(lv)
2799
+ TamerInsetsModule.attachHostView(lv)
2557
2800
  lv.loadTemplate(fromURL: "main.lynx.bundle", initData: nil)
2558
2801
  self.lynxView = lv
2559
2802
  }
@@ -2571,7 +2814,7 @@ class ViewController: UIViewController {
2571
2814
  }
2572
2815
  }
2573
2816
  `);
2574
- writeFile2(path12.join(projectDir, "LynxProvider.swift"), `
2817
+ writeFile2(path13.join(projectDir, "LynxProvider.swift"), `
2575
2818
  import Foundation
2576
2819
 
2577
2820
  class LynxProvider: NSObject, LynxTemplateProvider {
@@ -2590,7 +2833,7 @@ class LynxProvider: NSObject, LynxTemplateProvider {
2590
2833
  }
2591
2834
  }
2592
2835
  `);
2593
- writeFile2(path12.join(projectDir, "LynxInitProcessor.swift"), `
2836
+ writeFile2(path13.join(projectDir, "LynxInitProcessor.swift"), `
2594
2837
  // Copyright 2024 The Lynx Authors. All rights reserved.
2595
2838
  // Licensed under the Apache License Version 2.0 that can be found in the
2596
2839
  // LICENSE file in the root directory of this source tree.
@@ -2630,7 +2873,7 @@ final class LynxInitProcessor {
2630
2873
  }
2631
2874
  `);
2632
2875
  }
2633
- writeFile2(path12.join(projectDir, bridgingHeader), `
2876
+ writeFile2(path13.join(projectDir, bridgingHeader), `
2634
2877
  #import <Lynx/LynxConfig.h>
2635
2878
  #import <Lynx/LynxEnv.h>
2636
2879
  #import <Lynx/LynxTemplateProvider.h>
@@ -2639,7 +2882,7 @@ final class LynxInitProcessor {
2639
2882
  #import <SDWebImage/SDWebImage.h>
2640
2883
  #import <SDWebImageWebPCoder/SDWebImageWebPCoder.h>
2641
2884
  `);
2642
- writeFile2(path12.join(projectDir, "Info.plist"), `
2885
+ writeFile2(path13.join(projectDir, "Info.plist"), `
2643
2886
  <?xml version="1.0" encoding="UTF-8"?>
2644
2887
  <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
2645
2888
  <plist version="1.0">
@@ -2697,39 +2940,21 @@ final class LynxInitProcessor {
2697
2940
  </dict>
2698
2941
  </plist>
2699
2942
  `);
2700
- const appIconDir = path12.join(projectDir, "Assets.xcassets", "AppIcon.appiconset");
2701
- fs11.mkdirSync(appIconDir, { recursive: true });
2943
+ const appIconDir = path13.join(projectDir, "Assets.xcassets", "AppIcon.appiconset");
2944
+ fs12.mkdirSync(appIconDir, { recursive: true });
2702
2945
  const iconPaths = resolveIconPaths(process.cwd(), config);
2703
- if (iconPaths?.ios) {
2704
- const entries = fs11.readdirSync(iconPaths.ios, { withFileTypes: true });
2705
- for (const e of entries) {
2706
- const dest = path12.join(appIconDir, e.name);
2707
- if (e.isDirectory()) {
2708
- fs11.cpSync(path12.join(iconPaths.ios, e.name), dest, { recursive: true });
2709
- } else {
2710
- fs11.copyFileSync(path12.join(iconPaths.ios, e.name), dest);
2711
- }
2712
- }
2713
- console.log("\u2705 Copied iOS icon from tamer.config.json icon.ios");
2714
- } else if (iconPaths?.source) {
2715
- const ext = path12.extname(iconPaths.source) || ".png";
2716
- const icon1024 = `Icon-1024${ext}`;
2717
- fs11.copyFileSync(iconPaths.source, path12.join(appIconDir, icon1024));
2718
- writeFile2(path12.join(appIconDir, "Contents.json"), JSON.stringify({
2719
- images: [{ filename: icon1024, idiom: "universal", platform: "ios", size: "1024x1024" }],
2720
- info: { author: "xcode", version: 1 }
2721
- }, null, 2));
2722
- console.log("\u2705 Copied app icon from tamer.config.json icon.source");
2946
+ if (applyIosAppIconAssets(appIconDir, iconPaths)) {
2947
+ console.log(iconPaths?.ios ? "\u2705 Copied iOS icon from tamer.config.json icon.ios" : "\u2705 Copied app icon from tamer.config.json icon.source");
2723
2948
  } else {
2724
- writeFile2(path12.join(appIconDir, "Contents.json"), `
2949
+ writeFile2(path13.join(appIconDir, "Contents.json"), `
2725
2950
  {
2726
2951
  "images" : [ { "idiom" : "universal", "platform" : "ios", "size" : "1024x1024" } ],
2727
2952
  "info" : { "author" : "xcode", "version" : 1 }
2728
2953
  }
2729
2954
  `);
2730
2955
  }
2731
- fs11.mkdirSync(xcodeprojDir, { recursive: true });
2732
- writeFile2(path12.join(xcodeprojDir, "project.pbxproj"), `
2956
+ fs12.mkdirSync(xcodeprojDir, { recursive: true });
2957
+ writeFile2(path13.join(xcodeprojDir, "project.pbxproj"), `
2733
2958
  // !$*UTF8*$!
2734
2959
  {
2735
2960
  archiveVersion = 1;
@@ -3015,450 +3240,16 @@ final class LynxInitProcessor {
3015
3240
  var create_default2 = create2;
3016
3241
 
3017
3242
  // src/ios/autolink.ts
3018
- import fs12 from "fs";
3019
- import path13 from "path";
3020
- import { execSync as execSync5 } from "child_process";
3021
- var autolink2 = () => {
3022
- let resolved;
3023
- try {
3024
- resolved = resolveHostPaths();
3025
- } catch (error) {
3026
- console.error(`\u274C Error loading configuration: ${error.message}`);
3027
- process.exit(1);
3028
- }
3029
- const projectRoot = resolved.projectRoot;
3030
- const iosProjectPath = resolved.iosDir;
3031
- function updateGeneratedSection(filePath, newContent, startMarker, endMarker) {
3032
- if (!fs12.existsSync(filePath)) {
3033
- console.warn(`\u26A0\uFE0F File not found, skipping update: ${filePath}`);
3034
- return;
3035
- }
3036
- let fileContent = fs12.readFileSync(filePath, "utf8");
3037
- const escapedStartMarker = startMarker.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
3038
- const escapedEndMarker = endMarker.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
3039
- const regex = new RegExp(`${escapedStartMarker}[\\s\\S]*?${escapedEndMarker}`, "g");
3040
- const replacementBlock = `${startMarker}
3041
- ${newContent}
3042
- ${endMarker}`;
3043
- if (regex.test(fileContent)) {
3044
- const firstStartIdx = fileContent.indexOf(startMarker);
3045
- fileContent = fileContent.replace(regex, "");
3046
- if (firstStartIdx !== -1) {
3047
- const before = fileContent.slice(0, firstStartIdx);
3048
- const after = fileContent.slice(firstStartIdx);
3049
- fileContent = `${before}${replacementBlock}${after}`;
3050
- } else {
3051
- fileContent += `
3052
- ${replacementBlock}
3053
- `;
3054
- }
3055
- } else {
3056
- console.warn(`\u26A0\uFE0F Could not find autolink markers in ${path13.basename(filePath)}. Appending to the end of the file.`);
3057
- fileContent += `
3058
- ${replacementBlock}
3243
+ import fs14 from "fs";
3244
+ import path15 from "path";
3245
+ import { execSync as execSync6 } from "child_process";
3246
+
3247
+ // src/common/hostNativeModulesManifest.ts
3248
+ var TAMER_HOST_NATIVE_MODULES_FILENAME = "tamer-host-native-modules.json";
3249
+ function buildHostNativeModulesManifestJson(moduleClassNames) {
3250
+ return `${JSON.stringify({ moduleClassNames }, null, 2)}
3059
3251
  `;
3060
- }
3061
- fs12.writeFileSync(filePath, fileContent, "utf8");
3062
- console.log(`\u2705 Updated autolinked section in ${path13.basename(filePath)}`);
3063
- }
3064
- function resolvePodDirectory(pkg) {
3065
- const configuredDir = path13.join(pkg.packagePath, pkg.config.ios?.podspecPath || ".");
3066
- if (fs12.existsSync(configuredDir)) {
3067
- return configuredDir;
3068
- }
3069
- const iosDir = path13.join(pkg.packagePath, "ios");
3070
- if (fs12.existsSync(iosDir)) {
3071
- const stack = [iosDir];
3072
- while (stack.length > 0) {
3073
- const current = stack.pop();
3074
- try {
3075
- const entries = fs12.readdirSync(current, { withFileTypes: true });
3076
- const podspec = entries.find((entry) => entry.isFile() && entry.name.endsWith(".podspec"));
3077
- if (podspec) {
3078
- return current;
3079
- }
3080
- for (const entry of entries) {
3081
- if (entry.isDirectory()) {
3082
- stack.push(path13.join(current, entry.name));
3083
- }
3084
- }
3085
- } catch {
3086
- }
3087
- }
3088
- }
3089
- return configuredDir;
3090
- }
3091
- function resolvePodName(pkg) {
3092
- const fullPodspecDir = resolvePodDirectory(pkg);
3093
- if (fs12.existsSync(fullPodspecDir)) {
3094
- try {
3095
- const files = fs12.readdirSync(fullPodspecDir);
3096
- const podspecFile = files.find((f) => f.endsWith(".podspec"));
3097
- if (podspecFile) return podspecFile.replace(".podspec", "");
3098
- } catch {
3099
- }
3100
- }
3101
- return pkg.name.split("/").pop().replace(/-/g, "");
3102
- }
3103
- function updatePodfile(packages) {
3104
- const podfilePath = path13.join(iosProjectPath, "Podfile");
3105
- let scriptContent = ` # This section is automatically generated by Tamer4Lynx.
3106
- # Manual edits will be overwritten.`;
3107
- const iosPackages = packages.filter((p) => p.config.ios);
3108
- if (iosPackages.length > 0) {
3109
- iosPackages.forEach((pkg) => {
3110
- const relativePath = path13.relative(iosProjectPath, resolvePodDirectory(pkg));
3111
- const podName = resolvePodName(pkg);
3112
- scriptContent += `
3113
- pod '${podName}', :path => '${relativePath}'`;
3114
- });
3115
- } else {
3116
- scriptContent += `
3117
- # No native modules found by Tamer4Lynx autolinker.`;
3118
- }
3119
- updateGeneratedSection(podfilePath, scriptContent.trim(), "# GENERATED AUTOLINK DEPENDENCIES START", "# GENERATED AUTOLINK DEPENDENCIES END");
3120
- }
3121
- function ensureXElementPod() {
3122
- const podfilePath = path13.join(iosProjectPath, "Podfile");
3123
- if (!fs12.existsSync(podfilePath)) return;
3124
- let content = fs12.readFileSync(podfilePath, "utf8");
3125
- if (content.includes("pod 'XElement'")) return;
3126
- const lynxVersionMatch = content.match(/pod\s+'Lynx',\s*'([^']+)'/);
3127
- const lynxVersion = lynxVersionMatch?.[1] ?? "3.6.0";
3128
- const xelementLine = `
3129
- pod 'XElement', '${lynxVersion}'`;
3130
- const insertAfter = /pod\s+'LynxService'[^\n]*(?:\n\s*'[^']*',?\s*)*/;
3131
- const serviceMatch = content.match(insertAfter);
3132
- if (serviceMatch) {
3133
- const idx = serviceMatch.index + serviceMatch[0].length;
3134
- content = content.slice(0, idx) + xelementLine + content.slice(idx);
3135
- } else {
3136
- content = content.replace(
3137
- /(# GENERATED AUTOLINK DEPENDENCIES START)/,
3138
- `pod 'XElement', '${lynxVersion}'
3139
-
3140
- $1`
3141
- );
3142
- }
3143
- fs12.writeFileSync(podfilePath, content, "utf8");
3144
- console.log(`\u2705 Added XElement pod (v${lynxVersion}) to Podfile`);
3145
- }
3146
- function ensureLynxPatchInPodfile() {
3147
- const podfilePath = path13.join(iosProjectPath, "Podfile");
3148
- if (!fs12.existsSync(podfilePath)) return;
3149
- let content = fs12.readFileSync(podfilePath, "utf8");
3150
- if (content.includes("content.gsub(/\\btypeof\\(/, '__typeof__(')")) return;
3151
- const patch = `
3152
- Dir.glob(File.join(installer.sandbox.root, 'Lynx/platform/darwin/**/*.{m,mm}')).each do |lynx_source|
3153
- next unless File.file?(lynx_source)
3154
- content = File.read(lynx_source)
3155
- next unless content.match?(/\\btypeof\\(/)
3156
- File.chmod(0644, lynx_source) rescue nil
3157
- File.write(lynx_source, content.gsub(/\\btypeof\\(/, '__typeof__('))
3158
- end`;
3159
- content = content.replace(/(\n end\s*\n)(end\s*)$/, `$1${patch}
3160
- $2`);
3161
- fs12.writeFileSync(podfilePath, content, "utf8");
3162
- console.log("\u2705 Added Lynx typeof patch to Podfile post_install.");
3163
- }
3164
- function ensurePodBuildSettings() {
3165
- const podfilePath = path13.join(iosProjectPath, "Podfile");
3166
- if (!fs12.existsSync(podfilePath)) return;
3167
- let content = fs12.readFileSync(podfilePath, "utf8");
3168
- let changed = false;
3169
- if (!content.includes("CLANG_ENABLE_EXPLICIT_MODULES")) {
3170
- content = content.replace(
3171
- /config\.build_settings\['IPHONEOS_DEPLOYMENT_TARGET'\]\s*=\s*'[^']*'/,
3172
- `$&
3173
- config.build_settings['CLANG_ENABLE_EXPLICIT_MODULES'] = 'NO'
3174
- config.build_settings['ONLY_ACTIVE_ARCH'] = 'YES'`
3175
- );
3176
- changed = true;
3177
- }
3178
- if (!content.includes("gsub('-Werror'")) {
3179
- const xcconfigStrip = `
3180
- Dir.glob(File.join(installer.sandbox.root, 'Target Support Files', 'Lynx', '*.xcconfig')).each do |xcconfig_path|
3181
- next unless File.file?(xcconfig_path)
3182
- content = File.read(xcconfig_path)
3183
- next unless content.include?('-Werror')
3184
- File.write(xcconfig_path, content.gsub('-Werror', ''))
3185
- end`;
3186
- content = content.replace(
3187
- /(Dir\.glob.*?Lynx\/platform\/darwin)/s,
3188
- `${xcconfigStrip}
3189
- $1`
3190
- );
3191
- changed = true;
3192
- }
3193
- if (changed) {
3194
- fs12.writeFileSync(podfilePath, content, "utf8");
3195
- console.log("\u2705 Added Xcode compatibility build settings to Podfile post_install.");
3196
- }
3197
- }
3198
- function updateLynxInitProcessor(packages) {
3199
- const appNameFromConfig = resolved.config.ios?.appName;
3200
- const candidatePaths = [];
3201
- if (appNameFromConfig) {
3202
- candidatePaths.push(path13.join(iosProjectPath, appNameFromConfig, "LynxInitProcessor.swift"));
3203
- }
3204
- candidatePaths.push(path13.join(iosProjectPath, "LynxInitProcessor.swift"));
3205
- const found = candidatePaths.find((p) => fs12.existsSync(p));
3206
- const lynxInitPath = found ?? candidatePaths[0];
3207
- const iosPackages = packages.filter((p) => getIosModuleClassNames(p.config.ios).length > 0 || Object.keys(getIosElements(p.config.ios)).length > 0);
3208
- const seenModules = /* @__PURE__ */ new Set();
3209
- const seenElements = /* @__PURE__ */ new Set();
3210
- const packagesWithContributions = /* @__PURE__ */ new Set();
3211
- for (const pkg of iosPackages) {
3212
- let hasUnique = false;
3213
- for (const cls of getIosModuleClassNames(pkg.config.ios)) {
3214
- if (!seenModules.has(cls)) {
3215
- seenModules.add(cls);
3216
- hasUnique = true;
3217
- } else {
3218
- console.warn(`\u26A0\uFE0F Skipping duplicate module "${cls}" from ${pkg.name} (already registered by another package)`);
3219
- }
3220
- }
3221
- for (const tag of Object.keys(getIosElements(pkg.config.ios))) {
3222
- if (!seenElements.has(tag)) {
3223
- seenElements.add(tag);
3224
- hasUnique = true;
3225
- } else {
3226
- console.warn(`\u26A0\uFE0F Skipping duplicate element "${tag}" from ${pkg.name} (already registered by another package)`);
3227
- }
3228
- }
3229
- if (hasUnique) packagesWithContributions.add(pkg);
3230
- }
3231
- const importPackages = iosPackages.filter((p) => packagesWithContributions.has(p));
3232
- function updateImportsSection(filePath, pkgs) {
3233
- const startMarker = "// GENERATED IMPORTS START";
3234
- const endMarker = "// GENERATED IMPORTS END";
3235
- if (pkgs.length === 0) {
3236
- const placeholder = "// No native imports found by Tamer4Lynx autolinker.";
3237
- updateGeneratedSection(filePath, placeholder, startMarker, endMarker);
3238
- return;
3239
- }
3240
- const imports = pkgs.map((pkg) => {
3241
- const podName = resolvePodName(pkg);
3242
- return `import ${podName}`;
3243
- }).join("\n");
3244
- const fileContent = fs12.readFileSync(filePath, "utf8");
3245
- if (fileContent.indexOf(startMarker) !== -1) {
3246
- updateGeneratedSection(filePath, imports, startMarker, endMarker);
3247
- return;
3248
- }
3249
- const importRegex = /^(import\s+[^\r\n]+)\r?\n/gm;
3250
- let match = null;
3251
- let lastMatchEnd = -1;
3252
- while ((match = importRegex.exec(fileContent)) !== null) {
3253
- lastMatchEnd = importRegex.lastIndex;
3254
- }
3255
- const block = `${startMarker}
3256
- ${imports}
3257
- ${endMarker}`;
3258
- let newContent;
3259
- if (lastMatchEnd !== -1) {
3260
- const before = fileContent.slice(0, lastMatchEnd);
3261
- const after = fileContent.slice(lastMatchEnd);
3262
- newContent = `${before}
3263
- ${block}
3264
- ${after}`;
3265
- } else {
3266
- const foundationIdx = fileContent.indexOf("import Foundation");
3267
- if (foundationIdx !== -1) {
3268
- const lineEnd = fileContent.indexOf("\n", foundationIdx);
3269
- const insertPos = lineEnd !== -1 ? lineEnd + 1 : foundationIdx + "import Foundation".length;
3270
- const before = fileContent.slice(0, insertPos);
3271
- const after = fileContent.slice(insertPos);
3272
- newContent = `${before}
3273
- ${block}
3274
- ${after}`;
3275
- } else {
3276
- newContent = `${block}
3277
-
3278
- ${fileContent}`;
3279
- }
3280
- }
3281
- fs12.writeFileSync(filePath, newContent, "utf8");
3282
- console.log(`\u2705 Updated imports in ${path13.basename(filePath)}`);
3283
- }
3284
- updateImportsSection(lynxInitPath, importPackages);
3285
- if (importPackages.length === 0) {
3286
- const placeholder = " // No native modules found by Tamer4Lynx autolinker.";
3287
- updateGeneratedSection(lynxInitPath, placeholder, "// GENERATED AUTOLINK START", "// GENERATED AUTOLINK END");
3288
- return;
3289
- }
3290
- const seenModules2 = /* @__PURE__ */ new Set();
3291
- const seenElements2 = /* @__PURE__ */ new Set();
3292
- const blocks = importPackages.flatMap((pkg) => {
3293
- const classNames = getIosModuleClassNames(pkg.config.ios);
3294
- const moduleBlocks = classNames.filter((cls) => {
3295
- if (seenModules2.has(cls)) return false;
3296
- seenModules2.add(cls);
3297
- return true;
3298
- }).map((classNameRaw) => [
3299
- ` // Register module from package: ${pkg.name}`,
3300
- ` globalConfig.register(${classNameRaw}.self)`
3301
- ].join("\n"));
3302
- const elementBlocks = Object.entries(getIosElements(pkg.config.ios)).filter(([tagName]) => {
3303
- if (seenElements2.has(tagName)) return false;
3304
- seenElements2.add(tagName);
3305
- return true;
3306
- }).map(([tagName, classNameRaw]) => [
3307
- ` // Register element from package: ${pkg.name}`,
3308
- ` globalConfig.registerUI(${classNameRaw}.self, withName: "${tagName}")`
3309
- ].join("\n"));
3310
- return [...moduleBlocks, ...elementBlocks];
3311
- });
3312
- const content = blocks.join("\n\n");
3313
- updateGeneratedSection(lynxInitPath, content, "// GENERATED AUTOLINK START", "// GENERATED AUTOLINK END");
3314
- }
3315
- function findInfoPlist() {
3316
- const appNameFromConfig = resolved.config.ios?.appName;
3317
- const candidates = [];
3318
- if (appNameFromConfig) {
3319
- candidates.push(path13.join(iosProjectPath, appNameFromConfig, "Info.plist"));
3320
- }
3321
- candidates.push(path13.join(iosProjectPath, "Info.plist"));
3322
- return candidates.find((p) => fs12.existsSync(p)) ?? null;
3323
- }
3324
- function readPlistXml(plistPath) {
3325
- return fs12.readFileSync(plistPath, "utf8");
3326
- }
3327
- function syncInfoPlistPermissions(packages) {
3328
- const plistPath = findInfoPlist();
3329
- if (!plistPath) return;
3330
- const allPermissions = {};
3331
- for (const pkg of packages) {
3332
- const iosPerms = pkg.config.ios?.iosPermissions;
3333
- if (iosPerms) {
3334
- for (const [key, desc] of Object.entries(iosPerms)) {
3335
- if (!allPermissions[key]) {
3336
- allPermissions[key] = desc;
3337
- }
3338
- }
3339
- }
3340
- }
3341
- if (Object.keys(allPermissions).length === 0) return;
3342
- let plist = readPlistXml(plistPath);
3343
- let added = 0;
3344
- for (const [key, desc] of Object.entries(allPermissions)) {
3345
- if (plist.includes(`<key>${key}</key>`)) continue;
3346
- const insertion = ` <key>${key}</key>
3347
- <string>${desc}</string>
3348
- `;
3349
- plist = plist.replace(
3350
- /(<\/dict>\s*<\/plist>)/,
3351
- `${insertion}$1`
3352
- );
3353
- added++;
3354
- }
3355
- if (added > 0) {
3356
- fs12.writeFileSync(plistPath, plist, "utf8");
3357
- console.log(`\u2705 Synced ${added} Info.plist permission description(s)`);
3358
- }
3359
- }
3360
- function syncInfoPlistUrlSchemes() {
3361
- const urlSchemes = resolved.config.ios?.urlSchemes;
3362
- if (!urlSchemes || urlSchemes.length === 0) return;
3363
- const plistPath = findInfoPlist();
3364
- if (!plistPath) return;
3365
- let plist = readPlistXml(plistPath);
3366
- const schemesXml = urlSchemes.map((s) => {
3367
- const role = s.role ?? "Editor";
3368
- return ` <dict>
3369
- <key>CFBundleTypeRole</key>
3370
- <string>${role}</string>
3371
- <key>CFBundleURLSchemes</key>
3372
- <array>
3373
- <string>${s.scheme}</string>
3374
- </array>
3375
- </dict>`;
3376
- }).join("\n");
3377
- const generatedBlock = ` <key>CFBundleURLTypes</key>
3378
- <array>
3379
- <!-- GENERATED URL SCHEMES START -->
3380
- ${schemesXml}
3381
- <!-- GENERATED URL SCHEMES END -->
3382
- </array>`;
3383
- const startMarker = "<!-- GENERATED URL SCHEMES START -->";
3384
- const endMarker = "<!-- GENERATED URL SCHEMES END -->";
3385
- if (plist.includes(startMarker) && plist.includes(endMarker)) {
3386
- const regex = new RegExp(
3387
- `${startMarker.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}[\\s\\S]*?${endMarker.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}`,
3388
- "g"
3389
- );
3390
- plist = plist.replace(regex, `${startMarker}
3391
- ${schemesXml}
3392
- ${endMarker}`);
3393
- } else if (plist.includes("<key>CFBundleURLTypes</key>")) {
3394
- console.log("\u2139\uFE0F CFBundleURLTypes exists but has no generated markers. Skipping URL scheme sync.");
3395
- return;
3396
- } else {
3397
- plist = plist.replace(
3398
- /(<\/dict>\s*<\/plist>)/,
3399
- `${generatedBlock}
3400
- $1`
3401
- );
3402
- }
3403
- fs12.writeFileSync(plistPath, plist, "utf8");
3404
- console.log(`\u2705 Synced ${urlSchemes.length} iOS URL scheme(s) into Info.plist`);
3405
- }
3406
- function runPodInstall(forcePath) {
3407
- const podfilePath = forcePath ?? path13.join(iosProjectPath, "Podfile");
3408
- if (!fs12.existsSync(podfilePath)) {
3409
- console.log("\u2139\uFE0F No Podfile found in ios directory; skipping `pod install`.");
3410
- return;
3411
- }
3412
- const cwd = path13.dirname(podfilePath);
3413
- try {
3414
- console.log(`\u2139\uFE0F Running \`pod install\` in ${cwd}...`);
3415
- try {
3416
- execSync5("pod install", { cwd, stdio: "inherit" });
3417
- } catch {
3418
- console.log("\u2139\uFE0F Retrying `pod install` with repo update...");
3419
- execSync5("pod install --repo-update", { cwd, stdio: "inherit" });
3420
- }
3421
- console.log("\u2705 `pod install` completed successfully.");
3422
- } catch (e) {
3423
- console.warn(`\u26A0\uFE0F 'pod install' failed: ${e.message}`);
3424
- console.log("\u26A0\uFE0F You can run `pod install` manually in the ios directory.");
3425
- }
3426
- }
3427
- function run() {
3428
- console.log("\u{1F50E} Finding Lynx extension packages (lynx.ext.json / tamer.json)...");
3429
- const packages = discoverModules(projectRoot).filter((p) => p.config.ios);
3430
- if (packages.length > 0) {
3431
- console.log(`Found ${packages.length} package(s): ${packages.map((p) => p.name).join(", ")}`);
3432
- } else {
3433
- console.log("\u2139\uFE0F No Tamer4Lynx native packages found.");
3434
- }
3435
- updatePodfile(packages);
3436
- ensureXElementPod();
3437
- ensureLynxPatchInPodfile();
3438
- ensurePodBuildSettings();
3439
- updateLynxInitProcessor(packages);
3440
- syncInfoPlistPermissions(packages);
3441
- syncInfoPlistUrlSchemes();
3442
- const appNameFromConfig = resolved.config.ios?.appName;
3443
- if (appNameFromConfig) {
3444
- const appPodfile = path13.join(iosProjectPath, appNameFromConfig, "Podfile");
3445
- if (fs12.existsSync(appPodfile)) {
3446
- runPodInstall(appPodfile);
3447
- console.log("\u2728 Autolinking complete for iOS.");
3448
- return;
3449
- }
3450
- }
3451
- runPodInstall();
3452
- console.log("\u2728 Autolinking complete for iOS.");
3453
- }
3454
- run();
3455
- };
3456
- var autolink_default2 = autolink2;
3457
-
3458
- // src/ios/bundle.ts
3459
- import fs14 from "fs";
3460
- import path15 from "path";
3461
- import { execSync as execSync6 } from "child_process";
3252
+ }
3462
3253
 
3463
3254
  // src/ios/syncHost.ts
3464
3255
  import fs13 from "fs";
@@ -3615,344 +3406,842 @@ function writeFile(filePath, content) {
3615
3406
  function getAppDelegateSwift() {
3616
3407
  return `import UIKit
3617
3408
 
3618
- @UIApplicationMain
3619
- class AppDelegate: UIResponder, UIApplicationDelegate {
3620
- func application(
3621
- _ application: UIApplication,
3622
- didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
3623
- ) -> Bool {
3624
- LynxInitProcessor.shared.setupEnvironment()
3625
- return true
3409
+ @UIApplicationMain
3410
+ class AppDelegate: UIResponder, UIApplicationDelegate {
3411
+ func application(
3412
+ _ application: UIApplication,
3413
+ didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
3414
+ ) -> Bool {
3415
+ LynxInitProcessor.shared.setupEnvironment()
3416
+ return true
3417
+ }
3418
+
3419
+ func application(
3420
+ _ application: UIApplication,
3421
+ configurationForConnecting connectingSceneSession: UISceneSession,
3422
+ options: UIScene.ConnectionOptions
3423
+ ) -> UISceneConfiguration {
3424
+ return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role)
3425
+ }
3426
+ }
3427
+ `;
3428
+ }
3429
+ function getSceneDelegateSwift() {
3430
+ return `import UIKit
3431
+
3432
+ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
3433
+ var window: UIWindow?
3434
+
3435
+ func scene(
3436
+ _ scene: UIScene,
3437
+ willConnectTo session: UISceneSession,
3438
+ options connectionOptions: UIScene.ConnectionOptions
3439
+ ) {
3440
+ guard let windowScene = scene as? UIWindowScene else { return }
3441
+ window = UIWindow(windowScene: windowScene)
3442
+ window?.rootViewController = ViewController()
3443
+ window?.makeKeyAndVisible()
3444
+ }
3445
+ }
3446
+ `;
3447
+ }
3448
+ function getViewControllerSwift() {
3449
+ return `import UIKit
3450
+ import Lynx
3451
+ import tamerinsets
3452
+
3453
+ class ViewController: UIViewController {
3454
+ private var lynxView: LynxView?
3455
+
3456
+ override func viewDidLoad() {
3457
+ super.viewDidLoad()
3458
+ view.backgroundColor = .black
3459
+ edgesForExtendedLayout = .all
3460
+ extendedLayoutIncludesOpaqueBars = true
3461
+ additionalSafeAreaInsets = .zero
3462
+ view.insetsLayoutMarginsFromSafeArea = false
3463
+ view.preservesSuperviewLayoutMargins = false
3464
+ viewRespectsSystemMinimumLayoutMargins = false
3465
+ }
3466
+
3467
+ override func viewDidLayoutSubviews() {
3468
+ super.viewDidLayoutSubviews()
3469
+ guard view.bounds.width > 0, view.bounds.height > 0 else { return }
3470
+ if lynxView == nil {
3471
+ setupLynxView()
3472
+ } else {
3473
+ applyFullscreenLayout(to: lynxView!)
3474
+ }
3475
+ }
3476
+
3477
+ override func viewSafeAreaInsetsDidChange() {
3478
+ super.viewSafeAreaInsetsDidChange()
3479
+ TamerInsetsModule.reRequestInsets()
3480
+ }
3481
+
3482
+ override var preferredStatusBarStyle: UIStatusBarStyle { .lightContent }
3483
+
3484
+ private func buildLynxView() -> LynxView {
3485
+ let bounds = view.bounds
3486
+ let lv = LynxView { builder in
3487
+ builder.config = LynxConfig(provider: LynxProvider())
3488
+ builder.screenSize = bounds.size
3489
+ builder.fontScale = 1.0
3490
+ }
3491
+ lv.autoresizingMask = [.flexibleWidth, .flexibleHeight]
3492
+ lv.insetsLayoutMarginsFromSafeArea = false
3493
+ lv.preservesSuperviewLayoutMargins = false
3494
+ applyFullscreenLayout(to: lv)
3495
+ return lv
3496
+ }
3497
+
3498
+ private func setupLynxView() {
3499
+ let lv = buildLynxView()
3500
+ view.addSubview(lv)
3501
+ TamerInsetsModule.attachHostView(lv)
3502
+ lv.loadTemplate(fromURL: "main.lynx.bundle", initData: nil)
3503
+ self.lynxView = lv
3504
+ }
3505
+
3506
+ private func applyFullscreenLayout(to lynxView: LynxView) {
3507
+ let bounds = view.bounds
3508
+ let size = bounds.size
3509
+ lynxView.frame = bounds
3510
+ lynxView.updateScreenMetrics(withWidth: size.width, height: size.height)
3511
+ lynxView.updateViewport(withPreferredLayoutWidth: size.width, preferredLayoutHeight: size.height, needLayout: true)
3512
+ lynxView.preferredLayoutWidth = size.width
3513
+ lynxView.preferredLayoutHeight = size.height
3514
+ lynxView.layoutWidthMode = .exact
3515
+ lynxView.layoutHeightMode = .exact
3516
+ }
3517
+ }
3518
+ `;
3519
+ }
3520
+ function getDevViewControllerSwift() {
3521
+ return `import UIKit
3522
+ import Lynx
3523
+ import tamerdevclient
3524
+ import tamerinsets
3525
+ import tamersystemui
3526
+
3527
+ class ViewController: UIViewController {
3528
+ private var lynxView: LynxView?
3529
+
3530
+ override func viewDidLoad() {
3531
+ super.viewDidLoad()
3532
+ view.backgroundColor = .black
3533
+ edgesForExtendedLayout = .all
3534
+ extendedLayoutIncludesOpaqueBars = true
3535
+ additionalSafeAreaInsets = .zero
3536
+ view.insetsLayoutMarginsFromSafeArea = false
3537
+ view.preservesSuperviewLayoutMargins = false
3538
+ viewRespectsSystemMinimumLayoutMargins = false
3539
+ setupLynxView()
3540
+ setupDevClientModule()
3626
3541
  }
3627
3542
 
3628
- func application(
3629
- _ application: UIApplication,
3630
- configurationForConnecting connectingSceneSession: UISceneSession,
3631
- options: UIScene.ConnectionOptions
3632
- ) -> UISceneConfiguration {
3633
- return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role)
3543
+ override func viewDidLayoutSubviews() {
3544
+ super.viewDidLayoutSubviews()
3545
+ if let lynxView = lynxView {
3546
+ applyFullscreenLayout(to: lynxView)
3547
+ }
3548
+ }
3549
+
3550
+ override func viewSafeAreaInsetsDidChange() {
3551
+ super.viewSafeAreaInsetsDidChange()
3552
+ TamerInsetsModule.reRequestInsets()
3553
+ }
3554
+
3555
+ override var preferredStatusBarStyle: UIStatusBarStyle { SystemUIModule.statusBarStyleForHost }
3556
+
3557
+ private func setupLynxView() {
3558
+ let size = fullscreenBounds().size
3559
+ let lv = LynxView { builder in
3560
+ builder.config = LynxConfig(provider: DevTemplateProvider())
3561
+ builder.screenSize = size
3562
+ builder.fontScale = 1.0
3563
+ }
3564
+ lv.autoresizingMask = [.flexibleWidth, .flexibleHeight]
3565
+ lv.insetsLayoutMarginsFromSafeArea = false
3566
+ lv.preservesSuperviewLayoutMargins = false
3567
+ view.addSubview(lv)
3568
+ applyFullscreenLayout(to: lv)
3569
+ TamerInsetsModule.attachHostView(lv)
3570
+ lv.loadTemplate(fromURL: "dev-client.lynx.bundle", initData: nil)
3571
+ DispatchQueue.main.asyncAfter(deadline: .now() + 0.25) { [weak self, weak lv] in
3572
+ guard let self, let lv else { return }
3573
+ self.applyFullscreenLayout(to: lv)
3574
+ }
3575
+ self.lynxView = lv
3576
+ }
3577
+
3578
+ private func applyFullscreenLayout(to lynxView: LynxView) {
3579
+ let bounds = fullscreenBounds()
3580
+ let size = bounds.size
3581
+ lynxView.frame = bounds
3582
+ lynxView.updateScreenMetrics(withWidth: size.width, height: size.height)
3583
+ lynxView.updateViewport(withPreferredLayoutWidth: size.width, preferredLayoutHeight: size.height, needLayout: true)
3584
+ lynxView.preferredLayoutWidth = size.width
3585
+ lynxView.preferredLayoutHeight = size.height
3586
+ lynxView.layoutWidthMode = .exact
3587
+ lynxView.layoutHeightMode = .exact
3588
+ }
3589
+
3590
+ private func fullscreenBounds() -> CGRect {
3591
+ let bounds = view.bounds
3592
+ if bounds.width > 0, bounds.height > 0 { return bounds }
3593
+ return UIScreen.main.bounds
3594
+ }
3595
+
3596
+ private func setupDevClientModule() {
3597
+ DevClientModule.presentQRScanner = { [weak self] completion in
3598
+ let scanner = QRScannerViewController()
3599
+ scanner.onResult = { url in
3600
+ scanner.dismiss(animated: true) { completion(url) }
3601
+ }
3602
+ scanner.modalPresentationStyle = .fullScreen
3603
+ self?.present(scanner, animated: true)
3604
+ }
3605
+
3606
+ DevClientModule.reloadProjectHandler = { [weak self] in
3607
+ guard let self = self else { return }
3608
+ let projectVC = ProjectViewController()
3609
+ projectVC.modalPresentationStyle = .fullScreen
3610
+ self.present(projectVC, animated: true)
3611
+ }
3634
3612
  }
3635
3613
  }
3636
3614
  `;
3637
3615
  }
3638
- function getSceneDelegateSwift() {
3639
- return `import UIKit
3616
+ function patchInfoPlist(infoPlistPath) {
3617
+ if (!fs13.existsSync(infoPlistPath)) return;
3618
+ let content = fs13.readFileSync(infoPlistPath, "utf8");
3619
+ content = content.replace(/\s*<key>UIMainStoryboardFile<\/key>\s*<string>[^<]*<\/string>/g, "");
3620
+ if (!content.includes("UILaunchStoryboardName")) {
3621
+ content = content.replace("</dict>\n</plist>", ` <key>UILaunchStoryboardName</key>
3622
+ <string>LaunchScreen</string>
3623
+ </dict>
3624
+ </plist>`);
3625
+ console.log("\u2705 Added UILaunchStoryboardName to Info.plist");
3626
+ }
3627
+ if (!content.includes("UIApplicationSceneManifest")) {
3628
+ const sceneManifest = ` <key>UIApplicationSceneManifest</key>
3629
+ <dict>
3630
+ <key>UIApplicationSupportsMultipleScenes</key>
3631
+ <false/>
3632
+ <key>UISceneConfigurations</key>
3633
+ <dict>
3634
+ <key>UIWindowSceneSessionRoleApplication</key>
3635
+ <array>
3636
+ <dict>
3637
+ <key>UISceneConfigurationName</key>
3638
+ <string>Default Configuration</string>
3639
+ <key>UISceneDelegateClassName</key>
3640
+ <string>$(PRODUCT_MODULE_NAME).SceneDelegate</string>
3641
+ </dict>
3642
+ </array>
3643
+ </dict>
3644
+ </dict>`;
3645
+ content = content.replace("</dict>\n</plist>", `${sceneManifest}
3646
+ </dict>
3647
+ </plist>`);
3648
+ console.log("\u2705 Added UIApplicationSceneManifest to Info.plist");
3649
+ }
3650
+ fs13.writeFileSync(infoPlistPath, content, "utf8");
3651
+ }
3652
+ function getSimpleLynxProviderSwift() {
3653
+ return `import Foundation
3654
+ import Lynx
3640
3655
 
3641
- class SceneDelegate: UIResponder, UIWindowSceneDelegate {
3642
- var window: UIWindow?
3656
+ class LynxProvider: NSObject, LynxTemplateProvider {
3657
+ func loadTemplate(withUrl url: String!, onComplete callback: LynxTemplateLoadBlock!) {
3658
+ DispatchQueue.global(qos: .background).async {
3659
+ guard let url = url,
3660
+ let bundleUrl = Bundle.main.url(forResource: url, withExtension: nil),
3661
+ let data = try? Data(contentsOf: bundleUrl) else {
3662
+ let err = NSError(domain: "LynxProvider", code: 404,
3663
+ userInfo: [NSLocalizedDescriptionKey: "Bundle not found: \\(url ?? "nil")"])
3664
+ callback?(nil, err)
3665
+ return
3666
+ }
3667
+ callback?(data, nil)
3668
+ }
3669
+ }
3670
+ }
3671
+ `;
3672
+ }
3673
+ function readTemplateOrFallback(devClientPkg, templateName, fallback, vars = {}) {
3674
+ if (devClientPkg) {
3675
+ const tplPath = path14.join(devClientPkg, "ios", "templates", templateName);
3676
+ if (fs13.existsSync(tplPath)) {
3677
+ let content = fs13.readFileSync(tplPath, "utf8");
3678
+ for (const [k, v] of Object.entries(vars)) {
3679
+ content = content.replace(new RegExp(`\\{\\{${k}\\}\\}`, "g"), v);
3680
+ }
3681
+ return content;
3682
+ }
3683
+ }
3684
+ return fallback;
3685
+ }
3686
+ function syncHostIos(opts) {
3687
+ const resolved = resolveHostPaths();
3688
+ const appName = resolved.config.ios?.appName;
3689
+ const release = opts?.release === true;
3690
+ const devClientPkg = findDevClientPackage(resolved.projectRoot);
3691
+ const useDevClient = opts?.includeDevClient ?? (!release && !!devClientPkg);
3692
+ if (!appName) {
3693
+ throw new Error('"ios.appName" must be defined in tamer.config.json');
3694
+ }
3695
+ const projectDir = path14.join(resolved.iosDir, appName);
3696
+ const infoPlistPath = path14.join(projectDir, "Info.plist");
3697
+ if (!fs13.existsSync(projectDir)) {
3698
+ throw new Error(`iOS project not found at ${projectDir}. Run \`tamer ios create\` first.`);
3699
+ }
3700
+ const pbxprojPath = path14.join(resolved.iosDir, `${appName}.xcodeproj`, "project.pbxproj");
3701
+ const baseLprojDir = path14.join(projectDir, "Base.lproj");
3702
+ const launchScreenPath = path14.join(baseLprojDir, "LaunchScreen.storyboard");
3703
+ patchInfoPlist(infoPlistPath);
3704
+ writeFile(path14.join(projectDir, "AppDelegate.swift"), getAppDelegateSwift());
3705
+ writeFile(path14.join(projectDir, "SceneDelegate.swift"), getSceneDelegateSwift());
3706
+ if (!fs13.existsSync(launchScreenPath)) {
3707
+ fs13.mkdirSync(baseLprojDir, { recursive: true });
3708
+ writeFile(launchScreenPath, getLaunchScreenStoryboard());
3709
+ addLaunchScreenToXcodeProject(pbxprojPath, appName);
3710
+ }
3711
+ addSwiftSourceToXcodeProject(pbxprojPath, appName, "SceneDelegate.swift");
3712
+ if (useDevClient) {
3713
+ const devClientPkg2 = findDevClientPackage(resolved.projectRoot);
3714
+ const segment = resolved.lynxProjectDir.split("/").filter(Boolean).pop() ?? "";
3715
+ const tplVars = { PROJECT_BUNDLE_SEGMENT: segment };
3716
+ writeFile(path14.join(projectDir, "ViewController.swift"), getDevViewControllerSwift());
3717
+ writeFile(path14.join(projectDir, "LynxProvider.swift"), getSimpleLynxProviderSwift());
3718
+ addSwiftSourceToXcodeProject(pbxprojPath, appName, "LynxProvider.swift");
3719
+ const devTPContent = readTemplateOrFallback(devClientPkg2, "DevTemplateProvider.swift", "", tplVars);
3720
+ if (devTPContent) {
3721
+ writeFile(path14.join(projectDir, "DevTemplateProvider.swift"), devTPContent);
3722
+ addSwiftSourceToXcodeProject(pbxprojPath, appName, "DevTemplateProvider.swift");
3723
+ }
3724
+ const projectVCContent = readTemplateOrFallback(devClientPkg2, "ProjectViewController.swift", "", tplVars);
3725
+ if (projectVCContent) {
3726
+ writeFile(path14.join(projectDir, "ProjectViewController.swift"), projectVCContent);
3727
+ addSwiftSourceToXcodeProject(pbxprojPath, appName, "ProjectViewController.swift");
3728
+ }
3729
+ const devCMContent = readTemplateOrFallback(devClientPkg2, "DevClientManager.swift", "", tplVars);
3730
+ if (devCMContent) {
3731
+ writeFile(path14.join(projectDir, "DevClientManager.swift"), devCMContent);
3732
+ addSwiftSourceToXcodeProject(pbxprojPath, appName, "DevClientManager.swift");
3733
+ }
3734
+ const qrContent = readTemplateOrFallback(devClientPkg2, "QRScannerViewController.swift", "", tplVars);
3735
+ if (qrContent) {
3736
+ writeFile(path14.join(projectDir, "QRScannerViewController.swift"), qrContent);
3737
+ addSwiftSourceToXcodeProject(pbxprojPath, appName, "QRScannerViewController.swift");
3738
+ }
3739
+ console.log("\u2705 Synced iOS host app (embedded dev mode) \u2014 ViewController, DevTemplateProvider, ProjectViewController, DevClientManager, QRScannerViewController");
3740
+ } else {
3741
+ writeFile(path14.join(projectDir, "ViewController.swift"), getViewControllerSwift());
3742
+ writeFile(path14.join(projectDir, "LynxProvider.swift"), getSimpleLynxProviderSwift());
3743
+ addSwiftSourceToXcodeProject(pbxprojPath, appName, "LynxProvider.swift");
3744
+ console.log("\u2705 Synced iOS host app controller files");
3745
+ }
3746
+ }
3747
+ var syncHost_default = syncHostIos;
3643
3748
 
3644
- func scene(
3645
- _ scene: UIScene,
3646
- willConnectTo session: UISceneSession,
3647
- options connectionOptions: UIScene.ConnectionOptions
3648
- ) {
3649
- guard let windowScene = scene as? UIWindowScene else { return }
3650
- window = UIWindow(windowScene: windowScene)
3651
- window?.rootViewController = ViewController()
3652
- window?.makeKeyAndVisible()
3749
+ // src/ios/autolink.ts
3750
+ var autolink2 = () => {
3751
+ let resolved;
3752
+ try {
3753
+ resolved = resolveHostPaths();
3754
+ } catch (error) {
3755
+ console.error(`\u274C Error loading configuration: ${error.message}`);
3756
+ process.exit(1);
3757
+ }
3758
+ const projectRoot = resolved.projectRoot;
3759
+ const iosProjectPath = resolved.iosDir;
3760
+ function updateGeneratedSection(filePath, newContent, startMarker, endMarker) {
3761
+ if (!fs14.existsSync(filePath)) {
3762
+ console.warn(`\u26A0\uFE0F File not found, skipping update: ${filePath}`);
3763
+ return;
3653
3764
  }
3654
- }
3765
+ let fileContent = fs14.readFileSync(filePath, "utf8");
3766
+ const escapedStartMarker = startMarker.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
3767
+ const escapedEndMarker = endMarker.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
3768
+ const regex = new RegExp(`${escapedStartMarker}[\\s\\S]*?${escapedEndMarker}`, "g");
3769
+ const replacementBlock = `${startMarker}
3770
+ ${newContent}
3771
+ ${endMarker}`;
3772
+ if (regex.test(fileContent)) {
3773
+ const firstStartIdx = fileContent.indexOf(startMarker);
3774
+ fileContent = fileContent.replace(regex, "");
3775
+ if (firstStartIdx !== -1) {
3776
+ const before = fileContent.slice(0, firstStartIdx);
3777
+ const after = fileContent.slice(firstStartIdx);
3778
+ fileContent = `${before}${replacementBlock}${after}`;
3779
+ } else {
3780
+ fileContent += `
3781
+ ${replacementBlock}
3782
+ `;
3783
+ }
3784
+ } else {
3785
+ console.warn(`\u26A0\uFE0F Could not find autolink markers in ${path15.basename(filePath)}. Appending to the end of the file.`);
3786
+ fileContent += `
3787
+ ${replacementBlock}
3655
3788
  `;
3656
- }
3657
- function getViewControllerSwift() {
3658
- return `import UIKit
3659
- import Lynx
3660
- import tamerinsets
3661
-
3662
- class ViewController: UIViewController {
3663
- private var lynxView: LynxView?
3664
-
3665
- override func viewDidLoad() {
3666
- super.viewDidLoad()
3667
- view.backgroundColor = .black
3668
- edgesForExtendedLayout = .all
3669
- extendedLayoutIncludesOpaqueBars = true
3670
- additionalSafeAreaInsets = .zero
3671
- view.insetsLayoutMarginsFromSafeArea = false
3672
- view.preservesSuperviewLayoutMargins = false
3673
- viewRespectsSystemMinimumLayoutMargins = false
3674
3789
  }
3675
-
3676
- override func viewDidLayoutSubviews() {
3677
- super.viewDidLayoutSubviews()
3678
- guard view.bounds.width > 0, view.bounds.height > 0 else { return }
3679
- if lynxView == nil {
3680
- setupLynxView()
3681
- } else {
3682
- applyFullscreenLayout(to: lynxView!)
3790
+ fs14.writeFileSync(filePath, fileContent, "utf8");
3791
+ console.log(`\u2705 Updated autolinked section in ${path15.basename(filePath)}`);
3792
+ }
3793
+ function resolvePodDirectory(pkg) {
3794
+ const configuredDir = path15.join(pkg.packagePath, pkg.config.ios?.podspecPath || ".");
3795
+ if (fs14.existsSync(configuredDir)) {
3796
+ return configuredDir;
3797
+ }
3798
+ const iosDir = path15.join(pkg.packagePath, "ios");
3799
+ if (fs14.existsSync(iosDir)) {
3800
+ const stack = [iosDir];
3801
+ while (stack.length > 0) {
3802
+ const current = stack.pop();
3803
+ try {
3804
+ const entries = fs14.readdirSync(current, { withFileTypes: true });
3805
+ const podspec = entries.find((entry) => entry.isFile() && entry.name.endsWith(".podspec"));
3806
+ if (podspec) {
3807
+ return current;
3808
+ }
3809
+ for (const entry of entries) {
3810
+ if (entry.isDirectory()) {
3811
+ stack.push(path15.join(current, entry.name));
3812
+ }
3813
+ }
3814
+ } catch {
3683
3815
  }
3816
+ }
3684
3817
  }
3685
-
3686
- override func viewSafeAreaInsetsDidChange() {
3687
- super.viewSafeAreaInsetsDidChange()
3688
- TamerInsetsModule.reRequestInsets()
3818
+ return configuredDir;
3819
+ }
3820
+ function resolvePodName(pkg) {
3821
+ const fullPodspecDir = resolvePodDirectory(pkg);
3822
+ if (fs14.existsSync(fullPodspecDir)) {
3823
+ try {
3824
+ const files = fs14.readdirSync(fullPodspecDir);
3825
+ const podspecFile = files.find((f) => f.endsWith(".podspec"));
3826
+ if (podspecFile) return podspecFile.replace(".podspec", "");
3827
+ } catch {
3828
+ }
3689
3829
  }
3690
-
3691
- override var preferredStatusBarStyle: UIStatusBarStyle { .lightContent }
3692
-
3693
- private func buildLynxView() -> LynxView {
3694
- let bounds = view.bounds
3695
- let lv = LynxView { builder in
3696
- builder.config = LynxConfig(provider: LynxProvider())
3697
- builder.screenSize = bounds.size
3698
- builder.fontScale = 1.0
3699
- }
3700
- lv.autoresizingMask = [.flexibleWidth, .flexibleHeight]
3701
- lv.insetsLayoutMarginsFromSafeArea = false
3702
- lv.preservesSuperviewLayoutMargins = false
3703
- applyFullscreenLayout(to: lv)
3704
- return lv
3830
+ return pkg.name.split("/").pop().replace(/-/g, "");
3831
+ }
3832
+ function updatePodfile(packages) {
3833
+ const podfilePath = path15.join(iosProjectPath, "Podfile");
3834
+ let scriptContent = ` # This section is automatically generated by Tamer4Lynx.
3835
+ # Manual edits will be overwritten.`;
3836
+ const iosPackages = packages.filter((p) => p.config.ios);
3837
+ if (iosPackages.length > 0) {
3838
+ iosPackages.forEach((pkg) => {
3839
+ const relativePath = path15.relative(iosProjectPath, resolvePodDirectory(pkg));
3840
+ const podName = resolvePodName(pkg);
3841
+ scriptContent += `
3842
+ pod '${podName}', :path => '${relativePath}'`;
3843
+ });
3844
+ } else {
3845
+ scriptContent += `
3846
+ # No native modules found by Tamer4Lynx autolinker.`;
3705
3847
  }
3848
+ updateGeneratedSection(podfilePath, scriptContent.trim(), "# GENERATED AUTOLINK DEPENDENCIES START", "# GENERATED AUTOLINK DEPENDENCIES END");
3849
+ }
3850
+ function ensureXElementPod() {
3851
+ const podfilePath = path15.join(iosProjectPath, "Podfile");
3852
+ if (!fs14.existsSync(podfilePath)) return;
3853
+ let content = fs14.readFileSync(podfilePath, "utf8");
3854
+ if (content.includes("pod 'XElement'")) return;
3855
+ const lynxVersionMatch = content.match(/pod\s+'Lynx',\s*'([^']+)'/);
3856
+ const lynxVersion = lynxVersionMatch?.[1] ?? "3.6.0";
3857
+ const xelementBlock = ` pod 'XElement', '${lynxVersion}'
3706
3858
 
3707
- private func setupLynxView() {
3708
- let lv = buildLynxView()
3709
- view.addSubview(lv)
3710
- lv.loadTemplate(fromURL: "main.lynx.bundle", initData: nil)
3711
- self.lynxView = lv
3859
+ `;
3860
+ if (content.includes("# GENERATED AUTOLINK DEPENDENCIES START")) {
3861
+ content = content.replace(
3862
+ /(# GENERATED AUTOLINK DEPENDENCIES START)/,
3863
+ `${xelementBlock}$1`
3864
+ );
3865
+ } else {
3866
+ const insertAfter = /pod\s+'LynxService'[^\n]*(?:\n\s*'[^']*',?\s*)*/;
3867
+ const serviceMatch = content.match(insertAfter);
3868
+ if (serviceMatch) {
3869
+ const idx = serviceMatch.index + serviceMatch[0].length;
3870
+ content = content.slice(0, idx) + `
3871
+ pod 'XElement', '${lynxVersion}'` + content.slice(idx);
3872
+ } else {
3873
+ content += `
3874
+ pod 'XElement', '${lynxVersion}'
3875
+ `;
3876
+ }
3712
3877
  }
3713
-
3714
- private func applyFullscreenLayout(to lynxView: LynxView) {
3715
- let bounds = view.bounds
3716
- let size = bounds.size
3717
- lynxView.frame = bounds
3718
- lynxView.updateScreenMetrics(withWidth: size.width, height: size.height)
3719
- lynxView.updateViewport(withPreferredLayoutWidth: size.width, preferredLayoutHeight: size.height, needLayout: true)
3720
- lynxView.preferredLayoutWidth = size.width
3721
- lynxView.preferredLayoutHeight = size.height
3722
- lynxView.layoutWidthMode = .exact
3723
- lynxView.layoutHeightMode = .exact
3878
+ fs14.writeFileSync(podfilePath, content, "utf8");
3879
+ console.log(`\u2705 Added XElement pod (v${lynxVersion}) to Podfile`);
3880
+ }
3881
+ function ensureLynxPatchInPodfile() {
3882
+ const podfilePath = path15.join(iosProjectPath, "Podfile");
3883
+ if (!fs14.existsSync(podfilePath)) return;
3884
+ let content = fs14.readFileSync(podfilePath, "utf8");
3885
+ if (content.includes("content.gsub(/\\btypeof\\(/, '__typeof__(')")) return;
3886
+ const patch = `
3887
+ Dir.glob(File.join(installer.sandbox.root, 'Lynx/platform/darwin/**/*.{m,mm}')).each do |lynx_source|
3888
+ next unless File.file?(lynx_source)
3889
+ content = File.read(lynx_source)
3890
+ next unless content.match?(/\\btypeof\\(/)
3891
+ File.chmod(0644, lynx_source) rescue nil
3892
+ File.write(lynx_source, content.gsub(/\\btypeof\\(/, '__typeof__('))
3893
+ end`;
3894
+ content = content.replace(/(\n end\s*\n)(end\s*)$/, `$1${patch}
3895
+ $2`);
3896
+ fs14.writeFileSync(podfilePath, content, "utf8");
3897
+ console.log("\u2705 Added Lynx typeof patch to Podfile post_install.");
3898
+ }
3899
+ function ensurePodBuildSettings() {
3900
+ const podfilePath = path15.join(iosProjectPath, "Podfile");
3901
+ if (!fs14.existsSync(podfilePath)) return;
3902
+ let content = fs14.readFileSync(podfilePath, "utf8");
3903
+ let changed = false;
3904
+ if (!content.includes("CLANG_ENABLE_EXPLICIT_MODULES")) {
3905
+ content = content.replace(
3906
+ /config\.build_settings\['IPHONEOS_DEPLOYMENT_TARGET'\]\s*=\s*'[^']*'/,
3907
+ `$&
3908
+ config.build_settings['CLANG_ENABLE_EXPLICIT_MODULES'] = 'NO'
3909
+ config.build_settings['ONLY_ACTIVE_ARCH'] = 'YES'`
3910
+ );
3911
+ changed = true;
3724
3912
  }
3725
- }
3726
- `;
3727
- }
3728
- function getDevViewControllerSwift() {
3729
- return `import UIKit
3730
- import Lynx
3731
- import tamerdevclient
3732
- import tamerinsets
3733
-
3734
- class ViewController: UIViewController {
3735
- private var lynxView: LynxView?
3736
-
3737
- override func viewDidLoad() {
3738
- super.viewDidLoad()
3739
- view.backgroundColor = .black
3740
- edgesForExtendedLayout = .all
3741
- extendedLayoutIncludesOpaqueBars = true
3742
- additionalSafeAreaInsets = .zero
3743
- view.insetsLayoutMarginsFromSafeArea = false
3744
- view.preservesSuperviewLayoutMargins = false
3745
- viewRespectsSystemMinimumLayoutMargins = false
3746
- setupLynxView()
3747
- setupDevClientModule()
3913
+ if (!content.includes("gsub('-Werror'")) {
3914
+ const xcconfigStrip = `
3915
+ Dir.glob(File.join(installer.sandbox.root, 'Target Support Files', 'Lynx', '*.xcconfig')).each do |xcconfig_path|
3916
+ next unless File.file?(xcconfig_path)
3917
+ content = File.read(xcconfig_path)
3918
+ next unless content.include?('-Werror')
3919
+ File.write(xcconfig_path, content.gsub('-Werror', ''))
3920
+ end`;
3921
+ content = content.replace(
3922
+ /(Dir\.glob.*?Lynx\/platform\/darwin)/s,
3923
+ `${xcconfigStrip}
3924
+ $1`
3925
+ );
3926
+ changed = true;
3748
3927
  }
3749
-
3750
- override func viewDidLayoutSubviews() {
3751
- super.viewDidLayoutSubviews()
3752
- if let lynxView = lynxView {
3753
- applyFullscreenLayout(to: lynxView)
3754
- }
3928
+ if (!content.includes("target.name == 'PrimJS'") && content.includes("target.name == 'Lynx'")) {
3929
+ const primjsBlock = `
3930
+ if target.name == 'PrimJS'
3931
+ target.build_configurations.each do |config|
3932
+ config.build_settings['OTHER_CFLAGS'] = "$(inherited) -Wno-macro-redefined"
3933
+ config.build_settings['OTHER_CPLUSPLUSFLAGS'] = "$(inherited) -Wno-macro-redefined"
3934
+ end
3935
+ end`;
3936
+ content = content.replace(
3937
+ /( end)\n( end)\n( Dir\.glob\(File\.join\(installer\.sandbox\.root,\s*'Target Support Files',\s*'Lynx')/,
3938
+ `$1${primjsBlock}
3939
+ $2
3940
+ $3`
3941
+ );
3942
+ changed = true;
3755
3943
  }
3756
-
3757
- override func viewSafeAreaInsetsDidChange() {
3758
- super.viewSafeAreaInsetsDidChange()
3759
- TamerInsetsModule.reRequestInsets()
3944
+ if (changed) {
3945
+ fs14.writeFileSync(podfilePath, content, "utf8");
3946
+ console.log("\u2705 Added Xcode compatibility build settings to Podfile post_install.");
3760
3947
  }
3761
-
3762
- override var preferredStatusBarStyle: UIStatusBarStyle { .lightContent }
3763
-
3764
- private func setupLynxView() {
3765
- let size = fullscreenBounds().size
3766
- let lv = LynxView { builder in
3767
- builder.config = LynxConfig(provider: DevTemplateProvider())
3768
- builder.screenSize = size
3769
- builder.fontScale = 1.0
3948
+ }
3949
+ function updateLynxInitProcessor(packages) {
3950
+ const appNameFromConfig = resolved.config.ios?.appName;
3951
+ const candidatePaths = [];
3952
+ if (appNameFromConfig) {
3953
+ candidatePaths.push(path15.join(iosProjectPath, appNameFromConfig, "LynxInitProcessor.swift"));
3954
+ }
3955
+ candidatePaths.push(path15.join(iosProjectPath, "LynxInitProcessor.swift"));
3956
+ const found = candidatePaths.find((p) => fs14.existsSync(p));
3957
+ const lynxInitPath = found ?? candidatePaths[0];
3958
+ const iosPackages = packages.filter((p) => getIosModuleClassNames(p.config.ios).length > 0 || Object.keys(getIosElements(p.config.ios)).length > 0);
3959
+ const seenModules = /* @__PURE__ */ new Set();
3960
+ const seenElements = /* @__PURE__ */ new Set();
3961
+ const packagesWithContributions = /* @__PURE__ */ new Set();
3962
+ for (const pkg of iosPackages) {
3963
+ let hasUnique = false;
3964
+ for (const cls of getIosModuleClassNames(pkg.config.ios)) {
3965
+ if (!seenModules.has(cls)) {
3966
+ seenModules.add(cls);
3967
+ hasUnique = true;
3968
+ } else {
3969
+ console.warn(`\u26A0\uFE0F Skipping duplicate module "${cls}" from ${pkg.name} (already registered by another package)`);
3770
3970
  }
3771
- lv.autoresizingMask = [.flexibleWidth, .flexibleHeight]
3772
- lv.insetsLayoutMarginsFromSafeArea = false
3773
- lv.preservesSuperviewLayoutMargins = false
3774
- view.addSubview(lv)
3775
- applyFullscreenLayout(to: lv)
3776
- lv.loadTemplate(fromURL: "dev-client.lynx.bundle", initData: nil)
3777
- DispatchQueue.main.asyncAfter(deadline: .now() + 0.25) { [weak self, weak lv] in
3778
- guard let self, let lv else { return }
3779
- self.applyFullscreenLayout(to: lv)
3971
+ }
3972
+ for (const tag of Object.keys(getIosElements(pkg.config.ios))) {
3973
+ if (!seenElements.has(tag)) {
3974
+ seenElements.add(tag);
3975
+ hasUnique = true;
3976
+ } else {
3977
+ console.warn(`\u26A0\uFE0F Skipping duplicate element "${tag}" from ${pkg.name} (already registered by another package)`);
3780
3978
  }
3781
- self.lynxView = lv
3979
+ }
3980
+ if (hasUnique) packagesWithContributions.add(pkg);
3782
3981
  }
3982
+ const importPackages = iosPackages.filter((p) => packagesWithContributions.has(p));
3983
+ function updateImportsSection(filePath, pkgs) {
3984
+ const startMarker = "// GENERATED IMPORTS START";
3985
+ const endMarker = "// GENERATED IMPORTS END";
3986
+ if (pkgs.length === 0) {
3987
+ const placeholder = "// No native imports found by Tamer4Lynx autolinker.";
3988
+ updateGeneratedSection(filePath, placeholder, startMarker, endMarker);
3989
+ return;
3990
+ }
3991
+ const imports = pkgs.map((pkg) => {
3992
+ const podName = resolvePodName(pkg);
3993
+ return `import ${podName}`;
3994
+ }).join("\n");
3995
+ const fileContent = fs14.readFileSync(filePath, "utf8");
3996
+ if (fileContent.indexOf(startMarker) !== -1) {
3997
+ updateGeneratedSection(filePath, imports, startMarker, endMarker);
3998
+ return;
3999
+ }
4000
+ const importRegex = /^(import\s+[^\r\n]+)\r?\n/gm;
4001
+ let match = null;
4002
+ let lastMatchEnd = -1;
4003
+ while ((match = importRegex.exec(fileContent)) !== null) {
4004
+ lastMatchEnd = importRegex.lastIndex;
4005
+ }
4006
+ const block = `${startMarker}
4007
+ ${imports}
4008
+ ${endMarker}`;
4009
+ let newContent;
4010
+ if (lastMatchEnd !== -1) {
4011
+ const before = fileContent.slice(0, lastMatchEnd);
4012
+ const after = fileContent.slice(lastMatchEnd);
4013
+ newContent = `${before}
4014
+ ${block}
4015
+ ${after}`;
4016
+ } else {
4017
+ const foundationIdx = fileContent.indexOf("import Foundation");
4018
+ if (foundationIdx !== -1) {
4019
+ const lineEnd = fileContent.indexOf("\n", foundationIdx);
4020
+ const insertPos = lineEnd !== -1 ? lineEnd + 1 : foundationIdx + "import Foundation".length;
4021
+ const before = fileContent.slice(0, insertPos);
4022
+ const after = fileContent.slice(insertPos);
4023
+ newContent = `${before}
4024
+ ${block}
4025
+ ${after}`;
4026
+ } else {
4027
+ newContent = `${block}
3783
4028
 
3784
- private func applyFullscreenLayout(to lynxView: LynxView) {
3785
- let bounds = fullscreenBounds()
3786
- let size = bounds.size
3787
- lynxView.frame = bounds
3788
- lynxView.updateScreenMetrics(withWidth: size.width, height: size.height)
3789
- lynxView.updateViewport(withPreferredLayoutWidth: size.width, preferredLayoutHeight: size.height, needLayout: true)
3790
- lynxView.preferredLayoutWidth = size.width
3791
- lynxView.preferredLayoutHeight = size.height
3792
- lynxView.layoutWidthMode = .exact
3793
- lynxView.layoutHeightMode = .exact
4029
+ ${fileContent}`;
4030
+ }
4031
+ }
4032
+ fs14.writeFileSync(filePath, newContent, "utf8");
4033
+ console.log(`\u2705 Updated imports in ${path15.basename(filePath)}`);
3794
4034
  }
3795
-
3796
- private func fullscreenBounds() -> CGRect {
3797
- let bounds = view.bounds
3798
- if bounds.width > 0, bounds.height > 0 { return bounds }
3799
- return UIScreen.main.bounds
4035
+ updateImportsSection(lynxInitPath, importPackages);
4036
+ if (importPackages.length === 0) {
4037
+ const placeholder = " // No native modules found by Tamer4Lynx autolinker.";
4038
+ updateGeneratedSection(lynxInitPath, placeholder, "// GENERATED AUTOLINK START", "// GENERATED AUTOLINK END");
4039
+ } else {
4040
+ const seenModules2 = /* @__PURE__ */ new Set();
4041
+ const seenElements2 = /* @__PURE__ */ new Set();
4042
+ const blocks = importPackages.flatMap((pkg) => {
4043
+ const classNames = getIosModuleClassNames(pkg.config.ios);
4044
+ const moduleBlocks = classNames.filter((cls) => {
4045
+ if (seenModules2.has(cls)) return false;
4046
+ seenModules2.add(cls);
4047
+ return true;
4048
+ }).map((classNameRaw) => [
4049
+ ` // Register module from package: ${pkg.name}`,
4050
+ ` globalConfig.register(${classNameRaw}.self)`
4051
+ ].join("\n"));
4052
+ const elementBlocks = Object.entries(getIosElements(pkg.config.ios)).filter(([tagName]) => {
4053
+ if (seenElements2.has(tagName)) return false;
4054
+ seenElements2.add(tagName);
4055
+ return true;
4056
+ }).map(([tagName, classNameRaw]) => [
4057
+ ` // Register element from package: ${pkg.name}`,
4058
+ ` globalConfig.registerUI(${classNameRaw}.self, withName: "${tagName}")`
4059
+ ].join("\n"));
4060
+ return [...moduleBlocks, ...elementBlocks];
4061
+ });
4062
+ const content = blocks.join("\n\n");
4063
+ updateGeneratedSection(lynxInitPath, content, "// GENERATED AUTOLINK START", "// GENERATED AUTOLINK END");
4064
+ }
4065
+ const hasDevClient = packages.some((p) => p.name === "@tamer4lynx/tamer-dev-client");
4066
+ const androidNames = getDedupedAndroidModuleClassNames(packages);
4067
+ let devClientSupportedBody;
4068
+ if (hasDevClient && androidNames.length > 0) {
4069
+ devClientSupportedBody = ` DevClientModule.attachSupportedModuleClassNames([
4070
+ ${androidNames.map((n) => ` "${n.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`).join(",\n")}
4071
+ ])`;
4072
+ } else if (hasDevClient) {
4073
+ devClientSupportedBody = " DevClientModule.attachSupportedModuleClassNames([])";
4074
+ } else {
4075
+ devClientSupportedBody = " // @tamer4lynx/tamer-dev-client not linked";
3800
4076
  }
3801
-
3802
- private func setupDevClientModule() {
3803
- DevClientModule.presentQRScanner = { [weak self] completion in
3804
- let scanner = QRScannerViewController()
3805
- scanner.onResult = { url in
3806
- scanner.dismiss(animated: true) { completion(url) }
3807
- }
3808
- scanner.modalPresentationStyle = .fullScreen
3809
- self?.present(scanner, animated: true)
3810
- }
3811
-
3812
- DevClientModule.reloadProjectHandler = { [weak self] in
3813
- guard let self = self else { return }
3814
- let projectVC = ProjectViewController()
3815
- projectVC.modalPresentationStyle = .fullScreen
3816
- self.present(projectVC, animated: true)
3817
- }
4077
+ if (fs14.readFileSync(lynxInitPath, "utf8").includes("GENERATED DEV_CLIENT_SUPPORTED START")) {
4078
+ updateGeneratedSection(lynxInitPath, devClientSupportedBody, "// GENERATED DEV_CLIENT_SUPPORTED START", "// GENERATED DEV_CLIENT_SUPPORTED END");
3818
4079
  }
3819
- }
3820
- `;
3821
- }
3822
- function patchInfoPlist(infoPlistPath) {
3823
- if (!fs13.existsSync(infoPlistPath)) return;
3824
- let content = fs13.readFileSync(infoPlistPath, "utf8");
3825
- content = content.replace(/\s*<key>UIMainStoryboardFile<\/key>\s*<string>[^<]*<\/string>/g, "");
3826
- if (!content.includes("UILaunchStoryboardName")) {
3827
- content = content.replace("</dict>\n</plist>", ` <key>UILaunchStoryboardName</key>
3828
- <string>LaunchScreen</string>
3829
- </dict>
3830
- </plist>`);
3831
- console.log("\u2705 Added UILaunchStoryboardName to Info.plist");
3832
4080
  }
3833
- if (!content.includes("UIApplicationSceneManifest")) {
3834
- const sceneManifest = ` <key>UIApplicationSceneManifest</key>
3835
- <dict>
3836
- <key>UIApplicationSupportsMultipleScenes</key>
3837
- <false/>
3838
- <key>UISceneConfigurations</key>
3839
- <dict>
3840
- <key>UIWindowSceneSessionRoleApplication</key>
3841
- <array>
3842
- <dict>
3843
- <key>UISceneConfigurationName</key>
3844
- <string>Default Configuration</string>
3845
- <key>UISceneDelegateClassName</key>
3846
- <string>$(PRODUCT_MODULE_NAME).SceneDelegate</string>
3847
- </dict>
3848
- </array>
3849
- </dict>
3850
- </dict>`;
3851
- content = content.replace("</dict>\n</plist>", `${sceneManifest}
3852
- </dict>
3853
- </plist>`);
3854
- console.log("\u2705 Added UIApplicationSceneManifest to Info.plist");
4081
+ function findInfoPlist() {
4082
+ const appNameFromConfig = resolved.config.ios?.appName;
4083
+ const candidates = [];
4084
+ if (appNameFromConfig) {
4085
+ candidates.push(path15.join(iosProjectPath, appNameFromConfig, "Info.plist"));
4086
+ }
4087
+ candidates.push(path15.join(iosProjectPath, "Info.plist"));
4088
+ return candidates.find((p) => fs14.existsSync(p)) ?? null;
3855
4089
  }
3856
- fs13.writeFileSync(infoPlistPath, content, "utf8");
3857
- }
3858
- function getSimpleLynxProviderSwift() {
3859
- return `import Foundation
3860
- import Lynx
3861
-
3862
- class LynxProvider: NSObject, LynxTemplateProvider {
3863
- func loadTemplate(withUrl url: String!, onComplete callback: LynxTemplateLoadBlock!) {
3864
- DispatchQueue.global(qos: .background).async {
3865
- guard let url = url,
3866
- let bundleUrl = Bundle.main.url(forResource: url, withExtension: nil),
3867
- let data = try? Data(contentsOf: bundleUrl) else {
3868
- let err = NSError(domain: "LynxProvider", code: 404,
3869
- userInfo: [NSLocalizedDescriptionKey: "Bundle not found: \\(url ?? "nil")"])
3870
- callback?(nil, err)
3871
- return
3872
- }
3873
- callback?(data, nil)
4090
+ function readPlistXml(plistPath) {
4091
+ return fs14.readFileSync(plistPath, "utf8");
4092
+ }
4093
+ function syncInfoPlistPermissions(packages) {
4094
+ const plistPath = findInfoPlist();
4095
+ if (!plistPath) return;
4096
+ const allPermissions = {};
4097
+ for (const pkg of packages) {
4098
+ const iosPerms = pkg.config.ios?.iosPermissions;
4099
+ if (iosPerms) {
4100
+ for (const [key, desc] of Object.entries(iosPerms)) {
4101
+ if (!allPermissions[key]) {
4102
+ allPermissions[key] = desc;
4103
+ }
3874
4104
  }
4105
+ }
3875
4106
  }
3876
- }
4107
+ if (Object.keys(allPermissions).length === 0) return;
4108
+ let plist = readPlistXml(plistPath);
4109
+ let added = 0;
4110
+ for (const [key, desc] of Object.entries(allPermissions)) {
4111
+ if (plist.includes(`<key>${key}</key>`)) continue;
4112
+ const insertion = ` <key>${key}</key>
4113
+ <string>${desc}</string>
3877
4114
  `;
3878
- }
3879
- function readTemplateOrFallback(devClientPkg, templateName, fallback, vars = {}) {
3880
- if (devClientPkg) {
3881
- const tplPath = path14.join(devClientPkg, "ios", "templates", templateName);
3882
- if (fs13.existsSync(tplPath)) {
3883
- let content = fs13.readFileSync(tplPath, "utf8");
3884
- for (const [k, v] of Object.entries(vars)) {
3885
- content = content.replace(new RegExp(`\\{\\{${k}\\}\\}`, "g"), v);
3886
- }
3887
- return content;
4115
+ plist = plist.replace(
4116
+ /(<\/dict>\s*<\/plist>)/,
4117
+ `${insertion}$1`
4118
+ );
4119
+ added++;
4120
+ }
4121
+ if (added > 0) {
4122
+ fs14.writeFileSync(plistPath, plist, "utf8");
4123
+ console.log(`\u2705 Synced ${added} Info.plist permission description(s)`);
3888
4124
  }
3889
4125
  }
3890
- return fallback;
3891
- }
3892
- function syncHostIos(opts) {
3893
- const resolved = resolveHostPaths();
3894
- const appName = resolved.config.ios?.appName;
3895
- const release = opts?.release === true;
3896
- const devClientPkg = findDevClientPackage(resolved.projectRoot);
3897
- const useDevClient = opts?.includeDevClient ?? (!release && !!devClientPkg);
3898
- if (!appName) {
3899
- throw new Error('"ios.appName" must be defined in tamer.config.json');
3900
- }
3901
- const projectDir = path14.join(resolved.iosDir, appName);
3902
- const infoPlistPath = path14.join(projectDir, "Info.plist");
3903
- if (!fs13.existsSync(projectDir)) {
3904
- throw new Error(`iOS project not found at ${projectDir}. Run \`tamer ios create\` first.`);
3905
- }
3906
- const pbxprojPath = path14.join(resolved.iosDir, `${appName}.xcodeproj`, "project.pbxproj");
3907
- const baseLprojDir = path14.join(projectDir, "Base.lproj");
3908
- const launchScreenPath = path14.join(baseLprojDir, "LaunchScreen.storyboard");
3909
- patchInfoPlist(infoPlistPath);
3910
- writeFile(path14.join(projectDir, "AppDelegate.swift"), getAppDelegateSwift());
3911
- writeFile(path14.join(projectDir, "SceneDelegate.swift"), getSceneDelegateSwift());
3912
- if (!fs13.existsSync(launchScreenPath)) {
3913
- fs13.mkdirSync(baseLprojDir, { recursive: true });
3914
- writeFile(launchScreenPath, getLaunchScreenStoryboard());
3915
- addLaunchScreenToXcodeProject(pbxprojPath, appName);
4126
+ function syncInfoPlistUrlSchemes() {
4127
+ const urlSchemes = resolved.config.ios?.urlSchemes;
4128
+ if (!urlSchemes || urlSchemes.length === 0) return;
4129
+ const plistPath = findInfoPlist();
4130
+ if (!plistPath) return;
4131
+ let plist = readPlistXml(plistPath);
4132
+ const schemesXml = urlSchemes.map((s) => {
4133
+ const role = s.role ?? "Editor";
4134
+ return ` <dict>
4135
+ <key>CFBundleTypeRole</key>
4136
+ <string>${role}</string>
4137
+ <key>CFBundleURLSchemes</key>
4138
+ <array>
4139
+ <string>${s.scheme}</string>
4140
+ </array>
4141
+ </dict>`;
4142
+ }).join("\n");
4143
+ const generatedBlock = ` <key>CFBundleURLTypes</key>
4144
+ <array>
4145
+ <!-- GENERATED URL SCHEMES START -->
4146
+ ${schemesXml}
4147
+ <!-- GENERATED URL SCHEMES END -->
4148
+ </array>`;
4149
+ const startMarker = "<!-- GENERATED URL SCHEMES START -->";
4150
+ const endMarker = "<!-- GENERATED URL SCHEMES END -->";
4151
+ if (plist.includes(startMarker) && plist.includes(endMarker)) {
4152
+ const regex = new RegExp(
4153
+ `${startMarker.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}[\\s\\S]*?${endMarker.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}`,
4154
+ "g"
4155
+ );
4156
+ plist = plist.replace(regex, `${startMarker}
4157
+ ${schemesXml}
4158
+ ${endMarker}`);
4159
+ } else if (plist.includes("<key>CFBundleURLTypes</key>")) {
4160
+ console.log("\u2139\uFE0F CFBundleURLTypes exists but has no generated markers. Skipping URL scheme sync.");
4161
+ return;
4162
+ } else {
4163
+ plist = plist.replace(
4164
+ /(<\/dict>\s*<\/plist>)/,
4165
+ `${generatedBlock}
4166
+ $1`
4167
+ );
4168
+ }
4169
+ fs14.writeFileSync(plistPath, plist, "utf8");
4170
+ console.log(`\u2705 Synced ${urlSchemes.length} iOS URL scheme(s) into Info.plist`);
3916
4171
  }
3917
- addSwiftSourceToXcodeProject(pbxprojPath, appName, "SceneDelegate.swift");
3918
- if (useDevClient) {
3919
- const devClientPkg2 = findDevClientPackage(resolved.projectRoot);
3920
- const segment = resolved.lynxProjectDir.split("/").filter(Boolean).pop() ?? "";
3921
- const tplVars = { PROJECT_BUNDLE_SEGMENT: segment };
3922
- writeFile(path14.join(projectDir, "ViewController.swift"), getDevViewControllerSwift());
3923
- writeFile(path14.join(projectDir, "LynxProvider.swift"), getSimpleLynxProviderSwift());
3924
- addSwiftSourceToXcodeProject(pbxprojPath, appName, "LynxProvider.swift");
3925
- const devTPContent = readTemplateOrFallback(devClientPkg2, "DevTemplateProvider.swift", "", tplVars);
3926
- if (devTPContent) {
3927
- writeFile(path14.join(projectDir, "DevTemplateProvider.swift"), devTPContent);
3928
- addSwiftSourceToXcodeProject(pbxprojPath, appName, "DevTemplateProvider.swift");
4172
+ function runPodInstall(forcePath) {
4173
+ const podfilePath = forcePath ?? path15.join(iosProjectPath, "Podfile");
4174
+ if (!fs14.existsSync(podfilePath)) {
4175
+ console.log("\u2139\uFE0F No Podfile found in ios directory; skipping `pod install`.");
4176
+ return;
3929
4177
  }
3930
- const projectVCContent = readTemplateOrFallback(devClientPkg2, "ProjectViewController.swift", "", tplVars);
3931
- if (projectVCContent) {
3932
- writeFile(path14.join(projectDir, "ProjectViewController.swift"), projectVCContent);
3933
- addSwiftSourceToXcodeProject(pbxprojPath, appName, "ProjectViewController.swift");
4178
+ const cwd = path15.dirname(podfilePath);
4179
+ try {
4180
+ console.log(`\u2139\uFE0F Running \`pod install\` in ${cwd}...`);
4181
+ try {
4182
+ execSync6("pod install", { cwd, stdio: "inherit" });
4183
+ } catch {
4184
+ console.log("\u2139\uFE0F Retrying `pod install` with repo update...");
4185
+ execSync6("pod install --repo-update", { cwd, stdio: "inherit" });
4186
+ }
4187
+ console.log("\u2705 `pod install` completed successfully.");
4188
+ } catch (e) {
4189
+ console.warn(`\u26A0\uFE0F 'pod install' failed: ${e.message}`);
4190
+ console.log("\u26A0\uFE0F You can run `pod install` manually in the ios directory.");
3934
4191
  }
3935
- const devCMContent = readTemplateOrFallback(devClientPkg2, "DevClientManager.swift", "", tplVars);
3936
- if (devCMContent) {
3937
- writeFile(path14.join(projectDir, "DevClientManager.swift"), devCMContent);
3938
- addSwiftSourceToXcodeProject(pbxprojPath, appName, "DevClientManager.swift");
4192
+ }
4193
+ function run() {
4194
+ console.log("\u{1F50E} Finding Lynx extension packages (lynx.ext.json / tamer.json)...");
4195
+ const packages = discoverModules(projectRoot).filter((p) => p.config.ios);
4196
+ if (packages.length > 0) {
4197
+ console.log(`Found ${packages.length} package(s): ${packages.map((p) => p.name).join(", ")}`);
4198
+ } else {
4199
+ console.log("\u2139\uFE0F No Tamer4Lynx native packages found.");
3939
4200
  }
3940
- const qrContent = readTemplateOrFallback(devClientPkg2, "QRScannerViewController.swift", "", tplVars);
3941
- if (qrContent) {
3942
- writeFile(path14.join(projectDir, "QRScannerViewController.swift"), qrContent);
3943
- addSwiftSourceToXcodeProject(pbxprojPath, appName, "QRScannerViewController.swift");
4201
+ updatePodfile(packages);
4202
+ ensureXElementPod();
4203
+ ensureLynxPatchInPodfile();
4204
+ ensurePodBuildSettings();
4205
+ updateLynxInitProcessor(packages);
4206
+ writeHostNativeModulesManifest();
4207
+ syncInfoPlistPermissions(packages);
4208
+ syncInfoPlistUrlSchemes();
4209
+ const appNameFromConfig = resolved.config.ios?.appName;
4210
+ if (appNameFromConfig) {
4211
+ const appPodfile = path15.join(iosProjectPath, appNameFromConfig, "Podfile");
4212
+ if (fs14.existsSync(appPodfile)) {
4213
+ runPodInstall(appPodfile);
4214
+ console.log("\u2728 Autolinking complete for iOS.");
4215
+ return;
4216
+ }
3944
4217
  }
3945
- console.log("\u2705 Synced iOS host app (embedded dev mode) \u2014 ViewController, DevTemplateProvider, ProjectViewController, DevClientManager, QRScannerViewController");
3946
- } else {
3947
- writeFile(path14.join(projectDir, "ViewController.swift"), getViewControllerSwift());
3948
- writeFile(path14.join(projectDir, "LynxProvider.swift"), getSimpleLynxProviderSwift());
3949
- addSwiftSourceToXcodeProject(pbxprojPath, appName, "LynxProvider.swift");
3950
- console.log("\u2705 Synced iOS host app controller files");
4218
+ runPodInstall();
4219
+ console.log("\u2728 Autolinking complete for iOS.");
3951
4220
  }
3952
- }
3953
- var syncHost_default = syncHostIos;
4221
+ function writeHostNativeModulesManifest() {
4222
+ const allPkgs = discoverModules(projectRoot);
4223
+ const hasDevClient = allPkgs.some((p) => p.name === "@tamer4lynx/tamer-dev-client");
4224
+ const appFolder = resolved.config.ios?.appName;
4225
+ if (!hasDevClient || !appFolder) return;
4226
+ const androidNames = getDedupedAndroidModuleClassNames(allPkgs);
4227
+ const appDir = path15.join(iosProjectPath, appFolder);
4228
+ fs14.mkdirSync(appDir, { recursive: true });
4229
+ const manifestPath = path15.join(appDir, TAMER_HOST_NATIVE_MODULES_FILENAME);
4230
+ fs14.writeFileSync(manifestPath, buildHostNativeModulesManifestJson(androidNames), "utf8");
4231
+ console.log(`\u2705 Wrote ${TAMER_HOST_NATIVE_MODULES_FILENAME} (native module ids for dev-client checks)`);
4232
+ const pbxprojPath = path15.join(iosProjectPath, `${appFolder}.xcodeproj`, "project.pbxproj");
4233
+ if (fs14.existsSync(pbxprojPath)) {
4234
+ addResourceToXcodeProject(pbxprojPath, appFolder, TAMER_HOST_NATIVE_MODULES_FILENAME);
4235
+ }
4236
+ }
4237
+ run();
4238
+ };
4239
+ var autolink_default2 = autolink2;
3954
4240
 
3955
4241
  // src/ios/bundle.ts
4242
+ import fs15 from "fs";
4243
+ import path16 from "path";
4244
+ import { execSync as execSync7 } from "child_process";
3956
4245
  function bundleAndDeploy2(opts = {}) {
3957
4246
  const release = opts.release === true;
3958
4247
  let resolved;
@@ -3969,53 +4258,60 @@ function bundleAndDeploy2(opts = {}) {
3969
4258
  const includeDevClient = !release && !!devClientPkg;
3970
4259
  const appName = resolved.config.ios.appName;
3971
4260
  const sourceBundlePath = resolved.lynxBundlePath;
3972
- const destinationDir = path15.join(resolved.iosDir, appName);
3973
- const destinationBundlePath = path15.join(destinationDir, resolved.lynxBundleFile);
4261
+ const destinationDir = path16.join(resolved.iosDir, appName);
4262
+ const destinationBundlePath = path16.join(destinationDir, resolved.lynxBundleFile);
3974
4263
  syncHost_default({ release, includeDevClient });
3975
4264
  autolink_default2();
4265
+ const iconPaths = resolveIconPaths(resolved.projectRoot, resolved.config);
4266
+ if (iconPaths) {
4267
+ const appIconDir = path16.join(destinationDir, "Assets.xcassets", "AppIcon.appiconset");
4268
+ if (applyIosAppIconAssets(appIconDir, iconPaths)) {
4269
+ console.log("\u2705 Synced iOS AppIcon from tamer.config.json");
4270
+ }
4271
+ }
3976
4272
  try {
3977
4273
  console.log("\u{1F4E6} Building Lynx bundle...");
3978
- execSync6("npm run build", { stdio: "inherit", cwd: resolved.lynxProjectDir });
4274
+ execSync7("npm run build", { stdio: "inherit", cwd: resolved.lynxProjectDir });
3979
4275
  console.log("\u2705 Build completed successfully.");
3980
4276
  } catch (error) {
3981
4277
  console.error("\u274C Build process failed. Please check the errors above.");
3982
4278
  process.exit(1);
3983
4279
  }
3984
4280
  try {
3985
- if (!fs14.existsSync(sourceBundlePath)) {
4281
+ if (!fs15.existsSync(sourceBundlePath)) {
3986
4282
  console.error(`\u274C Build output not found at: ${sourceBundlePath}`);
3987
4283
  process.exit(1);
3988
4284
  }
3989
- if (!fs14.existsSync(destinationDir)) {
4285
+ if (!fs15.existsSync(destinationDir)) {
3990
4286
  console.error(`Destination directory not found at: ${destinationDir}`);
3991
4287
  process.exit(1);
3992
4288
  }
3993
- const distDir = path15.dirname(sourceBundlePath);
4289
+ const distDir = path16.dirname(sourceBundlePath);
3994
4290
  console.log(`\u{1F69A} Copying bundle and assets to iOS project...`);
3995
4291
  copyDistAssets(distDir, destinationDir, resolved.lynxBundleFile);
3996
4292
  console.log(`\u2728 Successfully copied bundle to: ${destinationBundlePath}`);
3997
- const pbxprojPath = path15.join(resolved.iosDir, `${appName}.xcodeproj`, "project.pbxproj");
3998
- if (fs14.existsSync(pbxprojPath)) {
4293
+ const pbxprojPath = path16.join(resolved.iosDir, `${appName}.xcodeproj`, "project.pbxproj");
4294
+ if (fs15.existsSync(pbxprojPath)) {
3999
4295
  const skip = /* @__PURE__ */ new Set([".rspeedy", "stats.json"]);
4000
- for (const entry of fs14.readdirSync(distDir)) {
4001
- if (skip.has(entry) || fs14.statSync(path15.join(distDir, entry)).isDirectory()) continue;
4296
+ for (const entry of fs15.readdirSync(distDir)) {
4297
+ if (skip.has(entry) || fs15.statSync(path16.join(distDir, entry)).isDirectory()) continue;
4002
4298
  addResourceToXcodeProject(pbxprojPath, appName, entry);
4003
4299
  }
4004
4300
  }
4005
4301
  if (includeDevClient && devClientPkg) {
4006
- const devClientBundle = path15.join(destinationDir, "dev-client.lynx.bundle");
4302
+ const devClientBundle = path16.join(destinationDir, "dev-client.lynx.bundle");
4007
4303
  console.log("\u{1F4E6} Building dev-client bundle...");
4008
4304
  try {
4009
- execSync6("npm run build", { stdio: "inherit", cwd: devClientPkg });
4305
+ execSync7("npm run build", { stdio: "inherit", cwd: devClientPkg });
4010
4306
  } catch {
4011
4307
  console.warn("\u26A0\uFE0F dev-client build failed; skipping dev-client bundle");
4012
4308
  }
4013
- const builtBundle = path15.join(devClientPkg, "dist", "dev-client.lynx.bundle");
4014
- if (fs14.existsSync(builtBundle)) {
4015
- fs14.copyFileSync(builtBundle, devClientBundle);
4309
+ const builtBundle = path16.join(devClientPkg, "dist", "dev-client.lynx.bundle");
4310
+ if (fs15.existsSync(builtBundle)) {
4311
+ fs15.copyFileSync(builtBundle, devClientBundle);
4016
4312
  console.log("\u2728 Copied dev-client.lynx.bundle to iOS project");
4017
- const pbxprojPath2 = path15.join(resolved.iosDir, `${appName}.xcodeproj`, "project.pbxproj");
4018
- if (fs14.existsSync(pbxprojPath2)) {
4313
+ const pbxprojPath2 = path16.join(resolved.iosDir, `${appName}.xcodeproj`, "project.pbxproj");
4314
+ if (fs15.existsSync(pbxprojPath2)) {
4019
4315
  addResourceToXcodeProject(pbxprojPath2, appName, "dev-client.lynx.bundle");
4020
4316
  }
4021
4317
  }
@@ -4029,16 +4325,16 @@ function bundleAndDeploy2(opts = {}) {
4029
4325
  var bundle_default2 = bundleAndDeploy2;
4030
4326
 
4031
4327
  // src/ios/build.ts
4032
- import fs15 from "fs";
4033
- import path16 from "path";
4328
+ import fs16 from "fs";
4329
+ import path17 from "path";
4034
4330
  import os3 from "os";
4035
- import { execSync as execSync7 } from "child_process";
4331
+ import { execSync as execSync8 } from "child_process";
4036
4332
  function hostArch() {
4037
4333
  return os3.arch() === "arm64" ? "arm64" : "x86_64";
4038
4334
  }
4039
4335
  function findBootedSimulator() {
4040
4336
  try {
4041
- const out = execSync7("xcrun simctl list devices --json", { encoding: "utf8" });
4337
+ const out = execSync8("xcrun simctl list devices --json", { encoding: "utf8" });
4042
4338
  const json = JSON.parse(out);
4043
4339
  for (const runtimes of Object.values(json.devices)) {
4044
4340
  for (const device of runtimes) {
@@ -4060,11 +4356,11 @@ async function buildIpa(opts = {}) {
4060
4356
  const configuration = opts.release ? "Release" : "Debug";
4061
4357
  bundle_default2({ release: opts.release });
4062
4358
  const scheme = appName;
4063
- const workspacePath = path16.join(iosDir, `${appName}.xcworkspace`);
4064
- const projectPath = path16.join(iosDir, `${appName}.xcodeproj`);
4065
- const xcproject = fs15.existsSync(workspacePath) ? workspacePath : projectPath;
4359
+ const workspacePath = path17.join(iosDir, `${appName}.xcworkspace`);
4360
+ const projectPath = path17.join(iosDir, `${appName}.xcodeproj`);
4361
+ const xcproject = fs16.existsSync(workspacePath) ? workspacePath : projectPath;
4066
4362
  const flag = xcproject.endsWith(".xcworkspace") ? "-workspace" : "-project";
4067
- const derivedDataPath = path16.join(iosDir, "build");
4363
+ const derivedDataPath = path17.join(iosDir, "build");
4068
4364
  const sdk = opts.install ? "iphonesimulator" : "iphoneos";
4069
4365
  const signingArgs = opts.install ? "" : " CODE_SIGNING_ALLOWED=NO CODE_SIGNING_REQUIRED=NO";
4070
4366
  const archFlag = opts.install ? `-arch ${hostArch()} ` : "";
@@ -4074,20 +4370,20 @@ async function buildIpa(opts = {}) {
4074
4370
  ].join(" ");
4075
4371
  console.log(`
4076
4372
  \u{1F528} Building ${configuration} (${sdk})...`);
4077
- execSync7(
4373
+ execSync8(
4078
4374
  `xcodebuild ${flag} "${xcproject}" -scheme "${scheme}" -configuration ${configuration} -sdk ${sdk} ${archFlag}-derivedDataPath "${derivedDataPath}" ${extraSettings}${signingArgs}`,
4079
4375
  { stdio: "inherit", cwd: iosDir }
4080
4376
  );
4081
4377
  console.log(`\u2705 Build completed.`);
4082
4378
  if (opts.install) {
4083
- const appGlob = path16.join(
4379
+ const appGlob = path17.join(
4084
4380
  derivedDataPath,
4085
4381
  "Build",
4086
4382
  "Products",
4087
4383
  `${configuration}-iphonesimulator`,
4088
4384
  `${appName}.app`
4089
4385
  );
4090
- if (!fs15.existsSync(appGlob)) {
4386
+ if (!fs16.existsSync(appGlob)) {
4091
4387
  console.error(`\u274C Built app not found at: ${appGlob}`);
4092
4388
  process.exit(1);
4093
4389
  }
@@ -4097,10 +4393,10 @@ async function buildIpa(opts = {}) {
4097
4393
  process.exit(1);
4098
4394
  }
4099
4395
  console.log(`\u{1F4F2} Installing on simulator ${udid}...`);
4100
- execSync7(`xcrun simctl install "${udid}" "${appGlob}"`, { stdio: "inherit" });
4396
+ execSync8(`xcrun simctl install "${udid}" "${appGlob}"`, { stdio: "inherit" });
4101
4397
  if (bundleId) {
4102
4398
  console.log(`\u{1F680} Launching ${bundleId}...`);
4103
- execSync7(`xcrun simctl launch "${udid}" "${bundleId}"`, { stdio: "inherit" });
4399
+ execSync8(`xcrun simctl launch "${udid}" "${bundleId}"`, { stdio: "inherit" });
4104
4400
  console.log("\u2705 App launched.");
4105
4401
  } else {
4106
4402
  console.log('\u2705 App installed. (Set "ios.bundleId" in tamer.config.json to auto-launch.)');
@@ -4110,8 +4406,8 @@ async function buildIpa(opts = {}) {
4110
4406
  var build_default2 = buildIpa;
4111
4407
 
4112
4408
  // src/common/init.ts
4113
- import fs16 from "fs";
4114
- import path17 from "path";
4409
+ import fs17 from "fs";
4410
+ import path18 from "path";
4115
4411
  import readline from "readline";
4116
4412
  var rl = readline.createInterface({
4117
4413
  input: process.stdin,
@@ -4162,31 +4458,82 @@ async function init() {
4162
4458
  paths: { androidDir: "android", iosDir: "ios" }
4163
4459
  };
4164
4460
  if (lynxProject) config.lynxProject = lynxProject;
4165
- const configPath = path17.join(process.cwd(), "tamer.config.json");
4166
- fs16.writeFileSync(configPath, JSON.stringify(config, null, 2));
4461
+ const configPath = path18.join(process.cwd(), "tamer.config.json");
4462
+ fs17.writeFileSync(configPath, JSON.stringify(config, null, 2));
4167
4463
  console.log(`
4168
4464
  \u2705 Generated tamer.config.json at ${configPath}`);
4465
+ const tamerTypesInclude = "node_modules/@tamer4lynx/tamer-*/src/**/*.d.ts";
4466
+ const tsconfigCandidates = lynxProject ? [path18.join(process.cwd(), lynxProject, "tsconfig.json"), path18.join(process.cwd(), "tsconfig.json")] : [path18.join(process.cwd(), "tsconfig.json")];
4467
+ for (const tsconfigPath of tsconfigCandidates) {
4468
+ if (!fs17.existsSync(tsconfigPath)) continue;
4469
+ try {
4470
+ const raw = fs17.readFileSync(tsconfigPath, "utf-8");
4471
+ const tsconfig = JSON.parse(raw);
4472
+ const include = tsconfig.include ?? [];
4473
+ const arr = Array.isArray(include) ? include : [include];
4474
+ if (arr.some((p) => (typeof p === "string" ? p : "").includes("tamer-"))) continue;
4475
+ arr.push(tamerTypesInclude);
4476
+ tsconfig.include = arr;
4477
+ fs17.writeFileSync(tsconfigPath, JSON.stringify(tsconfig, null, 2));
4478
+ console.log(`\u2705 Updated ${path18.relative(process.cwd(), tsconfigPath)} to include tamer type declarations`);
4479
+ break;
4480
+ } catch (e) {
4481
+ console.warn(`\u26A0 Could not update ${tsconfigPath}:`, e.message);
4482
+ }
4483
+ }
4169
4484
  rl.close();
4170
4485
  }
4171
4486
  var init_default = init;
4172
4487
 
4173
4488
  // src/common/create.ts
4174
- import fs17 from "fs";
4175
- import path18 from "path";
4489
+ import fs18 from "fs";
4490
+ import path19 from "path";
4176
4491
  import readline2 from "readline";
4177
4492
  var rl2 = readline2.createInterface({ input: process.stdin, output: process.stdout, terminal: false });
4178
4493
  function ask2(question) {
4179
4494
  return new Promise((resolve) => rl2.question(question, (answer) => resolve(answer.trim())));
4180
4495
  }
4181
- async function create3() {
4496
+ async function create3(opts) {
4182
4497
  console.log("Tamer4Lynx: Create Lynx Extension\n");
4183
- console.log("Select extension types (space to toggle, enter to confirm):");
4184
- console.log(" [ ] Native Module");
4185
- console.log(" [ ] Element");
4186
- console.log(" [ ] Service\n");
4187
- const includeModule = /^y(es)?$/i.test(await ask2("Include Native Module? (Y/n): ") || "y");
4188
- const includeElement = /^y(es)?$/i.test(await ask2("Include Element? (y/N): ") || "n");
4189
- const includeService = /^y(es)?$/i.test(await ask2("Include Service? (y/N): ") || "n");
4498
+ let includeModule;
4499
+ let includeElement;
4500
+ let includeService;
4501
+ if (opts?.type) {
4502
+ switch (opts.type) {
4503
+ case "module":
4504
+ includeModule = true;
4505
+ includeElement = false;
4506
+ includeService = false;
4507
+ break;
4508
+ case "element":
4509
+ includeModule = false;
4510
+ includeElement = true;
4511
+ includeService = false;
4512
+ break;
4513
+ case "service":
4514
+ includeModule = false;
4515
+ includeElement = false;
4516
+ includeService = true;
4517
+ break;
4518
+ case "combo":
4519
+ includeModule = true;
4520
+ includeElement = true;
4521
+ includeService = true;
4522
+ break;
4523
+ default:
4524
+ includeModule = true;
4525
+ includeElement = false;
4526
+ includeService = false;
4527
+ }
4528
+ } else {
4529
+ console.log("Select extension types (space to toggle, enter to confirm):");
4530
+ console.log(" [ ] Native Module");
4531
+ console.log(" [ ] Element");
4532
+ console.log(" [ ] Service\n");
4533
+ includeModule = /^y(es)?$/i.test(await ask2("Include Native Module? (Y/n): ") || "y");
4534
+ includeElement = /^y(es)?$/i.test(await ask2("Include Element? (y/N): ") || "n");
4535
+ includeService = /^y(es)?$/i.test(await ask2("Include Service? (y/N): ") || "n");
4536
+ }
4190
4537
  if (!includeModule && !includeElement && !includeService) {
4191
4538
  console.error("\u274C At least one extension type is required.");
4192
4539
  rl2.close();
@@ -4202,13 +4549,13 @@ async function create3() {
4202
4549
  const simpleModuleName = extName.split("-").map((s) => s.charAt(0).toUpperCase() + s.slice(1)).join("") + "Module";
4203
4550
  const fullModuleClassName = `${packageName}.${simpleModuleName}`;
4204
4551
  const cwd = process.cwd();
4205
- const root = path18.join(cwd, extName);
4206
- if (fs17.existsSync(root)) {
4552
+ const root = path19.join(cwd, extName);
4553
+ if (fs18.existsSync(root)) {
4207
4554
  console.error(`\u274C Directory ${extName} already exists.`);
4208
4555
  rl2.close();
4209
4556
  process.exit(1);
4210
4557
  }
4211
- fs17.mkdirSync(root, { recursive: true });
4558
+ fs18.mkdirSync(root, { recursive: true });
4212
4559
  const lynxExt = {
4213
4560
  platforms: {
4214
4561
  android: {
@@ -4223,7 +4570,7 @@ async function create3() {
4223
4570
  web: {}
4224
4571
  }
4225
4572
  };
4226
- fs17.writeFileSync(path18.join(root, "lynx.ext.json"), JSON.stringify(lynxExt, null, 2));
4573
+ fs18.writeFileSync(path19.join(root, "lynx.ext.json"), JSON.stringify(lynxExt, null, 2));
4227
4574
  const pkg = {
4228
4575
  name: extName,
4229
4576
  version: "0.0.1",
@@ -4236,17 +4583,20 @@ async function create3() {
4236
4583
  engines: { node: ">=18" }
4237
4584
  };
4238
4585
  if (includeModule) pkg.types = "src/index.d.ts";
4239
- fs17.writeFileSync(path18.join(root, "package.json"), JSON.stringify(pkg, null, 2));
4586
+ fs18.writeFileSync(path19.join(root, "package.json"), JSON.stringify(pkg, null, 2));
4240
4587
  const pkgPath = packageName.replace(/\./g, "/");
4588
+ const hasSrc = includeModule || includeElement || includeService;
4589
+ if (hasSrc) {
4590
+ fs18.mkdirSync(path19.join(root, "src"), { recursive: true });
4591
+ }
4241
4592
  if (includeModule) {
4242
- fs17.mkdirSync(path18.join(root, "src"), { recursive: true });
4243
- fs17.writeFileSync(path18.join(root, "src", "index.d.ts"), `/** @lynxmodule */
4593
+ fs18.writeFileSync(path19.join(root, "src", "index.d.ts"), `/** @lynxmodule */
4244
4594
  export declare class ${simpleModuleName} {
4245
4595
  // Add your module methods here
4246
4596
  }
4247
4597
  `);
4248
- fs17.mkdirSync(path18.join(root, "android", "src", "main", "kotlin", pkgPath), { recursive: true });
4249
- fs17.writeFileSync(path18.join(root, "android", "build.gradle.kts"), `plugins {
4598
+ fs18.mkdirSync(path19.join(root, "android", "src", "main", "kotlin", pkgPath), { recursive: true });
4599
+ fs18.writeFileSync(path19.join(root, "android", "build.gradle.kts"), `plugins {
4250
4600
  id("com.android.library")
4251
4601
  id("org.jetbrains.kotlin.android")
4252
4602
  }
@@ -4267,7 +4617,7 @@ dependencies {
4267
4617
  implementation(libs.lynx.jssdk)
4268
4618
  }
4269
4619
  `);
4270
- fs17.writeFileSync(path18.join(root, "android", "src", "main", "AndroidManifest.xml"), `<?xml version="1.0" encoding="utf-8"?>
4620
+ fs18.writeFileSync(path19.join(root, "android", "src", "main", "AndroidManifest.xml"), `<?xml version="1.0" encoding="utf-8"?>
4271
4621
  <manifest />
4272
4622
  `);
4273
4623
  const ktContent = `package ${packageName}
@@ -4284,8 +4634,8 @@ class ${simpleModuleName}(context: Context) : LynxModule(context) {
4284
4634
  }
4285
4635
  }
4286
4636
  `;
4287
- fs17.writeFileSync(path18.join(root, "android", "src", "main", "kotlin", pkgPath, `${simpleModuleName}.kt`), ktContent);
4288
- fs17.mkdirSync(path18.join(root, "ios", extName, extName, "Classes"), { recursive: true });
4637
+ fs18.writeFileSync(path19.join(root, "android", "src", "main", "kotlin", pkgPath, `${simpleModuleName}.kt`), ktContent);
4638
+ fs18.mkdirSync(path19.join(root, "ios", extName, extName, "Classes"), { recursive: true });
4289
4639
  const podspec = `Pod::Spec.new do |s|
4290
4640
  s.name = '${extName}'
4291
4641
  s.version = '0.0.1'
@@ -4299,7 +4649,7 @@ class ${simpleModuleName}(context: Context) : LynxModule(context) {
4299
4649
  s.dependency 'Lynx'
4300
4650
  end
4301
4651
  `;
4302
- fs17.writeFileSync(path18.join(root, "ios", extName, `${extName}.podspec`), podspec);
4652
+ fs18.writeFileSync(path19.join(root, "ios", extName, `${extName}.podspec`), podspec);
4303
4653
  const swiftContent = `import Foundation
4304
4654
 
4305
4655
  @objc public class ${simpleModuleName}: NSObject {
@@ -4308,16 +4658,35 @@ end
4308
4658
  }
4309
4659
  }
4310
4660
  `;
4311
- fs17.writeFileSync(path18.join(root, "ios", extName, extName, "Classes", `${simpleModuleName}.swift`), swiftContent);
4661
+ fs18.writeFileSync(path19.join(root, "ios", extName, extName, "Classes", `${simpleModuleName}.swift`), swiftContent);
4662
+ }
4663
+ if (includeElement && !includeModule) {
4664
+ const elementName = extName.split("-").map((s) => s.charAt(0).toUpperCase() + s.slice(1)).join("");
4665
+ fs18.writeFileSync(path19.join(root, "src", "index.tsx"), `import type { FC } from '@lynx-js/react';
4666
+
4667
+ export const ${elementName}: FC = () => {
4668
+ return null;
4669
+ };
4670
+ `);
4312
4671
  }
4313
- fs17.writeFileSync(path18.join(root, "index.js"), `'use strict';
4672
+ fs18.writeFileSync(path19.join(root, "index.js"), `'use strict';
4314
4673
  module.exports = {};
4315
4674
  `);
4316
- fs17.writeFileSync(path18.join(root, "tsconfig.json"), JSON.stringify({
4317
- compilerOptions: { target: "ES2020", module: "ESNext", moduleResolution: "bundler", strict: true },
4318
- include: ["src"]
4675
+ const tsconfigCompiler = {
4676
+ target: "ES2020",
4677
+ module: "ESNext",
4678
+ moduleResolution: "bundler",
4679
+ strict: true
4680
+ };
4681
+ if (includeElement) {
4682
+ tsconfigCompiler.jsx = "preserve";
4683
+ tsconfigCompiler.jsxImportSource = "@lynx-js/react";
4684
+ }
4685
+ fs18.writeFileSync(path19.join(root, "tsconfig.json"), JSON.stringify({
4686
+ compilerOptions: tsconfigCompiler,
4687
+ include: includeElement ? ["src", "src/**/*.tsx"] : ["src"]
4319
4688
  }, null, 2));
4320
- fs17.writeFileSync(path18.join(root, "README.md"), `# ${extName}
4689
+ fs18.writeFileSync(path19.join(root, "README.md"), `# ${extName}
4321
4690
 
4322
4691
  Lynx extension for ${extName}.
4323
4692
 
@@ -4342,8 +4711,8 @@ This package uses \`lynx.ext.json\` (RFC-compliant) for autolinking.
4342
4711
  var create_default3 = create3;
4343
4712
 
4344
4713
  // src/common/codegen.ts
4345
- import fs18 from "fs";
4346
- import path19 from "path";
4714
+ import fs19 from "fs";
4715
+ import path20 from "path";
4347
4716
  function codegen() {
4348
4717
  const cwd = process.cwd();
4349
4718
  const config = loadExtensionConfig(cwd);
@@ -4351,9 +4720,9 @@ function codegen() {
4351
4720
  console.error("\u274C No lynx.ext.json or tamer.json found. Run from an extension package root.");
4352
4721
  process.exit(1);
4353
4722
  }
4354
- const srcDir = path19.join(cwd, "src");
4355
- const generatedDir = path19.join(cwd, "generated");
4356
- fs18.mkdirSync(generatedDir, { recursive: true });
4723
+ const srcDir = path20.join(cwd, "src");
4724
+ const generatedDir = path20.join(cwd, "generated");
4725
+ fs19.mkdirSync(generatedDir, { recursive: true });
4357
4726
  const dtsFiles = findDtsFiles(srcDir);
4358
4727
  const modules = extractLynxModules(dtsFiles);
4359
4728
  if (modules.length === 0) {
@@ -4363,28 +4732,28 @@ function codegen() {
4363
4732
  for (const mod of modules) {
4364
4733
  const tsContent = `export type { ${mod} } from '../src/index.js';
4365
4734
  `;
4366
- const outPath = path19.join(generatedDir, `${mod}.ts`);
4367
- fs18.writeFileSync(outPath, tsContent);
4735
+ const outPath = path20.join(generatedDir, `${mod}.ts`);
4736
+ fs19.writeFileSync(outPath, tsContent);
4368
4737
  console.log(`\u2705 Generated ${outPath}`);
4369
4738
  }
4370
4739
  if (config.android) {
4371
- const androidGenerated = path19.join(cwd, "android", "src", "main", "kotlin", config.android.moduleClassName.replace(/\./g, "/").replace(/[^/]+$/, ""), "generated");
4372
- fs18.mkdirSync(androidGenerated, { recursive: true });
4740
+ const androidGenerated = path20.join(cwd, "android", "src", "main", "kotlin", config.android.moduleClassName.replace(/\./g, "/").replace(/[^/]+$/, ""), "generated");
4741
+ fs19.mkdirSync(androidGenerated, { recursive: true });
4373
4742
  console.log(`\u2139\uFE0F Android generated dir: ${androidGenerated} (spec generation coming soon)`);
4374
4743
  }
4375
4744
  if (config.ios) {
4376
- const iosGenerated = path19.join(cwd, "ios", "generated");
4377
- fs18.mkdirSync(iosGenerated, { recursive: true });
4745
+ const iosGenerated = path20.join(cwd, "ios", "generated");
4746
+ fs19.mkdirSync(iosGenerated, { recursive: true });
4378
4747
  console.log(`\u2139\uFE0F iOS generated dir: ${iosGenerated} (spec generation coming soon)`);
4379
4748
  }
4380
4749
  console.log("\u2728 Codegen complete.");
4381
4750
  }
4382
4751
  function findDtsFiles(dir) {
4383
4752
  const result = [];
4384
- if (!fs18.existsSync(dir)) return result;
4385
- const entries = fs18.readdirSync(dir, { withFileTypes: true });
4753
+ if (!fs19.existsSync(dir)) return result;
4754
+ const entries = fs19.readdirSync(dir, { withFileTypes: true });
4386
4755
  for (const e of entries) {
4387
- const full = path19.join(dir, e.name);
4756
+ const full = path20.join(dir, e.name);
4388
4757
  if (e.isDirectory()) result.push(...findDtsFiles(full));
4389
4758
  else if (e.name.endsWith(".d.ts")) result.push(full);
4390
4759
  }
@@ -4394,7 +4763,7 @@ function extractLynxModules(files) {
4394
4763
  const modules = [];
4395
4764
  const seen = /* @__PURE__ */ new Set();
4396
4765
  for (const file of files) {
4397
- const content = fs18.readFileSync(file, "utf8");
4766
+ const content = fs19.readFileSync(file, "utf8");
4398
4767
  const regex = /\/\*\*\s*@lynxmodule\s*\*\/\s*export\s+declare\s+class\s+(\w+)/g;
4399
4768
  let m;
4400
4769
  while ((m = regex.exec(content)) !== null) {
@@ -4410,12 +4779,55 @@ var codegen_default = codegen;
4410
4779
 
4411
4780
  // src/common/devServer.ts
4412
4781
  import { spawn } from "child_process";
4413
- import fs19 from "fs";
4782
+ import fs20 from "fs";
4414
4783
  import http from "http";
4415
4784
  import os4 from "os";
4416
- import path20 from "path";
4785
+ import path21 from "path";
4786
+ import readline3 from "readline";
4417
4787
  import { WebSocketServer } from "ws";
4418
4788
  var DEFAULT_PORT = 3e3;
4789
+ var STATIC_MIME = {
4790
+ ".png": "image/png",
4791
+ ".jpg": "image/jpeg",
4792
+ ".jpeg": "image/jpeg",
4793
+ ".webp": "image/webp",
4794
+ ".ico": "image/x-icon",
4795
+ ".gif": "image/gif",
4796
+ ".svg": "image/svg+xml",
4797
+ ".pdf": "application/pdf"
4798
+ };
4799
+ function sendFileFromDisk(res, absPath) {
4800
+ fs20.readFile(absPath, (err, data) => {
4801
+ if (err) {
4802
+ res.writeHead(404);
4803
+ res.end("Not found");
4804
+ return;
4805
+ }
4806
+ const ext = path21.extname(absPath).toLowerCase();
4807
+ res.setHeader("Content-Type", STATIC_MIME[ext] ?? "application/octet-stream");
4808
+ res.setHeader("Access-Control-Allow-Origin", "*");
4809
+ res.end(data);
4810
+ });
4811
+ }
4812
+ function isPortInUse(port) {
4813
+ return new Promise((resolve) => {
4814
+ const server = http.createServer();
4815
+ server.once("error", (err) => {
4816
+ resolve(err.code === "EADDRINUSE");
4817
+ });
4818
+ server.once("listening", () => {
4819
+ server.close(() => resolve(false));
4820
+ });
4821
+ server.listen(port, "127.0.0.1");
4822
+ });
4823
+ }
4824
+ async function findAvailablePort(preferred, maxAttempts = 10) {
4825
+ for (let i = 0; i < maxAttempts; i++) {
4826
+ const port = preferred + i;
4827
+ if (!await isPortInUse(port)) return port;
4828
+ }
4829
+ throw new Error(`No available port in range ${preferred}-${preferred + maxAttempts - 1}`);
4830
+ }
4419
4831
  function getLanIp() {
4420
4832
  const nets = os4.networkInterfaces();
4421
4833
  for (const name of Object.keys(nets)) {
@@ -4431,13 +4843,12 @@ async function startDevServer(opts) {
4431
4843
  const verbose = opts?.verbose ?? false;
4432
4844
  const resolved = resolveHostPaths();
4433
4845
  const { projectRoot, lynxProjectDir, lynxBundlePath, lynxBundleFile, config } = resolved;
4434
- const distDir = path20.dirname(lynxBundlePath);
4435
- const port = config.devServer?.port ?? config.devServer?.httpPort ?? DEFAULT_PORT;
4846
+ const distDir = path21.dirname(lynxBundlePath);
4436
4847
  let buildProcess = null;
4437
4848
  function detectPackageManager2(cwd) {
4438
- const dir = path20.resolve(cwd);
4439
- if (fs19.existsSync(path20.join(dir, "pnpm-lock.yaml"))) return { cmd: "pnpm", args: ["run", "build"] };
4440
- if (fs19.existsSync(path20.join(dir, "bun.lockb")) || fs19.existsSync(path20.join(dir, "bun.lock"))) return { cmd: "bun", args: ["run", "build"] };
4849
+ const dir = path21.resolve(cwd);
4850
+ if (fs20.existsSync(path21.join(dir, "pnpm-lock.yaml"))) return { cmd: "pnpm", args: ["run", "build"] };
4851
+ if (fs20.existsSync(path21.join(dir, "bun.lockb")) || fs20.existsSync(path21.join(dir, "bun.lock"))) return { cmd: "bun", args: ["run", "build"] };
4441
4852
  return { cmd: "npm", args: ["run", "build"] };
4442
4853
  }
4443
4854
  function runBuild() {
@@ -4459,27 +4870,27 @@ async function startDevServer(opts) {
4459
4870
  });
4460
4871
  });
4461
4872
  }
4462
- const projectName = path20.basename(lynxProjectDir);
4873
+ const preferredPort = config.devServer?.port ?? config.devServer?.httpPort ?? DEFAULT_PORT;
4874
+ const port = await findAvailablePort(preferredPort);
4875
+ if (port !== preferredPort) {
4876
+ console.log(`\x1B[33m\u26A0 Port ${preferredPort} in use, using ${port}\x1B[0m`);
4877
+ }
4878
+ const projectName = path21.basename(lynxProjectDir);
4463
4879
  const basePath = `/${projectName}`;
4464
4880
  const iconPaths = resolveIconPaths(projectRoot, config);
4465
4881
  let iconFilePath = null;
4466
- if (iconPaths?.source && fs19.statSync(iconPaths.source).isFile()) {
4882
+ if (iconPaths?.source && fs20.statSync(iconPaths.source).isFile()) {
4467
4883
  iconFilePath = iconPaths.source;
4884
+ } else if (iconPaths?.androidAdaptiveForeground && fs20.statSync(iconPaths.androidAdaptiveForeground).isFile()) {
4885
+ iconFilePath = iconPaths.androidAdaptiveForeground;
4468
4886
  } else if (iconPaths?.android) {
4469
- const androidIcon = path20.join(iconPaths.android, "mipmap-xxxhdpi", "ic_launcher.png");
4470
- if (fs19.existsSync(androidIcon)) iconFilePath = androidIcon;
4887
+ const androidIcon = path21.join(iconPaths.android, "mipmap-xxxhdpi", "ic_launcher.png");
4888
+ if (fs20.existsSync(androidIcon)) iconFilePath = androidIcon;
4471
4889
  } else if (iconPaths?.ios) {
4472
- const iosIcon = path20.join(iconPaths.ios, "Icon-1024.png");
4473
- if (fs19.existsSync(iosIcon)) iconFilePath = iosIcon;
4474
- }
4475
- const iconExt = iconFilePath ? path20.extname(iconFilePath) || ".png" : "";
4476
- const iconMime = {
4477
- ".png": "image/png",
4478
- ".jpg": "image/jpeg",
4479
- ".jpeg": "image/jpeg",
4480
- ".webp": "image/webp",
4481
- ".ico": "image/x-icon"
4482
- };
4890
+ const iosIcon = path21.join(iconPaths.ios, "Icon-1024.png");
4891
+ if (fs20.existsSync(iosIcon)) iconFilePath = iosIcon;
4892
+ }
4893
+ const iconExt = iconFilePath ? path21.extname(iconFilePath) || ".png" : "";
4483
4894
  const httpServer = http.createServer((req, res) => {
4484
4895
  let reqPath = (req.url || "/").split("?")[0];
4485
4896
  if (reqPath === `${basePath}/status`) {
@@ -4491,6 +4902,11 @@ async function startDevServer(opts) {
4491
4902
  if (reqPath === `${basePath}/meta.json`) {
4492
4903
  const lanIp = getLanIp();
4493
4904
  const nativeModules = discoverNativeExtensions(projectRoot);
4905
+ const androidPackageName = config.android?.packageName?.trim();
4906
+ const iosBundleId = config.ios?.bundleId?.trim();
4907
+ const idParts = [androidPackageName?.toLowerCase(), iosBundleId?.toLowerCase()].filter(
4908
+ (x) => Boolean(x)
4909
+ );
4494
4910
  const meta = {
4495
4911
  name: projectName,
4496
4912
  slug: projectName,
@@ -4502,6 +4918,15 @@ async function startDevServer(opts) {
4502
4918
  packagerStatus: "running",
4503
4919
  nativeModules: nativeModules.map((m) => ({ packageName: m.packageName, moduleClassName: m.moduleClassName }))
4504
4920
  };
4921
+ if (androidPackageName) meta.androidPackageName = androidPackageName;
4922
+ if (iosBundleId) meta.iosBundleId = iosBundleId;
4923
+ if (idParts.length > 0) meta.tamerAppKey = idParts.join("|");
4924
+ const rawIcon = config.icon;
4925
+ if (rawIcon && typeof rawIcon === "object" && "source" in rawIcon && typeof rawIcon.source === "string") {
4926
+ meta.iconSource = rawIcon.source;
4927
+ } else if (typeof rawIcon === "string") {
4928
+ meta.iconSource = rawIcon;
4929
+ }
4505
4930
  if (iconFilePath) {
4506
4931
  meta.icon = `http://${lanIp}:${port}${basePath}/icon${iconExt}`;
4507
4932
  }
@@ -4511,32 +4936,67 @@ async function startDevServer(opts) {
4511
4936
  return;
4512
4937
  }
4513
4938
  if (iconFilePath && (reqPath === `${basePath}/icon` || reqPath === `${basePath}/icon${iconExt}`)) {
4514
- fs19.readFile(iconFilePath, (err, data) => {
4939
+ fs20.readFile(iconFilePath, (err, data) => {
4515
4940
  if (err) {
4516
4941
  res.writeHead(404);
4517
4942
  res.end();
4518
4943
  return;
4519
4944
  }
4520
- res.setHeader("Content-Type", iconMime[iconExt] ?? "image/png");
4945
+ res.setHeader("Content-Type", STATIC_MIME[iconExt] ?? "image/png");
4521
4946
  res.setHeader("Access-Control-Allow-Origin", "*");
4522
4947
  res.end(data);
4523
4948
  });
4524
4949
  return;
4525
4950
  }
4951
+ const lynxStaticMounts = [
4952
+ { prefix: `${basePath}/src/assets/`, rootSub: "src/assets" },
4953
+ { prefix: `${basePath}/assets/`, rootSub: "assets" }
4954
+ ];
4955
+ for (const { prefix, rootSub } of lynxStaticMounts) {
4956
+ if (!reqPath.startsWith(prefix)) continue;
4957
+ let rel = reqPath.slice(prefix.length);
4958
+ try {
4959
+ rel = decodeURIComponent(rel);
4960
+ } catch {
4961
+ res.writeHead(400);
4962
+ res.end();
4963
+ return;
4964
+ }
4965
+ const safe = path21.normalize(rel).replace(/^(\.\.(\/|\\|$))+/, "");
4966
+ if (path21.isAbsolute(safe) || safe.startsWith("..")) {
4967
+ res.writeHead(403);
4968
+ res.end();
4969
+ return;
4970
+ }
4971
+ const allowedRoot = path21.resolve(lynxProjectDir, rootSub);
4972
+ const abs = path21.resolve(allowedRoot, safe);
4973
+ if (!abs.startsWith(allowedRoot + path21.sep) && abs !== allowedRoot) {
4974
+ res.writeHead(403);
4975
+ res.end();
4976
+ return;
4977
+ }
4978
+ if (!fs20.existsSync(abs) || !fs20.statSync(abs).isFile()) {
4979
+ res.writeHead(404);
4980
+ res.end("Not found");
4981
+ return;
4982
+ }
4983
+ sendFileFromDisk(res, abs);
4984
+ return;
4985
+ }
4526
4986
  if (reqPath === "/" || reqPath === basePath || reqPath === `${basePath}/`) {
4527
4987
  reqPath = `${basePath}/${lynxBundleFile}`;
4528
4988
  } else if (!reqPath.startsWith(basePath)) {
4529
4989
  reqPath = basePath + (reqPath.startsWith("/") ? reqPath : "/" + reqPath);
4530
4990
  }
4531
4991
  const relPath = reqPath.replace(basePath, "").replace(/^\//, "") || lynxBundleFile;
4532
- const filePath = path20.resolve(distDir, relPath);
4533
- const distResolved = path20.resolve(distDir);
4534
- if (!filePath.startsWith(distResolved + path20.sep) && filePath !== distResolved) {
4992
+ const filePath = path21.resolve(distDir, relPath);
4993
+ const distResolved = path21.resolve(distDir);
4994
+ if (!filePath.startsWith(distResolved + path21.sep) && filePath !== distResolved) {
4535
4995
  res.writeHead(403);
4536
4996
  res.end();
4537
4997
  return;
4538
4998
  }
4539
- fs19.readFile(filePath, (err, data) => {
4999
+ fs20.readFile(filePath, (err, data) => {
4540
5000
  if (err) {
4541
5001
  res.writeHead(404);
4542
5002
  res.end("Not found");
@@ -4590,10 +5050,10 @@ async function startDevServer(opts) {
4590
5050
  }
4591
5051
  if (chokidar) {
4592
5052
  const watchPaths = [
4593
- path20.join(lynxProjectDir, "src"),
4594
- path20.join(lynxProjectDir, "lynx.config.ts"),
4595
- path20.join(lynxProjectDir, "lynx.config.js")
4596
- ].filter((p) => fs19.existsSync(p));
5053
+ path21.join(lynxProjectDir, "src"),
5054
+ path21.join(lynxProjectDir, "lynx.config.ts"),
5055
+ path21.join(lynxProjectDir, "lynx.config.js")
5056
+ ].filter((p) => fs20.existsSync(p));
4597
5057
  if (watchPaths.length > 0) {
4598
5058
  const watcher = chokidar.watch(watchPaths, { ignoreInitial: true });
4599
5059
  watcher.on("change", async () => {
@@ -4646,6 +5106,36 @@ async function startDevServer(opts) {
4646
5106
  qrcode.generate(devUrl, { small: true });
4647
5107
  }).catch(() => {
4648
5108
  });
5109
+ if (process.stdin.isTTY) {
5110
+ readline3.emitKeypressEvents(process.stdin);
5111
+ process.stdin.setRawMode(true);
5112
+ process.stdin.resume();
5113
+ process.stdin.setEncoding("utf8");
5114
+ const help = "\x1B[90m r: refresh c/Ctrl+L: clear Ctrl+C: exit\x1B[0m";
5115
+ console.log(help);
5116
+ process.stdin.on("keypress", (str, key) => {
5117
+ if (key.ctrl && key.name === "c") {
5118
+ void cleanup();
5119
+ return;
5120
+ }
5121
+ switch (key.name) {
5122
+ case "r":
5123
+ runBuild().then(() => {
5124
+ broadcastReload();
5125
+ console.log("\u{1F504} Refreshed, clients notified");
5126
+ }).catch((e) => console.error("Build failed:", e.message));
5127
+ break;
5128
+ case "c":
5129
+ process.stdout.write("\x1B[2J\x1B[H");
5130
+ break;
5131
+ case "l":
5132
+ if (key.ctrl) process.stdout.write("\x1B[2J\x1B[H");
5133
+ break;
5134
+ default:
5135
+ break;
5136
+ }
5137
+ });
5138
+ }
4649
5139
  });
4650
5140
  const cleanup = async () => {
4651
5141
  buildProcess?.kill();
@@ -4672,10 +5162,10 @@ async function start(opts) {
4672
5162
  var start_default = start;
4673
5163
 
4674
5164
  // src/common/injectHost.ts
4675
- import fs20 from "fs";
4676
- import path21 from "path";
5165
+ import fs21 from "fs";
5166
+ import path22 from "path";
4677
5167
  function readAndSubstitute(templatePath, vars) {
4678
- const raw = fs20.readFileSync(templatePath, "utf-8");
5168
+ const raw = fs21.readFileSync(templatePath, "utf-8");
4679
5169
  return Object.entries(vars).reduce(
4680
5170
  (s, [k, v]) => s.replace(new RegExp(`\\{\\{${k}\\}\\}`, "g"), v),
4681
5171
  raw
@@ -4696,32 +5186,32 @@ async function injectHostAndroid(opts) {
4696
5186
  process.exit(1);
4697
5187
  }
4698
5188
  const androidDir = config.paths?.androidDir ?? "android";
4699
- const rootDir = path21.join(projectRoot, androidDir);
5189
+ const rootDir = path22.join(projectRoot, androidDir);
4700
5190
  const packagePath = packageName.replace(/\./g, "/");
4701
- const javaDir = path21.join(rootDir, "app", "src", "main", "java", packagePath);
4702
- const kotlinDir = path21.join(rootDir, "app", "src", "main", "kotlin", packagePath);
4703
- if (!fs20.existsSync(javaDir) || !fs20.existsSync(kotlinDir)) {
5191
+ const javaDir = path22.join(rootDir, "app", "src", "main", "java", packagePath);
5192
+ const kotlinDir = path22.join(rootDir, "app", "src", "main", "kotlin", packagePath);
5193
+ if (!fs21.existsSync(javaDir) || !fs21.existsSync(kotlinDir)) {
4704
5194
  console.error("\u274C Android project not found. Run `t4l android create` first or ensure android/ exists.");
4705
5195
  process.exit(1);
4706
5196
  }
4707
- const templateDir = path21.join(hostPkg, "android", "templates");
5197
+ const templateDir = path22.join(hostPkg, "android", "templates");
4708
5198
  const vars = { PACKAGE_NAME: packageName, APP_NAME: appName };
4709
5199
  const files = [
4710
- { src: "App.java", dst: path21.join(javaDir, "App.java") },
4711
- { src: "TemplateProvider.java", dst: path21.join(javaDir, "TemplateProvider.java") },
4712
- { src: "MainActivity.kt", dst: path21.join(kotlinDir, "MainActivity.kt") }
5200
+ { src: "App.java", dst: path22.join(javaDir, "App.java") },
5201
+ { src: "TemplateProvider.java", dst: path22.join(javaDir, "TemplateProvider.java") },
5202
+ { src: "MainActivity.kt", dst: path22.join(kotlinDir, "MainActivity.kt") }
4713
5203
  ];
4714
5204
  for (const { src, dst } of files) {
4715
- const srcPath = path21.join(templateDir, src);
4716
- if (!fs20.existsSync(srcPath)) continue;
4717
- if (fs20.existsSync(dst) && !opts?.force) {
4718
- console.log(`\u23ED\uFE0F Skipping ${path21.basename(dst)} (use --force to overwrite)`);
5205
+ const srcPath = path22.join(templateDir, src);
5206
+ if (!fs21.existsSync(srcPath)) continue;
5207
+ if (fs21.existsSync(dst) && !opts?.force) {
5208
+ console.log(`\u23ED\uFE0F Skipping ${path22.basename(dst)} (use --force to overwrite)`);
4719
5209
  continue;
4720
5210
  }
4721
5211
  const content = readAndSubstitute(srcPath, vars);
4722
- fs20.mkdirSync(path21.dirname(dst), { recursive: true });
4723
- fs20.writeFileSync(dst, content);
4724
- console.log(`\u2705 Injected ${path21.basename(dst)}`);
5212
+ fs21.mkdirSync(path22.dirname(dst), { recursive: true });
5213
+ fs21.writeFileSync(dst, content);
5214
+ console.log(`\u2705 Injected ${path22.basename(dst)}`);
4725
5215
  }
4726
5216
  }
4727
5217
  async function injectHostIos(opts) {
@@ -4739,13 +5229,13 @@ async function injectHostIos(opts) {
4739
5229
  process.exit(1);
4740
5230
  }
4741
5231
  const iosDir = config.paths?.iosDir ?? "ios";
4742
- const rootDir = path21.join(projectRoot, iosDir);
4743
- const projectDir = path21.join(rootDir, appName);
4744
- if (!fs20.existsSync(projectDir)) {
5232
+ const rootDir = path22.join(projectRoot, iosDir);
5233
+ const projectDir = path22.join(rootDir, appName);
5234
+ if (!fs21.existsSync(projectDir)) {
4745
5235
  console.error("\u274C iOS project not found. Run `t4l ios create` first or ensure ios/ exists.");
4746
5236
  process.exit(1);
4747
5237
  }
4748
- const templateDir = path21.join(hostPkg, "ios", "templates");
5238
+ const templateDir = path22.join(hostPkg, "ios", "templates");
4749
5239
  const vars = { PACKAGE_NAME: bundleId, APP_NAME: appName, BUNDLE_ID: bundleId };
4750
5240
  const files = [
4751
5241
  "AppDelegate.swift",
@@ -4755,31 +5245,31 @@ async function injectHostIos(opts) {
4755
5245
  "LynxInitProcessor.swift"
4756
5246
  ];
4757
5247
  for (const f of files) {
4758
- const srcPath = path21.join(templateDir, f);
4759
- const dstPath = path21.join(projectDir, f);
4760
- if (!fs20.existsSync(srcPath)) continue;
4761
- if (fs20.existsSync(dstPath) && !opts?.force) {
5248
+ const srcPath = path22.join(templateDir, f);
5249
+ const dstPath = path22.join(projectDir, f);
5250
+ if (!fs21.existsSync(srcPath)) continue;
5251
+ if (fs21.existsSync(dstPath) && !opts?.force) {
4762
5252
  console.log(`\u23ED\uFE0F Skipping ${f} (use --force to overwrite)`);
4763
5253
  continue;
4764
5254
  }
4765
5255
  const content = readAndSubstitute(srcPath, vars);
4766
- fs20.writeFileSync(dstPath, content);
5256
+ fs21.writeFileSync(dstPath, content);
4767
5257
  console.log(`\u2705 Injected ${f}`);
4768
5258
  }
4769
5259
  }
4770
5260
 
4771
5261
  // src/common/buildEmbeddable.ts
4772
- import fs21 from "fs";
4773
- import path22 from "path";
4774
- import { execSync as execSync8 } from "child_process";
5262
+ import fs22 from "fs";
5263
+ import path23 from "path";
5264
+ import { execSync as execSync9 } from "child_process";
4775
5265
  var EMBEDDABLE_DIR = "embeddable";
4776
5266
  var LIB_PACKAGE = "com.tamer.embeddable";
4777
5267
  var GRADLE_VERSION = "8.14.2";
4778
5268
  var LIBS_VERSIONS_TOML = `[versions]
4779
5269
  agp = "8.9.1"
4780
- lynx = "3.3.1"
5270
+ lynx = "3.6.0"
4781
5271
  kotlin = "2.0.21"
4782
- primjs = "2.12.0"
5272
+ primjs = "3.6.1"
4783
5273
 
4784
5274
  [libraries]
4785
5275
  lynx = { module = "org.lynxsdk.lynx:lynx", version.ref = "lynx" }
@@ -4847,14 +5337,14 @@ object LynxEmbeddable {
4847
5337
  }
4848
5338
  `;
4849
5339
  function generateAndroidLibrary(outDir, androidDir, projectRoot, lynxBundlePath, lynxBundleFile, modules, abiFilters) {
4850
- const libDir = path22.join(androidDir, "lib");
4851
- const libSrcMain = path22.join(libDir, "src", "main");
4852
- const assetsDir = path22.join(libSrcMain, "assets");
4853
- const kotlinDir = path22.join(libSrcMain, "kotlin", LIB_PACKAGE.replace(/\./g, "/"));
4854
- const generatedDir = path22.join(kotlinDir, "generated");
4855
- fs21.mkdirSync(path22.join(androidDir, "gradle"), { recursive: true });
4856
- fs21.mkdirSync(generatedDir, { recursive: true });
4857
- fs21.mkdirSync(assetsDir, { recursive: true });
5340
+ const libDir = path23.join(androidDir, "lib");
5341
+ const libSrcMain = path23.join(libDir, "src", "main");
5342
+ const assetsDir = path23.join(libSrcMain, "assets");
5343
+ const kotlinDir = path23.join(libSrcMain, "kotlin", LIB_PACKAGE.replace(/\./g, "/"));
5344
+ const generatedDir = path23.join(kotlinDir, "generated");
5345
+ fs22.mkdirSync(path23.join(androidDir, "gradle"), { recursive: true });
5346
+ fs22.mkdirSync(generatedDir, { recursive: true });
5347
+ fs22.mkdirSync(assetsDir, { recursive: true });
4858
5348
  const androidModules = modules.filter((m) => m.config.android);
4859
5349
  const abiList = abiFilters.map((a) => `"${a}"`).join(", ");
4860
5350
  const settingsContent = `pluginManagement {
@@ -4874,7 +5364,7 @@ include(":lib")
4874
5364
  ${androidModules.map((p) => {
4875
5365
  const gradleName = p.name.replace(/^@/, "").replace(/\//g, "_");
4876
5366
  const sourceDir = p.config.android?.sourceDir || "android";
4877
- const absPath = path22.join(p.packagePath, sourceDir).replace(/\\/g, "/");
5367
+ const absPath = path23.join(p.packagePath, sourceDir).replace(/\\/g, "/");
4878
5368
  return `include(":${gradleName}")
4879
5369
  project(":${gradleName}").projectDir = file("${absPath}")`;
4880
5370
  }).join("\n")}
@@ -4923,10 +5413,10 @@ dependencies {
4923
5413
  ${libDeps}
4924
5414
  }
4925
5415
  `;
4926
- fs21.writeFileSync(path22.join(androidDir, "gradle", "libs.versions.toml"), LIBS_VERSIONS_TOML);
4927
- fs21.writeFileSync(path22.join(androidDir, "settings.gradle.kts"), settingsContent);
4928
- fs21.writeFileSync(
4929
- path22.join(androidDir, "build.gradle.kts"),
5416
+ fs22.writeFileSync(path23.join(androidDir, "gradle", "libs.versions.toml"), LIBS_VERSIONS_TOML);
5417
+ fs22.writeFileSync(path23.join(androidDir, "settings.gradle.kts"), settingsContent);
5418
+ fs22.writeFileSync(
5419
+ path23.join(androidDir, "build.gradle.kts"),
4930
5420
  `plugins {
4931
5421
  alias(libs.plugins.android.library) apply false
4932
5422
  alias(libs.plugins.kotlin.android) apply false
@@ -4934,26 +5424,26 @@ ${libDeps}
4934
5424
  }
4935
5425
  `
4936
5426
  );
4937
- fs21.writeFileSync(
4938
- path22.join(androidDir, "gradle.properties"),
5427
+ fs22.writeFileSync(
5428
+ path23.join(androidDir, "gradle.properties"),
4939
5429
  `org.gradle.jvmargs=-Xmx2048m
4940
5430
  android.useAndroidX=true
4941
5431
  kotlin.code.style=official
4942
5432
  `
4943
5433
  );
4944
- fs21.writeFileSync(path22.join(libDir, "build.gradle.kts"), libBuildContent);
4945
- fs21.writeFileSync(
4946
- path22.join(libSrcMain, "AndroidManifest.xml"),
5434
+ fs22.writeFileSync(path23.join(libDir, "build.gradle.kts"), libBuildContent);
5435
+ fs22.writeFileSync(
5436
+ path23.join(libSrcMain, "AndroidManifest.xml"),
4947
5437
  '<?xml version="1.0" encoding="utf-8"?>\n<manifest />'
4948
5438
  );
4949
- fs21.copyFileSync(lynxBundlePath, path22.join(assetsDir, lynxBundleFile));
4950
- fs21.writeFileSync(path22.join(kotlinDir, "LynxEmbeddable.kt"), LYNX_EMBEDDABLE_KT);
4951
- fs21.writeFileSync(
4952
- path22.join(generatedDir, "GeneratedLynxExtensions.kt"),
5439
+ fs22.copyFileSync(lynxBundlePath, path23.join(assetsDir, lynxBundleFile));
5440
+ fs22.writeFileSync(path23.join(kotlinDir, "LynxEmbeddable.kt"), LYNX_EMBEDDABLE_KT);
5441
+ fs22.writeFileSync(
5442
+ path23.join(generatedDir, "GeneratedLynxExtensions.kt"),
4953
5443
  generateLynxExtensionsKotlin(modules, LIB_PACKAGE)
4954
5444
  );
4955
- fs21.writeFileSync(
4956
- path22.join(generatedDir, "GeneratedActivityLifecycle.kt"),
5445
+ fs22.writeFileSync(
5446
+ path23.join(generatedDir, "GeneratedActivityLifecycle.kt"),
4957
5447
  generateActivityLifecycleKotlin(modules, LIB_PACKAGE)
4958
5448
  );
4959
5449
  }
@@ -4961,21 +5451,21 @@ async function buildEmbeddable(opts = {}) {
4961
5451
  const resolved = resolveHostPaths();
4962
5452
  const { lynxProjectDir, lynxBundlePath, lynxBundleFile, projectRoot, config } = resolved;
4963
5453
  console.log("\u{1F4E6} Building Lynx project (release)...");
4964
- execSync8("npm run build", { stdio: "inherit", cwd: lynxProjectDir });
4965
- if (!fs21.existsSync(lynxBundlePath)) {
5454
+ execSync9("npm run build", { stdio: "inherit", cwd: lynxProjectDir });
5455
+ if (!fs22.existsSync(lynxBundlePath)) {
4966
5456
  console.error(`\u274C Bundle not found at ${lynxBundlePath}`);
4967
5457
  process.exit(1);
4968
5458
  }
4969
- const outDir = path22.join(projectRoot, EMBEDDABLE_DIR);
4970
- fs21.mkdirSync(outDir, { recursive: true });
4971
- const distDir = path22.dirname(lynxBundlePath);
5459
+ const outDir = path23.join(projectRoot, EMBEDDABLE_DIR);
5460
+ fs22.mkdirSync(outDir, { recursive: true });
5461
+ const distDir = path23.dirname(lynxBundlePath);
4972
5462
  copyDistAssets(distDir, outDir, lynxBundleFile);
4973
5463
  const modules = discoverModules(projectRoot);
4974
5464
  const androidModules = modules.filter((m) => m.config.android);
4975
5465
  const abiFilters = resolveAbiFilters(config);
4976
- const androidDir = path22.join(outDir, "android");
4977
- if (fs21.existsSync(androidDir)) fs21.rmSync(androidDir, { recursive: true });
4978
- fs21.mkdirSync(androidDir, { recursive: true });
5466
+ const androidDir = path23.join(outDir, "android");
5467
+ if (fs22.existsSync(androidDir)) fs22.rmSync(androidDir, { recursive: true });
5468
+ fs22.mkdirSync(androidDir, { recursive: true });
4979
5469
  generateAndroidLibrary(
4980
5470
  outDir,
4981
5471
  androidDir,
@@ -4985,23 +5475,23 @@ async function buildEmbeddable(opts = {}) {
4985
5475
  modules,
4986
5476
  abiFilters
4987
5477
  );
4988
- const gradlewPath = path22.join(androidDir, "gradlew");
5478
+ const gradlewPath = path23.join(androidDir, "gradlew");
4989
5479
  const devAppDir = findDevAppPackage(projectRoot);
4990
5480
  const existingGradleDirs = [
4991
- path22.join(projectRoot, "android"),
4992
- devAppDir ? path22.join(devAppDir, "android") : null
5481
+ path23.join(projectRoot, "android"),
5482
+ devAppDir ? path23.join(devAppDir, "android") : null
4993
5483
  ].filter(Boolean);
4994
5484
  let hasWrapper = false;
4995
5485
  for (const d of existingGradleDirs) {
4996
- if (fs21.existsSync(path22.join(d, "gradlew"))) {
5486
+ if (fs22.existsSync(path23.join(d, "gradlew"))) {
4997
5487
  for (const name of ["gradlew", "gradlew.bat", "gradle"]) {
4998
- const src = path22.join(d, name);
4999
- if (fs21.existsSync(src)) {
5000
- const dest = path22.join(androidDir, name);
5001
- if (fs21.statSync(src).isDirectory()) {
5002
- fs21.cpSync(src, dest, { recursive: true });
5488
+ const src = path23.join(d, name);
5489
+ if (fs22.existsSync(src)) {
5490
+ const dest = path23.join(androidDir, name);
5491
+ if (fs22.statSync(src).isDirectory()) {
5492
+ fs22.cpSync(src, dest, { recursive: true });
5003
5493
  } else {
5004
- fs21.copyFileSync(src, dest);
5494
+ fs22.copyFileSync(src, dest);
5005
5495
  }
5006
5496
  }
5007
5497
  }
@@ -5015,15 +5505,15 @@ async function buildEmbeddable(opts = {}) {
5015
5505
  }
5016
5506
  try {
5017
5507
  console.log("\u{1F4E6} Building Android AAR...");
5018
- execSync8("./gradlew :lib:assembleRelease", { cwd: androidDir, stdio: "inherit" });
5508
+ execSync9("./gradlew :lib:assembleRelease", { cwd: androidDir, stdio: "inherit" });
5019
5509
  } catch (e) {
5020
5510
  console.error("\u274C Android AAR build failed. Run manually: cd embeddable/android && ./gradlew :lib:assembleRelease");
5021
5511
  throw e;
5022
5512
  }
5023
- const aarSrc = path22.join(androidDir, "lib", "build", "outputs", "aar", "lib-release.aar");
5024
- const aarDest = path22.join(outDir, "tamer-embeddable.aar");
5025
- if (fs21.existsSync(aarSrc)) {
5026
- fs21.copyFileSync(aarSrc, aarDest);
5513
+ const aarSrc = path23.join(androidDir, "lib", "build", "outputs", "aar", "lib-release.aar");
5514
+ const aarDest = path23.join(outDir, "tamer-embeddable.aar");
5515
+ if (fs22.existsSync(aarSrc)) {
5516
+ fs22.copyFileSync(aarSrc, aarDest);
5027
5517
  console.log(` - tamer-embeddable.aar`);
5028
5518
  }
5029
5519
  const snippetAndroid = `// Add to your app's build.gradle:
@@ -5034,7 +5524,7 @@ async function buildEmbeddable(opts = {}) {
5034
5524
  // LynxEmbeddable.init(applicationContext)
5035
5525
  // val lynxView = LynxEmbeddable.buildLynxView(containerViewGroup)
5036
5526
  `;
5037
- fs21.writeFileSync(path22.join(outDir, "snippet-android.kt"), snippetAndroid);
5527
+ fs22.writeFileSync(path23.join(outDir, "snippet-android.kt"), snippetAndroid);
5038
5528
  generateIosPod(outDir, projectRoot, lynxBundlePath, lynxBundleFile, modules);
5039
5529
  const readme = `# Embeddable Lynx Bundle
5040
5530
 
@@ -5065,7 +5555,7 @@ Add the \`Podfile.snippet\` entries to your Podfile (inside your app target), th
5065
5555
 
5066
5556
  - [Embedding LynxView](https://lynxjs.org/guide/embed-lynx-to-native)
5067
5557
  `;
5068
- fs21.writeFileSync(path22.join(outDir, "README.md"), readme);
5558
+ fs22.writeFileSync(path23.join(outDir, "README.md"), readme);
5069
5559
  console.log(`
5070
5560
  \u2705 Embeddable output at ${outDir}/`);
5071
5561
  console.log(" - main.lynx.bundle");
@@ -5077,20 +5567,20 @@ Add the \`Podfile.snippet\` entries to your Podfile (inside your app target), th
5077
5567
  console.log(" - README.md");
5078
5568
  }
5079
5569
  function generateIosPod(outDir, projectRoot, lynxBundlePath, lynxBundleFile, modules) {
5080
- const iosDir = path22.join(outDir, "ios");
5081
- const podDir = path22.join(iosDir, "TamerEmbeddable");
5082
- const resourcesDir = path22.join(podDir, "Resources");
5083
- fs21.mkdirSync(resourcesDir, { recursive: true });
5084
- fs21.copyFileSync(lynxBundlePath, path22.join(resourcesDir, lynxBundleFile));
5570
+ const iosDir = path23.join(outDir, "ios");
5571
+ const podDir = path23.join(iosDir, "TamerEmbeddable");
5572
+ const resourcesDir = path23.join(podDir, "Resources");
5573
+ fs22.mkdirSync(resourcesDir, { recursive: true });
5574
+ fs22.copyFileSync(lynxBundlePath, path23.join(resourcesDir, lynxBundleFile));
5085
5575
  const iosModules = modules.filter((m) => m.config.ios);
5086
5576
  const podDeps = iosModules.map((p) => {
5087
5577
  const podspecPath = p.config.ios?.podspecPath || ".";
5088
- const podspecDir = path22.join(p.packagePath, podspecPath);
5089
- if (!fs21.existsSync(podspecDir)) return null;
5090
- const files = fs21.readdirSync(podspecDir);
5578
+ const podspecDir = path23.join(p.packagePath, podspecPath);
5579
+ if (!fs22.existsSync(podspecDir)) return null;
5580
+ const files = fs22.readdirSync(podspecDir);
5091
5581
  const podspecFile = files.find((f) => f.endsWith(".podspec"));
5092
5582
  const podName = podspecFile ? podspecFile.replace(".podspec", "") : p.name.split("/").pop().replace(/-/g, "");
5093
- const absPath = path22.resolve(podspecDir);
5583
+ const absPath = path23.resolve(podspecDir);
5094
5584
  return { podName, absPath };
5095
5585
  }).filter(Boolean);
5096
5586
  const podDepLines = podDeps.map((d) => ` s.dependency '${d.podName}'`).join("\n");
@@ -5130,9 +5620,9 @@ end
5130
5620
  });
5131
5621
  const swiftImports = iosModules.map((p) => {
5132
5622
  const podspecPath = p.config.ios?.podspecPath || ".";
5133
- const podspecDir = path22.join(p.packagePath, podspecPath);
5134
- if (!fs21.existsSync(podspecDir)) return null;
5135
- const files = fs21.readdirSync(podspecDir);
5623
+ const podspecDir = path23.join(p.packagePath, podspecPath);
5624
+ if (!fs22.existsSync(podspecDir)) return null;
5625
+ const files = fs22.readdirSync(podspecDir);
5136
5626
  const podspecFile = files.find((f) => f.endsWith(".podspec"));
5137
5627
  return podspecFile ? podspecFile.replace(".podspec", "") : null;
5138
5628
  }).filter(Boolean);
@@ -5151,17 +5641,17 @@ ${regBlock}
5151
5641
  }
5152
5642
  }
5153
5643
  `;
5154
- fs21.writeFileSync(path22.join(iosDir, "TamerEmbeddable.podspec"), podspecContent);
5155
- fs21.writeFileSync(path22.join(podDir, "LynxEmbeddable.swift"), lynxEmbeddableSwift);
5156
- const absIosDir = path22.resolve(iosDir);
5644
+ fs22.writeFileSync(path23.join(iosDir, "TamerEmbeddable.podspec"), podspecContent);
5645
+ fs22.writeFileSync(path23.join(podDir, "LynxEmbeddable.swift"), lynxEmbeddableSwift);
5646
+ const absIosDir = path23.resolve(iosDir);
5157
5647
  const podfileSnippet = `# Paste into your app target in Podfile:
5158
5648
 
5159
5649
  pod 'TamerEmbeddable', :path => '${absIosDir}'
5160
5650
  ${podDeps.map((d) => `pod '${d.podName}', :path => '${d.absPath}'`).join("\n")}
5161
5651
  `;
5162
- fs21.writeFileSync(path22.join(iosDir, "Podfile.snippet"), podfileSnippet);
5163
- fs21.writeFileSync(
5164
- path22.join(outDir, "snippet-ios.swift"),
5652
+ fs22.writeFileSync(path23.join(iosDir, "Podfile.snippet"), podfileSnippet);
5653
+ fs22.writeFileSync(
5654
+ path23.join(outDir, "snippet-ios.swift"),
5165
5655
  `// Add LynxEmbeddable.initEnvironment() in your AppDelegate/SceneDelegate before presenting LynxView.
5166
5656
  // Then create LynxView with your bundle URL (main.lynx.bundle is in the pod resources).
5167
5657
  `
@@ -5169,32 +5659,29 @@ ${podDeps.map((d) => `pod '${d.podName}', :path => '${d.absPath}'`).join("\n")}
5169
5659
  }
5170
5660
 
5171
5661
  // src/common/add.ts
5172
- import fs22 from "fs";
5173
- import path23 from "path";
5174
- import { execSync as execSync9 } from "child_process";
5662
+ import fs23 from "fs";
5663
+ import path24 from "path";
5664
+ import { execSync as execSync10 } from "child_process";
5175
5665
  var CORE_PACKAGES = [
5176
5666
  "@tamer4lynx/tamer-app-shell",
5177
5667
  "@tamer4lynx/tamer-screen",
5178
5668
  "@tamer4lynx/tamer-router",
5179
5669
  "@tamer4lynx/tamer-insets",
5180
5670
  "@tamer4lynx/tamer-transports",
5181
- "@tamer4lynx/tamer-text-input",
5182
5671
  "@tamer4lynx/tamer-system-ui",
5183
5672
  "@tamer4lynx/tamer-icons"
5184
5673
  ];
5185
- var PACKAGE_ALIASES = {
5186
- input: "@tamer4lynx/tamer-text-input"
5187
- };
5674
+ var PACKAGE_ALIASES = {};
5188
5675
  function detectPackageManager(cwd) {
5189
- const dir = path23.resolve(cwd);
5190
- if (fs22.existsSync(path23.join(dir, "pnpm-lock.yaml"))) return "pnpm";
5191
- if (fs22.existsSync(path23.join(dir, "bun.lockb"))) return "bun";
5676
+ const dir = path24.resolve(cwd);
5677
+ if (fs23.existsSync(path24.join(dir, "pnpm-lock.yaml"))) return "pnpm";
5678
+ if (fs23.existsSync(path24.join(dir, "bun.lockb"))) return "bun";
5192
5679
  return "npm";
5193
5680
  }
5194
5681
  function runInstall(cwd, packages, pm) {
5195
5682
  const args = pm === "npm" ? ["install", ...packages] : ["add", ...packages];
5196
5683
  const cmd = pm === "npm" ? "npm" : pm === "pnpm" ? "pnpm" : "bun";
5197
- execSync9(`${cmd} ${args.join(" ")}`, { stdio: "inherit", cwd });
5684
+ execSync10(`${cmd} ${args.join(" ")}`, { stdio: "inherit", cwd });
5198
5685
  }
5199
5686
  function addCore() {
5200
5687
  const { lynxProjectDir } = resolveHostPaths();
@@ -5230,65 +5717,53 @@ function validateDebugRelease(debug, release) {
5230
5717
  process.exit(1);
5231
5718
  }
5232
5719
  }
5720
+ function parsePlatform(value) {
5721
+ const p = value?.toLowerCase();
5722
+ if (p === "ios" || p === "android") return p;
5723
+ if (p === "all" || p === "both") return "all";
5724
+ return null;
5725
+ }
5233
5726
  program.version(version).description("Tamer4Lynx CLI - A tool for managing Lynx projects");
5234
5727
  program.command("init").description("Initialize tamer.config.json interactively").action(() => {
5235
5728
  init_default();
5236
5729
  });
5237
- var android = program.command("android").description("Android project commands");
5238
- android.command("create").option("-t, --target <target>", "Create target: host (default) or dev-app", "host").description("Create a new Android project").action(async (opts) => {
5239
- await create_default({ target: opts.target });
5240
- });
5241
- android.command("link").description("Link native modules to the Android project").action(() => {
5242
- autolink_default();
5243
- });
5244
- android.command("bundle").option("-d, --debug", "Debug bundle with dev client embedded (default)").option("-r, --release", "Release bundle without dev client").description("Build Lynx bundle and copy to Android assets (runs autolink first)").action(async (opts) => {
5245
- validateDebugRelease(opts.debug, opts.release);
5246
- const release = opts.release === true;
5247
- await bundle_default({ release });
5248
- });
5249
- var androidBuildCmd = android.command("build").option("-i, --install", "Install APK to connected device and launch app after building").option("-e, --embeddable", "Build for embedding in existing app (host only). Use with --release for production-ready embeddable.").option("-d, --debug", "Debug APK with dev client embedded (default)").option("-r, --release", "Release APK without dev client").description("Build APK (autolink + bundle + gradle)").action(async () => {
5250
- const opts = androidBuildCmd.opts();
5251
- validateDebugRelease(opts.debug, opts.release);
5252
- const release = opts.release === true;
5253
- if (opts.embeddable) {
5254
- await buildEmbeddable({ release: true });
5730
+ program.command("create <target>").description("Create a project or extension. Target: ios | android | module | element | service | combo").option("-d, --debug", "For android: create host project (default)").option("-r, --release", "For android: create dev-app project").action(async (target, opts) => {
5731
+ const t = target.toLowerCase();
5732
+ if (t === "ios") {
5733
+ create_default2();
5255
5734
  return;
5256
5735
  }
5257
- await build_default({ install: opts.install, release });
5258
- });
5259
- android.command("sync").description("Sync dev client files (TemplateProvider, MainActivity, DevClientManager) from tamer.config.json").action(async () => {
5260
- await syncDevClient_default();
5261
- });
5262
- android.command("inject").option("-f, --force", "Overwrite existing files").description("Inject tamer-host templates into an existing Android project").action(async (opts) => {
5263
- await injectHostAndroid({ force: opts.force });
5264
- });
5265
- var ios = program.command("ios").description("iOS project commands");
5266
- ios.command("create").description("Create a new iOS project").action(() => {
5267
- create_default2();
5268
- });
5269
- ios.command("inject").option("-f, --force", "Overwrite existing files").description("Inject tamer-host templates into an existing iOS project").action(async (opts) => {
5270
- await injectHostIos({ force: opts.force });
5271
- });
5272
- ios.command("link").description("Link native modules to the iOS project").action(() => {
5273
- autolink_default2();
5274
- });
5275
- ios.command("bundle").option("-d, --debug", "Debug bundle with dev client embedded (default)").option("-r, --release", "Release bundle without dev client").description("Build Lynx bundle and copy to iOS project (runs autolink first)").action((opts) => {
5276
- validateDebugRelease(opts.debug, opts.release);
5277
- const release = opts.release === true;
5278
- bundle_default2({ release });
5736
+ if (t === "android") {
5737
+ if (opts.debug && opts.release) {
5738
+ console.error("Cannot use --debug and --release together.");
5739
+ process.exit(1);
5740
+ }
5741
+ await create_default({ target: opts.release ? "dev-app" : "host" });
5742
+ return;
5743
+ }
5744
+ if (["module", "element", "service", "combo"].includes(t)) {
5745
+ await create_default3({ type: t });
5746
+ return;
5747
+ }
5748
+ console.error(`Invalid create target: ${target}. Use ios | android | module | element | service | combo`);
5749
+ process.exit(1);
5279
5750
  });
5280
- var iosBuildCmd = ios.command("build").option("-e, --embeddable", "Output bundle + code snippets to embeddable/ for adding LynxView to an existing app. Use with --release.").option("-i, --install", "Install and launch on booted simulator after building").option("-d, --debug", "Debug build with dev client embedded (default)").option("-r, --release", "Release build without dev client").description("Build iOS app (autolink + bundle + xcodebuild)").action(async () => {
5281
- const opts = iosBuildCmd.opts();
5751
+ program.command("build [platform]").description("Build app. Platform: ios | android (default: both)").option("-e, --embeddable", "Output embeddable bundle + code for existing apps. Use with --release.").option("-d, --debug", "Debug build with dev client embedded (default)").option("-r, --release", "Release build without dev client").option("-i, --install", "Install after building").action(async (platform, opts) => {
5282
5752
  validateDebugRelease(opts.debug, opts.release);
5283
5753
  const release = opts.release === true;
5284
5754
  if (opts.embeddable) {
5285
5755
  await buildEmbeddable({ release: true });
5286
5756
  return;
5287
5757
  }
5288
- await build_default2({ install: opts.install, release });
5758
+ const p = parsePlatform(platform ?? "all") ?? "all";
5759
+ if (p === "android" || p === "all") {
5760
+ await build_default({ install: opts.install, release });
5761
+ }
5762
+ if (p === "ios" || p === "all") {
5763
+ await build_default2({ install: opts.install, release });
5764
+ }
5289
5765
  });
5290
- var linkCmd = program.command("link").option("-i, --ios", "Link iOS native modules").option("-a, --android", "Link Android native modules").option("-b, --both", "Link both iOS and Android native modules").option("-s, --silent", "Run in silent mode without outputting messages").description("Link native modules to the project").action(() => {
5291
- const opts = linkCmd.opts();
5766
+ program.command("link [platform]").description("Link native modules. Platform: ios | android | both (default: both)").option("-s, --silent", "Run in silent mode (e.g. for postinstall)").action((platform, opts) => {
5292
5767
  if (opts.silent) {
5293
5768
  console.log = () => {
5294
5769
  };
@@ -5297,59 +5772,133 @@ var linkCmd = program.command("link").option("-i, --ios", "Link iOS native modul
5297
5772
  console.warn = () => {
5298
5773
  };
5299
5774
  }
5300
- if (opts.ios) {
5775
+ const p = parsePlatform(platform ?? "both") ?? "both";
5776
+ if (p === "ios") {
5301
5777
  autolink_default2();
5302
5778
  return;
5303
5779
  }
5304
- if (opts.android) {
5780
+ if (p === "android") {
5305
5781
  autolink_default();
5306
5782
  return;
5307
5783
  }
5308
5784
  autolink_default2();
5309
5785
  autolink_default();
5310
5786
  });
5311
- program.command("start").option("-v, --verbose", "Show all logs (native + JS); default shows JS only").description("Start dev server with HMR and WebSocket support (Expo-like)").action(async (opts) => {
5312
- await start_default({ verbose: opts.verbose });
5313
- });
5314
- var buildCmd = program.command("build").option("-p, --platform <platform>", "android, ios, or all (default: all)", "all").option("-e, --embeddable", "Output bundle + code snippets to embeddable/ for adding LynxView to an existing app. Use with --release.").option("-d, --debug", "Debug build with dev client embedded (default)").option("-r, --release", "Release build without dev client").option("-i, --install", "Install after building").description("Build app (unified: delegates to android/ios build)").action(async () => {
5315
- const opts = buildCmd.opts();
5787
+ program.command("bundle [platform]").description("Build Lynx bundle and copy to native project. Platform: ios | android (default: both)").option("-d, --debug", "Debug bundle with dev client embedded (default)").option("-r, --release", "Release bundle without dev client").action(async (platform, opts) => {
5316
5788
  validateDebugRelease(opts.debug, opts.release);
5317
5789
  const release = opts.release === true;
5318
- if (opts.embeddable) {
5319
- await buildEmbeddable({ release: true });
5790
+ const p = parsePlatform(platform ?? "both") ?? "both";
5791
+ if (p === "android" || p === "all") await bundle_default({ release });
5792
+ if (p === "ios" || p === "all") bundle_default2({ release });
5793
+ });
5794
+ program.command("inject <platform>").description("Inject tamer-host templates into an existing project. Platform: ios | android").option("-f, --force", "Overwrite existing files").action(async (platform, opts) => {
5795
+ const p = platform?.toLowerCase();
5796
+ if (p === "ios") {
5797
+ await injectHostIos({ force: opts.force });
5320
5798
  return;
5321
5799
  }
5322
- const p = opts.platform?.toLowerCase();
5323
- const platform = p === "ios" || p === "android" ? p : "all";
5324
- if (platform === "android" || platform === "all") {
5325
- await build_default({ install: opts.install, release });
5800
+ if (p === "android") {
5801
+ await injectHostAndroid({ force: opts.force });
5802
+ return;
5326
5803
  }
5327
- if (platform === "ios" || platform === "all") {
5328
- await build_default2({ install: opts.install, release });
5804
+ console.error(`Invalid inject platform: ${platform}. Use ios | android`);
5805
+ process.exit(1);
5806
+ });
5807
+ program.command("sync [platform]").description("Sync dev client files from tamer.config.json. Platform: android (default)").action(async (platform) => {
5808
+ const p = (platform ?? "android").toLowerCase();
5809
+ if (p !== "android") {
5810
+ console.error("sync only supports android.");
5811
+ process.exit(1);
5329
5812
  }
5813
+ await syncDevClient_default();
5814
+ });
5815
+ program.command("start").option("-v, --verbose", "Show all logs (native + JS); default shows JS only").description("Start dev server with HMR and WebSocket support (Expo-like)").action(async (opts) => {
5816
+ await start_default({ verbose: opts.verbose });
5330
5817
  });
5331
- program.command("build-dev-app").option("-p, --platform <platform>", "Platform: android, ios, or all (default)", "all").option("-i, --install", "Install APK to connected device and launch app after building").description("(Deprecated) Use: t4l build -p android -d --install").action(async (opts) => {
5332
- console.warn("\u26A0\uFE0F build-dev-app is deprecated. Use: t4l build -p android -d [--install]");
5333
- const p = opts.platform?.toLowerCase();
5334
- const platform = p === "ios" || p === "android" ? p : "all";
5335
- if (platform === "android" || platform === "all") {
5818
+ program.command("build-dev-app").option("-p, --platform <platform>", "Platform: android, ios, or all (default)", "all").option("-i, --install", "Install APK to connected device and launch app after building").description("(Deprecated) Use: t4l build android -d [--install]").action(async (opts) => {
5819
+ console.warn("\u26A0\uFE0F build-dev-app is deprecated. Use: t4l build android -d [--install]");
5820
+ const p = parsePlatform(opts.platform ?? "all") ?? "all";
5821
+ if (p === "android" || p === "all") {
5336
5822
  await build_default({ install: opts.install, release: false });
5337
5823
  }
5338
- if (platform === "ios" || platform === "all") {
5824
+ if (p === "ios" || p === "all") {
5339
5825
  await build_default2({ install: opts.install, release: false });
5340
5826
  }
5341
5827
  });
5342
5828
  program.command("add [packages...]").description("Add @tamer4lynx packages to the Lynx project. Future: will track versions for compatibility (Expo-style).").action((packages) => add(packages));
5343
- program.command("add-core").description("Add core packages (app-shell, screen, router, insets, transports, input/text-input, system-ui, icons)").action(() => addCore());
5344
- program.command("create").description("Create a new Lynx extension project (RFC-compliant)").action(() => create_default3());
5829
+ program.command("add-core").description("Add core packages (app-shell, screen, router, insets, transports, system-ui, icons)").action(() => addCore());
5345
5830
  program.command("codegen").description("Generate code from @lynxmodule declarations").action(() => {
5346
5831
  codegen_default();
5347
5832
  });
5833
+ program.command("android <subcommand>").description("(Legacy) Use: t4l <command> android. e.g. t4l create android").option("-d, --debug", "Create: host project. Bundle/build: debug with dev client.").option("-r, --release", "Create: dev-app project. Bundle/build: release without dev client.").option("-i, --install", "Install after build").option("-e, --embeddable", "Build embeddable").option("-f, --force", "Force (inject)").action(async (subcommand, opts) => {
5834
+ const sub = subcommand?.toLowerCase();
5835
+ if (sub === "create") {
5836
+ if (opts.debug && opts.release) {
5837
+ console.error("Cannot use --debug and --release together.");
5838
+ process.exit(1);
5839
+ }
5840
+ await create_default({ target: opts.release ? "dev-app" : "host" });
5841
+ return;
5842
+ }
5843
+ if (sub === "link") {
5844
+ autolink_default();
5845
+ return;
5846
+ }
5847
+ if (sub === "bundle") {
5848
+ validateDebugRelease(opts.debug, opts.release);
5849
+ await bundle_default({ release: opts.release === true });
5850
+ return;
5851
+ }
5852
+ if (sub === "build") {
5853
+ validateDebugRelease(opts.debug, opts.release);
5854
+ if (opts.embeddable) await buildEmbeddable({ release: true });
5855
+ else await build_default({ install: opts.install, release: opts.release === true });
5856
+ return;
5857
+ }
5858
+ if (sub === "sync") {
5859
+ await syncDevClient_default();
5860
+ return;
5861
+ }
5862
+ if (sub === "inject") {
5863
+ await injectHostAndroid({ force: opts.force });
5864
+ return;
5865
+ }
5866
+ console.error(`Unknown android subcommand: ${subcommand}. Use: create | link | bundle | build | sync | inject`);
5867
+ process.exit(1);
5868
+ });
5869
+ program.command("ios <subcommand>").description("(Legacy) Use: t4l <command> ios. e.g. t4l create ios").option("-d, --debug", "Debug (bundle/build)").option("-r, --release", "Release (bundle/build)").option("-i, --install", "Install after build").option("-e, --embeddable", "Build embeddable").option("-f, --force", "Force (inject)").action(async (subcommand, opts) => {
5870
+ const sub = subcommand?.toLowerCase();
5871
+ if (sub === "create") {
5872
+ create_default2();
5873
+ return;
5874
+ }
5875
+ if (sub === "link") {
5876
+ autolink_default2();
5877
+ return;
5878
+ }
5879
+ if (sub === "bundle") {
5880
+ validateDebugRelease(opts.debug, opts.release);
5881
+ bundle_default2({ release: opts.release === true });
5882
+ return;
5883
+ }
5884
+ if (sub === "build") {
5885
+ validateDebugRelease(opts.debug, opts.release);
5886
+ if (opts.embeddable) await buildEmbeddable({ release: true });
5887
+ else await build_default2({ install: opts.install, release: opts.release === true });
5888
+ return;
5889
+ }
5890
+ if (sub === "inject") {
5891
+ await injectHostIos({ force: opts.force });
5892
+ return;
5893
+ }
5894
+ console.error(`Unknown ios subcommand: ${subcommand}. Use: create | link | bundle | build | inject`);
5895
+ process.exit(1);
5896
+ });
5348
5897
  program.command("autolink-toggle").alias("autolink").description("Toggle autolink on/off in tamer.config.json (controls postinstall linking)").action(async () => {
5349
- const configPath = path24.join(process.cwd(), "tamer.config.json");
5898
+ const configPath = path25.join(process.cwd(), "tamer.config.json");
5350
5899
  let config = {};
5351
- if (fs23.existsSync(configPath)) {
5352
- config = JSON.parse(fs23.readFileSync(configPath, "utf8"));
5900
+ if (fs24.existsSync(configPath)) {
5901
+ config = JSON.parse(fs24.readFileSync(configPath, "utf8"));
5353
5902
  }
5354
5903
  if (config.autolink) {
5355
5904
  delete config.autolink;
@@ -5358,7 +5907,7 @@ program.command("autolink-toggle").alias("autolink").description("Toggle autolin
5358
5907
  config.autolink = true;
5359
5908
  console.log("Autolink enabled in tamer.config.json");
5360
5909
  }
5361
- fs23.writeFileSync(configPath, JSON.stringify(config, null, 2));
5910
+ fs24.writeFileSync(configPath, JSON.stringify(config, null, 2));
5362
5911
  console.log(`Updated ${configPath}`);
5363
5912
  });
5364
5913
  if (process.argv.length <= 2 || process.argv.length === 3 && process.argv[2] === "init") {