diff --git a/src/build/request.ts b/src/build/request.ts index c4986d33..1a1f401b 100644 --- a/src/build/request.ts +++ b/src/build/request.ts @@ -662,7 +662,8 @@ async function pollBuildStatus( * Extract native node_modules roots that contain platform folders. */ interface NativeDependencies { - packages: Set // Package paths like @capacitor/app + packages: Set // Capacitor package paths like @capacitor/app + cordovaPackages: Set // Cordova plugin package paths like onesignal-cordova-plugin usesSPM: boolean usesCocoaPods: boolean } @@ -673,6 +674,7 @@ async function extractNativeDependencies( platformDir: string, ): Promise { const packages = new Set() + const cordovaPackages = new Set() let usesSPM = false let usesCocoaPods = false @@ -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//.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//..." (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 } } /** @@ -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 `/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 `/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}/` @@ -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}`) }) diff --git a/test/test-build-zip-filter.mjs b/test/test-build-zip-filter.mjs index 611d20c7..8dcc556c 100644 --- a/test/test-build-zip-filter.mjs +++ b/test/test-build-zip-filter.mjs @@ -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( @@ -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'), '') + + // 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'), + "\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')