@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,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,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;
|