@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.
@@ -0,0 +1,400 @@
1
+ import { discoverNativeExtensions } from "../common/config";
2
+ import { fetchExplorerFile } from "./ref";
3
+ const EXPLORER_APP = "android/lynx_explorer/src/main/java/com/lynx/explorer/ExplorerApplication.java";
4
+ const EXPLORER_PROVIDER = "android/lynx_explorer/src/main/java/com/lynx/explorer/provider/DemoTemplateProvider.java";
5
+ export async function fetchAndPatchApplication(vars) {
6
+ const raw = await fetchExplorerFile(EXPLORER_APP);
7
+ let out = raw
8
+ .replace(/package com\.lynx\.explorer;/, `package ${vars.packageName};`)
9
+ .replace(/public class ExplorerApplication/, "public class App")
10
+ .replace(/LynxEnv\.inst\(\)\.init\(this, null, new DemoTemplateProvider\(\), null\);/, `LynxEnv.inst().init(this, null, new TemplateProvider(this), null);`)
11
+ .replace(/import com\.lynx\.explorer\.provider\.DemoTemplateProvider;/, "")
12
+ .replace(/import com\.lynx\.explorer\.modules\.LynxModuleAdapter;/, "")
13
+ .replace(/import com\.lynx\.explorer\.shell\.LynxRecorderDefaultActionCallback;/, "")
14
+ .replace(/import com\.lynx\.devtool\.recorder\.LynxRecorderPageManager;/, "")
15
+ .replace(/import com\.lynx\.service\.devtool\.LynxDevToolService;/, "")
16
+ .replace(/import com\.lynx\.tasm\.service\.ILynxHttpService;/, "")
17
+ .replace(/import com\.lynx\.tasm\.service\.ILynxImageService;/, "");
18
+ out = out.replace(/@Override\s+public void onCreate\(\)\s*\{[\s\S]*?initLynxRecorder\(\);\s*\}/, `@Override
19
+ public void onCreate() {
20
+ super.onCreate();
21
+ initLynxService();
22
+ initFresco();
23
+ initLynxEnv();
24
+ }`);
25
+ out = out.replace(/private void initLynxRecorder\(\)\s*{\s*LynxRecorderPageManager\.getInstance\(\)\.registerCallback\(new LynxRecorderDefaultActionCallback\(\)\);\s*}\s*/, "");
26
+ out = out.replace(/private void installLynxJSModule\(\)\s*{\s*LynxModuleAdapter\.getInstance\(\)\.Init\(this\);\s*}\s*/, "");
27
+ out = out.replace(/\n\s*\/\/ merge it into InitProcessor later\.\s*\n/, "\n");
28
+ out = out.replace(/LynxServiceCenter\.inst\(\)\.registerService\(LynxDevToolService\.getINSTANCE\(\)\);\s*\n\s*\/\/ enable all sessions debug[\s\S]*?LynxDevToolService\.getINSTANCE\(\)\.setLoadV8Bridge\(true\);\s*/, "");
29
+ out = out.replace(/import com\.lynx\.tasm\.service\.LynxServiceCenter;/, `import com.lynx.tasm.service.LynxServiceCenter;
30
+ import ${vars.packageName}.generated.GeneratedLynxExtensions;
31
+ `);
32
+ out = out.replace(/private void initLynxEnv\(\)\s*\{\s*LynxEnv\.inst\(\)\.init\(this, null, new TemplateProvider\(this\), null\);\s*}/, `private void initLynxEnv() {
33
+ GeneratedLynxExtensions.INSTANCE.register(this);
34
+ LynxEnv.inst().init(this, null, new TemplateProvider(this), null);
35
+ }`);
36
+ return out.replace(/\n{3,}/g, "\n\n");
37
+ }
38
+ function getLoadTemplateBody(vars) {
39
+ if (vars.devMode !== "embedded") {
40
+ return ` @Override
41
+ public void loadTemplate(String url, final Callback callback) {
42
+ new Thread(() -> {
43
+ try {
44
+ java.io.ByteArrayOutputStream baos = new java.io.ByteArrayOutputStream();
45
+ try (java.io.InputStream is = context.getAssets().open(url)) {
46
+ byte[] buf = new byte[1024];
47
+ int n;
48
+ while ((n = is.read(buf)) != -1) {
49
+ baos.write(buf, 0, n);
50
+ }
51
+ }
52
+ callback.onSuccess(baos.toByteArray());
53
+ } catch (java.io.IOException e) {
54
+ callback.onFailed(e.getMessage());
55
+ }
56
+ }).start();
57
+ }`;
58
+ }
59
+ return ` private static final String DEV_CLIENT_BUNDLE = "dev-client.lynx.bundle";
60
+
61
+ @Override
62
+ public void loadTemplate(String url, final Callback callback) {
63
+ new Thread(() -> {
64
+ if (url != null && (url.equals(DEV_CLIENT_BUNDLE) || url.endsWith("/" + DEV_CLIENT_BUNDLE) || url.contains(DEV_CLIENT_BUNDLE))) {
65
+ try {
66
+ java.io.ByteArrayOutputStream baos = new java.io.ByteArrayOutputStream();
67
+ try (java.io.InputStream is = context.getAssets().open(DEV_CLIENT_BUNDLE)) {
68
+ byte[] buf = new byte[1024];
69
+ int n;
70
+ while ((n = is.read(buf)) != -1) baos.write(buf, 0, n);
71
+ }
72
+ callback.onSuccess(baos.toByteArray());
73
+ } catch (java.io.IOException e) {
74
+ callback.onFailed(e.getMessage());
75
+ }
76
+ return;
77
+ }
78
+ if (BuildConfig.DEBUG) {
79
+ String devUrl = DevServerPrefs.INSTANCE.getUrl(context);
80
+ if (devUrl != null && !devUrl.isEmpty()) {
81
+ try {
82
+ java.net.URL u = new java.net.URL(devUrl);
83
+ String base = u.getProtocol() + "://" + u.getHost() + (u.getPort() > 0 ? ":" + u.getPort() : ":3000") + (u.getPath() != null && !u.getPath().isEmpty() ? u.getPath() : "");
84
+ String fetchUrl = base.endsWith("/") ? base + url : base + "/" + url;
85
+ okhttp3.OkHttpClient client = new okhttp3.OkHttpClient.Builder()
86
+ .connectTimeout(10, java.util.concurrent.TimeUnit.SECONDS)
87
+ .readTimeout(15, java.util.concurrent.TimeUnit.SECONDS)
88
+ .build();
89
+ okhttp3.Request request = new okhttp3.Request.Builder().url(fetchUrl).build();
90
+ try (okhttp3.Response response = client.newCall(request).execute()) {
91
+ if (response.isSuccessful() && response.body() != null) {
92
+ callback.onSuccess(response.body().bytes());
93
+ return;
94
+ }
95
+ callback.onFailed("HTTP " + response.code() + " for " + fetchUrl);
96
+ return;
97
+ }
98
+ } catch (Exception e) {
99
+ callback.onFailed("Fetch failed: " + (e.getMessage() != null ? e.getMessage() : e.getClass().getSimpleName()));
100
+ return;
101
+ }
102
+ }
103
+ }
104
+ try {
105
+ java.io.ByteArrayOutputStream baos = new java.io.ByteArrayOutputStream();
106
+ try (java.io.InputStream is = context.getAssets().open(url)) {
107
+ byte[] buf = new byte[1024];
108
+ int n;
109
+ while ((n = is.read(buf)) != -1) {
110
+ baos.write(buf, 0, n);
111
+ }
112
+ }
113
+ callback.onSuccess(baos.toByteArray());
114
+ } catch (java.io.IOException e) {
115
+ callback.onFailed(e.getMessage());
116
+ }
117
+ }).start();
118
+ }`;
119
+ }
120
+ export async function fetchAndPatchTemplateProvider(vars) {
121
+ const raw = await fetchExplorerFile(EXPLORER_PROVIDER);
122
+ const loadBody = getLoadTemplateBody(vars);
123
+ const out = raw
124
+ .replace(/package com\.lynx\.explorer\.provider;/, `package ${vars.packageName};`)
125
+ .replace(/public class DemoTemplateProvider/, "public class TemplateProvider")
126
+ .replace(/extends AbsTemplateProvider \{/, `extends AbsTemplateProvider {
127
+ private final android.content.Context context;
128
+
129
+ public TemplateProvider(android.content.Context context) {
130
+ this.context = context.getApplicationContext();
131
+ }
132
+
133
+ `)
134
+ .replace(/@Override\s+public void loadTemplate\(String url, final Callback callback\)\s*\{[\s\S]*?\}\s*\)\s*;\s*\n\s*\}/, loadBody)
135
+ .replace(/import okhttp3\.ResponseBody;[\s\n]*/, "")
136
+ .replace(/import retrofit2\.Call;[\s\n]*/, "")
137
+ .replace(/import retrofit2\.Response;[\s\n]*/, "")
138
+ .replace(/import retrofit2\.Retrofit;[\s\n]*/, "")
139
+ .replace(/import java\.io\.IOException;[\s\n]*/, "");
140
+ const withBuildConfig = vars.devMode === "embedded"
141
+ ? out.replace(/(package [^;]+;)/, `$1\nimport ${vars.packageName}.BuildConfig;\nimport ${vars.packageName}.DevServerPrefs;`)
142
+ : out;
143
+ return withBuildConfig.replace(/\n{3,}/g, "\n\n");
144
+ }
145
+ export function getDevClientManager(vars) {
146
+ if (vars.devMode !== "embedded")
147
+ return null;
148
+ return `package ${vars.packageName}
149
+
150
+ import android.content.Context
151
+ import android.net.Uri
152
+ import android.os.Handler
153
+ import android.os.Looper
154
+ import okhttp3.OkHttpClient
155
+ import okhttp3.Request
156
+ import okhttp3.Response
157
+ import okhttp3.WebSocket
158
+ import okhttp3.WebSocketListener
159
+
160
+ class DevClientManager(private val context: Context, private val onReload: Runnable) {
161
+ private var webSocket: WebSocket? = null
162
+ private val handler = Handler(Looper.getMainLooper())
163
+ private val client = OkHttpClient.Builder()
164
+ .connectTimeout(5, java.util.concurrent.TimeUnit.SECONDS)
165
+ .readTimeout(0, java.util.concurrent.TimeUnit.SECONDS)
166
+ .build()
167
+
168
+ fun connect() {
169
+ val devUrl = DevServerPrefs.getUrl(context) ?: return
170
+ val uri = Uri.parse(devUrl)
171
+ val scheme = if (uri.scheme == "https") "wss" else "ws"
172
+ val host = uri.host ?: return
173
+ val port = if (uri.port > 0) ":\${uri.port}" else ""
174
+ val path = (uri.path ?: "").let { p -> (if (p.endsWith("/")) p else p + "/") + "__hmr" }
175
+ val wsUrl = "$scheme://$host$port$path"
176
+ val request = Request.Builder()
177
+ .url(wsUrl)
178
+ .build()
179
+ webSocket = client.newWebSocket(request, object : WebSocketListener() {
180
+ override fun onMessage(webSocket: WebSocket, text: String) {
181
+ try {
182
+ if (text.contains("\\\"type\\\":\\\"reload\\\"")) {
183
+ handler.post(onReload)
184
+ }
185
+ } catch (_: Exception) { }
186
+ }
187
+ override fun onFailure(webSocket: WebSocket, t: Throwable, response: Response?) { }
188
+ override fun onClosed(webSocket: WebSocket, code: Int, reason: String) { }
189
+ })
190
+ }
191
+
192
+ fun disconnect() {
193
+ webSocket?.close(1000, null)
194
+ webSocket = null
195
+ }
196
+ }
197
+ `;
198
+ }
199
+ export function getProjectActivity(vars) {
200
+ const hasDevClient = vars.devMode === "embedded";
201
+ const extensions = vars.projectRoot ? discoverNativeExtensions(vars.projectRoot) : [];
202
+ const hasTamerInsets = extensions.some((e) => e.packageName === "tamer-insets");
203
+ const devClientInit = hasDevClient
204
+ ? `
205
+ devClientManager = DevClientManager(this) { lynxView?.renderTemplateUrl("main.lynx.bundle", "") }
206
+ devClientManager?.connect()
207
+ `
208
+ : "";
209
+ const devClientField = hasDevClient ? ` private var devClientManager: DevClientManager? = null
210
+ ` : "";
211
+ const devClientCleanup = hasDevClient
212
+ ? `
213
+ devClientManager?.disconnect()
214
+ `
215
+ : "";
216
+ const devClientImports = hasDevClient
217
+ ? `
218
+ import ${vars.packageName}.DevClientManager`
219
+ : "";
220
+ const routerImport = `
221
+ import com.nanofuxion.tamerrouter.TamerRouterNativeModule`;
222
+ const insetsImport = hasTamerInsets ? `
223
+ import com.nanofuxion.tamerinsets.TamerInsetsModule` : "";
224
+ const insetsAttach = hasTamerInsets ? `
225
+ TamerInsetsModule.attachHostView(lynxView)` : "";
226
+ const insetsDetach = hasTamerInsets ? `
227
+ TamerInsetsModule.attachHostView(null)` : "";
228
+ const insetsListenerBlock = hasTamerInsets
229
+ ? ""
230
+ : `
231
+ ViewCompat.setOnApplyWindowInsetsListener(lynxView!!) { view, insets ->
232
+ val imeVisible = insets.isVisible(WindowInsetsCompat.Type.ime())
233
+ val imeHeight = insets.getInsets(WindowInsetsCompat.Type.ime()).bottom
234
+ view.updatePadding(bottom = if (imeVisible) imeHeight else 0)
235
+ insets
236
+ }`;
237
+ const insetsImports = hasTamerInsets
238
+ ? ""
239
+ : `
240
+ import androidx.core.view.WindowInsetsCompat
241
+ import androidx.core.view.updatePadding
242
+ import androidx.core.view.ViewCompat`;
243
+ return `package ${vars.packageName}
244
+
245
+ import android.os.Bundle
246
+ import androidx.appcompat.app.AppCompatActivity
247
+ import androidx.core.view.WindowCompat
248
+ import androidx.core.view.WindowInsetsControllerCompat${insetsImports}
249
+ import com.lynx.tasm.LynxView
250
+ import com.lynx.tasm.LynxViewBuilder${devClientImports}${routerImport}${insetsImport}
251
+
252
+ class ProjectActivity : AppCompatActivity() {
253
+ private var lynxView: LynxView? = null
254
+ ${devClientField}
255
+ override fun onCreate(savedInstanceState: Bundle?) {
256
+ super.onCreate(savedInstanceState)
257
+ WindowCompat.setDecorFitsSystemWindows(window, false)
258
+ WindowInsetsControllerCompat(window, window.decorView).isAppearanceLightStatusBars = true
259
+ lynxView = buildLynxView()
260
+ setContentView(lynxView)${insetsListenerBlock}
261
+ TamerRouterNativeModule.attachHostView(lynxView)${insetsAttach}
262
+ lynxView?.renderTemplateUrl("main.lynx.bundle", "")${devClientInit}
263
+ }
264
+
265
+ @Deprecated("Deprecated in Java")
266
+ override fun onBackPressed() {
267
+ TamerRouterNativeModule.requestBack { consumed ->
268
+ if (!consumed) {
269
+ runOnUiThread { super.onBackPressed() }
270
+ }
271
+ }
272
+ }
273
+
274
+ override fun onDestroy() {
275
+ TamerRouterNativeModule.attachHostView(null)${insetsDetach}
276
+ lynxView?.destroy()
277
+ lynxView = null${devClientCleanup}
278
+ super.onDestroy()
279
+ }
280
+
281
+ private fun buildLynxView(): LynxView {
282
+ val viewBuilder = LynxViewBuilder()
283
+ viewBuilder.setTemplateProvider(TemplateProvider(this))
284
+ return viewBuilder.build(this)
285
+ }
286
+ }
287
+ `;
288
+ }
289
+ export function getStandaloneMainActivity(vars) {
290
+ const hasDevClient = vars.devMode === "embedded";
291
+ const devClientImports = hasDevClient
292
+ ? `
293
+ import android.Manifest
294
+ import android.content.BroadcastReceiver
295
+ import android.content.Context
296
+ import android.content.Intent
297
+ import android.content.IntentFilter
298
+ import androidx.activity.result.contract.ActivityResultContracts
299
+ import com.google.zxing.integration.android.IntentIntegrator
300
+ import com.google.zxing.integration.android.IntentResult
301
+ import com.nanofuxion.tamerdevclient.DevClientModule
302
+ `
303
+ : "";
304
+ const devClientUriInit = hasDevClient ? "" : "";
305
+ const devClientInit = hasDevClient
306
+ ? `
307
+ DevClientModule.attachHostActivity(this)
308
+ DevClientModule.attachLynxView(lynxView)
309
+ DevClientModule.attachCameraPermissionRequester { onGranted ->
310
+ pendingScanOnPermissionGranted = onGranted
311
+ cameraPermissionLauncher.launch(Manifest.permission.CAMERA)
312
+ }
313
+ DevClientModule.attachScanLauncher {
314
+ scanResultLauncher.launch(IntentIntegrator(this).setPrompt("Scan dev server QR").createScanIntent())
315
+ }
316
+ DevClientModule.attachReloadProjectLauncher {
317
+ startActivity(Intent(this@MainActivity, ProjectActivity::class.java).addFlags(
318
+ Intent.FLAG_ACTIVITY_NEW_DOCUMENT or Intent.FLAG_ACTIVITY_MULTIPLE_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP
319
+ ))
320
+ }
321
+ reloadReceiver = object : BroadcastReceiver() {
322
+ override fun onReceive(ctx: Context, intent: Intent) {
323
+ if (intent.action == DevClientModule.ACTION_RELOAD_PROJECT) {
324
+ runOnUiThread {
325
+ startActivity(Intent(this@MainActivity, ProjectActivity::class.java).addFlags(
326
+ Intent.FLAG_ACTIVITY_NEW_DOCUMENT or Intent.FLAG_ACTIVITY_MULTIPLE_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP
327
+ ))
328
+ }
329
+ }
330
+ }
331
+ }
332
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
333
+ registerReceiver(reloadReceiver, IntentFilter(DevClientModule.ACTION_RELOAD_PROJECT), Context.RECEIVER_NOT_EXPORTED)
334
+ } else {
335
+ registerReceiver(reloadReceiver, IntentFilter(DevClientModule.ACTION_RELOAD_PROJECT))
336
+ }
337
+ `
338
+ : "";
339
+ const devClientField = hasDevClient
340
+ ? ` private var reloadReceiver: BroadcastReceiver? = null
341
+ private val currentUri = "dev-client.lynx.bundle"
342
+ private var pendingScanOnPermissionGranted: Runnable? = null
343
+ private val cameraPermissionLauncher = registerForActivityResult(ActivityResultContracts.RequestPermission()) { granted ->
344
+ if (granted) pendingScanOnPermissionGranted?.run()
345
+ pendingScanOnPermissionGranted = null
346
+ }
347
+ private val scanResultLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
348
+ val scanResult = IntentIntegrator.parseActivityResult(result.resultCode, result.data)
349
+ scanResult?.contents?.let { DevClientModule.instance?.deliverScanResult(it) }
350
+ }
351
+ `
352
+ : "";
353
+ const devClientCleanup = hasDevClient
354
+ ? `
355
+ override fun onDestroy() {
356
+ reloadReceiver?.let { unregisterReceiver(it) }
357
+ DevClientModule.attachReloadProjectLauncher(null)
358
+ DevClientModule.attachLynxView(null)
359
+ super.onDestroy()
360
+ }
361
+ `
362
+ : "";
363
+ return `package ${vars.packageName}
364
+
365
+ import android.os.Build
366
+ import android.os.Bundle
367
+ import androidx.appcompat.app.AppCompatActivity
368
+ import androidx.core.view.WindowCompat
369
+ import androidx.core.view.WindowInsetsCompat
370
+ import androidx.core.view.WindowInsetsControllerCompat
371
+ import androidx.core.view.updatePadding
372
+ import com.lynx.tasm.LynxView
373
+ import com.lynx.tasm.LynxViewBuilder${devClientImports}
374
+
375
+ class MainActivity : AppCompatActivity() {
376
+ ${devClientField} private var lynxView: LynxView? = null
377
+
378
+ override fun onCreate(savedInstanceState: Bundle?) {
379
+ super.onCreate(savedInstanceState)
380
+ WindowCompat.setDecorFitsSystemWindows(window, false)
381
+ WindowInsetsControllerCompat(window, window.decorView).isAppearanceLightStatusBars = true
382
+ lynxView = buildLynxView()
383
+ setContentView(lynxView)
384
+ androidx.core.view.ViewCompat.setOnApplyWindowInsetsListener(lynxView!!) { view, insets ->
385
+ val imeVisible = insets.isVisible(WindowInsetsCompat.Type.ime())
386
+ val imeHeight = insets.getInsets(WindowInsetsCompat.Type.ime()).bottom
387
+ view.updatePadding(bottom = if (imeVisible) imeHeight else 0)
388
+ insets
389
+ }
390
+ ${devClientUriInit}lynxView?.renderTemplateUrl(${hasDevClient ? 'currentUri' : 'uri'}, "")${devClientInit}
391
+ }
392
+
393
+ private fun buildLynxView(): LynxView {
394
+ val viewBuilder = LynxViewBuilder()
395
+ viewBuilder.setTemplateProvider(TemplateProvider(this))
396
+ return viewBuilder.build(this)
397
+ }${devClientCleanup}
398
+ }
399
+ `;
400
+ }
@@ -0,0 +1,9 @@
1
+ export const LYNX_EXPLORER_REF = 'https://github.com/lynx-family/lynx/tree/develop/explorer';
2
+ export const LYNX_RAW_BASE = 'https://raw.githubusercontent.com/lynx-family/lynx/develop/explorer';
3
+ export async function fetchExplorerFile(relativePath) {
4
+ const url = `${LYNX_RAW_BASE}/${relativePath}`;
5
+ const res = await fetch(url);
6
+ if (!res.ok)
7
+ throw new Error(`Failed to fetch ${url}: ${res.status}`);
8
+ return res.text();
9
+ }