Skip to content
This repository was archived by the owner on May 1, 2026. It is now read-only.
Merged
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
57 changes: 54 additions & 3 deletions src/build/request.ts
Original file line number Diff line number Diff line change
Expand Up @@ -662,7 +662,8 @@ async function pollBuildStatus(
* Extract native node_modules roots that contain platform folders.
*/
interface NativeDependencies {
packages: Set<string> // Package paths like @capacitor/app
packages: Set<string> // Capacitor package paths like @capacitor/app
cordovaPackages: Set<string> // Cordova plugin package paths like onesignal-cordova-plugin
usesSPM: boolean
usesCocoaPods: boolean
}
Expand All @@ -673,6 +674,7 @@ async function extractNativeDependencies(
platformDir: string,
): Promise<NativeDependencies> {
const packages = new Set<string>()
const cordovaPackages = new Set<string>()
let usesSPM = false
let usesCocoaPods = false

Expand Down Expand Up @@ -748,9 +750,35 @@ async function extractNativeDependencies(
packages.add(packagePath)
}
}

// Parse Cordova plugin references from capacitor-cordova-android-plugins/build.gradle.
// These plugins are NOT listed in capacitor.settings.gradle. They are wired via
// `apply from: "../../node_modules/<plugin>/<file>.gradle"` lines that `cap sync`
// injects between the PLUGIN GRADLE EXTENSIONS markers. The referenced files live
// at the package root, not under an `android/` subfolder, so we must include the
// entire package contents in the upload bundle.
const cordovaBuildGradlePath = join(projectDir, platformDir, 'capacitor-cordova-android-plugins', 'build.gradle')
if (existsSync(cordovaBuildGradlePath)) {
const cordovaContent = await readFileAsync(cordovaBuildGradlePath, 'utf-8')
// Match: apply from: "../../node_modules/<pkg>/..." (any depth of ../, single or double quotes)
const applyFromMatches = cordovaContent.matchAll(/apply\s+from\s*:\s*["'](?:\.\.\/)+node_modules\/([^"']+)["']/g)
for (const match of applyFromMatches) {
let fullPath = match[1]
// Normalize pnpm paths
const lastNodeModulesIdx = fullPath.lastIndexOf('node_modules/')
if (lastNodeModulesIdx !== -1)
fullPath = fullPath.substring(lastNodeModulesIdx + 'node_modules/'.length)
// Extract package name: scoped (@scope/pkg) takes two segments, otherwise one
const segments = fullPath.split('/')
const packagePath = segments[0].startsWith('@') && segments.length >= 2
? `${segments[0]}/${segments[1]}`
: segments[0]
cordovaPackages.add(packagePath)
}
}
}

return { packages, usesSPM, usesCocoaPods }
return { packages, cordovaPackages, usesSPM, usesCocoaPods }
}

/**
Expand Down Expand Up @@ -778,6 +806,28 @@ export function shouldIncludeFile(filePath: string, platform: 'ios' | 'android',
if (platform === 'android' && normalizedPath.startsWith('node_modules/@capacitor/android/'))
return true

// Cordova plugins: include the entire package contents EXCEPT the plugin's own
// nested node_modules. Cordova plugins don't follow Capacitor's `<pkg>/android/`
// convention — supporting files like `build-extras-*.gradle` live at the package
// root, native sources may live under `src/android/`, and `plugin.xml` is at the
// root. We include all of those, but exclude any bundled transitive dependencies
// under `<pkg>/node_modules/...` to avoid pulling unrelated code (and arbitrary
// size) into the upload bundle.
if (platform === 'android') {
for (const cordovaPkg of nativeDeps.cordovaPackages) {
const cordovaPrefix = `node_modules/${cordovaPkg}/`
if (normalizedPath === `node_modules/${cordovaPkg}/package.json`)
return true
if (normalizedPath.startsWith(cordovaPrefix)) {
const subpath = normalizedPath.slice(cordovaPrefix.length)
// Reject anything inside the plugin's own bundled node_modules.
if (subpath === 'node_modules' || subpath.startsWith('node_modules/'))
continue
return true
}
}
}

// Check if file is in one of the native dependencies
for (const packagePath of nativeDeps.packages) {
const packagePrefix = `node_modules/${packagePath}/`
Expand Down Expand Up @@ -856,11 +906,12 @@ function addDirectoryToZip(
// 1. This directory itself should be included (matches a pattern)
// 2. This directory is a prefix of a dependency path (need to traverse to reach it)
const normalizedItemPath = itemZipPath.replace(/\\/g, '/')
const allPackages = [...nativeDeps.packages, ...nativeDeps.cordovaPackages]
const shouldRecurse = shouldIncludeFile(itemZipPath, platform, nativeDeps, platformDir)
// Ensure we can reach nested platform directories like projects/app/android.
|| platformDir === normalizedItemPath
|| platformDir.startsWith(`${normalizedItemPath}/`)
|| Array.from(nativeDeps.packages).some((pkg) => {
|| allPackages.some((pkg) => {
const depPath = `node_modules/${pkg}/`
return depPath.startsWith(`${normalizedItemPath}/`) || normalizedItemPath.startsWith(`node_modules/${pkg}`)
})
Expand Down
110 changes: 109 additions & 1 deletion test/test-build-zip-filter.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ function writeFile(filePath, content) {
writeFileSync(filePath, content, 'utf-8')
}

const nativeDeps = { packages: new Set(['@capacitor/app']), usesSPM: false, usesCocoaPods: true }
const nativeDeps = { packages: new Set(['@capacitor/app']), cordovaPackages: new Set(), usesSPM: false, usesCocoaPods: true }

await t('should include package metadata for @capacitor dependencies in zip filter', () => {
assert.equal(
Expand Down Expand Up @@ -302,4 +302,112 @@ await t('generated build zip includes SPM and CocoaPods metadata when both manag
rmSync(testRoot, { recursive: true, force: true })
}
})
await t('generated build zip includes Cordova plugin files referenced from capacitor-cordova-android-plugins/build.gradle', async () => {
const testRoot = mkdtempSync(join(tmpdir(), 'capgo-build-zip-filter-'))
const zipPath = join(testRoot, 'build.zip')

try {
writeFile(
join(testRoot, 'package.json'),
JSON.stringify({
dependencies: {
'@capacitor/core': '^6.0.0',
'@capacitor/android': '^6.0.0',
'onesignal-cordova-plugin': '^5.3.0',
},
}, null, 2),
)

writeFile(
join(testRoot, 'capacitor.config.json'),
JSON.stringify({
appId: 'com.example.app',
appName: 'Example',
webDir: 'www',
}, null, 2),
)

writeFile(join(testRoot, 'www', 'index.html'), '<!doctype html><html></html>')

// Capacitor settings.gradle only lists @capacitor/android — Cordova plugins are not here.
writeFile(
join(testRoot, 'android', 'capacitor.settings.gradle'),
"include ':capacitor-android'\nproject(':capacitor-android').projectDir = new File('../node_modules/@capacitor/android/capacitor')\n",
)

// Cordova plugins are wired via apply from in this generated file.
writeFile(
join(testRoot, 'android', 'capacitor-cordova-android-plugins', 'build.gradle'),
"apply from: \"cordova.variables.gradle\"\napply from: \"../../node_modules/onesignal-cordova-plugin/build-extras-onesignal.gradle\"\n",
)

writeFile(
join(testRoot, 'node_modules', '@capacitor', 'android', 'package.json'),
JSON.stringify({ name: '@capacitor/android', version: '6.0.0' }),
)

writeFile(
join(testRoot, 'node_modules', 'onesignal-cordova-plugin', 'package.json'),
JSON.stringify({ name: 'onesignal-cordova-plugin', version: '5.3.0' }),
)
writeFile(
join(testRoot, 'node_modules', 'onesignal-cordova-plugin', 'build-extras-onesignal.gradle'),
"// Onesignal extras\n",
)
writeFile(
join(testRoot, 'node_modules', 'onesignal-cordova-plugin', 'plugin.xml'),
"<plugin />\n",
)
writeFile(
join(testRoot, 'node_modules', 'onesignal-cordova-plugin', 'src', 'android', 'OneSignal.java'),
"package com.onesignal;",
)

// Simulated bundled transitive dependency that must NOT be included.
writeFile(
join(testRoot, 'node_modules', 'onesignal-cordova-plugin', 'node_modules', 'bundled-dep', 'package.json'),
JSON.stringify({ name: 'bundled-dep', version: '1.0.0' }),
)
writeFile(
join(testRoot, 'node_modules', 'onesignal-cordova-plugin', 'node_modules', 'bundled-dep', 'index.js'),
"module.exports = {}",
)

await zipDirectory(testRoot, zipPath, 'android', {
android: { path: 'android' },
})

const zip = new AdmZip(zipPath)
const entries = zip.getEntries().map(entry => entry.entryName).sort()

assert.ok(
entries.includes('node_modules/onesignal-cordova-plugin/build-extras-onesignal.gradle'),
'missing Cordova plugin gradle script referenced via apply from',
)
assert.ok(
entries.includes('node_modules/onesignal-cordova-plugin/plugin.xml'),
'missing Cordova plugin.xml at package root',
)
assert.ok(
entries.includes('node_modules/onesignal-cordova-plugin/src/android/OneSignal.java'),
'missing Cordova plugin native source under src/android',
)
assert.ok(
entries.includes('node_modules/onesignal-cordova-plugin/package.json'),
'missing Cordova plugin package.json',
)
assert.ok(
entries.includes('android/capacitor-cordova-android-plugins/build.gradle'),
'missing capacitor-cordova-android-plugins build.gradle',
)
assert.ok(
!entries.some(e => e.startsWith('node_modules/onesignal-cordova-plugin/node_modules/')),
'bundled transitive deps under cordova plugin must be excluded',
)
}
finally {
rmSync(testRoot, { recursive: true, force: true })
}
})

process.stdout.write('OK\n')
Loading