apptuner 0.1.0
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/README.md +101 -0
- package/dist/cli.js +360 -0
- package/dist/cli.js.map +7 -0
- package/metro-bundle.js +310 -0
- package/metro-server.cjs +405 -0
- package/package.json +87 -0
- package/watcher-server.cjs +115 -0
package/metro-bundle.js
ADDED
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// AppTuner Metro Bundle Script
|
|
4
|
+
// Bundles a React Native project using Metro bundler.
|
|
5
|
+
//
|
|
6
|
+
// Usage: node metro-bundle.js /path/to/react-native-project [entryPoint]
|
|
7
|
+
//
|
|
8
|
+
// This script is part of the AppTuner installation and is called by metro-server.cjs.
|
|
9
|
+
// It uses Metro from AppTuner's own node_modules, but loads metro.config.js from
|
|
10
|
+
// the target project directory so the user's configuration is respected.
|
|
11
|
+
|
|
12
|
+
const Metro = require('metro');
|
|
13
|
+
const path = require('path');
|
|
14
|
+
const fs = require('fs');
|
|
15
|
+
const { execSync } = require('child_process');
|
|
16
|
+
|
|
17
|
+
// Project root is passed as first argument; falls back to cwd
|
|
18
|
+
const projectRoot = process.argv[2] ? path.resolve(process.argv[2]) : process.cwd();
|
|
19
|
+
|
|
20
|
+
// Detect the entry file: prefer index.js/ts/tsx (standard RN convention).
|
|
21
|
+
// The entryPoint arg from the CLI may be 'App.tsx' but Metro should be pointed at
|
|
22
|
+
// index.js which registers the component and sets global.App.
|
|
23
|
+
function detectEntryFile() {
|
|
24
|
+
const candidates = ['index.js', 'index.ts', 'index.tsx', process.argv[3]].filter(Boolean);
|
|
25
|
+
for (const name of candidates) {
|
|
26
|
+
const full = path.join(projectRoot, name);
|
|
27
|
+
if (fs.existsSync(full)) return name;
|
|
28
|
+
}
|
|
29
|
+
return process.argv[3] || 'index.js';
|
|
30
|
+
}
|
|
31
|
+
const entryFileName = detectEntryFile();
|
|
32
|
+
|
|
33
|
+
async function bundle() {
|
|
34
|
+
const entryFile = path.join(projectRoot, entryFileName);
|
|
35
|
+
|
|
36
|
+
console.log('📦 Bundling from project directory...');
|
|
37
|
+
console.log('📁 Project root:', projectRoot);
|
|
38
|
+
console.log('📄 Entry file:', entryFile);
|
|
39
|
+
|
|
40
|
+
// Clear Metro cache to ensure fresh bundles
|
|
41
|
+
const metroCacheDir = path.join(projectRoot, '.metro');
|
|
42
|
+
try {
|
|
43
|
+
if (fs.existsSync(metroCacheDir)) {
|
|
44
|
+
console.log('🗑️ Clearing Metro cache directory:', metroCacheDir);
|
|
45
|
+
fs.rmSync(metroCacheDir, { recursive: true, force: true });
|
|
46
|
+
}
|
|
47
|
+
try {
|
|
48
|
+
execSync(`rm -rf /tmp/metro-*`, { stdio: 'ignore' });
|
|
49
|
+
console.log('🗑️ Cleared Metro temp cache');
|
|
50
|
+
} catch (e) {
|
|
51
|
+
// Ignore rm errors
|
|
52
|
+
}
|
|
53
|
+
} catch (e) {
|
|
54
|
+
console.warn('⚠️ Failed to clear Metro cache:', e.message);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Load config from the target project directory
|
|
58
|
+
// This respects the project's own metro.config.js if it exists
|
|
59
|
+
const config = await Metro.loadConfig({
|
|
60
|
+
cwd: projectRoot,
|
|
61
|
+
resetCache: true,
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
// Bundle the project
|
|
65
|
+
let code;
|
|
66
|
+
let assetData = [];
|
|
67
|
+
|
|
68
|
+
try {
|
|
69
|
+
const result = await Metro.runBuild(config, {
|
|
70
|
+
entry: entryFile,
|
|
71
|
+
dev: true,
|
|
72
|
+
minify: false,
|
|
73
|
+
platform: 'ios',
|
|
74
|
+
sourceMap: false,
|
|
75
|
+
});
|
|
76
|
+
code = result.code;
|
|
77
|
+
|
|
78
|
+
// Extract asset data from bundle code BEFORE wrapping
|
|
79
|
+
console.log('🔍 Extracting asset data from bundle...');
|
|
80
|
+
const assetRegex = /registerAsset\(\{([^}]+(?:\}[^}]*)*)\}\)/g;
|
|
81
|
+
let match;
|
|
82
|
+
|
|
83
|
+
while ((match = assetRegex.exec(code)) !== null) {
|
|
84
|
+
try {
|
|
85
|
+
const assetStr = match[1];
|
|
86
|
+
const nameMatch = assetStr.match(/"name":\s*"([^"]+)"/);
|
|
87
|
+
const uriMatch = assetStr.match(/"uri":\s*"data:([^"]+)"/);
|
|
88
|
+
const typeMatch = assetStr.match(/"type":\s*"([^"]+)"/);
|
|
89
|
+
|
|
90
|
+
if (nameMatch && uriMatch) {
|
|
91
|
+
assetData.push({
|
|
92
|
+
name: nameMatch[1],
|
|
93
|
+
uri: 'data:' + uriMatch[1],
|
|
94
|
+
type: typeMatch ? typeMatch[1] : 'unknown'
|
|
95
|
+
});
|
|
96
|
+
console.log(`✅ Found asset: ${nameMatch[1]}.${typeMatch ? typeMatch[1] : 'unknown'}`);
|
|
97
|
+
}
|
|
98
|
+
} catch (e) {
|
|
99
|
+
console.warn('⚠️ Failed to parse asset:', e.message);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
console.log(`📦 Extracted ${assetData.length} assets from bundle`);
|
|
104
|
+
} catch (error) {
|
|
105
|
+
console.error('Metro build error:', error);
|
|
106
|
+
if (error.type === 'TransformError' || error.message) {
|
|
107
|
+
const errorInfo = {
|
|
108
|
+
type: error.type || 'BuildError',
|
|
109
|
+
message: error.message,
|
|
110
|
+
filename: error.filename,
|
|
111
|
+
lineNumber: error.lineNumber,
|
|
112
|
+
column: error.column,
|
|
113
|
+
stack: error.stack
|
|
114
|
+
};
|
|
115
|
+
process.stderr.write(JSON.stringify(errorInfo) + '\n');
|
|
116
|
+
}
|
|
117
|
+
process.exit(1);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// CRITICAL FIX: Patch NativeEventEmitter constructor to handle null modules
|
|
121
|
+
console.log('[Bundle] Patching NativeEventEmitter invariant checks...');
|
|
122
|
+
console.log('[Bundle] Original bundle size:', code.length, 'bytes');
|
|
123
|
+
|
|
124
|
+
const originalCode = code;
|
|
125
|
+
|
|
126
|
+
code = code.replace(
|
|
127
|
+
/if\s*\(\s*Platform\.OS\s*===\s*['"]ios['"]\s*\)\s*\{[\s\S]{0,200}?invariant\s*\([\s\S]{0,200}?nativeModule[\s\S]{0,200}?\);[\s\S]{0,50}?\}/g,
|
|
128
|
+
`if (Platform.OS === 'ios' && !nativeModule) {
|
|
129
|
+
console.warn('[NativeEventEmitter] Created with null module, using mock');
|
|
130
|
+
nativeModule = { addListener: function(){}, removeListeners: function(){} };
|
|
131
|
+
}`
|
|
132
|
+
);
|
|
133
|
+
|
|
134
|
+
const wasPatched = code !== originalCode;
|
|
135
|
+
console.log('[Bundle] NativeEventEmitter invariant checks', wasPatched ? 'PATCHED' : 'NOT FOUND');
|
|
136
|
+
console.log('[Bundle] Patched bundle size:', code.length, 'bytes');
|
|
137
|
+
|
|
138
|
+
// HOT RELOAD FIX: Patch Metro's __d to allow hot reload with cache invalidation
|
|
139
|
+
console.log('[Bundle] Patching Metro __d to allow hot reload with cache invalidation...');
|
|
140
|
+
|
|
141
|
+
let hotReloadCode = code;
|
|
142
|
+
|
|
143
|
+
// Pattern 1: Newer Metro with modules.has()
|
|
144
|
+
const newMetroPattern = /if\s*\(\s*modules\.has\(moduleId\)\s*\)\s*\{[\s\S]{1,300}?return;\s*\}/g;
|
|
145
|
+
const matches1 = (hotReloadCode.match(newMetroPattern) || []).length;
|
|
146
|
+
hotReloadCode = hotReloadCode.replace(
|
|
147
|
+
newMetroPattern,
|
|
148
|
+
`if (modules.has(moduleId)) {
|
|
149
|
+
// HOT RELOAD: Track redefinition and reset initialized flag
|
|
150
|
+
const existingMod = modules.get(moduleId);
|
|
151
|
+
if (existingMod) {
|
|
152
|
+
const _realGlobal = (typeof global !== 'undefined' ? global : (typeof window !== 'undefined' ? window : (typeof globalThis !== 'undefined' ? globalThis : this)));
|
|
153
|
+
const _g = _realGlobal && _realGlobal.__APPTUNER_GLOBAL ? _realGlobal.__APPTUNER_GLOBAL : _realGlobal;
|
|
154
|
+
if (_g && _g.__dirtyModules) {
|
|
155
|
+
_g.__dirtyModules.add(moduleId);
|
|
156
|
+
}
|
|
157
|
+
existingMod.isInitialized = false;
|
|
158
|
+
if (_g && typeof _g.__c === 'function') {
|
|
159
|
+
try {
|
|
160
|
+
_g.__c(moduleId);
|
|
161
|
+
} catch (e) {
|
|
162
|
+
console.warn('[HotReload] Cache clear failed:', e);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
// Continue with redefinition instead of returning
|
|
167
|
+
}`
|
|
168
|
+
);
|
|
169
|
+
console.log('[Bundle] New Metro pattern (modules.has) replaced:', matches1, 'occurrences');
|
|
170
|
+
|
|
171
|
+
// Pattern 2: Older Metro with modules[moduleId]
|
|
172
|
+
const oldMetroPattern = /if\s*\(\s*modules\[moduleId\]\s*!=\s*null\s*\)\s*\{[\s\S]{1,300}?\/\/\s*prevent[\s\S]{1,100}?return;\s*\}/g;
|
|
173
|
+
const matches2 = (hotReloadCode.match(oldMetroPattern) || []).length;
|
|
174
|
+
hotReloadCode = hotReloadCode.replace(
|
|
175
|
+
oldMetroPattern,
|
|
176
|
+
`if (modules[moduleId] != null) {
|
|
177
|
+
// HOT RELOAD: Reset initialized flag to force re-execution
|
|
178
|
+
modules[moduleId].isInitialized = false;
|
|
179
|
+
// Continue with redefinition instead of returning
|
|
180
|
+
}`
|
|
181
|
+
);
|
|
182
|
+
console.log('[Bundle] Old Metro pattern (!=) replaced:', matches2, 'occurrences');
|
|
183
|
+
|
|
184
|
+
code = hotReloadCode;
|
|
185
|
+
|
|
186
|
+
// Generate bundle ID before wrapper
|
|
187
|
+
const bundleId = Date.now();
|
|
188
|
+
const bundleTime = new Date().toISOString();
|
|
189
|
+
|
|
190
|
+
// Wrap the bundle with initialization code
|
|
191
|
+
const wrappedCode =
|
|
192
|
+
'// AppTuner Metro Bundle Wrapper\n' +
|
|
193
|
+
'(function() {\n' +
|
|
194
|
+
' console.log(\'[Metro Bundle] Starting...\');\n' +
|
|
195
|
+
'\n' +
|
|
196
|
+
' const globalObj = (typeof global !== \'undefined\' ? global : (typeof window !== \'undefined\' ? window : this));\n' +
|
|
197
|
+
' const originalDefine = globalObj.__d;\n' +
|
|
198
|
+
'\n' +
|
|
199
|
+
' // Install __d hijack to patch react-native module at definition time\n' +
|
|
200
|
+
' if (originalDefine) {\n' +
|
|
201
|
+
' globalObj.__d = function(factory, moduleId, dependencyMap) {\n' +
|
|
202
|
+
' const wrappedFactory = function(g, require, importDefault, importAll, moduleObject, exports, dependencyMap) {\n' +
|
|
203
|
+
' factory(g, require, importDefault, importAll, moduleObject, exports, dependencyMap);\n' +
|
|
204
|
+
' if (moduleObject.exports && moduleObject.exports.NativeEventEmitter) {\n' +
|
|
205
|
+
' const OriginalNE = moduleObject.exports.NativeEventEmitter;\n' +
|
|
206
|
+
' const PatchedNE = function(nativeModule) {\n' +
|
|
207
|
+
' if (!nativeModule) {\n' +
|
|
208
|
+
' console.warn(\'[Bundle __d] NativeEventEmitter null arg blocked, using mock\');\n' +
|
|
209
|
+
' return OriginalNE.call(this, { addListener: function(){}, removeListeners: function(){} });\n' +
|
|
210
|
+
' }\n' +
|
|
211
|
+
' return OriginalNE.call(this, nativeModule);\n' +
|
|
212
|
+
' };\n' +
|
|
213
|
+
' PatchedNE.prototype = OriginalNE.prototype;\n' +
|
|
214
|
+
' for (var key in OriginalNE) {\n' +
|
|
215
|
+
' if (OriginalNE.hasOwnProperty(key)) { PatchedNE[key] = OriginalNE[key]; }\n' +
|
|
216
|
+
' }\n' +
|
|
217
|
+
' moduleObject.exports.NativeEventEmitter = PatchedNE;\n' +
|
|
218
|
+
' }\n' +
|
|
219
|
+
' };\n' +
|
|
220
|
+
' return originalDefine(wrappedFactory, moduleId, dependencyMap);\n' +
|
|
221
|
+
' };\n' +
|
|
222
|
+
' }\n' +
|
|
223
|
+
'\n' +
|
|
224
|
+
' // Set __APPTUNER_GLOBAL for hot reload module tracking\n' +
|
|
225
|
+
' if (!globalObj.__APPTUNER_GLOBAL) {\n' +
|
|
226
|
+
' globalObj.__APPTUNER_GLOBAL = globalObj;\n' +
|
|
227
|
+
' }\n' +
|
|
228
|
+
'\n' +
|
|
229
|
+
' // Cache clearing function used by patched __d\n' +
|
|
230
|
+
' globalObj.__APPTUNER_GLOBAL.__c = function(moduleId) {\n' +
|
|
231
|
+
' const metroRequire = globalObj.__r;\n' +
|
|
232
|
+
' if (metroRequire && metroRequire.c) {\n' +
|
|
233
|
+
' metroRequire.c.delete(moduleId);\n' +
|
|
234
|
+
' }\n' +
|
|
235
|
+
' };\n' +
|
|
236
|
+
'\n' +
|
|
237
|
+
' // Patch NativeEventEmitter before any modules load\n' +
|
|
238
|
+
' if (this.ReactNative && this.ReactNative.NativeEventEmitter) {\n' +
|
|
239
|
+
' const OrigNE = this.ReactNative.NativeEventEmitter;\n' +
|
|
240
|
+
' const SafeNE = function(nativeModule) {\n' +
|
|
241
|
+
' if (!nativeModule) {\n' +
|
|
242
|
+
' console.warn(\'[Bundle] NativeEventEmitter created with null module, returning mock\');\n' +
|
|
243
|
+
' this.addListener = function() { return { remove: function() {} }; };\n' +
|
|
244
|
+
' this.removeListener = function() {};\n' +
|
|
245
|
+
' this.removeAllListeners = function() {};\n' +
|
|
246
|
+
' this.emit = function() {};\n' +
|
|
247
|
+
' this.listenerCount = function() { return 0; };\n' +
|
|
248
|
+
' return this;\n' +
|
|
249
|
+
' }\n' +
|
|
250
|
+
' return OrigNE.call(this, nativeModule);\n' +
|
|
251
|
+
' };\n' +
|
|
252
|
+
' SafeNE.prototype = OrigNE.prototype;\n' +
|
|
253
|
+
' Object.setPrototypeOf(SafeNE, OrigNE);\n' +
|
|
254
|
+
' this.ReactNative.NativeEventEmitter = SafeNE;\n' +
|
|
255
|
+
' if (globalObj && globalObj.ReactNative) { globalObj.ReactNative.NativeEventEmitter = SafeNE; }\n' +
|
|
256
|
+
' }\n' +
|
|
257
|
+
'\n' +
|
|
258
|
+
' // Set up AssetRegistry before bundle executes\n' +
|
|
259
|
+
' const assetMap = new Map();\n' +
|
|
260
|
+
' const enhancedRegistry = {\n' +
|
|
261
|
+
' registerAsset: function(asset) {\n' +
|
|
262
|
+
' const assetId = assetMap.size + 1;\n' +
|
|
263
|
+
' assetMap.set(assetId, asset);\n' +
|
|
264
|
+
' return assetId;\n' +
|
|
265
|
+
' },\n' +
|
|
266
|
+
' getAssetByID: function(assetId) { return assetMap.get(assetId) || null; }\n' +
|
|
267
|
+
' };\n' +
|
|
268
|
+
' globalObj.AssetRegistry = enhancedRegistry;\n' +
|
|
269
|
+
'\n' +
|
|
270
|
+
' const extractedAssets = ' + JSON.stringify(assetData) + ';\n' +
|
|
271
|
+
' extractedAssets.forEach(asset => enhancedRegistry.registerAsset(asset));\n' +
|
|
272
|
+
'\n' +
|
|
273
|
+
' const resolveAssetSource = function(source) {\n' +
|
|
274
|
+
' if (typeof source === \'number\') {\n' +
|
|
275
|
+
' const asset = assetMap.get(source);\n' +
|
|
276
|
+
' if (asset && asset.uri) { return { uri: asset.uri }; }\n' +
|
|
277
|
+
' }\n' +
|
|
278
|
+
' return source;\n' +
|
|
279
|
+
' };\n' +
|
|
280
|
+
' globalObj.resolveAssetSource = resolveAssetSource;\n' +
|
|
281
|
+
' this.resolveAssetSource = resolveAssetSource;\n' +
|
|
282
|
+
'\n' +
|
|
283
|
+
' // Clear module 0 cache for hot reload if this is a re-bundle\n' +
|
|
284
|
+
' if (globalObj.__r && globalObj.__r.c && globalObj.__r.c.get) {\n' +
|
|
285
|
+
' const mod0 = globalObj.__r.c.get(0);\n' +
|
|
286
|
+
' if (mod0 && mod0.isInitialized) {\n' +
|
|
287
|
+
' mod0.isInitialized = false;\n' +
|
|
288
|
+
' }\n' +
|
|
289
|
+
' }\n' +
|
|
290
|
+
'\n' +
|
|
291
|
+
' // Execute the Metro bundle\n' +
|
|
292
|
+
code +
|
|
293
|
+
'\n' +
|
|
294
|
+
'\n' +
|
|
295
|
+
' globalObj.BUNDLE_ID = ' + bundleId + ';\n' +
|
|
296
|
+
' globalObj.BUNDLE_TIME = "' + bundleTime + '";\n' +
|
|
297
|
+
'}.call(this));\n';
|
|
298
|
+
|
|
299
|
+
const finalCode = wrappedCode;
|
|
300
|
+
|
|
301
|
+
// Output to stdout so metro-server can capture it
|
|
302
|
+
console.log('__BUNDLE_START__');
|
|
303
|
+
process.stdout.write(finalCode);
|
|
304
|
+
console.log('\n__BUNDLE_END__');
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
bundle().catch(error => {
|
|
308
|
+
console.error('Bundle error:', error);
|
|
309
|
+
process.exit(1);
|
|
310
|
+
});
|