{"record":{"id":"1aa00a43114eebe7","repo":"laurent22/joplin","slug":"repeated-categories-are-not-allowed","errorCode":null,"errorMessage":"Repeated categories are not allowed","messagePattern":"Repeated categories are not allowed","errorType":"validation","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/generator-joplin/generators/app/templates/webpack.config.js","lineNumber":93,"sourceCode":"}\n\nfunction currentGitInfo() {\n\ttry {\n\t\tlet branch = execSync('git rev-parse --abbrev-ref HEAD', { stdio: 'pipe' }).toString().trim();\n\t\tconst commit = execSync('git rev-parse HEAD', { stdio: 'pipe' }).toString().trim();\n\t\tif (branch === 'HEAD') branch = 'master';\n\t\treturn `${branch}:${commit}`;\n\t} catch (error) {\n\t\tconst messages = error.message ? error.message.split('\\n') : [''];\n\t\tconsole.info(chalk.cyan('Could not get git commit (not a git repo?):', messages[0].trim()));\n\t\tconsole.info(chalk.cyan('Git information will not be stored in plugin info file'));\n\t\treturn '';\n\t}\n}\n\nfunction validateCategories(categories) {\n\tif (!categories) return null;\n\tif ((categories.length !== new Set(categories).size)) throw new Error('Repeated categories are not allowed');\n\t// eslint-disable-next-line github/array-foreach -- Old code before rule was applied\n\tcategories.forEach(category => {\n\t\tif (!allPossibleCategories.map(category => { return category.name; }).includes(category)) throw new Error(`${category} is not a valid category. Please make sure that the category name is lowercase. Valid categories are: \\n${allPossibleCategories.map(category => { return category.name; })}\\n`);\n\t});\n}\n\nfunction validateScreenshots(screenshots) {\n\tif (!screenshots) return null;\n\tfor (const screenshot of screenshots) {\n\t\tif (!screenshot.src) throw new Error('You must specify a src for each screenshot');\n\n\t\t// Avoid attempting to download and verify URL screenshots.\n\t\tif (screenshot.src.startsWith('https://') || screenshot.src.startsWith('http://')) {\n\t\t\tcontinue;\n\t\t}\n\n\t\tconst screenshotType = screenshot.src.split('.').pop();\n\t\tif (!allPossibleScreenshotsType.includes(screenshotType)) throw new Error(`${screenshotType} is not a valid screenshot type. Valid types are: \\n${allPossibleScreenshotsType}\\n`);","sourceCodeStart":75,"sourceCodeEnd":111,"githubUrl":"https://github.com/laurent22/joplin/blob/2654b33620775080d1d59c552259d41e33dad3d2/packages/generator-joplin/generators/app/templates/webpack.config.js#L75-L111","documentation":"Thrown by validateCategories (in the plugin template's webpack.config.js) when the manifest.json categories array contains duplicate entries — i.e. categories.length !== new Set(categories).size. Categories classify the plugin on the Joplin plugin registry and duplicates are rejected to keep the listing clean. Validation runs at build time when readManifest parses src/manifest.json.","triggerScenarios":"manifest.json has \"categories\": [\"editor\", \"editor\"] or [\"productivity\", \"productivity\", \"tags\"]; a copy-paste when editing the manifest duplicated an entry; a script regenerated the manifest and inserted a category twice.","commonSituations":"Plugin author hand-edited manifest.json and duplicated a category; a merge conflict resolution left a doubled entry; a scaffolding/tooling bug wrote the same category twice.","solutions":["Open src/manifest.json and remove duplicate entries from the categories array.","Use a JSON-aware editor or lint the manifest to detect dupes before building.","De-duplicate programmatically: [...new Set(manifest.categories)]."],"exampleFix":"// before — src/manifest.json\n\"categories\": [\"editor\", \"productivity\", \"editor\"]\n// after\n\"categories\": [\"editor\", \"productivity\"]","handlingStrategy":"validation","validationCode":"const manifest = JSON.parse(fs.readFileSync('src/manifest.json', 'utf8'));\nif (manifest.categories) {\n  const set = new Set(manifest.categories);\n  if (set.size !== manifest.categories.length) {\n    throw new Error('Duplicate categories in manifest.json');\n  }\n}","typeGuard":"function hasUniqueCategories(cats) {\n  return Array.isArray(cats) && new Set(cats).size === cats.length;\n}","tryCatchPattern":null,"preventionTips":["De-duplicate categories before building: [...new Set(manifest.categories)].","Lint manifest.json in a pre-build step.","Avoid hand-editing the categories array under merge conflicts."],"tags":["plugin-publish","manifest","generator-joplin","config","build"],"backgroundTag":null,"analyzedSha":"2654b33620775080d1d59c552259d41e33dad3d2","analyzedAt":"2026-08-12T14:26:46.263Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}