diff --git a/scripts/bundle-metadata-plugin.js b/scripts/bundle-metadata-plugin.js new file mode 100644 index 00000000..b7a8a8c8 --- /dev/null +++ b/scripts/bundle-metadata-plugin.js @@ -0,0 +1,184 @@ +const crypto = require('crypto'); +const fs = require('fs'); +const path = require('path'); +const PROJECT_ROOT = path.resolve(__dirname, '../../..'); +console.log(`[Bundle Metadata] Project root: ${PROJECT_ROOT}`); + +function calculateContentHash(bundleCode) { + const hash = crypto.createHash('sha256'); + hash.update(bundleCode, 'utf8'); + return hash.digest('hex'); +} + +function generateMetadataInjection(contentHash) { + return `// Auto-injected bundle metadata by Metro plugin + var __BUNDLE_METADATA__ = { + contentHash: '${contentHash}' + }; +`; +} + +function generateMetadataComment(contentHash) { + return `\n//# BUNDLE_METADATA ${JSON.stringify({ + contentHash +})}`; +} + +function setupSingleHermesc(hermescPath, locationName) { + const hermescDir = path.dirname(hermescPath); + const backupHermescPath = path.join(hermescDir, '_hermesc'); + const wrapperSourcePath = path.join(__dirname, 'hermesc-wrapper.js'); + + if (fs.existsSync(backupHermescPath)) { + console.log(`⏭️ [Hermesc Setup] ${locationName} already configured, skipping...`); + return true; + } + + if (!fs.existsSync(hermescPath)) { + console.log(`ℹ️ [Hermesc Setup] ${locationName} hermesc not found at: ${hermescPath}`); + return false; + } + + if (!fs.existsSync(wrapperSourcePath)) { + console.error(`❌ [Hermesc Setup] Wrapper script not found at: ${wrapperSourcePath}`); + return false; + } + + try { + console.log(`🔧 [Hermesc Setup] Setting up hermesc wrapper for ${locationName}...`); + + fs.renameSync(hermescPath, backupHermescPath); + console.log(`✅ [Hermesc Setup] ${locationName}: Renamed hermesc -> _hermesc`); + + const shellScript = `#!/bin/bash +# Hermesc wrapper script - auto-generated +# This script calls the Node.js wrapper which handles post-processing + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +WRAPPER_SCRIPT="${wrapperSourcePath}" + +find_node() { + if command -v node >/dev/null 2>&1; then + command -v node + return 0 + fi + + local NODE_PATHS=( + "/usr/local/bin/node" + "/opt/homebrew/bin/node" + "$HOME/.nvm/versions/node/$(ls -t "$HOME/.nvm/versions/node" 2>/dev/null | head -1)/bin/node" + "/usr/bin/node" + ) + + for node_path in "\${NODE_PATHS[@]}"; do + if [ -x "$node_path" ]; then + echo "$node_path" + return 0 + fi + done + + echo "Error: node executable not found" >&2 + echo "Please ensure Node.js is installed and accessible" >&2 + exit 1 +} + +NODE_BIN=$(find_node) +exec "$NODE_BIN" "$WRAPPER_SCRIPT" "$@" +`; + + fs.writeFileSync(hermescPath, shellScript, { mode: 0o755 }); + console.log(`✅ [Hermesc Setup] ${locationName}: Created hermesc wrapper shell script`); + + console.log(`🎉 [Hermesc Setup] ${locationName} configured successfully!`); + console.log(`📋 [Hermesc Setup] ${locationName} details:`); + console.log(` - Original: ${backupHermescPath}`); + console.log(` - Wrapper: ${hermescPath}`); + console.log(` - Handler: ${wrapperSourcePath}`); + + return true; + } catch (error) { + console.error(`❌ [Hermesc Setup] Failed to setup hermesc wrapper for ${locationName}:`, error); + + if (fs.existsSync(backupHermescPath) && !fs.existsSync(hermescPath)) { + try { + fs.renameSync(backupHermescPath, hermescPath); + console.log(`🔄 [Hermesc Setup] ${locationName}: Rolled back changes`); + } catch (rollbackError) { + console.error(`❌ [Hermesc Setup] ${locationName}: Rollback failed:`, rollbackError); + } + } + + return false; + } +} + +function setupHermescWrapper() { + const wrapperSourcePath = path.join(__dirname, 'hermesc-wrapper.js'); + + if (!fs.existsSync(wrapperSourcePath)) { + console.error(`❌ [Hermesc Setup] Wrapper script not found at: ${wrapperSourcePath}`); + return; + } + + try { + fs.chmodSync(wrapperSourcePath, 0o755); + } catch (error) { + console.error('❌ [Hermesc Setup] Failed to set execute permissions on wrapper:', error); + } + + console.log('🔧 [Hermesc Setup] Starting hermesc wrapper setup...'); + + const hermescLocations = [ + { + path: path.join(PROJECT_ROOT, 'node_modules/react-native/sdks/hermesc/osx-bin/hermesc'), + name: 'Node Modules' + }, + { + path: path.join(PROJECT_ROOT, 'ios/Pods/hermes-engine/destroot/bin/hermesc'), + name: 'iOS Pods' + } + ]; + + let successCount = 0; + let totalProcessed = 0; + + for (const location of hermescLocations) { + const success = setupSingleHermesc(location.path, location.name); + if (success) { + successCount++; + } + totalProcessed++; + } + + console.log(`\n📊 [Hermesc Setup] Summary: ${successCount}/${totalProcessed} locations configured successfully`); +} + +function metadataSerializer(entryPoint, preModules, graph, options) { + setupHermescWrapper(); + const baseJSBundle = require('metro/src/DeltaBundler/Serializers/baseJSBundle'); + const bundleToString = require('metro/src/lib/bundleToString'); + const bundle = baseJSBundle(entryPoint, preModules, graph, options); + const { code: bundleCode } = bundleToString(bundle); + const contentHash = calculateContentHash(bundleCode); + const metadataInjection = generateMetadataInjection(contentHash); + const metadataComment = generateMetadataComment(contentHash); + const hashFilePath = path.join(PROJECT_ROOT, 'bundle-hash.json'); + + try { + const hashData = { + contentHash, + timestamp: new Date().toISOString(), + }; + fs.writeFileSync(hashFilePath, JSON.stringify(hashData, null, 2)); + console.log(`✅ [Metro] Saved hash to: ${hashFilePath}`); + console.log(`🔐 [Metro] Hash: ${contentHash.slice(0, 16)}...`); + } catch (error) { + console.error('❌ [Metro] Failed to save hash file:', error); + } + + return bundleCode + metadataInjection + metadataComment; +} + +module.exports = { + metadataSerializer, +}; diff --git a/scripts/hermesc-wrapper.js b/scripts/hermesc-wrapper.js new file mode 100755 index 00000000..68b63f06 --- /dev/null +++ b/scripts/hermesc-wrapper.js @@ -0,0 +1,97 @@ +#!/usr/bin/env node + +const { spawn } = require('child_process'); +const path = require('path'); +const fs = require('fs'); +const PROJECT_ROOT = path.resolve(__dirname, '../../..'); +const realHermescPath = path.join(PROJECT_ROOT, 'node_modules/react-native/sdks/hermesc/osx-bin/_hermesc'); +const args = process.argv.slice(2); + +console.log(`[Hermesc Wrapper] Executing Hermes compilation...`); +console.log(`[Hermesc Wrapper] Args:`, args.join(' ')); + +const isCompileOperation = args.includes('-emit-binary'); +let outputFile = null; + +const outIndex = args.indexOf('-out'); +if (outIndex !== -1 && outIndex + 1 < args.length) { + outputFile = args[outIndex + 1]; +} + +const hermesc = spawn(realHermescPath, args, { + stdio: 'inherit', + env: process.env +}); + +hermesc.on('error', (error) => { + console.error(`[Hermesc Wrapper] ❌ Failed to start hermesc:`, error); + process.exit(1); +}); + +hermesc.on('close', (code) => { + console.log(`[Hermesc Wrapper] Hermes compilation completed with code: ${code}`); + + if (code === 0 && isCompileOperation && outputFile) { + console.log(`[Hermesc Wrapper] 🔄 Post-processing HBC file: ${outputFile}`); + + setTimeout(() => { + processHBCFile(outputFile); + }, 500); + } else { + process.exit(code); + } +}); + +function processHBCFile(hbcFilePath) { + const hashFilePath = path.join(PROJECT_ROOT, 'bundle-hash.json'); + + if (!fs.existsSync(hashFilePath)) { + console.warn(`[Hermesc Wrapper] ⚠️ Hash file not found: ${hashFilePath}`); + console.warn(`[Hermesc Wrapper] Skipping metadata injection.`); + process.exit(0); + return; + } + + if (!fs.existsSync(hbcFilePath)) { + console.warn(`[Hermesc Wrapper] ⚠️ HBC file not found: ${hbcFilePath}`); + console.warn(`[Hermesc Wrapper] Skipping metadata injection.`); + process.exit(0); + return; + } + + try { + const hashData = JSON.parse(fs.readFileSync(hashFilePath, 'utf8')); + const { contentHash } = hashData; + + console.log(`[Hermesc Wrapper] 📝 Injecting metadata into HBC...`); + console.log(`[Hermesc Wrapper] Hash: ${contentHash.slice(0, 16)}...`); + + const hbcBuffer = fs.readFileSync(hbcFilePath); + + const metadata = { contentHash }; + const metadataJson = JSON.stringify(metadata); + + const MAGIC = Buffer.from('RNUPDATE', 'utf8'); + const jsonBuffer = Buffer.from(metadataJson, 'utf8'); + const lengthBuffer = Buffer.alloc(4); + lengthBuffer.writeUInt32LE(jsonBuffer.length); + + const finalBuffer = Buffer.concat([ + hbcBuffer, + MAGIC, + jsonBuffer, + lengthBuffer, + MAGIC, + ]); + + fs.writeFileSync(hbcFilePath, finalBuffer); + + console.log(`[Hermesc Wrapper] ✅ Successfully injected metadata into: ${hbcFilePath}`); + console.log(`[Hermesc Wrapper] 🧹 Cleaning up hash file...`); + + process.exit(0); + } catch (error) { + console.error(`[Hermesc Wrapper] ❌ Failed to process HBC file:`, error); + process.exit(1); + } +}