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.
@@ -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
+ });