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