Project 4: The Dynamic Snippet Library (Metaprogramming)

Project 4: The Dynamic Snippet Library (Metaprogramming)

Project Overview

Attribute Value
Difficulty Beginner
Time Estimate Weekend
Main Language JSON / TextMate Snippet Syntax
Knowledge Area Templating / Efficiency
Prerequisites Basic understanding of regex

Learning Objectives

By completing this project, you will:

  1. Master snippet syntax including tabstops, placeholders, and variables
  2. Create regex transformations to generate dynamic content from filenames
  3. Build context-aware snippets that adapt to the current file
  4. Understand TextMate grammar underlying VS Code snippets
  5. Create team-shared snippets for project consistency

The Core Question

“How can I code at the speed of thought without getting bogged down by syntax?”


Deep Theoretical Foundation

The Cost of Typing Boilerplate

Every time you create a React component, you type the same boilerplate:

import React from 'react';

interface MyComponentProps {
  // props
}

export const MyComponent: React.FC<MyComponentProps> = (props) => {
  return (
    <div>
      {/* content */}
    </div>
  );
};

export default MyComponent;

This takes 2 minutes and you make typos. Over 100 components, that’s 3+ hours of repetitive typing. Snippets eliminate this entirely.

TextMate Snippet Engine

VS Code uses the TextMate snippet syntax—the same engine used by Sublime Text, Atom, and other editors. Understanding this syntax gives you skills transferable across editors.

Snippet Syntax Components

1. Tabstops ($1, $2, $0)

Tabstops define cursor positions. Press Tab to jump between them.

{
  "body": [
    "function ${1:name}($2) {",
    "  $0",
    "}"
  ]
}
  • $1: First cursor position
  • $2: Second position
  • $0: Final position (where cursor ends)

2. Placeholders (${1:default})

Placeholders provide default text that’s selected when you reach the tabstop:

"body": "const ${1:name} = ${2:value};"

When triggered:

  1. name is highlighted—type to replace or Tab to keep
  2. value is highlighted
  3. Cursor ends after semicolon

3. Variables

VS Code provides dynamic content based on context:

Variable Example Value
$TM_FILENAME user-controller.ts
$TM_FILENAME_BASE user-controller
$TM_DIRECTORY /src/controllers
$CLIPBOARD (clipboard content)
$CURRENT_YEAR 2024
$CURRENT_MONTH 12
$CURRENT_DATE 27

4. Regex Transformations

The real power: ${VARIABLE/REGEX/REPLACEMENT/FLAGS}

"${TM_FILENAME_BASE/^(.)|-(.)/${1:/upcase}${2:/upcase}/g}"

This transforms user-profileUserProfile

The kebab-case to PascalCase Problem

Most projects use kebab-case for filenames (user-profile.tsx) but PascalCase for component names (UserProfile). Regex transformations solve this automatically.

Pattern: ^(.)|-(.)

  • ^(.) matches first character
  • -(.) matches any character after a dash

Replacement: ${1:/upcase}${2:/upcase}

  • Uppercase both capture groups
  • Dash is removed (not in a capture group)

Project Specification

What You’re Building

A “Smart Snippet” library that generates context-aware boilerplate:

  • React components that extract names from filenames
  • Test files that auto-import the module being tested
  • API routes with proper naming conventions
  • File headers with auto-inserted dates

Deliverables

  1. React Component Snippet: Extracts PascalCase from kebab-case filename
  2. Test File Snippet: Auto-imports based on filename
  3. API Route Snippet: Generates route handlers
  4. File Header Snippet: Inserts date and filename
  5. Workspace Snippets: Team-shared snippets in .vscode/

Success Criteria

  • Snippets trigger correctly with prefix
  • Filename transformations work (kebab → PascalCase)
  • Tabstops guide cursor through logical edit points
  • Snippets are scoped to appropriate languages
  • Team can use workspace snippets without configuration

Solution Architecture

Snippet File Locations

User Snippets (Personal):
  macOS: ~/Library/Application Support/Code/User/snippets/
  Windows: %APPDATA%\Code\User\snippets\
  Linux: ~/.config/Code/User/snippets/

Workspace Snippets (Team-shared):
  .vscode/*.code-snippets

Transformation Reference

From To Pattern Replacement
kebab-case PascalCase ^(.)\|-(.) ${1:/upcase}${2:/upcase}
kebab-case camelCase -(.) ${1:/upcase}
snake_case camelCase _(.) ${1:/upcase}
any UPPERCASE (.*) ${1:/upcase}
any lowercase (.*) ${1:/downcase}

Phased Implementation Guide

Phase 1: Your First Snippet (30 minutes)

Goal: Create a simple snippet and understand the mechanics.

  1. Open User Snippets:
    • Cmd+Shift+P → “Preferences: Configure User Snippets”
    • Select “New Global Snippets file”
    • Name it my-snippets.code-snippets
  2. Create a console.log snippet:
{
  "Console Log": {
    "prefix": "clog",
    "body": [
      "console.log('$1:', $1);$0"
    ],
    "description": "Console log with label"
  }
}
  1. Test it:
    • Create a .js file
    • Type clog and press Tab
    • Type user → becomes console.log('user:', user);
  2. Understand the components:
    • "Console Log": Snippet name (shown in IntelliSense)
    • "prefix": What you type to trigger
    • "body": The template (array = multiple lines)
    • "description": Shown in IntelliSense popup

Phase 2: Variables and Placeholders (45 minutes)

Goal: Use dynamic content and defaults.

  1. File header snippet:
{
  "File Header": {
    "prefix": "header",
    "body": [
      "/**",
      " * @file $TM_FILENAME",
      " * @author ${1:Your Name}",
      " * @created $CURRENT_YEAR-$CURRENT_MONTH-$CURRENT_DATE",
      " * @description ${2:Description}",
      " */",
      "$0"
    ],
    "description": "Insert file header comment"
  }
}
  1. Test in any file:
    • Type header + Tab
    • Notice $TM_FILENAME auto-filled
    • Notice date auto-inserted
  2. Arrow function snippet:
{
  "Arrow Function": {
    "prefix": "af",
    "body": [
      "const ${1:name} = (${2:params}) => {",
      "  $0",
      "};"
    ],
    "description": "Arrow function"
  }
}
  1. Scoped snippet (only in TypeScript):
{
  "TypeScript Interface": {
    "prefix": "int",
    "scope": "typescript,typescriptreact",
    "body": [
      "interface ${1:Name} {",
      "  $0",
      "}"
    ],
    "description": "TypeScript interface"
  }
}

Phase 3: Regex Transformations (1 hour)

Goal: Master filename-based transformations.

  1. Understand the transformation syntax:
${VARIABLE/PATTERN/REPLACEMENT/FLAGS}
  • VARIABLE: Which variable to transform
  • PATTERN: Regex to match
  • REPLACEMENT: What to replace with
  • FLAGS: g for global (all occurrences)
  1. Build the kebab-to-PascalCase transformation step by step:

Step 1: Just output the filename:

"body": "const Component = '${TM_FILENAME_BASE}';"

Result: const Component = 'user-profile';

Step 2: Match first character only:

"body": "const Component = '${TM_FILENAME_BASE/(.)/${1:/upcase}/}';"

Result: const Component = 'User-profile';

Step 3: Match dash + character:

"body": "const Component = '${TM_FILENAME_BASE/-(.)/${1:/upcase}/g}';"

Result: const Component = 'userProfile'; (removes dashes, capitalizes next char)

Step 4: Combine both patterns:

"body": "const Component = '${TM_FILENAME_BASE/^(.)|-(.)/${1:/upcase}${2:/upcase}/g}';"

Result: const Component = 'UserProfile';

  1. Create React component snippet:
{
  "React Functional Component": {
    "prefix": "rfc",
    "scope": "typescriptreact,javascriptreact",
    "body": [
      "import React from 'react';",
      "",
      "interface ${TM_FILENAME_BASE/^(.)|-(.)/${1:/upcase}${2:/upcase}/g}Props {",
      "  $1",
      "}",
      "",
      "export const ${TM_FILENAME_BASE/^(.)|-(.)/${1:/upcase}${2:/upcase}/g}: React.FC<${TM_FILENAME_BASE/^(.)|-(.)/${1:/upcase}${2:/upcase}/g}Props> = (props) => {",
      "  return (",
      "    <div className=\"${TM_FILENAME_BASE}\">",
      "      $2",
      "    </div>",
      "  );",
      "};",
      "",
      "export default ${TM_FILENAME_BASE/^(.)|-(.)/${1:/upcase}${2:/upcase}/g};$0"
    ],
    "description": "React functional component with props interface"
  }
}
  1. Test it:
    • Create file product-card.tsx
    • Type rfc + Tab
    • Observe: ProductCard appears in 4 places!

Phase 4: Advanced Snippets (45 minutes)

Goal: Create production-quality snippets.

  1. Test file snippet (auto-imports the file being tested):
{
  "Jest Test Suite": {
    "prefix": "test-suite",
    "scope": "typescript,typescriptreact",
    "body": [
      "import { describe, it, expect } from 'vitest';",
      "import { ${TM_FILENAME_BASE/.test|.spec//} } from './${TM_FILENAME_BASE/.test|.spec//}';",
      "",
      "describe('${TM_FILENAME_BASE/.test|.spec//}', () => {",
      "  it('${1:should}', () => {",
      "    $2",
      "    expect($3).toBe($4);",
      "  });",
      "});$0"
    ],
    "description": "Jest/Vitest test suite"
  }
}

In file auth-service.test.ts:

  • Imports from auth-service
  • Describe block for auth-service
  1. Choice elements (dropdown options):
{
  "HTTP Method Handler": {
    "prefix": "route",
    "scope": "typescript",
    "body": [
      "router.${1|get,post,put,patch,delete|}('/${2:path}', async (req, res) => {",
      "  try {",
      "    $3",
      "    res.status(${4|200,201,204,400,404,500|}).json({ $5 });",
      "  } catch (error) {",
      "    res.status(500).json({ error: error.message });",
      "  }",
      "});$0"
    ],
    "description": "Express route handler"
  }
}

When triggered, VS Code shows dropdowns for method and status code!

  1. Multiple transformations in one snippet:
{
  "Express Routes File": {
    "prefix": "routes-file",
    "scope": "typescript",
    "body": [
      "import { Router, Request, Response } from 'express';",
      "",
      "const ${TM_FILENAME_BASE/-(.)/${1:/upcase}/g} = Router();",
      "",
      "/**",
      " * GET /${TM_FILENAME_BASE/-routes//}s",
      " * List all ${TM_FILENAME_BASE/-routes//}s",
      " */",
      "${TM_FILENAME_BASE/-(.)/${1:/upcase}/g}.get('/', async (req: Request, res: Response) => {",
      "  $1",
      "});",
      "",
      "export default ${TM_FILENAME_BASE/-(.)/${1:/upcase}/g};$0"
    ],
    "description": "Express routes file template"
  }
}

In file user-routes.ts:

  • Variable: userRoutes (camelCase)
  • Endpoint: /users (removes -routes, adds s)
  • Export: userRoutes

Phase 5: Workspace Snippets (30 minutes)

Goal: Create team-shared snippets.

  1. Create workspace snippet file:
mkdir -p .vscode
touch .vscode/team-snippets.code-snippets
  1. Add team-specific patterns:
{
  "Company API Handler": {
    "prefix": "api",
    "scope": "typescript",
    "body": [
      "import { Request, Response, NextFunction } from 'express';",
      "import { ApiError } from '@company/errors';",
      "import { logger } from '@company/logger';",
      "",
      "/**",
      " * ${1:Description}",
      " */",
      "export const ${TM_FILENAME_BASE/-(.)/${1:/upcase}/g} = async (",
      "  req: Request,",
      "  res: Response,",
      "  next: NextFunction",
      ") => {",
      "  try {",
      "    logger.info('${TM_FILENAME_BASE/-(.)/${1:/upcase}/g} called', { userId: req.user?.id });",
      "    $2",
      "    res.status(200).json({ success: true, data: $3 });",
      "  } catch (error) {",
      "    next(new ApiError(500, error.message));",
      "  }",
      "};$0"
    ],
    "description": "Company standard API handler"
  }
}
  1. Commit to version control:
git add .vscode/team-snippets.code-snippets
git commit -m "Add team snippet standards"

All team members now have access to these snippets automatically!


Complete Snippet Collection

Here’s a complete set of production-ready snippets:

{
  "Console Log with Label": {
    "prefix": "clog",
    "body": "console.log('$1:', $1);$0",
    "description": "Console log with label"
  },

  "File Header": {
    "prefix": "header",
    "body": [
      "/**",
      " * @file $TM_FILENAME",
      " * @author ${1:Author}",
      " * @created $CURRENT_YEAR-$CURRENT_MONTH-$CURRENT_DATE",
      " * @description ${2:Description}",
      " */",
      "$0"
    ]
  },

  "React Component": {
    "prefix": "rfc",
    "scope": "typescriptreact,javascriptreact",
    "body": [
      "import React from 'react';",
      "",
      "interface ${TM_FILENAME_BASE/^(.)|-(.)/${1:/upcase}${2:/upcase}/g}Props {",
      "  $1",
      "}",
      "",
      "export const ${TM_FILENAME_BASE/^(.)|-(.)/${1:/upcase}${2:/upcase}/g}: React.FC<${TM_FILENAME_BASE/^(.)|-(.)/${1:/upcase}${2:/upcase}/g}Props> = ({ $2 }) => {",
      "  return (",
      "    <div className=\"${TM_FILENAME_BASE}\">",
      "      $0",
      "    </div>",
      "  );",
      "};",
      "",
      "export default ${TM_FILENAME_BASE/^(.)|-(.)/${1:/upcase}${2:/upcase}/g};"
    ]
  },

  "Test Suite": {
    "prefix": "test",
    "scope": "typescript,typescriptreact",
    "body": [
      "import { describe, it, expect } from 'vitest';",
      "",
      "describe('${1:subject}', () => {",
      "  it('${2:should}', () => {",
      "    $0",
      "  });",
      "});"
    ]
  },

  "Try-Catch": {
    "prefix": "tc",
    "body": [
      "try {",
      "  $1",
      "} catch (error) {",
      "  console.error('${2:Error}:', error);",
      "  $0",
      "}"
    ]
  },

  "Async Function": {
    "prefix": "afn",
    "body": [
      "async function ${1:name}($2): Promise<${3:void}> {",
      "  $0",
      "}"
    ]
  }
}

Testing Strategy

Verification Checklist

  • Each snippet triggers correctly with its prefix
  • Tabstops move in logical order
  • Filename transformations produce correct output
  • Scoped snippets only appear in correct file types
  • Workspace snippets appear for all team members

Test Matrix

Filename Expected Component Name
user-profile.tsx UserProfile
auth-service.ts AuthService
product-card-list.tsx ProductCardList
api.ts Api

Common Pitfalls and Debugging

Pitfall 1: JSON Escape Sequences

Problem: Regex backslashes cause JSON errors.

Solution: Double-escape backslashes:

  • Wrong: \d+
  • Right: \\d+

Pitfall 2: Snippet Doesn’t Appear

Problem: Type prefix but no IntelliSense suggestion.

Solutions:

  1. Check scope matches current file type
  2. Verify JSON syntax is valid
  3. Reload VS Code (Developer: Reload Window)

Pitfall 3: Transformation Returns Empty

Problem: ${TM_FILENAME_BASE/pattern/replacement/g} returns nothing.

Solution: Test regex at regex101.com first. Ensure pattern matches.

Pitfall 4: Wrong File Type Language ID

Problem: Snippet scoped to "typescript" doesn’t appear in .ts files.

Solution: Check language identifier:

  • .tstypescript
  • .tsxtypescriptreact
  • .jsjavascript
  • .jsxjavascriptreact

Interview Questions

  1. “How do you enforce code consistency across a team?”

    Answer: “I create workspace snippets in .vscode/*.code-snippets that are version-controlled. Every developer types the same prefix and gets the same structure—proper imports, error handling, logging. This ensures 100% consistency.”

  2. “What’s the difference between $1 and ${1:placeholder}?”

    Answer: “$1 is just a tabstop—cursor goes there, you type from scratch. ${1:placeholder} provides a default that’s selected. Type to replace or Tab to keep it.”

  3. “How would you create a snippet that generates different code based on filename?”

    Answer: “I use the $TM_FILENAME_BASE variable with regex transformations. For example, ${TM_FILENAME_BASE/^(.)|-(.)/${1:/upcase}${2:/upcase}/g} converts user-profile to UserProfile.”


Resources

Essential Reading

Resource Topic
Mastering Regular Expressions (Friedl) Regex patterns for transformations
Visual Studio Code Distilled (Del Sole) VS Code snippet syntax
VS Code Snippets Documentation Official reference

Regex Testing


Self-Assessment Checklist

  • I can create snippets with tabstops and placeholders
  • I understand variable substitution ($TM_FILENAME, etc.)
  • I can write regex transformations for naming conventions
  • I can scope snippets to specific languages
  • I have created workspace snippets for team sharing
  • I understand the difference between user and workspace snippets

Previous: P03-one-touch-automation-task-runner.md Next: P05-works-on-my-machine-killer.md