From bb2f8cb1623ac0409c8e3989473e3336c4e90f17 Mon Sep 17 00:00:00 2001 From: Nathan Walker Date: Sun, 26 Jul 2026 15:14:03 -0700 Subject: [PATCH] fix(node-api): decode percent-encoded path before process.dlopen MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `URL.pathname` is percent-encoded, but `process.dlopen` expects a filesystem path. Any space in the package's path therefore reaches dlopen as "%20" and the addon fails to load: TypeError: dlopen(/Applications/My%20App.app/.../NativeScript, 0x0001): tried: '.../My%20App.app/...' (no such file) This breaks every packaged app whose path contains a space — for example an .app installed to /Applications with a display name like "My App.app". It goes unnoticed in development because the package normally resolves from a node_modules path with no spaces, where decoding is a no-op; only a distributed bundle exercises the failing path. Wrap the resolved pathname in decodeURIComponent() at all three dlopen sites: - packages/macos-node-api/index.mjs - packages/macos-node-api/index.cjs - packages/ios-node-api/index.js Verified on macOS by copying packages/macos-node-api together with its built NativeScript.framework into a directory whose name contains a space, then importing it under Deno: - before: dlopen fails with the %20 path shown above - after: the addon loads and init() runs Note: paths containing "#" or "?" are still mishandled, because the base URL is built by string concatenation and those characters are parsed as fragment/query delimiters. Spaces are by far the common case; a complete fix would resolve the addon path without going through URL at all. Co-Authored-By: Claude Opus 5 (1M context) --- packages/ios-node-api/index.js | 6 +++++- packages/macos-node-api/index.cjs | 14 ++++++++++---- packages/macos-node-api/index.mjs | 6 +++++- 3 files changed, 20 insertions(+), 6 deletions(-) diff --git a/packages/ios-node-api/index.js b/packages/ios-node-api/index.js index c8eb06111..9ae341c4b 100644 --- a/packages/ios-node-api/index.js +++ b/packages/ios-node-api/index.js @@ -15,8 +15,12 @@ if (typeof interop === "undefined") { const module = { exports: {} }; + // `URL.pathname` is percent-encoded, but `process.dlopen` takes a filesystem path. Without + // decoding, a space in the path arrives as "%20" and the load fails with "no such file" — + // which happens for any app bundle whose path contains a space, e.g. + // /Applications/My App.app/… → /Applications/My%20App.app/… // deno-lint-ignore no-process-globals - process.dlopen(module, new URL(path, metaURL).pathname); + process.dlopen(module, decodeURIComponent(new URL(path, metaURL).pathname)); module.exports.init( // deno-lint-ignore no-process-globals diff --git a/packages/macos-node-api/index.cjs b/packages/macos-node-api/index.cjs index fcbc583b2..3104fe8fc 100644 --- a/packages/macos-node-api/index.cjs +++ b/packages/macos-node-api/index.cjs @@ -16,12 +16,18 @@ if (!isNativeScriptRuntime) { // === const module = { exports: {} }; + // `URL.pathname` is percent-encoded, but `process.dlopen` takes a filesystem path. Without + // decoding, a space in the path arrives as "%20" and the load fails with "no such file" — + // which happens for any app bundle whose path contains a space, e.g. + // /Applications/My App.app/… → /Applications/My%20App.app/… process.dlopen( module, - new URL( - "./build/RelWithDebInfo/NativeScript.apple.node/macos-arm64/NativeScript.framework/Versions/A/NativeScript", - `file://${__filename}`, - ).pathname, + decodeURIComponent( + new URL( + "./build/RelWithDebInfo/NativeScript.apple.node/macos-arm64/NativeScript.framework/Versions/A/NativeScript", + `file://${__filename}`, + ).pathname, + ), ); module.exports.init(process.env.METADATA_PATH); } else if (typeof require !== "undefined") { diff --git a/packages/macos-node-api/index.mjs b/packages/macos-node-api/index.mjs index d5a649b32..db0c197b7 100644 --- a/packages/macos-node-api/index.mjs +++ b/packages/macos-node-api/index.mjs @@ -24,8 +24,12 @@ if (!isNativeScriptRuntime) { const module = { exports: {} }; + // `URL.pathname` is percent-encoded, but `process.dlopen` takes a filesystem path. Without + // decoding, a space in the path arrives as "%20" and the load fails with "no such file" — + // which happens for any app bundle whose path contains a space, e.g. + // /Applications/My App.app/… → /Applications/My%20App.app/… // deno-lint-ignore no-process-globals - process.dlopen(module, new URL(path, metaURL).pathname); + process.dlopen(module, decodeURIComponent(new URL(path, metaURL).pathname)); module.exports.init( // deno-lint-ignore no-process-globals