@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,246 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { execSync } from 'child_process';
|
|
4
|
+
import { loadExtensionConfig, hasExtensionConfig } from '../common/config';
|
|
5
|
+
import { resolveHostPaths } from '../common/hostConfig';
|
|
6
|
+
const autolink = () => {
|
|
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 projectRoot = resolved.projectRoot;
|
|
16
|
+
let nodeModulesPath = path.join(projectRoot, 'node_modules');
|
|
17
|
+
const workspaceRoot = path.join(projectRoot, '..', '..');
|
|
18
|
+
const rootNodeModules = path.join(workspaceRoot, 'node_modules');
|
|
19
|
+
if (fs.existsSync(path.join(workspaceRoot, 'package.json')) && fs.existsSync(rootNodeModules) && path.basename(path.dirname(projectRoot)) === 'packages') {
|
|
20
|
+
nodeModulesPath = rootNodeModules;
|
|
21
|
+
}
|
|
22
|
+
else if (!fs.existsSync(nodeModulesPath)) {
|
|
23
|
+
const altRoot = path.join(projectRoot, '..', '..');
|
|
24
|
+
const altNodeModules = path.join(altRoot, 'node_modules');
|
|
25
|
+
if (fs.existsSync(path.join(altRoot, 'package.json')) && fs.existsSync(altNodeModules)) {
|
|
26
|
+
nodeModulesPath = altNodeModules;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
const iosProjectPath = resolved.iosDir;
|
|
30
|
+
// --- Core Logic ---
|
|
31
|
+
function updateGeneratedSection(filePath, newContent, startMarker, endMarker) {
|
|
32
|
+
if (!fs.existsSync(filePath)) {
|
|
33
|
+
console.warn(`⚠️ File not found, skipping update: ${filePath}`);
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
let fileContent = fs.readFileSync(filePath, 'utf8');
|
|
37
|
+
const escapedStartMarker = startMarker.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
38
|
+
const escapedEndMarker = endMarker.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
39
|
+
const regex = new RegExp(`${escapedStartMarker}[\\s\\S]*?${escapedEndMarker}`, 'g');
|
|
40
|
+
const replacementBlock = `${startMarker}\n${newContent}\n${endMarker}`;
|
|
41
|
+
if (regex.test(fileContent)) {
|
|
42
|
+
// Remove all existing generated blocks first to avoid creating duplicates,
|
|
43
|
+
// then insert a single replacement at the position of the first found start marker.
|
|
44
|
+
const firstStartIdx = fileContent.indexOf(startMarker);
|
|
45
|
+
// Remove all occurrences
|
|
46
|
+
fileContent = fileContent.replace(regex, '');
|
|
47
|
+
if (firstStartIdx !== -1) {
|
|
48
|
+
const before = fileContent.slice(0, firstStartIdx);
|
|
49
|
+
const after = fileContent.slice(firstStartIdx);
|
|
50
|
+
fileContent = `${before}${replacementBlock}${after}`;
|
|
51
|
+
}
|
|
52
|
+
else {
|
|
53
|
+
// Fallback: append at end
|
|
54
|
+
fileContent += `\n${replacementBlock}\n`;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
else {
|
|
58
|
+
// If markers aren't present at all — append at end and warn so user can add markers to control placement.
|
|
59
|
+
console.warn(`⚠️ Could not find autolink markers in ${path.basename(filePath)}. Appending to the end of the file.`);
|
|
60
|
+
fileContent += `\n${replacementBlock}\n`;
|
|
61
|
+
}
|
|
62
|
+
fs.writeFileSync(filePath, fileContent, 'utf8');
|
|
63
|
+
console.log(`✅ Updated autolinked section in ${path.basename(filePath)}`);
|
|
64
|
+
}
|
|
65
|
+
function findExtensionPackages() {
|
|
66
|
+
const packages = [];
|
|
67
|
+
if (!fs.existsSync(nodeModulesPath)) {
|
|
68
|
+
console.warn('⚠️ node_modules directory not found. Skipping autolinking.');
|
|
69
|
+
return [];
|
|
70
|
+
}
|
|
71
|
+
const packageDirs = fs.readdirSync(nodeModulesPath);
|
|
72
|
+
for (const dirName of packageDirs) {
|
|
73
|
+
const fullPath = path.join(nodeModulesPath, dirName);
|
|
74
|
+
const checkPackage = (name, packagePath) => {
|
|
75
|
+
if (!hasExtensionConfig(packagePath))
|
|
76
|
+
return;
|
|
77
|
+
const config = loadExtensionConfig(packagePath);
|
|
78
|
+
if (config?.ios) {
|
|
79
|
+
packages.push({ name, config, packagePath });
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
if (dirName.startsWith('@')) {
|
|
83
|
+
try {
|
|
84
|
+
const scopedDirs = fs.readdirSync(fullPath);
|
|
85
|
+
for (const scopedDirName of scopedDirs) {
|
|
86
|
+
const scopedPackagePath = path.join(fullPath, scopedDirName);
|
|
87
|
+
checkPackage(`${dirName}/${scopedDirName}`, scopedPackagePath);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
catch (e) {
|
|
91
|
+
console.warn(`⚠️ Could not read scoped package directory ${fullPath}: ${e.message}`);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
else {
|
|
95
|
+
checkPackage(dirName, fullPath);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return packages;
|
|
99
|
+
}
|
|
100
|
+
function updatePodfile(packages) {
|
|
101
|
+
const podfilePath = path.join(iosProjectPath, 'Podfile');
|
|
102
|
+
let scriptContent = ` # This section is automatically generated by Tamer4Lynx.\n # Manual edits will be overwritten.`;
|
|
103
|
+
const iosPackages = packages.filter(p => p.config.ios);
|
|
104
|
+
if (iosPackages.length > 0) {
|
|
105
|
+
iosPackages.forEach(pkg => {
|
|
106
|
+
const podspecPath = pkg.config.ios?.podspecPath || '.';
|
|
107
|
+
const relativePath = path.relative(iosProjectPath, path.join(pkg.packagePath, podspecPath));
|
|
108
|
+
scriptContent += `\n pod '${pkg.name}', :path => '${relativePath}'`;
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
else {
|
|
112
|
+
scriptContent += `\n # No native modules found by Tamer4Lynx autolinker.`;
|
|
113
|
+
}
|
|
114
|
+
updateGeneratedSection(podfilePath, scriptContent.trim(), '# GENERATED AUTOLINK DEPENDENCIES START', '# GENERATED AUTOLINK DEPENDENCIES END');
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Update LynxInitProcessor.swift with module registrations.
|
|
118
|
+
* Emits runtime-safe registrations using NSClassFromString to avoid compile-time
|
|
119
|
+
* dependency issues when modules are provided by CocoaPods.
|
|
120
|
+
*/
|
|
121
|
+
function updateLynxInitProcessor(packages) {
|
|
122
|
+
const appNameFromConfig = resolved.config.ios?.appName;
|
|
123
|
+
const candidatePaths = [];
|
|
124
|
+
if (appNameFromConfig) {
|
|
125
|
+
candidatePaths.push(path.join(iosProjectPath, appNameFromConfig, 'LynxInitProcessor.swift'));
|
|
126
|
+
}
|
|
127
|
+
candidatePaths.push(path.join(iosProjectPath, 'LynxInitProcessor.swift'));
|
|
128
|
+
const found = candidatePaths.find(p => fs.existsSync(p));
|
|
129
|
+
const lynxInitPath = (found ?? candidatePaths[0]);
|
|
130
|
+
const iosPackages = packages.filter(p => p.config.ios?.moduleClassName);
|
|
131
|
+
// --- Generate import statements for discovered native packages ---
|
|
132
|
+
function updateImportsSection(filePath, pkgs) {
|
|
133
|
+
const startMarker = '// GENERATED IMPORTS START';
|
|
134
|
+
const endMarker = '// GENERATED IMPORTS END';
|
|
135
|
+
if (pkgs.length === 0) {
|
|
136
|
+
const placeholder = '// No native imports found by Tamer4Lynx autolinker.';
|
|
137
|
+
// If markers exist, replace the block, otherwise try to insert after existing imports
|
|
138
|
+
updateGeneratedSection(filePath, placeholder, startMarker, endMarker);
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
const imports = pkgs.map(pkg => {
|
|
142
|
+
// Use the last path segment as the Swift module name (handle scoped packages)
|
|
143
|
+
const raw = pkg.name.split('/').pop() || pkg.name;
|
|
144
|
+
const moduleName = raw.replace(/[^A-Za-z0-9_]/g, '_');
|
|
145
|
+
return `import ${moduleName}`;
|
|
146
|
+
}).join('\n');
|
|
147
|
+
// If the file already contains markers, let updateGeneratedSection handle replacement.
|
|
148
|
+
const fileContent = fs.readFileSync(filePath, 'utf8');
|
|
149
|
+
if (fileContent.indexOf(startMarker) !== -1) {
|
|
150
|
+
updateGeneratedSection(filePath, imports, startMarker, endMarker);
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
// Otherwise insert the generated imports after the last existing import statement (if any),
|
|
154
|
+
// or after `import Foundation` specifically, or at the top as a fallback.
|
|
155
|
+
const importRegex = /^(import\s+[^\r\n]+)\r?\n/gm;
|
|
156
|
+
let match = null;
|
|
157
|
+
let lastMatchEnd = -1;
|
|
158
|
+
while ((match = importRegex.exec(fileContent)) !== null) {
|
|
159
|
+
lastMatchEnd = importRegex.lastIndex;
|
|
160
|
+
}
|
|
161
|
+
const block = `${startMarker}\n${imports}\n${endMarker}`;
|
|
162
|
+
let newContent;
|
|
163
|
+
if (lastMatchEnd !== -1) {
|
|
164
|
+
const before = fileContent.slice(0, lastMatchEnd);
|
|
165
|
+
const after = fileContent.slice(lastMatchEnd);
|
|
166
|
+
newContent = `${before}\n${block}\n${after}`;
|
|
167
|
+
}
|
|
168
|
+
else {
|
|
169
|
+
const foundationIdx = fileContent.indexOf('import Foundation');
|
|
170
|
+
if (foundationIdx !== -1) {
|
|
171
|
+
const lineEnd = fileContent.indexOf('\n', foundationIdx);
|
|
172
|
+
const insertPos = lineEnd !== -1 ? lineEnd + 1 : foundationIdx + 'import Foundation'.length;
|
|
173
|
+
const before = fileContent.slice(0, insertPos);
|
|
174
|
+
const after = fileContent.slice(insertPos);
|
|
175
|
+
newContent = `${before}\n${block}\n${after}`;
|
|
176
|
+
}
|
|
177
|
+
else {
|
|
178
|
+
// Prepend at top
|
|
179
|
+
newContent = `${block}\n\n${fileContent}`;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
fs.writeFileSync(filePath, newContent, 'utf8');
|
|
183
|
+
console.log(`✅ Updated imports in ${path.basename(filePath)}`);
|
|
184
|
+
}
|
|
185
|
+
// Update imports first so registration lines can reference imported modules if needed
|
|
186
|
+
updateImportsSection(lynxInitPath, iosPackages);
|
|
187
|
+
if (iosPackages.length === 0) {
|
|
188
|
+
const placeholder = ' // No native modules found by Tamer4Lynx autolinker.';
|
|
189
|
+
updateGeneratedSection(lynxInitPath, placeholder, '// GENERATED AUTOLINK START', '// GENERATED AUTOLINK END');
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
const blocks = iosPackages.map((pkg) => {
|
|
193
|
+
const classNameRaw = pkg.config.ios.moduleClassName;
|
|
194
|
+
return [
|
|
195
|
+
` // Register module from package: ${pkg.name}`,
|
|
196
|
+
` globalConfig.register(${classNameRaw}.self)`,
|
|
197
|
+
].join('\n');
|
|
198
|
+
});
|
|
199
|
+
const content = blocks.join('\n\n');
|
|
200
|
+
updateGeneratedSection(lynxInitPath, content, '// GENERATED AUTOLINK START', '// GENERATED AUTOLINK END');
|
|
201
|
+
}
|
|
202
|
+
// --- Pod install helper ---
|
|
203
|
+
function runPodInstall(forcePath) {
|
|
204
|
+
const podfilePath = forcePath ?? path.join(iosProjectPath, 'Podfile');
|
|
205
|
+
if (!fs.existsSync(podfilePath)) {
|
|
206
|
+
console.log('ℹ️ No Podfile found in ios directory; skipping `pod install`.');
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
const cwd = path.dirname(podfilePath);
|
|
210
|
+
try {
|
|
211
|
+
console.log(`ℹ️ Running ` + '`pod install`' + ` in ${cwd}...`);
|
|
212
|
+
execSync('pod install', { cwd, stdio: 'inherit' });
|
|
213
|
+
console.log('✅ `pod install` completed successfully.');
|
|
214
|
+
}
|
|
215
|
+
catch (e) {
|
|
216
|
+
console.warn(`⚠️ 'pod install' failed: ${e.message}`);
|
|
217
|
+
console.log('⚠️ You can run `pod install` manually in the ios directory.');
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
// --- Main Execution ---
|
|
221
|
+
function run() {
|
|
222
|
+
console.log('🔎 Finding Lynx extension packages (lynx.ext.json / tamer.json)...');
|
|
223
|
+
const packages = findExtensionPackages();
|
|
224
|
+
if (packages.length > 0) {
|
|
225
|
+
console.log(`Found ${packages.length} package(s): ${packages.map(p => p.name).join(', ')}`);
|
|
226
|
+
}
|
|
227
|
+
else {
|
|
228
|
+
console.log('ℹ️ No Tamer4Lynx native packages found.');
|
|
229
|
+
}
|
|
230
|
+
updatePodfile(packages);
|
|
231
|
+
updateLynxInitProcessor(packages);
|
|
232
|
+
const appNameFromConfig = resolved.config.ios?.appName;
|
|
233
|
+
if (appNameFromConfig) {
|
|
234
|
+
const appPodfile = path.join(iosProjectPath, appNameFromConfig, 'Podfile');
|
|
235
|
+
if (fs.existsSync(appPodfile)) {
|
|
236
|
+
runPodInstall(appPodfile);
|
|
237
|
+
console.log('✨ Autolinking complete for iOS.');
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
runPodInstall();
|
|
242
|
+
console.log('✨ Autolinking complete for iOS.');
|
|
243
|
+
}
|
|
244
|
+
run();
|
|
245
|
+
};
|
|
246
|
+
export default autolink;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { execSync } from 'child_process';
|
|
4
|
+
import { resolveHostPaths } from '../common/hostConfig';
|
|
5
|
+
import ios_bundle from './bundle';
|
|
6
|
+
function buildIpa(opts = {}) {
|
|
7
|
+
const target = opts.target ?? 'host';
|
|
8
|
+
const resolved = resolveHostPaths();
|
|
9
|
+
if (!resolved.config.ios?.appName) {
|
|
10
|
+
throw new Error('"ios.appName" must be defined in tamer.config.json');
|
|
11
|
+
}
|
|
12
|
+
if (target === 'dev-app') {
|
|
13
|
+
console.error('❌ iOS dev-app target not yet implemented.');
|
|
14
|
+
process.exit(1);
|
|
15
|
+
}
|
|
16
|
+
const appName = resolved.config.ios.appName;
|
|
17
|
+
const iosDir = resolved.iosDir;
|
|
18
|
+
ios_bundle({ target });
|
|
19
|
+
const scheme = appName;
|
|
20
|
+
const workspacePath = path.join(iosDir, `${appName}.xcworkspace`);
|
|
21
|
+
const projectPath = path.join(iosDir, `${appName}.xcodeproj`);
|
|
22
|
+
const xcproject = fs.existsSync(workspacePath) ? workspacePath : projectPath;
|
|
23
|
+
const flag = xcproject.endsWith('.xcworkspace') ? '-workspace' : '-project';
|
|
24
|
+
console.log(`\n🔨 Building IPA...`);
|
|
25
|
+
execSync(`xcodebuild -${flag} "${xcproject}" -scheme "${scheme}" -configuration Debug -sdk iphoneos -derivedDataPath build`, {
|
|
26
|
+
stdio: 'inherit',
|
|
27
|
+
cwd: iosDir,
|
|
28
|
+
});
|
|
29
|
+
console.log('✅ IPA built successfully.');
|
|
30
|
+
}
|
|
31
|
+
export default buildIpa;
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { execSync } from 'child_process';
|
|
4
|
+
import { resolveHostPaths } from '../common/hostConfig';
|
|
5
|
+
import ios_autolink from './autolink';
|
|
6
|
+
function bundleAndDeploy(opts = {}) {
|
|
7
|
+
const target = opts.target ?? 'host';
|
|
8
|
+
let resolved;
|
|
9
|
+
try {
|
|
10
|
+
resolved = resolveHostPaths();
|
|
11
|
+
if (!resolved.config.ios?.appName) {
|
|
12
|
+
throw new Error('"ios.appName" must be defined in tamer.config.json');
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
catch (error) {
|
|
16
|
+
console.error(`❌ Error loading configuration: ${error.message}`);
|
|
17
|
+
process.exit(1);
|
|
18
|
+
}
|
|
19
|
+
const appName = resolved.config.ios.appName;
|
|
20
|
+
const sourceBundlePath = resolved.lynxBundlePath;
|
|
21
|
+
const destinationDir = path.join(resolved.iosDir, appName);
|
|
22
|
+
const destinationBundlePath = path.join(destinationDir, resolved.lynxBundleFile);
|
|
23
|
+
if (target === 'dev-app') {
|
|
24
|
+
console.error('❌ iOS dev-app target not yet implemented.');
|
|
25
|
+
process.exit(1);
|
|
26
|
+
}
|
|
27
|
+
ios_autolink();
|
|
28
|
+
if (resolved.devMode === 'embedded' && resolved.devClientBundlePath) {
|
|
29
|
+
const devAppDir = path.dirname(path.dirname(resolved.devClientBundlePath));
|
|
30
|
+
try {
|
|
31
|
+
console.log('📦 Building tamer-dev-app...');
|
|
32
|
+
execSync('npm run build', { stdio: 'inherit', cwd: devAppDir });
|
|
33
|
+
console.log('✅ Dev client build completed.');
|
|
34
|
+
}
|
|
35
|
+
catch (error) {
|
|
36
|
+
console.error('❌ Dev client build failed.');
|
|
37
|
+
process.exit(1);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
try {
|
|
41
|
+
console.log('📦 Starting the build process...');
|
|
42
|
+
execSync('npm run build', { stdio: 'inherit', cwd: resolved.lynxProjectDir });
|
|
43
|
+
console.log('✅ Build completed successfully.');
|
|
44
|
+
}
|
|
45
|
+
catch (error) {
|
|
46
|
+
console.error('❌ Build process failed. Please check the errors above.');
|
|
47
|
+
process.exit(1);
|
|
48
|
+
}
|
|
49
|
+
try {
|
|
50
|
+
if (!fs.existsSync(sourceBundlePath)) {
|
|
51
|
+
console.error(`❌ Build output not found at: ${sourceBundlePath}`);
|
|
52
|
+
process.exit(1);
|
|
53
|
+
}
|
|
54
|
+
if (!fs.existsSync(destinationDir)) {
|
|
55
|
+
console.error(`Destination directory not found at: ${destinationDir}`);
|
|
56
|
+
process.exit(1);
|
|
57
|
+
}
|
|
58
|
+
if (resolved.devMode === 'embedded' && resolved.devClientBundlePath && fs.existsSync(resolved.devClientBundlePath)) {
|
|
59
|
+
const devClientDest = path.join(destinationDir, 'dev-client.lynx.bundle');
|
|
60
|
+
fs.copyFileSync(resolved.devClientBundlePath, devClientDest);
|
|
61
|
+
console.log(`✨ Copied dev-client.lynx.bundle to iOS project`);
|
|
62
|
+
}
|
|
63
|
+
console.log(`🚚 Copying bundle to iOS project...`);
|
|
64
|
+
fs.copyFileSync(sourceBundlePath, destinationBundlePath);
|
|
65
|
+
console.log(`✨ Successfully copied bundle to: ${destinationBundlePath}`);
|
|
66
|
+
}
|
|
67
|
+
catch (error) {
|
|
68
|
+
console.error('❌ Failed to copy the bundle file.');
|
|
69
|
+
console.error(error.message);
|
|
70
|
+
process.exit(1);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
export default bundleAndDeploy;
|