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:
- Master snippet syntax including tabstops, placeholders, and variables
- Create regex transformations to generate dynamic content from filenames
- Build context-aware snippets that adapt to the current file
- Understand TextMate grammar underlying VS Code snippets
- 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:
nameis highlighted—type to replace or Tab to keepvalueis highlighted- 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-profile → UserProfile
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
- React Component Snippet: Extracts PascalCase from kebab-case filename
- Test File Snippet: Auto-imports based on filename
- API Route Snippet: Generates route handlers
- File Header Snippet: Inserts date and filename
- 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.
- Open User Snippets:
Cmd+Shift+P→ “Preferences: Configure User Snippets”- Select “New Global Snippets file”
- Name it
my-snippets.code-snippets
- Create a console.log snippet:
{
"Console Log": {
"prefix": "clog",
"body": [
"console.log('$1:', $1);$0"
],
"description": "Console log with label"
}
}
- Test it:
- Create a
.jsfile - Type
clogand press Tab - Type
user→ becomesconsole.log('user:', user);
- Create a
- 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.
- 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"
}
}
- Test in any file:
- Type
header+ Tab - Notice
$TM_FILENAMEauto-filled - Notice date auto-inserted
- Type
- Arrow function snippet:
{
"Arrow Function": {
"prefix": "af",
"body": [
"const ${1:name} = (${2:params}) => {",
" $0",
"};"
],
"description": "Arrow function"
}
}
- 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.
- Understand the transformation syntax:
${VARIABLE/PATTERN/REPLACEMENT/FLAGS}
VARIABLE: Which variable to transformPATTERN: Regex to matchREPLACEMENT: What to replace withFLAGS:gfor global (all occurrences)
- 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';
- 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"
}
}
- Test it:
- Create file
product-card.tsx - Type
rfc+ Tab - Observe:
ProductCardappears in 4 places!
- Create file
Phase 4: Advanced Snippets (45 minutes)
Goal: Create production-quality snippets.
- 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
- 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!
- 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, addss) - Export:
userRoutes
Phase 5: Workspace Snippets (30 minutes)
Goal: Create team-shared snippets.
- Create workspace snippet file:
mkdir -p .vscode
touch .vscode/team-snippets.code-snippets
- 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"
}
}
- 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:
- Check
scopematches current file type - Verify JSON syntax is valid
- 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:
.ts→typescript.tsx→typescriptreact.js→javascript.jsx→javascriptreact
Interview Questions
-
“How do you enforce code consistency across a team?”
Answer: “I create workspace snippets in
.vscode/*.code-snippetsthat are version-controlled. Every developer types the same prefix and gets the same structure—proper imports, error handling, logging. This ensures 100% consistency.” -
“What’s the difference between
$1and${1:placeholder}?”Answer: “
$1is 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.” -
“How would you create a snippet that generates different code based on filename?”
Answer: “I use the
$TM_FILENAME_BASEvariable with regex transformations. For example,${TM_FILENAME_BASE/^(.)|-(.)/${1:/upcase}${2:/upcase}/g}convertsuser-profiletoUserProfile.”
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
- regex101.com - Test patterns before using in snippets
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