@tamer4lynx/cli 0.0.1
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/LICENSE +21 -0
- package/README.md +307 -0
- package/dist/android/autolink.js +272 -0
- package/dist/android/build.js +36 -0
- package/dist/android/bundle.js +99 -0
- package/dist/android/coreElements.js +129 -0
- package/dist/android/create.js +423 -0
- package/dist/android/getGradle.js +92 -0
- package/dist/android/postinstall-and.js +7 -0
- package/dist/android/postinstall.js +7 -0
- package/dist/android/syncDevClient.js +70 -0
- package/dist/common/buildDevApp.js +43 -0
- package/dist/common/codegen.js +69 -0
- package/dist/common/config.js +113 -0
- package/dist/common/create.js +170 -0
- package/dist/common/devServer.js +231 -0
- package/dist/common/hostConfig.js +256 -0
- package/dist/common/init.js +65 -0
- package/dist/common/postinstall.js +39 -0
- package/dist/common/start.js +5 -0
- package/dist/explorer/devLauncher.js +47 -0
- package/dist/explorer/patches.js +400 -0
- package/dist/explorer/ref.js +9 -0
- package/dist/index.js +6381 -0
- package/dist/ios/autolink.js +246 -0
- package/dist/ios/build.js +31 -0
- package/dist/ios/bundle.js +73 -0
- package/dist/ios/create.js +597 -0
- package/dist/ios/getPod.js +53 -0
- package/dist/ios/postinstall.js +7 -0
- package/package.json +92 -0
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { execSync } from 'child_process';
|
|
4
|
+
import { resolveHostPaths, resolveDevAppPaths } from '../common/hostConfig';
|
|
5
|
+
import android_autolink from './autolink';
|
|
6
|
+
import android_create from './create';
|
|
7
|
+
import android_syncDevClient from './syncDevClient';
|
|
8
|
+
function findRepoRoot(start) {
|
|
9
|
+
let dir = path.resolve(start);
|
|
10
|
+
const root = path.parse(dir).root;
|
|
11
|
+
while (dir !== root) {
|
|
12
|
+
const pkgPath = path.join(dir, 'package.json');
|
|
13
|
+
if (fs.existsSync(pkgPath)) {
|
|
14
|
+
try {
|
|
15
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
|
|
16
|
+
if (pkg.workspaces)
|
|
17
|
+
return dir;
|
|
18
|
+
}
|
|
19
|
+
catch { }
|
|
20
|
+
}
|
|
21
|
+
dir = path.dirname(dir);
|
|
22
|
+
}
|
|
23
|
+
return start;
|
|
24
|
+
}
|
|
25
|
+
async function bundleAndDeploy(opts = {}) {
|
|
26
|
+
const target = (opts.target ?? 'host');
|
|
27
|
+
const origCwd = process.cwd();
|
|
28
|
+
let resolved;
|
|
29
|
+
try {
|
|
30
|
+
if (target === 'dev-app') {
|
|
31
|
+
const repoRoot = findRepoRoot(origCwd);
|
|
32
|
+
resolved = resolveDevAppPaths(repoRoot);
|
|
33
|
+
const devAppDir = resolved.projectRoot;
|
|
34
|
+
const androidDir = resolved.androidDir;
|
|
35
|
+
if (!fs.existsSync(androidDir)) {
|
|
36
|
+
console.log('📱 Creating Tamer Dev App Android project...');
|
|
37
|
+
await android_create({ target: 'dev-app' });
|
|
38
|
+
}
|
|
39
|
+
process.chdir(devAppDir);
|
|
40
|
+
}
|
|
41
|
+
else {
|
|
42
|
+
resolved = resolveHostPaths();
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
catch (error) {
|
|
46
|
+
console.error(`❌ Error loading configuration: ${error.message}`);
|
|
47
|
+
process.exit(1);
|
|
48
|
+
}
|
|
49
|
+
const { lynxProjectDir, lynxBundlePath, androidAssetsDir, devClientBundlePath, devMode } = resolved;
|
|
50
|
+
const destinationDir = androidAssetsDir;
|
|
51
|
+
android_autolink();
|
|
52
|
+
if (devMode === 'embedded') {
|
|
53
|
+
await android_syncDevClient();
|
|
54
|
+
}
|
|
55
|
+
try {
|
|
56
|
+
console.log('📦 Building Lynx project...');
|
|
57
|
+
execSync('npm run build', { stdio: 'inherit', cwd: lynxProjectDir });
|
|
58
|
+
console.log('✅ Build completed successfully.');
|
|
59
|
+
}
|
|
60
|
+
catch (error) {
|
|
61
|
+
console.error('❌ Build process failed.');
|
|
62
|
+
process.exit(1);
|
|
63
|
+
}
|
|
64
|
+
if (target === 'dev-app') {
|
|
65
|
+
process.chdir(origCwd);
|
|
66
|
+
}
|
|
67
|
+
if (target !== 'dev-app' && devMode === 'embedded' && devClientBundlePath && !fs.existsSync(devClientBundlePath)) {
|
|
68
|
+
const devClientDir = path.dirname(path.dirname(devClientBundlePath));
|
|
69
|
+
try {
|
|
70
|
+
console.log('📦 Building dev launcher (tamer-dev-client)...');
|
|
71
|
+
execSync('npm run build', { stdio: 'inherit', cwd: devClientDir });
|
|
72
|
+
console.log('✅ Dev launcher build completed.');
|
|
73
|
+
}
|
|
74
|
+
catch (error) {
|
|
75
|
+
console.error('❌ Dev launcher build failed.');
|
|
76
|
+
process.exit(1);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
try {
|
|
80
|
+
fs.mkdirSync(destinationDir, { recursive: true });
|
|
81
|
+
if (target !== 'dev-app' && devMode === 'embedded' && devClientBundlePath && fs.existsSync(devClientBundlePath)) {
|
|
82
|
+
fs.copyFileSync(devClientBundlePath, path.join(destinationDir, 'dev-client.lynx.bundle'));
|
|
83
|
+
console.log(`✨ Copied dev-client.lynx.bundle to assets`);
|
|
84
|
+
}
|
|
85
|
+
if (!fs.existsSync(lynxBundlePath)) {
|
|
86
|
+
console.error(`❌ Build output not found at: ${lynxBundlePath}`);
|
|
87
|
+
process.exit(1);
|
|
88
|
+
}
|
|
89
|
+
fs.copyFileSync(lynxBundlePath, path.join(destinationDir, resolved.lynxBundleFile));
|
|
90
|
+
console.log(`✨ Copied ${resolved.lynxBundleFile} to assets`);
|
|
91
|
+
}
|
|
92
|
+
catch (error) {
|
|
93
|
+
console.error(`❌ Failed to copy bundle: ${error.message}`);
|
|
94
|
+
process.exit(1);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
export default bundleAndDeploy;
|
|
98
|
+
// // --- Main Execution ---
|
|
99
|
+
// bundleAndDeploy();
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
export function getLynxExplorerInputSource(packageName) {
|
|
2
|
+
return `package ${packageName}.core
|
|
3
|
+
|
|
4
|
+
import android.content.Context
|
|
5
|
+
import android.graphics.Color
|
|
6
|
+
import android.text.Editable
|
|
7
|
+
import android.text.TextWatcher
|
|
8
|
+
import android.util.TypedValue
|
|
9
|
+
import android.view.Gravity
|
|
10
|
+
import android.view.View
|
|
11
|
+
import android.view.inputmethod.EditorInfo
|
|
12
|
+
import android.view.inputmethod.InputMethodManager
|
|
13
|
+
import androidx.appcompat.widget.AppCompatEditText
|
|
14
|
+
import com.lynx.react.bridge.Callback
|
|
15
|
+
import com.lynx.react.bridge.ReadableMap
|
|
16
|
+
import com.lynx.tasm.behavior.LynxContext
|
|
17
|
+
import com.lynx.tasm.behavior.LynxProp
|
|
18
|
+
import com.lynx.tasm.behavior.LynxUIMethod
|
|
19
|
+
import com.lynx.tasm.behavior.LynxUIMethodConstants
|
|
20
|
+
import com.lynx.tasm.behavior.ui.LynxUI
|
|
21
|
+
import com.lynx.tasm.event.LynxCustomEvent
|
|
22
|
+
|
|
23
|
+
class LynxExplorerInput(context: LynxContext) : LynxUI<AppCompatEditText>(context) {
|
|
24
|
+
|
|
25
|
+
override fun createView(context: Context): AppCompatEditText {
|
|
26
|
+
val view = AppCompatEditText(context)
|
|
27
|
+
view.setLines(1)
|
|
28
|
+
view.isSingleLine = true
|
|
29
|
+
view.gravity = Gravity.CENTER_VERTICAL
|
|
30
|
+
view.background = null
|
|
31
|
+
view.imeOptions = EditorInfo.IME_ACTION_NONE
|
|
32
|
+
view.setHorizontallyScrolling(true)
|
|
33
|
+
view.setPadding(0, 0, 0, 0)
|
|
34
|
+
view.setTextSize(TypedValue.COMPLEX_UNIT_SP, 16f)
|
|
35
|
+
view.minHeight = TypedValue.applyDimension(
|
|
36
|
+
TypedValue.COMPLEX_UNIT_DIP,
|
|
37
|
+
52f,
|
|
38
|
+
context.resources.displayMetrics
|
|
39
|
+
).toInt()
|
|
40
|
+
view.setTextColor(Color.WHITE)
|
|
41
|
+
view.setHintTextColor(Color.argb(160, 255, 255, 255))
|
|
42
|
+
view.includeFontPadding = false
|
|
43
|
+
view.isFocusableInTouchMode = true
|
|
44
|
+
view.addTextChangedListener(object : TextWatcher {
|
|
45
|
+
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
|
|
46
|
+
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {}
|
|
47
|
+
override fun afterTextChanged(s: Editable?) {
|
|
48
|
+
emitEvent("input", mapOf("value" to (s?.toString() ?: "")))
|
|
49
|
+
}
|
|
50
|
+
})
|
|
51
|
+
view.setOnFocusChangeListener { _: View?, hasFocus: Boolean ->
|
|
52
|
+
if (!hasFocus) emitEvent("blur", null)
|
|
53
|
+
}
|
|
54
|
+
return view
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
override fun onLayoutUpdated() {
|
|
58
|
+
super.onLayoutUpdated()
|
|
59
|
+
val paddingTop = mPaddingTop + mBorderTopWidth
|
|
60
|
+
val paddingBottom = mPaddingBottom + mBorderBottomWidth
|
|
61
|
+
val paddingLeft = mPaddingLeft + mBorderLeftWidth
|
|
62
|
+
val paddingRight = mPaddingRight + mBorderRightWidth
|
|
63
|
+
mView.setPadding(paddingLeft, paddingTop, paddingRight, paddingBottom)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
@LynxProp(name = "value")
|
|
67
|
+
fun setValue(value: String) {
|
|
68
|
+
if (value != mView.text.toString()) {
|
|
69
|
+
mView.setText(value)
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
@LynxProp(name = "placeholder")
|
|
74
|
+
fun setPlaceholder(value: String) {
|
|
75
|
+
mView.hint = value
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
@LynxUIMethod
|
|
79
|
+
fun focus(params: ReadableMap?, callback: Callback) {
|
|
80
|
+
if (mView.requestFocus()) {
|
|
81
|
+
if (showSoftInput()) {
|
|
82
|
+
callback.invoke(LynxUIMethodConstants.SUCCESS)
|
|
83
|
+
} else {
|
|
84
|
+
callback.invoke(LynxUIMethodConstants.UNKNOWN, "fail to show keyboard")
|
|
85
|
+
}
|
|
86
|
+
} else {
|
|
87
|
+
callback.invoke(LynxUIMethodConstants.UNKNOWN, "fail to focus")
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
private fun showSoftInput(): Boolean {
|
|
92
|
+
val imm = lynxContext.getSystemService(Context.INPUT_METHOD_SERVICE) as? InputMethodManager
|
|
93
|
+
?: return false
|
|
94
|
+
return imm.showSoftInput(mView, InputMethodManager.SHOW_IMPLICIT, null)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
private fun emitEvent(name: String, detail: Map<String, Any>?) {
|
|
98
|
+
val event = LynxCustomEvent(sign, name)
|
|
99
|
+
detail?.forEach { (k, v) -> event.addDetail(k, v) }
|
|
100
|
+
lynxContext.eventEmitter.sendCustomEvent(event)
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
`;
|
|
104
|
+
}
|
|
105
|
+
export function getAppBarElementSource(packageName) {
|
|
106
|
+
return `package ${packageName}.core
|
|
107
|
+
|
|
108
|
+
import android.content.Context
|
|
109
|
+
import android.view.View
|
|
110
|
+
import androidx.core.view.ViewCompat
|
|
111
|
+
import androidx.core.view.WindowInsetsCompat
|
|
112
|
+
import com.lynx.tasm.behavior.LynxContext
|
|
113
|
+
import com.lynx.tasm.behavior.ui.LynxUI
|
|
114
|
+
|
|
115
|
+
class AppBarElement(context: LynxContext) : LynxUI<View>(context) {
|
|
116
|
+
override fun createView(context: Context): View {
|
|
117
|
+
return View(context).apply {
|
|
118
|
+
minimumHeight = (56 * context.resources.displayMetrics.density).toInt()
|
|
119
|
+
ViewCompat.setOnApplyWindowInsetsListener(this) { v, insets ->
|
|
120
|
+
val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
|
|
121
|
+
v.setPadding(systemBars.left, systemBars.top, systemBars.right, 0)
|
|
122
|
+
insets
|
|
123
|
+
}
|
|
124
|
+
requestApplyInsets()
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
`;
|
|
129
|
+
}
|
|
@@ -0,0 +1,423 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import os from "os";
|
|
4
|
+
import { setupGradleWrapper } from "./getGradle";
|
|
5
|
+
import { loadHostConfig, resolveDevMode, resolveHostPaths, resolveIconPaths } from "../common/hostConfig";
|
|
6
|
+
import { fetchAndPatchApplication, fetchAndPatchTemplateProvider, getDevClientManager, getProjectActivity, getStandaloneMainActivity, } from "../explorer/patches";
|
|
7
|
+
import { getDevServerPrefs } from "../explorer/devLauncher";
|
|
8
|
+
import { getLynxExplorerInputSource } from "./coreElements";
|
|
9
|
+
function findRepoRoot(start) {
|
|
10
|
+
let dir = path.resolve(start);
|
|
11
|
+
const root = path.parse(dir).root;
|
|
12
|
+
while (dir !== root) {
|
|
13
|
+
const pkgPath = path.join(dir, "package.json");
|
|
14
|
+
if (fs.existsSync(pkgPath)) {
|
|
15
|
+
try {
|
|
16
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8"));
|
|
17
|
+
if (pkg.workspaces)
|
|
18
|
+
return dir;
|
|
19
|
+
}
|
|
20
|
+
catch { }
|
|
21
|
+
}
|
|
22
|
+
dir = path.dirname(dir);
|
|
23
|
+
}
|
|
24
|
+
return start;
|
|
25
|
+
}
|
|
26
|
+
const create = async (opts = {}) => {
|
|
27
|
+
const target = opts.target ?? "host";
|
|
28
|
+
const origCwd = process.cwd();
|
|
29
|
+
if (target === "dev-app") {
|
|
30
|
+
const repoRoot = findRepoRoot(origCwd);
|
|
31
|
+
const devAppDir = path.join(repoRoot, "packages", "tamer-dev-app");
|
|
32
|
+
if (!fs.existsSync(path.join(devAppDir, "tamer.config.json"))) {
|
|
33
|
+
console.error("❌ packages/tamer-dev-app/tamer.config.json not found.");
|
|
34
|
+
process.exit(1);
|
|
35
|
+
}
|
|
36
|
+
process.chdir(devAppDir);
|
|
37
|
+
}
|
|
38
|
+
let appName;
|
|
39
|
+
let packageName;
|
|
40
|
+
let androidSdk;
|
|
41
|
+
let config;
|
|
42
|
+
try {
|
|
43
|
+
config = loadHostConfig();
|
|
44
|
+
packageName = config.android?.packageName;
|
|
45
|
+
appName = config.android?.appName;
|
|
46
|
+
androidSdk = config.android?.sdk;
|
|
47
|
+
if (androidSdk && androidSdk.startsWith("~")) {
|
|
48
|
+
androidSdk = androidSdk.replace(/^~/, os.homedir());
|
|
49
|
+
}
|
|
50
|
+
if (!appName || !packageName) {
|
|
51
|
+
throw new Error('"android.appName" and "android.packageName" must be defined in tamer.config.json');
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
catch (error) {
|
|
55
|
+
console.error(`❌ Error loading configuration: ${error.message}`);
|
|
56
|
+
process.exit(1);
|
|
57
|
+
}
|
|
58
|
+
const packagePath = packageName.replace(/\./g, "/");
|
|
59
|
+
const gradleVersion = "8.14.2";
|
|
60
|
+
const androidDir = config.paths?.androidDir ?? "android";
|
|
61
|
+
const rootDir = path.join(process.cwd(), androidDir);
|
|
62
|
+
const appDir = path.join(rootDir, "app");
|
|
63
|
+
const mainDir = path.join(appDir, "src", "main");
|
|
64
|
+
const javaDir = path.join(mainDir, "java", packagePath);
|
|
65
|
+
const kotlinDir = path.join(mainDir, "kotlin", packagePath);
|
|
66
|
+
const kotlinGeneratedDir = path.join(kotlinDir, "generated");
|
|
67
|
+
const assetsDir = path.join(mainDir, "assets");
|
|
68
|
+
const themesDir = path.join(mainDir, "res", "values");
|
|
69
|
+
const gradleDir = path.join(rootDir, "gradle");
|
|
70
|
+
function writeFile(filePath, content, options) {
|
|
71
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
72
|
+
fs.writeFileSync(filePath, content.trimStart(), options?.encoding ?? "utf8");
|
|
73
|
+
}
|
|
74
|
+
// Clean up previous generation if it exists
|
|
75
|
+
if (fs.existsSync(rootDir)) {
|
|
76
|
+
console.log(`🧹 Removing existing directory: ${rootDir}`);
|
|
77
|
+
fs.rmSync(rootDir, { recursive: true, force: true });
|
|
78
|
+
}
|
|
79
|
+
console.log(`🚀 Creating a new Tamer4Lynx project in: ${rootDir}`);
|
|
80
|
+
// --- Start File Generation ---
|
|
81
|
+
// gradle/libs.versions.toml
|
|
82
|
+
writeFile(path.join(gradleDir, "libs.versions.toml"), `
|
|
83
|
+
[versions]
|
|
84
|
+
agp = "8.9.1"
|
|
85
|
+
commonsCompress = "1.26.1"
|
|
86
|
+
commonsLang3 = "3.14.0"
|
|
87
|
+
fresco = "2.3.0"
|
|
88
|
+
kotlin = "2.0.21"
|
|
89
|
+
coreKtx = "1.10.1"
|
|
90
|
+
junit = "4.13.2"
|
|
91
|
+
junitVersion = "1.1.5"
|
|
92
|
+
espressoCore = "3.5.1"
|
|
93
|
+
appcompat = "1.6.1"
|
|
94
|
+
lynx = "3.3.1"
|
|
95
|
+
material = "1.10.0"
|
|
96
|
+
activity = "1.8.0"
|
|
97
|
+
constraintlayout = "2.1.4"
|
|
98
|
+
okhttp = "4.9.0"
|
|
99
|
+
primjs = "2.12.0"
|
|
100
|
+
zxing = "4.3.0"
|
|
101
|
+
|
|
102
|
+
[libraries]
|
|
103
|
+
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
|
|
104
|
+
animated-base = { module = "com.facebook.fresco:animated-base", version.ref = "fresco" }
|
|
105
|
+
animated-gif = { module = "com.facebook.fresco:animated-gif", version.ref = "fresco" }
|
|
106
|
+
animated-webp = { module = "com.facebook.fresco:animated-webp", version.ref = "fresco" }
|
|
107
|
+
commons-compress = { module = "org.apache.commons:commons-compress", version.ref = "commonsCompress" }
|
|
108
|
+
commons-lang3 = { module = "org.apache.commons:commons-lang3", version.ref = "commonsLang3" }
|
|
109
|
+
fresco = { module = "com.facebook.fresco:fresco", version.ref = "fresco" }
|
|
110
|
+
junit = { group = "junit", name = "junit", version.ref = "junit" }
|
|
111
|
+
androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" }
|
|
112
|
+
androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" }
|
|
113
|
+
androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" }
|
|
114
|
+
lynx = { module = "org.lynxsdk.lynx:lynx", version.ref = "lynx" }
|
|
115
|
+
lynx-jssdk = { module = "org.lynxsdk.lynx:lynx-jssdk", version.ref = "lynx" }
|
|
116
|
+
lynx-processor = { module = "org.lynxsdk.lynx:lynx-processor", version.ref = "lynx" }
|
|
117
|
+
lynx-service-http = { module = "org.lynxsdk.lynx:lynx-service-http", version.ref = "lynx" }
|
|
118
|
+
lynx-service-image = { module = "org.lynxsdk.lynx:lynx-service-image", version.ref = "lynx" }
|
|
119
|
+
lynx-service-log = { module = "org.lynxsdk.lynx:lynx-service-log", version.ref = "lynx" }
|
|
120
|
+
lynx-trace = { module = "org.lynxsdk.lynx:lynx-trace", version.ref = "lynx" }
|
|
121
|
+
material = { group = "com.google.android.material", name = "material", version.ref = "material" }
|
|
122
|
+
androidx-activity = { group = "androidx.activity", name = "activity", version.ref = "activity" }
|
|
123
|
+
androidx-constraintlayout = { group = "androidx.constraintlayout", name = "constraintlayout", version.ref = "constraintlayout" }
|
|
124
|
+
okhttp = { module = "com.squareup.okhttp3:okhttp", version.ref = "okhttp" }
|
|
125
|
+
primjs = { module = "org.lynxsdk.lynx:primjs", version.ref = "primjs" }
|
|
126
|
+
webpsupport = { module = "com.facebook.fresco:webpsupport", version.ref = "fresco" }
|
|
127
|
+
zxing = { module = "com.journeyapps:zxing-android-embedded", version.ref = "zxing" }
|
|
128
|
+
|
|
129
|
+
[plugins]
|
|
130
|
+
android-application = { id = "com.android.application", version.ref = "agp" }
|
|
131
|
+
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
|
|
132
|
+
kotlin-kapt = { id = "org.jetbrains.kotlin.kapt", version.ref = "kotlin" }
|
|
133
|
+
`);
|
|
134
|
+
// settings.gradle.kts (with GENERATED block)
|
|
135
|
+
writeFile(path.join(rootDir, "settings.gradle.kts"), `
|
|
136
|
+
pluginManagement {
|
|
137
|
+
repositories {
|
|
138
|
+
google {
|
|
139
|
+
content {
|
|
140
|
+
includeGroupByRegex("com\\\\.android.*")
|
|
141
|
+
includeGroupByRegex("com\\\\.google.*")
|
|
142
|
+
includeGroupByRegex("androidx.*")
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
mavenCentral()
|
|
146
|
+
gradlePluginPortal()
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
dependencyResolutionManagement {
|
|
150
|
+
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
|
|
151
|
+
repositories {
|
|
152
|
+
google()
|
|
153
|
+
mavenCentral()
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
rootProject.name = "${appName}"
|
|
158
|
+
include(":app")
|
|
159
|
+
|
|
160
|
+
// GENERATED AUTOLINK START
|
|
161
|
+
// This section is automatically generated by Tamer4Lynx.
|
|
162
|
+
// Manual edits will be overwritten.
|
|
163
|
+
println("If you have native modules please run tamer android link")
|
|
164
|
+
// GENERATED AUTOLINK END
|
|
165
|
+
`);
|
|
166
|
+
// Root build.gradle
|
|
167
|
+
writeFile(path.join(rootDir, "build.gradle.kts"), `
|
|
168
|
+
// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
|
169
|
+
plugins {
|
|
170
|
+
alias(libs.plugins.android.application) apply false
|
|
171
|
+
alias(libs.plugins.kotlin.android) apply false
|
|
172
|
+
}
|
|
173
|
+
`);
|
|
174
|
+
// gradle.properties
|
|
175
|
+
writeFile(path.join(rootDir, "gradle.properties"), `
|
|
176
|
+
org.gradle.jvmargs=-Xmx2048m
|
|
177
|
+
android.useAndroidX=true
|
|
178
|
+
kotlin.code.style=official
|
|
179
|
+
android.enableJetifier=true
|
|
180
|
+
`);
|
|
181
|
+
// app/build.gradle.kts (UPDATED)
|
|
182
|
+
writeFile(path.join(appDir, "build.gradle.kts"), `
|
|
183
|
+
plugins {
|
|
184
|
+
alias(libs.plugins.android.application)
|
|
185
|
+
alias(libs.plugins.kotlin.android)
|
|
186
|
+
id("org.jetbrains.kotlin.kapt")
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
android {
|
|
190
|
+
namespace = "${packageName}"
|
|
191
|
+
compileSdk = 35
|
|
192
|
+
|
|
193
|
+
defaultConfig {
|
|
194
|
+
applicationId = "${packageName}"
|
|
195
|
+
minSdk = 28
|
|
196
|
+
targetSdk = 35
|
|
197
|
+
versionCode = 1
|
|
198
|
+
versionName = "1.0"
|
|
199
|
+
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
|
200
|
+
ndk {
|
|
201
|
+
abiFilters += listOf("armeabi-v7a", "arm64-v8a")
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
buildTypes {
|
|
206
|
+
release {
|
|
207
|
+
isMinifyEnabled = false
|
|
208
|
+
proguardFiles(
|
|
209
|
+
getDefaultProguardFile("proguard-android-optimize.txt"),
|
|
210
|
+
"proguard-rules.pro"
|
|
211
|
+
)
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
compileOptions {
|
|
216
|
+
sourceCompatibility = JavaVersion.VERSION_17
|
|
217
|
+
targetCompatibility = JavaVersion.VERSION_17
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
kotlinOptions {
|
|
221
|
+
jvmTarget = "17"
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
buildFeatures {
|
|
225
|
+
buildConfig = true
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
sourceSets {
|
|
229
|
+
getByName("main") {
|
|
230
|
+
jniLibs.srcDirs("src/main/jniLibs")
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
dependencies {
|
|
236
|
+
implementation(libs.androidx.core.ktx)
|
|
237
|
+
implementation(libs.androidx.appcompat)
|
|
238
|
+
implementation(libs.material)
|
|
239
|
+
implementation(libs.androidx.activity)
|
|
240
|
+
implementation(libs.androidx.constraintlayout)
|
|
241
|
+
testImplementation(libs.junit)
|
|
242
|
+
androidTestImplementation(libs.androidx.junit)
|
|
243
|
+
androidTestImplementation(libs.androidx.espresso.core)
|
|
244
|
+
implementation(libs.lynx)
|
|
245
|
+
implementation(libs.lynx.jssdk)
|
|
246
|
+
implementation(libs.lynx.trace)
|
|
247
|
+
implementation(libs.primjs)
|
|
248
|
+
implementation(libs.lynx.service.image)
|
|
249
|
+
implementation(libs.fresco)
|
|
250
|
+
implementation(libs.animated.gif)
|
|
251
|
+
implementation(libs.animated.webp)
|
|
252
|
+
implementation(libs.webpsupport)
|
|
253
|
+
implementation(libs.animated.base)
|
|
254
|
+
implementation(libs.lynx.service.log)
|
|
255
|
+
implementation(libs.lynx.service.http)
|
|
256
|
+
implementation(libs.okhttp)
|
|
257
|
+
implementation(libs.zxing)
|
|
258
|
+
kapt(libs.lynx.processor)
|
|
259
|
+
implementation(libs.commons.lang3)
|
|
260
|
+
implementation(libs.commons.compress)
|
|
261
|
+
|
|
262
|
+
// GENERATED AUTOLINK DEPENDENCIES START
|
|
263
|
+
// This section is automatically generated by Tamer4Lynx.
|
|
264
|
+
// Manual edits will be overwritten.
|
|
265
|
+
// GENERATED AUTOLINK DEPENDENCIES END
|
|
266
|
+
}
|
|
267
|
+
`);
|
|
268
|
+
// themes.xml
|
|
269
|
+
writeFile(path.join(themesDir, "themes.xml"), `
|
|
270
|
+
<resources>
|
|
271
|
+
<style name="Theme.MyApp" parent="Theme.AppCompat.Light.NoActionBar">
|
|
272
|
+
<item name="android:statusBarColor">@android:color/transparent</item>
|
|
273
|
+
<item name="android:windowLightStatusBar">false</item>
|
|
274
|
+
<item name="android:navigationBarColor">@android:color/transparent</item>
|
|
275
|
+
</style>
|
|
276
|
+
</resources>
|
|
277
|
+
`);
|
|
278
|
+
const devMode = resolveDevMode(config);
|
|
279
|
+
const hasDevLauncher = devMode === "embedded";
|
|
280
|
+
const manifestActivities = hasDevLauncher
|
|
281
|
+
? `
|
|
282
|
+
<activity android:name=".MainActivity" android:exported="true">
|
|
283
|
+
<intent-filter>
|
|
284
|
+
<action android:name="android.intent.action.MAIN" />
|
|
285
|
+
<category android:name="android.intent.category.LAUNCHER" />
|
|
286
|
+
</intent-filter>
|
|
287
|
+
</activity>
|
|
288
|
+
<activity android:name=".ProjectActivity" android:exported="false" android:taskAffinity="" android:launchMode="singleTask" android:documentLaunchMode="always" />
|
|
289
|
+
`
|
|
290
|
+
: `
|
|
291
|
+
<activity android:name=".MainActivity" android:exported="true">
|
|
292
|
+
<intent-filter>
|
|
293
|
+
<action android:name="android.intent.action.MAIN" />
|
|
294
|
+
<category android:name="android.intent.category.LAUNCHER" />
|
|
295
|
+
</intent-filter>
|
|
296
|
+
</activity>
|
|
297
|
+
`;
|
|
298
|
+
const manifestPermissions = hasDevLauncher
|
|
299
|
+
? ` <uses-permission android:name="android.permission.INTERNET" />
|
|
300
|
+
<uses-permission android:name="android.permission.CAMERA" />`
|
|
301
|
+
: ` <uses-permission android:name="android.permission.INTERNET" />`;
|
|
302
|
+
const iconPaths = resolveIconPaths(process.cwd(), config);
|
|
303
|
+
const manifestIconAttrs = iconPaths
|
|
304
|
+
? " android:icon=\"@mipmap/ic_launcher\"\n android:roundIcon=\"@mipmap/ic_launcher\"\n"
|
|
305
|
+
: "";
|
|
306
|
+
writeFile(path.join(mainDir, "AndroidManifest.xml"), `
|
|
307
|
+
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
|
308
|
+
${manifestPermissions}
|
|
309
|
+
<application
|
|
310
|
+
android:name=".App"
|
|
311
|
+
android:label="${appName}"
|
|
312
|
+
${manifestIconAttrs} android:usesCleartextTraffic="true"
|
|
313
|
+
android:theme="@style/Theme.MyApp">${manifestActivities}
|
|
314
|
+
</application>
|
|
315
|
+
</manifest>
|
|
316
|
+
`);
|
|
317
|
+
// Placeholder GeneratedLynxExtensions.kt
|
|
318
|
+
writeFile(path.join(kotlinGeneratedDir, "GeneratedLynxExtensions.kt"), `
|
|
319
|
+
package ${packageName}.generated
|
|
320
|
+
|
|
321
|
+
import android.content.Context
|
|
322
|
+
import com.lynx.tasm.LynxEnv
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* This file is generated by the Tamer4Lynx autolinker.
|
|
326
|
+
* Do not edit this file manually.
|
|
327
|
+
*/
|
|
328
|
+
object GeneratedLynxExtensions {
|
|
329
|
+
fun register(context: Context) {
|
|
330
|
+
// This will be populated by the autolinker.
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
`);
|
|
334
|
+
const devServer = config.devServer
|
|
335
|
+
? {
|
|
336
|
+
host: config.devServer.host ?? "10.0.2.2",
|
|
337
|
+
port: config.devServer.port ?? config.devServer.httpPort ?? 3000,
|
|
338
|
+
}
|
|
339
|
+
: undefined;
|
|
340
|
+
const resolved = resolveHostPaths(process.cwd());
|
|
341
|
+
const vars = { packageName, appName, devMode, devServer, projectRoot: resolved.lynxProjectDir };
|
|
342
|
+
const [applicationSource, templateProviderSource] = await Promise.all([
|
|
343
|
+
fetchAndPatchApplication(vars),
|
|
344
|
+
fetchAndPatchTemplateProvider(vars),
|
|
345
|
+
]);
|
|
346
|
+
writeFile(path.join(javaDir, "App.java"), applicationSource);
|
|
347
|
+
writeFile(path.join(javaDir, "TemplateProvider.java"), templateProviderSource);
|
|
348
|
+
writeFile(path.join(kotlinDir, "MainActivity.kt"), getStandaloneMainActivity(vars));
|
|
349
|
+
if (hasDevLauncher) {
|
|
350
|
+
writeFile(path.join(kotlinDir, "ProjectActivity.kt"), getProjectActivity(vars));
|
|
351
|
+
}
|
|
352
|
+
const coreDir = path.join(kotlinDir, "core");
|
|
353
|
+
writeFile(path.join(coreDir, "LynxExplorerInput.kt"), getLynxExplorerInputSource(packageName));
|
|
354
|
+
const devClientManagerSource = getDevClientManager(vars);
|
|
355
|
+
if (devClientManagerSource) {
|
|
356
|
+
writeFile(path.join(kotlinDir, "DevClientManager.kt"), devClientManagerSource);
|
|
357
|
+
writeFile(path.join(kotlinDir, "DevServerPrefs.kt"), getDevServerPrefs(vars));
|
|
358
|
+
}
|
|
359
|
+
if (iconPaths) {
|
|
360
|
+
const resDir = path.join(mainDir, "res");
|
|
361
|
+
if (iconPaths.android) {
|
|
362
|
+
const src = iconPaths.android;
|
|
363
|
+
const entries = fs.readdirSync(src, { withFileTypes: true });
|
|
364
|
+
for (const e of entries) {
|
|
365
|
+
const dest = path.join(resDir, e.name);
|
|
366
|
+
if (e.isDirectory()) {
|
|
367
|
+
fs.cpSync(path.join(src, e.name), dest, { recursive: true });
|
|
368
|
+
}
|
|
369
|
+
else {
|
|
370
|
+
fs.mkdirSync(resDir, { recursive: true });
|
|
371
|
+
fs.copyFileSync(path.join(src, e.name), dest);
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
console.log("✅ Copied Android icon from tamer.config.json icon.android");
|
|
375
|
+
}
|
|
376
|
+
else if (iconPaths.source) {
|
|
377
|
+
const mipmapDensities = ["mdpi", "hdpi", "xhdpi", "xxhdpi", "xxxhdpi"];
|
|
378
|
+
for (const d of mipmapDensities) {
|
|
379
|
+
const dir = path.join(resDir, `mipmap-${d}`);
|
|
380
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
381
|
+
fs.copyFileSync(iconPaths.source, path.join(dir, "ic_launcher.png"));
|
|
382
|
+
}
|
|
383
|
+
console.log("✅ Copied app icon from tamer.config.json icon.source");
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
// Create an empty assets directory so the project builds correctly
|
|
387
|
+
fs.mkdirSync(assetsDir, { recursive: true });
|
|
388
|
+
fs.writeFileSync(path.join(assetsDir, ".gitkeep"), "");
|
|
389
|
+
console.log(`✅ Android Kotlin project created at ${rootDir}`);
|
|
390
|
+
async function finalizeProjectSetup() {
|
|
391
|
+
if (androidSdk) {
|
|
392
|
+
try {
|
|
393
|
+
const sdkDirContent = `sdk.dir=${androidSdk.replace(/\\/g, "/")}`;
|
|
394
|
+
writeFile(path.join(rootDir, "local.properties"), sdkDirContent);
|
|
395
|
+
console.log("📦 Created local.properties from tamer.config.json.");
|
|
396
|
+
}
|
|
397
|
+
catch (err) {
|
|
398
|
+
console.error(`❌ Failed to create local.properties: ${err.message}`);
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
else {
|
|
402
|
+
const localPropsPath = path.join(process.cwd(), "local.properties");
|
|
403
|
+
if (fs.existsSync(localPropsPath)) {
|
|
404
|
+
try {
|
|
405
|
+
fs.copyFileSync(localPropsPath, path.join(rootDir, "local.properties"));
|
|
406
|
+
console.log("📦 Copied existing local.properties to the android project.");
|
|
407
|
+
}
|
|
408
|
+
catch (err) {
|
|
409
|
+
console.error("❌ Failed to copy local.properties:", err);
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
else {
|
|
413
|
+
console.warn("⚠️ `android.sdk` not found in tamer.config.json. You may need to create local.properties manually.");
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
// The Gradle wrapper setup logic is now handled by the imported function
|
|
417
|
+
await setupGradleWrapper(rootDir, gradleVersion);
|
|
418
|
+
}
|
|
419
|
+
await finalizeProjectSetup();
|
|
420
|
+
if (target === "dev-app")
|
|
421
|
+
process.chdir(origCwd);
|
|
422
|
+
};
|
|
423
|
+
export default create;
|