{"record":{"id":"61654b6ba9f62ea0","repo":"apache/cordova-android","slug":"required-attribute-target-dir-not-specified-in","errorCode":null,"errorMessage":"Required attribute \"target-dir\" not specified in <source-file> element from plugin: ${plugin.id}","messagePattern":"Required attribute \"target-dir\" not specified in <source-file> element from plugin: (.+?)","errorType":"validation","errorClass":"CordovaError","httpStatus":null,"severity":"error","filePath":"lib/pluginHandlers.js","lineNumber":30,"sourceCode":"    Unless required by applicable law or agreed to in writing,\n    software distributed under the License is distributed on an\n    \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n    KIND, either express or implied.  See the License for the\n    specific language governing permissions and limitations\n    under the License.\n*/\n\nconst fs = require('node:fs');\nconst path = require('node:path');\nconst isPathInside = require('is-path-inside');\nconst events = require('cordova-common').events;\nconst CordovaError = require('cordova-common').CordovaError;\n\nconst handlers = {\n    'source-file': {\n        install: function (obj, plugin, project, options) {\n            if (!obj.src) throw new CordovaError(generateAttributeError('src', 'source-file', plugin.id));\n            if (!obj.targetDir) throw new CordovaError(generateAttributeError('target-dir', 'source-file', plugin.id));\n\n            const dest = getInstallDestination(obj);\n\n            if (options && options.force) {\n                copyFile(plugin.dir, obj.src, project.projectDir, dest, !!(options && options.link));\n            } else {\n                copyNewFile(plugin.dir, obj.src, project.projectDir, dest, !!(options && options.link));\n            }\n        },\n        uninstall: function (obj, plugin, project, options) {\n            const dest = getInstallDestination(obj);\n\n            // TODO: Add Koltin extension to uninstall, since they are handled like Java files\n            if (obj.src.endsWith('java')) {\n                deleteJava(project.projectDir, dest);\n            } else {\n                // Just remove the file, not the whole parent directory\n                removeFileF(path.resolve(project.projectDir, dest));","sourceCodeStart":12,"sourceCodeEnd":48,"githubUrl":"https://github.com/apache/cordova-android/blob/7c1e190064e349ffa4bbc6ac37b77cd773e4dbd3/lib/pluginHandlers.js#L12-L48","documentation":"The second validation in the 'source-file' installer: after src is present, the element must also declare target-dir, which decides the destination path inside the Android project (getInstallDestination builds it from target-dir). Thrown during plugin install when target-dir is missing or empty.","triggerScenarios":"`cordova plugin add <plugin>` where plugin.xml has <source-file src=\"...\"/> with no target-dir attribute - the src check passes, then this one fires.","commonSituations":"Plugin authors assuming a default destination exists (there is none - the copy needs an explicit target); partial migration of an old plugin.xml; elements copied from <resource-file> which does not require target-dir.","solutions":["Add target-dir naming the destination under the Android project: <source-file src=\"src/android/Foo.java\" target-dir=\"src/com/example/foo\" />.","Use the conventional location matching the Java package (src/<package path>) so the sources compile.","If the file is a resource rather than compiled source, move it to a <resource-file> element targeting a res/ or assets/ directory instead.","Patch or pin the offending third-party plugin."],"exampleFix":"<!-- before -->\n<source-file src=\"src/android/Foo.java\" />\n\n<!-- after -->\n<source-file src=\"src/android/Foo.java\" target-dir=\"src/com/example/foo\" />","handlingStrategy":"validation","validationCode":"const et = require('elementtree');\nconst pluginXml = et.parse(fs.readFileSync('plugin.xml', 'utf8').toString());\nfor (const el of pluginXml.getroot().iter('source-file')) {\n  if (!el.attrib['target-dir']) {\n    throw new Error(`<source-file src=\"${el.attrib.src}\"> is missing target-dir=\"...\"`);\n  }\n}","typeGuard":"const isValidSourceFileElement = (el) =>\n  Boolean(el && el.tag === 'source-file' && el.attrib && el.attrib.src && el.attrib['target-dir']);","tryCatchPattern":"try {\n  await api.addPlugin(pluginInfo);\n} catch (e) {\n  if (/Required attribute \"target-dir\" not specified in <source-file>/.test(e.message)) {\n    // add target-dir matching the Java package path, e.g. src/com/example, then retry\n  }\n  throw e;\n}","preventionTips":["Always pair src with target-dir on <source-file>; use the Java package path (src/<package>) as the destination.","Use <resource-file> for non-compiled assets so target-dir is not required.","Lint plugin.xml for required attributes in the plugin's CI."],"tags":["plugin-xml","source-file","plugin-install","validation"],"backgroundTag":"plugin-manifest-missing-attribute","analyzedSha":"7c1e190064e349ffa4bbc6ac37b77cd773e4dbd3","analyzedAt":"2026-08-22T04:57:58.868Z","schemaVersion":2},"datasetVersion":"2026-08-22T09:17:25.309Z"}