@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,92 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import os from 'os';
4
+ import { execSync } from 'child_process';
5
+ import fetch from 'node-fetch';
6
+ import AdmZip from 'adm-zip';
7
+ /**
8
+ * Downloads and extracts a specific version of Gradle.
9
+ * @param gradleVersion The version of Gradle to download.
10
+ */
11
+ export async function downloadGradle(gradleVersion) {
12
+ const gradleBaseUrl = `https://services.gradle.org/distributions/gradle-${gradleVersion}-bin.zip`;
13
+ // Place the gradle directory in the current working directory of the project
14
+ const downloadDir = path.join(process.cwd(), 'gradle');
15
+ const zipPath = path.join(downloadDir, `gradle-${gradleVersion}.zip`);
16
+ const extractedPath = path.join(downloadDir, `gradle-${gradleVersion}`);
17
+ // Check if Gradle is already downloaded and extracted
18
+ if (fs.existsSync(extractedPath)) {
19
+ console.log(`āœ… Gradle ${gradleVersion} already exists at ${extractedPath}. Skipping download.`);
20
+ return;
21
+ }
22
+ if (!fs.existsSync(downloadDir)) {
23
+ fs.mkdirSync(downloadDir, { recursive: true });
24
+ }
25
+ console.log(`šŸ“„ Downloading Gradle ${gradleVersion} from ${gradleBaseUrl}...`);
26
+ const response = await fetch(gradleBaseUrl);
27
+ if (!response.ok || !response.body) {
28
+ throw new Error(`Failed to download: ${response.statusText}`);
29
+ }
30
+ const fileStream = fs.createWriteStream(zipPath);
31
+ await new Promise((resolve, reject) => {
32
+ response.body?.pipe(fileStream);
33
+ response.body?.on('error', reject);
34
+ fileStream.on('finish', resolve);
35
+ });
36
+ console.log('āœ… Download complete. Extracting...');
37
+ try {
38
+ const zip = new AdmZip(zipPath);
39
+ zip.extractAllTo(downloadDir, true);
40
+ }
41
+ catch (err) {
42
+ console.error(`āŒ Failed to extract Gradle zip: ${err}`);
43
+ throw err;
44
+ }
45
+ // Clean up the downloaded zip file
46
+ fs.unlinkSync(zipPath);
47
+ console.log(`āœ… Gradle ${gradleVersion} extracted to ${extractedPath}`);
48
+ }
49
+ /**
50
+ * Sets up the Gradle wrapper for the project, detecting the OS for the correct executable.
51
+ * @param rootDir The root directory of the Android project.
52
+ * @param gradleVersion The version of Gradle to use.
53
+ */
54
+ export async function setupGradleWrapper(rootDir, gradleVersion) {
55
+ try {
56
+ console.log("šŸ“¦ Setting up Gradle wrapper...");
57
+ await downloadGradle(gradleVersion);
58
+ const gradleBinDir = path.join(process.cwd(), "gradle", `gradle-${gradleVersion}`, "bin");
59
+ // Detect OS and use the appropriate executable
60
+ const gradleExecutable = os.platform() === 'win32' ? 'gradle.bat' : 'gradle';
61
+ const gradleExecutablePath = path.join(gradleBinDir, gradleExecutable);
62
+ if (!fs.existsSync(gradleExecutablePath)) {
63
+ throw new Error(`Gradle executable not found at ${gradleExecutablePath}`);
64
+ }
65
+ // Make the gradle script executable on non-Windows systems
66
+ if (os.platform() !== 'win32') {
67
+ fs.chmodSync(gradleExecutablePath, "755");
68
+ }
69
+ console.log(`šŸš€ Executing Gradle wrapper in: ${rootDir}`);
70
+ execSync(`"${gradleExecutablePath}" wrapper`, {
71
+ cwd: rootDir,
72
+ stdio: "inherit",
73
+ });
74
+ console.log("āœ… Gradle wrapper created successfully.");
75
+ }
76
+ catch (err) {
77
+ console.error("āŒ Failed to create Gradle wrapper.", err.message);
78
+ // Exit the process if wrapper creation fails, as it's a critical step
79
+ process.exit(1);
80
+ }
81
+ }
82
+ /**
83
+ * Fetches the latest version of Gradle.
84
+ * @returns A promise that resolves to the latest Gradle version string.
85
+ */
86
+ async function getLatestGradleVersion() {
87
+ const res = await fetch('https://services.gradle.org/versions/current');
88
+ if (!res.ok)
89
+ throw new Error(`Failed to get version: ${res.statusText}`);
90
+ const data = await res.json();
91
+ return data.version;
92
+ }
@@ -0,0 +1,7 @@
1
+ import fs from 'fs';
2
+ import link from "./autolink";
3
+ import path from 'path';
4
+ const androidRoot = path.join(process.cwd(), 'android');
5
+ if (fs.existsSync(androidRoot)) {
6
+ link();
7
+ }
@@ -0,0 +1,7 @@
1
+ import fs from 'fs';
2
+ import link from "./autolink";
3
+ import path from 'path';
4
+ const androidRoot = path.join(process.cwd(), 'android');
5
+ if (fs.existsSync(androidRoot)) {
6
+ link();
7
+ }
@@ -0,0 +1,70 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+ import { resolveHostPaths } from "../common/hostConfig";
4
+ import { fetchAndPatchTemplateProvider, getDevClientManager, getProjectActivity, getStandaloneMainActivity, } from "../explorer/patches";
5
+ import { getDevServerPrefs } from "../explorer/devLauncher";
6
+ async function syncDevClient() {
7
+ let resolved;
8
+ try {
9
+ resolved = resolveHostPaths();
10
+ }
11
+ catch (error) {
12
+ console.error(`āŒ Error loading configuration: ${error.message}`);
13
+ process.exit(1);
14
+ }
15
+ const { config, androidDir: rootDir } = resolved;
16
+ const packageName = config.android?.packageName;
17
+ const appName = config.android?.appName;
18
+ const packagePath = packageName.replace(/\./g, "/");
19
+ const javaDir = path.join(rootDir, "app", "src", "main", "java", packagePath);
20
+ const kotlinDir = path.join(rootDir, "app", "src", "main", "kotlin", packagePath);
21
+ if (!fs.existsSync(javaDir) || !fs.existsSync(kotlinDir)) {
22
+ console.error("āŒ Android project not found. Run `tamer android create` first.");
23
+ process.exit(1);
24
+ }
25
+ const devMode = resolved.devMode;
26
+ const devServer = config.devServer
27
+ ? {
28
+ host: config.devServer.host ?? "10.0.2.2",
29
+ port: config.devServer.port ?? config.devServer.httpPort ?? 3000,
30
+ }
31
+ : undefined;
32
+ const vars = { packageName, appName, devMode, devServer, projectRoot: resolved.lynxProjectDir };
33
+ const [templateProviderSource] = await Promise.all([
34
+ fetchAndPatchTemplateProvider(vars),
35
+ ]);
36
+ fs.writeFileSync(path.join(javaDir, "TemplateProvider.java"), templateProviderSource);
37
+ fs.writeFileSync(path.join(kotlinDir, "MainActivity.kt"), getStandaloneMainActivity(vars));
38
+ const appDir = path.join(rootDir, "app");
39
+ const mainDir = path.join(appDir, "src", "main");
40
+ const manifestPath = path.join(mainDir, "AndroidManifest.xml");
41
+ const devClientManagerSource = getDevClientManager(vars);
42
+ if (devClientManagerSource) {
43
+ fs.writeFileSync(path.join(kotlinDir, "DevClientManager.kt"), devClientManagerSource);
44
+ fs.writeFileSync(path.join(kotlinDir, "DevServerPrefs.kt"), getDevServerPrefs(vars));
45
+ fs.writeFileSync(path.join(kotlinDir, "ProjectActivity.kt"), getProjectActivity(vars));
46
+ let manifest = fs.readFileSync(manifestPath, "utf-8");
47
+ const projectActivityEntry = ' <activity android:name=".ProjectActivity" android:exported="false" android:taskAffinity="" android:launchMode="singleTask" android:documentLaunchMode="always" />';
48
+ if (!manifest.includes("ProjectActivity")) {
49
+ manifest = manifest.replace(/(\s*)(<\/application>)/, `${projectActivityEntry}\n$1$2`);
50
+ }
51
+ else {
52
+ manifest = manifest.replace(/\s*<activity android:name="\.ProjectActivity"[^\/]*\/>\n?/g, projectActivityEntry + "\n");
53
+ }
54
+ fs.writeFileSync(manifestPath, manifest);
55
+ console.log("āœ… Synced dev client (TemplateProvider, MainActivity, ProjectActivity, DevClientManager)");
56
+ }
57
+ else {
58
+ for (const f of ["DevClientManager.kt", "DevServerPrefs.kt", "ProjectActivity.kt"]) {
59
+ try {
60
+ fs.rmSync(path.join(kotlinDir, f));
61
+ }
62
+ catch { /* ignore */ }
63
+ }
64
+ let manifest = fs.readFileSync(manifestPath, "utf-8");
65
+ manifest = manifest.replace(/\s*<activity android:name="\.ProjectActivity"[^\/]*\/>\n?/g, "");
66
+ fs.writeFileSync(manifestPath, manifest);
67
+ console.log("āœ… Synced (dev client disabled - set dev.mode: \"embedded\" in tamer.config.json to enable)");
68
+ }
69
+ }
70
+ export default syncDevClient;
@@ -0,0 +1,43 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import { findDevClientPackage } from './hostConfig';
4
+ import android_bundle from '../android/bundle';
5
+ import android_build from '../android/build';
6
+ function findRepoRoot(start) {
7
+ let dir = path.resolve(start);
8
+ const root = path.parse(dir).root;
9
+ while (dir !== root) {
10
+ const pkgPath = path.join(dir, 'package.json');
11
+ if (fs.existsSync(pkgPath)) {
12
+ try {
13
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
14
+ if (pkg.workspaces)
15
+ return dir;
16
+ }
17
+ catch { }
18
+ }
19
+ dir = path.dirname(dir);
20
+ }
21
+ return start;
22
+ }
23
+ async function buildDevApp(opts = {}) {
24
+ const cwd = process.cwd();
25
+ const repoRoot = findRepoRoot(cwd);
26
+ const devClientPkg = findDevClientPackage(repoRoot) ?? findDevClientPackage(cwd);
27
+ if (!devClientPkg) {
28
+ console.error('āŒ tamer-dev-client is not installed in this project.');
29
+ console.error(' Add it as a dependency or ensure packages/tamer-dev-client exists in the repo.');
30
+ process.exit(1);
31
+ }
32
+ const platform = opts.platform ?? 'all';
33
+ if (platform === 'ios' || platform === 'all') {
34
+ console.log('āš ļø iOS dev-app build is not yet implemented. Skipping.');
35
+ if (platform === 'ios')
36
+ process.exit(0);
37
+ }
38
+ if (platform === 'android' || platform === 'all') {
39
+ await android_bundle({ target: 'dev-app' });
40
+ await android_build({ target: 'dev-app', install: opts.install });
41
+ }
42
+ }
43
+ export default buildDevApp;
@@ -0,0 +1,69 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import { loadExtensionConfig } from './config';
4
+ function codegen() {
5
+ const cwd = process.cwd();
6
+ const config = loadExtensionConfig(cwd);
7
+ if (!config) {
8
+ console.error('āŒ No lynx.ext.json or tamer.json found. Run from an extension package root.');
9
+ process.exit(1);
10
+ }
11
+ const srcDir = path.join(cwd, 'src');
12
+ const generatedDir = path.join(cwd, 'generated');
13
+ fs.mkdirSync(generatedDir, { recursive: true });
14
+ const dtsFiles = findDtsFiles(srcDir);
15
+ const modules = extractLynxModules(dtsFiles);
16
+ if (modules.length === 0) {
17
+ console.log('ā„¹ļø No @lynxmodule declarations found in src/. Add /** @lynxmodule */ to your module class.');
18
+ return;
19
+ }
20
+ for (const mod of modules) {
21
+ const tsContent = `export type { ${mod} } from '../src/index.js';
22
+ `;
23
+ const outPath = path.join(generatedDir, `${mod}.ts`);
24
+ fs.writeFileSync(outPath, tsContent);
25
+ console.log(`āœ… Generated ${outPath}`);
26
+ }
27
+ if (config.android) {
28
+ const androidGenerated = path.join(cwd, 'android', 'src', 'main', 'kotlin', config.android.moduleClassName.replace(/\./g, '/').replace(/[^/]+$/, ''), 'generated');
29
+ fs.mkdirSync(androidGenerated, { recursive: true });
30
+ console.log(`ā„¹ļø Android generated dir: ${androidGenerated} (spec generation coming soon)`);
31
+ }
32
+ if (config.ios) {
33
+ const iosGenerated = path.join(cwd, 'ios', 'generated');
34
+ fs.mkdirSync(iosGenerated, { recursive: true });
35
+ console.log(`ā„¹ļø iOS generated dir: ${iosGenerated} (spec generation coming soon)`);
36
+ }
37
+ console.log('✨ Codegen complete.');
38
+ }
39
+ function findDtsFiles(dir) {
40
+ const result = [];
41
+ if (!fs.existsSync(dir))
42
+ return result;
43
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
44
+ for (const e of entries) {
45
+ const full = path.join(dir, e.name);
46
+ if (e.isDirectory())
47
+ result.push(...findDtsFiles(full));
48
+ else if (e.name.endsWith('.d.ts'))
49
+ result.push(full);
50
+ }
51
+ return result;
52
+ }
53
+ function extractLynxModules(files) {
54
+ const modules = [];
55
+ const seen = new Set();
56
+ for (const file of files) {
57
+ const content = fs.readFileSync(file, 'utf8');
58
+ const regex = /\/\*\*\s*@lynxmodule\s*\*\/\s*export\s+declare\s+class\s+(\w+)/g;
59
+ let m;
60
+ while ((m = regex.exec(content)) !== null) {
61
+ if (!seen.has(m[1])) {
62
+ seen.add(m[1]);
63
+ modules.push(m[1]);
64
+ }
65
+ }
66
+ }
67
+ return modules;
68
+ }
69
+ export default codegen;
@@ -0,0 +1,113 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ const LYNX_EXT_JSON = 'lynx.ext.json';
4
+ const TAMER_JSON = 'tamer.json';
5
+ function loadLynxExtJson(packagePath) {
6
+ const p = path.join(packagePath, LYNX_EXT_JSON);
7
+ if (!fs.existsSync(p))
8
+ return null;
9
+ try {
10
+ return JSON.parse(fs.readFileSync(p, 'utf8'));
11
+ }
12
+ catch {
13
+ return null;
14
+ }
15
+ }
16
+ function loadTamerJson(packagePath) {
17
+ const p = path.join(packagePath, TAMER_JSON);
18
+ if (!fs.existsSync(p))
19
+ return null;
20
+ try {
21
+ return JSON.parse(fs.readFileSync(p, 'utf8'));
22
+ }
23
+ catch {
24
+ return null;
25
+ }
26
+ }
27
+ export function loadExtensionConfig(packagePath) {
28
+ const lynxExt = loadLynxExtJson(packagePath);
29
+ const tamer = loadTamerJson(packagePath);
30
+ const raw = lynxExt ?? tamer;
31
+ if (!raw)
32
+ return null;
33
+ const normalized = {};
34
+ if (raw.platforms?.android || raw.android) {
35
+ const a = raw.platforms?.android ?? raw.android;
36
+ const moduleClassName = a?.moduleClassName ?? raw.android?.moduleClassName;
37
+ const elements = a?.elements ?? raw.android?.elements;
38
+ const permissions = a?.permissions ?? raw.android?.permissions;
39
+ if (moduleClassName || elements || permissions) {
40
+ normalized.android = {
41
+ ...(moduleClassName && { moduleClassName }),
42
+ sourceDir: a?.sourceDir ?? raw.android?.sourceDir ?? 'android',
43
+ ...(elements && Object.keys(elements).length > 0 && { elements }),
44
+ ...(permissions && Array.isArray(permissions) && permissions.length > 0 && { permissions }),
45
+ };
46
+ }
47
+ }
48
+ if (raw.platforms?.ios || raw.ios) {
49
+ const i = raw.platforms?.ios ?? raw.ios;
50
+ const moduleClassName = i?.moduleClassName ?? raw.ios?.moduleClassName;
51
+ if (moduleClassName) {
52
+ normalized.ios = {
53
+ moduleClassName,
54
+ podspecPath: i?.podspecPath ?? raw.ios?.podspecPath ?? '.',
55
+ };
56
+ }
57
+ }
58
+ if (Object.keys(normalized).length === 0)
59
+ return null;
60
+ return normalized;
61
+ }
62
+ export function hasExtensionConfig(packagePath) {
63
+ return fs.existsSync(path.join(packagePath, LYNX_EXT_JSON)) ||
64
+ fs.existsSync(path.join(packagePath, TAMER_JSON));
65
+ }
66
+ export function getNodeModulesPath(projectRoot) {
67
+ let nodeModulesPath = path.join(projectRoot, 'node_modules');
68
+ const workspaceRoot = path.join(projectRoot, '..', '..');
69
+ const rootNodeModules = path.join(workspaceRoot, 'node_modules');
70
+ if (fs.existsSync(path.join(workspaceRoot, 'package.json')) && fs.existsSync(rootNodeModules) && path.basename(path.dirname(projectRoot)) === 'packages') {
71
+ nodeModulesPath = rootNodeModules;
72
+ }
73
+ else if (!fs.existsSync(nodeModulesPath)) {
74
+ const altRoot = path.join(projectRoot, '..', '..');
75
+ const altNodeModules = path.join(altRoot, 'node_modules');
76
+ if (fs.existsSync(path.join(altRoot, 'package.json')) && fs.existsSync(altNodeModules)) {
77
+ nodeModulesPath = altNodeModules;
78
+ }
79
+ }
80
+ return nodeModulesPath;
81
+ }
82
+ export function discoverNativeExtensions(projectRoot) {
83
+ const nodeModulesPath = getNodeModulesPath(projectRoot);
84
+ const result = [];
85
+ if (!fs.existsSync(nodeModulesPath))
86
+ return result;
87
+ const packageDirs = fs.readdirSync(nodeModulesPath);
88
+ const check = (name, packagePath) => {
89
+ if (!hasExtensionConfig(packagePath))
90
+ return;
91
+ const config = loadExtensionConfig(packagePath);
92
+ const className = config?.android?.moduleClassName;
93
+ if (className)
94
+ result.push({ packageName: name, moduleClassName: className });
95
+ };
96
+ for (const dirName of packageDirs) {
97
+ const fullPath = path.join(nodeModulesPath, dirName);
98
+ if (dirName.startsWith('@')) {
99
+ try {
100
+ for (const scopedDirName of fs.readdirSync(fullPath)) {
101
+ check(`${dirName}/${scopedDirName}`, path.join(fullPath, scopedDirName));
102
+ }
103
+ }
104
+ catch {
105
+ /* ignore */
106
+ }
107
+ }
108
+ else {
109
+ check(dirName, fullPath);
110
+ }
111
+ }
112
+ return result;
113
+ }
@@ -0,0 +1,170 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import readline from 'readline';
4
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
5
+ function ask(question) {
6
+ return new Promise(resolve => rl.question(question, answer => resolve(answer.trim())));
7
+ }
8
+ async function create() {
9
+ console.log('Tamer4Lynx: Create Lynx Extension\n');
10
+ console.log('Select extension types (space to toggle, enter to confirm):');
11
+ console.log(' [ ] Native Module');
12
+ console.log(' [ ] Element');
13
+ console.log(' [ ] Service\n');
14
+ const includeModule = /^y(es)?$/i.test(await ask('Include Native Module? (Y/n): ') || 'y');
15
+ const includeElement = /^y(es)?$/i.test(await ask('Include Element? (y/N): ') || 'n');
16
+ const includeService = /^y(es)?$/i.test(await ask('Include Service? (y/N): ') || 'n');
17
+ if (!includeModule && !includeElement && !includeService) {
18
+ console.error('āŒ At least one extension type is required.');
19
+ rl.close();
20
+ process.exit(1);
21
+ }
22
+ const extName = await ask('Extension package name (e.g. my-lynx-module): ');
23
+ if (!extName || !/^[a-z0-9-_]+$/.test(extName)) {
24
+ console.error('āŒ Invalid package name. Use lowercase letters, numbers, hyphens, underscores.');
25
+ rl.close();
26
+ process.exit(1);
27
+ }
28
+ const packageName = await ask('Android package name (e.g. com.example.mymodule): ') || `com.example.${extName.replace(/-/g, '')}`;
29
+ const simpleModuleName = extName.split('-').map((s) => s.charAt(0).toUpperCase() + s.slice(1)).join('') + 'Module';
30
+ const fullModuleClassName = `${packageName}.${simpleModuleName}`;
31
+ const cwd = process.cwd();
32
+ const root = path.join(cwd, extName);
33
+ if (fs.existsSync(root)) {
34
+ console.error(`āŒ Directory ${extName} already exists.`);
35
+ rl.close();
36
+ process.exit(1);
37
+ }
38
+ fs.mkdirSync(root, { recursive: true });
39
+ const lynxExt = {
40
+ platforms: {
41
+ android: {
42
+ packageName,
43
+ moduleClassName: fullModuleClassName,
44
+ sourceDir: 'android',
45
+ },
46
+ ios: {
47
+ podspecPath: `ios/${extName}`,
48
+ moduleClassName: simpleModuleName,
49
+ },
50
+ web: {},
51
+ },
52
+ };
53
+ fs.writeFileSync(path.join(root, 'lynx.ext.json'), JSON.stringify(lynxExt, null, 2));
54
+ const pkg = {
55
+ name: extName,
56
+ version: '0.0.1',
57
+ type: 'module',
58
+ main: 'index.js',
59
+ description: `Lynx extension: ${extName}`,
60
+ scripts: { codegen: 't4l codegen' },
61
+ devDependencies: { typescript: '^5' },
62
+ peerDependencies: { typescript: '^5' },
63
+ engines: { node: '>=18' },
64
+ };
65
+ if (includeModule)
66
+ pkg.types = 'src/index.d.ts';
67
+ fs.writeFileSync(path.join(root, 'package.json'), JSON.stringify(pkg, null, 2));
68
+ const pkgPath = packageName.replace(/\./g, '/');
69
+ if (includeModule) {
70
+ fs.mkdirSync(path.join(root, 'src'), { recursive: true });
71
+ fs.writeFileSync(path.join(root, 'src', 'index.d.ts'), `/** @lynxmodule */
72
+ export declare class ${simpleModuleName} {
73
+ // Add your module methods here
74
+ }
75
+ `);
76
+ fs.mkdirSync(path.join(root, 'android', 'src', 'main', 'kotlin', pkgPath), { recursive: true });
77
+ fs.writeFileSync(path.join(root, 'android', 'build.gradle.kts'), `plugins {
78
+ id("com.android.library")
79
+ id("org.jetbrains.kotlin.android")
80
+ }
81
+
82
+ android {
83
+ namespace = "${packageName}"
84
+ compileSdk = 35
85
+ defaultConfig { minSdk = 28 }
86
+ compileOptions {
87
+ sourceCompatibility = JavaVersion.VERSION_17
88
+ targetCompatibility = JavaVersion.VERSION_17
89
+ }
90
+ kotlinOptions { jvmTarget = "17" }
91
+ }
92
+
93
+ dependencies {
94
+ implementation(libs.lynx)
95
+ implementation(libs.lynx.jssdk)
96
+ }
97
+ `);
98
+ fs.writeFileSync(path.join(root, 'android', 'src', 'main', 'AndroidManifest.xml'), `<?xml version="1.0" encoding="utf-8"?>
99
+ <manifest />
100
+ `);
101
+ const ktContent = `package ${packageName}
102
+
103
+ import android.content.Context
104
+ import com.lynx.jsbridge.LynxMethod
105
+ import com.lynx.jsbridge.LynxModule
106
+
107
+ class ${simpleModuleName}(context: Context) : LynxModule(context) {
108
+
109
+ @LynxMethod
110
+ fun example(): String {
111
+ return "Hello from ${extName}"
112
+ }
113
+ }
114
+ `;
115
+ fs.writeFileSync(path.join(root, 'android', 'src', 'main', 'kotlin', pkgPath, `${simpleModuleName}.kt`), ktContent);
116
+ fs.mkdirSync(path.join(root, 'ios', extName, extName, 'Classes'), { recursive: true });
117
+ const podspec = `Pod::Spec.new do |s|
118
+ s.name = '${extName}'
119
+ s.version = '0.0.1'
120
+ s.summary = 'Lynx extension: ${extName}'
121
+ s.homepage = ''
122
+ s.license = { :type => 'MIT' }
123
+ s.author = ''
124
+ s.source = { :git => '' }
125
+ s.platform = :ios, '12.0'
126
+ s.source_files = 'Classes/**/*'
127
+ s.dependency 'Lynx'
128
+ end
129
+ `;
130
+ fs.writeFileSync(path.join(root, 'ios', extName, `${extName}.podspec`), podspec);
131
+ const swiftContent = `import Foundation
132
+
133
+ @objc public class ${simpleModuleName}: NSObject {
134
+ @objc public func example() -> String {
135
+ return "Hello from ${extName}"
136
+ }
137
+ }
138
+ `;
139
+ fs.writeFileSync(path.join(root, 'ios', extName, extName, 'Classes', `${simpleModuleName}.swift`), swiftContent);
140
+ }
141
+ fs.writeFileSync(path.join(root, 'index.js'), `'use strict';
142
+ module.exports = {};
143
+ `);
144
+ fs.writeFileSync(path.join(root, 'tsconfig.json'), JSON.stringify({
145
+ compilerOptions: { target: 'ES2020', module: 'ESNext', moduleResolution: 'bundler', strict: true },
146
+ include: ['src'],
147
+ }, null, 2));
148
+ fs.writeFileSync(path.join(root, 'README.md'), `# ${extName}
149
+
150
+ Lynx extension for ${extName}.
151
+
152
+ ## Usage
153
+
154
+ \`\`\`bash
155
+ npm install ${extName}
156
+ \`\`\`
157
+
158
+ ## Configuration
159
+
160
+ This package uses \`lynx.ext.json\` (RFC-compliant) for autolinking.
161
+ `);
162
+ console.log(`\nāœ… Created extension at ${root}`);
163
+ console.log('\nNext steps:');
164
+ console.log(` cd ${extName}`);
165
+ console.log(' npm install');
166
+ if (includeModule)
167
+ console.log(' npm run codegen');
168
+ rl.close();
169
+ }
170
+ export default create;