Skip to content
Closed
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
33 changes: 33 additions & 0 deletions packages/cli-v3/src/build/externals.ts
Original file line number Diff line number Diff line change
Expand Up @@ -402,6 +402,14 @@ function createExternalsCollector(
return markExternal("binding.gyp exists");
}

// Check if the package distributes platform-specific native binaries
// via optionalDependencies (common pattern for Rust/napi-rs packages).
// These packages use createRequire(import.meta.url).resolve() at runtime
// to locate the correct platform binary, which breaks when bundled.
if (hasPlatformSpecificOptionalDeps(packageJson)) {
return markExternal("has platform-specific optionalDependencies");
}

// Cache the negative result
isExternalCache.set(packageRoot, false);

Expand Down Expand Up @@ -655,3 +663,28 @@ async function findNearestPackageJson(
cache.set(baseDir, packageJsonPath);
return packageJsonPath;
}

// Matches platform/arch identifiers commonly found in native binary package names
// e.g. @secure-exec/v8-darwin-arm64, @rollup/rollup-linux-x64-gnu, esbuild-windows-64
const platformPattern =
/[-.](darwin|linux|win32|windows|freebsd|android|macos|sunos|openbsd|aix)/i;
Comment on lines +669 to +670
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🚩 Regex does not match @scope/platform-arch naming convention (e.g. @esbuild/*)

The platformPattern regex at packages/cli-v3/src/build/externals.ts:670 uses [-.](darwin|linux|...) which requires a dot or hyphen before the platform name. This means scoped packages where the platform name immediately follows the / separator — like @esbuild/linux-x64, @esbuild/darwin-arm64 — will NOT match. I verified this by testing the regex against both naming conventions:

  • @rollup/rollup-linux-x64-gnu → ✅ matches (-linux)
  • @swc/core-darwin-arm64 → ✅ matches (-darwin)
  • @esbuild/linux-x64 → ❌ no match (/linux)
  • @esbuild/darwin-arm64 → ❌ no match (/darwin)

This is likely acceptable because: (1) the PR specifically targets napi-rs/Rust packages which use the name-platform-arch convention, (2) the @scope/platform-arch convention is primarily used by esbuild which is a build tool rarely needed at task runtime, and (3) adding / to the character class [-./] could increase false positive risk for scoped packages. However, if a user's task code depends on esbuild at runtime, it won't be auto-detected as external.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +669 to +670
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Expand delimiter matching in platformPattern to avoid false negatives.

Line 670 only matches platform tokens preceded by - or ., so names like @esbuild/linux-x64 won’t match and can slip past auto-external detection.

💡 Proposed fix
-const platformPattern =
-  /[-.](darwin|linux|win32|windows|freebsd|android|macos|sunos|openbsd|aix)/i;
+const platformPattern =
+  /(?:^|[-./_])(darwin|linux|win32|windows|freebsd|android|macos|sunos|openbsd|aix)(?:$|[-./_])/i;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/cli-v3/src/build/externals.ts` around lines 669 - 670, The
platformPattern regex only matches when platform tokens are preceded by '-' or
'.', causing cases like '@esbuild/linux-x64' to be missed; update the const
platformPattern to accept start-of-string or additional delimiters such as '/'
and '@' (e.g., allow ^ or characters like / and @ before the (darwin|linux|...)
group) while keeping the i flag so names like `@esbuild/linux-x64` or linux-x64
will be detected by the auto-external logic.


function hasPlatformSpecificOptionalDeps(packageJson: Record<string, unknown>): boolean {
const optionalDeps = packageJson.optionalDependencies;

if (!optionalDeps || typeof optionalDeps !== "object") {
return false;
}

const depNames = Object.keys(optionalDeps);

if (depNames.length === 0) {
return false;
}

// If a significant portion of optionalDependencies match platform patterns,
// this is likely a native binary distributor package
const platformDeps = depNames.filter((name) => platformPattern.test(name));

return platformDeps.length >= 2;
}
Loading