Address vue/require-name-property offenses
Everyone can contribute. Help move this issue forward while earning points, leveling up and collecting rewards.
❗ COMMUNITY CONTRIBUTORS, PLEASE ONLY PICK UP ONE DIRECTORY OF <20 FILES AT A TIME TO KEEP MERGE REQUESTS MANAGEABLE ❗
The vue/require-name-property ESLint rule was enabled in Adds linting for Vue events formatting (!214221 - merged) and uncovered many offenses
This epic tracks the progress for addressing existing offenses in the files that are currently ignored in .eslint_todo/vue-require-name-property.mjs.
Example from the docs
<script>
/* ✓ GOOD */
export default {
name: 'OurButton'
}
</script>
<script>
/* ✗ BAD */
export default {}
</script>
Implementation
Technical Details
I asked GitLab Duo Agent to create a script for this.
The implementation plan is as follows. Create a script to add the name property to the Vue files
- Pick a directory of files from .eslint_todo/vue-require-name-property.mjs
- Add the
name: <NAME-OF-FILE>key/value to the Vue object in each Vue component in the directory - Remove the modified files from .eslint_todo/vue-require-name-property.mjs
- Stage all changes
git add . - Run Prettier on all staged files
yarn run lint:prettier:staged:fix - Stage all new changes
git add . - Run eslint to verify all files were fixed or fix any any issues
node scripts/frontend/eslint.js --no-warn-ignored --no-error-on-unmatched-pattern --format gitlab .eslint_todo/vue-require-name-property.mjs ./path/to/directory
Script to Add Vue Component Names
Here's a Node.js script that processes .vue files in a directory and adds the name property as the first key in the exported object:
Script that just updates the Vue files
#!/usr/bin/env node
const fs = require('fs');
const path = require('path');
// Convert filename to PascalCase
function toPascalCase(filename) {
return filename
.replace(/\.vue$/, '') // Remove .vue extension
.split(/[-_]/) // Split on hyphens and underscores
.map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
.join('');
}
// Process a single Vue file
function processVueFile(filePath) {
const content = fs.readFileSync(filePath, 'utf8');
const filename = path.basename(filePath);
const componentName = toPascalCase(filename);
// Match the script block and export default object
const scriptMatch = content.match(/<script[^>]*>([\s\S]*?)<\/script>/);
if (!scriptMatch) {
console.log(`⚠️ No script block found in ${filename}`);
return false;
}
const scriptContent = scriptMatch[1];
// Check if name property already exists
if (/name\s*:\s*['"`]/.test(scriptContent)) {
console.log(`✓ ${filename} already has a name property`);
return false;
}
// Match export default { ... }
const exportMatch = scriptContent.match(/export\s+default\s*\{/);
if (!exportMatch) {
console.log(`⚠️ No export default object found in ${filename}`);
return false;
}
// Insert name property after the opening brace
const newScriptContent = scriptContent.replace(
/export\s+default\s*\{/,
`export default {\n name: '${componentName}',`
);
const newContent = content.replace(
/<script[^>]*>[\s\S]*?<\/script>/,
`<script>${newScriptContent}</script>`
);
fs.writeFileSync(filePath, newContent, 'utf8');
console.log(`✓ Updated ${filename} with name: '${componentName}'`);
return true;
}
// Main function
function main() {
const targetDir = process.argv[2];
if (!targetDir) {
console.error('Usage: node add-vue-names.js <directory>');
process.exit(1);
}
if (!fs.existsSync(targetDir)) {
console.error(`Directory not found: ${targetDir}`);
process.exit(1);
}
const files = fs.readdirSync(targetDir)
.filter(file => file.endsWith('.vue'))
.map(file => path.join(targetDir, file));
if (files.length === 0) {
console.log(`No .vue files found in ${targetDir}`);
return;
}
console.log(`Processing ${files.length} Vue files...\n`);
let updated = 0;
files.forEach(file => {
if (processVueFile(file)) {
updated++;
}
});
console.log(`\n✓ Updated ${updated} files`);
}
main();
Usage:
node add-vue-names.js ./path/to/directory
The script:
- Finds all
.vuefiles in the specified directory - Converts filenames to PascalCase (e.g.,
my-button.vue→MyButton) - Adds
name: 'ComponentName'as the first property in theexport defaultobject - Skips files that already have a name property
- Logs progress for each file
After running this, follow steps 4-7 from the issue's implementation plan (git add, prettier, eslint).
Do everything in the implementation plan in one script
Here's the modified script that automates all implementation steps:
Script that does everything
#!/usr/bin/env node
const fs = require('fs');
const path = require('path');
const { execSync } = require('child_process');
// Convert filename to PascalCase
function toPascalCase(filename) {
return filename
.replace(/\.vue$/, '')
.split(/[-_]/)
.map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
.join('');
}
// Process a single Vue file
function processVueFile(filePath) {
const content = fs.readFileSync(filePath, 'utf8');
const filename = path.basename(filePath);
const componentName = toPascalCase(filename);
const scriptMatch = content.match(/<script[^>]*>([\s\S]*?)<\/script>/);
if (!scriptMatch) {
console.log(`⚠️ No script block found in ${filename}`);
return null;
}
const scriptContent = scriptMatch[1];
if (/name\s*:\s*['"`]/.test(scriptContent)) {
console.log(`✓ ${filename} already has a name property`);
return null;
}
const exportMatch = scriptContent.match(/export\s+default\s*\{/);
if (!exportMatch) {
console.log(`⚠️ No export default object found in ${filename}`);
return null;
}
const newScriptContent = scriptContent.replace(
/export\s+default\s*\{/,
`export default {\n name: '${componentName}',`
);
const newContent = content.replace(
/<script[^>]*>[\s\S]*?<\/script>/,
`<script>${newScriptContent}</script>`
);
fs.writeFileSync(filePath, newContent, 'utf8');
console.log(`✓ Updated ${filename} with name: '${componentName}'`);
return filePath;
}
// Remove files from eslint todo
function removeFromEslintTodo(updatedFiles) {
const todoPath = '.eslint_todo/vue-require-name-property.mjs';
if (!fs.existsSync(todoPath)) {
console.log(`⚠️ ${todoPath} not found`);
return;
}
let content = fs.readFileSync(todoPath, 'utf8');
updatedFiles.forEach(filePath => {
const relativePath = path.relative(process.cwd(), filePath);
// Remove the file path from the todo file (handles various quote styles)
content = content.replace(new RegExp(`['"\`]${relativePath.replace(/\\/g, '/')}['"\`],?\\s*`, 'g'), '');
});
fs.writeFileSync(todoPath, content, 'utf8');
console.log(`✓ Removed ${updatedFiles.length} files from ${todoPath}`);
}
// Execute shell command
function exec(command, description) {
try {
console.log(`\n📋 ${description}`);
console.log(` Running: ${command}`);
execSync(command, { stdio: 'inherit' });
console.log(`✓ ${description} completed`);
} catch (error) {
console.error(`✗ ${description} failed`);
throw error;
}
}
// Main function
function main() {
const targetDir = process.argv[2];
if (!targetDir) {
console.error('Usage: node fix-vue-names.js <directory>');
process.exit(1);
}
if (!fs.existsSync(targetDir)) {
console.error(`Directory not found: ${targetDir}`);
process.exit(1);
}
const files = fs.readdirSync(targetDir)
.filter(file => file.endsWith('.vue'))
.map(file => path.join(targetDir, file));
if (files.length === 0) {
console.log(`No .vue files found in ${targetDir}`);
return;
}
console.log(`\n🚀 Processing ${files.length} Vue files from ${targetDir}\n`);
// Step 1-2: Add name properties to Vue files
console.log('Step 1-2: Adding name properties to Vue components...');
const updatedFiles = files
.map(file => processVueFile(file))
.filter(file => file !== null);
if (updatedFiles.length === 0) {
console.log('No files were updated');
return;
}
console.log(`\n✓ Updated ${updatedFiles.length} files\n`);
// Step 3: Remove from eslint todo
console.log('Step 3: Removing files from .eslint_todo/vue-require-name-property.mjs...');
removeFromEslintTodo(updatedFiles);
// Step 4: Stage all changes
exec('git add .', 'Step 4: Staging all changes');
// Step 5: Run Prettier
exec('yarn run lint:prettier:staged:fix', 'Step 5: Running Prettier on staged files');
// Step 6: Stage new changes
exec('git add .', 'Step 6: Staging Prettier changes');
// Step 7: Run ESLint
exec(
`node scripts/frontend/eslint.js --no-warn-ignored --no-error-on-unmatched-pattern --format gitlab .eslint_todo/vue-require-name-property.mjs ${targetDir}`,
'Step 7: Running ESLint to verify fixes'
);
console.log('\n✅ All steps completed!');
console.log('\nNext steps:');
console.log('1. Review the changes: git diff HEAD');
console.log('2. Create a merge request with the description:');
console.log(' Related to: https://gitlab.com/gitlab-org/gitlab/-/issues/583556');
}
main();
Usage:
node fix-vue-names.js ./path/to/directory
This script now:
- Adds
nameproperties to Vue files - Removes processed files from
.eslint_todo/vue-require-name-property.mjs - Stages changes with
git add . - Runs Prettier on staged files
- Stages Prettier changes
- Runs ESLint to verify all fixes
- Provides next steps for creating the merge request
Steps
- Pick a directory of files from .eslint_todo/vue-require-name-property.mjs
- Download the script to fix the eslint rule in that directory gitlab-fix-eslint-vue-require-name-property.js
- Navigate to
path/to/gdk/gitlaband run the scriptnode path/to/gitlab-fix-eslint-vue-require-name.js ./path/to/directory - Open a merge request. For details with how to open a merge request, see Contribute to GitLab.
In the merge request description, include this text:
Related to: https://gitlab.com/gitlab-org/gitlab/-/issues/583556 - Add a video/gif/screenshot of the affected components working as intended to the merge request
Support
Support contact: @aturinske