fix(native-host): resolve "Unexpected end of JSON input" on Windows
CI / Build & Test (push) Successful in 4m6s

Write all path variants for every .json in the bundle when building
for win32 (backslash, with/without leading slash) so bundle.read()
returns content regardless of runtime key normalization. Refill empty
or invalid JSON from alternate key or disk before writing variants.
Explicitly write tt-native package.json under Windows path variants
in Fix 1. Document in CHANGELOG.
This commit is contained in:
Raven Scott
2026-03-06 20:33:02 -05:00
parent 600a1d4f38
commit ea2daadac5
2 changed files with 26 additions and 3 deletions
+1
View File
@@ -24,6 +24,7 @@ All notable changes to Holesail Browser are documented here.
**Bug fixes and stability (36 issues resolved)**
- Fixed tt-native not available on Linux (SSH PTY disabled): build script now sets a host-specific addon resolution map so each platform binary loads the correct tt-native prebuild instead of the first hosts (darwin-arm64).
- Fixed tt-native not available on Windows (SSH PTY disabled): use forward-slash addon resolution key for all platforms (including win32) so the runtime finds the addon when the bundle normalizes paths; Windows prebuild is still written under both forward and backslash keys for compatibility.
- Fixed "Unexpected end of JSON input" on Windows when running the native host exe: write all path variants for every .json in the bundle (forward slash, backslash, with/without leading slash) so bundle.read() returns content regardless of how the runtime normalizes keys; refill empty or invalid JSON from disk or alternate key before writing variants.
- Fixed `ReferenceError` for undeclared `regenerated` variable in `certificate-authority.js` on Windows
- Added `hs.removeAllListeners()` in error paths of `setVirtualHost` and `startServiceTunnel` to prevent stale listener leaks
- Cleared `reconnectTimer` when replacing an existing virtual host or service tunnel entry
+25 -3
View File
@@ -210,12 +210,19 @@ function patchBundle(bundle, hosts) {
)
// Ensure tt-native package.json has content in the bundle. On Windows the runtime
// can end up reading empty content for this key (path/bundle handling), causing
// "Unexpected end of JSON input" when the .json loader runs. Explicitly write it.
// "Unexpected end of JSON input" when the .json loader runs. Explicitly write it
// under all key variants (with/without leading slash, and backslash for win32).
const pkgPath = path.join(NATIVE_HOST_DIR, 'node_modules', 'tt-native', 'package.json')
if (fs.existsSync(pkgPath)) {
const pkgContent = fs.readFileSync(pkgPath)
bundle.write(ttNativePkgKey, pkgContent)
bundle.write(ttNativePkgKey.startsWith('/') ? ttNativePkgKey.slice(1) : '/' + ttNativePkgKey, pkgContent)
if (hosts.some((h) => h.startsWith('win32'))) {
const winPkgKey = ttNativePkgKey.replace(/\//g, '\\')
bundle.write(winPkgKey, pkgContent)
bundle.write(winPkgKey.replace(/^\\+/, ''), pkgContent)
if (!winPkgKey.startsWith('\\')) bundle.write('\\' + winPkgKey, pkgContent)
}
}
console.log(' Patched tt-native binding (host-specific resolutions)')
}
@@ -284,13 +291,20 @@ module.exports = EventEmitter;
// ── Fix 3: Windows JSON empty-content and path normalization ──────────────────
// On Windows the runtime can request .json keys with backslashes or get empty
// content for some keys. Ensure every .json entry has valid content and add
// backslash variants for win32 so the runtime finds them.
// all backslash path variants for win32 so bundle.read() finds content
// regardless of how the runtime normalizes the key.
const hasWin32 = hosts.some((h) => h.startsWith('win32'))
let jsonFixed = 0
const keysToProcess = typeof bundle.keys === 'function' ? [...bundle.keys()] : Object.keys(bundle.files || {})
for (const key of keysToProcess) {
if (!key.endsWith('.json')) continue
let content = bundle.read(key)
// Try alternate key (with/without leading slash) in case empty due to key mismatch
if (!content || content.length === 0) {
const altKey = key.startsWith('/') ? key.slice(1) : '/' + key.replace(/^\/+/, '')
content = bundle.read(altKey)
if (content && content.length > 0) bundle.write(key, content)
}
const isEmpty = !content || content.length === 0
const invalidJson = content && content.length > 0 && (() => { try { JSON.parse(content.toString()); return false } catch (_) { return true } })()
if (isEmpty || invalidJson) {
@@ -303,7 +317,15 @@ module.exports = EventEmitter;
}
if (hasWin32 && content && content.length > 0) {
const winKey = key.replace(/\//g, '\\')
if (winKey !== key) bundle.write(winKey, content)
if (winKey !== key) {
bundle.write(winKey, content)
// Also write no-leading-backslash and leading-backslash variants so lookup
// finds content whether the runtime uses "node_modules\..." or "\node_modules\..."
const winKeyNoLead = winKey.replace(/^\\+/, '')
if (winKeyNoLead !== winKey) bundle.write(winKeyNoLead, content)
const winKeyWithLead = winKey.startsWith('\\') ? winKey : '\\' + winKey
if (winKeyWithLead !== winKey) bundle.write(winKeyWithLead, content)
}
}
}
if (jsonFixed > 0) console.log(` Patched ${jsonFixed} empty/invalid .json entries`)