Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
184 changes: 184 additions & 0 deletions scripts/bundle-metadata-plugin.js
Original file line number Diff line number Diff line change
@@ -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`);

Comment on lines +47 to +52
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Symlink/permissions-safe swap when wrapping hermesc
Pods binaries can be symlinks; rename may fail or break subsequent installs. Prefer copy+rename fallback and detect symlinks before swapping.

   try {
     console.log(`🔧 [Hermesc Setup] Setting up hermesc wrapper for ${locationName}...`);

-    fs.renameSync(hermescPath, backupHermescPath);
+    const stat = fs.lstatSync(hermescPath);
+    if (stat.isSymbolicLink()) {
+      // Preserve original target, copy real file as _hermesc and replace link with wrapper
+      const real = fs.realpathSync(hermescPath);
+      fs.copyFileSync(real, backupHermescPath);
+      fs.unlinkSync(hermescPath);
+    } else {
+      fs.renameSync(hermescPath, backupHermescPath);
+    }
     console.log(`✅ [Hermesc Setup] ${locationName}: Renamed hermesc -> _hermesc`);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
try {
console.log(`🔧 [Hermesc Setup] Setting up hermesc wrapper for ${locationName}...`);
fs.renameSync(hermescPath, backupHermescPath);
console.log(`✅ [Hermesc Setup] ${locationName}: Renamed hermesc -> _hermesc`);
try {
console.log(`🔧 [Hermesc Setup] Setting up hermesc wrapper for ${locationName}...`);
const stat = fs.lstatSync(hermescPath);
if (stat.isSymbolicLink()) {
// Preserve original target, copy real file as _hermesc and replace link with wrapper
const real = fs.realpathSync(hermescPath);
fs.copyFileSync(real, backupHermescPath);
fs.unlinkSync(hermescPath);
} else {
fs.renameSync(hermescPath, backupHermescPath);
}
console.log(`✅ [Hermesc Setup] ${locationName}: Renamed hermesc -> _hermesc`);
🤖 Prompt for AI Agents
In scripts/bundle-metadata-plugin.js around lines 47-52, the current code uses
fs.renameSync to swap hermesc which will fail or break when hermesc is a
symlink; detect symlinks with fs.lstatSync(hermescPath).isSymbolicLink() and
handle them specially, and also add a rename-fallback that copies instead of
renaming. Concretely: if hermesc is a symlink, resolve the real file
(fs.realpathSync or fs.readlinkSync + path.resolve), copy the real target to
backupHermescPath (fs.copyFileSync) and preserve permissions (fs.chmodSync with
mode from fs.statSync), then replace hermescPath with your wrapper (write new
file or create a symlink to the wrapper target as appropriate). If not a
symlink, try fs.renameSync as before but catch errors and fall back to
copy+chmod to create the backup file so installs won’t break.

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" "$@"
`;
Comment on lines +53 to +87
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Shell wrapper must pass the hermesc directory to the Node wrapper

Export HERMESC_DIR so hermesc-wrapper can spawn the correct local _hermesc (Pods vs node_modules, Linux).

-    const shellScript = `#!/bin/bash
+    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}"
@@
 NODE_BIN=$(find_node)
-exec "$NODE_BIN" "$WRAPPER_SCRIPT" "$@"
+export HERMESC_DIR="$SCRIPT_DIR"
+exec "$NODE_BIN" "$WRAPPER_SCRIPT" "$@"
 `;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
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" "$@"
`;
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)
export HERMESC_DIR="$SCRIPT_DIR"
exec "$NODE_BIN" "$WRAPPER_SCRIPT" "$@"
`;
🤖 Prompt for AI Agents
In scripts/bundle-metadata-plugin.js around lines 53 to 87, the generated shell
wrapper doesn't export HERMESC_DIR so the hermesc Node wrapper can't locate the
correct local _hermesc (Pods vs node_modules, Linux); update the script to
export HERMESC_DIR pointing to the hermesc directory (for example export
HERMESC_DIR="$SCRIPT_DIR" or another computed path if hermesc lives elsewhere
relative to SCRIPT_DIR) before invoking the Node wrapper so hermesc-wrapper can
spawn the correct _hermesc.


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,
};
97 changes: 97 additions & 0 deletions scripts/hermesc-wrapper.js
Original file line number Diff line number Diff line change
@@ -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);
Comment on lines +7 to +8
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Blocker: wrapper hard-codes a single macOS hermesc path; breaks for iOS Pods/Linux and mismatches the wrapped location

Derive the real hermesc from the invoking hermesc dir (exported by the shell wrapper) and fall back to known locations. Without this, invocations from Pods or Linux fail or compile with the wrong binary.

Apply:

-const realHermescPath = path.join(PROJECT_ROOT, 'node_modules/react-native/sdks/hermesc/osx-bin/_hermesc');
+function resolveRealHermescPath() {
+  const candidates = [];
+  if (process.env.HERMESC_DIR) {
+    candidates.push(path.join(process.env.HERMESC_DIR, '_hermesc'));
+  }
+  candidates.push(
+    path.join(PROJECT_ROOT, 'node_modules/react-native/sdks/hermesc/osx-bin/_hermesc'),
+    path.join(PROJECT_ROOT, 'node_modules/react-native/sdks/hermesc/linux64-bin/_hermesc'),
+    path.join(PROJECT_ROOT, 'ios/Pods/hermes-engine/destroot/bin/_hermesc'),
+  );
+  return candidates.find(p => fs.existsSync(p));
+}
+const realHermescPath = resolveRealHermescPath();
+if (!realHermescPath) {
+  console.error('[Hermesc Wrapper] ❌ Could not locate _hermesc (set HERMESC_DIR or install Hermes).');
+  process.exit(1);
+}
+console.log(`[Hermesc Wrapper] Using hermesc: ${realHermescPath}`);

Committable suggestion skipped: line range outside the PR's diff.


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);
Comment on lines +37 to +39
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Remove the 500ms setTimeout workaround.

The arbitrary delay suggests a race condition or file system synchronization issue that should be resolved properly rather than masked with a timeout. This is brittle and may fail under load or on slower systems.

Consider one of these approaches:

Option 1: Synchronous waiting (if hermesc writes are buffered)

-    setTimeout(() => {
-      processHBCFile(outputFile);
-    }, 500);
+    processHBCFile(outputFile);

Option 2: Polling with retry (if writes are truly async)

-    setTimeout(() => {
-      processHBCFile(outputFile);
-    }, 500);
+    waitForFileStable(outputFile, (err) => {
+      if (err) {
+        console.error(`[Hermesc Wrapper] ❌ Output file not ready:`, err);
+        process.exit(1);
+      }
+      processHBCFile(outputFile);
+    });
+
+function waitForFileStable(filePath, callback, maxRetries = 10, delay = 100) {
+  let lastSize = -1;
+  let retries = 0;
+  
+  const check = () => {
+    try {
+      const stats = fs.statSync(filePath);
+      if (stats.size === lastSize) {
+        callback(null);
+      } else {
+        lastSize = stats.size;
+        retries++;
+        if (retries >= maxRetries) {
+          callback(new Error('File not stable'));
+        } else {
+          setTimeout(check, delay);
+        }
+      }
+    } catch (err) {
+      callback(err);
+    }
+  };
+  check();
+}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
setTimeout(() => {
processHBCFile(outputFile);
}, 500);
processHBCFile(outputFile);
🤖 Prompt for AI Agents
In scripts/hermesc-wrapper.js around lines 37 to 39, remove the brittle
setTimeout(…,500) workaround and replace it with a deterministic wait: if the
hermesc write exposes a completion callback or stream, invoke
processHBCFile(outputFile) from that completion handler; otherwise implement a
short polling/retry loop using fs.promises.stat or fs.promises.access to confirm
the file exists and its size is stable (repeat check with small delays and a max
timeout/backoff), then call processHBCFile once the file is present and
unchanged; ensure errors/timeouts are handled and surfaced rather than relying
on a fixed sleep.

} else {
process.exit(code);
}
});

function processHBCFile(hbcFilePath) {
const hashFilePath = path.join(PROJECT_ROOT, 'bundle-hash.json');
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Verify bundle-hash.json is in .gitignore.

The bundle-hash.json file is a runtime-generated artifact that should not be committed to version control. A previous review flagged this, but it's unclear if it was addressed.


🏁 Script executed:

#!/bin/bash
# Verify bundle-hash.json is ignored
if [ -f .gitignore ]; then
  if grep -q "bundle-hash\.json" .gitignore; then
    echo "✅ bundle-hash.json is in .gitignore"
  else
    echo "❌ bundle-hash.json is NOT in .gitignore"
  fi
else
  echo "⚠️  No .gitignore file found at repository root"
fi

# Show .gitignore contents for context
echo -e "\n=== Current .gitignore ==="
cat .gitignore 2>/dev/null || echo "File not found"

Length of output: 932


🏁 Script executed:

#!/bin/bash
# Check for nested .gitignore files that might ignore bundle-hash.json
echo "=== Searching for nested .gitignore files ==="
find . -name ".gitignore" -type f 2>/dev/null | head -20

# Search for bundle-hash.json references in codebase
echo -e "\n=== References to bundle-hash.json ==="
rg "bundle-hash\.json" --type-list | head -5
rg "bundle-hash" -n -B2 -A2 2>/dev/null || echo "No additional references found"

Length of output: 1539


🏁 Script executed:

#!/bin/bash
# Examine context around bundle-hash.json usage in hermesc-wrapper.js
echo "=== hermesc-wrapper.js context (lines 40-60) ==="
sed -n '40,60p' git/scripts/hermesc-wrapper.js

echo -e "\n=== bundle-metadata-plugin.js context (lines 160-175) ==="
sed -n '160,175p' git/scripts/bundle-metadata-plugin.js

Length of output: 262


🏁 Script executed:

#!/bin/bash
# Examine context around bundle-hash.json usage - corrected paths
echo "=== hermesc-wrapper.js context (lines 40-60) ==="
sed -n '40,60p' scripts/hermesc-wrapper.js 2>/dev/null || echo "File not found at scripts/hermesc-wrapper.js"

echo -e "\n=== bundle-metadata-plugin.js context (lines 160-175) ==="
sed -n '160,175p' scripts/bundle-metadata-plugin.js 2>/dev/null || echo "File not found"

# Also try finding the files
echo -e "\n=== Locating files ==="
find . -name "hermesc-wrapper.js" -o -name "bundle-metadata-plugin.js" 2>/dev/null | head -10

Length of output: 1495


Add bundle-hash.json to .gitignore at repository root.

The file is a runtime-generated artifact created by bundle-metadata-plugin.js (line 171: fs.writeFileSync()) and must not be committed to version control. Currently it is missing from .gitignore.

Action: Add bundle-hash.json to the root .gitignore file.

🤖 Prompt for AI Agents
In scripts/hermesc-wrapper.js around line 46, the runtime-generated file
bundle-hash.json (created by bundle-metadata-plugin.js) is not ignored; add
"bundle-hash.json" as a new entry to the repository root .gitignore file so the
generated artifact is not committed to version control.


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);
Comment on lines +62 to +87
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Make metadata appends idempotent and guard against missing/invalid hash

Repeated runs currently re-append RNUPDATE blocks and bloat the HBC. Also, slice() on an undefined hash can throw.

   try {
-    const hashData = JSON.parse(fs.readFileSync(hashFilePath, 'utf8'));
-    const { contentHash } = hashData;
+    const hashData = JSON.parse(fs.readFileSync(hashFilePath, 'utf8'));
+    const { contentHash } = hashData || {};
+    if (typeof contentHash !== 'string' || !contentHash.length) {
+      console.warn('[Hermesc Wrapper] ⚠️  Invalid contentHash in bundle-hash.json; skipping.');
+      process.exit(0);
+      return;
+    }
@@
-    const hbcBuffer = fs.readFileSync(hbcFilePath);
+    let hbcBuffer = fs.readFileSync(hbcFilePath);
@@
-    const finalBuffer = Buffer.concat([
-      hbcBuffer,
+    // Strip existing RNUPDATE footer if present (idempotent)
+    function stripRNUpdateFooter(buf, MAGIC) {
+      const m = MAGIC.length;
+      if (buf.length < (m * 2 + 4)) return { base: buf, stripped: false };
+      const endMagicStart = buf.length - m;
+      if (!buf.slice(endMagicStart).equals(MAGIC)) return { base: buf, stripped: false };
+      const lenStart = endMagicStart - 4;
+      if (lenStart < 0) return { base: buf, stripped: false };
+      const jsonLen = buf.readUInt32LE(lenStart);
+      const startMagicStart = lenStart - jsonLen - m;
+      if (startMagicStart < 0) return { base: buf, stripped: false };
+      if (!buf.slice(startMagicStart, startMagicStart + m).equals(MAGIC)) return { base: buf, stripped: false };
+      return { base: buf.slice(0, startMagicStart), stripped: true };
+    }
+    const { base: baseBuffer } = stripRNUpdateFooter(hbcBuffer, MAGIC);
+
+    const finalBuffer = Buffer.concat([
+      baseBuffer,
       MAGIC,
       jsonBuffer,
       lengthBuffer,
       MAGIC,
     ]);


console.log(`[Hermesc Wrapper] ✅ Successfully injected metadata into: ${hbcFilePath}`);
console.log(`[Hermesc Wrapper] 🧹 Cleaning up hash file...`);

process.exit(0);
Comment on lines +89 to +92
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Misleading log: no cleanup actually happens

Either remove the message or implement opt-in cleanup. Recommend removing to avoid confusion.

-    console.log(`[Hermesc Wrapper] 🧹 Cleaning up hash file...`);
🤖 Prompt for AI Agents
In scripts/hermesc-wrapper.js around lines 89 to 92, the log states "Cleaning up
hash file..." but no cleanup is performed; remove or replace this misleading
console.log and either delete the message entirely or implement an opt-in
cleanup (e.g., check a flag/env var and unlink the hash file before exiting).
Update the log to reflect the actual behavior if you choose not to implement
cleanup.

} catch (error) {
console.error(`[Hermesc Wrapper] ❌ Failed to process HBC file:`, error);
process.exit(1);
}
}
Comment on lines +45 to +97
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Race condition: parallel builds will corrupt bundle-hash.json.

If multiple hermesc processes compile in parallel (common in multi-platform or multi-architecture builds), they will all attempt to read/write the same bundle-hash.json at the project root. This can cause:

  • Read of stale or incorrect hash data
  • Writes interleaved or overwritten
  • Metadata injected into the wrong HBC file

Consider one of these solutions:

Option 1: Use per-output hash files

-  const hashFilePath = path.join(PROJECT_ROOT, 'bundle-hash.json');
+  const hashFilePath = hbcFilePath.replace(/\.hbc$/, '.hash.json');

Then update bundle-metadata-plugin.js to write hash files adjacent to each output.

Option 2: Add process locking
Use a locking library like proper-lockfile to ensure exclusive access to bundle-hash.json.

Option 3: Pass hash via environment variable
Have the plugin set an environment variable with the hash, avoiding the file altogether:

-  const hashFilePath = path.join(PROJECT_ROOT, 'bundle-hash.json');
-  if (!fs.existsSync(hashFilePath)) {
-    console.warn(`[Hermesc Wrapper] ⚠️  Hash file not found: ${hashFilePath}`);
+  const contentHash = process.env.RNUPDATE_CONTENT_HASH;
+  if (!contentHash) {
+    console.warn(`[Hermesc Wrapper] ⚠️  RNUPDATE_CONTENT_HASH not set`);
     console.warn(`[Hermesc Wrapper] Skipping metadata injection.`);
     process.exit(0);
-    return;
   }

Committable suggestion skipped: line range outside the PR's diff.