Address vue/require-name-property offenses

Everyone can contribute. Help move this issue forward while earning points, leveling up and collecting rewards.

  • Work on this issue
  • Close this issue

❗ 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

  1. Pick a directory of files from .eslint_todo/vue-require-name-property.mjs
  2. Add the name: <NAME-OF-FILE> key/value to the Vue object in each Vue component in the directory
  3. Remove the modified files from .eslint_todo/vue-require-name-property.mjs
  4. Stage all changes
    git add .
  5. Run Prettier on all staged files
    yarn run lint:prettier:staged:fix
  6. Stage all new changes
    git add .
  7. 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 .vue files in the specified directory
  • Converts filenames to PascalCase (e.g., my-button.vue → MyButton)
  • Adds name: 'ComponentName' as the first property in the export default object
  • 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:

  1. Adds name properties to Vue files
  2. Removes processed files from .eslint_todo/vue-require-name-property.mjs
  3. Stages changes with git add .
  4. Runs Prettier on staged files
  5. Stages Prettier changes
  6. Runs ESLint to verify all fixes
  7. Provides next steps for creating the merge request

Steps

  1. Pick a directory of files from .eslint_todo/vue-require-name-property.mjs
  2. Download the script to fix the eslint rule in that directory gitlab-fix-eslint-vue-require-name-property.js
  3. Navigate to path/to/gdk/gitlab and run the script
    node path/to/gitlab-fix-eslint-vue-require-name.js ./path/to/directory
  4. 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
  5. Add a video/gif/screenshot of the affected components working as intended to the merge request

Support

Support contact: @aturinske

Edited Jan 27, 2026 by Alexander Turinske
Assignee Loading
Time tracking Loading