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